diff options
Diffstat (limited to 'lib')
28 files changed, 943 insertions, 141 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp index c582cf94..4c0fe6b8 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" @@ -90,6 +91,10 @@ class Connection::Private DirectChatUsersMap directChatUsers; std::unordered_map<QString, EventPtr> accountData; QString userId; + int syncLoopTimeout = -1; + + GetCapabilitiesJob* capabilitiesJob = nullptr; + GetCapabilitiesJob::Capabilities capabilities; SyncJob* syncJob = nullptr; @@ -230,6 +235,11 @@ void Connection::doConnectToServer(const QString& user, const QString& password, }); } +void Connection::syncLoopIteration() +{ + sync(d->syncLoopTimeout); +} + void Connection::connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId) @@ -238,6 +248,39 @@ 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) + if (r->joinState() == JoinState::Join && r->successorId().isEmpty()) + 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) @@ -250,7 +293,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, @@ -319,6 +362,13 @@ void Connection::sync(int timeout) }); } +void Connection::syncLoop(int timeout) +{ + d->syncLoopTimeout = timeout; + connect(this, &Connection::syncDone, this, &Connection::syncLoopIteration); + syncLoopIteration(); // initial sync to start the loop +} + void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { d->data->setLastEvent(data.nextBatch()); for (auto&& roomData: data.takeRoomData()) @@ -343,8 +393,14 @@ void Connection::onSyncSuccess(SyncData &&data, bool fromCache) { 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()) @@ -537,7 +593,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) @@ -546,7 +603,7 @@ 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)); @@ -648,7 +705,7 @@ CreateRoomJob* Connection::createDirectChat(const QString& userId, const QString& topic, const QString& name) { return createRoom(UnpublishRoom, "", name, topic, {userId}, - "trusted_private_chat", true); + "trusted_private_chat", {}, true); } ForgetRoomJob* Connection::forgetRoom(const QString& id) @@ -1245,9 +1302,54 @@ void QMatrixClient::Connection::setLazyLoading(bool newValue) 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 9e4121f4..1faee255 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -102,6 +102,7 @@ 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(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) @@ -257,6 +258,44 @@ 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; } + + // Pretty-printing + + friend QDebug operator<<(QDebug dbg, + const SupportedRoomVersion& v) + { + QDebugStateSaver _(dbg); + return dbg.nospace() << v.id << '/' << v.status; + } + + friend QDebug operator<<(QDebug dbg, + const QVector<SupportedRoomVersion>& vs) + { + return QtPrivate::printSequentialContainer( + dbg, "", vs); + } + }; + + /// 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. * @@ -365,12 +404,19 @@ 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; @@ -402,7 +448,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 = {}); @@ -499,6 +545,7 @@ namespace QMatrixClient void resolveError(QString error); void homeserverChanged(QUrl baseUrl); + void capabilitiesLoaded(); void connected(); void reconnected(); //< \deprecated Use connected() instead @@ -679,6 +726,9 @@ namespace QMatrixClient */ void onSyncSuccess(SyncData &&data, bool fromCache = false); + protected slots: + void syncLoopIteration(); + private: class Private; std::unique_ptr<Private> d; 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..39e2f4d1 --- /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; + }; + + /// Gets information about the server's supported feature set + /// and other relevant capabilities. + 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 + + /// Gets information about the server's supported feature set + /// and other relevant capabilities. + const Capabilities& capabilities() const; + + protected: + Status parseJson(const QJsonDocument& data) override; + + private: + class Private; + QScopedPointer<Private> d; + }; +} // 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/gtad.yaml b/lib/csapi/gtad.yaml index c6ea8a13..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`: diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index ee33dac2..5e369b9a 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -67,6 +67,7 @@ class LoginJob::Private QString accessToken; QString homeServer; QString deviceId; + Omittable<DiscoveryInformation> wellKnown; }; static const auto LoginJobName = QStringLiteral("LoginJob"); @@ -111,6 +112,11 @@ 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(); @@ -118,6 +124,7 @@ BaseJob::Status LoginJob::parseJson(const QJsonDocument& data) 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/presence.cpp b/lib/csapi/presence.cpp index 8a5510b8..024d7a34 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -83,49 +83,3 @@ BaseJob::Status GetPresenceJob::parseJson(const QJsonDocument& data) 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) -{ - fromJson(data, d->data); - return Success; -} - diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h index c8f80357..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 @@ -74,56 +73,4 @@ namespace QMatrixClient 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(); - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - class Private; - QScopedPointer<Private> d; - }; } // namespace QMatrixClient 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..6f712f10 --- /dev/null +++ b/lib/csapi/room_upgrades.h @@ -0,0 +1,43 @@ +/****************************************************************************** + * 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, migrating as much + /// data as possible over to the new room. See the `room_upgrades <#room-upgrades>`_ + /// module for more information on what this entails. + 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/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/versions.cpp b/lib/csapi/versions.cpp index c853ec06..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(); + 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/wellknown.cpp b/lib/csapi/wellknown.cpp index 97505830..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" }; - fromJson(json.value("m.homeserver"_ls), d->homeserver); - fromJson(json.value("m.identity_server"_ls), d->identityServer); + 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/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/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/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 4a7780b1..628d10ec 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -325,7 +325,15 @@ 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 + 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(IncorrectRequestError, json.value("error"_ls).toString()); } @@ -568,6 +576,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"); } diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index dd6f9fc8..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 diff --git a/lib/room.cpp b/lib/room.cpp index d806183f..c6376a26 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -29,7 +29,10 @@ #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" @@ -53,6 +56,7 @@ #include <QtCore/QPointer> #include <QtCore/QDir> #include <QtCore/QTemporaryFile> +#include <QtCore/QRegularExpression> #include <QtCore/QMimeDatabase> #include <array> @@ -250,11 +254,17 @@ class Room::Private 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> @@ -296,6 +306,12 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ d->q = this; d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name + connectUntil(connection, &Connection::loadedRoomState, this, + [this] (Room* r) { + if (this == r) + emit baseStateLoaded(); + return this == r; // loadedRoomState fires only once per room + }); qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id; } @@ -309,6 +325,28 @@ const QString& Room::id() const return d->id; } +QString Room::version() const +{ + const auto v = d->getCurrentState<RoomCreateEvent>()->version(); + return v.isEmpty() ? "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; @@ -543,6 +581,26 @@ void Room::markAllMessagesAsRead() d->markMessagesAsRead(d->timeline.crbegin()); } +bool Room::canSwitchVersions() const +{ + // TODO, #276: m.room.power_levels + const auto* plEvt = + d->currentState.value({"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").toObject() + .value("m.room.tombstone"_ls).toInt( + plJson.value("state_default"_ls).toInt()); + return currentUserLevel >= tombstonePowerLevel; +} + bool Room::hasUnreadMessages() const { return unreadCount() >= 0; @@ -762,6 +820,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(); @@ -1300,6 +1366,8 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) event->setTransactionId(connection->generateTxnId()); auto* pEvent = rawPtr(event); emit q->pendingEventAboutToAdd(pEvent); + // FIXME: This sometimes causes a bad read: + // https://travis-ci.org/QMatrixClient/libqmatrixclient/jobs/492156899#L2596 unsyncedEvents.emplace_back(move(event)); emit q->pendingEventAdded(); return pEvent; @@ -1307,7 +1375,11 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) QString Room::Private::sendEvent(RoomEventPtr&& event) { - return doSendEvent(addAsPending(std::move(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) @@ -1569,7 +1641,24 @@ bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) bool Room::supportsCalls() const { - return joinedCount() == 2; + return joinedCount() == 2; +} + +void Room::checkVersion() +{ + const auto defaultVersion = connection()->defaultRoomVersion(); + const auto stableVersions = connection()->stableRoomVersions(); + Q_ASSERT(!defaultVersion.isEmpty() && successorId().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, @@ -1716,7 +1805,8 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) { // 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); @@ -1799,7 +1889,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target, 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"), @@ -2123,7 +2213,23 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) } , [this] (const EncryptionEvent&) { emit encryption(); // It can only be done once, so emit it here. - return EncryptionOn; + 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; } ); } @@ -80,6 +80,10 @@ 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) @@ -125,7 +129,7 @@ namespace QMatrixClient JoinStateChange = 0x20, TagsChange = 0x40, MembersChange = 0x80, - EncryptionOn = 0x100, + /* = 0x100, */ AccountDataChange = 0x200, SummaryChange = 0x400, ReadMarkerChange = 0x800, @@ -143,6 +147,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; @@ -371,6 +379,9 @@ 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, @@ -415,7 +426,22 @@ 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); @@ -513,6 +539,15 @@ namespace QMatrixClient void fileTransferCancelled(QString id); void callEvent(Room* room, const RoomEvent* event); + + /// 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); + /// The room is about to be deleted void beforeDestruction(Room*); @@ -103,6 +103,9 @@ namespace QMatrixClient } 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; @@ -156,7 +159,6 @@ namespace QMatrixClient } value_type&& release() { _omitted = true; return std::move(_value); } - operator const value_type&() const & { return value(); } const value_type* operator->() const & { return &value(); } value_type* operator->() & { return &editValue(); } const value_type& operator*() const & { return value(); } |