diff options
Diffstat (limited to 'lib')
146 files changed, 4280 insertions, 2295 deletions
diff --git a/lib/application-service/definitions/location.cpp b/lib/application-service/definitions/location.cpp index 958a55bf..a53db8d7 100644 --- a/lib/application-service/definitions/location.cpp +++ b/lib/application-service/definitions/location.cpp @@ -6,25 +6,19 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const ThirdPartyLocation& pod) +void JsonObjectConverter<ThirdPartyLocation>::dumpTo( + QJsonObject& jo, const ThirdPartyLocation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("alias"), pod.alias); addParam<>(jo, QStringLiteral("protocol"), pod.protocol); addParam<>(jo, QStringLiteral("fields"), pod.fields); - return jo; } -ThirdPartyLocation FromJsonObject<ThirdPartyLocation>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<ThirdPartyLocation>::fillFrom( + const QJsonObject& jo, ThirdPartyLocation& result) { - ThirdPartyLocation result; - result.alias = - fromJson<QString>(jo.value("alias"_ls)); - result.protocol = - fromJson<QString>(jo.value("protocol"_ls)); - result.fields = - fromJson<QJsonObject>(jo.value("fields"_ls)); - - return result; + fromJson(jo.value("alias"_ls), result.alias); + fromJson(jo.value("protocol"_ls), result.protocol); + fromJson(jo.value("fields"_ls), result.fields); } diff --git a/lib/application-service/definitions/location.h b/lib/application-service/definitions/location.h index 89b48a43..5586cfc6 100644 --- a/lib/application-service/definitions/location.h +++ b/lib/application-service/definitions/location.h @@ -21,12 +21,10 @@ namespace QMatrixClient /// Information used to identify this third party location. QJsonObject fields; }; - - QJsonObject toJson(const ThirdPartyLocation& pod); - - template <> struct FromJsonObject<ThirdPartyLocation> + template <> struct JsonObjectConverter<ThirdPartyLocation> { - ThirdPartyLocation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const ThirdPartyLocation& pod); + static void fillFrom(const QJsonObject& jo, ThirdPartyLocation& pod); }; } // namespace QMatrixClient diff --git a/lib/application-service/definitions/protocol.cpp b/lib/application-service/definitions/protocol.cpp index 04bb7dfc..2a62b15d 100644 --- a/lib/application-service/definitions/protocol.cpp +++ b/lib/application-service/definitions/protocol.cpp @@ -6,75 +6,55 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const FieldType& pod) +void JsonObjectConverter<FieldType>::dumpTo( + QJsonObject& jo, const FieldType& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("regexp"), pod.regexp); addParam<>(jo, QStringLiteral("placeholder"), pod.placeholder); - return jo; } -FieldType FromJsonObject<FieldType>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<FieldType>::fillFrom( + const QJsonObject& jo, FieldType& result) { - FieldType result; - result.regexp = - fromJson<QString>(jo.value("regexp"_ls)); - result.placeholder = - fromJson<QString>(jo.value("placeholder"_ls)); - - return result; + fromJson(jo.value("regexp"_ls), result.regexp); + fromJson(jo.value("placeholder"_ls), result.placeholder); } -QJsonObject QMatrixClient::toJson(const ProtocolInstance& pod) +void JsonObjectConverter<ProtocolInstance>::dumpTo( + QJsonObject& jo, const ProtocolInstance& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("desc"), pod.desc); addParam<IfNotEmpty>(jo, QStringLiteral("icon"), pod.icon); addParam<>(jo, QStringLiteral("fields"), pod.fields); addParam<>(jo, QStringLiteral("network_id"), pod.networkId); - return jo; } -ProtocolInstance FromJsonObject<ProtocolInstance>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<ProtocolInstance>::fillFrom( + const QJsonObject& jo, ProtocolInstance& result) { - ProtocolInstance result; - result.desc = - fromJson<QString>(jo.value("desc"_ls)); - result.icon = - fromJson<QString>(jo.value("icon"_ls)); - result.fields = - fromJson<QJsonObject>(jo.value("fields"_ls)); - result.networkId = - fromJson<QString>(jo.value("network_id"_ls)); - - return result; + fromJson(jo.value("desc"_ls), result.desc); + fromJson(jo.value("icon"_ls), result.icon); + fromJson(jo.value("fields"_ls), result.fields); + fromJson(jo.value("network_id"_ls), result.networkId); } -QJsonObject QMatrixClient::toJson(const ThirdPartyProtocol& pod) +void JsonObjectConverter<ThirdPartyProtocol>::dumpTo( + QJsonObject& jo, const ThirdPartyProtocol& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("user_fields"), pod.userFields); addParam<>(jo, QStringLiteral("location_fields"), pod.locationFields); addParam<>(jo, QStringLiteral("icon"), pod.icon); addParam<>(jo, QStringLiteral("field_types"), pod.fieldTypes); addParam<>(jo, QStringLiteral("instances"), pod.instances); - return jo; } -ThirdPartyProtocol FromJsonObject<ThirdPartyProtocol>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<ThirdPartyProtocol>::fillFrom( + const QJsonObject& jo, ThirdPartyProtocol& result) { - ThirdPartyProtocol result; - result.userFields = - fromJson<QStringList>(jo.value("user_fields"_ls)); - result.locationFields = - fromJson<QStringList>(jo.value("location_fields"_ls)); - result.icon = - fromJson<QString>(jo.value("icon"_ls)); - result.fieldTypes = - fromJson<QHash<QString, FieldType>>(jo.value("field_types"_ls)); - result.instances = - fromJson<QVector<ProtocolInstance>>(jo.value("instances"_ls)); - - return result; + fromJson(jo.value("user_fields"_ls), result.userFields); + fromJson(jo.value("location_fields"_ls), result.locationFields); + fromJson(jo.value("icon"_ls), result.icon); + fromJson(jo.value("field_types"_ls), result.fieldTypes); + fromJson(jo.value("instances"_ls), result.instances); } diff --git a/lib/application-service/definitions/protocol.h b/lib/application-service/definitions/protocol.h index 2aca7d66..0a1f9a21 100644 --- a/lib/application-service/definitions/protocol.h +++ b/lib/application-service/definitions/protocol.h @@ -25,12 +25,10 @@ namespace QMatrixClient /// An placeholder serving as a valid example of the field value. QString placeholder; }; - - QJsonObject toJson(const FieldType& pod); - - template <> struct FromJsonObject<FieldType> + template <> struct JsonObjectConverter<FieldType> { - FieldType operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const FieldType& pod); + static void fillFrom(const QJsonObject& jo, FieldType& pod); }; struct ProtocolInstance @@ -45,12 +43,10 @@ namespace QMatrixClient /// A unique identifier across all instances. QString networkId; }; - - QJsonObject toJson(const ProtocolInstance& pod); - - template <> struct FromJsonObject<ProtocolInstance> + template <> struct JsonObjectConverter<ProtocolInstance> { - ProtocolInstance operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const ProtocolInstance& pod); + static void fillFrom(const QJsonObject& jo, ProtocolInstance& pod); }; struct ThirdPartyProtocol @@ -78,12 +74,10 @@ namespace QMatrixClient /// same application service. QVector<ProtocolInstance> instances; }; - - QJsonObject toJson(const ThirdPartyProtocol& pod); - - template <> struct FromJsonObject<ThirdPartyProtocol> + template <> struct JsonObjectConverter<ThirdPartyProtocol> { - ThirdPartyProtocol operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const ThirdPartyProtocol& pod); + static void fillFrom(const QJsonObject& jo, ThirdPartyProtocol& pod); }; } // namespace QMatrixClient diff --git a/lib/application-service/definitions/user.cpp b/lib/application-service/definitions/user.cpp index ca334236..8ba92321 100644 --- a/lib/application-service/definitions/user.cpp +++ b/lib/application-service/definitions/user.cpp @@ -6,25 +6,19 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const ThirdPartyUser& pod) +void JsonObjectConverter<ThirdPartyUser>::dumpTo( + QJsonObject& jo, const ThirdPartyUser& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("userid"), pod.userid); addParam<>(jo, QStringLiteral("protocol"), pod.protocol); addParam<>(jo, QStringLiteral("fields"), pod.fields); - return jo; } -ThirdPartyUser FromJsonObject<ThirdPartyUser>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<ThirdPartyUser>::fillFrom( + const QJsonObject& jo, ThirdPartyUser& result) { - ThirdPartyUser result; - result.userid = - fromJson<QString>(jo.value("userid"_ls)); - result.protocol = - fromJson<QString>(jo.value("protocol"_ls)); - result.fields = - fromJson<QJsonObject>(jo.value("fields"_ls)); - - return result; + fromJson(jo.value("userid"_ls), result.userid); + fromJson(jo.value("protocol"_ls), result.protocol); + fromJson(jo.value("fields"_ls), result.fields); } diff --git a/lib/application-service/definitions/user.h b/lib/application-service/definitions/user.h index 79ca7789..062d2cac 100644 --- a/lib/application-service/definitions/user.h +++ b/lib/application-service/definitions/user.h @@ -21,12 +21,10 @@ namespace QMatrixClient /// Information used to identify this third party location. QJsonObject fields; }; - - QJsonObject toJson(const ThirdPartyUser& pod); - - template <> struct FromJsonObject<ThirdPartyUser> + template <> struct JsonObjectConverter<ThirdPartyUser> { - ThirdPartyUser operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const ThirdPartyUser& pod); + static void fillFrom(const QJsonObject& jo, ThirdPartyUser& pod); }; } // namespace QMatrixClient diff --git a/lib/avatar.cpp b/lib/avatar.cpp index b8e1096d..9279ef9d 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -190,18 +190,8 @@ bool Avatar::Private::checkUrl(const QUrl& url) const return _imageSource != Banned; } -QString cacheLocation() { - const auto cachePath = - QStandardPaths::writableLocation(QStandardPaths::CacheLocation) - + "/avatar/"; - QDir dir; - if (!dir.exists(cachePath)) - dir.mkpath(cachePath); - return cachePath; -} - QString Avatar::Private::localFile() const { - static const auto cachePath = cacheLocation(); + static const auto cachePath = cacheLocation(QStringLiteral("avatars")); return cachePath % _url.authority() % '_' % _url.fileName() % ".png"; } diff --git a/lib/connection.cpp b/lib/connection.cpp index 6bdedf26..ac69228b 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -24,6 +24,7 @@ #include "room.h" #include "settings.h" #include "csapi/login.h" +#include "csapi/capabilities.h" #include "csapi/logout.h" #include "csapi/receipts.h" #include "csapi/leaving.h" @@ -39,11 +40,11 @@ #include <QtNetwork/QDnsLookup> #include <QtCore/QFile> #include <QtCore/QDir> -#include <QtCore/QFileInfo> #include <QtCore/QStandardPaths> #include <QtCore/QStringBuilder> #include <QtCore/QElapsedTimer> #include <QtCore/QRegularExpression> +#include <QtCore/QMimeDatabase> #include <QtCore/QCoreApplication> using namespace QMatrixClient; @@ -65,10 +66,6 @@ HashT erase_if(HashT& hashMap, Pred pred) return removals; } -#ifndef TRIM_RAW_DATA -#define TRIM_RAW_DATA 65535 -#endif - class Connection::Private { public: @@ -76,8 +73,7 @@ class Connection::Private : data(move(connection)) { } Q_DISABLE_COPY(Private) - Private(Private&&) = delete; - Private operator=(Private&&) = delete; + DISABLE_MOVE(Private) Connection* q = nullptr; std::unique_ptr<ConnectionData> data; @@ -86,24 +82,34 @@ class Connection::Private // separately so we should, e.g., keep objects for Invite and // Leave state of the same room. QHash<QPair<QString, bool>, Room*> roomMap; + // Mapping from aliases to room ids, as per the last sync + QHash<QString, QString> roomAliasMap; QVector<QString> roomIdsToForget; QVector<Room*> firstTimeRooms; + QVector<QString> pendingStateRoomIds; QMap<QString, User*> userMap; DirectChatsMap directChats; DirectChatUsersMap directChatUsers; + // The below two variables track local changes between sync completions. + // See also: https://github.com/QMatrixClient/libqmatrixclient/wiki/Handling-direct-chat-events + DirectChatsMap dcLocalAdditions; + DirectChatsMap dcLocalRemovals; std::unordered_map<QString, EventPtr> accountData; QString userId; + int syncLoopTimeout = -1; + + GetCapabilitiesJob* capabilitiesJob = nullptr; + GetCapabilitiesJob::Capabilities capabilities; SyncJob* syncJob = nullptr; bool cacheState = true; bool cacheToBinary = SettingsGroup("libqmatrixclient") .value("cache_type").toString() != "json"; + bool lazyLoading = false; void connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId); - void broadcastDirectChatUpdates(const DirectChatsMap& additions, - const DirectChatsMap& removals); template <typename EventT> EventT* unpackAccountData() const @@ -228,11 +234,15 @@ void Connection::doConnectToServer(const QString& user, const QString& password, }); connect(loginJob, &BaseJob::failure, this, [this, loginJob] { - emit loginError(loginJob->errorString(), - loginJob->rawData(TRIM_RAW_DATA)); + emit loginError(loginJob->errorString(), loginJob->rawDataSample()); }); } +void Connection::syncLoopIteration() +{ + sync(d->syncLoopTimeout); +} + void Connection::connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId) @@ -241,6 +251,38 @@ void Connection::connectWithToken(const QString& userId, [=] { d->connectWithToken(userId, accessToken, deviceId); }); } +void Connection::reloadCapabilities() +{ + d->capabilitiesJob = callApi<GetCapabilitiesJob>(BackgroundRequest); + connect(d->capabilitiesJob, &BaseJob::finished, this, [this] { + if (d->capabilitiesJob->error() == BaseJob::Success) + d->capabilities = d->capabilitiesJob->capabilities(); + else if (d->capabilitiesJob->error() == BaseJob::IncorrectRequestError) + qCDebug(MAIN) << "Server doesn't support /capabilities"; + + if (d->capabilities.roomVersions.omitted()) + { + qCWarning(MAIN) << "Pinning supported room version to 1"; + d->capabilities.roomVersions = { "1", {{ "1", "stable" }} }; + } else { + qCDebug(MAIN) << "Room versions:" + << defaultRoomVersion() << "is default, full list:" + << availableRoomVersions(); + } + Q_ASSERT(!d->capabilities.roomVersions.omitted()); + emit capabilitiesLoaded(); + for (auto* r: d->roomMap) + r->checkVersion(); + }); +} + +bool Connection::loadingCapabilities() const +{ + // (Ab)use the fact that room versions cannot be omitted after + // the capabilities have been loaded (see reloadCapabilities() above). + return d->capabilities.roomVersions.omitted(); +} + void Connection::Private::connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId) @@ -253,7 +295,7 @@ void Connection::Private::connectWithToken(const QString& user, << "by user" << userId << "from device" << deviceId; emit q->stateChanged(); emit q->connected(); - + q->reloadCapabilities(); } void Connection::checkAndConnect(const QString& userId, @@ -280,11 +322,14 @@ void Connection::checkAndConnect(const QString& userId, void Connection::logout() { auto job = callApi<LogoutJob>(); - connect( job, &LogoutJob::success, this, [this] { - stopSync(); - d->data->setToken({}); - emit stateChanged(); - emit loggedOut(); + connect( job, &LogoutJob::finished, this, [job,this] { + if (job->status().good() || job->error() == BaseJob::ContentAccessError) + { + stopSync(); + d->data->setToken({}); + emit stateChanged(); + emit loggedOut(); + } }); } @@ -293,11 +338,11 @@ void Connection::sync(int timeout) if (d->syncJob) return; - // Raw string: http://en.cppreference.com/w/cpp/language/string_literal - const auto filter = - QStringLiteral(R"({"room": { "timeline": { "limit": 100 } } })"); + Filter filter; + filter.room->timeline->limit = 100; + filter.room->state->lazyLoadMembers = d->lazyLoading; auto job = d->syncJob = callApi<SyncJob>(BackgroundRequest, - d->data->lastEvent(), filter, timeout); + d->data->lastEvent(), filter, timeout); connect( job, &SyncJob::success, this, [this, job] { onSyncSuccess(job->takeData()); d->syncJob = nullptr; @@ -306,7 +351,7 @@ void Connection::sync(int timeout) connect( job, &SyncJob::retryScheduled, this, [this,job] (int retriesTaken, int nextInMilliseconds) { - emit networkError(job->errorString(), job->rawData(TRIM_RAW_DATA), + emit networkError(job->errorString(), job->rawDataSample(), retriesTaken, nextInMilliseconds); }); connect( job, &SyncJob::failure, this, [this, job] { @@ -315,14 +360,35 @@ void Connection::sync(int timeout) { qCWarning(SYNCJOB) << "Sync job failed with ContentAccessError - login expired?"; - emit loginError(job->errorString(), job->rawData(TRIM_RAW_DATA)); + emit loginError(job->errorString(), job->rawDataSample()); } else - emit syncError(job->errorString(), job->rawData(TRIM_RAW_DATA)); + emit syncError(job->errorString(), job->rawDataSample()); }); } -void Connection::onSyncSuccess(SyncData &&data) { +void Connection::syncLoop(int timeout) +{ + d->syncLoopTimeout = timeout; + connect(this, &Connection::syncDone, this, &Connection::syncLoopIteration); + syncLoopIteration(); // initial sync to start the loop +} + +QJsonObject toJson(const Connection::DirectChatsMap& directChats) +{ + QJsonObject json; + for (auto it = directChats.begin(); it != directChats.end();) + { + QJsonArray roomIds; + const auto* user = it.key(); + for (; it != directChats.end() && it.key() == user; ++it) + roomIds.append(*it); + json.insert(user->id(), roomIds); + } + return json; +} + +void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { d->data->setLastEvent(data.nextBatch()); for (auto&& roomData: data.takeRoomData()) { @@ -343,80 +409,123 @@ void Connection::onSyncSuccess(SyncData &&data) { } if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) ) { - r->updateData(std::move(roomData)); + d->pendingStateRoomIds.removeOne(roomData.roomId); + r->updateData(std::move(roomData), fromCache); if (d->firstTimeRooms.removeOne(r)) + { emit loadedRoomState(r); + if (!d->capabilities.roomVersions.omitted()) + r->checkVersion(); + // Otherwise, the version will be checked in reloadCapabilities() + } } + // Let UI update itself after updating each room QCoreApplication::processEvents(); } - for (auto&& accountEvent: data.takeAccountData()) + // After running this loop, the account data events not saved in + // d->accountData (see the end of the loop body) are auto-cleaned away + for (auto& eventPtr : data.takeAccountData()) { - if (is<DirectChatEvent>(*accountEvent)) - { - const auto usersToDCs = ptrCast<DirectChatEvent>(move(accountEvent)) - ->usersToDirectChats(); - DirectChatsMap removals = - erase_if(d->directChats, [&usersToDCs] (auto it) { - return !usersToDCs.contains(it.key()->id(), it.value()); - }); - erase_if(d->directChatUsers, [&usersToDCs] (auto it) { - return !usersToDCs.contains(it.value()->id(), it.key()); - }); - if (MAIN().isDebugEnabled()) - for (auto it = removals.begin(); it != removals.end(); ++it) - qCDebug(MAIN) << it.value() - << "is no more a direct chat with" << it.key()->id(); - - DirectChatsMap additions; - for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) - { - if (auto* u = user(it.key())) - { - if (!d->directChats.contains(u, it.value())) - { - Q_ASSERT(!d->directChatUsers.contains(it.value(), u)); - additions.insert(u, it.value()); - d->directChats.insert(u, it.value()); - d->directChatUsers.insert(it.value(), u); - qCDebug(MAIN) << "Marked room" << it.value() + visit(*eventPtr, + [this](const DirectChatEvent& dce) { + // See https://github.com/QMatrixClient/libqmatrixclient/wiki/Handling-direct-chat-events + const auto& usersToDCs = dce.usersToDirectChats(); + DirectChatsMap remoteRemovals = + erase_if(d->directChats, [&usersToDCs, this](auto it) { + return !(usersToDCs.contains(it.key()->id(), it.value()) + || d->dcLocalAdditions.contains(it.key(), + it.value())); + }); + erase_if(d->directChatUsers, [&remoteRemovals](auto it) { + return remoteRemovals.contains(it.value(), it.key()); + }); + // Remove from dcLocalRemovals what the server already has. + erase_if(d->dcLocalRemovals, [&remoteRemovals](auto it) { + return remoteRemovals.contains(it.key(), it.value()); + }); + if (MAIN().isDebugEnabled()) + for (auto it = remoteRemovals.begin(); + it != remoteRemovals.end(); ++it) { + qCDebug(MAIN) + << it.value() << "is no more a direct chat with" + << it.key()->id(); + } + + DirectChatsMap remoteAdditions; + for (auto it = usersToDCs.begin(); it != usersToDCs.end(); + ++it) { + if (auto* u = user(it.key())) { + if (!d->directChats.contains(u, it.value()) + && !d->dcLocalRemovals.contains(u, it.value())) + { + Q_ASSERT( + !d->directChatUsers.contains(it.value(), u)); + remoteAdditions.insert(u, it.value()); + d->directChats.insert(u, it.value()); + d->directChatUsers.insert(it.value(), u); + qCDebug(MAIN) + << "Marked room" << it.value() << "as a direct chat with" << u->id(); - } - } else - qCWarning(MAIN) - << "Couldn't get a user object for" << it.key(); - } - if (!additions.isEmpty() || !removals.isEmpty()) - emit directChatsListChanged(additions, removals); - - continue; - } - if (is<IgnoredUsersEvent>(*accountEvent)) - qCDebug(MAIN) << "Users ignored by" << d->userId << "updated:" - << QStringList::fromSet(ignoredUsers()).join(','); - - auto& currentData = d->accountData[accountEvent->matrixType()]; - // A polymorphic event-specific comparison might be a bit more - // efficient; maaybe do it another day - if (!currentData || - currentData->contentJson() != accountEvent->contentJson()) - { - currentData = std::move(accountEvent); - qCDebug(MAIN) << "Updated account data of type" - << currentData->matrixType(); - emit accountDataChanged(currentData->matrixType()); - } + } + } else + qCWarning(MAIN) << "Couldn't get a user object for" + << it.key(); + } + // Remove from dcLocalAdditions what the server already has. + erase_if(d->dcLocalAdditions, [&remoteAdditions](auto it) { + return remoteAdditions.contains(it.key(), it.value()); + }); + if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty()) + emit directChatsListChanged(remoteAdditions, remoteRemovals); + }, + // catch-all, passing eventPtr for a possible take-over + [this, &eventPtr](const Event& accountEvent) { + if (is<IgnoredUsersEvent>(accountEvent)) + qCDebug(MAIN) + << "Users ignored by" << d->userId << "updated:" + << QStringList::fromSet(ignoredUsers()).join(','); + + auto& currentData = d->accountData[accountEvent.matrixType()]; + // A polymorphic event-specific comparison might be a bit more + // efficient; maaybe do it another day + if (!currentData + || currentData->contentJson() + != accountEvent.contentJson()) { + currentData = std::move(eventPtr); + qCDebug(MAIN) << "Updated account data of type" + << currentData->matrixType(); + emit accountDataChanged(currentData->matrixType()); + } + }); + } + if (!d->dcLocalAdditions.isEmpty() || !d->dcLocalRemovals.isEmpty()) { + qDebug(MAIN) << "Sending updated direct chats to the server:" + << d->dcLocalRemovals.size() << "removal(s)," + << d->dcLocalAdditions.size() << "addition(s)"; + callApi<SetAccountDataJob>(d->userId, QStringLiteral("m.direct"), + toJson(d->directChats)); + d->dcLocalAdditions.clear(); + d->dcLocalRemovals.clear(); } } void Connection::stopSync() { - if (d->syncJob) + // If there's a sync loop, break it + disconnect(this, &Connection::syncDone, + this, &Connection::syncLoopIteration); + if (d->syncJob) // If there's an ongoing sync job, stop it too { d->syncJob->abandon(); d->syncJob = nullptr; } } +QString Connection::nextBatchToken() const +{ + return d->data->lastEvent(); +} + PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const { return callApi<PostReceiptJob>(room->id(), "m.read", event->id()); @@ -426,14 +535,32 @@ JoinRoomJob* Connection::joinRoom(const QString& roomAlias, const QStringList& serverNames) { auto job = callApi<JoinRoomJob>(roomAlias, serverNames); + // Upon completion, ensure a room object in Join state is created but only + // if it's not already there due to a sync completing earlier. connect(job, &JoinRoomJob::success, - this, [this, job] { provideRoom(job->roomId(), JoinState::Join); }); + this, [this, job] { provideRoom(job->roomId()); }); return job; } -void Connection::leaveRoom(Room* room) +LeaveRoomJob* Connection::leaveRoom(Room* room) { - callApi<LeaveRoomJob>(room->id()); + const auto& roomId = room->id(); + const auto job = callApi<LeaveRoomJob>(roomId); + if (room->joinState() == JoinState::Invite) + { + // Workaround matrix-org/synapse#2181 - if the room is in invite state + // the invite may have been cancelled but Synapse didn't send it in + // `/sync`. See also #273 for the discussion in the library context. + d->pendingStateRoomIds.push_back(roomId); + connect(job, &LeaveRoomJob::success, this, [this,roomId] { + if (d->pendingStateRoomIds.removeOne(roomId)) + { + qCDebug(MAIN) << "Forcing the room to Leave status"; + provideRoom(roomId, JoinState::Leave); + } + }); + } + return job; } inline auto splitMediaId(const QString& mediaId) @@ -466,13 +593,21 @@ MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, } UploadContentJob* Connection::uploadContent(QIODevice* contentSource, - const QString& filename, const QString& contentType) const + const QString& filename, const QString& overrideContentType) const { + auto contentType = overrideContentType; + if (contentType.isEmpty()) + { + contentType = + QMimeDatabase().mimeTypeForFileNameAndData(filename, contentSource) + .name(); + contentSource->open(QIODevice::ReadOnly); + } return callApi<UploadContentJob>(contentSource, filename, contentType); } UploadContentJob* Connection::uploadFile(const QString& fileName, - const QString& contentType) + const QString& overrideContentType) { auto sourceFile = new QFile(fileName); if (!sourceFile->open(QIODevice::ReadOnly)) @@ -482,7 +617,7 @@ UploadContentJob* Connection::uploadFile(const QString& fileName, return nullptr; } return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(), - contentType); + overrideContentType); } GetContentJob* Connection::getContent(const QString& mediaId) const @@ -508,7 +643,8 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url, CreateRoomJob* Connection::createRoom(RoomVisibility visibility, const QString& alias, const QString& name, const QString& topic, - QStringList invites, const QString& presetName, bool isDirect, + QStringList invites, const QString& presetName, + const QString& roomVersion, bool isDirect, const QVector<CreateRoomJob::StateEvent>& initialState, const QVector<CreateRoomJob::Invite3pid>& invite3pids, const QJsonObject& creationContent) @@ -517,10 +653,19 @@ CreateRoomJob* Connection::createRoom(RoomVisibility visibility, auto job = callApi<CreateRoomJob>( visibility == PublishRoom ? QStringLiteral("public") : QStringLiteral("private"), - alias, name, topic, invites, invite3pids, QString(/*TODO: #233*/), + alias, name, topic, invites, invite3pids, roomVersion, creationContent, initialState, presetName, isDirect); - connect(job, &BaseJob::success, this, [this,job] { - emit createdRoom(provideRoom(job->roomId(), JoinState::Join)); + connect(job, &BaseJob::success, this, [this,job,invites,isDirect] { + auto* room = provideRoom(job->roomId(), JoinState::Join); + if (!room) + { + Q_ASSERT_X(room, "Connection::createRoom", "Failed to create a room"); + return; + } + emit createdRoom(room); + if (isDirect) + for (const auto& i: invites) + addToDirectChats(room, user(i)); }); return job; } @@ -556,8 +701,8 @@ void Connection::doInDirectChat(User* u, { Q_ASSERT(u); const auto& userId = u->id(); - // There can be more than one DC; find the first valid, and delete invalid - // (left/forgotten) ones along the way. + // There can be more than one DC; find the first valid (existing and + // not left), and delete inexistent (forgotten?) ones along the way. DirectChatsMap removals; for (auto it = d->directChats.find(u); it != d->directChats.end() && it.key() == u; ++it) @@ -567,7 +712,7 @@ void Connection::doInDirectChat(User* u, { Q_ASSERT(r->id() == roomId); // A direct chat with yourself should only involve yourself :) - if (userId == d->userId && r->memberCount() > 1) + if (userId == d->userId && r->totalMemberCount() > 1) continue; qCDebug(MAIN) << "Requested direct chat with" << userId << "is already available as" << r->id(); @@ -594,6 +739,8 @@ void Connection::doInDirectChat(User* u, << roomId << "is not valid and will be discarded"; // Postpone actual deletion until we finish iterating d->directChats. removals.insert(it.key(), it.value()); + // Add to the list of updates to send to the server upon the next sync. + d->dcLocalRemovals.insert(it.key(), it.value()); } if (!removals.isEmpty()) { @@ -603,7 +750,7 @@ void Connection::doInDirectChat(User* u, d->directChatUsers.remove(it.value(), const_cast<User*>(it.key())); // FIXME } - d->broadcastDirectChatUpdates({}, removals); + emit directChatsListChanged({}, removals); } auto j = createDirectChat(userId); @@ -618,8 +765,8 @@ void Connection::doInDirectChat(User* u, CreateRoomJob* Connection::createDirectChat(const QString& userId, const QString& topic, const QString& name) { - return createRoom(UnpublishRoom, "", name, topic, {userId}, - "trusted_private_chat", true); + return createRoom(UnpublishRoom, {}, name, topic, {userId}, + QStringLiteral("trusted_private_chat"), {}, true); } ForgetRoomJob* Connection::forgetRoom(const QString& id) @@ -698,6 +845,11 @@ QUrl Connection::homeserver() const return d->data->baseUrl(); } +QString Connection::domain() const +{ + return d->userId.section(':', 1); +} + Room* Connection::room(const QString& roomId, JoinStates states) const { Room* room = d->roomMap.value({roomId, false}, nullptr); @@ -716,6 +868,41 @@ Room* Connection::room(const QString& roomId, JoinStates states) const return nullptr; } +Room* Connection::roomByAlias(const QString& roomAlias, JoinStates states) const +{ + const auto id = d->roomAliasMap.value(roomAlias); + if (!id.isEmpty()) + return room(id, states); + qCWarning(MAIN) << "Room for alias" << roomAlias + << "is not found under account" << userId(); + return nullptr; +} + +void Connection::updateRoomAliases(const QString& roomId, + const QStringList& previousRoomAliases, + const QStringList& roomAliases) +{ + for (const auto& a: previousRoomAliases) + if (d->roomAliasMap.remove(a) == 0) + qCWarning(MAIN) << "Alias" << a << "is not found (already deleted?)"; + + for (const auto& a: roomAliases) + { + auto& mappedId = d->roomAliasMap[a]; + if (!mappedId.isEmpty()) + { + if (mappedId == roomId) + qCDebug(MAIN) << "Alias" << a << "is already mapped to room" + << roomId; + else + qCWarning(MAIN) << "Alias" << a + << "will be force-remapped from room" + << mappedId << "to" << roomId; + } + mappedId = roomId; + } +} + Room* Connection::invitation(const QString& roomId) const { return d->roomMap.value({roomId, true}, nullptr); @@ -825,7 +1012,8 @@ QHash<QString, QVector<Room*>> Connection::tagsToRooms() const QHash<QString, QVector<Room*>> result; for (auto* r: qAsConst(d->roomMap)) { - for (const auto& tagName: r->tagNames()) + const auto& tagNames = r->tagNames(); + for (const auto& tagName: tagNames) result[tagName].push_back(r); } for (auto it = result.begin(); it != result.end(); ++it) @@ -840,9 +1028,12 @@ QStringList Connection::tagNames() const { QStringList tags ({FavouriteTag}); for (auto* r: qAsConst(d->roomMap)) - for (const auto& tag: r->tagNames()) + { + const auto& tagNames = r->tagNames(); + for (const auto& tag: tagNames) if (tag != LowPriorityTag && !tags.contains(tag)) tags.push_back(tag); + } tags.push_back(LowPriorityTag); return tags; } @@ -860,28 +1051,6 @@ Connection::DirectChatsMap Connection::directChats() const return d->directChats; } -QJsonObject toJson(const Connection::DirectChatsMap& directChats) -{ - QJsonObject json; - for (auto it = directChats.begin(); it != directChats.end();) - { - QJsonArray roomIds; - const auto* user = it.key(); - for (; it != directChats.end() && it.key() == user; ++it) - roomIds.append(*it); - json.insert(user->id(), roomIds); - } - return json; -} - -void Connection::Private::broadcastDirectChatUpdates(const DirectChatsMap& additions, - const DirectChatsMap& removals) -{ - q->callApi<SetAccountDataJob>(userId, QStringLiteral("m.direct"), - toJson(directChats)); - emit q->directChatsListChanged(additions, removals); -} - void Connection::addToDirectChats(const Room* room, User* user) { Q_ASSERT(room != nullptr && user != nullptr); @@ -890,8 +1059,8 @@ void Connection::addToDirectChats(const Room* room, User* user) Q_ASSERT(!d->directChatUsers.contains(room->id(), user)); d->directChats.insert(user, room->id()); d->directChatUsers.insert(room->id(), user); - DirectChatsMap additions { { user, room->id() } }; - d->broadcastDirectChatUpdates(additions, {}); + d->dcLocalAdditions.insert(user, room->id()); + emit directChatsListChanged({ { user, room->id() } }, {}); } void Connection::removeFromDirectChats(const QString& roomId, User* user) @@ -904,15 +1073,17 @@ void Connection::removeFromDirectChats(const QString& roomId, User* user) DirectChatsMap removals; if (user != nullptr) { - removals.insert(user, roomId); d->directChats.remove(user, roomId); d->directChatUsers.remove(roomId, user); + removals.insert(user, roomId); + d->dcLocalRemovals.insert(user, roomId); } else { removals = erase_if(d->directChats, [&roomId] (auto it) { return it.value() == roomId; }); d->directChatUsers.remove(roomId); + d->dcLocalRemovals += removals; } - d->broadcastDirectChatUpdates({}, removals); + emit directChatsListChanged({}, removals); } bool Connection::isDirectChat(const QString& roomId) const @@ -972,11 +1143,12 @@ const ConnectionData* Connection::connectionData() const return d->data.get(); } -Room* Connection::provideRoom(const QString& id, JoinState joinState) +Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState) { // TODO: This whole function is a strong case for a RoomManager class. Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id"); + // If joinState.omitted(), all joinState == comparisons below are false. const auto roomKey = qMakePair(id, joinState == JoinState::Invite); auto* room = d->roomMap.value(roomKey, nullptr); if (room) @@ -986,10 +1158,19 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) // and emit a signal. For Invite and Join, there's no such problem. if (room->joinState() == joinState && joinState != JoinState::Leave) return room; + } else if (joinState.omitted()) + { + // No Join and Leave, maybe Invite? + room = d->roomMap.value({id, true}, nullptr); + if (room) + return room; + // No Invite either, setup a new room object below } - else + + if (!room) { - room = roomFactory()(this, id, joinState); + room = roomFactory()(this, id, + joinState.omitted() ? JoinState::Join : joinState.value()); if (!room) { qCCritical(MAIN) << "Failed to create a room" << id; @@ -1001,6 +1182,9 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) this, &Connection::aboutToDeleteRoom); emit newRoom(room); } + if (joinState.omitted()) + return room; + if (joinState == JoinState::Invite) { // prev is either Leave or nullptr @@ -1009,7 +1193,7 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) } else { - room->setJoinState(joinState); + room->setJoinState(joinState.value()); // Preempt the Invite room (if any) with a room in Join/Leave state. auto* prevInvite = d->roomMap.take({id, true}); if (joinState == JoinState::Join) @@ -1018,6 +1202,9 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) emit leftRoom(room, prevInvite); if (prevInvite) { + const auto dcUsers = prevInvite->directChatUsers(); + for (auto* u: dcUsers) + addToDirectChats(room, u); qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id(); emit prevInvite->beforeDestruction(prevInvite); prevInvite->deleteLater(); @@ -1064,56 +1251,64 @@ void Connection::setHomeserver(const QUrl& url) emit homeserverChanged(homeserver()); } -static constexpr int CACHE_VERSION_MAJOR = 8; -static constexpr int CACHE_VERSION_MINOR = 0; +void Connection::saveRoomState(Room* r) const +{ + Q_ASSERT(r); + if (!d->cacheState) + return; -void Connection::saveState(const QUrl &toFile) const + QFile outRoomFile { stateCachePath() % SyncData::fileNameForRoom(r->id()) }; + if (outRoomFile.open(QFile::WriteOnly)) + { + QJsonDocument json { r->toJson() }; + auto data = d->cacheToBinary ? json.toBinaryData() + : json.toJson(QJsonDocument::Compact); + outRoomFile.write(data.data(), data.size()); + qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName(); + } else { + qCWarning(MAIN) << "Error opening" << outRoomFile.fileName() + << ":" << outRoomFile.errorString(); + } +} + +void Connection::saveState() const { if (!d->cacheState) return; QElapsedTimer et; et.start(); - QFileInfo stateFile { - toFile.isEmpty() ? stateCachePath() : toFile.toLocalFile() - }; - if (!stateFile.dir().exists()) - stateFile.dir().mkpath("."); - - QFile outfile { stateFile.absoluteFilePath() }; - if (!outfile.open(QFile::WriteOnly)) + QFile outFile { stateCachePath() % "state.json" }; + if (!outFile.open(QFile::WriteOnly)) { - qCWarning(MAIN) << "Error opening" << stateFile.absoluteFilePath() - << ":" << outfile.errorString(); + qCWarning(MAIN) << "Error opening" << outFile.fileName() + << ":" << outFile.errorString(); qCWarning(MAIN) << "Caching the rooms state disabled"; d->cacheState = false; return; } - QJsonObject rootObj; + QJsonObject rootObj { + { QStringLiteral("cache_version"), QJsonObject { + { QStringLiteral("major"), SyncData::cacheVersion().first }, + { QStringLiteral("minor"), SyncData::cacheVersion().second } + }}}; { QJsonObject rooms; QJsonObject inviteRooms; - for (const auto* i : roomMap()) // Pass on rooms in Leave state - { - if (i->joinState() == JoinState::Invite) - inviteRooms.insert(i->id(), i->toJson()); - else - rooms.insert(i->id(), i->toJson()); - QElapsedTimer et1; et1.start(); - QCoreApplication::processEvents(); - if (et1.elapsed() > 1) - qCDebug(PROFILER) << "processEvents() borrowed" << et1; - } + 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 roomObj; if (!rooms.isEmpty()) - roomObj.insert("join", rooms); + roomObj.insert(QStringLiteral("join"), rooms); if (!inviteRooms.isEmpty()) - roomObj.insert("invite", inviteRooms); + roomObj.insert(QStringLiteral("invite"), inviteRooms); - rootObj.insert("next_batch", d->data->lastEvent()); - rootObj.insert("rooms", roomObj); + rootObj.insert(QStringLiteral("next_batch"), d->data->lastEvent()); + rootObj.insert(QStringLiteral("rooms"), roomObj); } { QJsonArray accountDataEvents { @@ -1123,67 +1318,39 @@ void Connection::saveState(const QUrl &toFile) const accountDataEvents.append( basicEventJson(e.first, e.second->contentJson())); - rootObj.insert("account_data", + rootObj.insert(QStringLiteral("account_data"), QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); } - QJsonObject versionObj; - versionObj.insert("major", CACHE_VERSION_MAJOR); - versionObj.insert("minor", CACHE_VERSION_MINOR); - rootObj.insert("cache_version", versionObj); - QJsonDocument json { rootObj }; auto data = d->cacheToBinary ? json.toBinaryData() : json.toJson(QJsonDocument::Compact); qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; - outfile.write(data.data(), data.size()); - qCDebug(MAIN) << "State cache saved to" << outfile.fileName(); + outFile.write(data.data(), data.size()); + qCDebug(MAIN) << "State cache saved to" << outFile.fileName(); } -void Connection::loadState(const QUrl &fromFile) +void Connection::loadState() { if (!d->cacheState) return; QElapsedTimer et; et.start(); - QFile file { - fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile() - }; - if (!file.exists()) - { - qCDebug(MAIN) << "No state cache file found"; - return; - } - if(!file.open(QFile::ReadOnly)) - { - qCWarning(MAIN) << "file " << file.fileName() << "failed to open for read"; - return; - } - QByteArray data = file.readAll(); - auto jsonDoc = d->cacheToBinary ? QJsonDocument::fromBinaryData(data) : - QJsonDocument::fromJson(data); - if (jsonDoc.isNull()) - { - qCWarning(MAIN) << "Cache file broken, discarding"; + SyncData sync { stateCachePath() % "state.json" }; + if (sync.nextBatch().isEmpty()) // No token means no cache by definition return; - } - auto actualCacheVersionMajor = - jsonDoc.object() - .value("cache_version").toObject() - .value("major").toInt(); - if (actualCacheVersionMajor < CACHE_VERSION_MAJOR) + + if (!sync.unresolvedRooms().isEmpty()) { - qCWarning(MAIN) - << "Major version of the cache file is" << actualCacheVersionMajor - << "but" << CACHE_VERSION_MAJOR << "required; discarding the cache"; + qCWarning(MAIN) << "State cache incomplete, discarding"; return; } - - SyncData sync; - sync.parseJson(jsonDoc); - onSyncSuccess(std::move(sync)); + // TODO: to handle load failures, instead of the above block: + // 1. Do initial sync on failed rooms without saving the nextBatch token + // 2. Do the sync across all rooms as normal + onSyncSuccess(std::move(sync), true); qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et; } @@ -1191,8 +1358,7 @@ QString Connection::stateCachePath() const { auto safeUserId = userId(); safeUserId.replace(':', '_'); - return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) - % '/' % safeUserId % "_state.json"; + return cacheLocation(safeUserId); } bool Connection::cacheState() const @@ -1209,11 +1375,70 @@ void Connection::setCacheState(bool newValue) } } +bool QMatrixClient::Connection::lazyLoading() const +{ + return d->lazyLoading; +} + +void QMatrixClient::Connection::setLazyLoading(bool newValue) +{ + if (d->lazyLoading != newValue) + { + d->lazyLoading = newValue; + emit lazyLoadingChanged(); + } +} + void Connection::getTurnServers() { - auto job = callApi<GetTurnServerJob>(); - connect( job, &GetTurnServerJob::success, [=] { - emit turnServersChanged(job->data()); - }); + auto job = callApi<GetTurnServerJob>(); + connect(job, &GetTurnServerJob::success, + this, [=] { emit turnServersChanged(job->data()); }); +} + +const QString Connection::SupportedRoomVersion::StableTag = + QStringLiteral("stable"); + +QString Connection::defaultRoomVersion() const +{ + Q_ASSERT(!d->capabilities.roomVersions.omitted()); + return d->capabilities.roomVersions->defaultVersion; +} +QStringList Connection::stableRoomVersions() const +{ + Q_ASSERT(!d->capabilities.roomVersions.omitted()); + QStringList l; + const auto& allVersions = d->capabilities.roomVersions->available; + for (auto it = allVersions.begin(); it != allVersions.end(); ++it) + if (it.value() == SupportedRoomVersion::StableTag) + l.push_back(it.key()); + return l; +} + +inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1, + const Connection::SupportedRoomVersion& v2) +{ + bool ok1 = false, ok2 = false; + const auto vNum1 = v1.id.toFloat(&ok1); + const auto vNum2 = v2.id.toFloat(&ok2); + return ok1 && ok2 ? vNum1 < vNum2 : v1.id < v2.id; +} + +QVector<Connection::SupportedRoomVersion> Connection::availableRoomVersions() const +{ + Q_ASSERT(!d->capabilities.roomVersions.omitted()); + QVector<SupportedRoomVersion> result; + result.reserve(d->capabilities.roomVersions->available.size()); + for (auto it = d->capabilities.roomVersions->available.begin(); + it != d->capabilities.roomVersions->available.end(); ++it) + result.push_back({ it.key(), it.value() }); + // Put stable versions over unstable; within each group, + // sort numeric versions as numbers, the rest as strings. + const auto mid = std::partition(result.begin(), result.end(), + std::mem_fn(&SupportedRoomVersion::isStable)); + std::sort(result.begin(), mid, roomVersionLess); + std::sort(mid, result.end(), roomVersionLess); + + return result; } diff --git a/lib/connection.h b/lib/connection.h index b06fb143..ea5be51a 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -21,6 +21,7 @@ #include "csapi/create_room.h" #include "joinstate.h" #include "events/accountdataevents.h" +#include "qt_connection_util.h" #include <QtCore/QObject> #include <QtCore/QUrl> @@ -48,26 +49,7 @@ namespace QMatrixClient class DownloadFileJob; class SendToDeviceJob; class SendMessageJob; - - /** Create a single-shot connection that triggers on the signal and - * then self-disconnects - * - * Only supports DirectConnection type. - */ - template <typename SenderT1, typename SignalT, - typename ReceiverT2, typename SlotT> - inline auto connectSingleShot(SenderT1* sender, SignalT signal, - ReceiverT2* receiver, SlotT slot) - { - QMetaObject::Connection connection; - connection = QObject::connect(sender, signal, receiver, slot, - Qt::DirectConnection); - Q_ASSERT(connection); - QObject::connect(sender, signal, receiver, - [connection] { QObject::disconnect(connection); }, - Qt::DirectConnection); - return connection; - } + class LeaveRoomJob; class Connection; @@ -120,8 +102,12 @@ namespace QMatrixClient Q_PROPERTY(QString localUserId READ userId NOTIFY stateChanged) Q_PROPERTY(QString deviceId READ deviceId NOTIFY stateChanged) Q_PROPERTY(QByteArray accessToken READ accessToken NOTIFY stateChanged) + Q_PROPERTY(QString defaultRoomVersion READ defaultRoomVersion NOTIFY capabilitiesLoaded) Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) + Q_PROPERTY(QString domain READ domain NOTIFY homeserverChanged) Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) + Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) + public: // Room ids, rather than room pointers, are used in the direct chat // map types because the library keeps Invite rooms separate from @@ -246,8 +232,8 @@ namespace QMatrixClient */ void addToIgnoredUsers(const User* user); - /** Remove the user from the ignore list - * Similar to adding, the change signal is emitted synchronously. + /** Remove the user from the ignore list */ + /** Similar to adding, the change signal is emitted synchronously. * * \sa ignoredUsersListChanged */ @@ -256,9 +242,22 @@ namespace QMatrixClient /** Get the full list of users known to this account */ QMap<QString, User*> users() const; + /** Get the base URL of the homeserver to connect to */ QUrl homeserver() const; + /** Get the domain name used for ids/aliases on the server */ + QString domain() 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; + JoinStates states = JoinState::Invite|JoinState::Join) const; + /** Find a room by its alias and a mask of applicable states */ + Q_INVOKABLE Room* roomByAlias(const QString& roomAlias, + JoinStates states = JoinState::Invite|JoinState::Join) const; + /** Update the internal map of room aliases to IDs */ + /// This is used for internal bookkeeping of rooms. Do NOT use + /// it to try change aliases, use Room::setAliases instead + void updateRoomAliases(const QString& roomId, + const QStringList& previousRoomAliases, + const QStringList& roomAliases); Q_INVOKABLE Room* invitation(const QString& roomId) const; Q_INVOKABLE User* user(const QString& userId); const User* user() const; @@ -273,6 +272,35 @@ namespace QMatrixClient Q_INVOKABLE QString token() const; Q_INVOKABLE void getTurnServers(); + struct SupportedRoomVersion + { + QString id; + QString status; + + static const QString StableTag; // "stable", as of CS API 0.5 + bool isStable() const { return status == StableTag; } + + friend QDebug operator<<(QDebug dbg, + const SupportedRoomVersion& v) + { + QDebugStateSaver _(dbg); + return dbg.nospace() << v.id << '/' << v.status; + } + }; + + /// Get the room version recommended by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ + QString defaultRoomVersion() const; + /// Get the room version considered stable by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ + QStringList stableRoomVersions() const; + /// Get all room versions supported by the server + /** Only works after server capabilities have been loaded. + * \sa loadingCapabilities */ + QVector<SupportedRoomVersion> availableRoomVersions() const; + /** * Call this before first sync to load from previously saved file. * @@ -280,7 +308,7 @@ namespace QMatrixClient * to be QML-friendly. Empty parameter means using a path * defined by stateCachePath(). */ - Q_INVOKABLE void loadState(const QUrl &fromFile = {}); + Q_INVOKABLE void loadState(); /** * This method saves the current state of rooms (but not messages * in them) to a local cache file, so that it could be loaded by @@ -290,7 +318,10 @@ namespace QMatrixClient * QML-friendly. Empty parameter means using a path defined by * stateCachePath(). */ - Q_INVOKABLE void saveState(const QUrl &toFile = {}) const; + Q_INVOKABLE void saveState() const; + + /// This method saves the current state of a single room. + void saveRoomState(Room* r) const; /** * The default path to store the cached room state, defined as @@ -305,6 +336,9 @@ namespace QMatrixClient bool cacheState() const; void setCacheState(bool newValue); + bool lazyLoading() const; + void setLazyLoading(bool newValue); + /** Start a job of a specified type with specified arguments and policy * * This is a universal method to start a job of a type passed @@ -375,13 +409,21 @@ namespace QMatrixClient const QString& deviceId = {}); void connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId); + /// Explicitly request capabilities from the server + void reloadCapabilities(); + + /// Find out if capabilites are still loading from the server + bool loadingCapabilities() const; /** @deprecated Use stopSync() instead */ void disconnectFromServer() { stopSync(); } void logout(); void sync(int timeout = -1); + void syncLoop(int timeout = -1); + void stopSync(); + QString nextBatchToken() const; virtual MediaThumbnailJob* getThumbnail(const QString& mediaId, QSize requestedSize, RunningPolicy policy = BackgroundRequest) const; @@ -393,10 +435,10 @@ namespace QMatrixClient // QIODevice* should already be open UploadContentJob* uploadContent(QIODevice* contentSource, - const QString& filename = {}, - const QString& contentType = {}) const; + const QString& filename = {}, + const QString& overrideContentType = {}) const; UploadContentJob* uploadFile(const QString& fileName, - const QString& contentType = {}); + const QString& overrideContentType = {}); GetContentJob* getContent(const QString& mediaId) const; GetContentJob* getContent(const QUrl& url) const; // If localFilename is empty, a temporary file will be created @@ -411,7 +453,7 @@ namespace QMatrixClient CreateRoomJob* createRoom(RoomVisibility visibility, const QString& alias, const QString& name, const QString& topic, QStringList invites, const QString& presetName = {}, - bool isDirect = false, + const QString& roomVersion = {}, bool isDirect = false, const QVector<CreateRoomJob::StateEvent>& initialState = {}, const QVector<CreateRoomJob::Invite3pid>& invite3pids = {}, const QJsonObject& creationContent = {}); @@ -485,14 +527,14 @@ namespace QMatrixClient SendMessageJob* sendMessage(const QString& roomId, const RoomEvent& event) const; + /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ + virtual LeaveRoomJob* leaveRoom( Room* room ); + // Old API that will be abolished any time soon. DO NOT USE. /** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead */ virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event) const; - /** @deprecated Use callApi<LeaveRoomJob>() or Room::leaveRoom() instead */ - virtual void leaveRoom( Room* room ); - signals: /** * @deprecated @@ -508,6 +550,7 @@ namespace QMatrixClient void resolveError(QString error); void homeserverChanged(QUrl baseUrl); + void capabilitiesLoaded(); void connected(); void reconnected(); //< \deprecated Use connected() instead @@ -519,7 +562,7 @@ namespace QMatrixClient * a successful login and logout and are constant at other times. */ void stateChanged(); - void loginError(QString message, QByteArray details); + void loginError(QString message, QString details); /** A network request (job) failed * @@ -537,11 +580,11 @@ namespace QMatrixClient * @param retriesTaken - how many retries have already been taken * @param nextRetryInMilliseconds - when the job will retry again */ - void networkError(QString message, QByteArray details, + void networkError(QString message, QString details, int retriesTaken, int nextRetryInMilliseconds); void syncDone(); - void syncError(QString message, QByteArray details); + void syncError(QString message, QString details); void newUser(User* user); @@ -652,6 +695,7 @@ namespace QMatrixClient IgnoredUsersList removals); void cacheStateChanged(); + void lazyLoadingChanged(); void turnServersChanged(const QJsonObject& servers); protected: @@ -660,21 +704,35 @@ namespace QMatrixClient */ const ConnectionData* connectionData() const; - /** - * @brief Find a (possibly new) Room object for the specified id - * Use this method whenever you need to find a Room object in - * the local list of rooms. Note that this does not interact with - * the server; in particular, does not automatically create rooms - * on the server. - * @return a pointer to a Room object with the specified id; nullptr - * if roomId is empty or roomFactory() failed to create a Room object. - */ - Room* provideRoom(const QString& roomId, JoinState joinState); + /** Get a Room object for the given id in the given state + * + * Use this method when you need a Room object in the local list + * of rooms, with the given state. Note that this does not interact + * with the server; in particular, does not automatically create + * rooms on the server. This call performs necessary join state + * transitions; e.g., if it finds a room in Invite but + * `joinState == JoinState::Join` then the Invite room object + * will be deleted and a new room object with Join state created. + * In contrast, switching between Join and Leave happens within + * the same object. + * \param roomId room id (not alias!) + * \param joinState desired (target) join state of the room; if + * omitted, any state will be found and return unchanged, or a + * new Join room created. + * @return a pointer to a Room object with the specified id and the + * specified state; nullptr if roomId is empty or if roomFactory() + * failed to create a Room object. + */ + Room* provideRoom(const QString& roomId, + Omittable<JoinState> joinState = none); /** * Completes loading sync data. */ - void onSyncSuccess(SyncData &&data); + void onSyncSuccess(SyncData &&data, bool fromCache = false); + + protected slots: + void syncLoopIteration(); private: class Private; diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index eb516ef7..91cda09f 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -25,7 +25,7 @@ using namespace QMatrixClient; struct ConnectionData::Private { - explicit Private(const QUrl& url) : baseUrl(url) { } + explicit Private(QUrl url) : baseUrl(std::move(url)) { } QUrl baseUrl; QByteArray accessToken; @@ -37,7 +37,7 @@ struct ConnectionData::Private }; ConnectionData::ConnectionData(QUrl baseUrl) - : d(std::make_unique<Private>(baseUrl)) + : d(std::make_unique<Private>(std::move(baseUrl))) { } ConnectionData::~ConnectionData() = default; @@ -98,7 +98,7 @@ QString ConnectionData::lastEvent() const void ConnectionData::setLastEvent(QString identifier) { - d->lastEvent = identifier; + d->lastEvent = std::move(identifier); } QByteArray ConnectionData::generateTxnId() const diff --git a/lib/converters.cpp b/lib/converters.cpp index 41a9a65e..88f5267e 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -22,38 +22,34 @@ using namespace QMatrixClient; -QJsonValue QMatrixClient::variantToJson(const QVariant& v) +QJsonValue JsonConverter<QVariant>::dump(const QVariant& v) { return QJsonValue::fromVariant(v); } -QJsonObject QMatrixClient::toJson(const QVariantMap& map) +QVariant JsonConverter<QVariant>::load(const QJsonValue& jv) { - return QJsonObject::fromVariantMap(map); + return jv.toVariant(); } -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) -QJsonObject QMatrixClient::toJson(const QVariantHash& hMap) +QJsonObject JsonConverter<variant_map_t>::dump(const variant_map_t& map) { - return QJsonObject::fromVariantHash(hMap); -} + return +#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) + QJsonObject::fromVariantHash +#else + QJsonObject::fromVariantMap #endif - -QVariant FromJson<QVariant>::operator()(const QJsonValue& jv) const -{ - return jv.toVariant(); + (map); } -QMap<QString, QVariant> -FromJson<QMap<QString, QVariant>>::operator()(const QJsonValue& jv) const +variant_map_t JsonConverter<QVariantHash>::load(const QJsonValue& jv) { - return jv.toObject().toVariantMap(); -} - + return jv.toObject(). #if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) -QHash<QString, QVariant> -FromJson<QHash<QString, QVariant>>::operator()(const QJsonValue& jv) const -{ - return jv.toObject().toVariantHash(); -} + toVariantHash +#else + toVariantMap #endif + (); +} diff --git a/lib/converters.h b/lib/converters.h index 53855a1f..af2be645 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -57,238 +57,226 @@ class QVariant; namespace QMatrixClient { - // This catches anything implicitly convertible to QJsonValue/Object/Array - inline auto toJson(const QJsonValue& val) { return val; } - inline auto toJson(const QJsonObject& o) { return o; } - inline auto toJson(const QJsonArray& arr) { return arr; } - // Special-case QString to avoid ambiguity between QJsonValue - // and QVariant (also, QString.isEmpty() is used in _impl::AddNode<> below) - inline auto toJson(const QString& s) { return s; } - - inline QJsonArray toJson(const QStringList& strings) - { - return QJsonArray::fromStringList(strings); - } - - inline QString toJson(const QByteArray& bytes) + template <typename T> + struct JsonObjectConverter { - return bytes.constData(); - } + static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod; } + static void fillFrom(const QJsonObject& jo, T& pod) { pod = jo; } + }; - // QVariant is outrageously omnivorous - it consumes whatever is not - // exactly matching the signature of other toJson overloads. The trick - // below disables implicit conversion to QVariant through its numerous - // non-explicit constructors. - QJsonValue variantToJson(const QVariant& v); template <typename T> - inline auto toJson(T&& /* const QVariant& or QVariant&& */ var) - -> std::enable_if_t<std::is_same<std::decay_t<T>, QVariant>::value, - QJsonValue> + struct JsonConverter { - return variantToJson(var); - } - QJsonObject toJson(const QMap<QString, QVariant>& map); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) - QJsonObject toJson(const QHash<QString, QVariant>& hMap); -#endif + static QJsonObject dump(const T& pod) + { + QJsonObject jo; + JsonObjectConverter<T>::dumpTo(jo, pod); + return jo; + } + static T doLoad(const QJsonObject& jo) + { + T pod; + JsonObjectConverter<T>::fillFrom(jo, pod); + return pod; + } + static T load(const QJsonValue& jv) { return doLoad(jv.toObject()); } + static T load(const QJsonDocument& jd) { return doLoad(jd.object()); } + }; template <typename T> - inline QJsonArray toJson(const std::vector<T>& vals) + inline auto toJson(const T& pod) { - QJsonArray ar; - for (const auto& v: vals) - ar.push_back(toJson(v)); - return ar; + return JsonConverter<T>::dump(pod); } template <typename T> - inline QJsonArray toJson(const QVector<T>& vals) + inline auto fillJson(QJsonObject& json, const T& data) { - QJsonArray ar; - for (const auto& v: vals) - ar.push_back(toJson(v)); - return ar; + JsonObjectConverter<T>::dumpTo(json, data); } template <typename T> - inline QJsonObject toJson(const QSet<T>& set) + inline auto fromJson(const QJsonValue& jv) { - QJsonObject json; - for (auto e: set) - json.insert(toJson(e), QJsonObject{}); - return json; + return JsonConverter<T>::load(jv); } template <typename T> - inline QJsonObject toJson(const QHash<QString, T>& hashMap) + inline T fromJson(const QJsonDocument& jd) { - QJsonObject json; - for (auto it = hashMap.begin(); it != hashMap.end(); ++it) - json.insert(it.key(), toJson(it.value())); - return json; + return JsonConverter<T>::load(jd); } template <typename T> - inline QJsonObject toJson(const std::unordered_map<QString, T>& hashMap) + inline void fromJson(const QJsonValue& jv, T& pod) { - QJsonObject json; - for (auto it = hashMap.begin(); it != hashMap.end(); ++it) - json.insert(it.key(), toJson(it.value())); - return json; + if (!jv.isUndefined()) + pod = fromJson<T>(jv); } template <typename T> - struct FromJsonObject + inline void fromJson(const QJsonDocument& jd, T& pod) { - T operator()(const QJsonObject& jo) const { return T(jo); } - }; + pod = fromJson<T>(jd); + } + // Unfolds Omittable<> template <typename T> - struct FromJson + inline void fromJson(const QJsonValue& jv, Omittable<T>& pod) { - T operator()(const QJsonValue& jv) const - { - return FromJsonObject<T>()(jv.toObject()); - } - T operator()(const QJsonDocument& jd) const - { - return FromJsonObject<T>()(jd.object()); - } - }; + if (jv.isUndefined()) + pod = none; + else + pod = fromJson<T>(jv); + } template <typename T> - inline auto fromJson(const QJsonValue& jv) + inline void fillFromJson(const QJsonValue& jv, T& pod) { - return FromJson<T>()(jv); + if (jv.isObject()) + JsonObjectConverter<T>::fillFrom(jv.toObject(), pod); } + // JsonConverter<> specialisations + template <typename T> - inline auto fromJson(const QJsonDocument& jd) + struct TrivialJsonDumper { - return FromJson<T>()(jd); - } + // Works for: QJsonValue (and all things it can consume), + // QJsonObject, QJsonArray + static auto dump(const T& val) { return val; } + }; - template <> struct FromJson<bool> + template <> struct JsonConverter<bool> : public TrivialJsonDumper<bool> { - auto operator()(const QJsonValue& jv) const { return jv.toBool(); } + static auto load(const QJsonValue& jv) { return jv.toBool(); } }; - template <> struct FromJson<int> + template <> struct JsonConverter<int> : public TrivialJsonDumper<int> { - auto operator()(const QJsonValue& jv) const { return jv.toInt(); } + static auto load(const QJsonValue& jv) { return jv.toInt(); } }; - template <> struct FromJson<double> + template <> struct JsonConverter<double> + : public TrivialJsonDumper<double> { - auto operator()(const QJsonValue& jv) const { return jv.toDouble(); } + static auto load(const QJsonValue& jv) { return jv.toDouble(); } }; - template <> struct FromJson<float> + template <> struct JsonConverter<float> : public TrivialJsonDumper<float> { - auto operator()(const QJsonValue& jv) const { return float(jv.toDouble()); } + static auto load(const QJsonValue& jv) { return float(jv.toDouble()); } }; - template <> struct FromJson<qint64> + template <> struct JsonConverter<qint64> + : public TrivialJsonDumper<qint64> { - auto operator()(const QJsonValue& jv) const { return qint64(jv.toDouble()); } + static auto load(const QJsonValue& jv) { return qint64(jv.toDouble()); } }; - template <> struct FromJson<QString> + template <> struct JsonConverter<QString> + : public TrivialJsonDumper<QString> { - auto operator()(const QJsonValue& jv) const { return jv.toString(); } + static auto load(const QJsonValue& jv) { return jv.toString(); } }; - template <> struct FromJson<QDateTime> + template <> struct JsonConverter<QDateTime> { - auto operator()(const QJsonValue& jv) const + static auto dump(const QDateTime& val) = delete; // not provided yet + static auto load(const QJsonValue& jv) { - return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC); + return QDateTime::fromMSecsSinceEpoch( + fromJson<qint64>(jv), Qt::UTC); } }; - template <> struct FromJson<QDate> + template <> struct JsonConverter<QDate> { - auto operator()(const QJsonValue& jv) const + static auto dump(const QDate& val) = delete; // not provided yet + static auto load(const QJsonValue& jv) { return fromJson<QDateTime>(jv).date(); } }; - template <> struct FromJson<QJsonArray> + template <> struct JsonConverter<QJsonArray> + : public TrivialJsonDumper<QJsonArray> { - auto operator()(const QJsonValue& jv) const - { - return jv.toArray(); - } + static auto load(const QJsonValue& jv) { return jv.toArray(); } }; - template <> struct FromJson<QByteArray> + template <> struct JsonConverter<QByteArray> { - auto operator()(const QJsonValue& jv) const + static QString dump(const QByteArray& ba) { return ba.constData(); } + static auto load(const QJsonValue& jv) { return fromJson<QString>(jv).toLatin1(); } }; - template <> struct FromJson<QVariant> + template <> struct JsonConverter<QVariant> { - QVariant operator()(const QJsonValue& jv) const; + static QJsonValue dump(const QVariant& v); + static QVariant load(const QJsonValue& jv); }; - template <typename VectorT> - struct ArrayFromJson + template <typename VectorT, + typename T = typename VectorT::value_type> + struct JsonArrayConverter { - auto operator()(const QJsonArray& ja) const + static void dumpTo(QJsonArray& ar, const VectorT& vals) { - using size_type = typename VectorT::size_type; - VectorT vect; vect.resize(size_type(ja.size())); - std::transform(ja.begin(), ja.end(), - vect.begin(), FromJson<typename VectorT::value_type>()); - return vect; + for (const auto& v: vals) + ar.push_back(toJson(v)); } - auto operator()(const QJsonValue& jv) const + static auto dump(const VectorT& vals) { - return operator()(jv.toArray()); + QJsonArray ja; + dumpTo(ja, vals); + return ja; } - auto operator()(const QJsonDocument& jd) const + static auto load(const QJsonArray& ja) { - return operator()(jd.array()); + VectorT vect; vect.reserve(typename VectorT::size_type(ja.size())); + for (const auto& i: ja) + vect.push_back(fromJson<T>(i)); + return vect; } + static auto load(const QJsonValue& jv) { return load(jv.toArray()); } + static auto load(const QJsonDocument& jd) { return load(jd.array()); } }; - template <typename T> - struct FromJson<std::vector<T>> : ArrayFromJson<std::vector<T>> + template <typename T> struct JsonConverter<std::vector<T>> + : public JsonArrayConverter<std::vector<T>> { }; - template <typename T> - struct FromJson<QVector<T>> : ArrayFromJson<QVector<T>> + template <typename T> struct JsonConverter<QVector<T>> + : public JsonArrayConverter<QVector<T>> + { }; + + template <typename T> struct JsonConverter<QList<T>> + : public JsonArrayConverter<QList<T>> { }; - template <typename T> struct FromJson<QList<T>> + template <> struct JsonConverter<QStringList> + : public JsonConverter<QList<QString>> { - auto operator()(const QJsonValue& jv) const + static auto dump(const QStringList& sl) { - const auto jsonArray = jv.toArray(); - QList<T> sl; sl.reserve(jsonArray.size()); - std::transform(jsonArray.begin(), jsonArray.end(), - std::back_inserter(sl), FromJson<T>()); - return sl; + return QJsonArray::fromStringList(sl); } }; - template <> struct FromJson<QStringList> : FromJson<QList<QString>> { }; - - template <> struct FromJson<QMap<QString, QVariant>> + template <> struct JsonObjectConverter<QSet<QString>> { - QMap<QString, QVariant> operator()(const QJsonValue& jv) const; - }; - - template <typename T> struct FromJson<QSet<T>> - { - auto operator()(const QJsonValue& jv) const + static void dumpTo(QJsonObject& json, const QSet<QString>& s) + { + for (const auto& e: s) + json.insert(toJson(e), QJsonObject{}); + } + static auto fillFrom(const QJsonObject& json, QSet<QString>& s) { - const auto json = jv.toObject(); - QSet<T> s; s.reserve(json.size()); + s.reserve(s.size() + json.size()); for (auto it = json.begin(); it != json.end(); ++it) s.insert(it.key()); return s; @@ -298,39 +286,44 @@ namespace QMatrixClient template <typename HashMapT> struct HashMapFromJson { - auto operator()(const QJsonObject& jo) const + static void dumpTo(QJsonObject& json, const HashMapT& hashMap) + { + for (auto it = hashMap.begin(); it != hashMap.end(); ++it) + json.insert(it.key(), toJson(it.value())); + } + static void fillFrom(const QJsonObject& jo, HashMapT& h) { - HashMapT h; h.reserve(jo.size()); + h.reserve(jo.size()); for (auto it = jo.begin(); it != jo.end(); ++it) h[it.key()] = fromJson<typename HashMapT::mapped_type>(it.value()); - return h; - } - auto operator()(const QJsonValue& jv) const - { - return operator()(jv.toObject()); - } - auto operator()(const QJsonDocument& jd) const - { - return operator()(jd.object()); } }; template <typename T> - struct FromJson<std::unordered_map<QString, T>> - : HashMapFromJson<std::unordered_map<QString, T>> + struct JsonObjectConverter<std::unordered_map<QString, T>> + : public HashMapFromJson<std::unordered_map<QString, T>> { }; template <typename T> - struct FromJson<QHash<QString, T>> : HashMapFromJson<QHash<QString, T>> + struct JsonObjectConverter<QHash<QString, T>> + : public HashMapFromJson<QHash<QString, T>> { }; + // We could use std::conditional<> below but QT_VERSION* macros in C++ code + // cause (kinda valid but useless and noisy) compiler warnings about + // bitwise operations on signed integers; so use the preprocessor for now. + using variant_map_t = #if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) - template <> struct FromJson<QHash<QString, QVariant>> + QVariantHash; +#else + QVariantMap; +#endif + template <> struct JsonConverter<variant_map_t> { - QHash<QString, QVariant> operator()(const QJsonValue& jv) const; + static QJsonObject dump(const variant_map_t& vh); + static QVariantHash load(const QJsonValue& jv); }; -#endif // Conditional insertion into a QJsonObject diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp index 5021c73a..96b32a92 100644 --- a/lib/csapi/account-data.cpp +++ b/lib/csapi/account-data.cpp @@ -21,6 +21,20 @@ SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, setRequestData(Data(toJson(content))); } +QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + basePath % "/user/" % userId % "/account_data/" % type); +} + +static const auto GetAccountDataJobName = QStringLiteral("GetAccountDataJob"); + +GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type) + : BaseJob(HttpVerb::Get, GetAccountDataJobName, + basePath % "/user/" % userId % "/account_data/" % type) +{ +} + static const auto SetAccountDataPerRoomJobName = QStringLiteral("SetAccountDataPerRoomJob"); SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content) @@ -30,3 +44,17 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const setRequestData(Data(toJson(content))); } +QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type); +} + +static const auto GetAccountDataPerRoomJobName = QStringLiteral("GetAccountDataPerRoomJob"); + +GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type) + : BaseJob(HttpVerb::Get, GetAccountDataPerRoomJobName, + basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type) +{ +} + diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h index f3656a14..b067618f 100644 --- a/lib/csapi/account-data.h +++ b/lib/csapi/account-data.h @@ -22,8 +22,8 @@ namespace QMatrixClient public: /*! Set some account_data for the user. * \param userId - * The id of the user to set account_data for. The access token must be - * authorized to make requests for this user id. + * The ID of the user to set account_data for. The access token must be + * authorized to make requests for this user ID. * \param type * The event type of the account_data to set. Custom types should be * namespaced to avoid clashes. @@ -33,6 +33,33 @@ namespace QMatrixClient explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {}); }; + /// Get some account_data for the user. + /// + /// Get some account_data for the client. This config is only visible to the user + /// that set the account_data. + class GetAccountDataJob : public BaseJob + { + public: + /*! Get some account_data for the user. + * \param userId + * The ID of the user to get account_data for. The access token must be + * authorized to make requests for this user ID. + * \param type + * The event type of the account_data to get. Custom types should be + * namespaced to avoid clashes. + */ + explicit GetAccountDataJob(const QString& userId, const QString& type); + + /*! Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * GetAccountDataJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type); + + }; + /// Set some account_data for the user. /// /// Set some account_data for the client on a given room. This config is only @@ -43,10 +70,10 @@ namespace QMatrixClient public: /*! Set some account_data for the user. * \param userId - * The id of the user to set account_data for. The access token must be - * authorized to make requests for this user id. + * The ID of the user to set account_data for. The access token must be + * authorized to make requests for this user ID. * \param roomId - * The id of the room to set account_data on. + * The ID of the room to set account_data on. * \param type * The event type of the account_data to set. Custom types should be * namespaced to avoid clashes. @@ -55,4 +82,33 @@ namespace QMatrixClient */ explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {}); }; + + /// Get some account_data for the user. + /// + /// Get some account_data for the client on a given room. This config is only + /// visible to the user that set the account_data. + class GetAccountDataPerRoomJob : public BaseJob + { + public: + /*! Get some account_data for the user. + * \param userId + * The ID of the user to set account_data for. The access token must be + * authorized to make requests for this user ID. + * \param roomId + * The ID of the room to get account_data for. + * \param type + * The event type of the account_data to get. Custom types should be + * namespaced to avoid clashes. + */ + explicit GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type); + + /*! Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * GetAccountDataPerRoomJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type); + + }; } // namespace QMatrixClient diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp index 6066d4d9..ce06a56d 100644 --- a/lib/csapi/admin.cpp +++ b/lib/csapi/admin.cpp @@ -16,43 +16,29 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetWhoIsJob::ConnectionInfo> + template <> struct JsonObjectConverter<GetWhoIsJob::ConnectionInfo> { - GetWhoIsJob::ConnectionInfo operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetWhoIsJob::ConnectionInfo& result) { - GetWhoIsJob::ConnectionInfo result; - result.ip = - fromJson<QString>(jo.value("ip"_ls)); - result.lastSeen = - fromJson<qint64>(jo.value("last_seen"_ls)); - result.userAgent = - fromJson<QString>(jo.value("user_agent"_ls)); - - return result; + fromJson(jo.value("ip"_ls), result.ip); + fromJson(jo.value("last_seen"_ls), result.lastSeen); + fromJson(jo.value("user_agent"_ls), result.userAgent); } }; - template <> struct FromJsonObject<GetWhoIsJob::SessionInfo> + template <> struct JsonObjectConverter<GetWhoIsJob::SessionInfo> { - GetWhoIsJob::SessionInfo operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetWhoIsJob::SessionInfo& result) { - GetWhoIsJob::SessionInfo result; - result.connections = - fromJson<QVector<GetWhoIsJob::ConnectionInfo>>(jo.value("connections"_ls)); - - return result; + fromJson(jo.value("connections"_ls), result.connections); } }; - template <> struct FromJsonObject<GetWhoIsJob::DeviceInfo> + template <> struct JsonObjectConverter<GetWhoIsJob::DeviceInfo> { - GetWhoIsJob::DeviceInfo operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetWhoIsJob::DeviceInfo& result) { - GetWhoIsJob::DeviceInfo result; - result.sessions = - fromJson<QVector<GetWhoIsJob::SessionInfo>>(jo.value("sessions"_ls)); - - return result; + fromJson(jo.value("sessions"_ls), result.sessions); } }; } // namespace QMatrixClient @@ -94,8 +80,8 @@ const QHash<QString, GetWhoIsJob::DeviceInfo>& GetWhoIsJob::devices() const BaseJob::Status GetWhoIsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->userId = fromJson<QString>(json.value("user_id"_ls)); - d->devices = fromJson<QHash<QString, DeviceInfo>>(json.value("devices"_ls)); + fromJson(json.value("user_id"_ls), d->userId); + fromJson(json.value("devices"_ls), d->devices); return Success; } diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp index f62002a6..11385dff 100644 --- a/lib/csapi/administrative_contact.cpp +++ b/lib/csapi/administrative_contact.cpp @@ -16,21 +16,14 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetAccount3PIDsJob::ThirdPartyIdentifier> + template <> struct JsonObjectConverter<GetAccount3PIDsJob::ThirdPartyIdentifier> { - GetAccount3PIDsJob::ThirdPartyIdentifier operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetAccount3PIDsJob::ThirdPartyIdentifier& result) { - GetAccount3PIDsJob::ThirdPartyIdentifier result; - result.medium = - fromJson<QString>(jo.value("medium"_ls)); - result.address = - fromJson<QString>(jo.value("address"_ls)); - result.validatedAt = - fromJson<qint64>(jo.value("validated_at"_ls)); - result.addedAt = - fromJson<qint64>(jo.value("added_at"_ls)); - - return result; + fromJson(jo.value("medium"_ls), result.medium); + fromJson(jo.value("address"_ls), result.address); + fromJson(jo.value("validated_at"_ls), result.validatedAt); + fromJson(jo.value("added_at"_ls), result.addedAt); } }; } // namespace QMatrixClient @@ -66,7 +59,7 @@ const QVector<GetAccount3PIDsJob::ThirdPartyIdentifier>& GetAccount3PIDsJob::thr BaseJob::Status GetAccount3PIDsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->threepids = fromJson<QVector<ThirdPartyIdentifier>>(json.value("threepids"_ls)); + fromJson(json.value("threepids"_ls), d->threepids); return Success; } @@ -74,19 +67,20 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const Post3PIDsJob::ThreePidCredentials& pod) + template <> struct JsonObjectConverter<Post3PIDsJob::ThreePidCredentials> { - QJsonObject jo; - addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); - addParam<>(jo, QStringLiteral("id_server"), pod.idServer); - addParam<>(jo, QStringLiteral("sid"), pod.sid); - return jo; - } + static void dumpTo(QJsonObject& jo, const Post3PIDsJob::ThreePidCredentials& pod) + { + addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); + addParam<>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<>(jo, QStringLiteral("sid"), pod.sid); + } + }; } // namespace QMatrixClient static const auto Post3PIDsJobName = QStringLiteral("Post3PIDsJob"); -Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind) +Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, Omittable<bool> bind) : BaseJob(HttpVerb::Post, Post3PIDsJobName, basePath % "/account/3pid") { @@ -139,7 +133,7 @@ const Sid& RequestTokenTo3PIDEmailJob::data() const BaseJob::Status RequestTokenTo3PIDEmailJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Sid>(data); + fromJson(data, d->data); return Success; } @@ -175,7 +169,7 @@ const Sid& RequestTokenTo3PIDMSISDNJob::data() const BaseJob::Status RequestTokenTo3PIDMSISDNJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Sid>(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h index 3fb3d44c..02aeee4d 100644 --- a/lib/csapi/administrative_contact.h +++ b/lib/csapi/administrative_contact.h @@ -113,7 +113,7 @@ namespace QMatrixClient * identifier to the account's Matrix ID with the passed identity * server. Default: ``false``. */ - explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind = false); + explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, Omittable<bool> bind = none); }; /// Deletes a third party identifier from the user's account diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp new file mode 100644 index 00000000..210423f5 --- /dev/null +++ b/lib/csapi/capabilities.cpp @@ -0,0 +1,84 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "capabilities.h" + +#include "converters.h" + +#include <QtCore/QStringBuilder> + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +namespace QMatrixClient +{ + // Converters + + template <> struct JsonObjectConverter<GetCapabilitiesJob::ChangePasswordCapability> + { + static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::ChangePasswordCapability& result) + { + fromJson(jo.value("enabled"_ls), result.enabled); + } + }; + + template <> struct JsonObjectConverter<GetCapabilitiesJob::RoomVersionsCapability> + { + static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::RoomVersionsCapability& result) + { + fromJson(jo.value("default"_ls), result.defaultVersion); + fromJson(jo.value("available"_ls), result.available); + } + }; + + template <> struct JsonObjectConverter<GetCapabilitiesJob::Capabilities> + { + static void fillFrom(QJsonObject jo, GetCapabilitiesJob::Capabilities& result) + { + fromJson(jo.take("m.change_password"_ls), result.changePassword); + fromJson(jo.take("m.room_versions"_ls), result.roomVersions); + fromJson(jo, result.additionalProperties); + } + }; +} // namespace QMatrixClient + +class GetCapabilitiesJob::Private +{ + public: + Capabilities capabilities; +}; + +QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + basePath % "/capabilities"); +} + +static const auto GetCapabilitiesJobName = QStringLiteral("GetCapabilitiesJob"); + +GetCapabilitiesJob::GetCapabilitiesJob() + : BaseJob(HttpVerb::Get, GetCapabilitiesJobName, + basePath % "/capabilities") + , d(new Private) +{ +} + +GetCapabilitiesJob::~GetCapabilitiesJob() = default; + +const GetCapabilitiesJob::Capabilities& GetCapabilitiesJob::capabilities() const +{ + return d->capabilities; +} + +BaseJob::Status GetCapabilitiesJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + if (!json.contains("capabilities"_ls)) + return { JsonParseError, + "The key 'capabilities' not found in the response" }; + fromJson(json.value("capabilities"_ls), d->capabilities); + return Success; +} + diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h new file mode 100644 index 00000000..06a8bf0d --- /dev/null +++ b/lib/csapi/capabilities.h @@ -0,0 +1,82 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +#include <QtCore/QJsonObject> +#include "converters.h" +#include <QtCore/QHash> + +namespace QMatrixClient +{ + // Operations + + /// Gets information about the server's capabilities. + /// + /// Gets information about the server's supported feature set + /// and other relevant capabilities. + class GetCapabilitiesJob : public BaseJob + { + public: + // Inner data structures + + /// Capability to indicate if the user can change their password. + struct ChangePasswordCapability + { + /// True if the user can change their password, false otherwise. + bool enabled; + }; + + /// The room versions the server supports. + struct RoomVersionsCapability + { + /// The default room version the server is using for new rooms. + QString defaultVersion; + /// A detailed description of the room versions the server supports. + QHash<QString, QString> available; + }; + + /// The custom capabilities the server supports, using the + /// Java package naming convention. + struct Capabilities + { + /// Capability to indicate if the user can change their password. + Omittable<ChangePasswordCapability> changePassword; + /// The room versions the server supports. + Omittable<RoomVersionsCapability> roomVersions; + /// The custom capabilities the server supports, using the + /// Java package naming convention. + QHash<QString, QJsonObject> additionalProperties; + }; + + // Construction/destruction + + explicit GetCapabilitiesJob(); + + /*! Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * GetCapabilitiesJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl); + + ~GetCapabilitiesJob() override; + + // Result properties + + /// The custom capabilities the server supports, using the + /// Java package naming convention. + const Capabilities& capabilities() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer<Private> d; + }; +} // namespace QMatrixClient diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index 9b590e42..22223985 100644 --- a/lib/csapi/content-repo.cpp +++ b/lib/csapi/content-repo.cpp @@ -52,7 +52,7 @@ BaseJob::Status UploadContentJob::parseJson(const QJsonDocument& data) if (!json.contains("content_uri"_ls)) return { JsonParseError, "The key 'content_uri' not found in the response" }; - d->contentUri = fromJson<QString>(json.value("content_uri"_ls)); + fromJson(json.value("content_uri"_ls), d->contentUri); return Success; } @@ -276,8 +276,8 @@ const QString& GetUrlPreviewJob::ogImage() const BaseJob::Status GetUrlPreviewJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->matrixImageSize = fromJson<qint64>(json.value("matrix:image:size"_ls)); - d->ogImage = fromJson<QString>(json.value("og:image"_ls)); + fromJson(json.value("matrix:image:size"_ls), d->matrixImageSize); + fromJson(json.value("og:image"_ls), d->ogImage); return Success; } @@ -312,7 +312,7 @@ Omittable<qint64> GetConfigJob::uploadSize() const BaseJob::Status GetConfigJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->uploadSize = fromJson<qint64>(json.value("m.upload.size"_ls)); + fromJson(json.value("m.upload.size"_ls), d->uploadSize); return Success; } diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp index 36f83727..448547ae 100644 --- a/lib/csapi/create_room.cpp +++ b/lib/csapi/create_room.cpp @@ -16,23 +16,25 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const CreateRoomJob::Invite3pid& pod) + template <> struct JsonObjectConverter<CreateRoomJob::Invite3pid> { - QJsonObject jo; - addParam<>(jo, QStringLiteral("id_server"), pod.idServer); - addParam<>(jo, QStringLiteral("medium"), pod.medium); - addParam<>(jo, QStringLiteral("address"), pod.address); - return jo; - } + static void dumpTo(QJsonObject& jo, const CreateRoomJob::Invite3pid& pod) + { + addParam<>(jo, QStringLiteral("id_server"), pod.idServer); + addParam<>(jo, QStringLiteral("medium"), pod.medium); + addParam<>(jo, QStringLiteral("address"), pod.address); + } + }; - QJsonObject toJson(const CreateRoomJob::StateEvent& pod) + template <> struct JsonObjectConverter<CreateRoomJob::StateEvent> { - QJsonObject jo; - addParam<>(jo, QStringLiteral("type"), pod.type); - addParam<IfNotEmpty>(jo, QStringLiteral("state_key"), pod.stateKey); - addParam<>(jo, QStringLiteral("content"), pod.content); - return jo; - } + static void dumpTo(QJsonObject& jo, const CreateRoomJob::StateEvent& pod) + { + addParam<>(jo, QStringLiteral("type"), pod.type); + addParam<IfNotEmpty>(jo, QStringLiteral("state_key"), pod.stateKey); + addParam<>(jo, QStringLiteral("content"), pod.content); + } + }; } // namespace QMatrixClient class CreateRoomJob::Private @@ -43,7 +45,7 @@ class CreateRoomJob::Private static const auto CreateRoomJobName = QStringLiteral("CreateRoomJob"); -CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QStringList& invite, const QVector<Invite3pid>& invite3pid, const QString& roomVersion, const QJsonObject& creationContent, const QVector<StateEvent>& initialState, const QString& preset, bool isDirect, const QJsonObject& powerLevelContentOverride) +CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QStringList& invite, const QVector<Invite3pid>& invite3pid, const QString& roomVersion, const QJsonObject& creationContent, const QVector<StateEvent>& initialState, const QString& preset, Omittable<bool> isDirect, const QJsonObject& powerLevelContentOverride) : BaseJob(HttpVerb::Post, CreateRoomJobName, basePath % "/createRoom") , d(new Private) @@ -77,7 +79,7 @@ BaseJob::Status CreateRoomJob::parseJson(const QJsonDocument& data) if (!json.contains("room_id"_ls)) return { JsonParseError, "The key 'room_id' not found in the response" }; - d->roomId = fromJson<QString>(json.value("room_id"_ls)); + fromJson(json.value("room_id"_ls), d->roomId); return Success; } diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h index a0a64df0..d7c01d00 100644 --- a/lib/csapi/create_room.h +++ b/lib/csapi/create_room.h @@ -216,7 +216,7 @@ namespace QMatrixClient * event content prior to it being sent to the room. Defaults to * overriding nothing. */ - explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QStringList& invite = {}, const QVector<Invite3pid>& invite3pid = {}, const QString& roomVersion = {}, const QJsonObject& creationContent = {}, const QVector<StateEvent>& initialState = {}, const QString& preset = {}, bool isDirect = false, const QJsonObject& powerLevelContentOverride = {}); + explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QStringList& invite = {}, const QVector<Invite3pid>& invite3pid = {}, const QString& roomVersion = {}, const QJsonObject& creationContent = {}, const QVector<StateEvent>& initialState = {}, const QString& preset = {}, Omittable<bool> isDirect = none, const QJsonObject& powerLevelContentOverride = {}); ~CreateRoomJob() override; // Result properties diff --git a/lib/csapi/definitions/auth_data.cpp b/lib/csapi/definitions/auth_data.cpp index f8639432..006b8c7e 100644 --- a/lib/csapi/definitions/auth_data.cpp +++ b/lib/csapi/definitions/auth_data.cpp @@ -6,23 +6,20 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const AuthenticationData& pod) +void JsonObjectConverter<AuthenticationData>::dumpTo( + QJsonObject& jo, const AuthenticationData& pod) { - QJsonObject jo = toJson(pod.authInfo); + fillJson(jo, pod.authInfo); addParam<>(jo, QStringLiteral("type"), pod.type); addParam<IfNotEmpty>(jo, QStringLiteral("session"), pod.session); - return jo; } -AuthenticationData FromJsonObject<AuthenticationData>::operator()(QJsonObject jo) const +void JsonObjectConverter<AuthenticationData>::fillFrom( + QJsonObject jo, AuthenticationData& result) { - AuthenticationData result; - result.type = - fromJson<QString>(jo.take("type"_ls)); - result.session = - fromJson<QString>(jo.take("session"_ls)); + fromJson(jo.take("type"_ls), result.type); + fromJson(jo.take("session"_ls), result.session); - result.authInfo = fromJson<QHash<QString, QJsonObject>>(jo); - return result; + fromJson(jo, result.authInfo); } diff --git a/lib/csapi/definitions/auth_data.h b/lib/csapi/definitions/auth_data.h index 661d3e5f..26eb205c 100644 --- a/lib/csapi/definitions/auth_data.h +++ b/lib/csapi/definitions/auth_data.h @@ -23,12 +23,10 @@ namespace QMatrixClient /// Keys dependent on the login type QHash<QString, QJsonObject> authInfo; }; - - QJsonObject toJson(const AuthenticationData& pod); - - template <> struct FromJsonObject<AuthenticationData> + template <> struct JsonObjectConverter<AuthenticationData> { - AuthenticationData operator()(QJsonObject jo) const; + static void dumpTo(QJsonObject& jo, const AuthenticationData& pod); + static void fillFrom(QJsonObject jo, AuthenticationData& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/client_device.cpp b/lib/csapi/definitions/client_device.cpp index 4a192f85..752b806a 100644 --- a/lib/csapi/definitions/client_device.cpp +++ b/lib/csapi/definitions/client_device.cpp @@ -6,28 +6,21 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const Device& pod) +void JsonObjectConverter<Device>::dumpTo( + QJsonObject& jo, const Device& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); addParam<IfNotEmpty>(jo, QStringLiteral("display_name"), pod.displayName); addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ip"), pod.lastSeenIp); addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ts"), pod.lastSeenTs); - return jo; } -Device FromJsonObject<Device>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<Device>::fillFrom( + const QJsonObject& jo, Device& result) { - Device result; - result.deviceId = - fromJson<QString>(jo.value("device_id"_ls)); - result.displayName = - fromJson<QString>(jo.value("display_name"_ls)); - result.lastSeenIp = - fromJson<QString>(jo.value("last_seen_ip"_ls)); - result.lastSeenTs = - fromJson<qint64>(jo.value("last_seen_ts"_ls)); - - return result; + fromJson(jo.value("device_id"_ls), result.deviceId); + fromJson(jo.value("display_name"_ls), result.displayName); + fromJson(jo.value("last_seen_ip"_ls), result.lastSeenIp); + fromJson(jo.value("last_seen_ts"_ls), result.lastSeenTs); } diff --git a/lib/csapi/definitions/client_device.h b/lib/csapi/definitions/client_device.h index 9f10888a..a6224f71 100644 --- a/lib/csapi/definitions/client_device.h +++ b/lib/csapi/definitions/client_device.h @@ -28,12 +28,10 @@ namespace QMatrixClient /// reasons). Omittable<qint64> lastSeenTs; }; - - QJsonObject toJson(const Device& pod); - - template <> struct FromJsonObject<Device> + template <> struct JsonObjectConverter<Device> { - Device operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const Device& pod); + static void fillFrom(const QJsonObject& jo, Device& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/device_keys.cpp b/lib/csapi/definitions/device_keys.cpp index a0e0ca42..1e79499f 100644 --- a/lib/csapi/definitions/device_keys.cpp +++ b/lib/csapi/definitions/device_keys.cpp @@ -6,31 +6,23 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const DeviceKeys& pod) +void JsonObjectConverter<DeviceKeys>::dumpTo( + QJsonObject& jo, const DeviceKeys& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("user_id"), pod.userId); addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); addParam<>(jo, QStringLiteral("algorithms"), pod.algorithms); addParam<>(jo, QStringLiteral("keys"), pod.keys); addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - return jo; } -DeviceKeys FromJsonObject<DeviceKeys>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<DeviceKeys>::fillFrom( + const QJsonObject& jo, DeviceKeys& result) { - DeviceKeys result; - result.userId = - fromJson<QString>(jo.value("user_id"_ls)); - result.deviceId = - fromJson<QString>(jo.value("device_id"_ls)); - result.algorithms = - fromJson<QStringList>(jo.value("algorithms"_ls)); - result.keys = - fromJson<QHash<QString, QString>>(jo.value("keys"_ls)); - result.signatures = - fromJson<QHash<QString, QHash<QString, QString>>>(jo.value("signatures"_ls)); - - return result; + fromJson(jo.value("user_id"_ls), result.userId); + fromJson(jo.value("device_id"_ls), result.deviceId); + fromJson(jo.value("algorithms"_ls), result.algorithms); + fromJson(jo.value("keys"_ls), result.keys); + fromJson(jo.value("signatures"_ls), result.signatures); } diff --git a/lib/csapi/definitions/device_keys.h b/lib/csapi/definitions/device_keys.h index 6023e7e8..8ebe1125 100644 --- a/lib/csapi/definitions/device_keys.h +++ b/lib/csapi/definitions/device_keys.h @@ -34,12 +34,10 @@ namespace QMatrixClient /// JSON`_. QHash<QString, QHash<QString, QString>> signatures; }; - - QJsonObject toJson(const DeviceKeys& pod); - - template <> struct FromJsonObject<DeviceKeys> + template <> struct JsonObjectConverter<DeviceKeys> { - DeviceKeys operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const DeviceKeys& pod); + static void fillFrom(const QJsonObject& jo, DeviceKeys& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/event_filter.cpp b/lib/csapi/definitions/event_filter.cpp index cc444db0..b20d7807 100644 --- a/lib/csapi/definitions/event_filter.cpp +++ b/lib/csapi/definitions/event_filter.cpp @@ -6,31 +6,23 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const EventFilter& pod) +void JsonObjectConverter<EventFilter>::dumpTo( + QJsonObject& jo, const EventFilter& pod) { - QJsonObject jo; addParam<IfNotEmpty>(jo, QStringLiteral("limit"), pod.limit); addParam<IfNotEmpty>(jo, QStringLiteral("not_senders"), pod.notSenders); addParam<IfNotEmpty>(jo, QStringLiteral("not_types"), pod.notTypes); addParam<IfNotEmpty>(jo, QStringLiteral("senders"), pod.senders); addParam<IfNotEmpty>(jo, QStringLiteral("types"), pod.types); - return jo; } -EventFilter FromJsonObject<EventFilter>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<EventFilter>::fillFrom( + const QJsonObject& jo, EventFilter& result) { - EventFilter result; - result.limit = - fromJson<int>(jo.value("limit"_ls)); - result.notSenders = - fromJson<QStringList>(jo.value("not_senders"_ls)); - result.notTypes = - fromJson<QStringList>(jo.value("not_types"_ls)); - result.senders = - fromJson<QStringList>(jo.value("senders"_ls)); - result.types = - fromJson<QStringList>(jo.value("types"_ls)); - - return result; + fromJson(jo.value("limit"_ls), result.limit); + fromJson(jo.value("not_senders"_ls), result.notSenders); + fromJson(jo.value("not_types"_ls), result.notTypes); + fromJson(jo.value("senders"_ls), result.senders); + fromJson(jo.value("types"_ls), result.types); } diff --git a/lib/csapi/definitions/event_filter.h b/lib/csapi/definitions/event_filter.h index 5c6a5b27..6de1fe79 100644 --- a/lib/csapi/definitions/event_filter.h +++ b/lib/csapi/definitions/event_filter.h @@ -25,12 +25,10 @@ namespace QMatrixClient /// A list of event types to include. If this list is absent then all event types are included. A ``'*'`` can be used as a wildcard to match any sequence of characters. QStringList types; }; - - QJsonObject toJson(const EventFilter& pod); - - template <> struct FromJsonObject<EventFilter> + template <> struct JsonObjectConverter<EventFilter> { - EventFilter operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const EventFilter& pod); + static void fillFrom(const QJsonObject& jo, EventFilter& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/public_rooms_response.cpp b/lib/csapi/definitions/public_rooms_response.cpp index 2f52501d..0d26662c 100644 --- a/lib/csapi/definitions/public_rooms_response.cpp +++ b/lib/csapi/definitions/public_rooms_response.cpp @@ -6,9 +6,9 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const PublicRoomsChunk& pod) +void JsonObjectConverter<PublicRoomsChunk>::dumpTo( + QJsonObject& jo, const PublicRoomsChunk& pod) { - QJsonObject jo; addParam<IfNotEmpty>(jo, QStringLiteral("aliases"), pod.aliases); addParam<IfNotEmpty>(jo, QStringLiteral("canonical_alias"), pod.canonicalAlias); addParam<IfNotEmpty>(jo, QStringLiteral("name"), pod.name); @@ -18,56 +18,37 @@ QJsonObject QMatrixClient::toJson(const PublicRoomsChunk& pod) addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); addParam<IfNotEmpty>(jo, QStringLiteral("avatar_url"), pod.avatarUrl); - return jo; } -PublicRoomsChunk FromJsonObject<PublicRoomsChunk>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<PublicRoomsChunk>::fillFrom( + const QJsonObject& jo, PublicRoomsChunk& result) { - PublicRoomsChunk result; - result.aliases = - fromJson<QStringList>(jo.value("aliases"_ls)); - result.canonicalAlias = - fromJson<QString>(jo.value("canonical_alias"_ls)); - result.name = - fromJson<QString>(jo.value("name"_ls)); - result.numJoinedMembers = - fromJson<int>(jo.value("num_joined_members"_ls)); - result.roomId = - fromJson<QString>(jo.value("room_id"_ls)); - result.topic = - fromJson<QString>(jo.value("topic"_ls)); - result.worldReadable = - fromJson<bool>(jo.value("world_readable"_ls)); - result.guestCanJoin = - fromJson<bool>(jo.value("guest_can_join"_ls)); - result.avatarUrl = - fromJson<QString>(jo.value("avatar_url"_ls)); - - return result; + fromJson(jo.value("aliases"_ls), result.aliases); + fromJson(jo.value("canonical_alias"_ls), result.canonicalAlias); + fromJson(jo.value("name"_ls), result.name); + fromJson(jo.value("num_joined_members"_ls), result.numJoinedMembers); + fromJson(jo.value("room_id"_ls), result.roomId); + fromJson(jo.value("topic"_ls), result.topic); + fromJson(jo.value("world_readable"_ls), result.worldReadable); + fromJson(jo.value("guest_can_join"_ls), result.guestCanJoin); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } -QJsonObject QMatrixClient::toJson(const PublicRoomsResponse& pod) +void JsonObjectConverter<PublicRoomsResponse>::dumpTo( + QJsonObject& jo, const PublicRoomsResponse& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("chunk"), pod.chunk); addParam<IfNotEmpty>(jo, QStringLiteral("next_batch"), pod.nextBatch); addParam<IfNotEmpty>(jo, QStringLiteral("prev_batch"), pod.prevBatch); addParam<IfNotEmpty>(jo, QStringLiteral("total_room_count_estimate"), pod.totalRoomCountEstimate); - return jo; } -PublicRoomsResponse FromJsonObject<PublicRoomsResponse>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<PublicRoomsResponse>::fillFrom( + const QJsonObject& jo, PublicRoomsResponse& result) { - PublicRoomsResponse result; - result.chunk = - fromJson<QVector<PublicRoomsChunk>>(jo.value("chunk"_ls)); - result.nextBatch = - fromJson<QString>(jo.value("next_batch"_ls)); - result.prevBatch = - fromJson<QString>(jo.value("prev_batch"_ls)); - result.totalRoomCountEstimate = - fromJson<int>(jo.value("total_room_count_estimate"_ls)); - - return result; + fromJson(jo.value("chunk"_ls), result.chunk); + fromJson(jo.value("next_batch"_ls), result.nextBatch); + fromJson(jo.value("prev_batch"_ls), result.prevBatch); + fromJson(jo.value("total_room_count_estimate"_ls), result.totalRoomCountEstimate); } diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h index 88c805ba..4c54ac25 100644 --- a/lib/csapi/definitions/public_rooms_response.h +++ b/lib/csapi/definitions/public_rooms_response.h @@ -36,12 +36,10 @@ namespace QMatrixClient /// The URL for the room's avatar, if one is set. QString avatarUrl; }; - - QJsonObject toJson(const PublicRoomsChunk& pod); - - template <> struct FromJsonObject<PublicRoomsChunk> + template <> struct JsonObjectConverter<PublicRoomsChunk> { - PublicRoomsChunk operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod); + static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod); }; /// A list of the rooms on the server. @@ -61,12 +59,10 @@ namespace QMatrixClient /// server has an estimate. Omittable<int> totalRoomCountEstimate; }; - - QJsonObject toJson(const PublicRoomsResponse& pod); - - template <> struct FromJsonObject<PublicRoomsResponse> + template <> struct JsonObjectConverter<PublicRoomsResponse> { - PublicRoomsResponse operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PublicRoomsResponse& pod); + static void fillFrom(const QJsonObject& jo, PublicRoomsResponse& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/push_condition.cpp b/lib/csapi/definitions/push_condition.cpp index 045094bc..ace02755 100644 --- a/lib/csapi/definitions/push_condition.cpp +++ b/lib/csapi/definitions/push_condition.cpp @@ -6,28 +6,21 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const PushCondition& pod) +void JsonObjectConverter<PushCondition>::dumpTo( + QJsonObject& jo, const PushCondition& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("kind"), pod.kind); addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern); addParam<IfNotEmpty>(jo, QStringLiteral("is"), pod.is); - return jo; } -PushCondition FromJsonObject<PushCondition>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<PushCondition>::fillFrom( + const QJsonObject& jo, PushCondition& result) { - PushCondition result; - result.kind = - fromJson<QString>(jo.value("kind"_ls)); - result.key = - fromJson<QString>(jo.value("key"_ls)); - result.pattern = - fromJson<QString>(jo.value("pattern"_ls)); - result.is = - fromJson<QString>(jo.value("is"_ls)); - - return result; + fromJson(jo.value("kind"_ls), result.kind); + fromJson(jo.value("key"_ls), result.key); + fromJson(jo.value("pattern"_ls), result.pattern); + fromJson(jo.value("is"_ls), result.is); } diff --git a/lib/csapi/definitions/push_condition.h b/lib/csapi/definitions/push_condition.h index defcebb3..e45526d2 100644 --- a/lib/csapi/definitions/push_condition.h +++ b/lib/csapi/definitions/push_condition.h @@ -28,12 +28,10 @@ namespace QMatrixClient /// so forth. If no prefix is present, this parameter defaults to ==. QString is; }; - - QJsonObject toJson(const PushCondition& pod); - - template <> struct FromJsonObject<PushCondition> + template <> struct JsonObjectConverter<PushCondition> { - PushCondition operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PushCondition& pod); + static void fillFrom(const QJsonObject& jo, PushCondition& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/push_rule.cpp b/lib/csapi/definitions/push_rule.cpp index baddd187..abbb04b5 100644 --- a/lib/csapi/definitions/push_rule.cpp +++ b/lib/csapi/definitions/push_rule.cpp @@ -6,34 +6,25 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const PushRule& pod) +void JsonObjectConverter<PushRule>::dumpTo( + QJsonObject& jo, const PushRule& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("actions"), pod.actions); addParam<>(jo, QStringLiteral("default"), pod.isDefault); addParam<>(jo, QStringLiteral("enabled"), pod.enabled); addParam<>(jo, QStringLiteral("rule_id"), pod.ruleId); addParam<IfNotEmpty>(jo, QStringLiteral("conditions"), pod.conditions); addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern); - return jo; } -PushRule FromJsonObject<PushRule>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<PushRule>::fillFrom( + const QJsonObject& jo, PushRule& result) { - PushRule result; - result.actions = - fromJson<QVector<QVariant>>(jo.value("actions"_ls)); - result.isDefault = - fromJson<bool>(jo.value("default"_ls)); - result.enabled = - fromJson<bool>(jo.value("enabled"_ls)); - result.ruleId = - fromJson<QString>(jo.value("rule_id"_ls)); - result.conditions = - fromJson<QVector<PushCondition>>(jo.value("conditions"_ls)); - result.pattern = - fromJson<QString>(jo.value("pattern"_ls)); - - return result; + fromJson(jo.value("actions"_ls), result.actions); + fromJson(jo.value("default"_ls), result.isDefault); + fromJson(jo.value("enabled"_ls), result.enabled); + fromJson(jo.value("rule_id"_ls), result.ruleId); + fromJson(jo.value("conditions"_ls), result.conditions); + fromJson(jo.value("pattern"_ls), result.pattern); } diff --git a/lib/csapi/definitions/push_rule.h b/lib/csapi/definitions/push_rule.h index 5f52876d..bea13e96 100644 --- a/lib/csapi/definitions/push_rule.h +++ b/lib/csapi/definitions/push_rule.h @@ -7,10 +7,10 @@ #include "converters.h" #include "csapi/definitions/push_condition.h" -#include "converters.h" +#include <QtCore/QJsonObject> #include <QtCore/QVector> #include <QtCore/QVariant> -#include <QtCore/QJsonObject> +#include "converters.h" namespace QMatrixClient { @@ -34,12 +34,10 @@ namespace QMatrixClient /// rules. QString pattern; }; - - QJsonObject toJson(const PushRule& pod); - - template <> struct FromJsonObject<PushRule> + template <> struct JsonObjectConverter<PushRule> { - PushRule operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PushRule& pod); + static void fillFrom(const QJsonObject& jo, PushRule& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/push_ruleset.cpp b/lib/csapi/definitions/push_ruleset.cpp index 14b7a4b6..f1bad882 100644 --- a/lib/csapi/definitions/push_ruleset.cpp +++ b/lib/csapi/definitions/push_ruleset.cpp @@ -6,31 +6,23 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const PushRuleset& pod) +void JsonObjectConverter<PushRuleset>::dumpTo( + QJsonObject& jo, const PushRuleset& pod) { - QJsonObject jo; addParam<IfNotEmpty>(jo, QStringLiteral("content"), pod.content); addParam<IfNotEmpty>(jo, QStringLiteral("override"), pod.override); addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room); addParam<IfNotEmpty>(jo, QStringLiteral("sender"), pod.sender); addParam<IfNotEmpty>(jo, QStringLiteral("underride"), pod.underride); - return jo; } -PushRuleset FromJsonObject<PushRuleset>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<PushRuleset>::fillFrom( + const QJsonObject& jo, PushRuleset& result) { - PushRuleset result; - result.content = - fromJson<QVector<PushRule>>(jo.value("content"_ls)); - result.override = - fromJson<QVector<PushRule>>(jo.value("override"_ls)); - result.room = - fromJson<QVector<PushRule>>(jo.value("room"_ls)); - result.sender = - fromJson<QVector<PushRule>>(jo.value("sender"_ls)); - result.underride = - fromJson<QVector<PushRule>>(jo.value("underride"_ls)); - - return result; + fromJson(jo.value("content"_ls), result.content); + fromJson(jo.value("override"_ls), result.override); + fromJson(jo.value("room"_ls), result.room); + fromJson(jo.value("sender"_ls), result.sender); + fromJson(jo.value("underride"_ls), result.underride); } diff --git a/lib/csapi/definitions/push_ruleset.h b/lib/csapi/definitions/push_ruleset.h index a274b72a..f2d937c0 100644 --- a/lib/csapi/definitions/push_ruleset.h +++ b/lib/csapi/definitions/push_ruleset.h @@ -22,12 +22,10 @@ namespace QMatrixClient QVector<PushRule> sender; QVector<PushRule> underride; }; - - QJsonObject toJson(const PushRuleset& pod); - - template <> struct FromJsonObject<PushRuleset> + template <> struct JsonObjectConverter<PushRuleset> { - PushRuleset operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const PushRuleset& pod); + static void fillFrom(const QJsonObject& jo, PushRuleset& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/room_event_filter.cpp b/lib/csapi/definitions/room_event_filter.cpp index f6f1e5cb..df92e684 100644 --- a/lib/csapi/definitions/room_event_filter.cpp +++ b/lib/csapi/definitions/room_event_filter.cpp @@ -6,25 +6,21 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const RoomEventFilter& pod) +void JsonObjectConverter<RoomEventFilter>::dumpTo( + QJsonObject& jo, const RoomEventFilter& pod) { - QJsonObject jo; + fillJson<EventFilter>(jo, pod); addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms); addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms); addParam<IfNotEmpty>(jo, QStringLiteral("contains_url"), pod.containsUrl); - return jo; } -RoomEventFilter FromJsonObject<RoomEventFilter>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<RoomEventFilter>::fillFrom( + const QJsonObject& jo, RoomEventFilter& result) { - RoomEventFilter result; - result.notRooms = - fromJson<QStringList>(jo.value("not_rooms"_ls)); - result.rooms = - fromJson<QStringList>(jo.value("rooms"_ls)); - result.containsUrl = - fromJson<bool>(jo.value("contains_url"_ls)); - - return result; + fillFromJson<EventFilter>(jo, result); + fromJson(jo.value("not_rooms"_ls), result.notRooms); + fromJson(jo.value("rooms"_ls), result.rooms); + fromJson(jo.value("contains_url"_ls), result.containsUrl); } diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h index 697fe661..6eb9a390 100644 --- a/lib/csapi/definitions/room_event_filter.h +++ b/lib/csapi/definitions/room_event_filter.h @@ -19,15 +19,13 @@ namespace QMatrixClient QStringList notRooms; /// A list of room IDs to include. If this list is absent then all rooms are included. QStringList rooms; - /// If ``true``, includes only events with a ``url`` key in their content. If ``false``, excludes those events. Defaults to ``false``. - bool containsUrl; + /// If ``true``, includes only events with a ``url`` key in their content. If ``false``, excludes those events. If omitted, ``url`` key is not considered for filtering. + Omittable<bool> containsUrl; }; - - QJsonObject toJson(const RoomEventFilter& pod); - - template <> struct FromJsonObject<RoomEventFilter> + template <> struct JsonObjectConverter<RoomEventFilter> { - RoomEventFilter operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const RoomEventFilter& pod); + static void fillFrom(const QJsonObject& jo, RoomEventFilter& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/sync_filter.cpp b/lib/csapi/definitions/sync_filter.cpp index bd87804c..32752d1f 100644 --- a/lib/csapi/definitions/sync_filter.cpp +++ b/lib/csapi/definitions/sync_filter.cpp @@ -6,9 +6,25 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const RoomFilter& pod) +void JsonObjectConverter<StateFilter>::dumpTo( + QJsonObject& jo, const StateFilter& pod) +{ + fillJson<RoomEventFilter>(jo, pod); + addParam<IfNotEmpty>(jo, QStringLiteral("lazy_load_members"), pod.lazyLoadMembers); + addParam<IfNotEmpty>(jo, QStringLiteral("include_redundant_members"), pod.includeRedundantMembers); +} + +void JsonObjectConverter<StateFilter>::fillFrom( + const QJsonObject& jo, StateFilter& result) +{ + fillFromJson<RoomEventFilter>(jo, result); + fromJson(jo.value("lazy_load_members"_ls), result.lazyLoadMembers); + fromJson(jo.value("include_redundant_members"_ls), result.includeRedundantMembers); +} + +void JsonObjectConverter<RoomFilter>::dumpTo( + QJsonObject& jo, const RoomFilter& pod) { - QJsonObject jo; addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms); addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms); addParam<IfNotEmpty>(jo, QStringLiteral("ephemeral"), pod.ephemeral); @@ -16,55 +32,37 @@ QJsonObject QMatrixClient::toJson(const RoomFilter& pod) addParam<IfNotEmpty>(jo, QStringLiteral("state"), pod.state); addParam<IfNotEmpty>(jo, QStringLiteral("timeline"), pod.timeline); addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), pod.accountData); - return jo; } -RoomFilter FromJsonObject<RoomFilter>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<RoomFilter>::fillFrom( + const QJsonObject& jo, RoomFilter& result) { - RoomFilter result; - result.notRooms = - fromJson<QStringList>(jo.value("not_rooms"_ls)); - result.rooms = - fromJson<QStringList>(jo.value("rooms"_ls)); - result.ephemeral = - fromJson<RoomEventFilter>(jo.value("ephemeral"_ls)); - result.includeLeave = - fromJson<bool>(jo.value("include_leave"_ls)); - result.state = - fromJson<RoomEventFilter>(jo.value("state"_ls)); - result.timeline = - fromJson<RoomEventFilter>(jo.value("timeline"_ls)); - result.accountData = - fromJson<RoomEventFilter>(jo.value("account_data"_ls)); - - return result; + fromJson(jo.value("not_rooms"_ls), result.notRooms); + fromJson(jo.value("rooms"_ls), result.rooms); + fromJson(jo.value("ephemeral"_ls), result.ephemeral); + fromJson(jo.value("include_leave"_ls), result.includeLeave); + fromJson(jo.value("state"_ls), result.state); + fromJson(jo.value("timeline"_ls), result.timeline); + fromJson(jo.value("account_data"_ls), result.accountData); } -QJsonObject QMatrixClient::toJson(const Filter& pod) +void JsonObjectConverter<Filter>::dumpTo( + QJsonObject& jo, const Filter& pod) { - QJsonObject jo; addParam<IfNotEmpty>(jo, QStringLiteral("event_fields"), pod.eventFields); addParam<IfNotEmpty>(jo, QStringLiteral("event_format"), pod.eventFormat); addParam<IfNotEmpty>(jo, QStringLiteral("presence"), pod.presence); addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), pod.accountData); addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room); - return jo; } -Filter FromJsonObject<Filter>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<Filter>::fillFrom( + const QJsonObject& jo, Filter& result) { - Filter result; - result.eventFields = - fromJson<QStringList>(jo.value("event_fields"_ls)); - result.eventFormat = - fromJson<QString>(jo.value("event_format"_ls)); - result.presence = - fromJson<EventFilter>(jo.value("presence"_ls)); - result.accountData = - fromJson<EventFilter>(jo.value("account_data"_ls)); - result.room = - fromJson<RoomFilter>(jo.value("room"_ls)); - - return result; + fromJson(jo.value("event_fields"_ls), result.eventFields); + fromJson(jo.value("event_format"_ls), result.eventFormat); + fromJson(jo.value("presence"_ls), result.presence); + fromJson(jo.value("account_data"_ls), result.accountData); + fromJson(jo.value("room"_ls), result.room); } diff --git a/lib/csapi/definitions/sync_filter.h b/lib/csapi/definitions/sync_filter.h index ca275a9a..d94c74d7 100644 --- a/lib/csapi/definitions/sync_filter.h +++ b/lib/csapi/definitions/sync_filter.h @@ -14,6 +14,37 @@ namespace QMatrixClient { // Data structures + /// The state events to include for rooms. + struct StateFilter : RoomEventFilter + { + /// If ``true``, the only ``m.room.member`` events returned in + /// the ``state`` section of the ``/sync`` response are those + /// which are definitely necessary for a client to display + /// the ``sender`` of the timeline events in that response. + /// If ``false``, ``m.room.member`` events are not filtered. + /// By default, servers should suppress duplicate redundant + /// lazy-loaded ``m.room.member`` events from being sent to a given + /// client across multiple calls to ``/sync``, given that most clients + /// cache membership events (see ``include_redundant_members`` + /// to change this behaviour). + Omittable<bool> lazyLoadMembers; + /// If ``true``, the ``state`` section of the ``/sync`` response will + /// always contain the ``m.room.member`` events required to display + /// the ``sender`` of the timeline events in that response, assuming + /// ``lazy_load_members`` is enabled. This means that redundant + /// duplicate member events may be returned across multiple calls to + /// ``/sync``. This is useful for naive clients who never track + /// membership data. If ``false``, duplicate ``m.room.member`` events + /// may be suppressed by the server across multiple calls to ``/sync``. + /// If ``lazy_load_members`` is ``false`` this field is ignored. + Omittable<bool> includeRedundantMembers; + }; + template <> struct JsonObjectConverter<StateFilter> + { + static void dumpTo(QJsonObject& jo, const StateFilter& pod); + static void fillFrom(const QJsonObject& jo, StateFilter& pod); + }; + /// Filters to be applied to room data. struct RoomFilter { @@ -24,20 +55,18 @@ namespace QMatrixClient /// The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms. Omittable<RoomEventFilter> ephemeral; /// Include rooms that the user has left in the sync, default false - bool includeLeave; + Omittable<bool> includeLeave; /// The state events to include for rooms. - Omittable<RoomEventFilter> state; + Omittable<StateFilter> state; /// The message and state update events to include for rooms. Omittable<RoomEventFilter> timeline; /// The per user account data to include for rooms. Omittable<RoomEventFilter> accountData; }; - - QJsonObject toJson(const RoomFilter& pod); - - template <> struct FromJsonObject<RoomFilter> + template <> struct JsonObjectConverter<RoomFilter> { - RoomFilter operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const RoomFilter& pod); + static void fillFrom(const QJsonObject& jo, RoomFilter& pod); }; struct Filter @@ -53,12 +82,10 @@ namespace QMatrixClient /// Filters to be applied to room data. Omittable<RoomFilter> room; }; - - QJsonObject toJson(const Filter& pod); - - template <> struct FromJsonObject<Filter> + template <> struct JsonObjectConverter<Filter> { - Filter operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const Filter& pod); + static void fillFrom(const QJsonObject& jo, Filter& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/user_identifier.cpp b/lib/csapi/definitions/user_identifier.cpp index 80a6d450..05a27c1c 100644 --- a/lib/csapi/definitions/user_identifier.cpp +++ b/lib/csapi/definitions/user_identifier.cpp @@ -6,20 +6,18 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const UserIdentifier& pod) +void JsonObjectConverter<UserIdentifier>::dumpTo( + QJsonObject& jo, const UserIdentifier& pod) { - QJsonObject jo = toJson(pod.additionalProperties); + fillJson(jo, pod.additionalProperties); addParam<>(jo, QStringLiteral("type"), pod.type); - return jo; } -UserIdentifier FromJsonObject<UserIdentifier>::operator()(QJsonObject jo) const +void JsonObjectConverter<UserIdentifier>::fillFrom( + QJsonObject jo, UserIdentifier& result) { - UserIdentifier result; - result.type = - fromJson<QString>(jo.take("type"_ls)); + fromJson(jo.take("type"_ls), result.type); - result.additionalProperties = fromJson<QVariantHash>(jo); - return result; + fromJson(jo, result.additionalProperties); } diff --git a/lib/csapi/definitions/user_identifier.h b/lib/csapi/definitions/user_identifier.h index 42614436..cbb1550f 100644 --- a/lib/csapi/definitions/user_identifier.h +++ b/lib/csapi/definitions/user_identifier.h @@ -20,12 +20,10 @@ namespace QMatrixClient /// Identification information for a user QVariantHash additionalProperties; }; - - QJsonObject toJson(const UserIdentifier& pod); - - template <> struct FromJsonObject<UserIdentifier> + template <> struct JsonObjectConverter<UserIdentifier> { - UserIdentifier operator()(QJsonObject jo) const; + static void dumpTo(QJsonObject& jo, const UserIdentifier& pod); + static void fillFrom(QJsonObject jo, UserIdentifier& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/wellknown/full.cpp b/lib/csapi/definitions/wellknown/full.cpp new file mode 100644 index 00000000..5ecef34f --- /dev/null +++ b/lib/csapi/definitions/wellknown/full.cpp @@ -0,0 +1,25 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "full.h" + +using namespace QMatrixClient; + +void JsonObjectConverter<DiscoveryInformation>::dumpTo( + QJsonObject& jo, const DiscoveryInformation& pod) +{ + fillJson(jo, pod.additionalProperties); + addParam<>(jo, QStringLiteral("m.homeserver"), pod.homeserver); + addParam<IfNotEmpty>(jo, QStringLiteral("m.identity_server"), pod.identityServer); +} + +void JsonObjectConverter<DiscoveryInformation>::fillFrom( + QJsonObject jo, DiscoveryInformation& result) +{ + fromJson(jo.take("m.homeserver"_ls), result.homeserver); + fromJson(jo.take("m.identity_server"_ls), result.identityServer); + + fromJson(jo, result.additionalProperties); +} + diff --git a/lib/csapi/definitions/wellknown/full.h b/lib/csapi/definitions/wellknown/full.h new file mode 100644 index 00000000..d9346acb --- /dev/null +++ b/lib/csapi/definitions/wellknown/full.h @@ -0,0 +1,38 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "converters.h" + +#include <QtCore/QJsonObject> +#include "converters.h" +#include "csapi/definitions/wellknown/homeserver.h" +#include "csapi/definitions/wellknown/identity_server.h" +#include <QtCore/QHash> + +namespace QMatrixClient +{ + // Data structures + + /// Used by clients to determine the homeserver, identity server, and other + /// optional components they should be interacting with. + struct DiscoveryInformation + { + /// Used by clients to determine the homeserver, identity server, and other + /// optional components they should be interacting with. + HomeserverInformation homeserver; + /// Used by clients to determine the homeserver, identity server, and other + /// optional components they should be interacting with. + Omittable<IdentityServerInformation> identityServer; + /// Application-dependent keys using Java package naming convention. + QHash<QString, QJsonObject> additionalProperties; + }; + template <> struct JsonObjectConverter<DiscoveryInformation> + { + static void dumpTo(QJsonObject& jo, const DiscoveryInformation& pod); + static void fillFrom(QJsonObject jo, DiscoveryInformation& pod); + }; + +} // namespace QMatrixClient diff --git a/lib/csapi/definitions/wellknown/homeserver.cpp b/lib/csapi/definitions/wellknown/homeserver.cpp index f1482ee4..0783f11b 100644 --- a/lib/csapi/definitions/wellknown/homeserver.cpp +++ b/lib/csapi/definitions/wellknown/homeserver.cpp @@ -6,19 +6,15 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const HomeserverInformation& pod) +void JsonObjectConverter<HomeserverInformation>::dumpTo( + QJsonObject& jo, const HomeserverInformation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); - return jo; } -HomeserverInformation FromJsonObject<HomeserverInformation>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<HomeserverInformation>::fillFrom( + const QJsonObject& jo, HomeserverInformation& result) { - HomeserverInformation result; - result.baseUrl = - fromJson<QString>(jo.value("base_url"_ls)); - - return result; + fromJson(jo.value("base_url"_ls), result.baseUrl); } diff --git a/lib/csapi/definitions/wellknown/homeserver.h b/lib/csapi/definitions/wellknown/homeserver.h index 09d6ba63..f6761c30 100644 --- a/lib/csapi/definitions/wellknown/homeserver.h +++ b/lib/csapi/definitions/wellknown/homeserver.h @@ -17,12 +17,10 @@ namespace QMatrixClient /// The base URL for the homeserver for client-server connections. QString baseUrl; }; - - QJsonObject toJson(const HomeserverInformation& pod); - - template <> struct FromJsonObject<HomeserverInformation> + template <> struct JsonObjectConverter<HomeserverInformation> { - HomeserverInformation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const HomeserverInformation& pod); + static void fillFrom(const QJsonObject& jo, HomeserverInformation& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/definitions/wellknown/identity_server.cpp b/lib/csapi/definitions/wellknown/identity_server.cpp index f9d7bc37..99f36641 100644 --- a/lib/csapi/definitions/wellknown/identity_server.cpp +++ b/lib/csapi/definitions/wellknown/identity_server.cpp @@ -6,19 +6,15 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const IdentityServerInformation& pod) +void JsonObjectConverter<IdentityServerInformation>::dumpTo( + QJsonObject& jo, const IdentityServerInformation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); - return jo; } -IdentityServerInformation FromJsonObject<IdentityServerInformation>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<IdentityServerInformation>::fillFrom( + const QJsonObject& jo, IdentityServerInformation& result) { - IdentityServerInformation result; - result.baseUrl = - fromJson<QString>(jo.value("base_url"_ls)); - - return result; + fromJson(jo.value("base_url"_ls), result.baseUrl); } diff --git a/lib/csapi/definitions/wellknown/identity_server.h b/lib/csapi/definitions/wellknown/identity_server.h index cb8ffcee..67d8b08d 100644 --- a/lib/csapi/definitions/wellknown/identity_server.h +++ b/lib/csapi/definitions/wellknown/identity_server.h @@ -17,12 +17,10 @@ namespace QMatrixClient /// The base URL for the identity server for client-server connections. QString baseUrl; }; - - QJsonObject toJson(const IdentityServerInformation& pod); - - template <> struct FromJsonObject<IdentityServerInformation> + template <> struct JsonObjectConverter<IdentityServerInformation> { - IdentityServerInformation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const IdentityServerInformation& pod); + static void fillFrom(const QJsonObject& jo, IdentityServerInformation& pod); }; } // namespace QMatrixClient diff --git a/lib/csapi/device_management.cpp b/lib/csapi/device_management.cpp index 861e1994..9c31db5d 100644 --- a/lib/csapi/device_management.cpp +++ b/lib/csapi/device_management.cpp @@ -43,7 +43,7 @@ const QVector<Device>& GetDevicesJob::devices() const BaseJob::Status GetDevicesJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->devices = fromJson<QVector<Device>>(json.value("devices"_ls)); + fromJson(json.value("devices"_ls), d->devices); return Success; } @@ -77,7 +77,7 @@ const Device& GetDeviceJob::data() const BaseJob::Status GetDeviceJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Device>(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp index 5353f3bc..4af86f7b 100644 --- a/lib/csapi/directory.cpp +++ b/lib/csapi/directory.cpp @@ -60,8 +60,8 @@ const QStringList& GetRoomIdByAliasJob::servers() const BaseJob::Status GetRoomIdByAliasJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->roomId = fromJson<QString>(json.value("room_id"_ls)); - d->servers = fromJson<QStringList>(json.value("servers"_ls)); + fromJson(json.value("room_id"_ls), d->roomId); + fromJson(json.value("servers"_ls), d->servers); return Success; } diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp index 806c1613..bb1f5301 100644 --- a/lib/csapi/event_context.cpp +++ b/lib/csapi/event_context.cpp @@ -82,12 +82,12 @@ StateEvents&& GetEventContextJob::state() BaseJob::Status GetEventContextJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->begin = fromJson<QString>(json.value("start"_ls)); - d->end = fromJson<QString>(json.value("end"_ls)); - d->eventsBefore = fromJson<RoomEvents>(json.value("events_before"_ls)); - d->event = fromJson<RoomEventPtr>(json.value("event"_ls)); - d->eventsAfter = fromJson<RoomEvents>(json.value("events_after"_ls)); - d->state = fromJson<StateEvents>(json.value("state"_ls)); + fromJson(json.value("start"_ls), d->begin); + fromJson(json.value("end"_ls), d->end); + fromJson(json.value("events_before"_ls), d->eventsBefore); + fromJson(json.value("event"_ls), d->event); + fromJson(json.value("events_after"_ls), d->eventsAfter); + fromJson(json.value("state"_ls), d->state); return Success; } diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp index 77dc9b92..982e60b5 100644 --- a/lib/csapi/filter.cpp +++ b/lib/csapi/filter.cpp @@ -41,7 +41,7 @@ BaseJob::Status DefineFilterJob::parseJson(const QJsonDocument& data) if (!json.contains("filter_id"_ls)) return { JsonParseError, "The key 'filter_id' not found in the response" }; - d->filterId = fromJson<QString>(json.value("filter_id"_ls)); + fromJson(json.value("filter_id"_ls), d->filterId); return Success; } @@ -75,7 +75,7 @@ const Filter& GetFilterJob::data() const BaseJob::Status GetFilterJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Filter>(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/gtad.yaml b/lib/csapi/gtad.yaml index cb5e553c..a44f803a 100644 --- a/lib/csapi/gtad.yaml +++ b/lib/csapi/gtad.yaml @@ -5,12 +5,15 @@ analyzer: identifiers: signed: signedData unsigned: unsignedData - default: isDefault + PushRule/default: isDefault + default: defaultVersion # getCapabilities/RoomVersionsCapability origin_server_ts: originServerTimestamp # Instead of originServerTs start: begin # Because start() is a method in BaseJob m.upload.size: uploadSize m.homeserver: homeserver m.identity_server: identityServer + m.change_password: changePassword + m.room_versions: roomVersions AuthenticationData/additionalProperties: authInfo # Structure inside `types`: @@ -38,7 +41,7 @@ analyzer: - number: - float: float - //: double - - boolean: { type: bool, omittedValue: 'false' } + - boolean: bool - string: - byte: &ByteStream type: QIODevice* @@ -86,12 +89,9 @@ analyzer: - /m\.room\.member$/: type: "EventsArray<RoomMemberEvent>" imports: '"events/roommemberevent.h"' - - /state_event.yaml$/: - type: StateEvents - - /room_event.yaml$/: - type: RoomEvents - - /event.yaml$/: - type: Events + - /state_event.yaml$/: StateEvents + - /room_event.yaml$/: RoomEvents + - /event.yaml$/: Events - //: { type: "QVector<{{1}}>", imports: <QtCore/QVector> } - map: # `additionalProperties` in OpenAPI - RoomState: diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp index 71781154..00d930fa 100644 --- a/lib/csapi/joining.cpp +++ b/lib/csapi/joining.cpp @@ -16,15 +16,16 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const JoinRoomByIdJob::ThirdPartySigned& pod) + template <> struct JsonObjectConverter<JoinRoomByIdJob::ThirdPartySigned> { - QJsonObject jo; - addParam<>(jo, QStringLiteral("sender"), pod.sender); - addParam<>(jo, QStringLiteral("mxid"), pod.mxid); - addParam<>(jo, QStringLiteral("token"), pod.token); - addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - return jo; - } + static void dumpTo(QJsonObject& jo, const JoinRoomByIdJob::ThirdPartySigned& pod) + { + addParam<>(jo, QStringLiteral("sender"), pod.sender); + addParam<>(jo, QStringLiteral("mxid"), pod.mxid); + addParam<>(jo, QStringLiteral("token"), pod.token); + addParam<>(jo, QStringLiteral("signatures"), pod.signatures); + } + }; } // namespace QMatrixClient class JoinRoomByIdJob::Private @@ -58,7 +59,7 @@ BaseJob::Status JoinRoomByIdJob::parseJson(const QJsonDocument& data) if (!json.contains("room_id"_ls)) return { JsonParseError, "The key 'room_id' not found in the response" }; - d->roomId = fromJson<QString>(json.value("room_id"_ls)); + fromJson(json.value("room_id"_ls), d->roomId); return Success; } @@ -66,22 +67,24 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const JoinRoomJob::Signed& pod) + template <> struct JsonObjectConverter<JoinRoomJob::Signed> { - QJsonObject jo; - addParam<>(jo, QStringLiteral("sender"), pod.sender); - addParam<>(jo, QStringLiteral("mxid"), pod.mxid); - addParam<>(jo, QStringLiteral("token"), pod.token); - addParam<>(jo, QStringLiteral("signatures"), pod.signatures); - return jo; - } - - QJsonObject toJson(const JoinRoomJob::ThirdPartySigned& pod) + static void dumpTo(QJsonObject& jo, const JoinRoomJob::Signed& pod) + { + addParam<>(jo, QStringLiteral("sender"), pod.sender); + addParam<>(jo, QStringLiteral("mxid"), pod.mxid); + addParam<>(jo, QStringLiteral("token"), pod.token); + addParam<>(jo, QStringLiteral("signatures"), pod.signatures); + } + }; + + template <> struct JsonObjectConverter<JoinRoomJob::ThirdPartySigned> { - QJsonObject jo; - addParam<>(jo, QStringLiteral("signed"), pod.signedData); - return jo; - } + static void dumpTo(QJsonObject& jo, const JoinRoomJob::ThirdPartySigned& pod) + { + addParam<>(jo, QStringLiteral("signed"), pod.signedData); + } + }; } // namespace QMatrixClient class JoinRoomJob::Private @@ -123,7 +126,7 @@ BaseJob::Status JoinRoomJob::parseJson(const QJsonDocument& data) if (!json.contains("room_id"_ls)) return { JsonParseError, "The key 'room_id' not found in the response" }; - d->roomId = fromJson<QString>(json.value("room_id"_ls)); + fromJson(json.value("room_id"_ls), d->roomId); return Success; } diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h index 137afbfc..52c8ea42 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -59,7 +59,7 @@ namespace QMatrixClient // Result properties - /// The joined room id + /// The joined room ID. const QString& roomId() const; protected: @@ -138,7 +138,7 @@ namespace QMatrixClient // Result properties - /// The joined room id + /// The joined room ID. const QString& roomId() const; protected: diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp index c7492411..6c16a8a3 100644 --- a/lib/csapi/keys.cpp +++ b/lib/csapi/keys.cpp @@ -44,7 +44,7 @@ BaseJob::Status UploadKeysJob::parseJson(const QJsonDocument& data) if (!json.contains("one_time_key_counts"_ls)) return { JsonParseError, "The key 'one_time_key_counts' not found in the response" }; - d->oneTimeKeyCounts = fromJson<QHash<QString, int>>(json.value("one_time_key_counts"_ls)); + fromJson(json.value("one_time_key_counts"_ls), d->oneTimeKeyCounts); return Success; } @@ -52,27 +52,20 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<QueryKeysJob::UnsignedDeviceInfo> + template <> struct JsonObjectConverter<QueryKeysJob::UnsignedDeviceInfo> { - QueryKeysJob::UnsignedDeviceInfo operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, QueryKeysJob::UnsignedDeviceInfo& result) { - QueryKeysJob::UnsignedDeviceInfo result; - result.deviceDisplayName = - fromJson<QString>(jo.value("device_display_name"_ls)); - - return result; + fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); } }; - template <> struct FromJsonObject<QueryKeysJob::DeviceInformation> + template <> struct JsonObjectConverter<QueryKeysJob::DeviceInformation> { - QueryKeysJob::DeviceInformation operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, QueryKeysJob::DeviceInformation& result) { - QueryKeysJob::DeviceInformation result; - result.unsignedData = - fromJson<QueryKeysJob::UnsignedDeviceInfo>(jo.value("unsigned"_ls)); - - return result; + fillFromJson<DeviceKeys>(jo, result); + fromJson(jo.value("unsigned"_ls), result.unsignedData); } }; } // namespace QMatrixClient @@ -113,8 +106,8 @@ const QHash<QString, QHash<QString, QueryKeysJob::DeviceInformation>>& QueryKeys BaseJob::Status QueryKeysJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->failures = fromJson<QHash<QString, QJsonObject>>(json.value("failures"_ls)); - d->deviceKeys = fromJson<QHash<QString, QHash<QString, DeviceInformation>>>(json.value("device_keys"_ls)); + fromJson(json.value("failures"_ls), d->failures); + fromJson(json.value("device_keys"_ls), d->deviceKeys); return Success; } @@ -153,8 +146,8 @@ const QHash<QString, QHash<QString, QVariant>>& ClaimKeysJob::oneTimeKeys() cons BaseJob::Status ClaimKeysJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->failures = fromJson<QHash<QString, QJsonObject>>(json.value("failures"_ls)); - d->oneTimeKeys = fromJson<QHash<QString, QHash<QString, QVariant>>>(json.value("one_time_keys"_ls)); + fromJson(json.value("failures"_ls), d->failures); + fromJson(json.value("one_time_keys"_ls), d->oneTimeKeys); return Success; } @@ -205,8 +198,8 @@ const QStringList& GetKeysChangesJob::left() const BaseJob::Status GetKeysChangesJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->changed = fromJson<QStringList>(json.value("changed"_ls)); - d->left = fromJson<QStringList>(json.value("left"_ls)); + fromJson(json.value("changed"_ls), d->changed); + fromJson(json.value("left"_ls), d->left); return Success; } diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h index 5968187e..714079cf 100644 --- a/lib/csapi/kicking.h +++ b/lib/csapi/kicking.h @@ -29,7 +29,7 @@ namespace QMatrixClient * \param userId * The fully qualified user ID of the user being kicked. * \param reason - * The reason the user has been kicked. This will be supplied as the + * The reason the user has been kicked. This will be supplied as the * ``reason`` on the target's updated `m.room.member`_ event. */ explicit KickJob(const QString& roomId, const QString& userId, const QString& reason = {}); diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp index a745dba1..85a9cae4 100644 --- a/lib/csapi/list_joined_rooms.cpp +++ b/lib/csapi/list_joined_rooms.cpp @@ -46,7 +46,7 @@ BaseJob::Status GetJoinedRoomsJob::parseJson(const QJsonDocument& data) if (!json.contains("joined_rooms"_ls)) return { JsonParseError, "The key 'joined_rooms' not found in the response" }; - d->joinedRooms = fromJson<QStringList>(json.value("joined_rooms"_ls)); + fromJson(json.value("joined_rooms"_ls), d->joinedRooms); return Success; } diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp index 2fdb2005..71b3c541 100644 --- a/lib/csapi/list_public_rooms.cpp +++ b/lib/csapi/list_public_rooms.cpp @@ -43,7 +43,7 @@ const QString& GetRoomVisibilityOnDirectoryJob::visibility() const BaseJob::Status GetRoomVisibilityOnDirectoryJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->visibility = fromJson<QString>(json.value("visibility"_ls)); + fromJson(json.value("visibility"_ls), d->visibility); return Success; } @@ -100,7 +100,7 @@ const PublicRoomsResponse& GetPublicRoomsJob::data() const BaseJob::Status GetPublicRoomsJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<PublicRoomsResponse>(data); + fromJson(data, d->data); return Success; } @@ -108,12 +108,13 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const QueryPublicRoomsJob::Filter& pod) + template <> struct JsonObjectConverter<QueryPublicRoomsJob::Filter> { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("generic_search_term"), pod.genericSearchTerm); - return jo; - } + static void dumpTo(QJsonObject& jo, const QueryPublicRoomsJob::Filter& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("generic_search_term"), pod.genericSearchTerm); + } + }; } // namespace QMatrixClient class QueryPublicRoomsJob::Private @@ -131,7 +132,7 @@ BaseJob::Query queryToQueryPublicRooms(const QString& server) static const auto QueryPublicRoomsJobName = QStringLiteral("QueryPublicRoomsJob"); -QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable<int> limit, const QString& since, const Omittable<Filter>& filter, bool includeAllNetworks, const QString& thirdPartyInstanceId) +QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable<int> limit, const QString& since, const Omittable<Filter>& filter, Omittable<bool> includeAllNetworks, const QString& thirdPartyInstanceId) : BaseJob(HttpVerb::Post, QueryPublicRoomsJobName, basePath % "/publicRooms", queryToQueryPublicRooms(server)) @@ -155,7 +156,7 @@ const PublicRoomsResponse& QueryPublicRoomsJob::data() const BaseJob::Status QueryPublicRoomsJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<PublicRoomsResponse>(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index 8401c134..a6498745 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -156,7 +156,7 @@ namespace QMatrixClient * The specific third party network/protocol to request from the * homeserver. Can only be used if ``include_all_networks`` is false. */ - explicit QueryPublicRoomsJob(const QString& server = {}, Omittable<int> limit = none, const QString& since = {}, const Omittable<Filter>& filter = none, bool includeAllNetworks = false, const QString& thirdPartyInstanceId = {}); + explicit QueryPublicRoomsJob(const QString& server = {}, Omittable<int> limit = none, const QString& since = {}, const Omittable<Filter>& filter = none, Omittable<bool> includeAllNetworks = none, const QString& thirdPartyInstanceId = {}); ~QueryPublicRoomsJob() override; // Result properties diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index 4d15a30b..5e369b9a 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -16,15 +16,11 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetLoginFlowsJob::LoginFlow> + template <> struct JsonObjectConverter<GetLoginFlowsJob::LoginFlow> { - GetLoginFlowsJob::LoginFlow operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetLoginFlowsJob::LoginFlow& result) { - GetLoginFlowsJob::LoginFlow result; - result.type = - fromJson<QString>(jo.value("type"_ls)); - - return result; + fromJson(jo.value("type"_ls), result.type); } }; } // namespace QMatrixClient @@ -60,7 +56,7 @@ const QVector<GetLoginFlowsJob::LoginFlow>& GetLoginFlowsJob::flows() const BaseJob::Status GetLoginFlowsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->flows = fromJson<QVector<LoginFlow>>(json.value("flows"_ls)); + fromJson(json.value("flows"_ls), d->flows); return Success; } @@ -71,6 +67,7 @@ class LoginJob::Private QString accessToken; QString homeServer; QString deviceId; + Omittable<DiscoveryInformation> wellKnown; }; static const auto LoginJobName = QStringLiteral("LoginJob"); @@ -115,13 +112,19 @@ const QString& LoginJob::deviceId() const return d->deviceId; } +const Omittable<DiscoveryInformation>& LoginJob::wellKnown() const +{ + return d->wellKnown; +} + BaseJob::Status LoginJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->userId = fromJson<QString>(json.value("user_id"_ls)); - d->accessToken = fromJson<QString>(json.value("access_token"_ls)); - d->homeServer = fromJson<QString>(json.value("home_server"_ls)); - d->deviceId = fromJson<QString>(json.value("device_id"_ls)); + fromJson(json.value("user_id"_ls), d->userId); + fromJson(json.value("access_token"_ls), d->accessToken); + fromJson(json.value("home_server"_ls), d->homeServer); + fromJson(json.value("device_id"_ls), d->deviceId); + fromJson(json.value("well_known"_ls), d->wellKnown); return Success; } diff --git a/lib/csapi/login.h b/lib/csapi/login.h index 957d8881..648316df 100644 --- a/lib/csapi/login.h +++ b/lib/csapi/login.h @@ -7,6 +7,7 @@ #include "jobs/basejob.h" #include <QtCore/QVector> +#include "csapi/definitions/wellknown/full.h" #include "csapi/definitions/user_identifier.h" #include "converters.h" @@ -118,6 +119,11 @@ namespace QMatrixClient /// ID of the logged-in device. Will be the same as the /// corresponding parameter in the request, if one was specified. const QString& deviceId() const; + /// Optional client configuration provided by the server. If present, + /// clients SHOULD use the provided object to reconfigure themselves, + /// optionally validating the URLs within. This object takes the same + /// form as the one returned from .well-known autodiscovery. + const Omittable<DiscoveryInformation>& wellKnown() const; protected: Status parseJson(const QJsonDocument& data) override; diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp index c59a51ab..9aca7ec9 100644 --- a/lib/csapi/message_pagination.cpp +++ b/lib/csapi/message_pagination.cpp @@ -68,9 +68,9 @@ RoomEvents&& GetRoomEventsJob::chunk() BaseJob::Status GetRoomEventsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->begin = fromJson<QString>(json.value("start"_ls)); - d->end = fromJson<QString>(json.value("end"_ls)); - d->chunk = fromJson<RoomEvents>(json.value("chunk"_ls)); + fromJson(json.value("start"_ls), d->begin); + fromJson(json.value("end"_ls), d->end); + fromJson(json.value("chunk"_ls), d->chunk); return Success; } diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp index 785a0a8a..c00b7cb0 100644 --- a/lib/csapi/notifications.cpp +++ b/lib/csapi/notifications.cpp @@ -16,25 +16,16 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetNotificationsJob::Notification> + template <> struct JsonObjectConverter<GetNotificationsJob::Notification> { - GetNotificationsJob::Notification operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetNotificationsJob::Notification& result) { - GetNotificationsJob::Notification result; - result.actions = - fromJson<QVector<QVariant>>(jo.value("actions"_ls)); - result.event = - fromJson<EventPtr>(jo.value("event"_ls)); - result.profileTag = - fromJson<QString>(jo.value("profile_tag"_ls)); - result.read = - fromJson<bool>(jo.value("read"_ls)); - result.roomId = - fromJson<QString>(jo.value("room_id"_ls)); - result.ts = - fromJson<int>(jo.value("ts"_ls)); - - return result; + fromJson(jo.value("actions"_ls), result.actions); + fromJson(jo.value("event"_ls), result.event); + fromJson(jo.value("profile_tag"_ls), result.profileTag); + fromJson(jo.value("read"_ls), result.read); + fromJson(jo.value("room_id"_ls), result.roomId); + fromJson(jo.value("ts"_ls), result.ts); } }; } // namespace QMatrixClient @@ -87,11 +78,11 @@ std::vector<GetNotificationsJob::Notification>&& GetNotificationsJob::notificati BaseJob::Status GetNotificationsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->nextToken = fromJson<QString>(json.value("next_token"_ls)); + fromJson(json.value("next_token"_ls), d->nextToken); if (!json.contains("notifications"_ls)) return { JsonParseError, "The key 'notifications' not found in the response" }; - d->notifications = fromJson<std::vector<Notification>>(json.value("notifications"_ls)); + fromJson(json.value("notifications"_ls), d->notifications); return Success; } diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp index 2547f0c8..b27fe0b8 100644 --- a/lib/csapi/openid.cpp +++ b/lib/csapi/openid.cpp @@ -59,19 +59,19 @@ BaseJob::Status RequestOpenIdTokenJob::parseJson(const QJsonDocument& data) if (!json.contains("access_token"_ls)) return { JsonParseError, "The key 'access_token' not found in the response" }; - d->accessToken = fromJson<QString>(json.value("access_token"_ls)); + fromJson(json.value("access_token"_ls), d->accessToken); if (!json.contains("token_type"_ls)) return { JsonParseError, "The key 'token_type' not found in the response" }; - d->tokenType = fromJson<QString>(json.value("token_type"_ls)); + fromJson(json.value("token_type"_ls), d->tokenType); if (!json.contains("matrix_server_name"_ls)) return { JsonParseError, "The key 'matrix_server_name' not found in the response" }; - d->matrixServerName = fromJson<QString>(json.value("matrix_server_name"_ls)); + fromJson(json.value("matrix_server_name"_ls), d->matrixServerName); if (!json.contains("expires_in"_ls)) return { JsonParseError, "The key 'expires_in' not found in the response" }; - d->expiresIn = fromJson<int>(json.value("expires_in"_ls)); + fromJson(json.value("expires_in"_ls), d->expiresIn); return Success; } diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp index e046a62e..3208d48d 100644 --- a/lib/csapi/peeking_events.cpp +++ b/lib/csapi/peeking_events.cpp @@ -66,9 +66,9 @@ RoomEvents&& PeekEventsJob::chunk() BaseJob::Status PeekEventsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->begin = fromJson<QString>(json.value("start"_ls)); - d->end = fromJson<QString>(json.value("end"_ls)); - d->chunk = fromJson<RoomEvents>(json.value("chunk"_ls)); + fromJson(json.value("start"_ls), d->begin); + fromJson(json.value("end"_ls), d->end); + fromJson(json.value("chunk"_ls), d->chunk); return Success; } diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp index 7aba8b61..024d7a34 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -30,7 +30,7 @@ class GetPresenceJob::Private QString presence; Omittable<int> lastActiveAgo; QString statusMsg; - bool currentlyActive; + Omittable<bool> currentlyActive; }; QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId) @@ -65,7 +65,7 @@ const QString& GetPresenceJob::statusMsg() const return d->statusMsg; } -bool GetPresenceJob::currentlyActive() const +Omittable<bool> GetPresenceJob::currentlyActive() const { return d->currentlyActive; } @@ -76,56 +76,10 @@ BaseJob::Status GetPresenceJob::parseJson(const QJsonDocument& data) if (!json.contains("presence"_ls)) return { JsonParseError, "The key 'presence' not found in the response" }; - d->presence = fromJson<QString>(json.value("presence"_ls)); - d->lastActiveAgo = fromJson<int>(json.value("last_active_ago"_ls)); - d->statusMsg = fromJson<QString>(json.value("status_msg"_ls)); - d->currentlyActive = fromJson<bool>(json.value("currently_active"_ls)); - return Success; -} - -static const auto ModifyPresenceListJobName = QStringLiteral("ModifyPresenceListJob"); - -ModifyPresenceListJob::ModifyPresenceListJob(const QString& userId, const QStringList& invite, const QStringList& drop) - : BaseJob(HttpVerb::Post, ModifyPresenceListJobName, - basePath % "/presence/list/" % userId) -{ - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("invite"), invite); - addParam<IfNotEmpty>(_data, QStringLiteral("drop"), drop); - setRequestData(_data); -} - -class GetPresenceForListJob::Private -{ - public: - Events data; -}; - -QUrl GetPresenceForListJob::makeRequestUrl(QUrl baseUrl, const QString& userId) -{ - return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/presence/list/" % userId); -} - -static const auto GetPresenceForListJobName = QStringLiteral("GetPresenceForListJob"); - -GetPresenceForListJob::GetPresenceForListJob(const QString& userId) - : BaseJob(HttpVerb::Get, GetPresenceForListJobName, - basePath % "/presence/list/" % userId, false) - , d(new Private) -{ -} - -GetPresenceForListJob::~GetPresenceForListJob() = default; - -Events&& GetPresenceForListJob::data() -{ - return std::move(d->data); -} - -BaseJob::Status GetPresenceForListJob::parseJson(const QJsonDocument& data) -{ - d->data = fromJson<Events>(data); + fromJson(json.value("presence"_ls), d->presence); + fromJson(json.value("last_active_ago"_ls), d->lastActiveAgo); + fromJson(json.value("status_msg"_ls), d->statusMsg); + fromJson(json.value("currently_active"_ls), d->currentlyActive); return Success; } diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h index 86b9d395..5e132d24 100644 --- a/lib/csapi/presence.h +++ b/lib/csapi/presence.h @@ -6,7 +6,6 @@ #include "jobs/basejob.h" -#include "events/eventloader.h" #include "converters.h" namespace QMatrixClient @@ -65,59 +64,7 @@ namespace QMatrixClient /// The state message for this user if one was set. const QString& statusMsg() const; /// Whether the user is currently active - bool currentlyActive() const; - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; - - /// Add or remove users from this presence list. - /// - /// Adds or removes users from this presence list. - class ModifyPresenceListJob : public BaseJob - { - public: - /*! Add or remove users from this presence list. - * \param userId - * The user whose presence list is being modified. - * \param invite - * A list of user IDs to add to the list. - * \param drop - * A list of user IDs to remove from the list. - */ - explicit ModifyPresenceListJob(const QString& userId, const QStringList& invite = {}, const QStringList& drop = {}); - }; - - /// Get presence events for this presence list. - /// - /// Retrieve a list of presence events for every user on this list. - class GetPresenceForListJob : public BaseJob - { - public: - /*! Get presence events for this presence list. - * \param userId - * The user whose presence list should be retrieved. - */ - explicit GetPresenceForListJob(const QString& userId); - - /*! Construct a URL without creating a full-fledged job object - * - * This function can be used when a URL for - * GetPresenceForListJob is necessary but the job - * itself isn't. - */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); - - ~GetPresenceForListJob() override; - - // Result properties - - /// A list of presence events for this list. - Events&& data(); + Omittable<bool> currentlyActive() const; protected: Status parseJson(const QJsonDocument& data) override; diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp index bb053062..4ed3ad9b 100644 --- a/lib/csapi/profile.cpp +++ b/lib/csapi/profile.cpp @@ -54,7 +54,7 @@ const QString& GetDisplayNameJob::displayname() const BaseJob::Status GetDisplayNameJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->displayname = fromJson<QString>(json.value("displayname"_ls)); + fromJson(json.value("displayname"_ls), d->displayname); return Success; } @@ -100,7 +100,7 @@ const QString& GetAvatarUrlJob::avatarUrl() const BaseJob::Status GetAvatarUrlJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->avatarUrl = fromJson<QString>(json.value("avatar_url"_ls)); + fromJson(json.value("avatar_url"_ls), d->avatarUrl); return Success; } @@ -141,8 +141,8 @@ const QString& GetUserProfileJob::displayname() const BaseJob::Status GetUserProfileJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->avatarUrl = fromJson<QString>(json.value("avatar_url"_ls)); - d->displayname = fromJson<QString>(json.value("displayname"_ls)); + fromJson(json.value("avatar_url"_ls), d->avatarUrl); + fromJson(json.value("displayname"_ls), d->displayname); return Success; } diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp index d20db88a..664959f4 100644 --- a/lib/csapi/pusher.cpp +++ b/lib/csapi/pusher.cpp @@ -16,43 +16,27 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetPushersJob::PusherData> + template <> struct JsonObjectConverter<GetPushersJob::PusherData> { - GetPushersJob::PusherData operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetPushersJob::PusherData& result) { - GetPushersJob::PusherData result; - result.url = - fromJson<QString>(jo.value("url"_ls)); - result.format = - fromJson<QString>(jo.value("format"_ls)); - - return result; + fromJson(jo.value("url"_ls), result.url); + fromJson(jo.value("format"_ls), result.format); } }; - template <> struct FromJsonObject<GetPushersJob::Pusher> + template <> struct JsonObjectConverter<GetPushersJob::Pusher> { - GetPushersJob::Pusher operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetPushersJob::Pusher& result) { - GetPushersJob::Pusher result; - result.pushkey = - fromJson<QString>(jo.value("pushkey"_ls)); - result.kind = - fromJson<QString>(jo.value("kind"_ls)); - result.appId = - fromJson<QString>(jo.value("app_id"_ls)); - result.appDisplayName = - fromJson<QString>(jo.value("app_display_name"_ls)); - result.deviceDisplayName = - fromJson<QString>(jo.value("device_display_name"_ls)); - result.profileTag = - fromJson<QString>(jo.value("profile_tag"_ls)); - result.lang = - fromJson<QString>(jo.value("lang"_ls)); - result.data = - fromJson<GetPushersJob::PusherData>(jo.value("data"_ls)); - - return result; + fromJson(jo.value("pushkey"_ls), result.pushkey); + fromJson(jo.value("kind"_ls), result.kind); + fromJson(jo.value("app_id"_ls), result.appId); + fromJson(jo.value("app_display_name"_ls), result.appDisplayName); + fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); + fromJson(jo.value("profile_tag"_ls), result.profileTag); + fromJson(jo.value("lang"_ls), result.lang); + fromJson(jo.value("data"_ls), result.data); } }; } // namespace QMatrixClient @@ -88,7 +72,7 @@ const QVector<GetPushersJob::Pusher>& GetPushersJob::pushers() const BaseJob::Status GetPushersJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->pushers = fromJson<QVector<Pusher>>(json.value("pushers"_ls)); + fromJson(json.value("pushers"_ls), d->pushers); return Success; } @@ -96,18 +80,19 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const PostPusherJob::PusherData& pod) + template <> struct JsonObjectConverter<PostPusherJob::PusherData> { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("url"), pod.url); - addParam<IfNotEmpty>(jo, QStringLiteral("format"), pod.format); - return jo; - } + static void dumpTo(QJsonObject& jo, const PostPusherJob::PusherData& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("url"), pod.url); + addParam<IfNotEmpty>(jo, QStringLiteral("format"), pod.format); + } + }; } // namespace QMatrixClient static const auto PostPusherJobName = QStringLiteral("PostPusherJob"); -PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag, bool append) +PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag, Omittable<bool> append) : BaseJob(HttpVerb::Post, PostPusherJobName, basePath % "/pushers/set") { diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h index 2b506183..da3303fe 100644 --- a/lib/csapi/pusher.h +++ b/lib/csapi/pusher.h @@ -164,6 +164,6 @@ namespace QMatrixClient * other pushers with the same App ID and pushkey for different * users. The default is ``false``. */ - explicit PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag = {}, bool append = false); + explicit PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag = {}, Omittable<bool> append = none); }; } // namespace QMatrixClient diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp index ea8ad02a..b91d18f7 100644 --- a/lib/csapi/pushrules.cpp +++ b/lib/csapi/pushrules.cpp @@ -46,7 +46,7 @@ BaseJob::Status GetPushRulesJob::parseJson(const QJsonDocument& data) if (!json.contains("global"_ls)) return { JsonParseError, "The key 'global' not found in the response" }; - d->global = fromJson<PushRuleset>(json.value("global"_ls)); + fromJson(json.value("global"_ls), d->global); return Success; } @@ -80,7 +80,7 @@ const PushRule& GetPushRuleJob::data() const BaseJob::Status GetPushRuleJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<PushRule>(data); + fromJson(data, d->data); return Success; } @@ -154,7 +154,7 @@ BaseJob::Status IsPushRuleEnabledJob::parseJson(const QJsonDocument& data) if (!json.contains("enabled"_ls)) return { JsonParseError, "The key 'enabled' not found in the response" }; - d->enabled = fromJson<bool>(json.value("enabled"_ls)); + fromJson(json.value("enabled"_ls), d->enabled); return Success; } @@ -203,7 +203,7 @@ BaseJob::Status GetPushRuleActionsJob::parseJson(const QJsonDocument& data) if (!json.contains("actions"_ls)) return { JsonParseError, "The key 'actions' not found in the response" }; - d->actions = fromJson<QStringList>(json.value("actions"_ls)); + fromJson(json.value("actions"_ls), d->actions); return Success; } diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h index f19f46b0..d982b477 100644 --- a/lib/csapi/read_markers.h +++ b/lib/csapi/read_markers.h @@ -26,7 +26,7 @@ namespace QMatrixClient * event MUST belong to the room. * \param mRead * The event ID to set the read receipt location at. This is - * equivalent to calling ``/receipt/m.read/$elsewhere:domain.com`` + * equivalent to calling ``/receipt/m.read/$elsewhere:example.org`` * and is provided here to save that extra call. */ explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead = {}); diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp index 64098670..1d54e36d 100644 --- a/lib/csapi/redaction.cpp +++ b/lib/csapi/redaction.cpp @@ -40,7 +40,7 @@ const QString& RedactEventJob::eventId() const BaseJob::Status RedactEventJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->eventId = fromJson<QString>(json.value("event_id"_ls)); + fromJson(json.value("event_id"_ls), d->eventId); return Success; } diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index 320ec796..5dc9c1e5 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -30,7 +30,7 @@ BaseJob::Query queryToRegister(const QString& kind) static const auto RegisterJobName = QStringLiteral("RegisterJob"); -RegisterJob::RegisterJob(const QString& kind, const Omittable<AuthenticationData>& auth, bool bindEmail, const QString& username, const QString& password, const QString& deviceId, const QString& initialDeviceDisplayName, bool inhibitLogin) +RegisterJob::RegisterJob(const QString& kind, const Omittable<AuthenticationData>& auth, Omittable<bool> bindEmail, const QString& username, const QString& password, const QString& deviceId, const QString& initialDeviceDisplayName, Omittable<bool> inhibitLogin) : BaseJob(HttpVerb::Post, RegisterJobName, basePath % "/register", queryToRegister(kind), @@ -76,10 +76,10 @@ BaseJob::Status RegisterJob::parseJson(const QJsonDocument& data) if (!json.contains("user_id"_ls)) return { JsonParseError, "The key 'user_id' not found in the response" }; - d->userId = fromJson<QString>(json.value("user_id"_ls)); - d->accessToken = fromJson<QString>(json.value("access_token"_ls)); - d->homeServer = fromJson<QString>(json.value("home_server"_ls)); - d->deviceId = fromJson<QString>(json.value("device_id"_ls)); + fromJson(json.value("user_id"_ls), d->userId); + fromJson(json.value("access_token"_ls), d->accessToken); + fromJson(json.value("home_server"_ls), d->homeServer); + fromJson(json.value("device_id"_ls), d->deviceId); return Success; } @@ -114,7 +114,7 @@ const Sid& RequestTokenToRegisterEmailJob::data() const BaseJob::Status RequestTokenToRegisterEmailJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Sid>(data); + fromJson(data, d->data); return Success; } @@ -150,7 +150,7 @@ const Sid& RequestTokenToRegisterMSISDNJob::data() const BaseJob::Status RequestTokenToRegisterMSISDNJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Sid>(data); + fromJson(data, d->data); return Success; } @@ -197,7 +197,7 @@ const Sid& RequestTokenToResetPasswordEmailJob::data() const BaseJob::Status RequestTokenToResetPasswordEmailJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Sid>(data); + fromJson(data, d->data); return Success; } @@ -233,7 +233,7 @@ const Sid& RequestTokenToResetPasswordMSISDNJob::data() const BaseJob::Status RequestTokenToResetPasswordMSISDNJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<Sid>(data); + fromJson(data, d->data); return Success; } @@ -251,7 +251,7 @@ DeactivateAccountJob::DeactivateAccountJob(const Omittable<AuthenticationData>& class CheckUsernameAvailabilityJob::Private { public: - bool available; + Omittable<bool> available; }; BaseJob::Query queryToCheckUsernameAvailability(const QString& username) @@ -281,7 +281,7 @@ CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& userna CheckUsernameAvailabilityJob::~CheckUsernameAvailabilityJob() = default; -bool CheckUsernameAvailabilityJob::available() const +Omittable<bool> CheckUsernameAvailabilityJob::available() const { return d->available; } @@ -289,7 +289,7 @@ bool CheckUsernameAvailabilityJob::available() const BaseJob::Status CheckUsernameAvailabilityJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->available = fromJson<bool>(json.value("available"_ls)); + fromJson(json.value("available"_ls), d->available); return Success; } diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index 9002b5c8..ca1a1c21 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -80,7 +80,7 @@ namespace QMatrixClient * returned from this call, therefore preventing an automatic * login. Defaults to false. */ - explicit RegisterJob(const QString& kind = QStringLiteral("user"), const Omittable<AuthenticationData>& auth = none, bool bindEmail = false, const QString& username = {}, const QString& password = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, bool inhibitLogin = false); + explicit RegisterJob(const QString& kind = QStringLiteral("user"), const Omittable<AuthenticationData>& auth = none, Omittable<bool> bindEmail = none, const QString& username = {}, const QString& password = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, Omittable<bool> inhibitLogin = none); ~RegisterJob() override; // Result properties @@ -418,7 +418,7 @@ namespace QMatrixClient /// A flag to indicate that the username is available. This should always /// be ``true`` when the server replies with 200 OK. - bool available() const; + Omittable<bool> available() const; protected: Status parseJson(const QJsonDocument& data) override; diff --git a/lib/csapi/room_send.cpp b/lib/csapi/room_send.cpp index 2b39ede2..0d25eb69 100644 --- a/lib/csapi/room_send.cpp +++ b/lib/csapi/room_send.cpp @@ -38,7 +38,7 @@ const QString& SendMessageJob::eventId() const BaseJob::Status SendMessageJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->eventId = fromJson<QString>(json.value("event_id"_ls)); + fromJson(json.value("event_id"_ls), d->eventId); return Success; } diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp index 8f87979d..3aa7d736 100644 --- a/lib/csapi/room_state.cpp +++ b/lib/csapi/room_state.cpp @@ -38,7 +38,7 @@ const QString& SetRoomStateWithKeyJob::eventId() const BaseJob::Status SetRoomStateWithKeyJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->eventId = fromJson<QString>(json.value("event_id"_ls)); + fromJson(json.value("event_id"_ls), d->eventId); return Success; } @@ -68,7 +68,7 @@ const QString& SetRoomStateJob::eventId() const BaseJob::Status SetRoomStateJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->eventId = fromJson<QString>(json.value("event_id"_ls)); + fromJson(json.value("event_id"_ls), d->eventId); return Success; } diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp new file mode 100644 index 00000000..f58fd675 --- /dev/null +++ b/lib/csapi/room_upgrades.cpp @@ -0,0 +1,49 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "room_upgrades.h" + +#include "converters.h" + +#include <QtCore/QStringBuilder> + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +class UpgradeRoomJob::Private +{ + public: + QString replacementRoom; +}; + +static const auto UpgradeRoomJobName = QStringLiteral("UpgradeRoomJob"); + +UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion) + : BaseJob(HttpVerb::Post, UpgradeRoomJobName, + basePath % "/rooms/" % roomId % "/upgrade") + , d(new Private) +{ + QJsonObject _data; + addParam<>(_data, QStringLiteral("new_version"), newVersion); + setRequestData(_data); +} + +UpgradeRoomJob::~UpgradeRoomJob() = default; + +const QString& UpgradeRoomJob::replacementRoom() const +{ + return d->replacementRoom; +} + +BaseJob::Status UpgradeRoomJob::parseJson(const QJsonDocument& data) +{ + auto json = data.object(); + if (!json.contains("replacement_room"_ls)) + return { JsonParseError, + "The key 'replacement_room' not found in the response" }; + fromJson(json.value("replacement_room"_ls), d->replacementRoom); + return Success; +} + diff --git a/lib/csapi/room_upgrades.h b/lib/csapi/room_upgrades.h new file mode 100644 index 00000000..4da5941a --- /dev/null +++ b/lib/csapi/room_upgrades.h @@ -0,0 +1,41 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + + +namespace QMatrixClient +{ + // Operations + + /// Upgrades a room to a new room version. + /// + /// Upgrades the given room to a particular room version. + class UpgradeRoomJob : public BaseJob + { + public: + /*! Upgrades a room to a new room version. + * \param roomId + * The ID of the room to upgrade. + * \param newVersion + * The new version for the room. + */ + explicit UpgradeRoomJob(const QString& roomId, const QString& newVersion); + ~UpgradeRoomJob() override; + + // Result properties + + /// The ID of the new room. + const QString& replacementRoom() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer<Private> d; + }; +} // namespace QMatrixClient diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp index 3befeee5..0b08ccec 100644 --- a/lib/csapi/rooms.cpp +++ b/lib/csapi/rooms.cpp @@ -42,16 +42,10 @@ EventPtr&& GetOneRoomEventJob::data() BaseJob::Status GetOneRoomEventJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<EventPtr>(data); + fromJson(data, d->data); return Success; } -class GetRoomStateWithKeyJob::Private -{ - public: - StateEventPtr data; -}; - QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey) { return BaseJob::makeRequestUrl(std::move(baseUrl), @@ -63,29 +57,9 @@ static const auto GetRoomStateWithKeyJobName = QStringLiteral("GetRoomStateWithK GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey) : BaseJob(HttpVerb::Get, GetRoomStateWithKeyJobName, basePath % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey) - , d(new Private) -{ -} - -GetRoomStateWithKeyJob::~GetRoomStateWithKeyJob() = default; - -StateEventPtr&& GetRoomStateWithKeyJob::data() -{ - return std::move(d->data); -} - -BaseJob::Status GetRoomStateWithKeyJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<StateEventPtr>(data); - return Success; } -class GetRoomStateByTypeJob::Private -{ - public: - StateEventPtr data; -}; - QUrl GetRoomStateByTypeJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType) { return BaseJob::makeRequestUrl(std::move(baseUrl), @@ -97,21 +71,7 @@ static const auto GetRoomStateByTypeJobName = QStringLiteral("GetRoomStateByType GetRoomStateByTypeJob::GetRoomStateByTypeJob(const QString& roomId, const QString& eventType) : BaseJob(HttpVerb::Get, GetRoomStateByTypeJobName, basePath % "/rooms/" % roomId % "/state/" % eventType) - , d(new Private) -{ -} - -GetRoomStateByTypeJob::~GetRoomStateByTypeJob() = default; - -StateEventPtr&& GetRoomStateByTypeJob::data() -{ - return std::move(d->data); -} - -BaseJob::Status GetRoomStateByTypeJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<StateEventPtr>(data); - return Success; } class GetRoomStateJob::Private @@ -144,7 +104,7 @@ StateEvents&& GetRoomStateJob::data() BaseJob::Status GetRoomStateJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<StateEvents>(data); + fromJson(data, d->data); return Success; } @@ -154,17 +114,28 @@ class GetMembersByRoomJob::Private EventsArray<RoomMemberEvent> chunk; }; -QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) +BaseJob::Query queryToGetMembersByRoom(const QString& at, const QString& membership, const QString& notMembership) +{ + BaseJob::Query _q; + addParam<IfNotEmpty>(_q, QStringLiteral("at"), at); + addParam<IfNotEmpty>(_q, QStringLiteral("membership"), membership); + addParam<IfNotEmpty>(_q, QStringLiteral("not_membership"), notMembership); + return _q; +} + +QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& at, const QString& membership, const QString& notMembership) { return BaseJob::makeRequestUrl(std::move(baseUrl), - basePath % "/rooms/" % roomId % "/members"); + basePath % "/rooms/" % roomId % "/members", + queryToGetMembersByRoom(at, membership, notMembership)); } static const auto GetMembersByRoomJobName = QStringLiteral("GetMembersByRoomJob"); -GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId) +GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId, const QString& at, const QString& membership, const QString& notMembership) : BaseJob(HttpVerb::Get, GetMembersByRoomJobName, - basePath % "/rooms/" % roomId % "/members") + basePath % "/rooms/" % roomId % "/members", + queryToGetMembersByRoom(at, membership, notMembership)) , d(new Private) { } @@ -179,7 +150,7 @@ EventsArray<RoomMemberEvent>&& GetMembersByRoomJob::chunk() BaseJob::Status GetMembersByRoomJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->chunk = fromJson<EventsArray<RoomMemberEvent>>(json.value("chunk"_ls)); + fromJson(json.value("chunk"_ls), d->chunk); return Success; } @@ -187,17 +158,12 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetJoinedMembersByRoomJob::RoomMember> + template <> struct JsonObjectConverter<GetJoinedMembersByRoomJob::RoomMember> { - GetJoinedMembersByRoomJob::RoomMember operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, GetJoinedMembersByRoomJob::RoomMember& result) { - GetJoinedMembersByRoomJob::RoomMember result; - result.displayName = - fromJson<QString>(jo.value("display_name"_ls)); - result.avatarUrl = - fromJson<QString>(jo.value("avatar_url"_ls)); - - return result; + fromJson(jo.value("display_name"_ls), result.displayName); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } }; } // namespace QMatrixClient @@ -233,7 +199,7 @@ const QHash<QString, GetJoinedMembersByRoomJob::RoomMember>& GetJoinedMembersByR BaseJob::Status GetJoinedMembersByRoomJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->joined = fromJson<QHash<QString, RoomMember>>(json.value("joined"_ls)); + fromJson(json.value("joined"_ls), d->joined); return Success; } diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index 2366918b..b4d3d9b6 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -80,19 +80,6 @@ namespace QMatrixClient */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey); - ~GetRoomStateWithKeyJob() override; - - // Result properties - - /// The content of the state event. - StateEventPtr&& data(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; }; /// Get the state identified by the type, with the empty state key. @@ -122,19 +109,6 @@ namespace QMatrixClient */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType); - ~GetRoomStateByTypeJob() override; - - // Result properties - - /// The content of the state event. - StateEventPtr&& data(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; }; /// Get all state events in the current state of a room. @@ -184,8 +158,17 @@ namespace QMatrixClient /*! Get the m.room.member events for the room. * \param roomId * The room to get the member events for. + * \param at + * The token defining the timeline position as-of which to return + * the list of members. This token can be obtained from a batch token + * returned for each room by the sync API, or from + * a ``start``/``end`` token returned by a ``/messages`` request. + * \param membership + * Only return users with the specified membership + * \param notMembership + * Only return users with membership state other than specified */ - explicit GetMembersByRoomJob(const QString& roomId); + explicit GetMembersByRoomJob(const QString& roomId, const QString& at = {}, const QString& membership = {}, const QString& notMembership = {}); /*! Construct a URL without creating a full-fledged job object * @@ -193,7 +176,7 @@ namespace QMatrixClient * GetMembersByRoomJob is necessary but the job * itself isn't. */ - static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& at = {}, const QString& membership = {}, const QString& notMembership = {}); ~GetMembersByRoomJob() override; diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp index 9436eb47..a5f83c79 100644 --- a/lib/csapi/search.cpp +++ b/lib/csapi/search.cpp @@ -16,146 +16,113 @@ namespace QMatrixClient { // Converters - QJsonObject toJson(const SearchJob::IncludeEventContext& pod) + template <> struct JsonObjectConverter<SearchJob::IncludeEventContext> { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("before_limit"), pod.beforeLimit); - addParam<IfNotEmpty>(jo, QStringLiteral("after_limit"), pod.afterLimit); - addParam<IfNotEmpty>(jo, QStringLiteral("include_profile"), pod.includeProfile); - return jo; - } - - QJsonObject toJson(const SearchJob::Group& pod) - { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); - return jo; - } + static void dumpTo(QJsonObject& jo, const SearchJob::IncludeEventContext& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("before_limit"), pod.beforeLimit); + addParam<IfNotEmpty>(jo, QStringLiteral("after_limit"), pod.afterLimit); + addParam<IfNotEmpty>(jo, QStringLiteral("include_profile"), pod.includeProfile); + } + }; - QJsonObject toJson(const SearchJob::Groupings& pod) + template <> struct JsonObjectConverter<SearchJob::Group> { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("group_by"), pod.groupBy); - return jo; - } + static void dumpTo(QJsonObject& jo, const SearchJob::Group& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); + } + }; - QJsonObject toJson(const SearchJob::RoomEventsCriteria& pod) - { - QJsonObject jo; - addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm); - addParam<IfNotEmpty>(jo, QStringLiteral("keys"), pod.keys); - addParam<IfNotEmpty>(jo, QStringLiteral("filter"), pod.filter); - addParam<IfNotEmpty>(jo, QStringLiteral("order_by"), pod.orderBy); - addParam<IfNotEmpty>(jo, QStringLiteral("event_context"), pod.eventContext); - addParam<IfNotEmpty>(jo, QStringLiteral("include_state"), pod.includeState); - addParam<IfNotEmpty>(jo, QStringLiteral("groupings"), pod.groupings); - return jo; - } - - QJsonObject toJson(const SearchJob::Categories& pod) + template <> struct JsonObjectConverter<SearchJob::Groupings> { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("room_events"), pod.roomEvents); - return jo; - } + static void dumpTo(QJsonObject& jo, const SearchJob::Groupings& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("group_by"), pod.groupBy); + } + }; - template <> struct FromJsonObject<SearchJob::UserProfile> + template <> struct JsonObjectConverter<SearchJob::RoomEventsCriteria> { - SearchJob::UserProfile operator()(const QJsonObject& jo) const + static void dumpTo(QJsonObject& jo, const SearchJob::RoomEventsCriteria& pod) { - SearchJob::UserProfile result; - result.displayname = - fromJson<QString>(jo.value("displayname"_ls)); - result.avatarUrl = - fromJson<QString>(jo.value("avatar_url"_ls)); + addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm); + addParam<IfNotEmpty>(jo, QStringLiteral("keys"), pod.keys); + addParam<IfNotEmpty>(jo, QStringLiteral("filter"), pod.filter); + addParam<IfNotEmpty>(jo, QStringLiteral("order_by"), pod.orderBy); + addParam<IfNotEmpty>(jo, QStringLiteral("event_context"), pod.eventContext); + addParam<IfNotEmpty>(jo, QStringLiteral("include_state"), pod.includeState); + addParam<IfNotEmpty>(jo, QStringLiteral("groupings"), pod.groupings); + } + }; - return result; + template <> struct JsonObjectConverter<SearchJob::Categories> + { + static void dumpTo(QJsonObject& jo, const SearchJob::Categories& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("room_events"), pod.roomEvents); } }; - template <> struct FromJsonObject<SearchJob::EventContext> + template <> struct JsonObjectConverter<SearchJob::UserProfile> { - SearchJob::EventContext operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::UserProfile& result) { - SearchJob::EventContext result; - result.begin = - fromJson<QString>(jo.value("start"_ls)); - result.end = - fromJson<QString>(jo.value("end"_ls)); - result.profileInfo = - fromJson<QHash<QString, SearchJob::UserProfile>>(jo.value("profile_info"_ls)); - result.eventsBefore = - fromJson<RoomEvents>(jo.value("events_before"_ls)); - result.eventsAfter = - fromJson<RoomEvents>(jo.value("events_after"_ls)); - - return result; + fromJson(jo.value("displayname"_ls), result.displayname); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } }; - template <> struct FromJsonObject<SearchJob::Result> + template <> struct JsonObjectConverter<SearchJob::EventContext> { - SearchJob::Result operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::EventContext& result) { - SearchJob::Result result; - result.rank = - fromJson<double>(jo.value("rank"_ls)); - result.result = - fromJson<RoomEventPtr>(jo.value("result"_ls)); - result.context = - fromJson<SearchJob::EventContext>(jo.value("context"_ls)); - - return result; + fromJson(jo.value("start"_ls), result.begin); + fromJson(jo.value("end"_ls), result.end); + fromJson(jo.value("profile_info"_ls), result.profileInfo); + fromJson(jo.value("events_before"_ls), result.eventsBefore); + fromJson(jo.value("events_after"_ls), result.eventsAfter); } }; - template <> struct FromJsonObject<SearchJob::GroupValue> + template <> struct JsonObjectConverter<SearchJob::Result> { - SearchJob::GroupValue operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::Result& result) { - SearchJob::GroupValue result; - result.nextBatch = - fromJson<QString>(jo.value("next_batch"_ls)); - result.order = - fromJson<int>(jo.value("order"_ls)); - result.results = - fromJson<QStringList>(jo.value("results"_ls)); - - return result; + fromJson(jo.value("rank"_ls), result.rank); + fromJson(jo.value("result"_ls), result.result); + fromJson(jo.value("context"_ls), result.context); } }; - template <> struct FromJsonObject<SearchJob::ResultRoomEvents> + template <> struct JsonObjectConverter<SearchJob::GroupValue> { - SearchJob::ResultRoomEvents operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::GroupValue& result) { - SearchJob::ResultRoomEvents result; - result.count = - fromJson<int>(jo.value("count"_ls)); - result.highlights = - fromJson<QStringList>(jo.value("highlights"_ls)); - result.results = - fromJson<std::vector<SearchJob::Result>>(jo.value("results"_ls)); - result.state = - fromJson<std::unordered_map<QString, StateEvents>>(jo.value("state"_ls)); - result.groups = - fromJson<QHash<QString, QHash<QString, SearchJob::GroupValue>>>(jo.value("groups"_ls)); - result.nextBatch = - fromJson<QString>(jo.value("next_batch"_ls)); - - return result; + fromJson(jo.value("next_batch"_ls), result.nextBatch); + fromJson(jo.value("order"_ls), result.order); + fromJson(jo.value("results"_ls), result.results); } }; - template <> struct FromJsonObject<SearchJob::ResultCategories> + template <> struct JsonObjectConverter<SearchJob::ResultRoomEvents> { - SearchJob::ResultCategories operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchJob::ResultRoomEvents& result) { - SearchJob::ResultCategories result; - result.roomEvents = - fromJson<SearchJob::ResultRoomEvents>(jo.value("room_events"_ls)); + fromJson(jo.value("count"_ls), result.count); + fromJson(jo.value("highlights"_ls), result.highlights); + fromJson(jo.value("results"_ls), result.results); + fromJson(jo.value("state"_ls), result.state); + fromJson(jo.value("groups"_ls), result.groups); + fromJson(jo.value("next_batch"_ls), result.nextBatch); + } + }; - return result; + template <> struct JsonObjectConverter<SearchJob::ResultCategories> + { + static void fillFrom(const QJsonObject& jo, SearchJob::ResultCategories& result) + { + fromJson(jo.value("room_events"_ls), result.roomEvents); } }; } // namespace QMatrixClient @@ -199,7 +166,7 @@ BaseJob::Status SearchJob::parseJson(const QJsonDocument& data) if (!json.contains("search_categories"_ls)) return { JsonParseError, "The key 'search_categories' not found in the response" }; - d->searchCategories = fromJson<ResultCategories>(json.value("search_categories"_ls)); + fromJson(json.value("search_categories"_ls), d->searchCategories); return Success; } diff --git a/lib/csapi/search.h b/lib/csapi/search.h index 85b0886b..86a0ee92 100644 --- a/lib/csapi/search.h +++ b/lib/csapi/search.h @@ -39,7 +39,7 @@ namespace QMatrixClient /// historic profile information for the users /// that sent the events that were returned. /// By default, this is ``false``. - bool includeProfile; + Omittable<bool> includeProfile; }; /// Configuration for group. @@ -74,7 +74,7 @@ namespace QMatrixClient Omittable<IncludeEventContext> eventContext; /// Requests the server return the current state for /// each room returned. - bool includeState; + Omittable<bool> includeState; /// Requests that the server partitions the result set /// based on the provided list of keys. Omittable<Groupings> groupings; diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp new file mode 100644 index 00000000..7323951c --- /dev/null +++ b/lib/csapi/sso_login_redirect.cpp @@ -0,0 +1,38 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "sso_login_redirect.h" + +#include "converters.h" + +#include <QtCore/QStringBuilder> + +using namespace QMatrixClient; + +static const auto basePath = QStringLiteral("/_matrix/client/r0"); + +BaseJob::Query queryToRedirectToSSO(const QString& redirectUrl) +{ + BaseJob::Query _q; + addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl); + return _q; +} + +QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + basePath % "/login/sso/redirect", + queryToRedirectToSSO(redirectUrl)); +} + +static const auto RedirectToSSOJobName = QStringLiteral("RedirectToSSOJob"); + +RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl) + : BaseJob(HttpVerb::Get, RedirectToSSOJobName, + basePath % "/login/sso/redirect", + queryToRedirectToSSO(redirectUrl), + {}, false) +{ +} + diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h new file mode 100644 index 00000000..c09365b0 --- /dev/null +++ b/lib/csapi/sso_login_redirect.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + + +namespace QMatrixClient +{ + // Operations + + /// Redirect the user's browser to the SSO interface. + /// + /// A web-based Matrix client should instruct the user's browser to + /// navigate to this endpoint in order to log in via SSO. + /// + /// The server MUST respond with an HTTP redirect to the SSO interface. + class RedirectToSSOJob : public BaseJob + { + public: + /*! Redirect the user's browser to the SSO interface. + * \param redirectUrl + * URI to which the user will be redirected after the homeserver has + * authenticated the user with SSO. + */ + explicit RedirectToSSOJob(const QString& redirectUrl); + + /*! Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * RedirectToSSOJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& redirectUrl); + + }; +} // namespace QMatrixClient diff --git a/lib/csapi/tags.cpp b/lib/csapi/tags.cpp index 808915ac..94026bb9 100644 --- a/lib/csapi/tags.cpp +++ b/lib/csapi/tags.cpp @@ -16,16 +16,12 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<GetRoomTagsJob::Tag> + template <> struct JsonObjectConverter<GetRoomTagsJob::Tag> { - GetRoomTagsJob::Tag operator()(QJsonObject jo) const + static void fillFrom(QJsonObject jo, GetRoomTagsJob::Tag& result) { - GetRoomTagsJob::Tag result; - result.order = - fromJson<float>(jo.take("order"_ls)); - - result.additionalProperties = fromJson<QVariantHash>(jo); - return result; + fromJson(jo.take("order"_ls), result.order); + fromJson(jo, result.additionalProperties); } }; } // namespace QMatrixClient @@ -61,7 +57,7 @@ const QHash<QString, GetRoomTagsJob::Tag>& GetRoomTagsJob::tags() const BaseJob::Status GetRoomTagsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->tags = fromJson<QHash<QString, Tag>>(json.value("tags"_ls)); + fromJson(json.value("tags"_ls), d->tags); return Success; } diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp index 3ba1a5ad..12cb7c59 100644 --- a/lib/csapi/third_party_lookup.cpp +++ b/lib/csapi/third_party_lookup.cpp @@ -42,7 +42,7 @@ const QHash<QString, ThirdPartyProtocol>& GetProtocolsJob::data() const BaseJob::Status GetProtocolsJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<QHash<QString, ThirdPartyProtocol>>(data); + fromJson(data, d->data); return Success; } @@ -76,7 +76,7 @@ const ThirdPartyProtocol& GetProtocolMetadataJob::data() const BaseJob::Status GetProtocolMetadataJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<ThirdPartyProtocol>(data); + fromJson(data, d->data); return Success; } @@ -119,7 +119,7 @@ const QVector<ThirdPartyLocation>& QueryLocationByProtocolJob::data() const BaseJob::Status QueryLocationByProtocolJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<QVector<ThirdPartyLocation>>(data); + fromJson(data, d->data); return Success; } @@ -162,7 +162,7 @@ const QVector<ThirdPartyUser>& QueryUserByProtocolJob::data() const BaseJob::Status QueryUserByProtocolJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<QVector<ThirdPartyUser>>(data); + fromJson(data, d->data); return Success; } @@ -205,7 +205,7 @@ const QVector<ThirdPartyLocation>& QueryLocationByAliasJob::data() const BaseJob::Status QueryLocationByAliasJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<QVector<ThirdPartyLocation>>(data); + fromJson(data, d->data); return Success; } @@ -248,7 +248,7 @@ const QVector<ThirdPartyUser>& QueryUserByIDJob::data() const BaseJob::Status QueryUserByIDJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<QVector<ThirdPartyUser>>(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp index deb9cb8a..97d8962d 100644 --- a/lib/csapi/users.cpp +++ b/lib/csapi/users.cpp @@ -16,19 +16,13 @@ namespace QMatrixClient { // Converters - template <> struct FromJsonObject<SearchUserDirectoryJob::User> + template <> struct JsonObjectConverter<SearchUserDirectoryJob::User> { - SearchUserDirectoryJob::User operator()(const QJsonObject& jo) const + static void fillFrom(const QJsonObject& jo, SearchUserDirectoryJob::User& result) { - SearchUserDirectoryJob::User result; - result.userId = - fromJson<QString>(jo.value("user_id"_ls)); - result.displayName = - fromJson<QString>(jo.value("display_name"_ls)); - result.avatarUrl = - fromJson<QString>(jo.value("avatar_url"_ls)); - - return result; + fromJson(jo.value("user_id"_ls), result.userId); + fromJson(jo.value("display_name"_ls), result.displayName); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } }; } // namespace QMatrixClient @@ -71,11 +65,11 @@ BaseJob::Status SearchUserDirectoryJob::parseJson(const QJsonDocument& data) if (!json.contains("results"_ls)) return { JsonParseError, "The key 'results' not found in the response" }; - d->results = fromJson<QVector<User>>(json.value("results"_ls)); + fromJson(json.value("results"_ls), d->results); if (!json.contains("limited"_ls)) return { JsonParseError, "The key 'limited' not found in the response" }; - d->limited = fromJson<bool>(json.value("limited"_ls)); + fromJson(json.value("limited"_ls), d->limited); return Success; } diff --git a/lib/csapi/versions.cpp b/lib/csapi/versions.cpp index 128902e2..6ee6725d 100644 --- a/lib/csapi/versions.cpp +++ b/lib/csapi/versions.cpp @@ -16,6 +16,7 @@ class GetVersionsJob::Private { public: QStringList versions; + QHash<QString, bool> unstableFeatures; }; QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl) @@ -40,10 +41,19 @@ const QStringList& GetVersionsJob::versions() const return d->versions; } +const QHash<QString, bool>& GetVersionsJob::unstableFeatures() const +{ + return d->unstableFeatures; +} + BaseJob::Status GetVersionsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); - d->versions = fromJson<QStringList>(json.value("versions"_ls)); + if (!json.contains("versions"_ls)) + return { JsonParseError, + "The key 'versions' not found in the response" }; + fromJson(json.value("versions"_ls), d->versions); + fromJson(json.value("unstable_features"_ls), d->unstableFeatures); return Success; } diff --git a/lib/csapi/versions.h b/lib/csapi/versions.h index 309de184..b56f293f 100644 --- a/lib/csapi/versions.h +++ b/lib/csapi/versions.h @@ -6,6 +6,8 @@ #include "jobs/basejob.h" +#include <QtCore/QHash> +#include "converters.h" namespace QMatrixClient { @@ -19,6 +21,19 @@ namespace QMatrixClient /// /// Only the latest ``Z`` value will be reported for each supported ``X.Y`` value. /// i.e. if the server implements ``r0.0.0``, ``r0.0.1``, and ``r1.2.0``, it will report ``r0.0.1`` and ``r1.2.0``. + /// + /// The server may additionally advertise experimental features it supports + /// through ``unstable_features``. These features should be namespaced and + /// may optionally include version information within their name if desired. + /// Features listed here are not for optionally toggling parts of the Matrix + /// specification and should only be used to advertise support for a feature + /// which has not yet landed in the spec. For example, a feature currently + /// undergoing the proposal process may appear here and eventually be taken + /// off this list once the feature lands in the spec and the server deems it + /// reasonable to do so. Servers may wish to keep advertising features here + /// after they've been released into the spec to give clients a chance to + /// upgrade appropriately. Additionally, clients should avoid using unstable + /// features in their stable releases. class GetVersionsJob : public BaseJob { public: @@ -38,6 +53,10 @@ namespace QMatrixClient /// The supported versions. const QStringList& versions() const; + /// Experimental features the server supports. Features not listed here, + /// or the lack of this property all together, indicate that a feature is + /// not supported. + const QHash<QString, bool>& unstableFeatures() const; protected: Status parseJson(const QJsonDocument& data) override; diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp index 0479b645..e8158723 100644 --- a/lib/csapi/voip.cpp +++ b/lib/csapi/voip.cpp @@ -42,7 +42,7 @@ const QJsonObject& GetTurnServerJob::data() const BaseJob::Status GetTurnServerJob::parseJson(const QJsonDocument& data) { - d->data = fromJson<QJsonObject>(data); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/wellknown.cpp b/lib/csapi/wellknown.cpp index d42534a0..a6107f86 100644 --- a/lib/csapi/wellknown.cpp +++ b/lib/csapi/wellknown.cpp @@ -15,8 +15,7 @@ static const auto basePath = QStringLiteral("/.well-known"); class GetWellknownJob::Private { public: - HomeserverInformation homeserver; - Omittable<IdentityServerInformation> identityServer; + DiscoveryInformation data; }; QUrl GetWellknownJob::makeRequestUrl(QUrl baseUrl) @@ -36,24 +35,14 @@ GetWellknownJob::GetWellknownJob() GetWellknownJob::~GetWellknownJob() = default; -const HomeserverInformation& GetWellknownJob::homeserver() const +const DiscoveryInformation& GetWellknownJob::data() const { - return d->homeserver; -} - -const Omittable<IdentityServerInformation>& GetWellknownJob::identityServer() const -{ - return d->identityServer; + return d->data; } BaseJob::Status GetWellknownJob::parseJson(const QJsonDocument& data) { - auto json = data.object(); - if (!json.contains("m.homeserver"_ls)) - return { JsonParseError, - "The key 'm.homeserver' not found in the response" }; - d->homeserver = fromJson<HomeserverInformation>(json.value("m.homeserver"_ls)); - d->identityServer = fromJson<IdentityServerInformation>(json.value("m.identity_server"_ls)); + fromJson(data, d->data); return Success; } diff --git a/lib/csapi/wellknown.h b/lib/csapi/wellknown.h index df4c8c6e..8da9ce9f 100644 --- a/lib/csapi/wellknown.h +++ b/lib/csapi/wellknown.h @@ -6,9 +6,8 @@ #include "jobs/basejob.h" +#include "csapi/definitions/wellknown/full.h" #include "converters.h" -#include "csapi/definitions/wellknown/identity_server.h" -#include "csapi/definitions/wellknown/homeserver.h" namespace QMatrixClient { @@ -41,10 +40,8 @@ namespace QMatrixClient // Result properties - /// Information about the homeserver to connect to. - const HomeserverInformation& homeserver() const; - /// Optional. Information about the identity server to connect to. - const Omittable<IdentityServerInformation>& identityServer() const; + /// Server discovery information. + const DiscoveryInformation& data() const; protected: Status parseJson(const QJsonDocument& data) override; diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp index cb6439ef..aebdf5d3 100644 --- a/lib/csapi/whoami.cpp +++ b/lib/csapi/whoami.cpp @@ -46,7 +46,7 @@ BaseJob::Status GetTokenOwnerJob::parseJson(const QJsonDocument& data) if (!json.contains("user_id"_ls)) return { JsonParseError, "The key 'user_id' not found in the response" }; - d->userId = fromJson<QString>(json.value("user_id"_ls)); + fromJson(json.value("user_id"_ls), d->userId); return Success; } diff --git a/lib/csapi/{{base}}.cpp.mustache b/lib/csapi/{{base}}.cpp.mustache index 64fd8bf3..ff888d76 100644 --- a/lib/csapi/{{base}}.cpp.mustache +++ b/lib/csapi/{{base}}.cpp.mustache @@ -8,49 +8,52 @@ {{/operations}} using namespace QMatrixClient; {{#models.model}}{{#in?}} -QJsonObject QMatrixClient::toJson(const {{qualifiedName}}& pod) +void JsonObjectConverter<{{qualifiedName}}>::dumpTo( + QJsonObject& jo, const {{qualifiedName}}& pod) { - QJsonObject jo{{#propertyMap}} = toJson(pod.{{nameCamelCase}}){{/propertyMap}};{{#vars}} - addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}});{{/vars}} - return jo; -} +{{#propertyMap}} fillJson(jo, pod.{{nameCamelCase}}); +{{/propertyMap}}{{#parents}} fillJson<{{name}}>(jo, pod); +{{/parents}}{{#vars}} addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}}); +{{/vars}}}{{!<- dumpTo() ends here}} {{/in?}}{{#out?}} -{{qualifiedName}} FromJsonObject<{{qualifiedName}}>::operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const +void JsonObjectConverter<{{qualifiedName}}>::fillFrom( + {{^propertyMap}}const QJsonObject&{{/propertyMap + }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{qualifiedName}}& result) { - {{qualifiedName}} result; -{{#vars}} result.{{nameCamelCase}} = - fromJson<{{dataType.qualifiedName}}>(jo.{{#propertyMap}}take{{/propertyMap}}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls)); +{{#parents}} fillFromJson<{{qualifiedName}}>(jo, result); +{{/parents}}{{#vars}} fromJson(jo.{{#propertyMap}}take{{/propertyMap + }}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls), result.{{nameCamelCase}}); {{/vars}}{{#propertyMap}} - result.{{nameCamelCase}} = fromJson<{{dataType.qualifiedName}}>(jo);{{/propertyMap}} - return result; -} + fromJson(jo, result.{{nameCamelCase}}); +{{/propertyMap}}} {{/out?}}{{/models.model}}{{#operations}} static const auto basePath = QStringLiteral("{{basePathWithoutHost}}"); {{# operation}}{{#models}} namespace QMatrixClient { // Converters -{{#model}}{{#in?}} - QJsonObject toJson(const {{qualifiedName}}& pod) - { - QJsonObject jo{{#propertyMap}} = toJson(pod.{{nameCamelCase}}){{/propertyMap}};{{#vars}} - addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}});{{/vars}} - return jo; - } -{{/in?}}{{#out?}} - template <> struct FromJsonObject<{{qualifiedName}}> +{{#model}} + template <> struct JsonObjectConverter<{{qualifiedName}}> { - {{qualifiedName}} operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const +{{#in?}} static void dumpTo(QJsonObject& jo, const {{qualifiedName}}& pod) { - {{qualifiedName}} result; -{{#vars}} result.{{nameCamelCase}} = - fromJson<{{dataType.qualifiedName}}>(jo.{{#propertyMap}}take{{/propertyMap}}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls)); -{{/vars}}{{#propertyMap}} - result.{{nameCamelCase}} = fromJson<{{dataType.qualifiedName}}>(jo);{{/propertyMap}} - return result; - } - }; -{{/out?}}{{/model}}} // namespace QMatrixClient +{{#propertyMap}} fillJson(jo, pod.{{nameCamelCase}}); + {{/propertyMap}}{{#parents}}fillJson<{{name}}>(jo, pod); + {{/parents}}{{#vars +}} addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}}); +{{/vars}} } +{{/in?}}{{#out? +}} static void fillFrom({{^propertyMap}}const QJsonObject&{{/propertyMap + }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{qualifiedName}}& result) + { +{{#parents}} fillFromJson<{{qualifiedName}}{{!of the parent!}}>(jo, result); + {{/parents}}{{#vars +}} fromJson(jo.{{#propertyMap}}take{{/propertyMap + }}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls), result.{{nameCamelCase}}); +{{/vars}}{{#propertyMap}} fromJson(jo, result.{{nameCamelCase}}); +{{/propertyMap}} } +{{/out?}} }; +{{/model}}} // namespace QMatrixClient {{/ models}}{{#responses}}{{#normalResponse?}}{{#allProperties?}} class {{camelCaseOperationId}}Job::Private { @@ -109,12 +112,12 @@ BaseJob::Status {{camelCaseOperationId}}Job::parseReply(QNetworkReply* reply) }{{/ producesNonJson?}}{{^producesNonJson?}} BaseJob::Status {{camelCaseOperationId}}Job::parseJson(const QJsonDocument& data) { -{{#inlineResponse}} d->{{paramName}} = fromJson<{{dataType.name}}>(data); +{{#inlineResponse}} fromJson(data, d->{{paramName}}); {{/inlineResponse}}{{^inlineResponse}} auto json = data.object(); {{#properties}}{{#required?}} if (!json.contains("{{baseName}}"_ls)) return { JsonParseError, "The key '{{baseName}}' not found in the response" }; -{{/required?}} d->{{paramName}} = fromJson<{{dataType.name}}>(json.value("{{baseName}}"_ls)); +{{/required?}} fromJson(json.value("{{baseName}}"_ls), d->{{paramName}}); {{/properties}}{{/inlineResponse}} return Success; }{{/ producesNonJson?}} {{/allProperties?}}{{/normalResponse?}}{{/responses}}{{/operation}}{{/operations}} diff --git a/lib/csapi/{{base}}.h.mustache b/lib/csapi/{{base}}.h.mustache index 147c8607..a9c3a63a 100644 --- a/lib/csapi/{{base}}.h.mustache +++ b/lib/csapi/{{base}}.h.mustache @@ -18,14 +18,13 @@ namespace QMatrixClient {{/vars}}{{#propertyMap}}{{#description}} /// {{_}} {{/description}} {{>maybeOmittableType}} {{nameCamelCase}}; {{/propertyMap}} }; -{{#in?}} - QJsonObject toJson(const {{name}}& pod); -{{/in?}}{{#out?}} - template <> struct FromJsonObject<{{name}}> + template <> struct JsonObjectConverter<{{name}}> { - {{name}} operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const; - }; -{{/ out?}}{{/model}} + {{#in?}}static void dumpTo(QJsonObject& jo, const {{name}}& pod); + {{/in?}}{{#out?}}static void fillFrom({{^propertyMap}}const QJsonObject&{{/propertyMap + }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{name}}& pod); +{{/out?}} }; +{{/model}} {{/models}}{{#operations}} // Operations {{# operation}}{{#summary}} /// {{summary}}{{#description?}}{{!add a linebreak between summary and description if both exist}} diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index 79ef769c..8ec3fe48 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -17,3 +17,29 @@ */ #include "eventitem.h" + +#include "events/roommessageevent.h" +#include "events/roomavatarevent.h" + +using namespace QMatrixClient; + +void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) +{ + // TODO: eventually we might introduce hasFileContent to RoomEvent, + // and unify the code below. + if (auto* rme = getAs<RoomMessageEvent>()) + { + Q_ASSERT(rme->hasFileContent()); + rme->editContent([remoteUrl] (EventContent::TypedBase& ec) { + ec.fileInfo()->url = remoteUrl; + }); + } + if (auto* rae = getAs<RoomAvatarEvent>()) + { + Q_ASSERT(rae->content().fileInfo()); + rae->editContent([remoteUrl] (EventContent::FileInfo& fi) { + fi.url = remoteUrl; + }); + } + setStatus(EventStatus::FileUploaded); +} diff --git a/lib/eventitem.h b/lib/eventitem.h index 5f1d10c9..36ed2132 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -33,16 +33,17 @@ namespace QMatrixClient /** Special marks an event can assume * * This is used to hint at a special status of some events in UI. - * Most status values are mutually exclusive. + * All values except Redacted and Hidden are mutually exclusive. */ enum Code { Normal = 0x0, //< No special designation Submitted = 0x01, //< The event has just been submitted for sending - Departed = 0x02, //< The event has left the client - ReachedServer = 0x03, //< The server has received the event - SendingFailed = 0x04, //< The server could not receive the event + FileUploaded = 0x02, //< The file attached to the event has been uploaded to the server + Departed = 0x03, //< The event has left the client + ReachedServer = 0x04, //< The server has received the event + SendingFailed = 0x05, //< The server could not receive the event Redacted = 0x08, //< The event has been redacted - Hidden = 0x10, //< The event should be hidden + Hidden = 0x10, //< The event should not be shown in the timeline }; Q_DECLARE_FLAGS(Status, Code) Q_FLAG(Status) @@ -70,6 +71,9 @@ namespace QMatrixClient return std::exchange(evt, move(other)); } + protected: + template <typename EventT> + EventT* getAs() { return eventCast<EventT>(evt); } private: RoomEventPtr evt; }; @@ -116,6 +120,7 @@ namespace QMatrixClient QString annotation() const { return _annotation; } void setDeparted() { setStatus(EventStatus::Departed); } + void setFileUploaded(const QUrl& remoteUrl); void setReachedServer(const QString& eventId) { setStatus(EventStatus::ReachedServer); diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index d1c1abc8..a99d85ac 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -36,37 +36,38 @@ namespace QMatrixClient order_type order; TagRecord (order_type order = none) : order(order) { } - explicit TagRecord(const QJsonObject& jo) + + bool operator<(const TagRecord& other) const + { + // Per The Spec, rooms with no order should be after those with order + return !order.omitted() && + (other.order.omitted() || order.value() < other.order.value()); + } + }; + + template <> struct JsonObjectConverter<TagRecord> + { + static void fillFrom(const QJsonObject& jo, TagRecord& rec) { // Parse a float both from JSON double and JSON string because // libqmatrixclient previously used to use strings to store order. const auto orderJv = jo.value("order"_ls); if (orderJv.isDouble()) - order = fromJson<float>(orderJv); - else if (orderJv.isString()) + rec.order = fromJson<float>(orderJv); + if (orderJv.isString()) { bool ok; - order = orderJv.toString().toFloat(&ok); + rec.order = orderJv.toString().toFloat(&ok); if (!ok) - order = none; + rec.order = none; } } - - bool operator<(const TagRecord& other) const + static void dumpTo(QJsonObject& jo, const TagRecord& rec) { - // Per The Spec, rooms with no order should be after those with order - return !order.omitted() && - (other.order.omitted() || order.value() < other.order.value()); + addParam<IfNotEmpty>(jo, QStringLiteral("order"), rec.order); } }; - inline QJsonValue toJson(const TagRecord& rec) - { - QJsonObject o; - addParam<IfNotEmpty>(o, QStringLiteral("order"), rec.order); - return o; - } - using TagsMap = QHash<QString, TagRecord>; #define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ diff --git a/lib/events/event.cpp b/lib/events/event.cpp index fd6e3939..6505d89a 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -38,7 +38,8 @@ event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId) QString EventTypeRegistry::getMatrixType(event_type_t typeId) { - return typeId < get().eventTypes.size() ? get().eventTypes[typeId] : ""; + return typeId < get().eventTypes.size() + ? get().eventTypes[typeId] : QString(); } Event::Event(Type type, const QJsonObject& json) @@ -77,3 +78,8 @@ const QJsonObject Event::unsignedJson() const { return fullJson()[UnsignedKeyL].toObject(); } + +void Event::dumpTo(QDebug dbg) const +{ + dbg << QJsonDocument(contentJson()).toJson(QJsonDocument::Compact); +} diff --git a/lib/events/event.h b/lib/events/event.h index e0d83976..b7bbd83e 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -32,19 +32,23 @@ namespace QMatrixClient template <typename EventT> using event_ptr_tt = std::unique_ptr<EventT>; + /// Unwrap a plain pointer from a smart pointer template <typename EventT> - inline EventT* rawPtr(const event_ptr_tt<EventT>& ptr) // unwrap + inline EventT* rawPtr(const event_ptr_tt<EventT>& ptr) { return ptr.get(); } + /// Unwrap a plain pointer and downcast it to the specified type template <typename TargetEventT, typename EventT> inline TargetEventT* weakPtrCast(const event_ptr_tt<EventT>& ptr) { return static_cast<TargetEventT*>(rawPtr(ptr)); } + /// Re-wrap a smart pointer to base into a smart pointer to derived template <typename TargetT, typename SourceT> + [[deprecated("Consider using eventCast() or visit() instead")]] inline event_ptr_tt<TargetT> ptrCast(event_ptr_tt<SourceT>&& ptr) { return unique_ptr_cast<TargetT>(ptr); @@ -208,6 +212,9 @@ namespace QMatrixClient template <typename EventT> inline auto registerEventType() { + // Initialise exactly once, even if this function is called twice for + // the same type (for whatever reason - you never know the ways of + // static initialisation is done). static const auto _ = setupFactory<EventT>(); return _; // Only to facilitate usage in static initialisation } @@ -257,8 +264,18 @@ namespace QMatrixClient return fromJson<T>(contentJson()[key]); } + friend QDebug operator<<(QDebug dbg, const Event& e) + { + QDebugStateSaver _dss { dbg }; + dbg.noquote().nospace() + << e.matrixType() << '(' << e.type() << "): "; + e.dumpTo(dbg); + return dbg; + } + virtual bool isStateEvent() const { return false; } virtual bool isCallEvent() const { return false; } + virtual void dumpTo(QDebug dbg) const; protected: QJsonObject& editJson() { return _json; } @@ -327,7 +344,8 @@ namespace QMatrixClient -> decltype(static_cast<EventT*>(&*eptr)) { Q_ASSERT(eptr); - return is<EventT>(*eptr) ? static_cast<EventT*>(&*eptr) : nullptr; + return is<std::decay_t<EventT>>(*eptr) + ? static_cast<EventT*>(&*eptr) : nullptr; } // A single generic catch-all visitor @@ -359,7 +377,7 @@ namespace QMatrixClient visit(const BaseEventT& event, FnT&& visitor) { using event_type = fn_arg_t<FnT>; - if (is<event_type>(event)) + if (is<std::decay_t<event_type>>(event)) visitor(static_cast<event_type>(event)); } @@ -373,7 +391,7 @@ namespace QMatrixClient fn_return_t<FnT>&& defaultValue = {}) { using event_type = fn_arg_t<FnT>; - if (is<event_type>(event)) + if (is<std::decay_t<event_type>>(event)) return visitor(static_cast<event_type>(event)); return std::forward<fn_return_t<FnT>>(defaultValue); } @@ -386,7 +404,7 @@ namespace QMatrixClient FnTs&&... visitors) { using event_type1 = fn_arg_t<FnT1>; - if (is<event_type1>(event)) + if (is<std::decay_t<event_type1>>(event)) return visitor1(static_cast<event_type1&>(event)); return visit(event, std::forward<FnT2>(visitor2), std::forward<FnTs>(visitors)...); diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index a6b1c763..77f756cd 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -17,6 +17,8 @@ */ #include "eventcontent.h" + +#include "converters.h" #include "util.h" #include <QtCore/QMimeDatabase> @@ -30,7 +32,7 @@ QJsonObject Base::toJson() const return o; } -FileInfo::FileInfo(const QUrl& u, int payloadSize, const QMimeType& mimeType, +FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType, const QString& originalFilename) : mimeType(mimeType), url(u), payloadSize(payloadSize) , originalName(originalFilename) @@ -41,23 +43,31 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, : originalInfoJson(infoJson) , mimeType(QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) , url(u) - , payloadSize(infoJson["size"_ls].toInt()) + , payloadSize(fromJson<qint64>(infoJson["size"_ls])) , originalName(originalFilename) { if (!mimeType.isValid()) mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); } +bool FileInfo::isValid() const +{ + return url.scheme() == "mxc" + && (url.authority() + url.path()).count('/') == 1; +} + void FileInfo::fillInfoJson(QJsonObject* infoJson) const { Q_ASSERT(infoJson); - infoJson->insert(QStringLiteral("size"), payloadSize); - infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); + if (payloadSize != -1) + infoJson->insert(QStringLiteral("size"), payloadSize); + if (mimeType.isValid()) + infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); } -ImageInfo::ImageInfo(const QUrl& u, int fileSize, QMimeType mimeType, - const QSize& imageSize) - : FileInfo(u, fileSize, mimeType), imageSize(imageSize) +ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType, + const QSize& imageSize, const QString& originalFilename) + : FileInfo(u, fileSize, mimeType, originalFilename), imageSize(imageSize) { } ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, @@ -69,8 +79,10 @@ ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, void ImageInfo::fillInfoJson(QJsonObject* infoJson) const { FileInfo::fillInfoJson(infoJson); - infoJson->insert(QStringLiteral("w"), imageSize.width()); - infoJson->insert(QStringLiteral("h"), imageSize.height()); + if (imageSize.width() != -1) + infoJson->insert(QStringLiteral("w"), imageSize.width()); + if (imageSize.height() != -1) + infoJson->insert(QStringLiteral("h"), imageSize.height()); } Thumbnail::Thumbnail(const QJsonObject& infoJson) @@ -80,7 +92,9 @@ Thumbnail::Thumbnail(const QJsonObject& infoJson) void Thumbnail::fillInfoJson(QJsonObject* infoJson) const { - infoJson->insert(QStringLiteral("thumbnail_url"), url.toString()); - infoJson->insert(QStringLiteral("thumbnail_info"), - toInfoJson<ImageInfo>(*this)); + if (url.isValid()) + infoJson->insert(QStringLiteral("thumbnail_url"), url.toString()); + if (!imageSize.isEmpty()) + infoJson->insert(QStringLiteral("thumbnail_info"), + toInfoJson<ImageInfo>(*this)); } diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 91d7a8c8..ab31a75d 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -43,9 +43,10 @@ namespace QMatrixClient class Base { public: - explicit Base (const QJsonObject& o = {}) : originalJson(o) { } + explicit Base (QJsonObject o = {}) : originalJson(std::move(o)) { } virtual ~Base() = default; + // FIXME: make toJson() from converters.* work on base classes QJsonObject toJson() const; public: @@ -87,12 +88,14 @@ namespace QMatrixClient class FileInfo { public: - explicit FileInfo(const QUrl& u, int payloadSize = -1, + explicit FileInfo(const QUrl& u, qint64 payloadSize = -1, const QMimeType& mimeType = {}, const QString& originalFilename = {}); FileInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename = {}); + bool isValid() const; + void fillInfoJson(QJsonObject* infoJson) const; /** @@ -108,7 +111,7 @@ namespace QMatrixClient QJsonObject originalInfoJson; QMimeType mimeType; QUrl url; - int payloadSize; + qint64 payloadSize; QString originalName; }; @@ -126,9 +129,10 @@ namespace QMatrixClient class ImageInfo : public FileInfo { public: - explicit ImageInfo(const QUrl& u, int fileSize = -1, + explicit ImageInfo(const QUrl& u, qint64 fileSize = -1, QMimeType mimeType = {}, - const QSize& imageSize = {}); + const QSize& imageSize = {}, + const QString& originalFilename = {}); ImageInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename = {}); @@ -148,10 +152,10 @@ namespace QMatrixClient class Thumbnail : public ImageInfo { public: + Thumbnail() : ImageInfo(QUrl()) { } // To allow empty thumbnails Thumbnail(const QJsonObject& infoJson); - Thumbnail(const ImageInfo& info) - : ImageInfo(info) - { } + Thumbnail(const ImageInfo& info) : ImageInfo(info) { } + using ImageInfo::ImageInfo; /** * Writes thumbnail information to "thumbnail_info" subobject @@ -166,6 +170,7 @@ namespace QMatrixClient explicit TypedBase(const QJsonObject& o = {}) : Base(o) { } virtual QMimeType type() const = 0; virtual const FileInfo* fileInfo() const { return nullptr; } + virtual FileInfo* fileInfo() { return nullptr; } virtual const Thumbnail* thumbnailInfo() const { return nullptr; } }; @@ -183,9 +188,7 @@ namespace QMatrixClient class UrlBasedContent : public TypedBase, public InfoT { public: - UrlBasedContent(QUrl url, InfoT&& info, QString filename = {}) - : InfoT(url, std::forward<InfoT>(info), filename) - { } + using InfoT::InfoT; explicit UrlBasedContent(const QJsonObject& json) : TypedBase(json) , InfoT(json["url"].toString(), json["info"].toObject(), @@ -197,6 +200,7 @@ namespace QMatrixClient QMimeType type() const override { return InfoT::mimeType; } const FileInfo* fileInfo() const override { return this; } + FileInfo* fileInfo() override { return this; } protected: void fillJson(QJsonObject* json) const override @@ -213,7 +217,7 @@ namespace QMatrixClient class UrlWithThumbnailContent : public UrlBasedContent<InfoT> { public: - // TODO: POD constructor + using UrlBasedContent<InfoT>::UrlBasedContent; explicit UrlWithThumbnailContent(const QJsonObject& json) : UrlBasedContent<InfoT>(json) , thumbnail(InfoT::originalInfoJson) diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 3ee9a181..da663392 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -19,7 +19,6 @@ #pragma once #include "stateevent.h" -#include "converters.h" namespace QMatrixClient { namespace _impl { @@ -58,11 +57,15 @@ namespace QMatrixClient { matrixType); } - template <typename EventT> struct FromJsonObject<event_ptr_tt<EventT>> + template <typename EventT> struct JsonConverter<event_ptr_tt<EventT>> { - auto operator()(const QJsonObject& jo) const + static auto load(const QJsonValue& jv) { - return loadEvent<EventT>(jo); + return loadEvent<EventT>(jv.toObject()); + } + static auto load(const QJsonDocument& jd) + { + return loadEvent<EventT>(jd.object()); } }; } // namespace QMatrixClient diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index 491861b1..a43d3a85 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -18,8 +18,7 @@ #pragma once -#include "event.h" - +#include "stateevent.h" #include "eventcontent.h" namespace QMatrixClient diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp new file mode 100644 index 00000000..8fd0f1de --- /dev/null +++ b/lib/events/roomcreateevent.cpp @@ -0,0 +1,45 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "roomcreateevent.h" + +using namespace QMatrixClient; + +bool RoomCreateEvent::isFederated() const +{ + return fromJson<bool>(contentJson()["m.federate"_ls]); +} + +QString RoomCreateEvent::version() const +{ + return fromJson<QString>(contentJson()["room_version"_ls]); +} + +RoomCreateEvent::Predecessor RoomCreateEvent::predecessor() const +{ + const auto predJson = contentJson()["predecessor"_ls].toObject(); + return { + fromJson<QString>(predJson["room_id"_ls]), + fromJson<QString>(predJson["event_id"_ls]) + }; +} + +bool RoomCreateEvent::isUpgrade() const +{ + return contentJson().contains("predecessor"_ls); +} diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h new file mode 100644 index 00000000..0a8f27cc --- /dev/null +++ b/lib/events/roomcreateevent.h @@ -0,0 +1,49 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#include "stateevent.h" + +namespace QMatrixClient +{ + class RoomCreateEvent : public StateEventBase + { + public: + DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent) + + explicit RoomCreateEvent() + : StateEventBase(typeId(), matrixTypeId()) + { } + explicit RoomCreateEvent(const QJsonObject& obj) + : StateEventBase(typeId(), obj) + { } + + struct Predecessor + { + QString roomId; + QString eventId; + }; + + bool isFederated() const; + QString version() const; + Predecessor predecessor() const; + bool isUpgrade() const; + }; + REGISTER_EVENT_TYPE(RoomCreateEvent) +} diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 80d121de..3d03509f 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -42,10 +42,6 @@ RoomEvent::RoomEvent(Type type, const QJsonObject& json) _redactedBecause = makeEvent<RedactionEvent>(redaction.toObject()); return; } - - const auto& txnId = transactionId(); - if (!txnId.isEmpty()) - qCDebug(EVENTS) << "Event transactionId:" << txnId; } RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job @@ -90,7 +86,6 @@ void RoomEvent::setTransactionId(const QString& txnId) auto unsignedData = fullJson()[UnsignedKeyL].toObject(); unsignedData.insert(QStringLiteral("transaction_id"), txnId); editJson().insert(UnsignedKey, unsignedData); - qCDebug(EVENTS) << "New event transactionId:" << txnId; Q_ASSERT(transactionId() == txnId); } diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index eaa3302c..6da76526 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -23,20 +23,17 @@ #include <array> -using namespace QMatrixClient; - static const std::array<QString, 5> membershipStrings = { { QStringLiteral("invite"), QStringLiteral("join"), QStringLiteral("knock"), QStringLiteral("leave"), QStringLiteral("ban") } }; -namespace QMatrixClient -{ +namespace QMatrixClient { template <> - struct FromJson<MembershipType> + struct JsonConverter<MembershipType> { - MembershipType operator()(const QJsonValue& jv) const + static MembershipType load(const QJsonValue& jv) { const auto& membershipString = jv.toString(); for (auto it = membershipStrings.begin(); @@ -48,13 +45,14 @@ namespace QMatrixClient return MembershipType::Undefined; } }; - } +using namespace QMatrixClient; + MemberEventContent::MemberEventContent(const QJsonObject& json) : membership(fromJson<MembershipType>(json["membership"_ls])) , isDirect(json["is_direct"_ls].toBool()) - , displayName(json["displayname"_ls].toString()) + , displayName(sanitized(json["displayname"_ls].toString())) , avatarUrl(json["avatar_url"_ls].toString()) { } diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index db25d026..b8224033 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -29,13 +29,10 @@ namespace QMatrixClient enum MembershipType : size_t { Invite = 0, Join, Knock, Leave, Ban, Undefined }; - explicit MemberEventContent(MembershipType mt = MembershipType::Join) + explicit MemberEventContent(MembershipType mt = Join) : membership(mt) { } explicit MemberEventContent(const QJsonObject& json); - explicit MemberEventContent(const QJsonValue& jv) - : MemberEventContent(jv.toObject()) - { } MembershipType membership; bool isDirect = false; @@ -60,11 +57,19 @@ namespace QMatrixClient : StateEvent(typeId(), obj) { } RoomMemberEvent(MemberEventContent&& c) - : StateEvent(typeId(), matrixTypeId(), c.toJson()) + : StateEvent(typeId(), matrixTypeId(), c) { } - // This is a special constructor enabling RoomMemberEvent to be - // a base class for more specific member events. + /// A special constructor to create unknown RoomMemberEvents + /** + * This is needed in order to use RoomMemberEvent as a "base event + * class" in cases like GetMembersByRoomJob when RoomMemberEvents + * (rather than RoomEvents or StateEvents) are resolved from JSON. + * For such cases loadEvent<> requires an underlying class to be + * constructible with unknownTypeId() instead of its genuine id. + * Don't use it directly. + * \sa GetMembersByRoomJob, loadEvent, unknownTypeId + */ RoomMemberEvent(Type type, const QJsonObject& fullJson) : StateEvent(type, fullJson) { } @@ -84,6 +89,18 @@ namespace QMatrixClient private: REGISTER_ENUM(MembershipType) }; + + template <> + class EventFactory<RoomMemberEvent> + { + public: + static event_ptr_tt<RoomMemberEvent> make(const QJsonObject& json, + const QString&) + { + return makeEvent<RoomMemberEvent>(json); + } + }; + REGISTER_EVENT_TYPE(RoomMemberEvent) DEFINE_EVENTTYPE_ALIAS(RoomMember, RoomMemberEvent) } // namespace QMatrixClient diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 1c5cf058..8f4e0ebc 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -21,18 +21,38 @@ #include "logging.h" #include <QtCore/QMimeDatabase> +#include <QtCore/QFileInfo> +#include <QtGui/QImageReader> +#include <QtMultimedia/QMediaResource> using namespace QMatrixClient; 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 TextTypeKey = "m.text"; +static const auto NoticeTypeKey = "m.notice"; + +static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html"); + template <typename ContentT> TypedBase* make(const QJsonObject& json) { return new ContentT(json); } +template <> +TypedBase* make<TextContent>(const QJsonObject& json) +{ + return json.contains(FormattedBodyKey) || json.contains(RelatesToKey) + ? new TextContent(json) : nullptr; +} + struct MsgTypeDesc { QString matrixType; @@ -41,9 +61,9 @@ struct MsgTypeDesc }; const std::vector<MsgTypeDesc> msgTypes = - { { QStringLiteral("m.text"), MsgType::Text, make<TextContent> } + { { TextTypeKey, MsgType::Text, make<TextContent> } , { QStringLiteral("m.emote"), MsgType::Emote, make<TextContent> } - , { QStringLiteral("m.notice"), MsgType::Notice, make<TextContent> } + , { NoticeTypeKey, MsgType::Notice, make<TextContent> } , { QStringLiteral("m.image"), MsgType::Image, make<ImageContent> } , { QStringLiteral("m.file"), MsgType::File, make<FileContent> } , { QStringLiteral("m.location"), MsgType::Location, make<LocationContent> } @@ -71,22 +91,26 @@ MsgType jsonToMsgType(const QString& matrixType) return MsgType::Unknown; } -inline QJsonObject toMsgJson(const QString& plainBody, const QString& jsonMsgType, - TypedBase* content) +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"; + } json.insert(QStringLiteral("msgtype"), jsonMsgType); json.insert(QStringLiteral("body"), plainBody); return json; } -static const auto MsgTypeKey = "msgtype"_ls; -static const auto BodyKey = "body"_ls; - RoomMessageEvent::RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) : RoomEvent(typeId(), matrixTypeId(), - toMsgJson(plainBody, jsonMsgType, content)) + assembleContentJson(plainBody, jsonMsgType, content)) , _content(content) { } @@ -95,6 +119,40 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody, : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content) { } +TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) +{ + auto filePath = file.absoluteFilePath(); + auto localUrl = QUrl::fromLocalFile(filePath); + auto mimeType = QMimeDatabase().mimeTypeForFile(file); + if (!asGenericFile) + { + auto mimeTypeName = mimeType.name(); + if (mimeTypeName.startsWith("image/")) + return new ImageContent(localUrl, file.size(), mimeType, + QImageReader(filePath).size(), + file.fileName()); + + // duration can only be obtained asynchronously and can only be reliably + // done by starting to play the file. Left for a future implementation. + if (mimeTypeName.startsWith("video/")) + return new VideoContent(localUrl, file.size(), mimeType, + QMediaResource(localUrl).resolution(), + file.fileName()); + + if (mimeTypeName.startsWith("audio/")) + return new AudioContent(localUrl, file.size(), mimeType, + file.fileName()); + } + return new FileContent(localUrl, file.size(), mimeType, file.fileName()); +} + +RoomMessageEvent::RoomMessageEvent(const QString& plainBody, + const QFileInfo& file, bool asGenericFile) + : RoomMessageEvent(plainBody, + asGenericFile ? QStringLiteral("m.file") : rawMsgTypeForFile(file), + contentFromFile(file, asGenericFile)) +{ } + RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj), _content(nullptr) { @@ -104,13 +162,17 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) if ( content.contains(MsgTypeKey) && content.contains(BodyKey) ) { auto msgtype = content[MsgTypeKey].toString(); + bool msgTypeFound = false; for (const auto& mt: msgTypes) if (mt.matrixType == msgtype) + { _content.reset(mt.maker(content)); + msgTypeFound = true; + } - if (!_content) + if (!msgTypeFound) { - qCWarning(EVENTS) << "RoomMessageEvent: couldn't load content," + qCWarning(EVENTS) << "RoomMessageEvent: unknown msg_type," << " full content dump follows"; qCWarning(EVENTS) << formatJson << content; } @@ -142,14 +204,13 @@ QMimeType RoomMessageEvent::mimeType() const static const auto PlainTextMimeType = QMimeDatabase().mimeTypeForName("text/plain"); return _content ? _content->type() : PlainTextMimeType; - ; } bool RoomMessageEvent::hasTextContent() const { - return content() && + return !content() || (msgtype() == MsgType::Text || msgtype() == MsgType::Emote || - msgtype() == MsgType::Notice); // FIXME: Unbind from specific msgtypes + msgtype() == MsgType::Notice); } bool RoomMessageEvent::hasFileContent() const @@ -162,10 +223,31 @@ bool RoomMessageEvent::hasThumbnail() const return content() && content()->thumbnailInfo(); } -TextContent::TextContent(const QString& text, const QString& contentType) +QString rawMsgTypeForMimeType(const QMimeType& mimeType) +{ + auto name = mimeType.name(); + return name.startsWith("image/") ? QStringLiteral("m.image") : + name.startsWith("video/") ? QStringLiteral("m.video") : + name.startsWith("audio/") ? QStringLiteral("m.audio") : + QStringLiteral("m.file"); +} + +QString RoomMessageEvent::rawMsgTypeForUrl(const QUrl& url) +{ + return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForUrl(url)); +} + +QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi) +{ + return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi)); +} + +TextContent::TextContent(const QString& text, const QString& contentType, + Omittable<RelatesTo> relatesTo) : mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text) + , relatesTo(std::move(relatesTo)) { - if (contentType == "org.matrix.custom.html") + if (contentType == HtmlContentTypeId) mimeType = QMimeDatabase().mimeTypeForName("text/html"); } @@ -177,16 +259,20 @@ TextContent::TextContent(const QJsonObject& json) // Special-casing the custom matrix.org's (actually, Riot's) way // of sending HTML messages. - if (json["format"_ls].toString() == "org.matrix.custom.html") + if (json["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; - body = json["formatted_body"_ls].toString(); + body = json[FormattedBodyKey].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(); } + const auto replyJson = json[RelatesToKey].toObject() + .value(RelatesTo::ReplyTypeId()).toObject(); + if (!replyJson.isEmpty()) + relatesTo = replyTo(fromJson<QString>(replyJson[EventIdKeyL])); } void TextContent::fillJson(QJsonObject* json) const @@ -194,13 +280,16 @@ void TextContent::fillJson(QJsonObject* json) const Q_ASSERT(json); if (mimeType.inherits("text/html")) { - json->insert(QStringLiteral("format"), - QStringLiteral("org.matrix.custom.html")); + json->insert(QStringLiteral("format"), HtmlContentTypeId); json->insert(QStringLiteral("formatted_body"), body); } + if (!relatesTo.omitted()) + json->insert(QStringLiteral("m.relates_to"), + QJsonObject { { relatesTo->type, relatesTo->eventId } }); } -LocationContent::LocationContent(const QString& geoUri, const ImageInfo& thumbnail) +LocationContent::LocationContent(const QString& geoUri, + const Thumbnail& thumbnail) : geoUri(geoUri), thumbnail(thumbnail) { } diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 4c29a93e..c2e075eb 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -21,6 +21,8 @@ #include "roomevent.h" #include "eventcontent.h" +class QFileInfo; + namespace QMatrixClient { namespace MessageEventContent = EventContent; // Back-compatibility @@ -49,6 +51,9 @@ namespace QMatrixClient explicit RoomMessageEvent(const QString& plainBody, MsgType msgType = MsgType::Text, EventContent::TypedBase* content = nullptr); + explicit RoomMessageEvent(const QString& plainBody, + const QFileInfo& file, + bool asGenericFile = false); explicit RoomMessageEvent(const QJsonObject& obj); MsgType msgtype() const; @@ -56,14 +61,27 @@ namespace QMatrixClient QString plainBody() const; EventContent::TypedBase* content() const { return _content.data(); } + template <typename VisitorT> + void editContent(VisitorT visitor) + { + visitor(*_content); + editJson()[ContentKeyL] = + assembleContentJson(plainBody(), rawMsgtype(), content()); + } QMimeType mimeType() const; bool hasTextContent() const; bool hasFileContent() const; bool hasThumbnail() const; + static QString rawMsgTypeForUrl(const QUrl& url); + static QString rawMsgTypeForFile(const QFileInfo& fi); + private: QScopedPointer<EventContent::TypedBase> _content; + static QJsonObject assembleContentJson(const QString& plainBody, + const QString& jsonMsgType, EventContent::TypedBase* content); + REGISTER_ENUM(MsgType) }; REGISTER_EVENT_TYPE(RoomMessageEvent) @@ -74,6 +92,17 @@ namespace QMatrixClient { // Additional event content types + struct RelatesTo + { + static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; } + QString type; // The only supported relation so far + QString eventId; + }; + inline RelatesTo replyTo(QString eventId) + { + return { RelatesTo::ReplyTypeId(), std::move(eventId) }; + } + /** * Rich text content for m.text, m.emote, m.notice * @@ -83,13 +112,15 @@ namespace QMatrixClient class TextContent: public TypedBase { public: - TextContent(const QString& text, const QString& contentType); + TextContent(const QString& text, const QString& contentType, + Omittable<RelatesTo> relatesTo = none); explicit TextContent(const QJsonObject& json); QMimeType type() const override { return mimeType; } QMimeType mimeType; QString body; + Omittable<RelatesTo> relatesTo; protected: void fillJson(QJsonObject* json) const override; @@ -112,7 +143,7 @@ namespace QMatrixClient { public: LocationContent(const QString& geoUri, - const ImageInfo& thumbnail); + const Thumbnail& thumbnail = {}); explicit LocationContent(const QJsonObject& json); QMimeType type() const override; @@ -132,6 +163,7 @@ namespace QMatrixClient class PlayableContent : public ContentT { public: + using ContentT::ContentT; PlayableContent(const QJsonObject& json) : ContentT(json) , duration(ContentT::originalInfoJson["duration"_ls].toInt()) diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp new file mode 100644 index 00000000..9c3bafd4 --- /dev/null +++ b/lib/events/roomtombstoneevent.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "roomtombstoneevent.h" + +using namespace QMatrixClient; + +QString RoomTombstoneEvent::serverMessage() const +{ + return fromJson<QString>(contentJson()["body"_ls]); +} + +QString RoomTombstoneEvent::successorRoomId() const +{ + return fromJson<QString>(contentJson()["replacement_room"_ls]); +} diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h new file mode 100644 index 00000000..c7008ec4 --- /dev/null +++ b/lib/events/roomtombstoneevent.h @@ -0,0 +1,41 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#include "stateevent.h" + +namespace QMatrixClient +{ + class RoomTombstoneEvent : public StateEventBase + { + public: + DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent) + + explicit RoomTombstoneEvent() + : StateEventBase(typeId(), matrixTypeId()) + { } + explicit RoomTombstoneEvent(const QJsonObject& obj) + : StateEventBase(typeId(), obj) + { } + + QString serverMessage() const; + QString successorRoomId() const; + }; + REGISTER_EVENT_TYPE(RoomTombstoneEvent) +} diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 56be947c..2c23d9ca 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -19,7 +19,6 @@ #pragma once #include "stateevent.h" -#include "eventcontent.h" #include "converters.h" @@ -28,7 +27,7 @@ namespace QMatrixClient namespace EventContent { template <typename T> - class SimpleContent: public Base + class SimpleContent { public: using value_type = T; @@ -39,41 +38,39 @@ namespace QMatrixClient : value(std::forward<TT>(value)), key(std::move(keyName)) { } SimpleContent(const QJsonObject& json, QString keyName) - : Base(json) - , value(QMatrixClient::fromJson<T>(json[keyName])) + : value(fromJson<T>(json[keyName])) , key(std::move(keyName)) { } + QJsonObject toJson() const + { + return { { key, QMatrixClient::toJson(value) } }; + } public: T value; protected: QString key; - - private: - void fillJson(QJsonObject* json) const override - { - Q_ASSERT(json); - json->insert(key, QMatrixClient::toJson(value)); - } }; } // namespace EventContent -#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ - class _Name : public StateEvent<EventContent::SimpleContent<_ContentType>> \ +#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ + class _Name : public StateEvent<EventContent::SimpleContent<_ValueType>> \ { \ public: \ - using content_type = _ContentType; \ + using value_type = content_type::value_type; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(const QJsonObject& obj) \ - : StateEvent(typeId(), obj, QStringLiteral(#_ContentKey)) \ - { } \ + explicit _Name() : _Name(value_type()) { } \ template <typename T> \ explicit _Name(T&& value) \ : StateEvent(typeId(), matrixTypeId(), \ QStringLiteral(#_ContentKey), \ std::forward<T>(value)) \ { } \ + explicit _Name(QJsonObject obj) \ + : StateEvent(typeId(), std::move(obj), \ + QStringLiteral(#_ContentKey)) \ + { } \ auto _ContentKey() const { return content().value; } \ }; \ REGISTER_EVENT_TYPE(_Name) \ diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 877d0fae..a84f302b 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -20,17 +20,20 @@ using namespace QMatrixClient; +// Aside from the normal factory to instantiate StateEventBase inheritors +// StateEventBase itself can be instantiated if there's a state_key JSON key +// but the event type is unknown. [[gnu::unused]] static auto stateEventTypeInitialised = RoomEvent::factory_t::addMethod( [] (const QJsonObject& json, const QString& matrixType) -> StateEventPtr { - if (!json.contains("state_key")) + if (!json.contains("state_key"_ls)) return nullptr; if (auto e = StateEventBase::factory_t::make(json, matrixType)) return e; - return nullptr; + return makeEvent<StateEventBase>(unknownEventTypeId(), json); }); bool StateEventBase::repeatsState() const @@ -38,3 +41,18 @@ bool StateEventBase::repeatsState() const const auto prevContentJson = unsignedJson().value(PrevContentKeyL); return fullJson().value(ContentKeyL) == prevContentJson; } + +QString StateEventBase::replacedState() const +{ + return unsignedJson().value("replaces_state"_ls).toString(); +} + +void StateEventBase::dumpTo(QDebug dbg) const +{ + if (!stateKey().isEmpty()) + dbg << '<' << stateKey() << "> "; + if (unsignedJson().contains(PrevContentKeyL)) + dbg << QJsonDocument(unsignedJson()[PrevContentKeyL].toObject()) + .toJson(QJsonDocument::Compact) << " -> "; + RoomEvent::dumpTo(dbg); +} diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 6032132e..3f54f7bf 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -30,11 +30,24 @@ namespace QMatrixClient { ~StateEventBase() override = default; bool isStateEvent() const override { return true; } + QString replacedState() const; + void dumpTo(QDebug dbg) const override; + virtual bool repeatsState() const; }; using StateEventPtr = event_ptr_tt<StateEventBase>; using StateEvents = EventsArray<StateEventBase>; + template <> + inline bool is<StateEventBase>(const Event& e) { return e.isStateEvent(); } + + /** + * A combination of event type and state key uniquely identifies a piece + * of state in Matrix. + * \sa https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events + */ + using StateEventKey = QPair<QString, QString>; + template <typename ContentT> struct Prev { @@ -78,6 +91,12 @@ namespace QMatrixClient { } const ContentT& content() const { return _content; } + template <typename VisitorT> + void editContent(VisitorT&& visitor) + { + visitor(_content); + editJson()[ContentKeyL] = _content.toJson(); + } [[deprecated("Use prevContent instead")]] const ContentT* prev_content() const { return prevContent(); } const ContentT* prevContent() const @@ -85,8 +104,18 @@ namespace QMatrixClient { QString prevSenderId() const { return _prev ? _prev->senderId : QString(); } - protected: + private: ContentT _content; std::unique_ptr<Prev<ContentT>> _prev; }; } // namespace QMatrixClient + +namespace std { + template <> struct hash<QMatrixClient::StateEventKey> + { + size_t operator()(const QMatrixClient::StateEventKey& k) const Q_DECL_NOEXCEPT + { + return qHash(k); + } + }; +} diff --git a/lib/identity/definitions/request_email_validation.cpp b/lib/identity/definitions/request_email_validation.cpp index 95088bcb..47463a8b 100644 --- a/lib/identity/definitions/request_email_validation.cpp +++ b/lib/identity/definitions/request_email_validation.cpp @@ -6,28 +6,21 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const RequestEmailValidation& pod) +void JsonObjectConverter<RequestEmailValidation>::dumpTo( + QJsonObject& jo, const RequestEmailValidation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); addParam<>(jo, QStringLiteral("email"), pod.email); addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink); - return jo; } -RequestEmailValidation FromJsonObject<RequestEmailValidation>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<RequestEmailValidation>::fillFrom( + const QJsonObject& jo, RequestEmailValidation& result) { - RequestEmailValidation result; - result.clientSecret = - fromJson<QString>(jo.value("client_secret"_ls)); - result.email = - fromJson<QString>(jo.value("email"_ls)); - result.sendAttempt = - fromJson<int>(jo.value("send_attempt"_ls)); - result.nextLink = - fromJson<QString>(jo.value("next_link"_ls)); - - return result; + fromJson(jo.value("client_secret"_ls), result.clientSecret); + fromJson(jo.value("email"_ls), result.email); + fromJson(jo.value("send_attempt"_ls), result.sendAttempt); + fromJson(jo.value("next_link"_ls), result.nextLink); } diff --git a/lib/identity/definitions/request_email_validation.h b/lib/identity/definitions/request_email_validation.h index 3e72275f..eb7d8ed6 100644 --- a/lib/identity/definitions/request_email_validation.h +++ b/lib/identity/definitions/request_email_validation.h @@ -33,12 +33,10 @@ namespace QMatrixClient /// server will redirect the user to this URL. QString nextLink; }; - - QJsonObject toJson(const RequestEmailValidation& pod); - - template <> struct FromJsonObject<RequestEmailValidation> + template <> struct JsonObjectConverter<RequestEmailValidation> { - RequestEmailValidation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const RequestEmailValidation& pod); + static void fillFrom(const QJsonObject& jo, RequestEmailValidation& pod); }; } // namespace QMatrixClient diff --git a/lib/identity/definitions/request_msisdn_validation.cpp b/lib/identity/definitions/request_msisdn_validation.cpp index 125baa9c..a123d326 100644 --- a/lib/identity/definitions/request_msisdn_validation.cpp +++ b/lib/identity/definitions/request_msisdn_validation.cpp @@ -6,31 +6,23 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const RequestMsisdnValidation& pod) +void JsonObjectConverter<RequestMsisdnValidation>::dumpTo( + QJsonObject& jo, const RequestMsisdnValidation& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); addParam<>(jo, QStringLiteral("country"), pod.country); addParam<>(jo, QStringLiteral("phone_number"), pod.phoneNumber); addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink); - return jo; } -RequestMsisdnValidation FromJsonObject<RequestMsisdnValidation>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<RequestMsisdnValidation>::fillFrom( + const QJsonObject& jo, RequestMsisdnValidation& result) { - RequestMsisdnValidation result; - result.clientSecret = - fromJson<QString>(jo.value("client_secret"_ls)); - result.country = - fromJson<QString>(jo.value("country"_ls)); - result.phoneNumber = - fromJson<QString>(jo.value("phone_number"_ls)); - result.sendAttempt = - fromJson<int>(jo.value("send_attempt"_ls)); - result.nextLink = - fromJson<QString>(jo.value("next_link"_ls)); - - return result; + fromJson(jo.value("client_secret"_ls), result.clientSecret); + fromJson(jo.value("country"_ls), result.country); + fromJson(jo.value("phone_number"_ls), result.phoneNumber); + fromJson(jo.value("send_attempt"_ls), result.sendAttempt); + fromJson(jo.value("next_link"_ls), result.nextLink); } diff --git a/lib/identity/definitions/request_msisdn_validation.h b/lib/identity/definitions/request_msisdn_validation.h index 77bea2bc..b48ed6d5 100644 --- a/lib/identity/definitions/request_msisdn_validation.h +++ b/lib/identity/definitions/request_msisdn_validation.h @@ -36,12 +36,10 @@ namespace QMatrixClient /// server will redirect the user to this URL. QString nextLink; }; - - QJsonObject toJson(const RequestMsisdnValidation& pod); - - template <> struct FromJsonObject<RequestMsisdnValidation> + template <> struct JsonObjectConverter<RequestMsisdnValidation> { - RequestMsisdnValidation operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const RequestMsisdnValidation& pod); + static void fillFrom(const QJsonObject& jo, RequestMsisdnValidation& pod); }; } // namespace QMatrixClient diff --git a/lib/identity/definitions/sid.cpp b/lib/identity/definitions/sid.cpp index 443dbedf..1ba4b3b5 100644 --- a/lib/identity/definitions/sid.cpp +++ b/lib/identity/definitions/sid.cpp @@ -6,19 +6,15 @@ using namespace QMatrixClient; -QJsonObject QMatrixClient::toJson(const Sid& pod) +void JsonObjectConverter<Sid>::dumpTo( + QJsonObject& jo, const Sid& pod) { - QJsonObject jo; addParam<>(jo, QStringLiteral("sid"), pod.sid); - return jo; } -Sid FromJsonObject<Sid>::operator()(const QJsonObject& jo) const +void JsonObjectConverter<Sid>::fillFrom( + const QJsonObject& jo, Sid& result) { - Sid result; - result.sid = - fromJson<QString>(jo.value("sid"_ls)); - - return result; + fromJson(jo.value("sid"_ls), result.sid); } diff --git a/lib/identity/definitions/sid.h b/lib/identity/definitions/sid.h index eae60c47..ac8c4130 100644 --- a/lib/identity/definitions/sid.h +++ b/lib/identity/definitions/sid.h @@ -19,12 +19,10 @@ namespace QMatrixClient /// must not be empty. QString sid; }; - - QJsonObject toJson(const Sid& pod); - - template <> struct FromJsonObject<Sid> + template <> struct JsonObjectConverter<Sid> { - Sid operator()(const QJsonObject& jo) const; + static void dumpTo(QJsonObject& jo, const Sid& pod); + static void fillFrom(const QJsonObject& jo, Sid& pod); }; } // namespace QMatrixClient diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index b21173ae..0d9b9f10 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -186,7 +186,7 @@ QUrl BaseJob::makeRequestUrl(QUrl baseUrl, if (!pathBase.endsWith('/') && !path.startsWith('/')) pathBase.push_back('/'); - baseUrl.setPath( pathBase + path ); + baseUrl.setPath(pathBase + path, QUrl::TolerantMode); baseUrl.setQuery(query); return baseUrl; } @@ -325,9 +325,16 @@ void BaseJob::gotReply() d->status.code = UserConsentRequiredError; d->errorUrl = json.value("consent_uri"_ls).toString(); } - else if (!json.isEmpty()) // Not localisable on the client side - setStatus(IncorrectRequestError, - json.value("error"_ls).toString()); + else if (errCode == "M_UNSUPPORTED_ROOM_VERSION" || + errCode == "M_INCOMPATIBLE_ROOM_VERSION") + { + d->status.code = UnsupportedRoomVersionError; + if (json.contains("room_version")) + d->status.message = + tr("Requested room version: %1") + .arg(json.value("room_version").toString()); + } else if (!json.isEmpty()) // Not localisable on the client side + setStatus(d->status.code, json.value("error"_ls).toString()); } } @@ -422,12 +429,12 @@ BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) { d->rawResponse = reply->readAll(); - QJsonParseError error; + QJsonParseError error { 0, QJsonParseError::MissingObject }; const auto& json = QJsonDocument::fromJson(d->rawResponse, &error); if( error.error == QJsonParseError::NoError ) return parseJson(json); - else - return { IncorrectResponseError, error.errorString() }; + + return { IncorrectResponseError, error.errorString() }; } BaseJob::Status BaseJob::parseJson(const QJsonDocument&) @@ -519,8 +526,19 @@ BaseJob::Status BaseJob::status() const QByteArray BaseJob::rawData(int bytesAtMost) const { - return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost ? - d->rawResponse.left(bytesAtMost) + "...(truncated)" : d->rawResponse; + return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost + ? d->rawResponse.left(bytesAtMost) : d->rawResponse; +} + +QString BaseJob::rawDataSample(int bytesAtMost) const +{ + auto data = rawData(bytesAtMost); + Q_ASSERT(data.size() <= d->rawResponse.size()); + return data.size() == d->rawResponse.size() + ? data : data + tr("...(truncated, %Ln bytes in total)", + "Comes after trimmed raw network response", + d->rawResponse.size()); + } QString BaseJob::statusCaption() const @@ -557,6 +575,8 @@ QString BaseJob::statusCaption() const return tr("Network authentication required"); case UserConsentRequiredError: return tr("User consent required"); + case UnsupportedRoomVersionError: + return tr("The server does not support the needed room version"); default: return tr("Request failed"); } @@ -579,10 +599,25 @@ QUrl BaseJob::errorUrl() const void BaseJob::setStatus(Status s) { + // The crash that led to this code has been reported in + // https://github.com/QMatrixClient/Quaternion/issues/566 - basically, + // when cleaning up childrent of a deleted Connection, there's a chance + // of pending jobs being abandoned, calling setStatus(Abandoned). + // There's nothing wrong with this; however, the safety check for + // cleartext access tokens below uses d->connection - which is a dangling + // pointer. + // To alleviate that, a stricter condition is applied, that for Abandoned + // and to-be-Abandoned jobs the status message will be disregarded entirely. + // For 0.6 we might rectify the situation by making d->connection + // a QPointer<> (and derive ConnectionData from QObject, respectively). + if (d->status.code == Abandoned || s.code == Abandoned) + s.message.clear(); + if (d->status == s) return; - if (!d->connection->accessToken().isEmpty()) + if (!s.message.isEmpty() + && d->connection && !d->connection->accessToken().isEmpty()) s.message.replace(d->connection->accessToken(), "(REDACTED)"); if (!s.good()) qCWarning(d->logCat) << this << "status" << s; @@ -597,9 +632,8 @@ void BaseJob::setStatus(int code, QString message) void BaseJob::abandon() { - beforeAbandon(d->reply.data()); + beforeAbandon(d->reply ? d->reply.data() : nullptr); setStatus(Abandoned); - this->disconnect(); if (d->reply) d->reply->disconnect(this); emit finished(this); diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 4ef25ab8..4c1c7706 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -64,6 +64,7 @@ namespace QMatrixClient , IncorrectResponseError , TooManyRequestsError , RequestNotImplementedError + , UnsupportedRoomVersionError , NetworkAuthRequiredError , UserConsentRequiredError , UserDefinedError = 200 @@ -138,8 +139,20 @@ namespace QMatrixClient Status status() const; /** Short human-friendly message on the job status */ QString statusCaption() const; - /** Raw response body as received from the server */ + /** Get raw response body as received from the server + * \param bytesAtMost return this number of leftmost bytes, or -1 + * to return the entire response + */ QByteArray rawData(int bytesAtMost = -1) const; + /** Get UI-friendly sample of raw data + * + * This is almost the same as rawData but appends the "truncated" + * suffix if not all data fit in bytesAtMost. This call is + * recommended to present a sample of raw data as "details" next to + * error messages. Note that the default \p bytesAtMost value is + * also tailored to UI cases. + */ + QString rawDataSample(int bytesAtMost = 65535) const; /** Error (more generally, status) code * Equivalent to status().code @@ -203,9 +216,9 @@ namespace QMatrixClient * * In general, to be notified of a job's completion, client code * should connect to result(), success(), or failure() - * rather than finished(). However if you store a list of jobs - * and need to track their lifecycle, then you should connect to this - * instead of result(), to avoid dangling pointers in your list. + * rather than finished(). However if you need to track the job's + * lifecycle you should connect to this instead of result(); + * in particular, only this signal will be emitted on abandoning. * * @param job the job that emitted this signal * diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 2bf9dd8f..672a7b2d 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -22,7 +22,8 @@ class DownloadFileJob::Private QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri) { - return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1)); + return makeRequestUrl( + std::move(baseUrl), mxcUri.authority(), mxcUri.path().mid(1)); } DownloadFileJob::DownloadFileJob(const QString& serverName, @@ -31,7 +32,7 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, : GetContentJob(serverName, mediaId) , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) { - setObjectName("DownloadFileJob"); + setObjectName(QStringLiteral("DownloadFileJob")); } QString DownloadFileJob::targetFileName() const diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp index aeb49839..edb9b156 100644 --- a/lib/jobs/mediathumbnailjob.cpp +++ b/lib/jobs/mediathumbnailjob.cpp @@ -59,5 +59,5 @@ BaseJob::Status MediaThumbnailJob::parseReply(QNetworkReply* reply) if( _thumbnail.loadFromData(data()->readAll()) ) return Success; - return { IncorrectResponseError, "Could not read image data" }; + return { IncorrectResponseError, QStringLiteral("Could not read image data") }; } diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp index 6baf388e..84385b55 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -18,10 +18,6 @@ #include "syncjob.h" -#include "events/eventloader.h" - -#include <QtCore/QElapsedTimer> - using namespace QMatrixClient; static size_t jobId = 0; @@ -46,111 +42,21 @@ SyncJob::SyncJob(const QString& since, const QString& filter, int timeout, setMaxRetries(std::numeric_limits<int>::max()); } -QString SyncData::nextBatch() const -{ - return nextBatch_; -} - -SyncDataList&& SyncData::takeRoomData() -{ - return std::move(roomData); -} - -Events&& SyncData::takePresenceData() -{ - return std::move(presenceData); -} - -Events&& SyncData::takeAccountData() -{ - return std::move(accountData); -} - -Events&&SyncData::takeToDeviceEvents() -{ - return std::move(toDeviceEvents); -} - -template <typename EventsArrayT, typename StrT> -inline EventsArrayT load(const QJsonObject& batches, StrT keyName) -{ - return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls)); -} +SyncJob::SyncJob(const QString& since, const Filter& filter, + int timeout, const QString& presence) + : SyncJob(since, + QJsonDocument(toJson(filter)).toJson(QJsonDocument::Compact), + timeout, presence) +{ } BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) { - return d.parseJson(data); -} - -BaseJob::Status SyncData::parseJson(const QJsonDocument &data) -{ - QElapsedTimer et; et.start(); - - auto json = data.object(); - nextBatch_ = json.value("next_batch"_ls).toString(); - presenceData = load<Events>(json, "presence"_ls); - accountData = load<Events>(json, "account_data"_ls); - toDeviceEvents = load<Events>(json, "to_device"_ls); + d.parseJson(data.object()); + if (d.unresolvedRooms().isEmpty()) + return BaseJob::Success; - auto rooms = json.value("rooms"_ls).toObject(); - JoinStates::Int ii = 1; // ii is used to make a JoinState value - auto totalRooms = 0; - auto totalEvents = 0; - for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) - { - const auto rs = rooms.value(JoinStateStrings[i]).toObject(); - // We have a Qt container on the right and an STL one on the left - roomData.reserve(static_cast<size_t>(rs.size())); - for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) - { - roomData.emplace_back(roomIt.key(), JoinState(ii), - roomIt.value().toObject()); - const auto& r = roomData.back(); - totalEvents += r.state.size() + r.ephemeral.size() + - r.accountData.size() + r.timeline.size(); - } - totalRooms += rs.size(); - } - if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" - << totalRooms << "room(s)," - << totalEvents << "event(s) in" << et; - return BaseJob::Success; + qCCritical(MAIN).noquote() << "Incomplete sync response, missing rooms:" + << d.unresolvedRooms().join(','); + return BaseJob::IncorrectResponseError; } -const QString SyncRoomData::UnreadCountKey = - QStringLiteral("x-qmatrixclient.unread_count"); - -SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, - const QJsonObject& room_) - : roomId(roomId_) - , joinState(joinState_) - , state(load<StateEvents>(room_, - joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls)) -{ - switch (joinState) { - case JoinState::Join: - ephemeral = load<Events>(room_, "ephemeral"_ls); - FALLTHROUGH; - case JoinState::Leave: - { - accountData = load<Events>(room_, "account_data"_ls); - timeline = load<RoomEvents>(room_, "timeline"_ls); - const auto timelineJson = room_.value("timeline"_ls).toObject(); - timelineLimited = timelineJson.value("limited"_ls).toBool(); - timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString(); - - break; - } - default: /* nothing on top of state */; - } - - const auto unreadJson = room_.value("unread_notifications"_ls).toObject(); - unreadCount = unreadJson.value(UnreadCountKey).toInt(-2); - highlightCount = unreadJson.value("highlight_count"_ls).toInt(); - notificationCount = unreadJson.value("notification_count"_ls).toInt(); - if (highlightCount > 0 || notificationCount > 0) - qCDebug(SYNCJOB) << "Room" << roomId_ - << "has highlights:" << highlightCount - << "and notifications:" << notificationCount; -} diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h index 6b9bedfa..036b25d0 100644 --- a/lib/jobs/syncjob.h +++ b/lib/jobs/syncjob.h @@ -20,62 +20,19 @@ #include "basejob.h" -#include "joinstate.h" -#include "events/stateevent.h" -#include "util.h" +#include "../syncdata.h" +#include "../csapi/definitions/sync_filter.h" namespace QMatrixClient { - class SyncRoomData - { - public: - QString roomId; - JoinState joinState; - StateEvents state; - RoomEvents timeline; - Events ephemeral; - Events accountData; - - bool timelineLimited; - QString timelinePrevBatch; - int unreadCount; - int highlightCount; - int notificationCount; - - SyncRoomData(const QString& roomId, JoinState joinState_, - const QJsonObject& room_); - SyncRoomData(SyncRoomData&&) = default; - SyncRoomData& operator=(SyncRoomData&&) = default; - - static const QString UnreadCountKey; - }; - // QVector cannot work with non-copiable objects, std::vector can. - using SyncDataList = std::vector<SyncRoomData>; - - class SyncData - { - public: - BaseJob::Status parseJson(const QJsonDocument &data); - Events&& takePresenceData(); - Events&& takeAccountData(); - Events&& takeToDeviceEvents(); - SyncDataList&& takeRoomData(); - QString nextBatch() const; - - private: - QString nextBatch_; - Events presenceData; - Events accountData; - Events toDeviceEvents; - SyncDataList roomData; - }; - class SyncJob: public BaseJob { public: explicit SyncJob(const QString& since = {}, const QString& filter = {}, int timeout = -1, const QString& presence = {}); + explicit SyncJob(const QString& since, const Filter& filter, + int timeout = -1, const QString& presence = {}); SyncData &&takeData() { return std::move(d); } diff --git a/lib/joinstate.h b/lib/joinstate.h index c172f576..379183f6 100644 --- a/lib/joinstate.h +++ b/lib/joinstate.h @@ -24,7 +24,7 @@ namespace QMatrixClient { - enum class JoinState + enum class JoinState : unsigned int { Join = 0x1, Invite = 0x2, diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index 89967a8a..7d9cb360 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -29,7 +29,8 @@ class NetworkAccessManager::Private QList<QSslError> ignoredSslErrors; }; -NetworkAccessManager::NetworkAccessManager(QObject* parent) : d(std::make_unique<Private>()) +NetworkAccessManager::NetworkAccessManager(QObject* parent) + : QNetworkAccessManager(parent), d(std::make_unique<Private>()) { } QList<QSslError> NetworkAccessManager::ignoredSslErrors() const diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp index 48bd09f3..6ff2bc1f 100644 --- a/lib/networksettings.cpp +++ b/lib/networksettings.cpp @@ -27,5 +27,5 @@ void NetworkSettings::setupApplicationProxy() const } QMC_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType) -QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", "", setProxyHostName) +QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", {}, setProxyHostName) QMC_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort) diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h new file mode 100644 index 00000000..c2bde8df --- /dev/null +++ b/lib/qt_connection_util.h @@ -0,0 +1,107 @@ +/****************************************************************************** + * Copyright (C) 2019 Kitsune Ral <kitsune-ral@users.sf.net> + * + * 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 "util.h" + +#include <QtCore/QPointer> + +namespace QMatrixClient { + namespace _impl { + template <typename SenderT, typename SignalT, + typename ContextT, typename... ArgTs> + inline QMetaObject::Connection connectUntil( + SenderT* sender, SignalT signal, ContextT* context, + std::function<bool(ArgTs...)> slot, Qt::ConnectionType connType) + { + // See https://bugreports.qt.io/browse/QTBUG-60339 +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) + auto pc = std::make_shared<QMetaObject::Connection>(); +#else + auto pc = std::make_unique<QMetaObject::Connection>(); +#endif + auto& c = *pc; // Resolve a reference before pc is moved to lambda + c = QObject::connect(sender, signal, context, + [pc=std::move(pc),slot] (ArgTs... args) { + Q_ASSERT(*pc); // If it's been triggered, it should exist + if (slot(std::forward<ArgTs>(args)...)) + QObject::disconnect(*pc); + }, connType); + return c; + } + } + + template <typename SenderT, typename SignalT, + typename ContextT, typename FunctorT> + inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context, + const FunctorT& slot, + Qt::ConnectionType connType = Qt::AutoConnection) + { + return _impl::connectUntil(sender, signal, context, + typename function_traits<FunctorT>::function_type(slot), + connType); + } + + /** Create a single-shot connection that triggers on the signal and + * then self-disconnects + * + * Only supports DirectConnection type. + */ + template <typename SenderT, typename SignalT, + typename ReceiverT, typename SlotT> + inline auto connectSingleShot(SenderT* sender, SignalT signal, + ReceiverT* receiver, SlotT slot) + { + QMetaObject::Connection connection; + connection = QObject::connect(sender, signal, receiver, slot, + Qt::DirectConnection); + Q_ASSERT(connection); + QObject::connect(sender, signal, receiver, + [connection] { QObject::disconnect(connection); }, + Qt::DirectConnection); + return connection; + } + + /** A guard pointer that disconnects an interested object upon destruction + * It's almost QPointer<> except that you have to initialise it with one + * more additional parameter - a pointer to a QObject that will be + * disconnected from signals of the underlying pointer upon the guard's + * destruction. + */ + template <typename T> + class ConnectionsGuard : public QPointer<T> + { + public: + ConnectionsGuard(T* publisher, QObject* subscriber) + : QPointer<T>(publisher), subscriber(subscriber) + { } + ~ConnectionsGuard() + { + if (*this) + (*this)->disconnect(subscriber); + } + ConnectionsGuard(ConnectionsGuard&&) = default; + ConnectionsGuard& operator=(ConnectionsGuard&&) = default; + Q_DISABLE_COPY(ConnectionsGuard) + using QPointer<T>::operator=; + + private: + QObject* subscriber; + }; +} diff --git a/lib/room.cpp b/lib/room.cpp index ea771f17..9e7ff8d2 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -25,11 +25,14 @@ #include "csapi/receipts.h" #include "csapi/redaction.h" #include "csapi/account-data.h" -#include "csapi/message_pagination.h" #include "csapi/room_state.h" #include "csapi/room_send.h" +#include "csapi/rooms.h" #include "csapi/tags.h" +#include "csapi/room_upgrades.h" #include "events/simplestateevents.h" +#include "events/roomcreateevent.h" +#include "events/roomtombstoneevent.h" #include "events/roomavatarevent.h" #include "events/roommemberevent.h" #include "events/typingevent.h" @@ -46,13 +49,15 @@ #include "connection.h" #include "user.h" #include "converters.h" +#include "syncdata.h" #include <QtCore/QHash> #include <QtCore/QStringBuilder> // for efficient string concats (operator%) -#include <QtCore/QElapsedTimer> #include <QtCore/QPointer> #include <QtCore/QDir> #include <QtCore/QTemporaryFile> +#include <QtCore/QRegularExpression> +#include <QtCore/QMimeDatabase> #include <array> #include <functional> @@ -67,12 +72,6 @@ using std::llround; enum EventsPlacement : int { Older = -1, Newer = 1 }; -// A workaround for MSVC 2015 that fails with "error C2440: 'return': -// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'" -#if (defined(_MSC_VER) && _MSC_VER < 1910) || (defined(__GNUC__) && __GNUC__ <= 4) -# define WORKAROUND_EXTENDED_INITIALIZER_LIST -#endif - class Room::Private { public: @@ -86,29 +85,27 @@ class Room::Private Room* q; - // This updates the room displayname field (which is the way a room - // should be shown in the room list) It should be called whenever the - // list of members or the room name (m.room.name) or canonical alias change. - void updateDisplayname(); - Connection* connection; + QString id; + JoinState joinState; + RoomSummary summary = { none, 0, none }; + /// The state of the room at timeline position before-0 + /// \sa timelineBase + std::unordered_map<StateEventKey, StateEventPtr> baseState; + /// The state of the room at timeline position after-maxTimelineIndex() + /// \sa Room::syncEdge + QHash<StateEventKey, const StateEventBase*> currentState; Timeline timeline; PendingEvents unsyncedEvents; QHash<QString, TimelineItem::index_t> eventsIndex; - QString id; - QStringList aliases; - QString canonicalAlias; - QString name; QString displayname; - QString topic; - QString encryptionAlgorithm; Avatar avatar; - JoinState joinState; int highlightCount = 0; int notificationCount = 0; members_map_t membersMap; QList<User*> usersTyping; QMultiHash<QString, User*> eventIdReadUsers; + QList<User*> usersInvited; QList<User*> membersLeft; int unreadMessages = 0; bool displayed = false; @@ -120,18 +117,21 @@ class Room::Private std::unordered_map<QString, EventPtr> accountData; QString prevBatch; QPointer<GetRoomEventsJob> eventsHistoryJob; + QPointer<GetMembersByRoomJob> allMembersJob; struct FileTransferPrivateInfo { -#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST FileTransferPrivateInfo() = default; - FileTransferPrivateInfo(BaseJob* j, QString fileName) - : job(j), localFileInfo(fileName) + FileTransferPrivateInfo(BaseJob* j, const QString& fileName, + bool isUploading = false) + : status(FileTransferInfo::Started), job(j) + , localFileInfo(fileName), isUpload(isUploading) { } -#endif + + FileTransferInfo::Status status = FileTransferInfo::None; QPointer<BaseJob> job = nullptr; QFileInfo localFileInfo { }; - FileTransferInfo::Status status = FileTransferInfo::Started; + bool isUpload = false; qint64 progress = 0; qint64 total = -1; @@ -164,13 +164,37 @@ class Room::Private const RoomMessageEvent* getEventWithFile(const QString& eventId) const; QString fileNameToDownload(const RoomMessageEvent* event) const; + Changes setSummary(RoomSummary&& newSummary); + //void inviteUser(User* u); // We might get it at some point in time. void insertMemberIntoMap(User* u); - void renameMember(User* u, QString oldName); + void renameMember(User* u, const QString& oldName); void removeMemberFromMap(const QString& username, User* u); + // This updates the room displayname field (which is the way a room + // should be shown in the room list); called whenever the list of + // members, the room name (m.room.name) or canonical alias change. + void updateDisplayname(); + // This is used by updateDisplayname() but only calculates the new name + // without any updates. + QString calculateDisplayname() const; + + /// A point in the timeline corresponding to baseState + rev_iter_t timelineBase() const { return q->findInTimeline(-1); } + void getPreviousContent(int limit = 10); + template <typename EventT> + const EventT* getCurrentState(const QString& stateKey = {}) const + { + static const EventT empty; + const auto* evt = + currentState.value({EventT::matrixTypeId(), stateKey}, &empty); + Q_ASSERT(evt->type() == EventT::typeId() && + evt->matrixType() == EventT::matrixTypeId()); + return static_cast<const EventT*>(evt); + } + bool isEventNotable(const TimelineItem& ti) const { return !ti->isRedacted() && @@ -178,7 +202,29 @@ class Room::Private is<RoomMessageEvent>(*ti); } - void addNewMessageEvents(RoomEvents&& events); + template <typename EventArrayT> + Changes updateStateFrom(EventArrayT&& events) + { + Changes changes = NoChange; + if (!events.empty()) + { + QElapsedTimer et; et.start(); + for (auto&& eptr: events) + { + const auto& evt = *eptr; + Q_ASSERT(evt.isStateEvent()); + // Update baseState afterwards to make sure that the old state + // is valid and usable inside processStateEvent + changes |= q->processStateEvent(evt); + baseState[{evt.matrixType(),evt.stateKey()}] = move(eptr); + } + if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** Room::Private::updateStateFrom():" + << events.size() << "event(s)," << et; + } + return changes; + } + Changes addNewMessageEvents(RoomEvents&& events); void addHistoricalMessageEvents(RoomEvents&& events); /** Move events into the timeline @@ -190,20 +236,22 @@ class Room::Private * @param placement - position and direction of insertion: Older for * historical messages, Newer for new ones */ - Timeline::difference_type moveEventsToTimeline(RoomEventsRange events, - EventsPlacement placement); + Timeline::size_type moveEventsToTimeline(RoomEventsRange events, + EventsPlacement placement); /** * Remove events from the passed container that are already in the timeline */ void dropDuplicateEvents(RoomEvents& events) const; - void setLastReadEvent(User* u, QString eventId); + Changes setLastReadEvent(User* u, QString eventId); void updateUnreadCount(rev_iter_t from, rev_iter_t to); - void promoteReadMarker(User* u, rev_iter_t newMarker, - bool force = false); + Changes promoteReadMarker(User* u, rev_iter_t newMarker, + bool force = false); - void markMessagesAsRead(rev_iter_t upToMarker); + Changes markMessagesAsRead(rev_iter_t upToMarker); + + void getAllMembers(); QString sendEvent(RoomEventPtr&& event); @@ -213,17 +261,23 @@ class Room::Private return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...)); } + RoomEvent* addAsPending(RoomEventPtr&& event); + QString doSendEvent(const RoomEvent* pEvent); - PendingEvents::iterator findAsPending(const RoomEvent* rawEvtPtr); - void onEventSendingFailure(const RoomEvent* pEvent, - const QString& txnId, BaseJob* call = nullptr); + void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); template <typename EvT> - auto requestSetState(const QString& stateKey, const EvT& event) + SetRoomStateWithKeyJob* requestSetState(const QString& stateKey, + const EvT& event) { - // TODO: Queue up state events sending (see #133). - return connection->callApi<SetRoomStateWithKeyJob>( + if (q->successorId().isEmpty()) + { + // TODO: Queue up state events sending (see #133). + return connection->callApi<SetRoomStateWithKeyJob>( id, EvT::matrixTypeId(), stateKey, event.contentJson()); + } + qCWarning(MAIN) << q << "has been upgraded, state won't be set"; + return nullptr; } template <typename EvT> @@ -246,8 +300,10 @@ class Room::Private QJsonObject toJson() const; private: - QString calculateDisplayname() const; - QString roomNameFromMemberNames(const QList<User*>& userlist) const; + using users_shortlist_t = std::array<User*, 3>; + template<typename ContT> + users_shortlist_t buildShortlist(const ContT& users) const; + users_shortlist_t buildShortlist(const QStringList& userIds) const; bool isLocalUser(const User* u) const { @@ -262,9 +318,13 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) // See "Accessing the Public Class" section in // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ d->q = this; - connect(this, &Room::userAdded, this, &Room::memberListChanged); - connect(this, &Room::userRemoved, this, &Room::memberListChanged); - connect(this, &Room::memberRenamed, this, &Room::memberListChanged); + d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name + connectUntil(connection, &Connection::loadedRoomState, this, + [this] (Room* r) { + if (this == r) + emit baseStateLoaded(); + return this == r; // loadedRoomState fires only once per room + }); qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id; } @@ -278,6 +338,28 @@ const QString& Room::id() const return d->id; } +QString Room::version() const +{ + const auto v = d->getCurrentState<RoomCreateEvent>()->version(); + return v.isEmpty() ? QStringLiteral("1") : v; +} + +bool Room::isUnstable() const +{ + return !connection()->loadingCapabilities() && + !connection()->stableRoomVersions().contains(version()); +} + +QString Room::predecessorId() const +{ + return d->getCurrentState<RoomCreateEvent>()->predecessor().roomId; +} + +QString Room::successorId() const +{ + return d->getCurrentState<RoomTombstoneEvent>()->successorRoomId(); +} + const Room::Timeline& Room::messageEvents() const { return d->timeline; @@ -290,17 +372,17 @@ const Room::PendingEvents& Room::pendingEvents() const QString Room::name() const { - return d->name; + return d->getCurrentState<RoomNameEvent>()->name(); } QStringList Room::aliases() const { - return d->aliases; + return d->getCurrentState<RoomAliasesEvent>()->aliases(); } QString Room::canonicalAlias() const { - return d->canonicalAlias; + return d->getCurrentState<RoomCanonicalAliasEvent>()->alias(); } QString Room::displayName() const @@ -308,9 +390,14 @@ QString Room::displayName() const return d->displayname; } +void Room::refreshDisplayName() +{ + d->updateDisplayname(); +} + QString Room::topic() const { - return d->topic; + return d->getCurrentState<RoomTopicEvent>()->topic(); } QString Room::avatarMediaId() const @@ -323,6 +410,11 @@ QUrl Room::avatarUrl() const return d->avatar.url(); } +const Avatar& Room::avatarObject() const +{ + return d->avatar; +} + QImage Room::avatar(int dimension) { return avatar(dimension, dimension); @@ -368,14 +460,15 @@ void Room::setJoinState(JoinState state) d->joinState = state; qCDebug(MAIN) << "Room" << id() << "changed state: " << int(oldState) << "->" << int(state); + emit changed(Change::JoinStateChange); emit joinStateChanged(oldState, state); } -void Room::Private::setLastReadEvent(User* u, QString eventId) +Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) { auto& storedId = lastReadEventIds[u]; if (storedId == eventId) - return; + return Change::NoChange; eventIdReadUsers.remove(storedId, u); eventIdReadUsers.insert(eventId, u); swap(storedId, eventId); @@ -386,7 +479,9 @@ void Room::Private::setLastReadEvent(User* u, QString eventId) if (storedId != serverReadMarker) connection->callApi<PostReadMarkersJob>(id, storedId); emit q->readMarkerMoved(eventId, storedId); + return Change::ReadMarkerChange; } + return Change::NoChange; } void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) @@ -429,14 +524,15 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) } } -void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) +Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, + bool force) { Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr"); Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend()); const auto prevMarker = q->readMarker(u); if (!force && prevMarker <= newMarker) // Remember, we deal with reverse iterators - return; + return Change::NoChange; Q_ASSERT(newMarker < timeline.crend()); @@ -445,13 +541,13 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) auto eagerMarker = find_if(newMarker.base(), timeline.cend(), [=](const TimelineItem& ti) { return ti->senderId() != u->id(); }); - setLastReadEvent(u, (*(eagerMarker - 1))->id()); + auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id()); if (isLocalUser(u)) { const auto oldUnreadCount = unreadMessages; QElapsedTimer et; et.start(); - unreadMessages = count_if(eagerMarker, timeline.cend(), - std::bind(&Room::Private::isEventNotable, this, _1)); + unreadMessages = int(count_if(eagerMarker, timeline.cend(), + std::bind(&Room::Private::isEventNotable, this, _1))); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Recounting unread messages took" << et; @@ -469,14 +565,16 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) qCDebug(MAIN) << "Room" << displayname << "still has" << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); + changes |= Change::UnreadNotifsChange; } } + return changes; } -void Room::Private::markMessagesAsRead(rev_iter_t upToMarker) +Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker) { const auto prevMarker = q->readMarker(); - promoteReadMarker(q->localUser(), upToMarker); + auto changes = promoteReadMarker(q->localUser(), upToMarker); if (prevMarker != upToMarker) qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker(); @@ -487,11 +585,12 @@ void Room::Private::markMessagesAsRead(rev_iter_t upToMarker) { if ((*upToMarker)->senderId() != q->localUser()->id()) { - connection->callApi<PostReceiptJob>(id, "m.read", - (*upToMarker)->id()); + connection->callApi<PostReceiptJob>(id, QStringLiteral("m.read"), + QUrl::toPercentEncoding((*upToMarker)->id())); break; } } + return changes; } void Room::markMessagesAsRead(QString uptoEventId) @@ -505,6 +604,29 @@ void Room::markAllMessagesAsRead() d->markMessagesAsRead(d->timeline.crbegin()); } +bool Room::canSwitchVersions() const +{ + if (!successorId().isEmpty()) + return false; // Noone can upgrade a room that's already upgraded + + // TODO, #276: m.room.power_levels + const auto* plEvt = + d->currentState.value({QStringLiteral("m.room.power_levels"), {}}); + if (!plEvt) + return true; + + const auto plJson = plEvt->contentJson(); + const auto currentUserLevel = + plJson.value("users"_ls).toObject() + .value(localUser()->id()).toInt( + plJson.value("users_default"_ls).toInt()); + const auto tombstonePowerLevel = + plJson.value("events"_ls).toObject() + .value("m.room.tombstone"_ls).toInt( + plJson.value("state_default"_ls).toInt()); + return currentUserLevel >= tombstonePowerLevel; +} + bool Room::hasUnreadMessages() const { return unreadCount() >= 0; @@ -515,11 +637,21 @@ int Room::unreadCount() const return d->unreadMessages; } -Room::rev_iter_t Room::timelineEdge() const +Room::rev_iter_t Room::historyEdge() const { return d->timeline.crend(); } +Room::Timeline::const_iterator Room::syncEdge() const +{ + return d->timeline.cend(); +} + +Room::rev_iter_t Room::timelineEdge() const +{ + return historyEdge(); +} + TimelineItem::index_t Room::minTimelineIndex() const { return d->timeline.empty() ? 0 : d->timeline.front().index(); @@ -554,6 +686,44 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const return timelineEdge(); } +Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId) +{ + return std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), + [txnId] (const auto& item) { return item->transactionId() == txnId; }); +} + +Room::PendingEvents::const_iterator +Room::findPendingEvent(const QString& txnId) const +{ + return std::find_if(d->unsyncedEvents.cbegin(), d->unsyncedEvents.cend(), + [txnId] (const auto& item) { return item->transactionId() == txnId; }); +} + +void Room::Private::getAllMembers() +{ + // If already loaded or already loading, there's nothing to do here. + if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob)) + return; + + allMembersJob = connection->callApi<GetMembersByRoomJob>( + id, connection->nextBatchToken(), "join"); + auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1; + connect( allMembersJob, &BaseJob::success, q, [=] { + Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1); + auto roomChanges = updateStateFrom(allMembersJob->chunk()); + // Replay member events that arrived after the point for which + // the full members list was requested. + if (!timeline.empty() ) + for (auto it = q->findInTimeline(nextIndex).base(); + it != timeline.cend(); ++it) + if (is<RoomMemberEvent>(**it)) + roomChanges |= q->processStateEvent(**it); + if (roomChanges&MembersChange) + emit q->memberListChanged(); + emit q->allMembersLoaded(); + }); +} + bool Room::displayed() const { return d->displayed; @@ -570,6 +740,7 @@ void Room::setDisplayed(bool displayed) { resetHighlightCount(); resetNotificationCount(); + d->getAllMembers(); } } @@ -669,6 +840,14 @@ void Room::resetHighlightCount() emit highlightCountChanged(this); } +void Room::switchVersion(QString newVersion) +{ + auto* job = connection()->callApi<UpgradeRoomJob>(id(), newVersion); + connect(job, &BaseJob::failure, this, [this,job] { + emit upgradeFailed(job->errorString()); + }); +} + bool Room::hasAccountData(const QString& type) const { return d->accountData.find(type) != d->accountData.end(); @@ -768,7 +947,7 @@ void Room::Private::setTags(TagsMap newTags) } tags = move(newTags); qCDebug(MAIN) << "Room" << q->objectName() << "is tagged with" - << q->tagNames().join(", "); + << q->tagNames().join(QStringLiteral(", ")); emit q->tagsChanged(); } @@ -839,7 +1018,7 @@ QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const return fileName; } -QUrl Room::urlToThumbnail(const QString& eventId) +QUrl Room::urlToThumbnail(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) if (event->hasThumbnail()) @@ -853,7 +1032,7 @@ QUrl Room::urlToThumbnail(const QString& eventId) return {}; } -QUrl Room::urlToDownload(const QString& eventId) +QUrl Room::urlToDownload(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) { @@ -865,7 +1044,7 @@ QUrl Room::urlToDownload(const QString& eventId) return {}; } -QString Room::fileNameToDownload(const QString& eventId) +QString Room::fileNameToDownload(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) return d->fileNameToDownload(event); @@ -890,7 +1069,7 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const total = INT_MAX; } -#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST +#ifdef BROKEN_INITIALIZER_LISTS FileTransferInfo fti; fti.status = infoIt->status; fti.progress = int(progress); @@ -899,13 +1078,28 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const fti.localPath = QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); return fti; #else - return { infoIt->status, int(progress), int(total), + return { infoIt->status, infoIt->isUpload, int(progress), int(total), QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()), QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()) }; #endif } +QUrl Room::fileSource(const QString& id) const +{ + auto url = urlToDownload(id); + if (url.isValid()) + return url; + + // No urlToDownload means it's a pending or completed upload. + auto infoIt = d->fileTransfers.find(id); + if (infoIt != d->fileTransfers.end()) + return QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); + + qCWarning(MAIN) << "File source for identifier" << id << "not found"; + return {}; +} + QString Room::prettyPrint(const QString& plainText) const { return QMatrixClient::prettyPrint(plainText); @@ -947,7 +1141,41 @@ int Room::timelineSize() const bool Room::usesEncryption() const { - return !d->encryptionAlgorithm.isEmpty(); + return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty(); +} + +int Room::joinedCount() const +{ + return d->summary.joinedMemberCount.omitted() + ? d->membersMap.size() + : d->summary.joinedMemberCount.value(); +} + +int Room::invitedCount() const +{ + // TODO: Store invited users in Room too + Q_ASSERT(!d->summary.invitedMemberCount.omitted()); + return d->summary.invitedMemberCount.value(); +} + +int Room::totalMemberCount() const +{ + return joinedCount() + invitedCount(); +} + +GetRoomEventsJob* Room::eventsHistoryJob() const +{ + return d->eventsHistoryJob; +} + +Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) +{ + if (!summary.merge(newSummary)) + return Change::NoChange; + qCDebug(MAIN).nospace().noquote() + << "Updated room summary for " << q->objectName() << ": " << summary; + emit q->memberListChanged(); + return Change::SummaryChange; } void Room::Private::insertMemberIntoMap(User *u) @@ -955,7 +1183,11 @@ void Room::Private::insertMemberIntoMap(User *u) const auto userName = u->name(q); // If there is exactly one namesake of the added user, signal member renaming // for that other one because the two should be disambiguated now. - auto namesakes = membersMap.values(userName); + const auto namesakes = membersMap.values(userName); + + // Callers should check they are not adding an existing user once more. + Q_ASSERT(!namesakes.contains(u)); + if (namesakes.size() == 1) emit q->memberAboutToRename(namesakes.front(), namesakes.front()->fullName(q)); @@ -964,7 +1196,7 @@ void Room::Private::insertMemberIntoMap(User *u) emit q->memberRenamed(namesakes.front()); } -void Room::Private::renameMember(User* u, QString oldName) +void Room::Private::renameMember(User* u, const QString& oldName) { if (u->name(q) == oldName) { @@ -977,7 +1209,6 @@ void Room::Private::renameMember(User* u, QString oldName) removeMemberFromMap(oldName, u); insertMemberIntoMap(u); } - emit q->memberRenamed(u); } void Room::Private::removeMemberFromMap(const QString& username, User* u) @@ -993,7 +1224,6 @@ void Room::Private::removeMemberFromMap(const QString& username, User* u) membersMap.remove(username, u); // If there was one namesake besides the removed user, signal member renaming // for it because it doesn't need to be disambiguated anymore. - // TODO: Think about left users. if (namesake) emit q->memberRenamed(namesake); } @@ -1003,13 +1233,14 @@ inline auto makeErrorStr(const Event& e, QByteArray msg) return msg.append("; event dump follows:\n").append(e.originalJson()); } -Room::Timeline::difference_type Room::Private::moveEventsToTimeline( +Room::Timeline::size_type Room::Private::moveEventsToTimeline( RoomEventsRange events, EventsPlacement placement) { Q_ASSERT(!events.empty()); // Historical messages arrive in newest-to-oldest order, so the process for - // them is symmetric to the one for new messages. - auto index = timeline.empty() ? -int(placement) : + // them is almost symmetric to the one for new messages. New messages get + // appended from index 0; old messages go backwards from index -1. + auto index = timeline.empty() ? -((placement+1)/2) /* 1 -> -1; -1 -> 0 */ : placement == Older ? timeline.front().index() : timeline.back().index(); auto baseIndex = index; @@ -1030,7 +1261,7 @@ Room::Timeline::difference_type Room::Private::moveEventsToTimeline( eventsIndex.insert(eId, index); Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId); } - const auto insertedSize = (index - baseIndex) * int(placement); + const auto insertedSize = (index - baseIndex) * placement; Q_ASSERT(insertedSize == int(events.size())); return insertedSize; } @@ -1076,50 +1307,40 @@ QString Room::roomMembername(const QString& userId) const return roomMembername(user(userId)); } -void Room::updateData(SyncRoomData&& data) +void Room::updateData(SyncRoomData&& data, bool fromCache) { if( d->prevBatch.isEmpty() ) d->prevBatch = data.timelinePrevBatch; setJoinState(data.joinState); + Changes roomChanges = Change::NoChange; QElapsedTimer et; et.start(); for (auto&& event: data.accountData) - processAccountDataEvent(move(event)); + roomChanges |= processAccountDataEvent(move(event)); - bool emitNamesChanged = false; - if (!data.state.empty()) - { - et.restart(); - for (const auto& e: data.state) - emitNamesChanged |= processStateEvent(*e); + roomChanges |= d->updateStateFrom(data.state); - if (data.state.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processStateEvents():" - << data.state.size() << "event(s)," << et; - } if (!data.timeline.empty()) { et.restart(); - // State changes can arrive in a timeline event; so check those. - for (const auto& e: data.timeline) - emitNamesChanged |= processStateEvent(*e); + roomChanges |= d->addNewMessageEvents(move(data.timeline)); if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):" + qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << data.timeline.size() << "event(s)," << et; } - if (emitNamesChanged) + if (roomChanges&TopicChange) + emit topicChanged(); + + if (roomChanges&NameChange) emit namesChanged(this); - d->updateDisplayname(); - if (!data.timeline.empty()) - { - et.restart(); - d->addNewMessageEvents(move(data.timeline)); - if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et; - } + if (roomChanges&MembersChange) + emit memberListChanged(); + + roomChanges |= d->setSummary(move(data.summary)); + for( auto&& ephemeralEvent: data.ephemeral ) - processEphemeralEvent(move(ephemeralEvent)); + roomChanges |= processEphemeralEvent(move(ephemeralEvent)); // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) @@ -1139,29 +1360,45 @@ void Room::updateData(SyncRoomData&& data) d->notificationCount = data.notificationCount; emit notificationCountChanged(this); } + if (roomChanges != Change::NoChange) + { + d->updateDisplayname(); + emit changed(roomChanges); + if (!fromCache) + connection()->saveRoomState(this); + } } -QString Room::Private::sendEvent(RoomEventPtr&& event) +RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) { if (event->transactionId().isEmpty()) event->setTransactionId(connection->generateTxnId()); auto* pEvent = rawPtr(event); - emit q->pendingEventAboutToAdd(); + emit q->pendingEventAboutToAdd(pEvent); unsyncedEvents.emplace_back(move(event)); emit q->pendingEventAdded(); - return doSendEvent(pEvent); + return pEvent; +} + +QString Room::Private::sendEvent(RoomEventPtr&& event) +{ + if (q->successorId().isEmpty()) + return doSendEvent(addAsPending(std::move(event))); + + qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; + return {}; } QString Room::Private::doSendEvent(const RoomEvent* pEvent) { - auto txnId = pEvent->transactionId(); + const auto txnId = pEvent->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. if (auto call = connection->callApi<SendMessageJob>(BackgroundRequest, id, pEvent->matrixType(), txnId, pEvent->contentJson())) { Room::connect(call, &BaseJob::started, q, - [this,pEvent,txnId] { - auto it = findAsPending(pEvent); + [this,txnId] { + auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { qWarning(EVENTS) << "Pending event for transaction" << txnId @@ -1169,16 +1406,14 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) return; } it->setDeparted(); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); }); Room::connect(call, &BaseJob::failure, q, - std::bind(&Room::Private::onEventSendingFailure, - this, pEvent, txnId, call)); + std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); Room::connect(call, &BaseJob::success, q, - [this,call,pEvent,txnId] { - // Find an event by the pointer saved in the lambda (the pointer - // may be dangling by now but we can still search by it). - auto it = findAsPending(pEvent); + [this,call,txnId] { + emit q->messageSent(txnId, call->eventId()); + auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { qDebug(EVENTS) << "Pending event for transaction" << txnId @@ -1187,26 +1422,16 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) } it->setReachedServer(call->eventId()); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); }); } else - onEventSendingFailure(pEvent, txnId); + onEventSendingFailure(txnId); return txnId; } -Room::PendingEvents::iterator Room::Private::findAsPending( - const RoomEvent* rawEvtPtr) -{ - const auto comp = - [rawEvtPtr] (const auto& pe) { return pe.event() == rawEvtPtr; }; - - return std::find_if(unsyncedEvents.begin(), unsyncedEvents.end(), comp); -} - -void Room::Private::onEventSendingFailure(const RoomEvent* pEvent, - const QString& txnId, BaseJob* call) +void Room::Private::onEventSendingFailure(const QString& txnId, BaseJob* call) { - auto it = findAsPending(pEvent); + auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { qCritical(EVENTS) << "Pending event for transaction" << txnId @@ -1216,15 +1441,40 @@ void Room::Private::onEventSendingFailure(const RoomEvent* pEvent, it->setSendingFailed(call ? call->statusCaption() % ": " % call->errorString() : tr("The call could not be started")); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); + emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); } QString Room::retryMessage(const QString& txnId) { - auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), - [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); + const auto it = findPendingEvent(txnId); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Retrying transaction" << txnId; + const auto& transferIt = d->fileTransfers.find(txnId); + if (transferIt != d->fileTransfers.end()) + { + Q_ASSERT(transferIt->isUpload); + if (transferIt->status == FileTransferInfo::Completed) + { + qCDebug(MAIN) << "File for transaction" << txnId + << "has already been uploaded, bypassing re-upload"; + } else { + if (isJobRunning(transferIt->job)) + { + qCDebug(MAIN) << "Abandoning the upload job for transaction" + << txnId << "and starting again"; + transferIt->job->abandon(); + emit fileTransferFailed(txnId, tr("File upload will be retried")); + } + uploadFile(txnId, + QUrl::fromLocalFile(transferIt->localFileInfo.absoluteFilePath())); + // FIXME: Content type is no more passed here but it should + } + } + if (it->deliveryStatus() == EventStatus::ReachedServer) + { + qCWarning(MAIN) << "The previous attempt has reached the server; two" + " events are likely to be in the timeline after retry"; + } it->resetStatus(); return d->doSendEvent(it->event()); } @@ -1235,7 +1485,22 @@ void Room::discardMessage(const QString& txnId) [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Discarding transaction" << txnId; - emit pendingEventAboutToDiscard(it - d->unsyncedEvents.begin()); + const auto& transferIt = d->fileTransfers.find(txnId); + if (transferIt != d->fileTransfers.end()) + { + Q_ASSERT(transferIt->isUpload); + if (isJobRunning(transferIt->job)) + { + transferIt->status = FileTransferInfo::Cancelled; + transferIt->job->abandon(); + emit fileTransferFailed(txnId, tr("File upload cancelled")); + } else if (transferIt->status == FileTransferInfo::Completed) + { + qCWarning(MAIN) << "File for transaction" << txnId + << "has been uploaded but the message was discarded"; + } + } + emit pendingEventAboutToDiscard(int(it - d->unsyncedEvents.begin())); d->unsyncedEvents.erase(it); emit pendingEventDiscarded(); } @@ -1251,7 +1516,7 @@ QString Room::postPlainText(const QString& plainText) } QString Room::postHtmlMessage(const QString& plainText, const QString& html, - MessageEventType type) + MessageEventType type) { return d->sendEvent<RoomMessageEvent>(plainText, type, new EventContent::TextContent(html, QStringLiteral("text/html"))); @@ -1259,7 +1524,65 @@ QString Room::postHtmlMessage(const QString& plainText, const QString& html, QString Room::postHtmlText(const QString& plainText, const QString& html) { - return postHtmlMessage(plainText, html, MessageEventType::Text); + return postHtmlMessage(plainText, html); +} + +QString Room::postFile(const QString& plainText, const QUrl& localPath, + bool asGenericFile) +{ + QFileInfo localFile { localPath.toLocalFile() }; + Q_ASSERT(localFile.isFile()); + + const auto txnId = connection()->generateTxnId(); + // Remote URL will only be known after upload; fill in the local path + // to enable the preview while the event is pending. + uploadFile(txnId, localPath); + { + auto&& event = + makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile); + event->setTransactionId(txnId); + d->addAsPending(std::move(event)); + } + auto* context = new QObject(this); + connect(this, &Room::fileTransferCompleted, context, + [context,this,txnId] (const QString& id, QUrl, const QUrl& mxcUri) { + if (id == txnId) + { + auto it = findPendingEvent(txnId); + if (it != d->unsyncedEvents.end()) + { + it->setFileUploaded(mxcUri); + emit pendingEventChanged( + int(it - d->unsyncedEvents.begin())); + d->doSendEvent(it->get()); + } else { + // Normally in this situation we should instruct + // the media server to delete the file; alas, there's no + // API specced for that. + qCWarning(MAIN) << "File uploaded to" << mxcUri + << "but the event referring to it was cancelled"; + } + context->deleteLater(); + } + }); + connect(this, &Room::fileTransferCancelled, this, + [context,this,txnId] (const QString& id) { + if (id == txnId) + { + auto it = findPendingEvent(txnId); + if (it != d->unsyncedEvents.end()) + { + const auto idx = int(it - d->unsyncedEvents.begin()); + emit pendingEventAboutToDiscard(idx); + // See #286 on why iterator may not be valid here. + d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx); + emit pendingEventDiscarded(); + } + context->deleteLater(); + } + }); + + return txnId; } QString Room::postEvent(RoomEvent* event) @@ -1288,6 +1611,11 @@ void Room::setCanonicalAlias(const QString& newAlias) d->requestSetState(RoomCanonicalAliasEvent(newAlias)); } +void Room::setAliases(const QStringList& aliases) +{ + d->requestSetState(RoomAliasesEvent(aliases)); +} + void Room::setTopic(const QString& newTopic) { d->requestSetState(RoomTopicEvent(newTopic)); @@ -1315,7 +1643,24 @@ bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) bool Room::supportsCalls() const { - return d->membersMap.size() == 2; + return joinedCount() == 2; +} + +void Room::checkVersion() +{ + const auto defaultVersion = connection()->defaultRoomVersion(); + const auto stableVersions = connection()->stableRoomVersions(); + Q_ASSERT(!defaultVersion.isEmpty()); + // This method is only called after the base state has been loaded + // or the server capabilities have been loaded. + emit stabilityUpdated(defaultVersion, stableVersions); + if (!stableVersions.contains(version())) + { + qCDebug(MAIN) << this << "version is" << version() + << "which the server doesn't count as stable"; + if (canSwitchVersions()) + qCDebug(MAIN) << "The current user has enough privileges to fix it"; + } } void Room::inviteCall(const QString& callId, const int lifetime, @@ -1358,15 +1703,18 @@ void Room::getPreviousContent(int limit) void Room::Private::getPreviousContent(int limit) { - if( !isJobRunning(eventsHistoryJob) ) - { - eventsHistoryJob = - connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit); - connect( eventsHistoryJob, &BaseJob::success, q, [=] { - prevBatch = eventsHistoryJob->end(); - addHistoricalMessageEvents(eventsHistoryJob->chunk()); - }); - } + if (isJobRunning(eventsHistoryJob)) + return; + + eventsHistoryJob = + connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit); + emit q->eventsHistoryJobChanged(); + connect( eventsHistoryJob, &BaseJob::success, q, [=] { + prevBatch = eventsHistoryJob->end(); + addHistoricalMessageEvents(eventsHistoryJob->chunk()); + }); + connect( eventsHistoryJob, &QObject::destroyed, + q, &Room::eventsHistoryJobChanged); } void Room::inviteToRoom(const QString& memberId) @@ -1376,7 +1724,8 @@ void Room::inviteToRoom(const QString& memberId) LeaveRoomJob* Room::leaveRoom() { - return connection()->callApi<LeaveRoomJob>(id()); + // FIXME, #63: It should be RoomManager, not Connection + return connection()->leaveRoom(this); } SetRoomStateWithKeyJob*Room::setMemberState(const QString& memberId, const RoomMemberEvent& event) const @@ -1401,8 +1750,8 @@ void Room::unban(const QString& userId) void Room::redactEvent(const QString& eventId, const QString& reason) { - connection()->callApi<RedactEventJob>( - id(), eventId, connection()->generateTxnId(), reason); + connection()->callApi<RedactEventJob>(id(), + QUrl::toPercentEncoding(eventId), connection()->generateTxnId(), reason); } void Room::uploadFile(const QString& id, const QUrl& localFilename, @@ -1414,7 +1763,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, auto job = connection()->uploadFile(fileName, overrideContentType); if (isJobRunning(job)) { - d->fileTransfers.insert(id, { job, fileName }); + d->fileTransfers.insert(id, { job, fileName, true }); connect(job, &BaseJob::uploadProgress, this, [this,id] (qint64 sent, qint64 total) { d->fileTransfers[id].update(sent, total); @@ -1437,8 +1786,8 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) if (ongoingTransfer != d->fileTransfers.end() && ongoingTransfer->status == FileTransferInfo::Started) { - qCWarning(MAIN) << "Download for" << eventId - << "already started; to restart, cancel it first"; + qCWarning(MAIN) << "Transfer for" << eventId + << "is ongoing; download won't start"; return; } @@ -1452,13 +1801,21 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) Q_ASSERT(false); return; } - const auto fileUrl = event->content()->fileInfo()->url; + const auto* const fileInfo = event->content()->fileInfo(); + if (!fileInfo->isValid()) + { + qCWarning(MAIN) << "Event" << eventId + << "has an empty or malformed mxc URL; won't download"; + return; + } + const auto fileUrl = fileInfo->url; auto filePath = localFilename.toLocalFile(); if (filePath.isEmpty()) { // Build our own file path, starting with temp directory and eventId. filePath = eventId; - filePath = QDir::tempPath() % '/' % filePath.replace(':', '_') % + filePath = QDir::tempPath() % '/' % + filePath.replace(QRegularExpression("[/\\<>|\"*?:]"), "_") % '#' % d->fileNameToDownload(event); } auto job = connection()->downloadFile(fileUrl, filePath); @@ -1533,22 +1890,29 @@ RoomEventPtr makeRedacted(const RoomEvent& target, const RedactionEvent& redaction) { auto originalJson = target.originalJsonObject(); - static const QStringList keepKeys = - { EventIdKey, TypeKey, QStringLiteral("room_id"), - QStringLiteral("sender"), QStringLiteral("state_key"), - QStringLiteral("prev_content"), ContentKey, - QStringLiteral("origin_server_ts") }; + 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"), + QStringLiteral("origin"), QStringLiteral("origin_server_ts"), + QStringLiteral("membership") + }; std::vector<std::pair<Event::Type, QStringList>> keepContentKeysMap { { RoomMemberEvent::typeId(), { QStringLiteral("membership") } } -// , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } + , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } // , { RoomPowerLevels::typeId(), // { QStringLiteral("ban"), QStringLiteral("events"), // QStringLiteral("events_default"), QStringLiteral("kick"), // QStringLiteral("redact"), QStringLiteral("state_default"), // QStringLiteral("users"), QStringLiteral("users_default") } } - , { RoomAliasesEvent::typeId(), { QStringLiteral("alias") } } + , { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } } +// , { RoomHistoryVisibility::typeId(), +// { QStringLiteral("history_visibility") } } }; for (auto it = originalJson.begin(); it != originalJson.end();) { @@ -1600,11 +1964,26 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) return true; } - // Make a new event from the redacted JSON, exchange events, - // notify everyone and delete the old event + // 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(makeRedacted(*ti, redaction)); - q->onRedaction(*oldEvent, *ti.event()); qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction.id(); + if (oldEvent->isStateEvent()) + { + const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() }; + Q_ASSERT(currentState.contains(evtKey)); + if (currentState.value(evtKey) == oldEvent.get()) + { + Q_ASSERT(ti.index() >= 0); // Historical states can't be in currentState + qCDebug(MAIN).nospace() << "Redacting state " + << oldEvent->matrixType() << "/" << oldEvent->stateKey(); + // Retarget the current state to the newly made event. + if (q->processStateEvent(*ti)) + emit q->namesChanged(q); + updateDisplayname(); + } + } + q->onRedaction(*oldEvent, *ti); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); return true; } @@ -1626,11 +2005,11 @@ inline bool isRedaction(const RoomEventPtr& ep) return is<RedactionEvent>(*ep); } -void Room::Private::addNewMessageEvents(RoomEvents&& events) +Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) { dropDuplicateEvents(events); if (events.empty()) - return; + return Change::NoChange; // Pre-process redactions so that events that get redacted in the same // batch landed in the timeline already redacted. @@ -1655,8 +2034,17 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) // If the target event comes later, it comes already redacted. } + // State changes arrive as a part of timeline; the current room state gets + // updated before merging events to the timeline because that's what + // clients historically expect. This may eventually change though if we + // postulate that the current state is only current between syncs but not + // within a sync. + Changes roomChanges = Change::NoChange; + for (const auto& eptr: events) + roomChanges |= q->processStateEvent(*eptr); + auto timelineSize = timeline.size(); - auto totalInserted = 0; + size_t totalInserted = 0; for (auto it = events.begin(); it != events.end();) { auto nextPendingPair = findFirstOf(it, events.end(), @@ -1677,12 +2065,22 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) break; it = nextPending + 1; - emit q->pendingEventAboutToMerge(nextPending->get(), - nextPendingPair.second - unsyncedEvents.begin()); + auto* nextPendingEvt = nextPending->get(); + const auto pendingEvtIdx = + int(nextPendingPair.second - unsyncedEvents.begin()); + emit q->pendingEventAboutToMerge(nextPendingEvt, pendingEvtIdx); qDebug(EVENTS) << "Merging pending event from transaction" - << (*nextPending)->transactionId() << "into" - << (*nextPending)->id(); - unsyncedEvents.erase(nextPendingPair.second); + << nextPendingEvt->transactionId() << "into" + << nextPendingEvt->id(); + auto transfer = fileTransfers.take(nextPendingEvt->transactionId()); + if (transfer.status != FileTransferInfo::None) + fileTransfers.insert(nextPendingEvt->id(), transfer); + // After emitting pendingEventAboutToMerge() above we cannot rely + // on the previously obtained nextPendingPair.second staying valid + // because a signal handler may send another message, thereby altering + // unsyncedEvents (see #286). Fortunately, unsyncedEvents only grows at + // its back so we can rely on the index staying valid at least. + unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx); if (auto insertedSize = moveEventsToTimeline({nextPending, it}, Newer)) { totalInserted += insertedSize; @@ -1713,15 +2111,17 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) auto firstWriter = q->user((*from)->senderId()); if (q->readMarker(firstWriter) != timeline.crend()) { - promoteReadMarker(firstWriter, rev_iter_t(from) - 1); + roomChanges |= promoteReadMarker(firstWriter, rev_iter_t(from) - 1); qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id() << "to" << *q->readMarker(firstWriter); } updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); + roomChanges |= Change::UnreadNotifsChange; } Q_ASSERT(timeline.size() == timelineSize + totalInserted); + return roomChanges; } void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) @@ -1730,14 +2130,25 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) const auto timelineSize = timeline.size(); dropDuplicateEvents(events); - RoomEventsRange normalEvents { - events.begin(), events.end() //remove_if(events.begin(), events.end(), isRedaction) - }; - if (normalEvents.empty()) + if (events.empty()) return; - emit q->aboutToAddHistoricalMessages(normalEvents); - const auto insertedSize = moveEventsToTimeline(normalEvents, Older); + // In case of lazy-loading new members may be loaded with historical + // messages. Also, the cache doesn't store events with empty content; + // so when such events show up in the timeline they should be properly + // incorporated. + for (const auto& eptr: events) + { + const auto& e = *eptr; + if (e.isStateEvent() && + !currentState.contains({e.matrixType(), e.stateKey()})) + { + q->processStateEvent(e); + } + } + + emit q->aboutToAddHistoricalMessages(events); + const auto insertedSize = moveEventsToTimeline(events, Older); const auto from = timeline.crend() - insertedSize; qCDebug(MAIN) << "Room" << displayname << "received" << insertedSize @@ -1754,52 +2165,89 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) << insertedSize << "event(s)," << et; } -bool Room::processStateEvent(const RoomEvent& e) +Room::Changes Room::processStateEvent(const RoomEvent& e) { + if (!e.isStateEvent()) + return Change::NoChange; + + const auto* oldStateEvent = std::exchange( + d->currentState[{e.matrixType(),e.stateKey()}], + static_cast<const StateEventBase*>(&e)); + Q_ASSERT(!oldStateEvent || + (oldStateEvent->matrixType() == e.matrixType() && + oldStateEvent->stateKey() == e.stateKey())); + if (!is<RoomMemberEvent>(e)) // Room member events are too numerous + qCDebug(EVENTS) << "Room state event:" << e; + return visit(e - , [this] (const RoomNameEvent& evt) { - d->name = evt.name(); - qCDebug(MAIN) << "Room name updated:" << d->name; - return true; + , [] (const RoomNameEvent&) { + return NameChange; } - , [this] (const RoomAliasesEvent& evt) { - d->aliases = evt.aliases(); - qCDebug(MAIN) << "Room aliases updated:" << d->aliases; - return true; + , [this,oldStateEvent] (const RoomAliasesEvent& ae) { + const auto previousAliases = oldStateEvent + ? static_cast<const RoomAliasesEvent*>(oldStateEvent)->aliases() + : QStringList(); + connection()->updateRoomAliases(id(), previousAliases, ae.aliases()); + return OtherChange; } , [this] (const RoomCanonicalAliasEvent& evt) { - d->canonicalAlias = evt.alias(); - if (!d->canonicalAlias.isEmpty()) - setObjectName(d->canonicalAlias); - qCDebug(MAIN) << "Room canonical alias updated:" - << d->canonicalAlias; - return true; + setObjectName(evt.alias().isEmpty() ? d->id : evt.alias()); + return CanonicalAliasChange; } - , [this] (const RoomTopicEvent& evt) { - d->topic = evt.topic(); - qCDebug(MAIN) << "Room topic updated:" << d->topic; - emit topicChanged(); - return false; + , [] (const RoomTopicEvent&) { + return TopicChange; } , [this] (const RoomAvatarEvent& evt) { if (d->avatar.updateUrl(evt.url())) - { - qCDebug(MAIN) << "Room avatar URL updated:" - << evt.url().toString(); emit avatarChanged(); - } - return false; + return AvatarChange; } - , [this] (const RoomMemberEvent& evt) { + , [this,oldStateEvent] (const RoomMemberEvent& evt) { auto* u = user(evt.userId()); - u->processEvent(evt, this); - if (u == localUser() && memberJoinState(u) == JoinState::Invite + const auto* oldMemberEvent = + static_cast<const RoomMemberEvent*>(oldStateEvent); + u->processEvent(evt, this, oldMemberEvent == nullptr); + const auto prevMembership = oldMemberEvent + ? oldMemberEvent->membership() : MembershipType::Leave; + if (u == localUser() && evt.membership() == MembershipType::Invite && evt.isDirect()) connection()->addToDirectChats(this, user(evt.senderId())); - if( evt.membership() == MembershipType::Join ) + switch (prevMembership) { - if (memberJoinState(u) != JoinState::Join) + case MembershipType::Invite: + if (evt.membership() != prevMembership) + { + d->usersInvited.removeOne(u); + Q_ASSERT(!d->usersInvited.contains(u)); + } + break; + case MembershipType::Join: + if (evt.membership() == MembershipType::Invite) + qCWarning(MAIN) + << "Invalid membership change from Join to Invite:" + << evt; + if (evt.membership() != prevMembership) + { + disconnect(u, &User::nameAboutToChange, this, nullptr); + disconnect(u, &User::nameChanged, this, nullptr); + d->removeMemberFromMap(u->name(this), u); + emit userRemoved(u); + } + break; + default: + if (evt.membership() == MembershipType::Invite + || evt.membership() == MembershipType::Join) + { + d->membersLeft.removeOne(u); + Q_ASSERT(!d->membersLeft.contains(u)); + } + } + + switch(evt.membership()) + { + case MembershipType::Join: + if (prevMembership != MembershipType::Join) { d->insertMemberIntoMap(u); connect(u, &User::nameAboutToChange, this, @@ -1810,35 +2258,50 @@ bool Room::processStateEvent(const RoomEvent& e) connect(u, &User::nameChanged, this, [=] (QString, QString oldName, const Room* context) { if (context == this) + { d->renameMember(u, oldName); + emit memberRenamed(u); + } }); emit userAdded(u); } + break; + case MembershipType::Invite: + if (!d->usersInvited.contains(u)) + d->usersInvited.push_back(u); + break; + default: + if (!d->membersLeft.contains(u)) + d->membersLeft.append(u); } - else if( evt.membership() == MembershipType::Leave ) - { - if (memberJoinState(u) == JoinState::Join) - { - if (!d->membersLeft.contains(u)) - d->membersLeft.append(u); - d->removeMemberFromMap(u->name(this), u); - emit userRemoved(u); - } - } - return false; + return MembersChange; } - , [this] (const EncryptionEvent& evt) { - d->encryptionAlgorithm = evt.algorithm(); - qCDebug(MAIN) << "Encryption switched on in room" << id() - << "with algorithm" << d->encryptionAlgorithm; - emit encryption(); - return false; + , [this] (const EncryptionEvent&) { + emit encryption(); // It can only be done once, so emit it here. + return OtherChange; + } + , [this] (const RoomTombstoneEvent& evt) { + const auto successorId = evt.successorRoomId(); + if (auto* successor = connection()->room(successorId)) + emit upgraded(evt.serverMessage(), successor); + else + connectUntil(connection(), &Connection::loadedRoomState, this, + [this,successorId,serverMsg=evt.serverMessage()] + (Room* newRoom) { + if (newRoom->id() != successorId) + return false; + emit upgraded(serverMsg, newRoom); + return true; + }); + + return OtherChange; } ); } -void Room::processEphemeralEvent(EventPtr&& event) +Room::Changes Room::processEphemeralEvent(EventPtr&& event) { + Changes changes = NoChange; QElapsedTimer et; et.start(); if (auto* evt = eventCast<TypingEvent>(event)) { @@ -1877,7 +2340,7 @@ void Room::processEphemeralEvent(EventPtr&& event) continue; // FIXME, #185 auto u = user(r.userId); if (memberJoinState(u) == JoinState::Join) - d->promoteReadMarker(u, newMarker); + changes |= d->promoteReadMarker(u, newMarker); } } else { @@ -1894,7 +2357,7 @@ void Room::processEphemeralEvent(EventPtr&& event) auto u = user(r.userId); if (memberJoinState(u) == JoinState::Join && readMarker(u) == timelineEdge()) - d->setLastReadEvent(u, p.evtId); + changes |= d->setLastReadEvent(u, p.evtId); } } } @@ -1904,12 +2367,17 @@ void Room::processEphemeralEvent(EventPtr&& event) << evt->eventsWithReceipts().size() << "event(s) with" << totalReceipts << "receipt(s)," << et; } + return changes; } -void Room::processAccountDataEvent(EventPtr&& event) +Room::Changes Room::processAccountDataEvent(EventPtr&& event) { + Changes changes = NoChange; if (auto* evt = eventCast<TagEvent>(event)) + { d->setTags(evt->tags()); + changes |= Change::TagsChange; + } if (auto* evt = eventCast<ReadMarkerEvent>(event)) { @@ -1917,10 +2385,9 @@ void Room::processAccountDataEvent(EventPtr&& event) qCDebug(MAIN) << "Server-side read marker at" << readEventId; d->serverReadMarker = readEventId; const auto newMarker = findInTimeline(readEventId); - if (newMarker != timelineEdge()) - d->markMessagesAsRead(newMarker); - else - d->setLastReadEvent(localUser(), readEventId); + changes |= newMarker != timelineEdge() + ? d->markMessagesAsRead(newMarker) + : d->setLastReadEvent(localUser(), readEventId); } // For all account data events auto& currentData = d->accountData[event->matrixType()]; @@ -1933,52 +2400,40 @@ void Room::processAccountDataEvent(EventPtr&& event) qCDebug(MAIN) << "Updated account data of type" << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); + return Change::AccountDataChange; } + return Change::NoChange; } -QString Room::Private::roomNameFromMemberNames(const QList<User *> &userlist) const +template <typename ContT> +Room::Private::users_shortlist_t +Room::Private::buildShortlist(const ContT& users) const { - // This is part 3(i,ii,iii) in the room displayname algorithm described - // in the CS spec (see also Room::Private::updateDisplayname() ). - // The spec requires to sort users lexicographically by state_key (user id) - // and use disambiguated display names of two topmost users excluding - // the current one to render the name of the room. - - // std::array is the leanest C++ container - std::array<User*, 2> first_two = { {nullptr, nullptr} }; + // To calculate room display name the spec requires to sort users + // lexicographically by state_key (user id) and use disambiguated + // display names of two topmost users excluding the current one to render + // the name of the room. The below code selects 3 topmost users, + // slightly extending the spec. + users_shortlist_t shortlist { }; // Prefill with nullptrs std::partial_sort_copy( - userlist.begin(), userlist.end(), - first_two.begin(), first_two.end(), - [this](const User* u1, const User* u2) { - // Filter out the "me" user so that it never hits the room name + users.begin(), users.end(), + shortlist.begin(), shortlist.end(), + [this] (const User* u1, const User* u2) { + // localUser(), if it's in the list, is sorted below all others return isLocalUser(u2) || (!isLocalUser(u1) && u1->id() < u2->id()); } ); + return shortlist; +} - // Spec extension. A single person in the chat but not the local user - // (the local user is invited). - if (userlist.size() == 1 && !isLocalUser(first_two.front()) && - joinState == JoinState::Invite) - return tr("Invitation from %1") - .arg(q->roomMembername(first_two.front())); - - // i. One-on-one chat. first_two[1] == localUser() in this case. - if (userlist.size() == 2) - return q->roomMembername(first_two[0]); - - // ii. Two users besides the current one. - if (userlist.size() == 3) - return tr("%1 and %2") - .arg(q->roomMembername(first_two[0]), - q->roomMembername(first_two[1])); - - // iii. More users. - if (userlist.size() > 3) - return tr("%1 and %Ln other(s)", "", userlist.size() - 3) - .arg(q->roomMembername(first_two[0])); - - // userlist.size() < 2 - apparently, there's only current user in the room - return QString(); +Room::Private::users_shortlist_t +Room::Private::buildShortlist(const QStringList& userIds) const +{ + QList<User*> users; + users.reserve(userIds.size()); + for (const auto& h: userIds) + users.push_back(q->user(h)); + return buildShortlist(users); } QString Room::Private::calculateDisplayname() const @@ -1987,27 +2442,73 @@ QString Room::Private::calculateDisplayname() const // Numbers below refer to respective parts in the spec. // 1. Name (from m.room.name) - if (!name.isEmpty()) { - return name; + auto dispName = q->name(); + if (!dispName.isEmpty()) { + return dispName; } // 2. Canonical alias - if (!canonicalAlias.isEmpty()) - return canonicalAlias; + dispName = q->canonicalAlias(); + if (!dispName.isEmpty()) + return dispName; // Using m.room.aliases in naming is explicitly discouraged by the spec - //if (!aliases.empty() && !aliases.at(0).isEmpty()) - // return aliases.at(0); + + // Supplementary code for 3 and 4: build the shortlist of users whose names + // will be used to construct the room name. Takes into account MSC688's + // "heroes" if available. + + const bool localUserIsIn = joinState == JoinState::Join; + const bool emptyRoom = membersMap.isEmpty() || + (membersMap.size() == 1 && isLocalUser(*membersMap.begin())); + const bool nonEmptySummary = + !summary.heroes.omitted() && !summary.heroes->empty(); + auto shortlist = nonEmptySummary ? buildShortlist(summary.heroes.value()) : + !emptyRoom ? buildShortlist(membersMap) : + users_shortlist_t { }; + + // When lazy-loading is on, we can rely on the heroes list. + // If it's off, the below code gathers invited and left members. + // NB: including invitations, if any, into naming is a spec extension. + // This kicks in when there's no lazy loading and it's a room with + // the local user as the only member, with more users invited. + if (!shortlist.front() && localUserIsIn) + shortlist = buildShortlist(usersInvited); + + if (!shortlist.front()) // Still empty shortlist; use left members + shortlist = buildShortlist(membersLeft); + + QStringList names; + for (auto u: shortlist) + { + if (u == nullptr || isLocalUser(u)) + break; + // Only disambiguate if the room is not empty + names.push_back(u->displayname(emptyRoom ? nullptr : q)); + } + + const auto usersCountExceptLocal = + !emptyRoom ? q->joinedCount() - int(joinState == JoinState::Join) : + !usersInvited.empty() ? usersInvited.count() : + membersLeft.size() - int(joinState == JoinState::Leave); + if (usersCountExceptLocal > int(shortlist.size())) + names << + tr("%Ln other(s)", + "Used to make a room name from user names: A, B and _N others_", + usersCountExceptLocal - int(shortlist.size())); + const auto namesList = QLocale().createSeparatedList(names); // 3. Room members - QString topMemberNames = roomNameFromMemberNames(membersMap.values()); - if (!topMemberNames.isEmpty()) - return topMemberNames; + if (!emptyRoom) + return namesList; + + // (Spec extension) Invited users + if (!usersInvited.empty()) + return tr("Empty room (invited: %1)").arg(namesList); // 4. Users that previously left the room - topMemberNames = roomNameFromMemberNames(membersLeft); - if (!topMemberNames.isEmpty()) - return tr("Empty room (was: %1)").arg(topMemberNames); + if (membersLeft.size() > 0) + return tr("Empty room (was: %1)").arg(namesList); // 5. Fail miserably return tr("Empty room (%1)").arg(id); @@ -2026,58 +2527,27 @@ void Room::Private::updateDisplayname() } } -void appendStateEvent(QJsonArray& events, const QString& type, - const QJsonObject& content, const QString& stateKey = {}) -{ - if (!content.isEmpty() || !stateKey.isEmpty()) - { - auto json = basicEventJson(type, content); - json.insert(QStringLiteral("state_key"), stateKey); - events.append(json); - } -} - -#define ADD_STATE_EVENT(events, type, name, content) \ - appendStateEvent((events), QStringLiteral(type), \ - {{ QStringLiteral(name), content }}); - -void appendEvent(QJsonArray& events, const QString& type, - const QJsonObject& content) -{ - if (!content.isEmpty()) - events.append(basicEventJson(type, content)); -} - -template <typename EvtT> -void appendEvent(QJsonArray& events, const EvtT& event) -{ - appendEvent(events, EvtT::matrixTypeId(), event.toJson()); -} - QJsonObject Room::Private::toJson() const { QElapsedTimer et; et.start(); QJsonObject result; + addParam<IfNotEmpty>(result, QStringLiteral("summary"), summary); { QJsonArray stateEvents; - ADD_STATE_EVENT(stateEvents, "m.room.name", "name", name); - ADD_STATE_EVENT(stateEvents, "m.room.topic", "topic", topic); - ADD_STATE_EVENT(stateEvents, "m.room.avatar", "url", - avatar.url().toString()); - ADD_STATE_EVENT(stateEvents, "m.room.aliases", "aliases", - QJsonArray::fromStringList(aliases)); - ADD_STATE_EVENT(stateEvents, "m.room.canonical_alias", "alias", - canonicalAlias); - ADD_STATE_EVENT(stateEvents, "m.room.encryption", "algorithm", - encryptionAlgorithm); - - for (const auto *m : membersMap) - appendStateEvent(stateEvents, QStringLiteral("m.room.member"), - { { QStringLiteral("membership"), QStringLiteral("join") } - , { QStringLiteral("displayname"), m->rawName(q) } - , { QStringLiteral("avatar_url"), m->avatarUrl(q).toString() } - }, m->id()); + for (const auto* evt: currentState) + { + Q_ASSERT(evt->isStateEvent()); + if ((evt->isRedacted() && !is<RoomMemberEvent>(*evt)) || + evt->contentJson().isEmpty()) + continue; + + auto json = evt->fullJson(); + auto unsignedJson = evt->unsignedJson(); + unsignedJson.remove(QStringLiteral("prev_content")); + json[UnsignedKeyL] = unsignedJson; + stateEvents.append(json); + } const auto stateObjName = joinState == JoinState::Invite ? QStringLiteral("invite_state") : QStringLiteral("state"); @@ -2085,14 +2555,17 @@ QJsonObject Room::Private::toJson() const QJsonObject {{ QStringLiteral("events"), stateEvents }}); } - QJsonArray accountDataEvents; if (!accountData.empty()) { + QJsonArray accountDataEvents; for (const auto& e: accountData) - appendEvent(accountDataEvents, e.first, e.second->contentJson()); + { + if (!e.second->contentJson().isEmpty()) + accountDataEvents.append(e.second->fullJson()); + } + result.insert(QStringLiteral("account_data"), + QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); } - result.insert(QStringLiteral("account_data"), - QJsonObject {{ QStringLiteral("events"), accountDataEvents }}); QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey, unreadMessages } }; @@ -18,7 +18,7 @@ #pragma once -#include "jobs/syncjob.h" +#include "csapi/message_pagination.h" #include "events/roommessageevent.h" #include "events/accountdataevents.h" #include "eventitem.h" @@ -33,6 +33,8 @@ namespace QMatrixClient { class Event; + class Avatar; + class SyncRoomData; class RoomMemberEvent; class Connection; class User; @@ -41,10 +43,17 @@ namespace QMatrixClient class SetRoomStateWithKeyJob; class RedactEventJob; + /** The data structure used to expose file transfer information to views + * + * This is specifically tuned to work with QML exposing all traits as + * Q_PROPERTY values. + */ class FileTransferInfo { Q_GADGET + Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT) Q_PROPERTY(bool active READ active CONSTANT) + Q_PROPERTY(bool started READ started CONSTANT) Q_PROPERTY(bool completed READ completed CONSTANT) Q_PROPERTY(bool failed READ failed CONSTANT) Q_PROPERTY(int progress MEMBER progress CONSTANT) @@ -52,16 +61,17 @@ namespace QMatrixClient Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT) Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT) public: - enum Status { None, Started, Completed, Failed }; + enum Status { None, Started, Completed, Failed, Cancelled }; Status status = None; + bool isUpload = false; int progress = 0; int total = -1; QUrl localDir { }; QUrl localPath { }; - bool active() const - { return status == Started || status == Completed; } + bool started() const { return status == Started; } bool completed() const { return status == Completed; } + bool active() const { return started() || completed(); } bool failed() const { return status == Failed; } }; @@ -71,10 +81,14 @@ namespace QMatrixClient Q_PROPERTY(Connection* connection READ connection CONSTANT) Q_PROPERTY(User* localUser READ localUser CONSTANT) Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(QString version READ version NOTIFY baseStateLoaded) + Q_PROPERTY(bool isUnstable READ isUnstable NOTIFY stabilityUpdated) + Q_PROPERTY(QString predecessorId READ predecessorId NOTIFY baseStateLoaded) + Q_PROPERTY(QString successorId READ successorId NOTIFY upgraded) Q_PROPERTY(QString name READ name NOTIFY namesChanged) Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged) Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) - Q_PROPERTY(QString displayName READ displayName NOTIFY namesChanged) + Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) @@ -83,6 +97,9 @@ namespace QMatrixClient Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged) Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged) + Q_PROPERTY(int joinedCount READ joinedCount NOTIFY memberListChanged) + Q_PROPERTY(int invitedCount READ invitedCount NOTIFY memberListChanged) + Q_PROPERTY(int totalMemberCount READ totalMemberCount NOTIFY memberListChanged) Q_PROPERTY(bool displayed READ displayed WRITE setDisplayed NOTIFY displayedChanged) Q_PROPERTY(QString firstDisplayedEventId READ firstDisplayedEventId WRITE setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged) @@ -95,12 +112,34 @@ namespace QMatrixClient Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged) Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged) + Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY eventsHistoryJobChanged) + public: using Timeline = std::deque<TimelineItem>; using PendingEvents = std::vector<PendingEventItem>; using rev_iter_t = Timeline::const_reverse_iterator; using timeline_iter_t = Timeline::const_iterator; + enum Change : uint { + NoChange = 0x0, + NameChange = 0x1, + CanonicalAliasChange = 0x2, + TopicChange = 0x4, + UnreadNotifsChange = 0x8, + AvatarChange = 0x10, + JoinStateChange = 0x20, + TagsChange = 0x40, + MembersChange = 0x80, + /* = 0x100, */ + AccountDataChange = 0x200, + SummaryChange = 0x400, + ReadMarkerChange = 0x800, + OtherChange = 0x8000, + AnyChange = 0xFFFF + }; + Q_DECLARE_FLAGS(Changes, Change) + Q_FLAG(Changes) + Room(Connection* connection, QString id, JoinState initialJoinState); ~Room() override; @@ -109,6 +148,10 @@ namespace QMatrixClient Connection* connection() const; User* localUser() const; const QString& id() const; + QString version() const; + bool isUnstable() const; + QString predecessorId() const; + QString successorId() const; QString name() const; QStringList aliases() const; QString canonicalAlias() const; @@ -116,15 +159,22 @@ namespace QMatrixClient QString topic() const; QString avatarMediaId() const; QUrl avatarUrl() const; + const Avatar& avatarObject() const; Q_INVOKABLE JoinState joinState() const; Q_INVOKABLE QList<User*> usersTyping() const; QList<User*> membersLeft() const; Q_INVOKABLE QList<User*> users() const; QStringList memberNames() const; + [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]] int memberCount() const; int timelineSize() const; bool usesEncryption() const; + int joinedCount() const; + int invitedCount() const; + int totalMemberCount() const; + + GetRoomEventsJob* eventsHistoryJob() const; /** * Returns a square room avatar with the given size and requests it @@ -177,9 +227,16 @@ namespace QMatrixClient const Timeline& messageEvents() const; const PendingEvents& pendingEvents() const; /** - * A convenience method returning the read marker to - * the before-oldest message + * A convenience method returning the read marker to the position + * before the "oldest" event; same as messageEvents().crend() */ + rev_iter_t historyEdge() const; + /** + * A convenience method returning the iterator beyond the latest + * arrived event; same as messageEvents().cend() + */ + Timeline::const_iterator syncEdge() const; + /// \deprecated Use historyEdge instead rev_iter_t timelineEdge() const; Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const; Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const; @@ -187,8 +244,17 @@ namespace QMatrixClient rev_iter_t findInTimeline(TimelineItem::index_t index) const; rev_iter_t findInTimeline(const QString& evtId) const; + PendingEvents::iterator findPendingEvent(const QString & txnId); + PendingEvents::const_iterator findPendingEvent(const QString & txnId) const; bool displayed() const; + /// Mark the room as currently displayed to the user + /** + * Marking the room displayed causes the room to obtain the full + * list of members if it's been lazy-loaded before; in the future + * it may do more things bound to "screen time" of the room, e.g. + * measure that "screen time". + */ void setDisplayed(bool displayed = true); QString firstDisplayedEventId() const; rev_iter_t firstDisplayedMarker() const; @@ -288,11 +354,32 @@ namespace QMatrixClient /// Get the list of users this room is a direct chat with QList<User*> directChatUsers() const; - Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId); - Q_INVOKABLE QUrl urlToDownload(const QString& eventId); - Q_INVOKABLE QString fileNameToDownload(const QString& eventId); + Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const; + Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const; + + /// Get a file name for downloading for a given event id + /*! + * The event MUST be RoomMessageEvent and have content + * for downloading. \sa RoomMessageEvent::hasContent + */ + Q_INVOKABLE QString fileNameToDownload(const QString& eventId) const; + + /// Get information on file upload/download + /*! + * \param id uploads are identified by the corresponding event's + * transactionId (because uploads are done before + * the event is even sent), while downloads are using + * the normal event id for identifier. + */ Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const; + /// Get the URL to the actual file source in a unified way + /*! + * For uploads it will return a URL to a local file; for downloads + * the URL will be taken from the corresponding room event. + */ + Q_INVOKABLE QUrl fileSource(const QString& id) const; + /** Pretty-prints plain text into HTML * As of now, it's exactly the same as QMatrixClient::prettyPrint(); * in the future, it will also linkify room aliases, mxids etc. @@ -314,11 +401,17 @@ namespace QMatrixClient Q_INVOKABLE bool supportsCalls() const; public slots: + /** Check whether the room should be upgraded */ + void checkVersion(); + QString postMessage(const QString& plainText, MessageEventType type); QString postPlainText(const QString& plainText); QString postHtmlMessage(const QString& plainText, - const QString& html, MessageEventType type); + const QString& html, + MessageEventType type = MessageEventType::Text); QString postHtmlText(const QString& plainText, const QString& html); + QString postFile(const QString& plainText, const QUrl& localPath, + bool asGenericFile = false); /** Post a pre-created room message event * * Takes ownership of the event, deleting it once the matching one @@ -332,8 +425,12 @@ namespace QMatrixClient void discardMessage(const QString& txnId); void setName(const QString& newName); void setCanonicalAlias(const QString& newAlias); + void setAliases(const QStringList& aliases); void setTopic(const QString& newTopic); + /// You shouldn't normally call this method; it's here for debugging + void refreshDisplayName(); + void getPreviousContent(int limit = 10); void inviteToRoom(const QString& memberId); @@ -356,19 +453,63 @@ namespace QMatrixClient /// Mark all messages in the room as read void markAllMessagesAsRead(); + /// Whether the current user is allowed to upgrade the room + bool canSwitchVersions() const; + + /// Switch the room's version (aka upgrade) + void switchVersion(QString newVersion); + signals: + /// Initial set of state events has been loaded + /** + * The initial set is what comes from the initial sync for the room. + * This includes all basic things like RoomCreateEvent, + * RoomNameEvent, a (lazy-loaded, not full) set of RoomMemberEvents + * etc. This is a per-room reflection of Connection::loadedRoomState + * \sa Connection::loadedRoomState + */ + void baseStateLoaded(); + void eventsHistoryJobChanged(); void aboutToAddHistoricalMessages(RoomEventsRange events); void aboutToAddNewMessages(RoomEventsRange events); void addedMessages(int fromIndex, int toIndex); - void pendingEventAboutToAdd(); + /// The event is about to be appended to the list of pending events + void pendingEventAboutToAdd(RoomEvent* event); + /// An event has been appended to the list of pending events void pendingEventAdded(); + /// The remote echo has arrived with the sync and will be merged + /// with its local counterpart + /** NB: Requires a sync loop to be emitted */ void pendingEventAboutToMerge(RoomEvent* serverEvent, int pendingEventIndex); + /// The remote and local copies of the event have been merged + /** NB: Requires a sync loop to be emitted */ void pendingEventMerged(); + /// An event will be removed from the list of pending events void pendingEventAboutToDiscard(int pendingEventIndex); + /// An event has just been removed from the list of pending events void pendingEventDiscarded(); + /// The status of a pending event has changed + /** \sa PendingEventItem::deliveryStatus */ void pendingEventChanged(int pendingEventIndex); + /// The server accepted the message + /** This is emitted when an event sending request has successfully + * completed. This does not mean that the event is already in the + * local timeline, only that the server has accepted it. + * \param txnId transaction id assigned by the client during sending + * \param eventId event id assigned by the server upon acceptance + * \sa postEvent, postPlainText, postMessage, postHtmlMessage + * \sa pendingEventMerged, aboutToAddNewMessages + */ + void messageSent(QString txnId, QString eventId); + /** A common signal for various kinds of changes in the room + * Aside from all changes in the room state + * @param changes a set of flags describing what changes occured + * upon the last sync + * \sa StateChange + */ + void changed(Changes changes); /** * \brief The room name, the canonical alias or other aliases changed * @@ -383,7 +524,17 @@ namespace QMatrixClient void userRemoved(User* user); void memberAboutToRename(User* user, QString newName); void memberRenamed(User* user); + /// The list of members has changed + /** Emitted no more than once per sync, this is a good signal to + * for cases when some action should be done upon any change in + * the member list. If you need per-item granularity you should use + * userAdded, userRemoved and memberAboutToRename / memberRenamed + * instead. + */ void memberListChanged(); + /// The previously lazy-loaded members list is now loaded entirely + /// \sa setDisplayed + void allMembersLoaded(); void encryption(); void joinStateChanged(JoinState oldState, JoinState newState); @@ -415,30 +566,40 @@ namespace QMatrixClient void fileTransferCancelled(QString id); void callEvent(Room* room, const RoomEvent* event); - /// The room is about to be deleted - void beforeDestruction(Room*); - public: // Used by Connection - not a part of the client API - QJsonObject toJson() const; - void updateData(SyncRoomData&& data ); + /// The room's version stability may have changed + void stabilityUpdated(QString recommendedDefault, + QStringList stableVersions); + /// This room has been upgraded and won't receive updates anymore + void upgraded(QString serverMessage, Room* successor); + /// An attempted room upgrade has failed + void upgradeFailed(QString errorMessage); - // Clients should use Connection::joinRoom() and Room::leaveRoom() - // to change the room state - void setJoinState( JoinState state ); + /// The room is about to be deleted + void beforeDestruction(Room*); protected: /// Returns true if any of room names/aliases has changed - virtual bool processStateEvent(const RoomEvent& e); - virtual void processEphemeralEvent(EventPtr&& event); - virtual void processAccountDataEvent(EventPtr&& event); + virtual Changes processStateEvent(const RoomEvent& e); + virtual Changes processEphemeralEvent(EventPtr&& event); + virtual Changes processAccountDataEvent(EventPtr&& event); virtual void onAddNewTimelineEvents(timeline_iter_t /*from*/) { } virtual void onAddHistoricalTimelineEvents(rev_iter_t /*from*/) { } virtual void onRedaction(const RoomEvent& /*prevEvent*/, const RoomEvent& /*after*/) { } + virtual QJsonObject toJson() const; + virtual void updateData(SyncRoomData&& data, bool fromCache = false); private: + friend class Connection; + class Private; Private* d; + + // This is called from Connection, reflecting a state change that + // arrived from the server. Clients should use + // Connection::joinRoom() and Room::leaveRoom() to change the state. + void setJoinState(JoinState state); }; class MemberSorter @@ -461,3 +622,4 @@ namespace QMatrixClient }; } // namespace QMatrixClient Q_DECLARE_METATYPE(QMatrixClient::FileTransferInfo) +Q_DECLARE_OPERATORS_FOR_FLAGS(QMatrixClient::Room::Changes) diff --git a/lib/settings.cpp b/lib/settings.cpp index 852e19cb..124d7042 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -84,18 +84,21 @@ void SettingsGroup::remove(const QString& key) Settings::remove(fullKey); } -QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", "", setDeviceId) -QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", "", setDeviceName) +QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, setDeviceId) +QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, setDeviceName) QMC_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn) +static const auto HomeserverKey = QStringLiteral("homeserver"); +static const auto AccessTokenKey = QStringLiteral("access_token"); + QUrl AccountSettings::homeserver() const { - return QUrl::fromUserInput(value("homeserver").toString()); + return QUrl::fromUserInput(value(HomeserverKey).toString()); } void AccountSettings::setHomeserver(const QUrl& url) { - setValue("homeserver", url.toString()); + setValue(HomeserverKey, url.toString()); } QString AccountSettings::userId() const @@ -105,19 +108,19 @@ QString AccountSettings::userId() const QString AccountSettings::accessToken() const { - return value("access_token").toString(); + return value(AccessTokenKey).toString(); } void AccountSettings::setAccessToken(const QString& accessToken) { qCWarning(MAIN) << "Saving access_token to QSettings is insecure." " Developers, please save access_token separately."; - setValue("access_token", accessToken); + setValue(AccessTokenKey, accessToken); } void AccountSettings::clearAccessToken() { - legacySettings.remove("access_token"); - legacySettings.remove("device_id"); // Force the server to re-issue it - remove("access_token"); + legacySettings.remove(AccessTokenKey); + legacySettings.remove(QStringLiteral("device_id")); // Force the server to re-issue it + remove(AccessTokenKey); } diff --git a/lib/settings.h b/lib/settings.h index 0b3ecaff..759bda35 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -119,7 +119,7 @@ type classname::propname() const \ \ void classname::setter(type newValue) \ { \ - setValue(QStringLiteral(qsettingname), newValue); \ + setValue(QStringLiteral(qsettingname), std::move(newValue)); \ } \ class AccountSettings: public SettingsGroup diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp new file mode 100644 index 00000000..21517884 --- /dev/null +++ b/lib/syncdata.cpp @@ -0,0 +1,228 @@ +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> + * + * 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 "syncdata.h" + +#include "events/eventloader.h" + +#include <QtCore/QFile> +#include <QtCore/QFileInfo> + +using namespace QMatrixClient; + +const QString SyncRoomData::UnreadCountKey = + QStringLiteral("x-qmatrixclient.unread_count"); + +bool RoomSummary::isEmpty() const +{ + return joinedMemberCount.omitted() && invitedMemberCount.omitted() && + heroes.omitted(); +} + +bool RoomSummary::merge(const RoomSummary& other) +{ + // Using bitwise OR to prevent computation shortcut. + return + joinedMemberCount.merge(other.joinedMemberCount) | + invitedMemberCount.merge(other.invitedMemberCount) | + heroes.merge(other.heroes); +} + +QDebug QMatrixClient::operator<<(QDebug dbg, const RoomSummary& rs) +{ + QDebugStateSaver _(dbg); + QStringList sl; + if (!rs.joinedMemberCount.omitted()) + sl << QStringLiteral("joined: %1").arg(rs.joinedMemberCount.value()); + if (!rs.invitedMemberCount.omitted()) + sl << QStringLiteral("invited: %1").arg(rs.invitedMemberCount.value()); + if (!rs.heroes.omitted()) + sl << QStringLiteral("heroes: [%1]").arg(rs.heroes.value().join(',')); + dbg.nospace().noquote() << sl.join(QStringLiteral("; ")); + return dbg; +} + +void JsonObjectConverter<RoomSummary>::dumpTo(QJsonObject& jo, + const RoomSummary& rs) +{ + addParam<IfNotEmpty>(jo, QStringLiteral("m.joined_member_count"), + rs.joinedMemberCount); + addParam<IfNotEmpty>(jo, QStringLiteral("m.invited_member_count"), + rs.invitedMemberCount); + addParam<IfNotEmpty>(jo, QStringLiteral("m.heroes"), rs.heroes); +} + +void JsonObjectConverter<RoomSummary>::fillFrom(const QJsonObject& jo, + RoomSummary& rs) +{ + fromJson(jo["m.joined_member_count"_ls], rs.joinedMemberCount); + fromJson(jo["m.invited_member_count"_ls], rs.invitedMemberCount); + fromJson(jo["m.heroes"_ls], rs.heroes); +} + +template <typename EventsArrayT, typename StrT> +inline EventsArrayT load(const QJsonObject& batches, StrT keyName) +{ + return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls)); +} + +SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, + const QJsonObject& room_) + : roomId(roomId_) + , joinState(joinState_) + , summary(fromJson<RoomSummary>(room_["summary"_ls])) + , state(load<StateEvents>(room_, joinState == JoinState::Invite + ? "invite_state"_ls : "state"_ls)) +{ + switch (joinState) { + case JoinState::Join: + ephemeral = load<Events>(room_, "ephemeral"_ls); + FALLTHROUGH; + case JoinState::Leave: + { + accountData = load<Events>(room_, "account_data"_ls); + timeline = load<RoomEvents>(room_, "timeline"_ls); + const auto timelineJson = room_.value("timeline"_ls).toObject(); + timelineLimited = timelineJson.value("limited"_ls).toBool(); + timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString(); + + break; + } + default: /* nothing on top of state */; + } + + const auto unreadJson = room_.value("unread_notifications"_ls).toObject(); + unreadCount = unreadJson.value(UnreadCountKey).toInt(-2); + highlightCount = unreadJson.value("highlight_count"_ls).toInt(); + notificationCount = unreadJson.value("notification_count"_ls).toInt(); + if (highlightCount > 0 || notificationCount > 0) + qCDebug(SYNCJOB) << "Room" << roomId_ + << "has highlights:" << highlightCount + << "and notifications:" << notificationCount; +} + +SyncData::SyncData(const QString& cacheFileName) +{ + QFileInfo cacheFileInfo { cacheFileName }; + auto json = loadJson(cacheFileName); + auto requiredVersion = std::get<0>(cacheVersion()); + auto actualVersion = json.value("cache_version"_ls).toObject() + .value("major"_ls).toInt(); + if (actualVersion == requiredVersion) + parseJson(json, cacheFileInfo.absolutePath() + '/'); + else + qCWarning(MAIN) + << "Major version of the cache file is" << actualVersion << "but" + << requiredVersion << "is required; discarding the cache"; +} + +SyncDataList&& SyncData::takeRoomData() +{ + return move(roomData); +} + +QString SyncData::fileNameForRoom(QString roomId) +{ + roomId.replace(':', '_'); + return roomId + ".json"; +} + +Events&& SyncData::takePresenceData() +{ + return std::move(presenceData); +} + +Events&& SyncData::takeAccountData() +{ + return std::move(accountData); +} + +Events&& SyncData::takeToDeviceEvents() +{ + return std::move(toDeviceEvents); +} + +QJsonObject SyncData::loadJson(const QString& fileName) +{ + QFile roomFile { fileName }; + if (!roomFile.exists()) + { + qCWarning(MAIN) << "No state cache file" << fileName; + return {}; + } + if(!roomFile.open(QIODevice::ReadOnly)) + { + qCWarning(MAIN) << "Failed to open state cache file" + << roomFile.fileName(); + return {}; + } + auto data = roomFile.readAll(); + + const auto json = + (data.startsWith('{') ? QJsonDocument::fromJson(data) + : QJsonDocument::fromBinaryData(data)).object(); + if (json.isEmpty()) + { + qCWarning(MAIN) << "State cache in" << fileName + << "is broken or empty, discarding"; + } + return json; +} + +void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) +{ + QElapsedTimer et; et.start(); + + nextBatch_ = json.value("next_batch"_ls).toString(); + presenceData = load<Events>(json, "presence"_ls); + accountData = load<Events>(json, "account_data"_ls); + toDeviceEvents = load<Events>(json, "to_device"_ls); + + auto rooms = json.value("rooms"_ls).toObject(); + JoinStates::Int ii = 1; // ii is used to make a JoinState value + auto totalRooms = 0; + auto totalEvents = 0; + for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) + { + const auto rs = rooms.value(JoinStateStrings[i]).toObject(); + // We have a Qt container on the right and an STL one on the left + roomData.reserve(static_cast<size_t>(rs.size())); + for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) + { + auto roomJson = roomIt->isObject() + ? roomIt->toObject() + : loadJson(baseDir + fileNameForRoom(roomIt.key())); + if (roomJson.isEmpty()) + { + unresolvedRoomIds.push_back(roomIt.key()); + continue; + } + roomData.emplace_back(roomIt.key(), JoinState(ii), roomJson); + const auto& r = roomData.back(); + totalEvents += r.state.size() + r.ephemeral.size() + + r.accountData.size() + r.timeline.size(); + } + totalRooms += rs.size(); + } + if (!unresolvedRoomIds.empty()) + qCWarning(MAIN) << "Unresolved rooms:" << unresolvedRoomIds.join(','); + if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" + << totalRooms << "room(s)," + << totalEvents << "event(s) in" << et; +} diff --git a/lib/syncdata.h b/lib/syncdata.h new file mode 100644 index 00000000..8694626e --- /dev/null +++ b/lib/syncdata.h @@ -0,0 +1,116 @@ +/****************************************************************************** + * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> + * + * 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 "joinstate.h" +#include "events/stateevent.h" + +namespace QMatrixClient { + /// Room summary, as defined in MSC688 + /** + * Every member of this structure is an Omittable; as per the MSC, only + * changed values are sent from the server so if nothing is in the payload + * the respective member will be omitted. In particular, `heroes.omitted()` + * means that nothing has come from the server; heroes.value().isEmpty() + * means a peculiar case of a room with the only member - the current user. + */ + struct RoomSummary + { + Omittable<int> joinedMemberCount; + Omittable<int> invitedMemberCount; + Omittable<QStringList> heroes; //< mxids of users to take part in the room name + + bool isEmpty() const; + /// Merge the contents of another RoomSummary object into this one + /// \return true, if the current object has changed; false otherwise + bool merge(const RoomSummary& other); + + friend QDebug operator<<(QDebug dbg, const RoomSummary& rs); + }; + + template <> + struct JsonObjectConverter<RoomSummary> + { + static void dumpTo(QJsonObject& jo, const RoomSummary& rs); + static void fillFrom(const QJsonObject& jo, RoomSummary& rs); + }; + + class SyncRoomData + { + public: + QString roomId; + JoinState joinState; + RoomSummary summary; + StateEvents state; + RoomEvents timeline; + Events ephemeral; + Events accountData; + + bool timelineLimited; + QString timelinePrevBatch; + int unreadCount; + int highlightCount; + int notificationCount; + + SyncRoomData(const QString& roomId, JoinState joinState_, + const QJsonObject& room_); + SyncRoomData(SyncRoomData&&) = default; + SyncRoomData& operator=(SyncRoomData&&) = default; + + static const QString UnreadCountKey; + }; + + // QVector cannot work with non-copiable objects, std::vector can. + using SyncDataList = std::vector<SyncRoomData>; + + class SyncData + { + public: + SyncData() = default; + explicit SyncData(const QString& cacheFileName); + /** Parse sync response into room events + * \param json response from /sync or a room state cache + * \return the list of rooms with missing cache files; always + * empty when parsing response from /sync + */ + void parseJson(const QJsonObject& json, const QString& baseDir = {}); + + Events&& takePresenceData(); + Events&& takeAccountData(); + Events&& takeToDeviceEvents(); + SyncDataList&& takeRoomData(); + + QString nextBatch() const { return nextBatch_; } + + QStringList unresolvedRooms() const { return unresolvedRoomIds; } + + static std::pair<int, int> cacheVersion() { return { 10, 0 }; } + static QString fileNameForRoom(QString roomId); + + private: + QString nextBatch_; + Events presenceData; + Events accountData; + Events toDeviceEvents; + SyncDataList roomData; + QStringList unresolvedRoomIds; + + static QJsonObject loadJson(const QString& fileName); + }; +} // namespace QMatrixClient diff --git a/lib/user.cpp b/lib/user.cpp index bfd23ae2..17db5760 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -59,7 +59,7 @@ class User::Private QMultiHash<QString, const Room*> otherNames; Avatar mostUsedAvatar { makeAvatar({}) }; std::vector<Avatar> otherAvatars; - auto otherAvatar(QUrl url) + auto otherAvatar(const QUrl& url) { return std::find_if(otherAvatars.begin(), otherAvatars.end(), [&url] (const auto& av) { return av.url() == url; }); @@ -69,7 +69,7 @@ class User::Private mutable int totalRooms = 0; QString nameForRoom(const Room* r, const QString& hint = {}) const; - void setNameForRoom(const Room* r, QString newName, QString oldName); + void setNameForRoom(const Room* r, QString newName, const QString& oldName); QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const; void setAvatarForRoom(const Room* r, const QUrl& newUrl, const QUrl& oldUrl); @@ -82,7 +82,8 @@ class User::Private QString User::Private::nameForRoom(const Room* r, const QString& hint) const { // If the hint is accurate, this function is O(1) instead of O(n) - if (hint == mostUsedName || otherNames.contains(hint, r)) + if (!hint.isNull() + && (hint == mostUsedName || otherNames.contains(hint, r))) return hint; return otherNames.key(r, mostUsedName); } @@ -90,7 +91,7 @@ QString User::Private::nameForRoom(const Room* r, const QString& hint) const static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20; void User::Private::setNameForRoom(const Room* r, QString newName, - QString oldName) + const QString& oldName) { Q_ASSERT(oldName != newName); Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r)); @@ -117,7 +118,8 @@ void User::Private::setNameForRoom(const Room* r, QString newName, et.start(); } - for (auto* r1: connection->roomMap()) + const auto& roomMap = connection->roomMap(); + for (auto* r1: roomMap) if (nameForRoom(r1) == mostUsedName) otherNames.insert(mostUsedName, r1); @@ -177,7 +179,8 @@ void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl, auto nextMostUsedIt = otherAvatar(newUrl); Q_ASSERT(nextMostUsedIt != otherAvatars.end()); std::swap(mostUsedAvatar, *nextMostUsedIt); - for (const auto* r1: connection->roomMap()) + const auto& roomMap = connection->roomMap(); + for (const auto* r1: roomMap) if (avatarUrlForRoom(r1) == nextMostUsedIt->url()) avatarsToRooms.insert(nextMostUsedIt->url(), r1); @@ -264,8 +267,9 @@ void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, void User::rename(const QString& newName) { - auto job = connection()->callApi<SetDisplayNameJob>(id(), newName); - connect(job, &BaseJob::success, this, [=] { updateName(newName); }); + const auto actualNewName = sanitized(newName); + connect(connection()->callApi<SetDisplayNameJob>(id(), actualNewName), + &BaseJob::success, this, [=] { updateName(actualNewName); }); } void User::rename(const QString& newName, const Room* r) @@ -279,10 +283,11 @@ void User::rename(const QString& newName, const Room* r) } Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__, "Attempt to rename a user that's not a room member"); + const auto actualNewName = sanitized(newName); MemberEventContent evtC; - evtC.displayName = newName; - auto job = r->setMemberState(id(), RoomMemberEvent(move(evtC))); - connect(job, &BaseJob::success, this, [=] { updateName(newName, r); }); + evtC.displayName = actualNewName; + connect(r->setMemberState(id(), RoomMemberEvent(move(evtC))), + &BaseJob::success, this, [=] { updateName(actualNewName, r); }); } bool User::setAvatar(const QString& fileName) @@ -312,6 +317,11 @@ void User::unmarkIgnore() connection()->removeFromIgnoredUsers(this); } +bool User::isIgnored() const +{ + return connection()->isIgnored(this); +} + void User::Private::setAvatarOnServer(QString contentUri, User* q) { auto* j = connection->callApi<SetAvatarUrlJob>(userId, contentUri); @@ -372,19 +382,18 @@ QUrl User::avatarUrl(const Room* room) const return avatarObject(room).url(); } -void User::processEvent(const RoomMemberEvent& event, const Room* room) +void User::processEvent(const RoomMemberEvent& event, const Room* room, + bool firstMention) { Q_ASSERT(room); + + if (firstMention) + ++d->totalRooms; + if (event.membership() != MembershipType::Invite && event.membership() != MembershipType::Join) return; - auto aboutToEnter = room->memberJoinState(this) == JoinState::Leave && - (event.membership() == MembershipType::Join || - event.membership() == MembershipType::Invite); - if (aboutToEnter) - ++d->totalRooms; - auto newName = event.displayName(); // `bridged` value uses the same notification signal as the name; // it is assumed that first setting of the bridge occurs together with @@ -392,7 +401,7 @@ void User::processEvent(const RoomMemberEvent& event, const Room* room) // exceptionally rare (the only reasonable case being that the bridge // changes the naming convention). For the same reason room-specific // bridge tags are not supported at all. - QRegularExpression reSuffix(" \\((IRC|Gitter|Telegram)\\)$"); + QRegularExpression reSuffix(QStringLiteral(" \\((IRC|Gitter|Telegram)\\)$")); auto match = reSuffix.match(newName); if (match.hasMatch()) { @@ -105,7 +105,11 @@ namespace QMatrixClient QString avatarMediaId(const Room* room = nullptr) const; QUrl avatarUrl(const Room* room = nullptr) const; - void processEvent(const RoomMemberEvent& event, const Room* r); + /// This method is for internal use and should not be called + /// from client code + // FIXME: Move it away to private in lib 0.6 + void processEvent(const RoomMemberEvent& event, const Room* r, + bool firstMention); public slots: /** Set a new name in the global user profile */ @@ -125,6 +129,8 @@ namespace QMatrixClient void ignore(); /** Remove the user from the ignore list */ void unmarkIgnore(); + /** Check whether the user is in ignore list */ + bool isIgnored() const; signals: void nameAboutToChange(QString newName, QString oldName, diff --git a/lib/util.cpp b/lib/util.cpp index 1773fcfe..17674b84 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -19,6 +19,9 @@ #include "util.h" #include <QtCore/QRegularExpression> +#include <QtCore/QStandardPaths> +#include <QtCore/QDir> +#include <QtCore/QStringBuilder> static const auto RegExpOptions = QRegularExpression::CaseInsensitiveOption @@ -35,29 +38,128 @@ static void linkifyUrls(QString& htmlEscapedText) // comma or dot // Note: outer parentheses are a part of C++ raw string delimiters, not of // the regex (see http://en.cppreference.com/w/cpp/language/string_literal). + // Note2: the next-outer parentheses are \N in the replacement. static const QRegularExpression FullUrlRegExp(QStringLiteral( - R"(((www\.(?!\.)|[a-z][a-z0-9+.-]*://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))" + R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet)://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))" ), RegExpOptions); // email address: // [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] static const QRegularExpression EmailAddressRegExp(QStringLiteral( - R"((mailto:)?(\b(\w|\.|-)+@(\w|\.|-)+\.\w+\b))" + R"(\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))" + ), RegExpOptions); + // 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]+))" ), RegExpOptions); - // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,& + // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&," htmlEscapedText.replace(EmailAddressRegExp, QStringLiteral(R"(<a href="mailto:\2">\1\2</a>)")); htmlEscapedText.replace(FullUrlRegExp, QStringLiteral(R"(<a href="\1">\1</a>)")); + htmlEscapedText.replace(MxIdRegExp, + QStringLiteral(R"(\1<a href="https://matrix.to/#/\2">\2</a>)")); +} + +QString QMatrixClient::sanitized(const QString& plainText) +{ + auto text = plainText; + text.remove(QChar(0x202e)); + text.remove(QChar(0x202d)); + return text; } QString QMatrixClient::prettyPrint(const QString& plainText) { auto pt = QStringLiteral("<span style='white-space:pre-wrap'>") + - plainText.toHtmlEscaped() + QStringLiteral("</span>"); + plainText.toHtmlEscaped() + QStringLiteral("</span>"); pt.replace('\n', QStringLiteral("<br/>")); linkifyUrls(pt); return pt; } + +QString QMatrixClient::cacheLocation(const QString& dirName) +{ + const QString cachePath = + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + % '/' % dirName % '/'; + QDir dir; + if (!dir.exists(cachePath)) + dir.mkpath(cachePath); + return cachePath; +} + +// Tests for function_traits<> + +#ifdef Q_CC_CLANG +#pragma clang diagnostic push +#pragma ide diagnostic ignored "OCSimplifyInspection" +#endif +using namespace QMatrixClient; + +int f(); +static_assert(std::is_same<fn_return_t<decltype(f)>, int>::value, + "Test fn_return_t<>"); + +void f1(int); +static_assert(function_traits<decltype(f1)>::arg_number == 1, + "Test fn_arg_number"); + +void f2(int, QString); +static_assert(std::is_same<fn_arg_t<decltype(f2), 1>, QString>::value, + "Test fn_arg_t<>"); + +struct S { int mf(); }; +static_assert(is_callable_v<decltype(&S::mf)>, "Test member function"); +static_assert(returns<int, decltype(&S::mf)>(), "Test returns<> with member function"); + +struct Fo { int operator()(); }; +static_assert(is_callable_v<Fo>, "Test is_callable<> with function object"); +static_assert(function_traits<Fo>::arg_number == 0, "Test function object"); +static_assert(std::is_same<fn_return_t<Fo>, int>::value, + "Test return type of function object"); + +struct Fo1 { void operator()(int); }; +static_assert(function_traits<Fo1>::arg_number == 1, "Test function object 1"); +static_assert(is_callable_v<Fo1>, "Test is_callable<> with function object 1"); +static_assert(std::is_same<fn_arg_t<Fo1>, int>(), + "Test fn_arg_t defaulting to first argument"); + +#if (!defined(_MSC_VER) || _MSC_VER >= 1910) +static auto l = [] { return 1; }; +static_assert(is_callable_v<decltype(l)>, "Test is_callable_v<> with lambda"); +static_assert(std::is_same<fn_return_t<decltype(l)>, int>::value, + "Test fn_return_t<> with lambda"); +#endif + +template <typename T> +struct fn_object +{ + static int smf(double) { return 0; } +}; +template <> +struct fn_object<QString> +{ + void operator()(QString); +}; +static_assert(is_callable_v<fn_object<QString>>, "Test function object"); +static_assert(returns<void, fn_object<QString>>(), + "Test returns<> with function object"); +static_assert(!is_callable_v<fn_object<int>>, "Test non-function object"); +// FIXME: These two don't work +//static_assert(is_callable_v<decltype(&fn_object<int>::smf)>, +// "Test static member function"); +//static_assert(returns<int, decltype(&fn_object<int>::smf)>(), +// "Test returns<> with static member function"); + +template <typename T> +QString ft(T&&) { return {}; } +static_assert(std::is_same<fn_arg_t<decltype(ft<QString>)>, QString&&>(), + "Test function templates"); + +#ifdef Q_CC_CLANG +#pragma clang diagnostic pop +#endif @@ -18,8 +18,9 @@ #pragma once -#include <QtCore/QPointer> -#if (QT_VERSION < QT_VERSION_CHECK(5, 5, 0)) +#include <QtCore/QLatin1String> + +#if QT_VERSION < QT_VERSION_CHECK(5, 5, 0) #include <QtCore/QMetaEnum> #include <QtCore/QDebug> #endif @@ -27,10 +28,12 @@ #include <functional> #include <memory> -#if __cplusplus >= 201703L +#if __has_cpp_attribute(fallthrough) #define FALLTHROUGH [[fallthrough]] #elif __has_cpp_attribute(clang::fallthrough) #define FALLTHROUGH [[clang::fallthrough]] +#elif __has_cpp_attribute(gnu::fallthrough) +#define FALLTHROUGH [[gnu::fallthrough]] #else #define FALLTHROUGH // -fallthrough #endif @@ -49,6 +52,14 @@ template <typename T> static void qAsConst(const T &&) Q_DECL_EQ_DELETE; #endif +// MSVC 2015 and older GCC's don't handle initialisation from initializer lists +// right in the absense of a constructor; MSVC 2015, notably, fails with +// "error C2440: 'return': cannot convert from 'initializer list' to '<type>'" +#if (defined(_MSC_VER) && _MSC_VER < 1910) || \ + (defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 4) +# define BROKEN_INITIALIZER_LISTS +#endif + namespace QMatrixClient { // The below enables pretty-printing of enums in logs @@ -86,74 +97,150 @@ namespace QMatrixClient static_assert(!std::is_reference<T>::value, "You cannot make an Omittable<> with a reference type"); public: + using value_type = std::decay_t<T>; + explicit Omittable() : Omittable(none) { } - Omittable(NoneTag) : _value(std::decay_t<T>()), _omitted(true) { } - Omittable(const std::decay_t<T>& val) : _value(val) { } - Omittable(std::decay_t<T>&& val) : _value(std::move(val)) { } - Omittable<T>& operator=(const std::decay_t<T>& val) + Omittable(NoneTag) : _value(value_type()), _omitted(true) { } + Omittable(const value_type& val) : _value(val) { } + Omittable(value_type&& val) : _value(std::move(val)) { } + Omittable<T>& operator=(const value_type& val) { _value = val; _omitted = false; return *this; } - Omittable<T>& operator=(std::decay_t<T>&& val) + Omittable<T>& operator=(value_type&& val) { + // For some reason GCC complains about -Wmaybe-uninitialized + // in the context of using Omittable<bool> with converters.h; + // though the logic looks very much benign (GCC bug???) _value = std::move(val); _omitted = false; return *this; } + bool operator==(const value_type& rhs) const + { + return !omitted() && value() == rhs; + } + friend bool operator==(const value_type& lhs, + const Omittable<value_type>& rhs) + { + return rhs == lhs; + } + bool operator!=(const value_type& rhs) const + { + return !operator==(rhs); + } + friend bool operator!=(const value_type& lhs, + const Omittable<value_type>& rhs) + { + return !(rhs == lhs); + } + bool omitted() const { return _omitted; } - const std::decay_t<T>& value() const { Q_ASSERT(!_omitted); return _value; } - std::decay_t<T>& value() { Q_ASSERT(!_omitted); return _value; } - std::decay_t<T>&& release() { _omitted = true; return std::move(_value); } + const value_type& value() const + { + Q_ASSERT(!_omitted); + return _value; + } + value_type& editValue() + { + _omitted = false; + return _value; + } + /// Merge the value from another Omittable + /// \return true if \p other is not omitted and the value of + /// the current Omittable was different (or omitted); + /// in other words, if the current Omittable has changed; + /// false otherwise + template <typename T1> + auto merge(const Omittable<T1>& other) + -> std::enable_if_t<std::is_convertible<T1, T>::value, bool> + { + if (other.omitted() || + (!_omitted && _value == other.value())) + return false; + _omitted = false; + _value = other.value(); + return true; + } + value_type&& release() { _omitted = true; return std::move(_value); } - operator bool() const { return !omitted(); } - const std::decay<T>* operator->() const { return &value(); } - std::decay_t<T>* operator->() { return &value(); } - const std::decay_t<T>& operator*() const { return value(); } - std::decay_t<T>& operator*() { return value(); } + const value_type* operator->() const & { return &value(); } + value_type* operator->() & { return &editValue(); } + const value_type& operator*() const & { return value(); } + value_type& operator*() & { return editValue(); } private: T _value; bool _omitted = false; }; + namespace _impl { + template <typename AlwaysVoid, typename> struct fn_traits; + } + /** Determine traits of an arbitrary function/lambda/functor - * This only works with arity of 1 (1-argument) for now but is extendable - * to other cases. Also, doesn't work with generic lambdas and function - * objects that have operator() overloaded + * Doesn't work with generic lambdas and function objects that have + * operator() overloaded. * \sa https://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda#7943765 */ template <typename T> - struct function_traits : public function_traits<decltype(&T::operator())> - { }; // A generic function object that has (non-overloaded) operator() + struct function_traits : public _impl::fn_traits<void, T> {}; // Specialisation for a function - template <typename ReturnT, typename ArgT> - struct function_traits<ReturnT(ArgT)> + template <typename ReturnT, typename... ArgTs> + struct function_traits<ReturnT(ArgTs...)> { + static constexpr auto is_callable = true; using return_type = ReturnT; - using arg_type = ArgT; + using arg_types = std::tuple<ArgTs...>; + using function_type = std::function<ReturnT(ArgTs...)>; + static constexpr auto arg_number = std::tuple_size<arg_types>::value; }; - // Specialisation for a member function - template <typename ReturnT, typename ClassT, typename ArgT> - struct function_traits<ReturnT(ClassT::*)(ArgT)> - : function_traits<ReturnT(ArgT)> - { }; + namespace _impl { + template <typename AlwaysVoid, typename T> + struct fn_traits + { + static constexpr auto is_callable = false; + }; + + template <typename T> + struct fn_traits<decltype(void(&T::operator())), T> + : public fn_traits<void, decltype(&T::operator())> + { }; // A generic function object that has (non-overloaded) operator() - // Specialisation for a const member function - template <typename ReturnT, typename ClassT, typename ArgT> - struct function_traits<ReturnT(ClassT::*)(ArgT) const> - : function_traits<ReturnT(ArgT)> - { }; + // Specialisation for a member function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<void, ReturnT(ClassT::*)(ArgTs...)> + : function_traits<ReturnT(ArgTs...)> + { }; + + // Specialisation for a const member function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<void, ReturnT(ClassT::*)(ArgTs...) const> + : function_traits<ReturnT(ArgTs...)> + { }; + } // namespace _impl template <typename FnT> using fn_return_t = typename function_traits<FnT>::return_type; - template <typename FnT> - using fn_arg_t = typename function_traits<FnT>::arg_type; + template <typename FnT, int ArgN = 0> + using fn_arg_t = + std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>; + + template <typename R, typename FnT> + constexpr bool returns() + { + return std::is_same<fn_return_t<FnT>, R>::value; + } + + // Poor-man's is_invokable + template <typename T> + constexpr auto is_callable_v = function_traits<T>::is_callable; inline auto operator"" _ls(const char* s, std::size_t size) { @@ -209,36 +296,20 @@ namespace QMatrixClient return std::make_pair(last, sLast); } - /** A guard pointer that disconnects an interested object upon destruction - * It's almost QPointer<> except that you have to initialise it with one - * more additional parameter - a pointer to a QObject that will be - * disconnected from signals of the underlying pointer upon the guard's - * destruction. + /** Sanitize the text before showing in HTML + * This does toHtmlEscaped() and removes Unicode BiDi marks. */ - template <typename T> - class ConnectionsGuard : public QPointer<T> - { - public: - ConnectionsGuard(T* publisher, QObject* subscriber) - : QPointer<T>(publisher), subscriber(subscriber) - { } - ~ConnectionsGuard() - { - if (*this) - (*this)->disconnect(subscriber); - } - ConnectionsGuard(ConnectionsGuard&&) = default; - ConnectionsGuard& operator=(ConnectionsGuard&&) = default; - Q_DISABLE_COPY(ConnectionsGuard) - using QPointer<T>::operator=; + QString sanitized(const QString& plainText); - private: - QObject* subscriber; - }; - - /** Pretty-prints plain text into HTML + /** Pretty-print plain text into HTML * This includes HTML escaping of <,>,",& and URLs linkification. */ QString prettyPrint(const QString& plainText); + + /** Return a path to cache directory after making sure that it exists + * The returned path has a trailing slash, clients don't need to append it. + * \param dir path to cache directory relative to the standard cache path + */ + QString cacheLocation(const QString& dirName); } // namespace QMatrixClient |