aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorarawaaa <77910862+arawaaa@users.noreply.github.com>2021-12-27 17:35:28 -0600
committerGitHub <noreply@github.com>2021-12-27 17:35:28 -0600
commit7ec3ba834dd313c4408622da30e04cdc6f4cf7c7 (patch)
tree679d7dc5939a229ad46676cd1d7aeaea7a25abce /lib
parent17bf4d180297c7e87363e179b8afa79ddb15dca7 (diff)
parent674e984e459375974f619d0e778d43a2cc928dc3 (diff)
downloadlibquotient-7ec3ba834dd313c4408622da30e04cdc6f4cf7c7.tar.gz
libquotient-7ec3ba834dd313c4408622da30e04cdc6f4cf7c7.zip
Merge branch 'dev' into pinned
Diffstat (limited to 'lib')
-rw-r--r--lib/accountregistry.cpp97
-rw-r--r--lib/accountregistry.h45
-rw-r--r--lib/application-service/definitions/protocol.h9
-rw-r--r--lib/avatar.h4
-rw-r--r--lib/connection.cpp97
-rw-r--r--lib/connection.h85
-rw-r--r--lib/connectiondata.cpp12
-rw-r--r--lib/connectiondata.h4
-rw-r--r--lib/converters.cpp11
-rw-r--r--lib/converters.h150
-rw-r--r--lib/csapi/account-data.cpp32
-rw-r--r--lib/csapi/account-data.h4
-rw-r--r--lib/csapi/admin.cpp8
-rw-r--r--lib/csapi/administrative_contact.cpp31
-rw-r--r--lib/csapi/administrative_contact.h65
-rw-r--r--lib/csapi/appservice_room_directory.cpp10
-rw-r--r--lib/csapi/appservice_room_directory.h14
-rw-r--r--lib/csapi/banning.cpp11
-rw-r--r--lib/csapi/banning.h10
-rw-r--r--lib/csapi/capabilities.cpp9
-rw-r--r--lib/csapi/content-repo.cpp59
-rw-r--r--lib/csapi/content-repo.h50
-rw-r--r--lib/csapi/create_room.cpp4
-rw-r--r--lib/csapi/create_room.h205
-rw-r--r--lib/csapi/cross_signing.cpp31
-rw-r--r--lib/csapi/cross_signing.h72
-rw-r--r--lib/csapi/definitions/cross_signing_key.h47
-rw-r--r--lib/csapi/definitions/device_keys.h8
-rw-r--r--lib/csapi/definitions/event_filter.h8
-rw-r--r--lib/csapi/definitions/openid_token.h4
-rw-r--r--lib/csapi/definitions/public_rooms_response.h10
-rw-r--r--lib/csapi/definitions/push_condition.h17
-rw-r--r--lib/csapi/definitions/push_rule.h4
-rw-r--r--lib/csapi/definitions/request_email_validation.h5
-rw-r--r--lib/csapi/definitions/request_msisdn_validation.h5
-rw-r--r--lib/csapi/definitions/request_token_response.h8
-rw-r--r--lib/csapi/definitions/room_event_filter.h25
-rw-r--r--lib/csapi/definitions/sync_filter.h8
-rw-r--r--lib/csapi/definitions/third_party_signed.h2
-rw-r--r--lib/csapi/definitions/user_identifier.h5
-rw-r--r--lib/csapi/definitions/wellknown/homeserver.h2
-rw-r--r--lib/csapi/definitions/wellknown/identity_server.h2
-rw-r--r--lib/csapi/device_management.cpp19
-rw-r--r--lib/csapi/device_management.h6
-rw-r--r--lib/csapi/directory.cpp26
-rw-r--r--lib/csapi/directory.h20
-rw-r--r--lib/csapi/event_context.cpp13
-rw-r--r--lib/csapi/event_context.h14
-rw-r--r--lib/csapi/filter.cpp15
-rw-r--r--lib/csapi/filter.h2
-rw-r--r--lib/csapi/inviting.cpp9
-rw-r--r--lib/csapi/inviting.h16
-rw-r--r--lib/csapi/joining.cpp16
-rw-r--r--lib/csapi/joining.h32
-rw-r--r--lib/csapi/keys.cpp16
-rw-r--r--lib/csapi/keys.h68
-rw-r--r--lib/csapi/kicking.cpp4
-rw-r--r--lib/csapi/kicking.h9
-rw-r--r--lib/csapi/knocking.cpp26
-rw-r--r--lib/csapi/knocking.h55
-rw-r--r--lib/csapi/leaving.cpp25
-rw-r--r--lib/csapi/leaving.h11
-rw-r--r--lib/csapi/list_joined_rooms.cpp9
-rw-r--r--lib/csapi/list_joined_rooms.h2
-rw-r--r--lib/csapi/list_public_rooms.cpp24
-rw-r--r--lib/csapi/list_public_rooms.h2
-rw-r--r--lib/csapi/login.cpp9
-rw-r--r--lib/csapi/login.h38
-rw-r--r--lib/csapi/logout.cpp14
-rw-r--r--lib/csapi/logout.h11
-rw-r--r--lib/csapi/message_pagination.cpp9
-rw-r--r--lib/csapi/message_pagination.h34
-rw-r--r--lib/csapi/notifications.cpp10
-rw-r--r--lib/csapi/notifications.h10
-rw-r--r--lib/csapi/openid.cpp8
-rw-r--r--lib/csapi/openid.h9
-rw-r--r--lib/csapi/peeking_events.cpp9
-rw-r--r--lib/csapi/peeking_events.h14
-rw-r--r--lib/csapi/presence.cpp12
-rw-r--r--lib/csapi/presence.h2
-rw-r--r--lib/csapi/profile.cpp30
-rw-r--r--lib/csapi/profile.h12
-rw-r--r--lib/csapi/pusher.cpp9
-rw-r--r--lib/csapi/pusher.h42
-rw-r--r--lib/csapi/pushrules.cpp61
-rw-r--r--lib/csapi/pushrules.h32
-rw-r--r--lib/csapi/read_markers.cpp5
-rw-r--r--lib/csapi/read_markers.h2
-rw-r--r--lib/csapi/receipts.cpp8
-rw-r--r--lib/csapi/receipts.h4
-rw-r--r--lib/csapi/redaction.cpp6
-rw-r--r--lib/csapi/redaction.h9
-rw-r--r--lib/csapi/registration.cpp40
-rw-r--r--lib/csapi/registration.h137
-rw-r--r--lib/csapi/report_content.cpp12
-rw-r--r--lib/csapi/report_content.h3
-rw-r--r--lib/csapi/room_send.cpp8
-rw-r--r--lib/csapi/room_send.h5
-rw-r--r--lib/csapi/room_state.cpp8
-rw-r--r--lib/csapi/room_state.h28
-rw-r--r--lib/csapi/room_upgrades.cpp5
-rw-r--r--lib/csapi/rooms.cpp43
-rw-r--r--lib/csapi/rooms.h18
-rw-r--r--lib/csapi/search.cpp6
-rw-r--r--lib/csapi/search.h26
-rw-r--r--lib/csapi/sso_login_redirect.cpp33
-rw-r--r--lib/csapi/sso_login_redirect.h36
-rw-r--r--lib/csapi/tags.cpp24
-rw-r--r--lib/csapi/tags.h4
-rw-r--r--lib/csapi/third_party_lookup.cpp49
-rw-r--r--lib/csapi/third_party_membership.cpp5
-rw-r--r--lib/csapi/third_party_membership.h15
-rw-r--r--lib/csapi/to_device.cpp8
-rw-r--r--lib/csapi/to_device.h2
-rw-r--r--lib/csapi/typing.cpp6
-rw-r--r--lib/csapi/typing.h6
-rw-r--r--lib/csapi/users.cpp4
-rw-r--r--lib/csapi/users.h6
-rw-r--r--lib/csapi/versions.cpp7
-rw-r--r--lib/csapi/versions.h10
-rw-r--r--lib/csapi/voip.cpp9
-rw-r--r--lib/csapi/wellknown.cpp7
-rw-r--r--lib/csapi/wellknown.h2
-rw-r--r--lib/csapi/whoami.cpp9
-rw-r--r--lib/csapi/whoami.h12
-rw-r--r--lib/eventitem.h14
-rw-r--r--lib/events/accountdataevents.h15
-rw-r--r--lib/events/callanswerevent.h4
-rw-r--r--lib/events/callcandidatesevent.cpp27
-rw-r--r--lib/events/callcandidatesevent.h6
-rw-r--r--lib/events/callhangupevent.cpp36
-rw-r--r--lib/events/callhangupevent.h8
-rw-r--r--lib/events/callinviteevent.h4
-rw-r--r--lib/events/encryptedevent.h14
-rw-r--r--lib/events/encryptedfile.h88
-rw-r--r--lib/events/encryptionevent.cpp8
-rw-r--r--lib/events/encryptionevent.h5
-rw-r--r--lib/events/event.cpp3
-rw-r--r--lib/events/event.h298
-rw-r--r--lib/events/eventcontent.cpp63
-rw-r--r--lib/events/eventcontent.h52
-rw-r--r--lib/events/eventloader.h18
-rw-r--r--lib/events/reactionevent.h2
-rw-r--r--lib/events/receiptevent.cpp29
-rw-r--r--lib/events/receiptevent.h5
-rw-r--r--lib/events/redactionevent.h2
-rw-r--r--lib/events/roomavatarevent.h4
-rw-r--r--lib/events/roomcreateevent.cpp27
-rw-r--r--lib/events/roomcreateevent.h2
-rw-r--r--lib/events/roomevent.cpp13
-rw-r--r--lib/events/roomevent.h52
-rw-r--r--lib/events/roomkeyevent.h8
-rw-r--r--lib/events/roommemberevent.cpp77
-rw-r--r--lib/events/roommemberevent.h58
-rw-r--r--lib/events/roommessageevent.cpp18
-rw-r--r--lib/events/roommessageevent.h25
-rw-r--r--lib/events/roomtombstoneevent.cpp4
-rw-r--r--lib/events/simplestateevents.h19
-rw-r--r--lib/events/stateevent.cpp31
-rw-r--r--lib/events/stateevent.h27
-rw-r--r--lib/events/stickerevent.cpp2
-rw-r--r--lib/events/typingevent.cpp2
-rw-r--r--lib/eventstats.cpp98
-rw-r--r--lib/eventstats.h114
-rw-r--r--lib/function_traits.cpp53
-rw-r--r--lib/function_traits.h93
-rw-r--r--lib/identity/definitions/request_email_validation.h6
-rw-r--r--lib/identity/definitions/request_msisdn_validation.h8
-rw-r--r--lib/jobs/basejob.cpp174
-rw-r--r--lib/jobs/basejob.h94
-rw-r--r--lib/jobs/mediathumbnailjob.cpp8
-rw-r--r--lib/jobs/requestdata.cpp5
-rw-r--r--lib/jobs/requestdata.h5
-rw-r--r--lib/jobs/syncjob.cpp2
-rw-r--r--lib/joinstate.h32
-rw-r--r--lib/logging.cpp2
-rw-r--r--lib/logging.h39
-rw-r--r--lib/mxcreply.cpp70
-rw-r--r--lib/mxcreply.h29
-rw-r--r--lib/networkaccessmanager.cpp78
-rw-r--r--lib/networkaccessmanager.h5
-rw-r--r--lib/qt_connection_util.h30
-rw-r--r--lib/quotient_common.h103
-rw-r--r--lib/room.cpp1196
-rw-r--r--lib/room.h473
-rw-r--r--lib/settings.cpp15
-rw-r--r--lib/settings.h19
-rw-r--r--lib/syncdata.cpp58
-rw-r--r--lib/syncdata.h24
-rw-r--r--lib/uri.cpp22
-rw-r--r--lib/uriresolver.cpp2
-rw-r--r--lib/uriresolver.h19
-rw-r--r--lib/user.cpp35
-rw-r--r--lib/user.h24
-rw-r--r--lib/util.cpp57
-rw-r--r--lib/util.h136
196 files changed, 4277 insertions, 2651 deletions
diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp
new file mode 100644
index 00000000..a292ed45
--- /dev/null
+++ b/lib/accountregistry.cpp
@@ -0,0 +1,97 @@
+// SPDX-FileCopyrightText: Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "accountregistry.h"
+
+#include "connection.h"
+
+using namespace Quotient;
+
+void AccountRegistry::add(Connection* c)
+{
+ if (m_accounts.contains(c))
+ return;
+ beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size());
+ m_accounts += c;
+ endInsertRows();
+}
+
+void AccountRegistry::drop(Connection* c)
+{
+ beginRemoveRows(QModelIndex(), m_accounts.indexOf(c), m_accounts.indexOf(c));
+ m_accounts.removeOne(c);
+ endRemoveRows();
+ Q_ASSERT(!m_accounts.contains(c));
+}
+
+bool AccountRegistry::isLoggedIn(const QString &userId) const
+{
+ return std::any_of(m_accounts.cbegin(), m_accounts.cend(),
+ [&userId](Connection* a) { return a->userId() == userId; });
+}
+
+bool AccountRegistry::contains(Connection *c) const
+{
+ return m_accounts.contains(c);
+}
+
+AccountRegistry::AccountRegistry() = default;
+
+QVariant AccountRegistry::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid()) {
+ return {};
+ }
+
+ if (index.row() >= m_accounts.count()) {
+ return {};
+ }
+
+ auto account = m_accounts[index.row()];
+
+ if (role == ConnectionRole) {
+ return QVariant::fromValue(account);
+ }
+
+ return {};
+}
+
+int AccountRegistry::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid()) {
+ return 0;
+ }
+
+ return m_accounts.count();
+}
+
+QHash<int, QByteArray> AccountRegistry::roleNames() const
+{
+ return {{ConnectionRole, "connection"}};
+}
+
+bool AccountRegistry::isEmpty() const
+{
+ return m_accounts.isEmpty();
+}
+
+int AccountRegistry::count() const
+{
+ return m_accounts.count();
+}
+
+const QVector<Connection*> AccountRegistry::accounts() const
+{
+ return m_accounts;
+}
+
+Connection* AccountRegistry::get(const QString& userId)
+{
+ for (const auto &connection : m_accounts) {
+ if(connection->userId() == userId) {
+ return connection;
+ }
+ }
+ return nullptr;
+}
diff --git a/lib/accountregistry.h b/lib/accountregistry.h
new file mode 100644
index 00000000..5efda459
--- /dev/null
+++ b/lib/accountregistry.h
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QtCore/QObject>
+#include <QtCore/QList>
+#include <QtCore/QAbstractListModel>
+
+namespace Quotient {
+class Connection;
+
+class AccountRegistry : public QAbstractListModel {
+ Q_OBJECT
+public:
+ enum EventRoles {
+ ConnectionRole = Qt::UserRole + 1,
+ };
+
+ static AccountRegistry &instance() {
+ static AccountRegistry _instance;
+ return _instance;
+ }
+
+ const QVector<Connection*> accounts() const;
+ void add(Connection* a);
+ void drop(Connection* a);
+ bool isLoggedIn(const QString& userId) const;
+ bool isEmpty() const;
+ int count() const;
+ bool contains(Connection*) const;
+ Connection* get(const QString& userId);
+
+ [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+
+ [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
+
+private:
+ AccountRegistry();
+
+ QVector<Connection *> m_accounts;
+};
+}
diff --git a/lib/application-service/definitions/protocol.h b/lib/application-service/definitions/protocol.h
index 6aee9c57..213dbf19 100644
--- a/lib/application-service/definitions/protocol.h
+++ b/lib/application-service/definitions/protocol.h
@@ -40,7 +40,7 @@ struct ProtocolInstance {
/// provided at the higher level Protocol object.
QString icon;
- /// Preset values for ``fields`` the client may use to search by.
+ /// Preset values for `fields` the client may use to search by.
QJsonObject fields;
/// A unique identifier across all instances.
@@ -81,10 +81,9 @@ struct ThirdPartyProtocol {
/// A content URI representing an icon for the third party protocol.
QString icon;
- /// The type definitions for the fields defined in the ``user_fields`` and
- /// ``location_fields``. Each entry in those arrays MUST have an entry here.
- /// The
- /// ``string`` key for this object is field name itself.
+ /// The type definitions for the fields defined in the `user_fields` and
+ /// `location_fields`. Each entry in those arrays MUST have an entry here.
+ /// The `string` key for this object is field name itself.
///
/// May be an empty object if no fields are defined.
QHash<QString, FieldType> fieldTypes;
diff --git a/lib/avatar.h b/lib/avatar.h
index be125c17..d4634aea 100644
--- a/lib/avatar.h
+++ b/lib/avatar.h
@@ -21,7 +21,7 @@ public:
Avatar& operator=(Avatar&&);
using get_callback_t = std::function<void()>;
- using upload_callback_t = std::function<void(QString)>;
+ using upload_callback_t = std::function<void(QUrl)>;
QImage get(Connection* connection, int dimension,
get_callback_t callback) const;
@@ -42,5 +42,3 @@ private:
std::unique_ptr<Private> d;
};
} // namespace Quotient
-/// \deprecated Use namespace Quotient instead
-namespace QMatrixClient = Quotient;
diff --git a/lib/connection.cpp b/lib/connection.cpp
index a384783c..8d1c80f1 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -13,6 +13,10 @@
#include "room.h"
#include "settings.h"
#include "user.h"
+#include "accountregistry.h"
+
+// NB: since Qt 6, moc_connection.cpp needs Room and User fully defined
+#include "moc_connection.cpp"
#include "csapi/account-data.h"
#include "csapi/capabilities.h"
@@ -55,7 +59,7 @@ using namespace Quotient;
// This is very much Qt-specific; STL iterators don't have key() and value()
template <typename HashT, typename Pred>
-HashT erase_if(HashT& hashMap, Pred pred)
+HashT remove_if(HashT& hashMap, Pred pred)
{
HashT removals;
for (auto it = hashMap.begin(); it != hashMap.end();) {
@@ -73,8 +77,6 @@ public:
explicit Private(std::unique_ptr<ConnectionData>&& connection)
: data(move(connection))
{}
- Q_DISABLE_COPY(Private)
- DISABLE_MOVE(Private)
Connection* q = nullptr;
std::unique_ptr<ConnectionData> data;
@@ -255,6 +257,7 @@ Connection::~Connection()
{
qCDebug(MAIN) << "deconstructing connection object for" << userId();
stopSync();
+ AccountRegistry::instance().drop(this);
}
void Connection::resolveServer(const QString& mxid)
@@ -283,7 +286,7 @@ void Connection::resolveServer(const QString& mxid)
if (d->resolverJob->error() == BaseJob::Abandoned)
return;
- if (d->resolverJob->error() != BaseJob::NotFoundError) {
+ if (d->resolverJob->error() != BaseJob::NotFound) {
if (!d->resolverJob->status().good()) {
qCWarning(MAIN)
<< "Fetching .well-known file failed, FAIL_PROMPT";
@@ -311,12 +314,6 @@ void Connection::resolveServer(const QString& mxid)
setHomeserver(maybeBaseUrl);
}
Q_ASSERT(d->loginFlowsJob != nullptr); // Ensured by setHomeserver()
- connect(d->loginFlowsJob, &BaseJob::success, this,
- &Connection::resolved);
- connect(d->loginFlowsJob, &BaseJob::failure, this, [this] {
- qCWarning(MAIN) << "Homeserver base URL sanity check failed";
- emit resolveError(tr("The homeserver doesn't seem to be working"));
- });
});
}
@@ -338,7 +335,7 @@ void Connection::loginWithPassword(const QString& userId,
const QString& initialDeviceName,
const QString& deviceId)
{
- d->checkAndConnect(userId, [=] {
+ d->checkAndConnect(userId, [=,this] {
d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(userId),
password, /*token*/ "", deviceId, initialDeviceName);
}, LoginFlows::Password);
@@ -398,7 +395,7 @@ void Connection::reloadCapabilities()
" disabling version upgrade recommendations to reduce noise";
});
connect(d->capabilitiesJob, &BaseJob::failure, this, [this] {
- if (d->capabilitiesJob->error() == BaseJob::IncorrectRequestError)
+ if (d->capabilitiesJob->error() == BaseJob::IncorrectRequest)
qCDebug(MAIN) << "Server doesn't support /capabilities;"
" version upgrade recommendations won't be issued";
});
@@ -440,6 +437,7 @@ void Connection::Private::completeSetup(const QString& mxId)
qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString()
<< "by user" << data->userId()
<< "from device" << data->deviceId();
+ AccountRegistry::instance().add(q);
#ifndef Quotient_E2EE_ENABLED
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
#else // Quotient_E2EE_ENABLED
@@ -474,10 +472,11 @@ void Connection::Private::checkAndConnect(const QString& userId,
connectFn();
else
emit q->loginError(
+ tr("Unsupported login flow"),
tr("The homeserver at %1 does not support"
" the login flow '%2'")
- .arg(data->baseUrl().toDisplayString()),
- flow->type);
+ .arg(data->baseUrl().toDisplayString(),
+ flow->type));
});
else
connectSingleShot(q, &Connection::homeserverChanged, q, connectFn);
@@ -637,7 +636,7 @@ void Connection::Private::consumeRoomData(SyncDataList&& roomDataList,
}
qWarning(MAIN) << "Room" << roomData.roomId
<< "has just been forgotten but /sync returned it in"
- << toCString(roomData.joinState)
+ << terse << roomData.joinState
<< "state - suspiciously fast turnaround";
}
if (auto* r = q->provideRoom(roomData.roomId, roomData.joinState)) {
@@ -660,21 +659,21 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents)
// After running this loop, the account data events not saved in
// accountData (see the end of the loop body) are auto-cleaned away
for (auto&& eventPtr: accountDataEvents) {
- visit(*eventPtr,
+ switchOnType(*eventPtr,
[this](const DirectChatEvent& dce) {
// https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events
const auto& usersToDCs = dce.usersToDirectChats();
DirectChatsMap remoteRemovals =
- erase_if(directChats, [&usersToDCs, this](auto it) {
+ remove_if(directChats, [&usersToDCs, this](auto it) {
return !(
usersToDCs.contains(it.key()->id(), it.value())
|| dcLocalAdditions.contains(it.key(), it.value()));
});
- erase_if(directChatUsers, [&remoteRemovals](auto it) {
+ remove_if(directChatUsers, [&remoteRemovals](auto it) {
return remoteRemovals.contains(it.value(), it.key());
});
// Remove from dcLocalRemovals what the server already has.
- erase_if(dcLocalRemovals, [&remoteRemovals](auto it) {
+ remove_if(dcLocalRemovals, [&remoteRemovals](auto it) {
return remoteRemovals.contains(it.key(), it.value());
});
if (MAIN().isDebugEnabled())
@@ -702,7 +701,7 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents)
<< "Couldn't get a user object for" << it.key();
}
// Remove from dcLocalAdditions what the server already has.
- erase_if(dcLocalAdditions, [&remoteAdditions](auto it) {
+ remove_if(dcLocalAdditions, [&remoteAdditions](auto it) {
return remoteAdditions.contains(it.key(), it.value());
});
if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty())
@@ -761,7 +760,7 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents)
<< ee.senderId();
// encryptionManager->updateDeviceKeys();
- visit(*sessionDecryptMessage(ee),
+ switchOnType(*sessionDecryptMessage(ee),
[this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) {
if (auto* detectedRoom = q->room(roomKeyEvent.roomId()))
detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey);
@@ -792,22 +791,17 @@ void Connection::stopSync()
QString Connection::nextBatchToken() const { return d->data->lastEvent(); }
-PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event)
-{
- return callApi<PostReceiptJob>(room->id(), "m.read", event->id());
-}
-
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
- // (or it might already be there due to a sync completing earlier).
- // finished() is used here instead of success() to overtake clients
- // that may add their own slots to finished().
+ auto* const job = callApi<JoinRoomJob>(roomAlias, serverNames);
+ // Upon completion, ensure a room object is created in case it hasn't come
+ // with a sync yet. If the room object is not there, provideRoom() will
+ // create it in Join state. finished() is used here instead of success()
+ // to overtake clients that may add their own slots to finished().
connect(job, &BaseJob::finished, this, [this, job] {
if (job->status().good())
- provideRoom(job->roomId(), JoinState::Join);
+ provideRoom(job->roomId());
});
return job;
}
@@ -840,6 +834,15 @@ inline auto splitMediaId(const QString& mediaId)
return idParts;
}
+QUrl Connection::makeMediaUrl(QUrl mxcUrl) const
+{
+ Q_ASSERT(mxcUrl.scheme() == "mxc");
+ QUrlQuery q(mxcUrl.query());
+ q.addQueryItem(QStringLiteral("user_id"), userId());
+ mxcUrl.setQuery(q);
+ return mxcUrl;
+}
+
MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId,
QSize requestedSize,
RunningPolicy policy)
@@ -1050,7 +1053,7 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id)
connect(leaveJob, &BaseJob::result, this,
[this, leaveJob, forgetJob, room] {
if (leaveJob->error() == BaseJob::Success
- || leaveJob->error() == BaseJob::NotFoundError) {
+ || leaveJob->error() == BaseJob::NotFound) {
run(forgetJob);
// If the matching /sync response hasn't arrived yet,
// mark the room for explicit deletion
@@ -1069,7 +1072,7 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id)
connect(forgetJob, &BaseJob::result, this, [this, id, forgetJob] {
// Leave room in case of success, or room not known by server
if (forgetJob->error() == BaseJob::Success
- || forgetJob->error() == BaseJob::NotFoundError)
+ || forgetJob->error() == BaseJob::NotFound)
d->removeRoom(id); // Delete the room from roomMap
else
qCWarning(MAIN).nospace() << "Error forgetting room " << id << ": "
@@ -1236,20 +1239,6 @@ int Connection::millisToReconnect() const
return d->syncJob ? d->syncJob->millisToRetry() : 0;
}
-QHash<QPair<QString, bool>, Room*> Connection::roomMap() const
-{
- // Copy-on-write-and-remove-elements is faster than copying elements one by
- // one.
- QHash<QPair<QString, bool>, Room*> roomMap = d->roomMap;
- for (auto it = roomMap.begin(); it != roomMap.end();) {
- if (it.value()->joinState() == JoinState::Leave)
- it = roomMap.erase(it);
- else
- ++it;
- }
- return roomMap;
-}
-
QVector<Room*> Connection::allRooms() const
{
QVector<Room*> result;
@@ -1352,8 +1341,8 @@ void Connection::Private::removeRoom(const QString& roomId)
{
for (auto f : { false, true })
if (auto r = roomMap.take({ roomId, f })) {
- qCDebug(MAIN) << "Room" << r->objectName() << "in state"
- << toCString(r->joinState()) << "will be deleted";
+ qCDebug(MAIN) << "Room" << r->objectName() << "in state" << terse
+ << r->joinState() << "will be deleted";
emit r->beforeDestruction(r);
r->deleteLater();
}
@@ -1385,7 +1374,7 @@ void Connection::removeFromDirectChats(const QString& roomId, User* user)
removals.insert(user, roomId);
d->dcLocalRemovals.insert(user, roomId);
} else {
- removals = erase_if(d->directChats,
+ removals = remove_if(d->directChats,
[&roomId](auto it) { return it.value() == roomId; });
d->directChatUsers.remove(roomId);
d->dcLocalRemovals += removals;
@@ -1524,8 +1513,8 @@ room_factory_t Connection::roomFactory() { return _roomFactory; }
user_factory_t Connection::userFactory() { return _userFactory; }
-room_factory_t Connection::_roomFactory = defaultRoomFactory<>();
-user_factory_t Connection::_userFactory = defaultUserFactory<>();
+room_factory_t Connection::_roomFactory = defaultRoomFactory<>;
+user_factory_t Connection::_userFactory = defaultUserFactory<>;
QByteArray Connection::generateTxnId() const
{
@@ -1722,7 +1711,7 @@ void Connection::getTurnServers()
{
auto job = callApi<GetTurnServerJob>();
connect(job, &GetTurnServerJob::success, this,
- [=] { emit turnServersChanged(job->data()); });
+ [this,job] { emit turnServersChanged(job->data()); });
}
const QString Connection::SupportedRoomVersion::StableTag =
diff --git a/lib/connection.h b/lib/connection.h
index 0d22d01f..0713af16 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -6,7 +6,6 @@
#pragma once
#include "ssosession.h"
-#include "joinstate.h"
#include "qt_connection_util.h"
#include "quotient_common.h"
@@ -82,11 +81,9 @@ using user_factory_t = std::function<User*(Connection*, const QString&)>;
* \sa Connection::setRoomFactory, Connection::setRoomType
*/
template <typename T = Room>
-static inline room_factory_t defaultRoomFactory()
+auto defaultRoomFactory(Connection* c, const QString& id, JoinState js)
{
- return [](Connection* c, const QString& id, JoinState js) {
- return new T(c, id, js);
- };
+ return new T(c, id, js);
}
/** The default factory to create user objects
@@ -95,9 +92,9 @@ static inline room_factory_t defaultRoomFactory()
* \sa Connection::setUserFactory, Connection::setUserType
*/
template <typename T = User>
-static inline user_factory_t defaultUserFactory()
+auto defaultUserFactory(Connection* c, const QString& id)
{
- return [](Connection* c, const QString& id) { return new T(id, c); };
+ return new T(id, c);
}
// Room ids, rather than room pointers, are used in the direct chat
@@ -125,6 +122,7 @@ class Connection : public QObject {
Q_PROPERTY(bool supportsPasswordAuth READ supportsPasswordAuth NOTIFY loginFlowsChanged STORED false)
Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged)
Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged)
+ Q_PROPERTY(bool canChangePassword READ canChangePassword NOTIFY capabilitiesLoaded)
public:
using UsersToDevicesToEvents =
@@ -139,15 +137,6 @@ public:
explicit Connection(const QUrl& server, QObject* parent = nullptr);
~Connection() override;
- /// Get all Invited and Joined rooms
- /*!
- * \return a hashmap from a composite key - room name and whether
- * it's an Invite rather than Join - to room pointers
- * \sa allRooms, rooms, roomsWithTag
- */
- [[deprecated("Use allRooms(), roomsWithTag() or rooms(joinStates) instead")]]
- QHash<QPair<QString, bool>, Room*> roomMap() const;
-
/// Get all rooms known within this Connection
/*!
* This includes Invite, Join and Leave rooms, in no particular order.
@@ -478,21 +467,34 @@ public:
template <typename T>
static void setRoomType()
{
- setRoomFactory(defaultRoomFactory<T>());
+ setRoomFactory(defaultRoomFactory<T>);
}
/// Set the user factory to default with the overriden user type
template <typename T>
static void setUserType()
{
- setUserFactory(defaultUserFactory<T>());
+ setUserFactory(defaultUserFactory<T>);
}
public Q_SLOTS:
- /** Set the homeserver base URL */
+ /// \brief Set the homeserver base URL and retrieve its login flows
+ ///
+ /// \sa LoginFlowsJob, loginFlows, loginFlowsChanged, homeserverChanged
void setHomeserver(const QUrl& baseUrl);
- /** Determine and set the homeserver from MXID */
+ /// \brief Determine and set the homeserver from MXID
+ ///
+ /// This attempts to resolve the homeserver by requesting
+ /// .well-known/matrix/client record from the server taken from the MXID
+ /// serverpart. If there is no record found, the serverpart itself is
+ /// attempted as the homeserver base URL; if the record is there but
+ /// is malformed (e.g., the homeserver base URL cannot be found in it)
+ /// resolveError() is emitted and further processing stops. Otherwise,
+ /// setHomeserver is called, preparing the Connection object for the login
+ /// attempt.
+ /// \param mxid user Matrix ID, such as @someone:example.org
+ /// \sa setHomeserver, homeserverChanged, loginFlowsChanged, resolveError
void resolveServer(const QString& mxid);
/** \brief Log in using a username and password pair
@@ -525,30 +527,12 @@ public Q_SLOTS:
*/
void assumeIdentity(const QString& mxId, const QString& accessToken,
const QString& deviceId);
- /*! \deprecated Use loginWithPassword instead */
- void connectToServer(const QString& userId, const QString& password,
- const QString& initialDeviceName,
- const QString& deviceId = {})
- {
- loginWithPassword(userId, password, initialDeviceName, deviceId);
- }
- /*! \deprecated
- * Use assumeIdentity() if you have an access token or
- * loginWithToken() if you have a login token.
- */
- void connectWithToken(const QString& userId, const QString& accessToken,
- const QString& deviceId)
- {
- assumeIdentity(userId, accessToken, deviceId);
- }
/// Explicitly request capabilities from the server
void reloadCapabilities();
/// Find out if capabilites are still loading from the server
bool loadingCapabilities() const;
- /** @deprecated Use stopSync() instead */
- void disconnectFromServer() { stopSync(); }
void logout();
void sync(int timeout = -1);
@@ -557,6 +541,8 @@ public Q_SLOTS:
void stopSync();
QString nextBatchToken() const;
+ Q_INVOKABLE QUrl makeMediaUrl(QUrl mxcUrl) const;
+
virtual MediaThumbnailJob*
getThumbnail(const QString& mediaId, QSize requestedSize,
RunningPolicy policy = BackgroundRequest);
@@ -663,24 +649,12 @@ public Q_SLOTS:
/** \deprecated Do not use this directly, use Room::leaveRoom() instead */
virtual LeaveRoomJob* leaveRoom(Room* room);
- // Old API that will be abolished any time soon. DO NOT USE.
-
- /** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead
- */
- virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event);
-
Q_SIGNALS:
- /**
- * @deprecated
- * This was a signal resulting from a successful resolveServer().
- * Since Connection now provides setHomeserver(), the HS URL
- * may change even without resolveServer() invocation. Use
- * loginFLowsChanged() instead of resolved(). You can also use
- * loginWith*() and assumeIdentity() without the HS URL set in
- * advance (i.e. without calling resolveServer), as they trigger
- * server name resolution from MXID if the server URL is not valid.
- */
- void resolved();
+ /// \brief Initial server resolution has failed
+ ///
+ /// This signal is emitted when resolveServer() did not manage to resolve
+ /// the homeserver using its .well-known/client record or otherwise.
+ /// \sa resolveServer
void resolveError(QString error);
void homeserverChanged(QUrl baseUrl);
@@ -688,7 +662,6 @@ Q_SIGNALS:
void capabilitiesLoaded();
void connected();
- void reconnected(); //< \deprecated Use connected() instead
void loggedOut();
/** Login data or state have changed
*
diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp
index e54d909b..87ad4577 100644
--- a/lib/connectiondata.cpp
+++ b/lib/connectiondata.cpp
@@ -118,18 +118,6 @@ void ConnectionData::setToken(QByteArray token)
d->accessToken = std::move(token);
}
-void ConnectionData::setHost(QString host)
-{
- d->baseUrl.setHost(host);
- qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl;
-}
-
-void ConnectionData::setPort(int port)
-{
- d->baseUrl.setPort(port);
- qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl;
-}
-
const QString& ConnectionData::deviceId() const { return d->deviceId; }
const QString& ConnectionData::userId() const { return d->userId; }
diff --git a/lib/connectiondata.h b/lib/connectiondata.h
index 7dd96f26..e16a2dac 100644
--- a/lib/connectiondata.h
+++ b/lib/connectiondata.h
@@ -31,10 +31,6 @@ public:
void setBaseUrl(QUrl baseUrl);
void setToken(QByteArray accessToken);
- [[deprecated("Use setBaseUrl() instead")]]
- void setHost(QString host);
- [[deprecated("Use setBaseUrl() instead")]]
- void setPort(int port);
void setDeviceId(const QString& deviceId);
void setUserId(const QString& userId);
void setNeedsToken(const QString& requestName);
diff --git a/lib/converters.cpp b/lib/converters.cpp
index 4338e8ed..444ca4f6 100644
--- a/lib/converters.cpp
+++ b/lib/converters.cpp
@@ -5,24 +5,23 @@
#include <QtCore/QVariant>
-using namespace Quotient;
-
-QJsonValue JsonConverter<QVariant>::dump(const QVariant& v)
+QJsonValue Quotient::JsonConverter<QVariant>::dump(const QVariant& v)
{
return QJsonValue::fromVariant(v);
}
-QVariant JsonConverter<QVariant>::load(const QJsonValue& jv)
+QVariant Quotient::JsonConverter<QVariant>::load(const QJsonValue& jv)
{
return jv.toVariant();
}
-QJsonObject JsonConverter<QVariantHash>::dump(const QVariantHash& vh)
+QJsonObject Quotient::toJson(const QVariantHash& vh)
{
return QJsonObject::fromVariantHash(vh);
}
-QVariantHash JsonConverter<QVariantHash>::load(const QJsonValue& jv)
+template<>
+QVariantHash Quotient::fromJson(const QJsonValue& jv)
{
return jv.toObject().toVariantHash();
}
diff --git a/lib/converters.h b/lib/converters.h
index e07b6ee4..9c3d5749 100644
--- a/lib/converters.h
+++ b/lib/converters.h
@@ -24,6 +24,21 @@ struct JsonObjectConverter {
static void fillFrom(const QJsonObject& jo, T& pod) { pod = T(jo); }
};
+//! \brief The switchboard for extra conversion algorithms behind from/toJson
+//!
+//! This template is mainly intended for partial conversion specialisations
+//! since from/toJson are functions and cannot be partially specialised.
+//! Another case for JsonConverter is to insulate types that can be constructed
+//! from basic types - namely, QVariant and QUrl can be directly constructed
+//! from QString and having an overload or specialisation for those leads to
+//! ambiguity between these and QJsonValue. For trivial (converting
+//! QJsonObject/QJsonValue) and most simple cases such as primitive types or
+//! QString this class is not needed.
+//!
+//! Do NOT call the functions of this class directly unless you know what you're
+//! doing; and do not try to specialise basic things unless you're really sure
+//! that they are not supported and it's not feasible to support those by means
+//! of overloading toJson() and specialising fromJson().
template <typename T>
struct JsonConverter {
static QJsonObject dump(const T& pod)
@@ -42,13 +57,17 @@ struct JsonConverter {
static T load(const QJsonDocument& jd) { return doLoad(jd.object()); }
};
-template <typename T>
+template <typename T,
+ typename = std::enable_if_t<!std::is_constructible_v<QJsonValue, T>>>
inline auto toJson(const T& pod)
+// -> can return anything from which QJsonValue or, in some cases, QJsonDocument
+// is constructible
{
return JsonConverter<T>::dump(pod);
}
inline auto toJson(const QJsonObject& jo) { return jo; }
+inline auto toJson(const QJsonValue& jv) { return jv; }
template <typename T>
inline void fillJson(QJsonObject& json, const T& data)
@@ -62,6 +81,9 @@ inline T fromJson(const QJsonValue& jv)
return JsonConverter<T>::load(jv);
}
+template<>
+inline QJsonValue fromJson(const QJsonValue& jv) { return jv; }
+
template <typename T>
inline T fromJson(const QJsonDocument& jd)
{
@@ -95,80 +117,68 @@ inline void fillFromJson(const QJsonValue& jv, T& pod)
// JsonConverter<> specialisations
-template <typename T>
-struct TrivialJsonDumper {
- // Works for: QJsonValue (and all things it can consume),
- // QJsonObject, QJsonArray
- static auto dump(const T& val) { return val; }
-};
+template<>
+inline bool fromJson(const QJsonValue& jv) { return jv.toBool(); }
template <>
-struct JsonConverter<bool> : public TrivialJsonDumper<bool> {
- static auto load(const QJsonValue& jv) { return jv.toBool(); }
-};
+inline int fromJson(const QJsonValue& jv) { return jv.toInt(); }
template <>
-struct JsonConverter<int> : public TrivialJsonDumper<int> {
- static auto load(const QJsonValue& jv) { return jv.toInt(); }
-};
+inline double fromJson(const QJsonValue& jv) { return jv.toDouble(); }
template <>
-struct JsonConverter<double> : public TrivialJsonDumper<double> {
- static auto load(const QJsonValue& jv) { return jv.toDouble(); }
-};
+inline float fromJson(const QJsonValue& jv) { return float(jv.toDouble()); }
template <>
-struct JsonConverter<float> : public TrivialJsonDumper<float> {
- static auto load(const QJsonValue& jv) { return float(jv.toDouble()); }
-};
+inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); }
template <>
-struct JsonConverter<qint64> : public TrivialJsonDumper<qint64> {
- static auto load(const QJsonValue& jv) { return qint64(jv.toDouble()); }
-};
+inline QString fromJson(const QJsonValue& jv) { return jv.toString(); }
template <>
-struct JsonConverter<QString> : public TrivialJsonDumper<QString> {
- static auto load(const QJsonValue& jv) { return jv.toString(); }
-};
+inline QJsonArray fromJson(const QJsonValue& jv) { return jv.toArray(); }
template <>
-struct JsonConverter<QDateTime> {
- static auto dump(const QDateTime& val) = delete; // not provided yet
- static auto load(const QJsonValue& jv)
- {
- return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC);
- }
-};
+inline QJsonArray fromJson(const QJsonDocument& jd) { return jd.array(); }
+inline QJsonValue toJson(const QDateTime& val)
+{
+ return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue();
+}
template <>
-struct JsonConverter<QDate> {
- static auto dump(const QDate& val) = delete; // not provided yet
- static auto load(const QJsonValue& jv)
- {
- return fromJson<QDateTime>(jv).date();
- }
-};
+inline QDateTime fromJson(const QJsonValue& jv)
+{
+ return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC);
+}
+inline QJsonValue toJson(const QDate& val) {
+ return toJson(
+#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
+ QDateTime(val)
+#else
+ val.startOfDay()
+#endif
+ );
+}
template <>
-struct JsonConverter<QUrl> : JsonConverter<QString> {
- static auto dump(const QUrl& url) // Override on top of that for QString
- {
- return JsonConverter<QString>::dump(url.toString(QUrl::FullyEncoded));
- }
-};
+inline QDate fromJson(const QJsonValue& jv)
+{
+ return fromJson<QDateTime>(jv).date();
+}
-template <>
-struct JsonConverter<QJsonArray> : public TrivialJsonDumper<QJsonArray> {
- static auto load(const QJsonValue& jv) { return jv.toArray(); }
-};
+// Insulate QVariant and QUrl conversions into JsonConverter so that they don't
+// interfere with toJson(const QJsonValue&) over QString, since both types are
+// constructible from QString (even if QUrl requires explicit construction).
template <>
-struct JsonConverter<QByteArray> {
- static QString dump(const QByteArray& ba) { return ba.constData(); }
+struct JsonConverter<QUrl> {
static auto load(const QJsonValue& jv)
{
- return fromJson<QString>(jv).toLatin1();
+ return QUrl(jv.toString());
+ }
+ static auto dump(const QUrl& url)
+ {
+ return url.toString(QUrl::FullyEncoded);
}
};
@@ -194,15 +204,11 @@ struct JsonConverter<Omittable<T>> {
template <typename VectorT, typename T = typename VectorT::value_type>
struct JsonArrayConverter {
- static void dumpTo(QJsonArray& ar, const VectorT& vals)
- {
- for (const auto& v : vals)
- ar.push_back(toJson(v));
- }
static auto dump(const VectorT& vals)
{
QJsonArray ja;
- dumpTo(ja, vals);
+ for (const auto& v : vals)
+ ja.push_back(toJson(v));
return ja;
}
static auto load(const QJsonArray& ja)
@@ -221,14 +227,16 @@ template <typename T>
struct JsonConverter<std::vector<T>>
: public JsonArrayConverter<std::vector<T>> {};
+#if QT_VERSION_MAJOR < 6 // QVector is an alias of QList in Qt6 but not in Qt 5
template <typename T>
struct JsonConverter<QVector<T>> : public JsonArrayConverter<QVector<T>> {};
+#endif
template <typename T>
struct JsonConverter<QList<T>> : public JsonArrayConverter<QList<T>> {};
template <>
-struct JsonConverter<QStringList> : public JsonConverter<QList<QString>> {
+struct JsonConverter<QStringList> : public JsonArrayConverter<QStringList> {
static auto dump(const QStringList& sl)
{
return QJsonArray::fromStringList(sl);
@@ -240,14 +248,13 @@ struct JsonObjectConverter<QSet<QString>> {
static void dumpTo(QJsonObject& json, const QSet<QString>& s)
{
for (const auto& e : s)
- json.insert(toJson(e), QJsonObject {});
+ json.insert(e, QJsonObject {});
}
- static auto fillFrom(const QJsonObject& json, QSet<QString>& s)
+ static void fillFrom(const QJsonObject& json, QSet<QString>& s)
{
s.reserve(s.size() + json.size());
for (auto it = json.begin(); it != json.end(); ++it)
s.insert(it.key());
- return s;
}
};
@@ -260,7 +267,7 @@ struct HashMapFromJson {
}
static void fillFrom(const QJsonObject& jo, HashMapT& h)
{
- h.reserve(jo.size());
+ h.reserve(h.size() + jo.size());
for (auto it = jo.begin(); it != jo.end(); ++it)
h[it.key()] = fromJson<typename HashMapT::mapped_type>(it.value());
}
@@ -274,11 +281,9 @@ template <typename T>
struct JsonObjectConverter<QHash<QString, T>>
: public HashMapFromJson<QHash<QString, T>> {};
+QJsonObject toJson(const QVariantHash& vh);
template <>
-struct JsonConverter<QVariantHash> {
- static QJsonObject dump(const QVariantHash& vh);
- static QVariantHash load(const QJsonValue& jv);
-};
+QVariantHash fromJson(const QJsonValue& jv);
// Conditional insertion into a QJsonObject
@@ -302,16 +307,15 @@ namespace _impl {
q.addQueryItem(k, v ? QStringLiteral("true") : QStringLiteral("false"));
}
- inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals)
+ inline void addTo(QUrlQuery& q, const QString& k, const QUrl& v)
{
- for (const auto& v : vals)
- q.addQueryItem(k, v);
+ q.addQueryItem(k, v.toEncoded());
}
- inline void addTo(QUrlQuery& q, const QString&, const QJsonObject& vals)
+ inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals)
{
- for (auto it = vals.begin(); it != vals.end(); ++it)
- q.addQueryItem(it.key(), it.value().toString());
+ for (const auto& v : vals)
+ q.addQueryItem(k, v);
}
// This one is for types that don't have isEmpty() and for all types
diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp
index 6a40e908..09fc8d40 100644
--- a/lib/csapi/account-data.cpp
+++ b/lib/csapi/account-data.cpp
@@ -4,31 +4,29 @@
#include "account-data.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type,
const QJsonObject& content)
: BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/account_data/" % type)
+ makePath("/_matrix/client/r0", "/user/", userId, "/account_data/",
+ type))
{
- setRequestData(Data(toJson(content)));
+ setRequestData(RequestData(toJson(content)));
}
QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId,
const QString& type)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0") % "/user/"
- % userId % "/account_data/" % type);
+ makePath("/_matrix/client/r0", "/user/",
+ userId, "/account_data/", type));
}
GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type)
: BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/account_data/" % type)
+ makePath("/_matrix/client/r0", "/user/", userId, "/account_data/",
+ type))
{}
SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId,
@@ -36,10 +34,10 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId,
const QString& type,
const QJsonObject& content)
: BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataPerRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/rooms/" % roomId % "/account_data/" % type)
+ makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ roomId, "/account_data/", type))
{
- setRequestData(Data(toJson(content)));
+ setRequestData(RequestData(toJson(content)));
}
QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl,
@@ -48,15 +46,15 @@ QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl,
const QString& type)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/user/" % userId % "/rooms/" % roomId
- % "/account_data/" % type);
+ makePath("/_matrix/client/r0", "/user/",
+ userId, "/rooms/", roomId,
+ "/account_data/", type));
}
GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId,
const QString& roomId,
const QString& type)
: BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataPerRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/rooms/" % roomId % "/account_data/" % type)
+ makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ roomId, "/account_data/", type))
{}
diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h
index 9a31596f..0c760e80 100644
--- a/lib/csapi/account-data.h
+++ b/lib/csapi/account-data.h
@@ -12,7 +12,7 @@ namespace Quotient {
*
* Set some account_data for the client. This config is only visible to the user
* that set the account_data. The config will be synced to clients in the
- * top-level ``account_data``.
+ * top-level `account_data`.
*/
class SetAccountDataJob : public BaseJob {
public:
@@ -65,7 +65,7 @@ public:
*
* Set some account_data for the client on a given room. This config is only
* visible to the user that set the account_data. The config will be synced to
- * clients in the per-room ``account_data``.
+ * clients in the per-room `account_data`.
*/
class SetAccountDataPerRoomJob : public BaseJob {
public:
diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp
index 9619c441..81dd0624 100644
--- a/lib/csapi/admin.cpp
+++ b/lib/csapi/admin.cpp
@@ -4,18 +4,16 @@
#include "admin.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetWhoIsJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/admin/whois/" % userId);
+ makePath("/_matrix/client/r0",
+ "/admin/whois/", userId));
}
GetWhoIsJob::GetWhoIsJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetWhoIsJob"),
- QStringLiteral("/_matrix/client/r0") % "/admin/whois/" % userId)
+ makePath("/_matrix/client/r0", "/admin/whois/", userId))
{}
diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp
index fa4f475a..589c9fc1 100644
--- a/lib/csapi/administrative_contact.cpp
+++ b/lib/csapi/administrative_contact.cpp
@@ -4,25 +4,22 @@
#include "administrative_contact.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetAccount3PIDsJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/account/3pid");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/account/3pid"));
}
GetAccount3PIDsJob::GetAccount3PIDsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetAccount3PIDsJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid")
+ makePath("/_matrix/client/r0", "/account/3pid"))
{}
Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds)
: BaseJob(HttpVerb::Post, QStringLiteral("Post3PIDsJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid")
+ makePath("/_matrix/client/r0", "/account/3pid"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("three_pid_creds"), threePidCreds);
@@ -32,7 +29,7 @@ Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds)
Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid,
const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Post, QStringLiteral("Add3PIDJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid/add")
+ makePath("/_matrix/client/r0", "/account/3pid/add"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
@@ -44,7 +41,7 @@ Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid,
Bind3PIDJob::Bind3PIDJob(const QString& clientSecret, const QString& idServer,
const QString& idAccessToken, const QString& sid)
: BaseJob(HttpVerb::Post, QStringLiteral("Bind3PIDJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid/bind")
+ makePath("/_matrix/client/r0", "/account/3pid/bind"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("client_secret"), clientSecret);
@@ -58,7 +55,7 @@ Delete3pidFromAccountJob::Delete3pidFromAccountJob(const QString& medium,
const QString& address,
const QString& idServer)
: BaseJob(HttpVerb::Post, QStringLiteral("Delete3pidFromAccountJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid/delete")
+ makePath("/_matrix/client/r0", "/account/3pid/delete"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer);
@@ -72,7 +69,7 @@ Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium,
const QString& address,
const QString& idServer)
: BaseJob(HttpVerb::Post, QStringLiteral("Unbind3pidFromAccountJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid/unbind")
+ makePath("/_matrix/client/r0", "/account/3pid/unbind"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer);
@@ -85,19 +82,19 @@ Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium,
RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob(
const EmailValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDEmailJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/account/3pid/email/requestToken",
+ makePath("/_matrix/client/r0",
+ "/account/3pid/email/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob(
const MsisdnValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDMSISDNJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/account/3pid/msisdn/requestToken",
+ makePath("/_matrix/client/r0",
+ "/account/3pid/msisdn/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h
index 1966d533..e436971d 100644
--- a/lib/csapi/administrative_contact.h
+++ b/lib/csapi/administrative_contact.h
@@ -93,14 +93,14 @@ struct JsonObjectConverter<GetAccount3PIDsJob::ThirdPartyIdentifier> {
*
* Adds contact information to the user's account.
*
- * This endpoint is deprecated in favour of the more specific ``/3pid/add``
- * and ``/3pid/bind`` endpoints.
+ * This endpoint is deprecated in favour of the more specific `/3pid/add`
+ * and `/3pid/bind` endpoints.
*
- * .. Note::
- * Previously this endpoint supported a ``bind`` parameter. This parameter
- * has been removed, making this endpoint behave as though it was ``false``.
- * This results in this endpoint being an equivalent to ``/3pid/bind`` rather
- * than dual-purpose.
+ * **Note:**
+ * Previously this endpoint supported a `bind` parameter. This parameter
+ * has been removed, making this endpoint behave as though it was `false`.
+ * This results in this endpoint being an equivalent to `/3pid/bind` rather
+ * than dual-purpose.
*/
class Post3PIDsJob : public BaseJob {
public:
@@ -144,7 +144,8 @@ struct JsonObjectConverter<Post3PIDsJob::ThreePidCredentials> {
/*! \brief Adds contact information to the user's account.
*
- * This API endpoint uses the `User-Interactive Authentication API`_.
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api).
*
* Adds contact information to the user's account. Homeservers should use 3PIDs
* added through this endpoint for password resets instead of relying on the
@@ -206,7 +207,7 @@ public:
* Removes a third party identifier from the user's account. This might not
* cause an unbind of the identifier from the identity server.
*
- * Unlike other endpoints, this endpoint does not take an ``id_access_token``
+ * Unlike other endpoints, this endpoint does not take an `id_access_token`
* parameter because the homeserver is expected to sign the request to the
* identity server instead.
*/
@@ -222,9 +223,9 @@ public:
*
* \param idServer
* The identity server to unbind from. If not provided, the homeserver
- * MUST use the ``id_server`` the identifier was added through. If the
- * homeserver does not know the original ``id_server``, it MUST return
- * a ``id_server_unbind_result`` of ``no-support``.
+ * MUST use the `id_server` the identifier was added through. If the
+ * homeserver does not know the original `id_server`, it MUST return
+ * a `id_server_unbind_result` of `no-support`.
*/
explicit Delete3pidFromAccountJob(const QString& medium,
const QString& address,
@@ -233,8 +234,8 @@ public:
// Result properties
/// An indicator as to whether or not the homeserver was able to unbind
- /// the 3PID from the identity server. ``success`` indicates that the
- /// indentity server has unbound the identifier whereas ``no-support``
+ /// the 3PID from the identity server. `success` indicates that the
+ /// indentity server has unbound the identifier whereas `no-support`
/// indicates that the identity server refuses to support the request
/// or the homeserver was not able to determine an identity server to
/// unbind from.
@@ -249,7 +250,7 @@ public:
* Removes a user's third party identifier from the provided identity server
* without removing it from the homeserver.
*
- * Unlike other endpoints, this endpoint does not take an ``id_access_token``
+ * Unlike other endpoints, this endpoint does not take an `id_access_token`
* parameter because the homeserver is expected to sign the request to the
* identity server instead.
*/
@@ -265,9 +266,9 @@ public:
*
* \param idServer
* The identity server to unbind from. If not provided, the homeserver
- * MUST use the ``id_server`` the identifier was added through. If the
- * homeserver does not know the original ``id_server``, it MUST return
- * a ``id_server_unbind_result`` of ``no-support``.
+ * MUST use the `id_server` the identifier was added through. If the
+ * homeserver does not know the original `id_server`, it MUST return
+ * a `id_server_unbind_result` of `no-support`.
*/
explicit Unbind3pidFromAccountJob(const QString& medium,
const QString& address,
@@ -276,8 +277,8 @@ public:
// Result properties
/// An indicator as to whether or not the identity server was able to unbind
- /// the 3PID. ``success`` indicates that the identity server has unbound the
- /// identifier whereas ``no-support`` indicates that the identity server
+ /// the 3PID. `success` indicates that the identity server has unbound the
+ /// identifier whereas `no-support` indicates that the identity server
/// refuses to support the request or the homeserver was not able to
/// determine an identity server to unbind from.
QString idServerUnbindResult() const
@@ -293,7 +294,9 @@ public:
* already associated with an account on this homeserver. This API should
* be used to request validation tokens when adding an email address to an
* account. This API's parameters and response are identical to that of
- * the |/register/email/requestToken|_ endpoint. The homeserver should validate
+ * the
+ * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken)
+ * endpoint. The homeserver should validate
* the email itself, either by sending a validation email itself or by using
* a service it has control over.
*/
@@ -307,9 +310,11 @@ public:
* already associated with an account on this homeserver. This API should
* be used to request validation tokens when adding an email address to an
* account. This API's parameters and response are identical to that of
- * the |/register/email/requestToken|_ endpoint. The homeserver should
- * validate the email itself, either by sending a validation email itself or
- * by using a service it has control over.
+ * the
+ * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken)
+ * endpoint. The homeserver should validate
+ * the email itself, either by sending a validation email itself or by
+ * using a service it has control over.
*/
explicit RequestTokenTo3PIDEmailJob(const EmailValidationData& body);
@@ -331,7 +336,9 @@ public:
* already associated with an account on this homeserver. This API should
* be used to request validation tokens when adding a phone number to an
* account. This API's parameters and response are identical to that of
- * the |/register/msisdn/requestToken|_ endpoint. The homeserver should validate
+ * the
+ * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken)
+ * endpoint. The homeserver should validate
* the phone number itself, either by sending a validation message itself or by
* using a service it has control over.
*/
@@ -345,9 +352,11 @@ public:
* already associated with an account on this homeserver. This API should
* be used to request validation tokens when adding a phone number to an
* account. This API's parameters and response are identical to that of
- * the |/register/msisdn/requestToken|_ endpoint. The homeserver should
- * validate the phone number itself, either by sending a validation message
- * itself or by using a service it has control over.
+ * the
+ * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken)
+ * endpoint. The homeserver should validate
+ * the phone number itself, either by sending a validation message itself
+ * or by using a service it has control over.
*/
explicit RequestTokenTo3PIDMSISDNJob(const MsisdnValidationData& body);
diff --git a/lib/csapi/appservice_room_directory.cpp b/lib/csapi/appservice_room_directory.cpp
index e8ec55bf..40d784c6 100644
--- a/lib/csapi/appservice_room_directory.cpp
+++ b/lib/csapi/appservice_room_directory.cpp
@@ -4,16 +4,14 @@
#include "appservice_room_directory.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
-UpdateAppserviceRoomDirectoryVsibilityJob::UpdateAppserviceRoomDirectoryVsibilityJob(
+UpdateAppserviceRoomDirectoryVisibilityJob::UpdateAppserviceRoomDirectoryVisibilityJob(
const QString& networkId, const QString& roomId, const QString& visibility)
: BaseJob(HttpVerb::Put,
- QStringLiteral("UpdateAppserviceRoomDirectoryVsibilityJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/directory/list/appservice/" % networkId % "/" % roomId)
+ QStringLiteral("UpdateAppserviceRoomDirectoryVisibilityJob"),
+ makePath("/_matrix/client/r0", "/directory/list/appservice/",
+ networkId, "/", roomId))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("visibility"), visibility);
diff --git a/lib/csapi/appservice_room_directory.h b/lib/csapi/appservice_room_directory.h
index 3fa02a07..56a69592 100644
--- a/lib/csapi/appservice_room_directory.h
+++ b/lib/csapi/appservice_room_directory.h
@@ -17,11 +17,11 @@ namespace Quotient {
* This API is similar to the room directory visibility API used by clients
* to update the homeserver's more general room directory.
*
- * This API requires the use of an application service access token
- * (``as_token``) instead of a typical client's access_token. This API cannot be
- * invoked by users who are not identified as application services.
+ * This API requires the use of an application service access token (`as_token`)
+ * instead of a typical client's access_token. This API cannot be invoked by
+ * users who are not identified as application services.
*/
-class UpdateAppserviceRoomDirectoryVsibilityJob : public BaseJob {
+class UpdateAppserviceRoomDirectoryVisibilityJob : public BaseJob {
public:
/*! \brief Updates a room's visibility in the application service's room
* directory.
@@ -38,9 +38,9 @@ public:
* Whether the room should be visible (public) in the directory
* or not (private).
*/
- explicit UpdateAppserviceRoomDirectoryVsibilityJob(const QString& networkId,
- const QString& roomId,
- const QString& visibility);
+ explicit UpdateAppserviceRoomDirectoryVisibilityJob(
+ const QString& networkId, const QString& roomId,
+ const QString& visibility);
};
} // namespace Quotient
diff --git a/lib/csapi/banning.cpp b/lib/csapi/banning.cpp
index 8a8ec664..472128bb 100644
--- a/lib/csapi/banning.cpp
+++ b/lib/csapi/banning.cpp
@@ -4,14 +4,12 @@
#include "banning.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
BanJob::BanJob(const QString& roomId, const QString& userId,
const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("BanJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/ban")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/ban"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("user_id"), userId);
@@ -19,12 +17,13 @@ BanJob::BanJob(const QString& roomId, const QString& userId,
setRequestData(std::move(_data));
}
-UnbanJob::UnbanJob(const QString& roomId, const QString& userId)
+UnbanJob::UnbanJob(const QString& roomId, const QString& userId,
+ const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("UnbanJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/unban")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/unban"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("user_id"), userId);
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
setRequestData(std::move(_data));
}
diff --git a/lib/csapi/banning.h b/lib/csapi/banning.h
index 37ae91ee..7a9697d3 100644
--- a/lib/csapi/banning.h
+++ b/lib/csapi/banning.h
@@ -30,7 +30,8 @@ public:
*
* \param reason
* The reason the user has been banned. This will be supplied as the
- * ``reason`` on the target's updated `m.room.member`_ event.
+ * `reason` on the target's updated
+ * [`m.room.member`](/client-server-api/#mroommember) event.
*/
explicit BanJob(const QString& roomId, const QString& userId,
const QString& reason = {});
@@ -54,8 +55,13 @@ public:
*
* \param userId
* The fully qualified user ID of the user being unbanned.
+ *
+ * \param reason
+ * Optional reason to be included as the `reason` on the subsequent
+ * membership event.
*/
- explicit UnbanJob(const QString& roomId, const QString& userId);
+ explicit UnbanJob(const QString& roomId, const QString& userId,
+ const QString& reason = {});
};
} // namespace Quotient
diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp
index 33a53cad..bc21e462 100644
--- a/lib/csapi/capabilities.cpp
+++ b/lib/csapi/capabilities.cpp
@@ -4,20 +4,17 @@
#include "capabilities.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/capabilities");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/capabilities"));
}
GetCapabilitiesJob::GetCapabilitiesJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetCapabilitiesJob"),
- QStringLiteral("/_matrix/client/r0") % "/capabilities")
+ makePath("/_matrix/client/r0", "/capabilities"))
{
addExpectedKey("capabilities");
}
diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp
index 7ae89739..6d1e38b6 100644
--- a/lib/csapi/content-repo.cpp
+++ b/lib/csapi/content-repo.cpp
@@ -4,13 +4,11 @@
#include "content-repo.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToUploadContent(const QString& filename)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("filename"), filename);
return _q;
}
@@ -18,17 +16,17 @@ auto queryToUploadContent(const QString& filename)
UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename,
const QString& contentType)
: BaseJob(HttpVerb::Post, QStringLiteral("UploadContentJob"),
- QStringLiteral("/_matrix/media/r0") % "/upload",
+ makePath("/_matrix/media/r0", "/upload"),
queryToUploadContent(filename))
{
setRequestHeader("Content-Type", contentType.toLatin1());
- setRequestData(Data(content));
+ setRequestData(RequestData(content));
addExpectedKey("content_uri");
}
auto queryToGetContent(bool allowRemote)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("allow_remote"), allowRemote);
return _q;
}
@@ -37,17 +35,16 @@ QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName,
const QString& mediaId, bool allowRemote)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/media/r0")
- % "/download/" % serverName % "/"
- % mediaId,
+ makePath("/_matrix/media/r0", "/download/",
+ serverName, "/", mediaId),
queryToGetContent(allowRemote));
}
GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId,
bool allowRemote)
: BaseJob(HttpVerb::Get, QStringLiteral("GetContentJob"),
- QStringLiteral("/_matrix/media/r0") % "/download/" % serverName
- % "/" % mediaId,
+ makePath("/_matrix/media/r0", "/download/", serverName, "/",
+ mediaId),
queryToGetContent(allowRemote), {}, false)
{
setExpectedContentTypes({ "*/*" });
@@ -55,7 +52,7 @@ GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId,
auto queryToGetContentOverrideName(bool allowRemote)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("allow_remote"), allowRemote);
return _q;
}
@@ -67,9 +64,9 @@ QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl,
bool allowRemote)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/media/r0")
- % "/download/" % serverName % "/"
- % mediaId % "/" % fileName,
+ makePath("/_matrix/media/r0", "/download/",
+ serverName, "/", mediaId, "/",
+ fileName),
queryToGetContentOverrideName(allowRemote));
}
@@ -78,8 +75,8 @@ GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName,
const QString& fileName,
bool allowRemote)
: BaseJob(HttpVerb::Get, QStringLiteral("GetContentOverrideNameJob"),
- QStringLiteral("/_matrix/media/r0") % "/download/" % serverName
- % "/" % mediaId % "/" % fileName,
+ makePath("/_matrix/media/r0", "/download/", serverName, "/",
+ mediaId, "/", fileName),
queryToGetContentOverrideName(allowRemote), {}, false)
{
setExpectedContentTypes({ "*/*" });
@@ -88,7 +85,7 @@ GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName,
auto queryToGetContentThumbnail(int width, int height, const QString& method,
bool allowRemote)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("width"), width);
addParam<>(_q, QStringLiteral("height"), height);
addParam<IfNotEmpty>(_q, QStringLiteral("method"), method);
@@ -104,8 +101,7 @@ QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl,
{
return BaseJob::makeRequestUrl(
std::move(baseUrl),
- QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName % "/"
- % mediaId,
+ makePath("/_matrix/media/r0", "/thumbnail/", serverName, "/", mediaId),
queryToGetContentThumbnail(width, height, method, allowRemote));
}
@@ -114,45 +110,44 @@ GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName,
int height, const QString& method,
bool allowRemote)
: BaseJob(HttpVerb::Get, QStringLiteral("GetContentThumbnailJob"),
- QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName
- % "/" % mediaId,
+ makePath("/_matrix/media/r0", "/thumbnail/", serverName, "/",
+ mediaId),
queryToGetContentThumbnail(width, height, method, allowRemote),
{}, false)
{
setExpectedContentTypes({ "image/jpeg", "image/png" });
}
-auto queryToGetUrlPreview(const QString& url, Omittable<qint64> ts)
+auto queryToGetUrlPreview(const QUrl& url, Omittable<qint64> ts)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("url"), url);
addParam<IfNotEmpty>(_q, QStringLiteral("ts"), ts);
return _q;
}
-QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QString& url,
+QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QUrl& url,
Omittable<qint64> ts)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/media/r0")
- % "/preview_url",
+ makePath("/_matrix/media/r0",
+ "/preview_url"),
queryToGetUrlPreview(url, ts));
}
-GetUrlPreviewJob::GetUrlPreviewJob(const QString& url, Omittable<qint64> ts)
+GetUrlPreviewJob::GetUrlPreviewJob(const QUrl& url, Omittable<qint64> ts)
: BaseJob(HttpVerb::Get, QStringLiteral("GetUrlPreviewJob"),
- QStringLiteral("/_matrix/media/r0") % "/preview_url",
+ makePath("/_matrix/media/r0", "/preview_url"),
queryToGetUrlPreview(url, ts))
{}
QUrl GetConfigJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/media/r0")
- % "/config");
+ makePath("/_matrix/media/r0", "/config"));
}
GetConfigJob::GetConfigJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetConfigJob"),
- QStringLiteral("/_matrix/media/r0") % "/config")
+ makePath("/_matrix/media/r0", "/config"))
{}
diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h
index ed67485c..28409f5c 100644
--- a/lib/csapi/content-repo.h
+++ b/lib/csapi/content-repo.h
@@ -32,11 +32,9 @@ public:
// Result properties
- /// The `MXC URI`_ to the uploaded content.
- QString contentUri() const
- {
- return loadFromJson<QString>("content_uri"_ls);
- }
+ /// The [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the
+ /// uploaded content.
+ QUrl contentUri() const { return loadFromJson<QUrl>("content_uri"_ls); }
};
/*! \brief Download content from the content repository.
@@ -47,10 +45,10 @@ public:
/*! \brief Download content from the content repository.
*
* \param serverName
- * The server name from the ``mxc://`` URI (the authoritory component)
+ * The server name from the `mxc://` URI (the authoritory component)
*
* \param mediaId
- * The media ID from the ``mxc://`` URI (the path component)
+ * The media ID from the `mxc://` URI (the path component)
*
* \param allowRemote
* Indicates to the server that it should not attempt to fetch the media
@@ -95,13 +93,13 @@ public:
* name
*
* \param serverName
- * The server name from the ``mxc://`` URI (the authoritory component)
+ * The server name from the `mxc://` URI (the authoritory component)
*
* \param mediaId
- * The media ID from the ``mxc://`` URI (the path component)
+ * The media ID from the `mxc://` URI (the path component)
*
* \param fileName
- * A filename to give in the ``Content-Disposition`` header.
+ * A filename to give in the `Content-Disposition` header.
*
* \param allowRemote
* Indicates to the server that it should not attempt to fetch the media
@@ -127,7 +125,7 @@ public:
/// The content type of the file that was previously uploaded.
QString contentType() const { return reply()->rawHeader("Content-Type"); }
- /// The ``fileName`` requested or the name of the file that was previously
+ /// The `fileName` requested or the name of the file that was previously
/// uploaded, if set.
QString contentDisposition() const
{
@@ -141,17 +139,18 @@ public:
/*! \brief Download a thumbnail of content from the content repository
*
* Download a thumbnail of content from the content repository.
- * See the `thumbnailing <#thumbnails>`_ section for more information.
+ * See the [Thumbnails](/client-server-api/#thumbnails) section for more
+ * information.
*/
class GetContentThumbnailJob : public BaseJob {
public:
/*! \brief Download a thumbnail of content from the content repository
*
* \param serverName
- * The server name from the ``mxc://`` URI (the authoritory component)
+ * The server name from the `mxc://` URI (the authoritory component)
*
* \param mediaId
- * The media ID from the ``mxc://`` URI (the path component)
+ * The media ID from the `mxc://` URI (the path component)
*
* \param width
* The *desired* width of the thumbnail. The actual thumbnail may be
@@ -162,8 +161,8 @@ public:
* larger than the size specified.
*
* \param method
- * The desired resizing method. See the `thumbnailing <#thumbnails>`_
- * section for more information.
+ * The desired resizing method. See the
+ * [Thumbnails](/client-server-api/#thumbnails) section for more information.
*
* \param allowRemote
* Indicates to the server that it should not attempt to fetch
@@ -199,11 +198,11 @@ public:
* Get information about a URL for the client. Typically this is called when a
* client sees a URL in a message and wants to render a preview for the user.
*
- * .. Note::
- * Clients should consider avoiding this endpoint for URLs posted in encrypted
- * rooms. Encrypted rooms often contain more sensitive information the users
- * do not want to share with the homeserver, and this can mean that the URLs
- * being shared should also not be shared with the homeserver.
+ * **Note:**
+ * Clients should consider avoiding this endpoint for URLs posted in encrypted
+ * rooms. Encrypted rooms often contain more sensitive information the users
+ * do not want to share with the homeserver, and this can mean that the URLs
+ * being shared should also not be shared with the homeserver.
*/
class GetUrlPreviewJob : public BaseJob {
public:
@@ -217,14 +216,14 @@ public:
* return a newer version if it does not have the requested version
* available.
*/
- explicit GetUrlPreviewJob(const QString& url, Omittable<qint64> ts = none);
+ explicit GetUrlPreviewJob(const QUrl& url, Omittable<qint64> ts = none);
/*! \brief Construct a URL without creating a full-fledged job object
*
* This function can be used when a URL for GetUrlPreviewJob
* is necessary but the job itself isn't.
*/
- static QUrl makeRequestUrl(QUrl baseUrl, const QString& url,
+ static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& url,
Omittable<qint64> ts = none);
// Result properties
@@ -235,8 +234,9 @@ public:
return loadFromJson<Omittable<qint64>>("matrix:image:size"_ls);
}
- /// An `MXC URI`_ to the image. Omitted if there is no image.
- QString ogImage() const { return loadFromJson<QString>("og:image"_ls); }
+ /// An [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the image.
+ /// Omitted if there is no image.
+ QUrl ogImage() const { return loadFromJson<QUrl>("og:image"_ls); }
};
/*! \brief Get the configuration for the content repository.
diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp
index a94f9951..9aaef87f 100644
--- a/lib/csapi/create_room.cpp
+++ b/lib/csapi/create_room.cpp
@@ -4,8 +4,6 @@
#include "create_room.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
CreateRoomJob::CreateRoomJob(const QString& visibility,
@@ -18,7 +16,7 @@ CreateRoomJob::CreateRoomJob(const QString& visibility,
const QString& preset, Omittable<bool> isDirect,
const QJsonObject& powerLevelContentOverride)
: BaseJob(HttpVerb::Post, QStringLiteral("CreateRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/createRoom")
+ makePath("/_matrix/client/r0", "/createRoom"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility);
diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h
index 6a718ff4..81dfbffc 100644
--- a/lib/csapi/create_room.h
+++ b/lib/csapi/create_room.h
@@ -16,47 +16,42 @@ namespace Quotient {
* the new room, including checking power levels for each event. It MUST
* apply the events implied by the request in the following order:
*
- * 1. The ``m.room.create`` event itself. Must be the first event in the
+ * 1. The `m.room.create` event itself. Must be the first event in the
* room.
*
- * 2. An ``m.room.member`` event for the creator to join the room. This is
+ * 2. An `m.room.member` event for the creator to join the room. This is
* needed so the remaining events can be sent.
*
- * 3. A default ``m.room.power_levels`` event, giving the room creator
+ * 3. A default `m.room.power_levels` event, giving the room creator
* (and not other members) permission to send state events. Overridden
- * by the ``power_level_content_override`` parameter.
+ * by the `power_level_content_override` parameter.
*
- * 4. Events set by the ``preset``. Currently these are the
- * ``m.room.join_rules``,
- * ``m.room.history_visibility``, and ``m.room.guest_access`` state events.
+ * 4. Events set by the `preset`. Currently these are the `m.room.join_rules`,
+ * `m.room.history_visibility`, and `m.room.guest_access` state events.
*
- * 5. Events listed in ``initial_state``, in the order that they are
+ * 5. Events listed in `initial_state`, in the order that they are
* listed.
*
- * 6. Events implied by ``name`` and ``topic`` (``m.room.name`` and
- * ``m.room.topic`` state events).
+ * 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic`
+ * state events).
*
- * 7. Invite events implied by ``invite`` and ``invite_3pid`` (``m.room.member``
- * with
- * ``membership: invite`` and ``m.room.third_party_invite``).
+ * 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member` with
+ * `membership: invite` and `m.room.third_party_invite`).
*
* The available presets do the following with respect to room state:
*
- * ======================== ============== ======================
- * ================ ========= Preset ``join_rules``
- * ``history_visibility`` ``guest_access`` Other
- * ======================== ============== ======================
- * ================ =========
- * ``private_chat`` ``invite`` ``shared`` ``can_join``
- * ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All
- * invitees are given the same power level as the room creator.
- * ``public_chat`` ``public`` ``shared`` ``forbidden``
- * ======================== ============== ======================
- * ================ =========
+ * | Preset | `join_rules` | `history_visibility` |
+ * `guest_access` | Other |
+ * |------------------------|--------------|----------------------|----------------|-------|
+ * | `private_chat` | `invite` | `shared` | `can_join`
+ * | | | `trusted_private_chat` | `invite` | `shared` |
+ * `can_join` | All invitees are given the same power level as the room
+ * creator. | | `public_chat` | `public` | `shared` |
+ * `forbidden` | |
*
- * The server will create a ``m.room.create`` event in the room with the
+ * The server will create a `m.room.create` event in the room with the
* requesting user as the creator, alongside other keys provided in the
- * ``creation_content``.
+ * `creation_content`.
*/
class CreateRoomJob : public BaseJob {
public:
@@ -68,50 +63,44 @@ public:
/// the new room, including checking power levels for each event. It MUST
/// apply the events implied by the request in the following order:
///
- /// 1. The ``m.room.create`` event itself. Must be the first event in the
+ /// 1. The `m.room.create` event itself. Must be the first event in the
/// room.
///
- /// 2. An ``m.room.member`` event for the creator to join the room. This is
+ /// 2. An `m.room.member` event for the creator to join the room. This is
/// needed so the remaining events can be sent.
///
- /// 3. A default ``m.room.power_levels`` event, giving the room creator
+ /// 3. A default `m.room.power_levels` event, giving the room creator
/// (and not other members) permission to send state events. Overridden
- /// by the ``power_level_content_override`` parameter.
+ /// by the `power_level_content_override` parameter.
///
- /// 4. Events set by the ``preset``. Currently these are the
- /// ``m.room.join_rules``,
- /// ``m.room.history_visibility``, and ``m.room.guest_access`` state
- /// events.
+ /// 4. Events set by the `preset`. Currently these are the
+ /// `m.room.join_rules`,
+ /// `m.room.history_visibility`, and `m.room.guest_access` state events.
///
- /// 5. Events listed in ``initial_state``, in the order that they are
+ /// 5. Events listed in `initial_state`, in the order that they are
/// listed.
///
- /// 6. Events implied by ``name`` and ``topic`` (``m.room.name`` and
- /// ``m.room.topic``
+ /// 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic`
/// state events).
///
- /// 7. Invite events implied by ``invite`` and ``invite_3pid``
- /// (``m.room.member`` with
- /// ``membership: invite`` and ``m.room.third_party_invite``).
+ /// 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member`
+ /// with
+ /// `membership: invite` and `m.room.third_party_invite`).
///
/// The available presets do the following with respect to room state:
///
- /// ======================== ============== ======================
- /// ================ =========
- /// Preset ``join_rules`` ``history_visibility``
- /// ``guest_access`` Other
- /// ======================== ============== ======================
- /// ================ =========
- /// ``private_chat`` ``invite`` ``shared`` ``can_join``
- /// ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All
- /// invitees are given the same power level as the room creator.
- /// ``public_chat`` ``public`` ``shared`` ``forbidden``
- /// ======================== ============== ======================
- /// ================ =========
+ /// | Preset | `join_rules` | `history_visibility` |
+ /// `guest_access` | Other |
+ /// |------------------------|--------------|----------------------|----------------|-------|
+ /// | `private_chat` | `invite` | `shared` |
+ /// `can_join` | | | `trusted_private_chat` | `invite` |
+ /// `shared` | `can_join` | All invitees are given the same
+ /// power level as the room creator. | | `public_chat` | `public`
+ /// | `shared` | `forbidden` | |
///
- /// The server will create a ``m.room.create`` event in the room with the
+ /// The server will create a `m.room.create` event in the room with the
/// requesting user as the creator, alongside other keys provided in the
- /// ``creation_content``.
+ /// `creation_content`.
struct Invite3pid {
/// The hostname+port of the identity server which should be used for
/// third party identifier lookups.
@@ -121,7 +110,7 @@ public:
/// r0.5-compatible clients and this specification version.
QString idAccessToken;
/// The kind of address being passed in the address field, for example
- /// ``email``.
+ /// `email`.
QString medium;
/// The invitee's third party identifier.
QString address;
@@ -133,50 +122,44 @@ public:
/// the new room, including checking power levels for each event. It MUST
/// apply the events implied by the request in the following order:
///
- /// 1. The ``m.room.create`` event itself. Must be the first event in the
+ /// 1. The `m.room.create` event itself. Must be the first event in the
/// room.
///
- /// 2. An ``m.room.member`` event for the creator to join the room. This is
+ /// 2. An `m.room.member` event for the creator to join the room. This is
/// needed so the remaining events can be sent.
///
- /// 3. A default ``m.room.power_levels`` event, giving the room creator
+ /// 3. A default `m.room.power_levels` event, giving the room creator
/// (and not other members) permission to send state events. Overridden
- /// by the ``power_level_content_override`` parameter.
+ /// by the `power_level_content_override` parameter.
///
- /// 4. Events set by the ``preset``. Currently these are the
- /// ``m.room.join_rules``,
- /// ``m.room.history_visibility``, and ``m.room.guest_access`` state
- /// events.
+ /// 4. Events set by the `preset`. Currently these are the
+ /// `m.room.join_rules`,
+ /// `m.room.history_visibility`, and `m.room.guest_access` state events.
///
- /// 5. Events listed in ``initial_state``, in the order that they are
+ /// 5. Events listed in `initial_state`, in the order that they are
/// listed.
///
- /// 6. Events implied by ``name`` and ``topic`` (``m.room.name`` and
- /// ``m.room.topic``
+ /// 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic`
/// state events).
///
- /// 7. Invite events implied by ``invite`` and ``invite_3pid``
- /// (``m.room.member`` with
- /// ``membership: invite`` and ``m.room.third_party_invite``).
+ /// 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member`
+ /// with
+ /// `membership: invite` and `m.room.third_party_invite`).
///
/// The available presets do the following with respect to room state:
///
- /// ======================== ============== ======================
- /// ================ =========
- /// Preset ``join_rules`` ``history_visibility``
- /// ``guest_access`` Other
- /// ======================== ============== ======================
- /// ================ =========
- /// ``private_chat`` ``invite`` ``shared`` ``can_join``
- /// ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All
- /// invitees are given the same power level as the room creator.
- /// ``public_chat`` ``public`` ``shared`` ``forbidden``
- /// ======================== ============== ======================
- /// ================ =========
+ /// | Preset | `join_rules` | `history_visibility` |
+ /// `guest_access` | Other |
+ /// |------------------------|--------------|----------------------|----------------|-------|
+ /// | `private_chat` | `invite` | `shared` |
+ /// `can_join` | | | `trusted_private_chat` | `invite` |
+ /// `shared` | `can_join` | All invitees are given the same
+ /// power level as the room creator. | | `public_chat` | `public`
+ /// | `shared` | `forbidden` | |
///
- /// The server will create a ``m.room.create`` event in the room with the
+ /// The server will create a `m.room.create` event in the room with the
/// requesting user as the creator, alongside other keys provided in the
- /// ``creation_content``.
+ /// `creation_content`.
struct StateEvent {
/// The type of event to send.
QString type;
@@ -191,12 +174,12 @@ public:
/*! \brief Create a new room
*
* \param visibility
- * A ``public`` visibility indicates that the room will be shown
- * in the published room list. A ``private`` visibility will hide
+ * A `public` visibility indicates that the room will be shown
+ * in the published room list. A `private` visibility will hide
* the room from the published room list. Rooms default to
- * ``private`` visibility if this key is not included. NB: This
- * should not be confused with ``join_rules`` which also uses the
- * word ``public``.
+ * `private` visibility if this key is not included. NB: This
+ * should not be confused with `join_rules` which also uses the
+ * word `public`.
*
* \param roomAliasName
* The desired room alias **local part**. If this is included, a
@@ -204,20 +187,20 @@ public:
* room. The alias will belong on the *same* homeserver which
* created the room. For example, if this was set to "foo" and
* sent to the homeserver "example.com" the complete room alias
- * would be ``#foo:example.com``.
+ * would be `#foo:example.com`.
*
* The complete room alias will become the canonical alias for
* the room.
*
* \param name
- * If this is included, an ``m.room.name`` event will be sent
+ * If this is included, an `m.room.name` event will be sent
* into the room to indicate the name of the room. See Room
- * Events for more information on ``m.room.name``.
+ * Events for more information on `m.room.name`.
*
* \param topic
- * If this is included, an ``m.room.topic`` event will be sent
+ * If this is included, an `m.room.topic` event will be sent
* into the room to indicate the topic for the room. See Room
- * Events for more information on ``m.room.topic``.
+ * Events for more information on `m.room.topic`.
*
* \param invite
* A list of user IDs to invite to the room. This will tell the
@@ -230,14 +213,14 @@ public:
* \param roomVersion
* The room version to set for the room. If not provided, the homeserver
* is to use its configured default. If provided, the homeserver will return
- * a 400 error with the errcode ``M_UNSUPPORTED_ROOM_VERSION`` if it does
- * not support the room version.
+ * a 400 error with the errcode `M_UNSUPPORTED_ROOM_VERSION` if it does not
+ * support the room version.
*
* \param creationContent
- * Extra keys, such as ``m.federate``, to be added to the content
- * of the `m.room.create`_ event. The server will clobber the following
- * keys: ``creator``, ``room_version``. Future versions of the
- * specification may allow the server to clobber other keys.
+ * Extra keys, such as `m.federate`, to be added to the content
+ * of the [`m.room.create`](client-server-api/#mroomcreate) event. The
+ * server will clobber the following keys: `creator`, `room_version`. Future
+ * versions of the specification may allow the server to clobber other keys.
*
* \param initialState
* A list of state events to set in the new room. This allows
@@ -245,28 +228,30 @@ public:
* room. The expected format of the state events are an object
* with type, state_key and content keys set.
*
- * Takes precedence over events set by ``preset``, but gets
- * overriden by ``name`` and ``topic`` keys.
+ * Takes precedence over events set by `preset`, but gets
+ * overriden by `name` and `topic` keys.
*
* \param preset
* Convenience parameter for setting various default state events
* based on a preset.
*
- * If unspecified, the server should use the ``visibility`` to determine
- * which preset to use. A visbility of ``public`` equates to a preset of
- * ``public_chat`` and ``private`` visibility equates to a preset of
- * ``private_chat``.
+ * If unspecified, the server should use the `visibility` to determine
+ * which preset to use. A visbility of `public` equates to a preset of
+ * `public_chat` and `private` visibility equates to a preset of
+ * `private_chat`.
*
* \param isDirect
- * This flag makes the server set the ``is_direct`` flag on the
- * ``m.room.member`` events sent to the users in ``invite`` and
- * ``invite_3pid``. See `Direct Messaging`_ for more information.
+ * This flag makes the server set the `is_direct` flag on the
+ * `m.room.member` events sent to the users in `invite` and
+ * `invite_3pid`. See [Direct
+ * Messaging](/client-server-api/#direct-messaging) for more information.
*
* \param powerLevelContentOverride
* The power level content to override in the default power level
* event. This object is applied on top of the generated
- * `m.room.power_levels`_ event content prior to it being sent to the room.
- * Defaults to overriding nothing.
+ * [`m.room.power_levels`](client-server-api/#mroompower_levels)
+ * event content prior to it being sent to the room. Defaults to
+ * overriding nothing.
*/
explicit CreateRoomJob(const QString& visibility = {},
const QString& roomAliasName = {},
diff --git a/lib/csapi/cross_signing.cpp b/lib/csapi/cross_signing.cpp
new file mode 100644
index 00000000..1fa0e949
--- /dev/null
+++ b/lib/csapi/cross_signing.cpp
@@ -0,0 +1,31 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "cross_signing.h"
+
+using namespace Quotient;
+
+UploadCrossSigningKeysJob::UploadCrossSigningKeysJob(
+ const Omittable<CrossSigningKey>& masterKey,
+ const Omittable<CrossSigningKey>& selfSigningKey,
+ const Omittable<CrossSigningKey>& userSigningKey)
+ : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningKeysJob"),
+ makePath("/_matrix/client/r0", "/keys/device_signing/upload"))
+{
+ QJsonObject _data;
+ addParam<IfNotEmpty>(_data, QStringLiteral("master_key"), masterKey);
+ addParam<IfNotEmpty>(_data, QStringLiteral("self_signing_key"),
+ selfSigningKey);
+ addParam<IfNotEmpty>(_data, QStringLiteral("user_signing_key"),
+ userSigningKey);
+ setRequestData(std::move(_data));
+}
+
+UploadCrossSigningSignaturesJob::UploadCrossSigningSignaturesJob(
+ const QHash<QString, QHash<QString, QJsonObject>>& signatures)
+ : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningSignaturesJob"),
+ makePath("/_matrix/client/r0", "/keys/signatures/upload"))
+{
+ setRequestData(RequestData(toJson(signatures)));
+}
diff --git a/lib/csapi/cross_signing.h b/lib/csapi/cross_signing.h
new file mode 100644
index 00000000..2ab65e06
--- /dev/null
+++ b/lib/csapi/cross_signing.h
@@ -0,0 +1,72 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "csapi/definitions/cross_signing_key.h"
+
+#include "jobs/basejob.h"
+
+namespace Quotient {
+
+/*! \brief Upload cross-signing keys.
+ *
+ * Publishes cross-signing keys for the user.
+ *
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api).
+ */
+class UploadCrossSigningKeysJob : public BaseJob {
+public:
+ /*! \brief Upload cross-signing keys.
+ *
+ * \param masterKey
+ * Optional. The user\'s master key.
+ *
+ * \param selfSigningKey
+ * Optional. The user\'s self-signing key. Must be signed by
+ * the accompanying master key, or by the user\'s most recently
+ * uploaded master key if no master key is included in the
+ * request.
+ *
+ * \param userSigningKey
+ * Optional. The user\'s user-signing key. Must be signed by
+ * the accompanying master key, or by the user\'s most recently
+ * uploaded master key if no master key is included in the
+ * request.
+ */
+ explicit UploadCrossSigningKeysJob(
+ const Omittable<CrossSigningKey>& masterKey = none,
+ const Omittable<CrossSigningKey>& selfSigningKey = none,
+ const Omittable<CrossSigningKey>& userSigningKey = none);
+};
+
+/*! \brief Upload cross-signing signatures.
+ *
+ * Publishes cross-signing signatures for the user. The request body is a
+ * map from user ID to key ID to signed JSON object.
+ */
+class UploadCrossSigningSignaturesJob : public BaseJob {
+public:
+ /*! \brief Upload cross-signing signatures.
+ *
+ * \param signatures
+ * The signatures to be published.
+ */
+ explicit UploadCrossSigningSignaturesJob(
+ const QHash<QString, QHash<QString, QJsonObject>>& signatures = {});
+
+ // Result properties
+
+ /// A map from user ID to key ID to an error for any signatures
+ /// that failed. If a signature was invalid, the `errcode` will
+ /// be set to `M_INVALID_SIGNATURE`.
+ QHash<QString, QHash<QString, QJsonObject>> failures() const
+ {
+ return loadFromJson<QHash<QString, QHash<QString, QJsonObject>>>(
+ "failures"_ls);
+ }
+};
+
+} // namespace Quotient
diff --git a/lib/csapi/definitions/cross_signing_key.h b/lib/csapi/definitions/cross_signing_key.h
new file mode 100644
index 00000000..0cec8161
--- /dev/null
+++ b/lib/csapi/definitions/cross_signing_key.h
@@ -0,0 +1,47 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "converters.h"
+
+namespace Quotient {
+/// Cross signing key
+struct CrossSigningKey {
+ /// The ID of the user the key belongs to.
+ QString userId;
+
+ /// What the key is used for.
+ QStringList usage;
+
+ /// The public key. The object must have exactly one property, whose name
+ /// is in the form `<algorithm>:<unpadded_base64_public_key>`, and whose
+ /// value is the unpadded base64 public key.
+ QHash<QString, QString> keys;
+
+ /// Signatures of the key, calculated using the process described at
+ /// [Signing JSON](/appendices/#signing-json). Optional for the master key.
+ /// Other keys must be signed by the user\'s master key.
+ QJsonObject signatures;
+};
+
+template <>
+struct JsonObjectConverter<CrossSigningKey> {
+ static void dumpTo(QJsonObject& jo, const CrossSigningKey& pod)
+ {
+ addParam<>(jo, QStringLiteral("user_id"), pod.userId);
+ addParam<>(jo, QStringLiteral("usage"), pod.usage);
+ addParam<>(jo, QStringLiteral("keys"), pod.keys);
+ addParam<IfNotEmpty>(jo, QStringLiteral("signatures"), pod.signatures);
+ }
+ static void fillFrom(const QJsonObject& jo, CrossSigningKey& pod)
+ {
+ fromJson(jo.value("user_id"_ls), pod.userId);
+ fromJson(jo.value("usage"_ls), pod.usage);
+ fromJson(jo.value("keys"_ls), pod.keys);
+ fromJson(jo.value("signatures"_ls), pod.signatures);
+ }
+};
+
+} // namespace Quotient
diff --git a/lib/csapi/definitions/device_keys.h b/lib/csapi/definitions/device_keys.h
index 3065f218..84ecefae 100644
--- a/lib/csapi/definitions/device_keys.h
+++ b/lib/csapi/definitions/device_keys.h
@@ -21,15 +21,15 @@ struct DeviceKeys {
QStringList algorithms;
/// Public identity keys. The names of the properties should be in the
- /// format ``<algorithm>:<device_id>``. The keys themselves should be
+ /// format `<algorithm>:<device_id>`. The keys themselves should be
/// encoded as specified by the key algorithm.
QHash<QString, QString> keys;
/// Signatures for the device key object. A map from user ID, to a map from
- /// ``<algorithm>:<device_id>`` to the signature.
+ /// `<algorithm>:<device_id>` to the signature.
///
- /// The signature is calculated using the process described at `Signing
- /// JSON`_.
+ /// The signature is calculated using the process described at [Signing
+ /// JSON](/appendices/#signing-json).
QHash<QString, QHash<QString, QString>> signatures;
};
diff --git a/lib/csapi/definitions/event_filter.h b/lib/csapi/definitions/event_filter.h
index 67497412..c55d4f92 100644
--- a/lib/csapi/definitions/event_filter.h
+++ b/lib/csapi/definitions/event_filter.h
@@ -14,13 +14,13 @@ struct EventFilter {
/// A list of sender IDs to exclude. If this list is absent then no senders
/// are excluded. A matching sender will be excluded even if it is listed in
- /// the ``'senders'`` filter.
+ /// the `'senders'` filter.
QStringList notSenders;
/// A list of event types to exclude. If this list is absent then no event
/// types are excluded. A matching type will be excluded even if it is
- /// listed in the ``'types'`` filter. A '*' can be used as a wildcard to
- /// match any sequence of characters.
+ /// listed in the `'types'` filter. A '*' can be used as a wildcard to match
+ /// any sequence of characters.
QStringList notTypes;
/// A list of senders IDs to include. If this list is absent then all
@@ -28,7 +28,7 @@ struct EventFilter {
QStringList senders;
/// A list of event types to include. If this list is absent then all event
- /// types are included. A ``'*'`` can be used as a wildcard to match any
+ /// types are included. A `'*'` can be used as a wildcard to match any
/// sequence of characters.
QStringList types;
};
diff --git a/lib/csapi/definitions/openid_token.h b/lib/csapi/definitions/openid_token.h
index 5e68c376..3c447321 100644
--- a/lib/csapi/definitions/openid_token.h
+++ b/lib/csapi/definitions/openid_token.h
@@ -11,10 +11,10 @@ namespace Quotient {
struct OpenidToken {
/// An access token the consumer may use to verify the identity of
/// the person who generated the token. This is given to the federation
- /// API ``GET /openid/userinfo`` to verify the user's identity.
+ /// API `GET /openid/userinfo` to verify the user's identity.
QString accessToken;
- /// The string ``Bearer``.
+ /// The string `Bearer`.
QString tokenType;
/// The homeserver domain the consumer should use when attempting to
diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h
index 8f30e607..2938b4ec 100644
--- a/lib/csapi/definitions/public_rooms_response.h
+++ b/lib/csapi/definitions/public_rooms_response.h
@@ -36,7 +36,13 @@ struct PublicRoomsChunk {
bool guestCanJoin;
/// The URL for the room's avatar, if one is set.
- QString avatarUrl;
+ QUrl avatarUrl;
+
+ /// The room's join rule. When not present, the room is assumed to
+ /// be `public`. Note that rooms with `invite` join rules are not
+ /// expected here, but rooms with `knock` rules are given their
+ /// near-public nature.
+ QString joinRule;
};
template <>
@@ -54,6 +60,7 @@ struct JsonObjectConverter<PublicRoomsChunk> {
addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable);
addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin);
addParam<IfNotEmpty>(jo, QStringLiteral("avatar_url"), pod.avatarUrl);
+ addParam<IfNotEmpty>(jo, QStringLiteral("join_rule"), pod.joinRule);
}
static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod)
{
@@ -66,6 +73,7 @@ struct JsonObjectConverter<PublicRoomsChunk> {
fromJson(jo.value("world_readable"_ls), pod.worldReadable);
fromJson(jo.value("guest_can_join"_ls), pod.guestCanJoin);
fromJson(jo.value("avatar_url"_ls), pod.avatarUrl);
+ fromJson(jo.value("join_rule"_ls), pod.joinRule);
}
};
diff --git a/lib/csapi/definitions/push_condition.h b/lib/csapi/definitions/push_condition.h
index a6decf1b..ce66d075 100644
--- a/lib/csapi/definitions/push_condition.h
+++ b/lib/csapi/definitions/push_condition.h
@@ -9,26 +9,27 @@
namespace Quotient {
struct PushCondition {
- /// The kind of condition to apply. See `conditions <#conditions>`_ for
- /// more information on the allowed kinds and how they work.
+ /// The kind of condition to apply. See
+ /// [conditions](/client-server-api/#conditions) for more information on the
+ /// allowed kinds and how they work.
QString kind;
- /// Required for ``event_match`` conditions. The dot-separated field of the
+ /// Required for `event_match` conditions. The dot-separated field of the
/// event to match.
///
- /// Required for ``sender_notification_permission`` conditions. The field in
+ /// Required for `sender_notification_permission` conditions. The field in
/// the power level event the user needs a minimum power level for. Fields
- /// must be specified under the ``notifications`` property in the power
- /// level event's ``content``.
+ /// must be specified under the `notifications` property in the power level
+ /// event's `content`.
QString key;
- /// Required for ``event_match`` conditions. The glob-style pattern to
+ /// Required for `event_match` conditions. The glob-style pattern to
/// match against. Patterns with no special glob characters should be
/// treated as having asterisks prepended and appended when testing the
/// condition.
QString pattern;
- /// Required for ``room_member_count`` conditions. A decimal integer
+ /// Required for `room_member_count` conditions. A decimal integer
/// optionally prefixed by one of, ==, <, >, >= or <=. A prefix of < matches
/// rooms where the member count is strictly less than the given number and
/// so forth. If no prefix is present, this parameter defaults to ==.
diff --git a/lib/csapi/definitions/push_rule.h b/lib/csapi/definitions/push_rule.h
index 43749bae..135537c1 100644
--- a/lib/csapi/definitions/push_rule.h
+++ b/lib/csapi/definitions/push_rule.h
@@ -25,10 +25,10 @@ struct PushRule {
/// The conditions that must hold true for an event in order for a rule to
/// be applied to an event. A rule with no conditions always matches. Only
- /// applicable to ``underride`` and ``override`` rules.
+ /// applicable to `underride` and `override` rules.
QVector<PushCondition> conditions;
- /// The glob-style pattern to match against. Only applicable to ``content``
+ /// The glob-style pattern to match against. Only applicable to `content`
/// rules.
QString pattern;
};
diff --git a/lib/csapi/definitions/request_email_validation.h b/lib/csapi/definitions/request_email_validation.h
index ab34862e..b1781e27 100644
--- a/lib/csapi/definitions/request_email_validation.h
+++ b/lib/csapi/definitions/request_email_validation.h
@@ -16,15 +16,14 @@ struct EmailValidationData : RequestEmailValidation {
/// 3PID verification.
///
/// This parameter is deprecated with a plan to be removed in a future
- /// specification version for ``/account/password`` and ``/register``
- /// requests.
+ /// specification version for `/account/password` and `/register` requests.
QString idServer;
/// An access token previously registered with the identity server. Servers
/// can treat this as optional to distinguish between r0.5-compatible
/// clients and this specification version.
///
- /// Required if an ``id_server`` is supplied.
+ /// Required if an `id_server` is supplied.
QString idAccessToken;
};
diff --git a/lib/csapi/definitions/request_msisdn_validation.h b/lib/csapi/definitions/request_msisdn_validation.h
index 8539cd98..4600b48c 100644
--- a/lib/csapi/definitions/request_msisdn_validation.h
+++ b/lib/csapi/definitions/request_msisdn_validation.h
@@ -16,15 +16,14 @@ struct MsisdnValidationData : RequestMsisdnValidation {
/// 3PID verification.
///
/// This parameter is deprecated with a plan to be removed in a future
- /// specification version for ``/account/password`` and ``/register``
- /// requests.
+ /// specification version for `/account/password` and `/register` requests.
QString idServer;
/// An access token previously registered with the identity server. Servers
/// can treat this as optional to distinguish between r0.5-compatible
/// clients and this specification version.
///
- /// Required if an ``id_server`` is supplied.
+ /// Required if an `id_server` is supplied.
QString idAccessToken;
};
diff --git a/lib/csapi/definitions/request_token_response.h b/lib/csapi/definitions/request_token_response.h
index 00222839..d5fbbadb 100644
--- a/lib/csapi/definitions/request_token_response.h
+++ b/lib/csapi/definitions/request_token_response.h
@@ -10,22 +10,22 @@ namespace Quotient {
struct RequestTokenResponse {
/// The session ID. Session IDs are opaque strings that must consist
- /// entirely of the characters ``[0-9a-zA-Z.=_-]``. Their length must not
+ /// entirely of the characters `[0-9a-zA-Z.=_-]`. Their length must not
/// exceed 255 characters and they must not be empty.
QString sid;
/// An optional field containing a URL where the client must submit the
/// validation token to, with identical parameters to the Identity Service
- /// API's ``POST /validate/email/submitToken`` endpoint (without the
+ /// API's `POST /validate/email/submitToken` endpoint (without the
/// requirement for an access token). The homeserver must send this token to
/// the user (if applicable), who should then be prompted to provide it to
/// the client.
///
/// If this field is not present, the client can assume that verification
/// will happen without the client's involvement provided the homeserver
- /// advertises this specification version in the ``/versions`` response
+ /// advertises this specification version in the `/versions` response
/// (ie: r0.5.0).
- QString submitUrl;
+ QUrl submitUrl;
};
template <>
diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h
index 11e87fde..91caf667 100644
--- a/lib/csapi/definitions/room_event_filter.h
+++ b/lib/csapi/definitions/room_event_filter.h
@@ -11,30 +11,31 @@
namespace Quotient {
struct RoomEventFilter : EventFilter {
- /// If ``true``, enables lazy-loading of membership events. See
- /// `Lazy-loading room members <#lazy-loading-room-members>`_
- /// for more information. Defaults to ``false``.
+ /// If `true`, enables lazy-loading of membership events. See
+ /// [Lazy-loading room
+ /// members](/client-server-api/#lazy-loading-room-members) for more
+ /// information. Defaults to `false`.
Omittable<bool> lazyLoadMembers;
- /// If ``true``, sends all membership events for all events, even if they
- /// have already been sent to the client. Does not apply unless
- /// ``lazy_load_members`` is ``true``. See `Lazy-loading room members
- /// <#lazy-loading-room-members>`_ for more information. Defaults to
- /// ``false``.
+ /// If `true`, sends all membership events for all events, even if they have
+ /// already been sent to the client. Does not apply unless
+ /// `lazy_load_members` is `true`. See [Lazy-loading room
+ /// members](/client-server-api/#lazy-loading-room-members) for more
+ /// information. Defaults to `false`.
Omittable<bool> includeRedundantMembers;
/// A list of room IDs to exclude. If this list is absent then no rooms are
/// excluded. A matching room will be excluded even if it is listed in the
- /// ``'rooms'`` filter.
+ /// `'rooms'` filter.
QStringList notRooms;
/// A list of room IDs to include. If this list is absent then all rooms are
/// included.
QStringList rooms;
- /// If ``true``, includes only events with a ``url`` key in their content.
- /// If ``false``, excludes those events. If omitted, ``url`` key is not
- /// considered for filtering.
+ /// If `true`, includes only events with a `url` key in their content. If
+ /// `false`, excludes those events. If omitted, `url` key is not considered
+ /// for filtering.
Omittable<bool> containsUrl;
};
diff --git a/lib/csapi/definitions/sync_filter.h b/lib/csapi/definitions/sync_filter.h
index 9c8f08d5..62e17962 100644
--- a/lib/csapi/definitions/sync_filter.h
+++ b/lib/csapi/definitions/sync_filter.h
@@ -14,13 +14,13 @@ namespace Quotient {
struct RoomFilter {
/// A list of room IDs to exclude. If this list is absent then no rooms are
/// excluded. A matching room will be excluded even if it is listed in the
- /// ``'rooms'`` filter. This filter is applied before the filters in
- /// ``ephemeral``, ``state``, ``timeline`` or ``account_data``
+ /// `'rooms'` filter. This filter is applied before the filters in
+ /// `ephemeral`, `state`, `timeline` or `account_data`
QStringList notRooms;
/// A list of room IDs to include. If this list is absent then all rooms are
- /// included. This filter is applied before the filters in ``ephemeral``,
- /// ``state``, ``timeline`` or ``account_data``
+ /// included. This filter is applied before the filters in `ephemeral`,
+ /// `state`, `timeline` or `account_data`
QStringList rooms;
/// The events that aren't recorded in the room history, e.g. typing and
diff --git a/lib/csapi/definitions/third_party_signed.h b/lib/csapi/definitions/third_party_signed.h
index 526545d0..7097bda4 100644
--- a/lib/csapi/definitions/third_party_signed.h
+++ b/lib/csapi/definitions/third_party_signed.h
@@ -7,7 +7,7 @@
#include "converters.h"
namespace Quotient {
-/// A signature of an ``m.third_party_invite`` token to prove that this user
+/// A signature of an `m.third_party_invite` token to prove that this user
/// owns a third party identity which has been invited to the room.
struct ThirdPartySigned {
/// The Matrix ID of the user who issued the invite.
diff --git a/lib/csapi/definitions/user_identifier.h b/lib/csapi/definitions/user_identifier.h
index dadf6f97..cb585a6a 100644
--- a/lib/csapi/definitions/user_identifier.h
+++ b/lib/csapi/definitions/user_identifier.h
@@ -9,8 +9,9 @@
namespace Quotient {
/// Identification information for a user
struct UserIdentifier {
- /// The type of identification. See `Identifier types`_ for supported
- /// values and additional property descriptions.
+ /// The type of identification. See [Identifier
+ /// types](/client-server-api/#identifier-types) for supported values and
+ /// additional property descriptions.
QString type;
/// Identification information for a user
diff --git a/lib/csapi/definitions/wellknown/homeserver.h b/lib/csapi/definitions/wellknown/homeserver.h
index 5cfaca24..b7db4182 100644
--- a/lib/csapi/definitions/wellknown/homeserver.h
+++ b/lib/csapi/definitions/wellknown/homeserver.h
@@ -10,7 +10,7 @@ namespace Quotient {
/// Used by clients to discover homeserver information.
struct HomeserverInformation {
/// The base URL for the homeserver for client-server connections.
- QString baseUrl;
+ QUrl baseUrl;
};
template <>
diff --git a/lib/csapi/definitions/wellknown/identity_server.h b/lib/csapi/definitions/wellknown/identity_server.h
index 3bd07bd1..885e3d34 100644
--- a/lib/csapi/definitions/wellknown/identity_server.h
+++ b/lib/csapi/definitions/wellknown/identity_server.h
@@ -10,7 +10,7 @@ namespace Quotient {
/// Used by clients to discover identity server information.
struct IdentityServerInformation {
/// The base URL for the identity server for client-server connections.
- QString baseUrl;
+ QUrl baseUrl;
};
template <>
diff --git a/lib/csapi/device_management.cpp b/lib/csapi/device_management.cpp
index eac9a545..da6dbc76 100644
--- a/lib/csapi/device_management.cpp
+++ b/lib/csapi/device_management.cpp
@@ -4,38 +4,35 @@
#include "device_management.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetDevicesJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/devices");
+ makePath("/_matrix/client/r0", "/devices"));
}
GetDevicesJob::GetDevicesJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetDevicesJob"),
- QStringLiteral("/_matrix/client/r0") % "/devices")
+ makePath("/_matrix/client/r0", "/devices"))
{}
QUrl GetDeviceJob::makeRequestUrl(QUrl baseUrl, const QString& deviceId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/devices/" % deviceId);
+ makePath("/_matrix/client/r0", "/devices/",
+ deviceId));
}
GetDeviceJob::GetDeviceJob(const QString& deviceId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetDeviceJob"),
- QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId)
+ makePath("/_matrix/client/r0", "/devices/", deviceId))
{}
UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId,
const QString& displayName)
: BaseJob(HttpVerb::Put, QStringLiteral("UpdateDeviceJob"),
- QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId)
+ makePath("/_matrix/client/r0", "/devices/", deviceId))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("display_name"), displayName);
@@ -45,7 +42,7 @@ UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId,
DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId,
const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"),
- QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId)
+ makePath("/_matrix/client/r0", "/devices/", deviceId))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
@@ -55,7 +52,7 @@ DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId,
DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices,
const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DeleteDevicesJob"),
- QStringLiteral("/_matrix/client/r0") % "/delete_devices")
+ makePath("/_matrix/client/r0", "/delete_devices"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("devices"), devices);
diff --git a/lib/csapi/device_management.h b/lib/csapi/device_management.h
index 47dc7ec8..7fb69873 100644
--- a/lib/csapi/device_management.h
+++ b/lib/csapi/device_management.h
@@ -83,7 +83,8 @@ public:
/*! \brief Delete a device
*
- * This API endpoint uses the `User-Interactive Authentication API`_.
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api).
*
* Deletes the given device, and invalidates any access token associated with it.
*/
@@ -104,7 +105,8 @@ public:
/*! \brief Bulk deletion of devices
*
- * This API endpoint uses the `User-Interactive Authentication API`_.
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api).
*
* Deletes the given devices, and invalidates any access token associated with
* them.
diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp
index 25ea82e2..b351b4ef 100644
--- a/lib/csapi/directory.cpp
+++ b/lib/csapi/directory.cpp
@@ -4,14 +4,11 @@
#include "directory.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId)
: BaseJob(HttpVerb::Put, QStringLiteral("SetRoomAliasJob"),
- QStringLiteral("/_matrix/client/r0") % "/directory/room/"
- % roomAlias)
+ makePath("/_matrix/client/r0", "/directory/room/", roomAlias))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("room_id"), roomId);
@@ -21,41 +18,38 @@ SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId
QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/directory/room/" % roomAlias);
+ makePath("/_matrix/client/r0",
+ "/directory/room/", roomAlias));
}
GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomIdByAliasJob"),
- QStringLiteral("/_matrix/client/r0") % "/directory/room/"
- % roomAlias,
+ makePath("/_matrix/client/r0", "/directory/room/", roomAlias),
false)
{}
QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/directory/room/" % roomAlias);
+ makePath("/_matrix/client/r0",
+ "/directory/room/", roomAlias));
}
DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomAliasJob"),
- QStringLiteral("/_matrix/client/r0") % "/directory/room/"
- % roomAlias)
+ makePath("/_matrix/client/r0", "/directory/room/", roomAlias))
{}
QUrl GetLocalAliasesJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/aliases");
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/aliases"));
}
GetLocalAliasesJob::GetLocalAliasesJob(const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetLocalAliasesJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/aliases")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/aliases"))
{
addExpectedKey("aliases");
}
diff --git a/lib/csapi/directory.h b/lib/csapi/directory.h
index 9b109aad..93a31595 100644
--- a/lib/csapi/directory.h
+++ b/lib/csapi/directory.h
@@ -68,13 +68,13 @@ public:
* instance that room aliases can only be deleted by their creator or a server
* administrator.
*
- * .. Note::
- * Servers may choose to update the ``alt_aliases`` for the
- * ``m.room.canonical_alias`` state event in the room when an alias is removed.
+ * **Note:**
+ * Servers may choose to update the `alt_aliases` for the
+ * `m.room.canonical_alias` state event in the room when an alias is removed.
* Servers which choose to update the canonical alias event are recommended to,
* in addition to their other relevant permission checks, delete the alias and
* return a successful response even if the user does not have permission to
- * update the ``m.room.canonical_alias`` event.
+ * update the `m.room.canonical_alias` event.
*/
class DeleteRoomAliasJob : public BaseJob {
public:
@@ -99,18 +99,18 @@ public:
* given room.
*
* This endpoint can be called by users who are in the room (external
- * users receive an ``M_FORBIDDEN`` error response). If the room's
- * ``m.room.history_visibility`` maps to ``world_readable``, any
+ * users receive an `M_FORBIDDEN` error response). If the room's
+ * `m.room.history_visibility` maps to `world_readable`, any
* user can call this endpoint.
*
* Servers may choose to implement additional access control checks here,
* such as allowing server administrators to view aliases regardless of
* membership.
*
- * .. Note::
- * Clients are recommended not to display this list of aliases prominently
- * as they are not curated, unlike those listed in the
- * ``m.room.canonical_alias`` state event.
+ * **Note:**
+ * Clients are recommended not to display this list of aliases prominently
+ * as they are not curated, unlike those listed in the `m.room.canonical_alias`
+ * state event.
*/
class GetLocalAliasesJob : public BaseJob {
public:
diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp
index d2a5f522..877838e2 100644
--- a/lib/csapi/event_context.cpp
+++ b/lib/csapi/event_context.cpp
@@ -4,13 +4,11 @@
#include "event_context.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToGetEventContext(Omittable<int> limit, const QString& filter)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
addParam<IfNotEmpty>(_q, QStringLiteral("filter"), filter);
return _q;
@@ -22,9 +20,8 @@ QUrl GetEventContextJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
const QString& filter)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/context/"
- % eventId,
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/context/", eventId),
queryToGetEventContext(limit, filter));
}
@@ -33,7 +30,7 @@ GetEventContextJob::GetEventContextJob(const QString& roomId,
Omittable<int> limit,
const QString& filter)
: BaseJob(HttpVerb::Get, QStringLiteral("GetEventContextJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/context/" % eventId,
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/context/",
+ eventId),
queryToGetEventContext(limit, filter))
{}
diff --git a/lib/csapi/event_context.h b/lib/csapi/event_context.h
index d82d16ab..4e50edf3 100644
--- a/lib/csapi/event_context.h
+++ b/lib/csapi/event_context.h
@@ -16,8 +16,8 @@ namespace Quotient {
* surrounding an event.
*
* *Note*: This endpoint supports lazy-loading of room member events. See
- * `Lazy-loading room members <#lazy-loading-room-members>`_ for more
- * information.
+ * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members)
+ * for more information.
*/
class GetEventContextJob : public BaseJob {
public:
@@ -33,13 +33,13 @@ public:
* The maximum number of events to return. Default: 10.
*
* \param filter
- * A JSON ``RoomEventFilter`` to filter the returned events with. The
- * filter is only applied to ``events_before``, ``events_after``, and
- * ``state``. It is not applied to the ``event`` itself. The filter may
- * be applied before or/and after the ``limit`` parameter - whichever the
+ * A JSON `RoomEventFilter` to filter the returned events with. The
+ * filter is only applied to `events_before`, `events_after`, and
+ * `state`. It is not applied to the `event` itself. The filter may
+ * be applied before or/and after the `limit` parameter - whichever the
* homeserver prefers.
*
- * See `Filtering <#filtering>`_ for more information.
+ * See [Filtering](/client-server-api/#filtering) for more information.
*/
explicit GetEventContextJob(const QString& roomId, const QString& eventId,
Omittable<int> limit = none,
diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp
index bb3a893f..38c68be7 100644
--- a/lib/csapi/filter.cpp
+++ b/lib/csapi/filter.cpp
@@ -4,16 +4,13 @@
#include "filter.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
DefineFilterJob::DefineFilterJob(const QString& userId, const Filter& filter)
: BaseJob(HttpVerb::Post, QStringLiteral("DefineFilterJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/filter")
+ makePath("/_matrix/client/r0", "/user/", userId, "/filter"))
{
- setRequestData(Data(toJson(filter)));
+ setRequestData(RequestData(toJson(filter)));
addExpectedKey("filter_id");
}
@@ -21,12 +18,12 @@ QUrl GetFilterJob::makeRequestUrl(QUrl baseUrl, const QString& userId,
const QString& filterId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0") % "/user/"
- % userId % "/filter/" % filterId);
+ makePath("/_matrix/client/r0", "/user/",
+ userId, "/filter/", filterId));
}
GetFilterJob::GetFilterJob(const QString& userId, const QString& filterId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetFilterJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/filter/" % filterId)
+ makePath("/_matrix/client/r0", "/user/", userId, "/filter/",
+ filterId))
{}
diff --git a/lib/csapi/filter.h b/lib/csapi/filter.h
index f07b489c..01bec36b 100644
--- a/lib/csapi/filter.h
+++ b/lib/csapi/filter.h
@@ -32,7 +32,7 @@ public:
// Result properties
/// The ID of the filter that was created. Cannot start
- /// with a ``{`` as this character is used to determine
+ /// with a `{` as this character is used to determine
/// if the filter provided is inline JSON or a previously
/// declared filter by homeservers on some APIs.
QString filterId() const { return loadFromJson<QString>("filter_id"_ls); }
diff --git a/lib/csapi/inviting.cpp b/lib/csapi/inviting.cpp
index 01620f9e..39d24611 100644
--- a/lib/csapi/inviting.cpp
+++ b/lib/csapi/inviting.cpp
@@ -4,16 +4,15 @@
#include "inviting.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
-InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId)
+InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId,
+ const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("InviteUserJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/invite")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/invite"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("user_id"), userId);
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
setRequestData(std::move(_data));
}
diff --git a/lib/csapi/inviting.h b/lib/csapi/inviting.h
index 59a61b89..eb13cc95 100644
--- a/lib/csapi/inviting.h
+++ b/lib/csapi/inviting.h
@@ -10,12 +10,11 @@ namespace Quotient {
/*! \brief Invite a user to participate in a particular room.
*
- * .. _invite-by-user-id-endpoint:
- *
* *Note that there are two forms of this API, which are documented separately.
* This version of the API requires that the inviter knows the Matrix
* identifier of the invitee. The other is documented in the*
- * `third party invites section`_.
+ * [third party invites
+ * section](/client-server-api/#post_matrixclientr0roomsroomidinvite-1).
*
* This API invites a user to participate in a particular room.
* They do not start participating in the room until they actually join the
@@ -25,9 +24,7 @@ namespace Quotient {
* join that room.
*
* If the user was invited to the room, the homeserver will append a
- * ``m.room.member`` event to the room.
- *
- * .. _third party invites section: `invite-by-third-party-id-endpoint`_
+ * `m.room.member` event to the room.
*/
class InviteUserJob : public BaseJob {
public:
@@ -38,8 +35,13 @@ public:
*
* \param userId
* The fully qualified user ID of the invitee.
+ *
+ * \param reason
+ * Optional reason to be included as the `reason` on the subsequent
+ * membership event.
*/
- explicit InviteUserJob(const QString& roomId, const QString& userId);
+ explicit InviteUserJob(const QString& roomId, const QString& userId,
+ const QString& reason = {});
};
} // namespace Quotient
diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp
index 4761e949..373c1c6a 100644
--- a/lib/csapi/joining.cpp
+++ b/lib/csapi/joining.cpp
@@ -4,39 +4,41 @@
#include "joining.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
JoinRoomByIdJob::JoinRoomByIdJob(
- const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned)
+ const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned,
+ const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomByIdJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/join")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/join"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"),
thirdPartySigned);
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
setRequestData(std::move(_data));
addExpectedKey("room_id");
}
auto queryToJoinRoom(const QStringList& serverName)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("server_name"), serverName);
return _q;
}
JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias,
const QStringList& serverName,
- const Omittable<ThirdPartySigned>& thirdPartySigned)
+ const Omittable<ThirdPartySigned>& thirdPartySigned,
+ const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/join/" % roomIdOrAlias,
+ makePath("/_matrix/client/r0", "/join/", roomIdOrAlias),
queryToJoinRoom(serverName))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"),
thirdPartySigned);
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
setRequestData(std::move(_data));
addExpectedKey("room_id");
}
diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h
index dd936f92..d0199b11 100644
--- a/lib/csapi/joining.h
+++ b/lib/csapi/joining.h
@@ -13,7 +13,7 @@ namespace Quotient {
/*! \brief Start the requesting user participating in a particular room.
*
* *Note that this API requires a room ID, not alias.*
- * ``/join/{roomIdOrAlias}`` *exists if you have a room alias.*
+ * `/join/{roomIdOrAlias}` *exists if you have a room alias.*
*
* This API starts a user participating in a particular room, if that user
* is allowed to participate in that room. After this call, the client is
@@ -21,7 +21,9 @@ namespace Quotient {
* events associated with the room until the user leaves the room.
*
* After a user has joined a room, the room will appear as an entry in the
- * response of the |/initialSync|_ and |/sync|_ APIs.
+ * response of the
+ * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and
+ * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs.
*/
class JoinRoomByIdJob : public BaseJob {
public:
@@ -32,12 +34,17 @@ public:
*
* \param thirdPartySigned
* If supplied, the homeserver must verify that it matches a pending
- * ``m.room.third_party_invite`` event in the room, and perform
+ * `m.room.third_party_invite` event in the room, and perform
* key validity checking if required by the event.
+ *
+ * \param reason
+ * Optional reason to be included as the `reason` on the subsequent
+ * membership event.
*/
explicit JoinRoomByIdJob(
const QString& roomId,
- const Omittable<ThirdPartySigned>& thirdPartySigned = none);
+ const Omittable<ThirdPartySigned>& thirdPartySigned = none,
+ const QString& reason = {});
// Result properties
@@ -48,7 +55,7 @@ public:
/*! \brief Start the requesting user participating in a particular room.
*
* *Note that this API takes either a room ID or alias, unlike*
- * ``/room/{roomId}/join``.
+ * `/room/{roomId}/join`.
*
* This API starts a user participating in a particular room, if that user
* is allowed to participate in that room. After this call, the client is
@@ -56,7 +63,9 @@ public:
* events associated with the room until the user leaves the room.
*
* After a user has joined a room, the room will appear as an entry in the
- * response of the |/initialSync|_ and |/sync|_ APIs.
+ * response of the
+ * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and
+ * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs.
*/
class JoinRoomJob : public BaseJob {
public:
@@ -70,13 +79,18 @@ public:
* must be participating in the room.
*
* \param thirdPartySigned
- * If a ``third_party_signed`` was supplied, the homeserver must verify
- * that it matches a pending ``m.room.third_party_invite`` event in the
+ * If a `third_party_signed` was supplied, the homeserver must verify
+ * that it matches a pending `m.room.third_party_invite` event in the
* room, and perform key validity checking if required by the event.
+ *
+ * \param reason
+ * Optional reason to be included as the `reason` on the subsequent
+ * membership event.
*/
explicit JoinRoomJob(
const QString& roomIdOrAlias, const QStringList& serverName = {},
- const Omittable<ThirdPartySigned>& thirdPartySigned = none);
+ const Omittable<ThirdPartySigned>& thirdPartySigned = none,
+ const QString& reason = {});
// Result properties
diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp
index 34ab47c9..d6bd2fab 100644
--- a/lib/csapi/keys.cpp
+++ b/lib/csapi/keys.cpp
@@ -4,14 +4,12 @@
#include "keys.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
UploadKeysJob::UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys,
const QHash<QString, QVariant>& oneTimeKeys)
: BaseJob(HttpVerb::Post, QStringLiteral("UploadKeysJob"),
- QStringLiteral("/_matrix/client/r0") % "/keys/upload")
+ makePath("/_matrix/client/r0", "/keys/upload"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("device_keys"), deviceKeys);
@@ -23,7 +21,7 @@ UploadKeysJob::UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys,
QueryKeysJob::QueryKeysJob(const QHash<QString, QStringList>& deviceKeys,
Omittable<int> timeout, const QString& token)
: BaseJob(HttpVerb::Post, QStringLiteral("QueryKeysJob"),
- QStringLiteral("/_matrix/client/r0") % "/keys/query")
+ makePath("/_matrix/client/r0", "/keys/query"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout);
@@ -36,7 +34,7 @@ ClaimKeysJob::ClaimKeysJob(
const QHash<QString, QHash<QString, QString>>& oneTimeKeys,
Omittable<int> timeout)
: BaseJob(HttpVerb::Post, QStringLiteral("ClaimKeysJob"),
- QStringLiteral("/_matrix/client/r0") % "/keys/claim")
+ makePath("/_matrix/client/r0", "/keys/claim"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout);
@@ -47,7 +45,7 @@ ClaimKeysJob::ClaimKeysJob(
auto queryToGetKeysChanges(const QString& from, const QString& to)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("from"), from);
addParam<>(_q, QStringLiteral("to"), to);
return _q;
@@ -57,13 +55,13 @@ QUrl GetKeysChangesJob::makeRequestUrl(QUrl baseUrl, const QString& from,
const QString& to)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/keys/changes",
+ makePath("/_matrix/client/r0",
+ "/keys/changes"),
queryToGetKeysChanges(from, to));
}
GetKeysChangesJob::GetKeysChangesJob(const QString& from, const QString& to)
: BaseJob(HttpVerb::Get, QStringLiteral("GetKeysChangesJob"),
- QStringLiteral("/_matrix/client/r0") % "/keys/changes",
+ makePath("/_matrix/client/r0", "/keys/changes"),
queryToGetKeysChanges(from, to))
{}
diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h
index 8f6c8cc9..7db09e8d 100644
--- a/lib/csapi/keys.h
+++ b/lib/csapi/keys.h
@@ -4,6 +4,7 @@
#pragma once
+#include "csapi/definitions/cross_signing_key.h"
#include "csapi/definitions/device_keys.h"
#include "jobs/basejob.h"
@@ -25,8 +26,8 @@ public:
* \param oneTimeKeys
* One-time public keys for "pre-key" messages. The names of
* the properties should be in the format
- * ``<algorithm>:<key_id>``. The format of the key is determined
- * by the `key algorithm <#key-algorithms>`_.
+ * `<algorithm>:<key_id>`. The format of the key is determined
+ * by the [key algorithm](/client-server-api/#key-algorithms).
*
* May be absent if no new one-time keys are required.
*/
@@ -98,7 +99,7 @@ public:
///
/// If the homeserver could be reached, but the user or device
/// was unknown, no failure is recorded. Instead, the corresponding
- /// user or device is missing from the ``device_keys`` result.
+ /// user or device is missing from the `device_keys` result.
QHash<QString, QJsonObject> failures() const
{
return loadFromJson<QHash<QString, QJsonObject>>("failures"_ls);
@@ -107,13 +108,45 @@ public:
/// Information on the queried devices. A map from user ID, to a
/// map from device ID to device information. For each device,
/// the information returned will be the same as uploaded via
- /// ``/keys/upload``, with the addition of an ``unsigned``
+ /// `/keys/upload`, with the addition of an `unsigned`
/// property.
QHash<QString, QHash<QString, DeviceInformation>> deviceKeys() const
{
return loadFromJson<QHash<QString, QHash<QString, DeviceInformation>>>(
"device_keys"_ls);
}
+
+ /// Information on the master cross-signing keys of the queried users.
+ /// A map from user ID, to master key information. For each key, the
+ /// information returned will be the same as uploaded via
+ /// `/keys/device_signing/upload`, along with the signatures
+ /// uploaded via `/keys/signatures/upload` that the requesting user
+ /// is allowed to see.
+ QHash<QString, CrossSigningKey> masterKeys() const
+ {
+ return loadFromJson<QHash<QString, CrossSigningKey>>("master_keys"_ls);
+ }
+
+ /// Information on the self-signing keys of the queried users. A map
+ /// from user ID, to self-signing key information. For each key, the
+ /// information returned will be the same as uploaded via
+ /// `/keys/device_signing/upload`.
+ QHash<QString, CrossSigningKey> selfSigningKeys() const
+ {
+ return loadFromJson<QHash<QString, CrossSigningKey>>(
+ "self_signing_keys"_ls);
+ }
+
+ /// Information on the user-signing key of the user making the
+ /// request, if they queried their own device information. A map
+ /// from user ID, to user-signing key information. The
+ /// information returned will be the same as uploaded via
+ /// `/keys/device_signing/upload`.
+ QHash<QString, CrossSigningKey> userSigningKeys() const
+ {
+ return loadFromJson<QHash<QString, CrossSigningKey>>(
+ "user_signing_keys"_ls);
+ }
};
template <>
@@ -163,18 +196,17 @@ public:
///
/// If the homeserver could be reached, but the user or device
/// was unknown, no failure is recorded. Instead, the corresponding
- /// user or device is missing from the ``one_time_keys`` result.
+ /// user or device is missing from the `one_time_keys` result.
QHash<QString, QJsonObject> failures() const
{
return loadFromJson<QHash<QString, QJsonObject>>("failures"_ls);
}
/// One-time keys for the queried devices. A map from user ID, to a
- /// map from devices to a map from ``<algorithm>:<key_id>`` to the key
- /// object.
+ /// map from devices to a map from `<algorithm>:<key_id>` to the key object.
///
- /// See the `key algorithms <#key-algorithms>`_ section for information
- /// on the Key Object format.
+ /// See the [key algorithms](/client-server-api/#key-algorithms) section for
+ /// information on the Key Object format.
QHash<QString, QHash<QString, QVariant>> oneTimeKeys() const
{
return loadFromJson<QHash<QString, QHash<QString, QVariant>>>(
@@ -190,26 +222,28 @@ public:
* The server should include in the results any users who:
*
* * currently share a room with the calling user (ie, both users have
- * membership state ``join``); *and*
+ * membership state `join`); *and*
* * added new device identity keys or removed an existing device with
- * identity keys, between ``from`` and ``to``.
+ * identity keys, between `from` and `to`.
*/
class GetKeysChangesJob : public BaseJob {
public:
/*! \brief Query users with recent device key updates.
*
* \param from
- * The desired start point of the list. Should be the ``next_batch`` field
- * from a response to an earlier call to |/sync|. Users who have not
+ * The desired start point of the list. Should be the `next_batch` field
+ * from a response to an earlier call to
+ * [`/sync`](/client-server-api/#get_matrixclientr0sync). Users who have not
* uploaded new device identity keys since this point, nor deleted
* existing devices with identity keys since then, will be excluded
* from the results.
*
* \param to
- * The desired end point of the list. Should be the ``next_batch``
- * field from a recent call to |/sync| - typically the most recent
- * such call. This may be used by the server as a hint to check its
- * caches are up to date.
+ * The desired end point of the list. Should be the `next_batch`
+ * field from a recent call to
+ * [`/sync`](/client-server-api/#get_matrixclientr0sync) - typically the
+ * most recent such call. This may be used by the server as a hint to check
+ * its caches are up to date.
*/
explicit GetKeysChangesJob(const QString& from, const QString& to);
diff --git a/lib/csapi/kicking.cpp b/lib/csapi/kicking.cpp
index 7de5ce01..433e592c 100644
--- a/lib/csapi/kicking.cpp
+++ b/lib/csapi/kicking.cpp
@@ -4,14 +4,12 @@
#include "kicking.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
KickJob::KickJob(const QString& roomId, const QString& userId,
const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("KickJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/kick")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/kick"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("user_id"), userId);
diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h
index 2645a54f..11018368 100644
--- a/lib/csapi/kicking.h
+++ b/lib/csapi/kicking.h
@@ -15,10 +15,10 @@ namespace Quotient {
* The caller must have the required power level in order to perform this
* operation.
*
- * Kicking a user adjusts the target member's membership state to be ``leave``
- * with an optional ``reason``. Like with other membership changes, a user can
+ * Kicking a user adjusts the target member's membership state to be `leave`
+ * with an optional `reason`. Like with other membership changes, a user can
* directly adjust the target member's state by making a request to
- * ``/rooms/<room id>/state/m.room.member/<user id>``.
+ * `/rooms/<room id>/state/m.room.member/<user id>`.
*/
class KickJob : public BaseJob {
public:
@@ -32,7 +32,8 @@ public:
*
* \param reason
* The reason the user has been kicked. This will be supplied as the
- * ``reason`` on the target's updated `m.room.member`_ event.
+ * `reason` on the target's updated
+ * [`m.room.member`](/client-server-api/#mroommember) event.
*/
explicit KickJob(const QString& roomId, const QString& userId,
const QString& reason = {});
diff --git a/lib/csapi/knocking.cpp b/lib/csapi/knocking.cpp
new file mode 100644
index 00000000..73e13e6e
--- /dev/null
+++ b/lib/csapi/knocking.cpp
@@ -0,0 +1,26 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "knocking.h"
+
+using namespace Quotient;
+
+auto queryToKnockRoom(const QStringList& serverName)
+{
+ QUrlQuery _q;
+ addParam<IfNotEmpty>(_q, QStringLiteral("server_name"), serverName);
+ return _q;
+}
+
+KnockRoomJob::KnockRoomJob(const QString& roomIdOrAlias,
+ const QStringList& serverName, const QString& reason)
+ : BaseJob(HttpVerb::Post, QStringLiteral("KnockRoomJob"),
+ makePath("/_matrix/client/r0", "/knock/", roomIdOrAlias),
+ queryToKnockRoom(serverName))
+{
+ QJsonObject _data;
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
+ setRequestData(std::move(_data));
+ addExpectedKey("room_id");
+}
diff --git a/lib/csapi/knocking.h b/lib/csapi/knocking.h
new file mode 100644
index 00000000..1108cb64
--- /dev/null
+++ b/lib/csapi/knocking.h
@@ -0,0 +1,55 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "jobs/basejob.h"
+
+namespace Quotient {
+
+/*! \brief Knock on a room, requesting permission to join.
+ *
+ * *Note that this API takes either a room ID or alias, unlike other membership
+ * APIs.*
+ *
+ * This API "knocks" on the room to ask for permission to join, if the user
+ * is allowed to knock on the room. Acceptance of the knock happens out of
+ * band from this API, meaning that the client will have to watch for updates
+ * regarding the acceptance/rejection of the knock.
+ *
+ * If the room history settings allow, the user will still be able to see
+ * history of the room while being in the "knock" state. The user will have
+ * to accept the invitation to join the room (acceptance of knock) to see
+ * messages reliably. See the `/join` endpoints for more information about
+ * history visibility to the user.
+ *
+ * The knock will appear as an entry in the response of the
+ * [`/sync`](/client-server-api/#get_matrixclientr0sync) API.
+ */
+class KnockRoomJob : public BaseJob {
+public:
+ /*! \brief Knock on a room, requesting permission to join.
+ *
+ * \param roomIdOrAlias
+ * The room identifier or alias to knock upon.
+ *
+ * \param serverName
+ * The servers to attempt to knock on the room through. One of the servers
+ * must be participating in the room.
+ *
+ * \param reason
+ * Optional reason to be included as the `reason` on the subsequent
+ * membership event.
+ */
+ explicit KnockRoomJob(const QString& roomIdOrAlias,
+ const QStringList& serverName = {},
+ const QString& reason = {});
+
+ // Result properties
+
+ /// The knocked room ID.
+ QString roomId() const { return loadFromJson<QString>("room_id"_ls); }
+};
+
+} // namespace Quotient
diff --git a/lib/csapi/leaving.cpp b/lib/csapi/leaving.cpp
index 8bd170bf..0e5386be 100644
--- a/lib/csapi/leaving.cpp
+++ b/lib/csapi/leaving.cpp
@@ -4,32 +4,25 @@
#include "leaving.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
-QUrl LeaveRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
+LeaveRoomJob::LeaveRoomJob(const QString& roomId, const QString& reason)
+ : BaseJob(HttpVerb::Post, QStringLiteral("LeaveRoomJob"),
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/leave"))
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/leave");
+ QJsonObject _data;
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
+ setRequestData(std::move(_data));
}
-LeaveRoomJob::LeaveRoomJob(const QString& roomId)
- : BaseJob(HttpVerb::Post, QStringLiteral("LeaveRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/leave")
-{}
-
QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/forget");
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/forget"));
}
ForgetRoomJob::ForgetRoomJob(const QString& roomId)
: BaseJob(HttpVerb::Post, QStringLiteral("ForgetRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/forget")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/forget"))
{}
diff --git a/lib/csapi/leaving.h b/lib/csapi/leaving.h
index 1bea7e41..2e402d16 100644
--- a/lib/csapi/leaving.h
+++ b/lib/csapi/leaving.h
@@ -28,15 +28,12 @@ public:
*
* \param roomId
* The room identifier to leave.
- */
- explicit LeaveRoomJob(const QString& roomId);
-
- /*! \brief Construct a URL without creating a full-fledged job object
*
- * This function can be used when a URL for LeaveRoomJob
- * is necessary but the job itself isn't.
+ * \param reason
+ * Optional reason to be included as the `reason` on the subsequent
+ * membership event.
*/
- static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId);
+ explicit LeaveRoomJob(const QString& roomId, const QString& reason = {});
};
/*! \brief Stop the requesting user remembering about a particular room.
diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp
index 8d7e267f..22ba04da 100644
--- a/lib/csapi/list_joined_rooms.cpp
+++ b/lib/csapi/list_joined_rooms.cpp
@@ -4,20 +4,17 @@
#include "list_joined_rooms.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetJoinedRoomsJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/joined_rooms");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/joined_rooms"));
}
GetJoinedRoomsJob::GetJoinedRoomsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedRoomsJob"),
- QStringLiteral("/_matrix/client/r0") % "/joined_rooms")
+ makePath("/_matrix/client/r0", "/joined_rooms"))
{
addExpectedKey("joined_rooms");
}
diff --git a/lib/csapi/list_joined_rooms.h b/lib/csapi/list_joined_rooms.h
index 1034aa7b..59a24a49 100644
--- a/lib/csapi/list_joined_rooms.h
+++ b/lib/csapi/list_joined_rooms.h
@@ -26,7 +26,7 @@ public:
// Result properties
- /// The ID of each room in which the user has ``joined`` membership.
+ /// The ID of each room in which the user has `joined` membership.
QStringList joinedRooms() const
{
return loadFromJson<QStringList>("joined_rooms"_ls);
diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp
index 415d816c..25f8da5c 100644
--- a/lib/csapi/list_public_rooms.cpp
+++ b/lib/csapi/list_public_rooms.cpp
@@ -4,31 +4,27 @@
#include "list_public_rooms.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetRoomVisibilityOnDirectoryJob::makeRequestUrl(QUrl baseUrl,
const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/directory/list/room/" % roomId);
+ makePath("/_matrix/client/r0",
+ "/directory/list/room/", roomId));
}
GetRoomVisibilityOnDirectoryJob::GetRoomVisibilityOnDirectoryJob(
const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomVisibilityOnDirectoryJob"),
- QStringLiteral("/_matrix/client/r0") % "/directory/list/room/"
- % roomId,
+ makePath("/_matrix/client/r0", "/directory/list/room/", roomId),
false)
{}
SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob(
const QString& roomId, const QString& visibility)
: BaseJob(HttpVerb::Put, QStringLiteral("SetRoomVisibilityOnDirectoryJob"),
- QStringLiteral("/_matrix/client/r0") % "/directory/list/room/"
- % roomId)
+ makePath("/_matrix/client/r0", "/directory/list/room/", roomId))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility);
@@ -38,7 +34,7 @@ SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob(
auto queryToGetPublicRooms(Omittable<int> limit, const QString& since,
const QString& server)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
addParam<IfNotEmpty>(_q, QStringLiteral("since"), since);
addParam<IfNotEmpty>(_q, QStringLiteral("server"), server);
@@ -50,15 +46,15 @@ QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable<int> limit,
const QString& server)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/publicRooms",
+ makePath("/_matrix/client/r0",
+ "/publicRooms"),
queryToGetPublicRooms(limit, since, server));
}
GetPublicRoomsJob::GetPublicRoomsJob(Omittable<int> limit, const QString& since,
const QString& server)
: BaseJob(HttpVerb::Get, QStringLiteral("GetPublicRoomsJob"),
- QStringLiteral("/_matrix/client/r0") % "/publicRooms",
+ makePath("/_matrix/client/r0", "/publicRooms"),
queryToGetPublicRooms(limit, since, server), {}, false)
{
addExpectedKey("chunk");
@@ -66,7 +62,7 @@ GetPublicRoomsJob::GetPublicRoomsJob(Omittable<int> limit, const QString& since,
auto queryToQueryPublicRooms(const QString& server)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("server"), server);
return _q;
}
@@ -78,7 +74,7 @@ QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server,
Omittable<bool> includeAllNetworks,
const QString& thirdPartyInstanceId)
: BaseJob(HttpVerb::Post, QStringLiteral("QueryPublicRoomsJob"),
- QStringLiteral("/_matrix/client/r0") % "/publicRooms",
+ makePath("/_matrix/client/r0", "/publicRooms"),
queryToQueryPublicRooms(server))
{
QJsonObject _data;
diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h
index b0cb79d2..963c8b56 100644
--- a/lib/csapi/list_public_rooms.h
+++ b/lib/csapi/list_public_rooms.h
@@ -170,7 +170,7 @@ public:
*
* \param thirdPartyInstanceId
* The specific third party network/protocol to request from the
- * homeserver. Can only be used if ``include_all_networks`` is false.
+ * homeserver. Can only be used if `include_all_networks` is false.
*/
explicit QueryPublicRoomsJob(const QString& server = {},
Omittable<int> limit = none,
diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp
index a5bac9ea..71fd93c5 100644
--- a/lib/csapi/login.cpp
+++ b/lib/csapi/login.cpp
@@ -4,20 +4,17 @@
#include "login.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetLoginFlowsJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/login");
+ makePath("/_matrix/client/r0", "/login"));
}
GetLoginFlowsJob::GetLoginFlowsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetLoginFlowsJob"),
- QStringLiteral("/_matrix/client/r0") % "/login", false)
+ makePath("/_matrix/client/r0", "/login"), false)
{}
LoginJob::LoginJob(const QString& type,
@@ -26,7 +23,7 @@ LoginJob::LoginJob(const QString& type,
const QString& deviceId,
const QString& initialDeviceDisplayName)
: BaseJob(HttpVerb::Post, QStringLiteral("LoginJob"),
- QStringLiteral("/_matrix/client/r0") % "/login", false)
+ makePath("/_matrix/client/r0", "/login"), false)
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("type"), type);
diff --git a/lib/csapi/login.h b/lib/csapi/login.h
index a406fc79..b35db1eb 100644
--- a/lib/csapi/login.h
+++ b/lib/csapi/login.h
@@ -14,17 +14,17 @@ namespace Quotient {
/*! \brief Get the supported login types to authenticate users
*
* Gets the homeserver's supported login types to authenticate users. Clients
- * should pick one of these and supply it as the ``type`` when logging in.
+ * should pick one of these and supply it as the `type` when logging in.
*/
class GetLoginFlowsJob : public BaseJob {
public:
// Inner data structures
/// Gets the homeserver's supported login types to authenticate users.
- /// Clients should pick one of these and supply it as the ``type`` when
+ /// Clients should pick one of these and supply it as the `type` when
/// logging in.
struct LoginFlow {
- /// The login type. This is supplied as the ``type`` when
+ /// The login type. This is supplied as the `type` when
/// logging in.
QString type;
};
@@ -64,13 +64,14 @@ struct JsonObjectConverter<GetLoginFlowsJob::LoginFlow> {
* Authenticates the user, and issues an access token they can
* use to authorize themself in subsequent requests.
*
- * If the client does not supply a ``device_id``, the server must
+ * If the client does not supply a `device_id`, the server must
* auto-generate one.
*
- * The returned access token must be associated with the ``device_id``
+ * The returned access token must be associated with the `device_id`
* supplied by the client or generated by the server. The server may
* invalidate any access token previously associated with that device. See
- * `Relationship between access tokens and devices`_.
+ * [Relationship between access tokens and
+ * devices](/client-server-api/#relationship-between-access-tokens-and-devices).
*/
class LoginJob : public BaseJob {
public:
@@ -83,30 +84,33 @@ public:
* Authenticates the user, and issues an access token they can
* use to authorize themself in subsequent requests.
*
- * If the client does not supply a ``device_id``, the server must
+ * If the client does not supply a `device_id`, the server must
* auto-generate one.
*
- * The returned access token must be associated with the ``device_id``
+ * The returned access token must be associated with the `device_id`
* supplied by the client or generated by the server. The server may
* invalidate any access token previously associated with that device. See
- * `Relationship between access tokens and devices`_.
+ * [Relationship between access tokens and
+ * devices](/client-server-api/#relationship-between-access-tokens-and-devices).
*
* \param password
- * Required when ``type`` is ``m.login.password``. The user's
+ * Required when `type` is `m.login.password`. The user's
* password.
*
* \param token
- * Required when ``type`` is ``m.login.token``. Part of `Token-based`_
- * login.
+ * Required when `type` is `m.login.token`. Part of Token-based login.
*
* \param deviceId
* ID of the client device. If this does not correspond to a
- * known client device, a new device will be created. The server
- * will auto-generate a device_id if this is not specified.
+ * known client device, a new device will be created. The given
+ * device ID must not be the same as a
+ * [cross-signing](/client-server-api/#cross-signing) key ID.
+ * The server will auto-generate a device_id
+ * if this is not specified.
*
* \param initialDeviceDisplayName
* A display name to assign to the newly-created device. Ignored
- * if ``device_id`` corresponds to a known device.
+ * if `device_id` corresponds to a known device.
*/
explicit LoginJob(const QString& type,
const Omittable<UserIdentifier>& identifier = none,
@@ -130,8 +134,8 @@ public:
/// been registered.
///
/// **Deprecated**. Clients should extract the server_name from
- /// ``user_id`` (by splitting at the first colon) if they require
- /// it. Note also that ``homeserver`` is not spelt this way.
+ /// `user_id` (by splitting at the first colon) if they require
+ /// it. Note also that `homeserver` is not spelt this way.
QString homeServer() const
{
return loadFromJson<QString>("home_server"_ls);
diff --git a/lib/csapi/logout.cpp b/lib/csapi/logout.cpp
index 9583b8ec..e8083e31 100644
--- a/lib/csapi/logout.cpp
+++ b/lib/csapi/logout.cpp
@@ -4,30 +4,26 @@
#include "logout.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl LogoutJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/logout");
+ makePath("/_matrix/client/r0", "/logout"));
}
LogoutJob::LogoutJob()
: BaseJob(HttpVerb::Post, QStringLiteral("LogoutJob"),
- QStringLiteral("/_matrix/client/r0") % "/logout")
+ makePath("/_matrix/client/r0", "/logout"))
{}
QUrl LogoutAllJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/logout/all");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/logout/all"));
}
LogoutAllJob::LogoutAllJob()
: BaseJob(HttpVerb::Post, QStringLiteral("LogoutAllJob"),
- QStringLiteral("/_matrix/client/r0") % "/logout/all")
+ makePath("/_matrix/client/r0", "/logout/all"))
{}
diff --git a/lib/csapi/logout.h b/lib/csapi/logout.h
index 78f14e40..2e4c2692 100644
--- a/lib/csapi/logout.h
+++ b/lib/csapi/logout.h
@@ -12,7 +12,8 @@ namespace Quotient {
*
* Invalidates an existing access token, so that it can no longer be used for
* authorization. The device associated with the access token is also deleted.
- * `Device keys <#device-keys>`_ for the device are deleted alongside the device.
+ * [Device keys](/client-server-api/#device-keys) for the device are deleted
+ * alongside the device.
*/
class LogoutJob : public BaseJob {
public:
@@ -31,10 +32,12 @@ public:
*
* Invalidates all access tokens for a user, so that they can no longer be used
* for authorization. This includes the access token that made this request. All
- * devices for the user are also deleted. `Device keys <#device-keys>`_ for the
- * device are deleted alongside the device.
+ * devices for the user are also deleted. [Device
+ * keys](/client-server-api/#device-keys) for the device are deleted alongside
+ * the device.
*
- * This endpoint does not use the `User-Interactive Authentication API`_ because
+ * This endpoint does not use the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api) because
* User-Interactive Authentication is designed to protect against attacks where
* the someone gets hold of a single access token then takes over the account.
* This endpoint invalidates all access tokens for the user, including the token
diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp
index 855c051f..1a93b75b 100644
--- a/lib/csapi/message_pagination.cpp
+++ b/lib/csapi/message_pagination.cpp
@@ -4,15 +4,13 @@
#include "message_pagination.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToGetRoomEvents(const QString& from, const QString& to,
const QString& dir, Omittable<int> limit,
const QString& filter)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("from"), from);
addParam<IfNotEmpty>(_q, QStringLiteral("to"), to);
addParam<>(_q, QStringLiteral("dir"), dir);
@@ -28,7 +26,7 @@ QUrl GetRoomEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
{
return BaseJob::makeRequestUrl(
std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/messages",
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/messages"),
queryToGetRoomEvents(from, to, dir, limit, filter));
}
@@ -36,7 +34,6 @@ GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& from,
const QString& dir, const QString& to,
Omittable<int> limit, const QString& filter)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomEventsJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/messages",
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/messages"),
queryToGetRoomEvents(from, to, dir, limit, filter))
{}
diff --git a/lib/csapi/message_pagination.h b/lib/csapi/message_pagination.h
index 286d4237..363e4d99 100644
--- a/lib/csapi/message_pagination.h
+++ b/lib/csapi/message_pagination.h
@@ -15,8 +15,8 @@ namespace Quotient {
* pagination query parameters to paginate history in the room.
*
* *Note*: This endpoint supports lazy-loading of room member events. See
- * `Lazy-loading room members <#lazy-loading-room-members>`_ for more
- * information.
+ * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members)
+ * for more information.
*/
class GetRoomEventsJob : public BaseJob {
public:
@@ -27,8 +27,8 @@ public:
*
* \param from
* The token to start returning events from. This token can be obtained
- * from a ``prev_batch`` token returned for each room by the sync API,
- * or from a ``start`` or ``end`` token returned by a previous request
+ * from a `prev_batch` token returned for each room by the sync API,
+ * or from a `start` or `end` token returned by a previous request
* to this endpoint.
*
* \param dir
@@ -36,8 +36,8 @@ public:
*
* \param to
* The token to stop returning events at. This token can be obtained from
- * a ``prev_batch`` token returned for each room by the sync endpoint,
- * or from a ``start`` or ``end`` token returned by a previous request to
+ * a `prev_batch` token returned for each room by the sync endpoint,
+ * or from a `start` or `end` token returned by a previous request to
* this endpoint.
*
* \param limit
@@ -64,25 +64,25 @@ public:
// Result properties
- /// The token the pagination starts from. If ``dir=b`` this will be
- /// the token supplied in ``from``.
+ /// The token the pagination starts from. If `dir=b` this will be
+ /// the token supplied in `from`.
QString begin() const { return loadFromJson<QString>("start"_ls); }
- /// The token the pagination ends at. If ``dir=b`` this token should
+ /// The token the pagination ends at. If `dir=b` this token should
/// be used again to request even earlier events.
QString end() const { return loadFromJson<QString>("end"_ls); }
- /// A list of room events. The order depends on the ``dir`` parameter.
- /// For ``dir=b`` events will be in reverse-chronological order,
- /// for ``dir=f`` in chronological order, so that events start
- /// at the ``from`` point.
+ /// A list of room events. The order depends on the `dir` parameter.
+ /// For `dir=b` events will be in reverse-chronological order,
+ /// for `dir=f` in chronological order, so that events start
+ /// at the `from` point.
RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); }
- /// A list of state events relevant to showing the ``chunk``. For example, if
- /// ``lazy_load_members`` is enabled in the filter then this may contain
- /// the membership events for the senders of events in the ``chunk``.
+ /// A list of state events relevant to showing the `chunk`. For example, if
+ /// `lazy_load_members` is enabled in the filter then this may contain
+ /// the membership events for the senders of events in the `chunk`.
///
- /// Unless ``include_redundant_members`` is ``true``, the server
+ /// Unless `include_redundant_members` is `true`, the server
/// may remove membership events which would have already been
/// sent to the client in prior calls to this endpoint, assuming
/// the membership of those members has not changed.
diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp
index a479d500..1e523c6f 100644
--- a/lib/csapi/notifications.cpp
+++ b/lib/csapi/notifications.cpp
@@ -4,14 +4,12 @@
#include "notifications.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToGetNotifications(const QString& from, Omittable<int> limit,
const QString& only)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
addParam<IfNotEmpty>(_q, QStringLiteral("only"), only);
@@ -23,8 +21,8 @@ QUrl GetNotificationsJob::makeRequestUrl(QUrl baseUrl, const QString& from,
const QString& only)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/notifications",
+ makePath("/_matrix/client/r0",
+ "/notifications"),
queryToGetNotifications(from, limit, only));
}
@@ -32,7 +30,7 @@ GetNotificationsJob::GetNotificationsJob(const QString& from,
Omittable<int> limit,
const QString& only)
: BaseJob(HttpVerb::Get, QStringLiteral("GetNotificationsJob"),
- QStringLiteral("/_matrix/client/r0") % "/notifications",
+ makePath("/_matrix/client/r0", "/notifications"),
queryToGetNotifications(from, limit, only))
{
addExpectedKey("notifications");
diff --git a/lib/csapi/notifications.h b/lib/csapi/notifications.h
index ff499c7a..0c38fe6b 100644
--- a/lib/csapi/notifications.h
+++ b/lib/csapi/notifications.h
@@ -22,7 +22,7 @@ public:
/// user has been, or would have been notified about.
struct Notification {
/// The action(s) to perform when the conditions for this rule are met.
- /// See `Push Rules: API`_.
+ /// See [Push Rules: API](/client-server-api/#push-rules-api).
QVector<QVariant> actions;
/// The Event object for the event that triggered the notification.
EventPtr event;
@@ -35,7 +35,7 @@ public:
QString roomId;
/// The unix timestamp at which the event notification was sent,
/// in milliseconds.
- int ts;
+ qint64 ts;
};
// Construction/destruction
@@ -49,7 +49,7 @@ public:
* Limit on the number of events to return in this request.
*
* \param only
- * Allows basic filtering of events returned. Supply ``highlight``
+ * Allows basic filtering of events returned. Supply `highlight`
* to return only events where the notification had the highlight
* tweak set.
*/
@@ -68,8 +68,8 @@ public:
// Result properties
- /// The token to supply in the ``from`` param of the next
- /// ``/notifications`` request in order to request more
+ /// The token to supply in the `from` param of the next
+ /// `/notifications` request in order to request more
/// events. If this is absent, there are no more results.
QString nextToken() const { return loadFromJson<QString>("next_token"_ls); }
diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp
index 3941e9c0..5c93a2d7 100644
--- a/lib/csapi/openid.cpp
+++ b/lib/csapi/openid.cpp
@@ -4,15 +4,13 @@
#include "openid.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId,
const QJsonObject& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestOpenIdTokenJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/openid/request_token")
+ makePath("/_matrix/client/r0", "/user/", userId,
+ "/openid/request_token"))
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h
index efb5f623..0be39c8c 100644
--- a/lib/csapi/openid.h
+++ b/lib/csapi/openid.h
@@ -18,7 +18,7 @@ namespace Quotient {
* OpenID.
*
* The access token generated is only valid for the OpenID API. It cannot
- * be used to request another OpenID access token or call ``/sync``, for
+ * be used to request another OpenID access token or call `/sync`, for
* example.
*/
class RequestOpenIdTokenJob : public BaseJob {
@@ -38,9 +38,10 @@ public:
// Result properties
/// OpenID token information. This response is nearly compatible with the
- /// response documented in the `OpenID Connect 1.0 Specification
- /// <http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse>`_
- /// with the only difference being the lack of an ``id_token``. Instead,
+ /// response documented in the
+ /// [OpenID Connect 1.0
+ /// Specification](http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse)
+ /// with the only difference being the lack of an `id_token`. Instead,
/// the Matrix homeserver's name is provided.
OpenidToken tokenData() const { return fromJson<OpenidToken>(jsonData()); }
};
diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp
index 70a5b6f3..eb5d22fa 100644
--- a/lib/csapi/peeking_events.cpp
+++ b/lib/csapi/peeking_events.cpp
@@ -4,14 +4,12 @@
#include "peeking_events.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToPeekEvents(const QString& from, Omittable<int> timeout,
const QString& roomId)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
addParam<IfNotEmpty>(_q, QStringLiteral("timeout"), timeout);
addParam<IfNotEmpty>(_q, QStringLiteral("room_id"), roomId);
@@ -22,14 +20,13 @@ QUrl PeekEventsJob::makeRequestUrl(QUrl baseUrl, const QString& from,
Omittable<int> timeout, const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/events",
+ makePath("/_matrix/client/r0", "/events"),
queryToPeekEvents(from, timeout, roomId));
}
PeekEventsJob::PeekEventsJob(const QString& from, Omittable<int> timeout,
const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("PeekEventsJob"),
- QStringLiteral("/_matrix/client/r0") % "/events",
+ makePath("/_matrix/client/r0", "/events"),
queryToPeekEvents(from, timeout, roomId))
{}
diff --git a/lib/csapi/peeking_events.h b/lib/csapi/peeking_events.h
index cecd9f2d..885ff340 100644
--- a/lib/csapi/peeking_events.h
+++ b/lib/csapi/peeking_events.h
@@ -13,12 +13,12 @@ namespace Quotient {
*
* This will listen for new events related to a particular room and return
* them to the caller. This will block until an event is received, or until
- * the ``timeout`` is reached.
+ * the `timeout` is reached.
*
- * This API is the same as the normal ``/events`` endpoint, but can be
+ * This API is the same as the normal `/events` endpoint, but can be
* called by users who have not joined the room.
*
- * Note that the normal ``/events`` endpoint has been deprecated. This
+ * Note that the normal `/events` endpoint has been deprecated. This
* API will also be deprecated at some point, but its replacement is not
* yet known.
*/
@@ -51,12 +51,12 @@ public:
// Result properties
- /// A token which correlates to the first value in ``chunk``. This
- /// is usually the same token supplied to ``from=``.
+ /// A token which correlates to the first value in `chunk`. This
+ /// is usually the same token supplied to `from=`.
QString begin() const { return loadFromJson<QString>("start"_ls); }
- /// A token which correlates to the last value in ``chunk``. This
- /// token should be used in the next request to ``/events``.
+ /// A token which correlates to the last value in `chunk`. This
+ /// token should be used in the next request to `/events`.
QString end() const { return loadFromJson<QString>("end"_ls); }
/// An array of events.
diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp
index 58d0d157..4f77c466 100644
--- a/lib/csapi/presence.cpp
+++ b/lib/csapi/presence.cpp
@@ -4,15 +4,12 @@
#include "presence.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence,
const QString& statusMsg)
: BaseJob(HttpVerb::Put, QStringLiteral("SetPresenceJob"),
- QStringLiteral("/_matrix/client/r0") % "/presence/" % userId
- % "/status")
+ makePath("/_matrix/client/r0", "/presence/", userId, "/status"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("presence"), presence);
@@ -23,14 +20,13 @@ SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence,
QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/presence/" % userId % "/status");
+ makePath("/_matrix/client/r0", "/presence/",
+ userId, "/status"));
}
GetPresenceJob::GetPresenceJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetPresenceJob"),
- QStringLiteral("/_matrix/client/r0") % "/presence/" % userId
- % "/status")
+ makePath("/_matrix/client/r0", "/presence/", userId, "/status"))
{
addExpectedKey("presence");
}
diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h
index a885bf4f..4ab50e25 100644
--- a/lib/csapi/presence.h
+++ b/lib/csapi/presence.h
@@ -12,7 +12,7 @@ namespace Quotient {
*
* This API sets the given user's presence state. When setting the status,
* the activity time is updated to reflect that activity; the client does
- * not need to specify the ``last_active_ago`` field. You cannot set the
+ * not need to specify the `last_active_ago` field. You cannot set the
* presence state of another user.
*/
class SetPresenceJob : public BaseJob {
diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp
index 8436b8e6..64ac84ca 100644
--- a/lib/csapi/profile.cpp
+++ b/lib/csapi/profile.cpp
@@ -4,15 +4,12 @@
#include "profile.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetDisplayNameJob::SetDisplayNameJob(const QString& userId,
const QString& displayname)
: BaseJob(HttpVerb::Put, QStringLiteral("SetDisplayNameJob"),
- QStringLiteral("/_matrix/client/r0") % "/profile/" % userId
- % "/displayname")
+ makePath("/_matrix/client/r0", "/profile/", userId, "/displayname"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("displayname"), displayname);
@@ -22,21 +19,19 @@ SetDisplayNameJob::SetDisplayNameJob(const QString& userId,
QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/profile/" % userId % "/displayname");
+ makePath("/_matrix/client/r0", "/profile/",
+ userId, "/displayname"));
}
GetDisplayNameJob::GetDisplayNameJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetDisplayNameJob"),
- QStringLiteral("/_matrix/client/r0") % "/profile/" % userId
- % "/displayname",
+ makePath("/_matrix/client/r0", "/profile/", userId, "/displayname"),
false)
{}
-SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl)
+SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl)
: BaseJob(HttpVerb::Put, QStringLiteral("SetAvatarUrlJob"),
- QStringLiteral("/_matrix/client/r0") % "/profile/" % userId
- % "/avatar_url")
+ makePath("/_matrix/client/r0", "/profile/", userId, "/avatar_url"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("avatar_url"), avatarUrl);
@@ -46,25 +41,24 @@ SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl
QUrl GetAvatarUrlJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/profile/" % userId % "/avatar_url");
+ makePath("/_matrix/client/r0", "/profile/",
+ userId, "/avatar_url"));
}
GetAvatarUrlJob::GetAvatarUrlJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetAvatarUrlJob"),
- QStringLiteral("/_matrix/client/r0") % "/profile/" % userId
- % "/avatar_url",
+ makePath("/_matrix/client/r0", "/profile/", userId, "/avatar_url"),
false)
{}
QUrl GetUserProfileJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/profile/" % userId);
+ makePath("/_matrix/client/r0", "/profile/",
+ userId));
}
GetUserProfileJob::GetUserProfileJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetUserProfileJob"),
- QStringLiteral("/_matrix/client/r0") % "/profile/" % userId, false)
+ makePath("/_matrix/client/r0", "/profile/", userId), false)
{}
diff --git a/lib/csapi/profile.h b/lib/csapi/profile.h
index 3858fab2..7f9c9e95 100644
--- a/lib/csapi/profile.h
+++ b/lib/csapi/profile.h
@@ -11,7 +11,7 @@ namespace Quotient {
/*! \brief Set the user's display name.
*
* This API sets the given user's display name. You must have permission to
- * set this user's display name, e.g. you need to have their ``access_token``.
+ * set this user's display name, e.g. you need to have their `access_token`.
*/
class SetDisplayNameJob : public BaseJob {
public:
@@ -61,7 +61,7 @@ public:
/*! \brief Set the user's avatar URL.
*
* This API sets the given user's avatar URL. You must have permission to
- * set this user's avatar URL, e.g. you need to have their ``access_token``.
+ * set this user's avatar URL, e.g. you need to have their `access_token`.
*/
class SetAvatarUrlJob : public BaseJob {
public:
@@ -73,7 +73,7 @@ public:
* \param avatarUrl
* The new avatar URL for this user.
*/
- explicit SetAvatarUrlJob(const QString& userId, const QString& avatarUrl);
+ explicit SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl);
};
/*! \brief Get the user's avatar URL.
@@ -101,7 +101,7 @@ public:
// Result properties
/// The user's avatar URL if they have set one, otherwise not present.
- QString avatarUrl() const { return loadFromJson<QString>("avatar_url"_ls); }
+ QUrl avatarUrl() const { return loadFromJson<QUrl>("avatar_url"_ls); }
};
/*! \brief Get this user's profile information.
@@ -109,7 +109,7 @@ public:
* Get the combined profile information for this user. This API may be used
* to fetch the user's own profile information or other users; either
* locally or on remote homeservers. This API may return keys which are not
- * limited to ``displayname`` or ``avatar_url``.
+ * limited to `displayname` or `avatar_url`.
*/
class GetUserProfileJob : public BaseJob {
public:
@@ -130,7 +130,7 @@ public:
// Result properties
/// The user's avatar URL if they have set one, otherwise not present.
- QString avatarUrl() const { return loadFromJson<QString>("avatar_url"_ls); }
+ QUrl avatarUrl() const { return loadFromJson<QUrl>("avatar_url"_ls); }
/// The user's display name if they have set one, otherwise not present.
QString displayname() const
diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp
index 028022c5..ef4b3767 100644
--- a/lib/csapi/pusher.cpp
+++ b/lib/csapi/pusher.cpp
@@ -4,20 +4,17 @@
#include "pusher.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetPushersJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/pushers");
+ makePath("/_matrix/client/r0", "/pushers"));
}
GetPushersJob::GetPushersJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushersJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushers")
+ makePath("/_matrix/client/r0", "/pushers"))
{}
PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind,
@@ -26,7 +23,7 @@ PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind,
const QString& lang, const PusherData& data,
const QString& profileTag, Omittable<bool> append)
: BaseJob(HttpVerb::Post, QStringLiteral("PostPusherJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushers/set")
+ makePath("/_matrix/client/r0", "/pushers/set"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("pushkey"), pushkey);
diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h
index ae0050d2..622b0df6 100644
--- a/lib/csapi/pusher.h
+++ b/lib/csapi/pusher.h
@@ -19,9 +19,9 @@ public:
/// A dictionary of information for the pusher implementation
/// itself.
struct PusherData {
- /// Required if ``kind`` is ``http``. The URL to use to send
+ /// Required if `kind` is `http`. The URL to use to send
/// notifications to.
- QString url;
+ QUrl url;
/// The format to use when sending notifications to the Push
/// Gateway.
QString format;
@@ -29,11 +29,11 @@ public:
/// Gets all currently active pushers for the authenticated user.
struct Pusher {
- /// This is a unique identifier for this pusher. See ``/set`` for
+ /// This is a unique identifier for this pusher. See `/set` for
/// more detail.
/// Max length, 512 bytes.
QString pushkey;
- /// The kind of pusher. ``"http"`` is a pusher that
+ /// The kind of pusher. `"http"` is a pusher that
/// sends HTTP pokes.
QString kind;
/// This is a reverse-DNS style identifier for the application.
@@ -104,27 +104,27 @@ struct JsonObjectConverter<GetPushersJob::Pusher> {
/*! \brief Modify a pusher for this user on the homeserver.
*
- * This endpoint allows the creation, modification and deletion of `pushers`_
- * for this user ID. The behaviour of this endpoint varies depending on the
- * values in the JSON body.
+ * This endpoint allows the creation, modification and deletion of
+ * [pushers](/client-server-api/#push-notifications) for this user ID. The
+ * behaviour of this endpoint varies depending on the values in the JSON body.
*/
class PostPusherJob : public BaseJob {
public:
// Inner data structures
/// A dictionary of information for the pusher implementation
- /// itself. If ``kind`` is ``http``, this should contain ``url``
+ /// itself. If `kind` is `http`, this should contain `url`
/// which is the URL to use to send notifications to.
struct PusherData {
- /// Required if ``kind`` is ``http``. The URL to use to send
+ /// Required if `kind` is `http`. The URL to use to send
/// notifications to. MUST be an HTTPS URL with a path of
- /// ``/_matrix/push/v1/notify``.
- QString url;
+ /// `/_matrix/push/v1/notify`.
+ QUrl url;
/// The format to send notifications in to Push Gateways if the
- /// ``kind`` is ``http``. The details about what fields the
+ /// `kind` is `http`. The details about what fields the
/// homeserver should send to the push gateway are defined in the
- /// `Push Gateway Specification`_. Currently the only format
- /// available is 'event_id_only'.
+ /// [Push Gateway Specification](/push-gateway-api/). Currently the only
+ /// format available is 'event_id_only'.
QString format;
};
@@ -140,13 +140,13 @@ public:
* client has no such concept, use any unique identifier.
* Max length, 512 bytes.
*
- * If the ``kind`` is ``"email"``, this is the email address to
+ * If the `kind` is `"email"`, this is the email address to
* send notifications to.
*
* \param kind
- * The kind of pusher to configure. ``"http"`` makes a pusher that
- * sends HTTP pokes. ``"email"`` makes a pusher that emails the
- * user with unread notifications. ``null`` deletes the pusher.
+ * The kind of pusher to configure. `"http"` makes a pusher that
+ * sends HTTP pokes. `"email"` makes a pusher that emails the
+ * user with unread notifications. `null` deletes the pusher.
*
* \param appId
* This is a reverse-DNS style identifier for the application.
@@ -154,7 +154,7 @@ public:
* different platform versions get different app identifiers.
* Max length, 64 chars.
*
- * If the ``kind`` is ``"email"``, this is ``"m.email"``.
+ * If the `kind` is `"email"`, this is `"m.email"`.
*
* \param appDisplayName
* A string that will allow the user to identify what application
@@ -170,7 +170,7 @@ public:
*
* \param data
* A dictionary of information for the pusher implementation
- * itself. If ``kind`` is ``http``, this should contain ``url``
+ * itself. If `kind` is `http`, this should contain `url`
* which is the URL to use to send notifications to.
*
* \param profileTag
@@ -182,7 +182,7 @@ public:
* given pushkey and App ID in addition to any others with
* different user IDs. Otherwise, the homeserver must remove any
* other pushers with the same App ID and pushkey for different
- * users. The default is ``false``.
+ * users. The default is `false`.
*/
explicit PostPusherJob(const QString& pushkey, const QString& kind,
const QString& appId, const QString& appDisplayName,
diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp
index 86165744..0d840788 100644
--- a/lib/csapi/pushrules.cpp
+++ b/lib/csapi/pushrules.cpp
@@ -4,20 +4,17 @@
#include "pushrules.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetPushRulesJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/pushrules");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/pushrules"));
}
GetPushRulesJob::GetPushRulesJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushRulesJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules")
+ makePath("/_matrix/client/r0", "/pushrules"))
{
addExpectedKey("global");
}
@@ -26,16 +23,15 @@ QUrl GetPushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
const QString& kind, const QString& ruleId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/pushrules/" % scope % "/" % kind
- % "/" % ruleId);
+ makePath("/_matrix/client/r0", "/pushrules/",
+ scope, "/", kind, "/", ruleId));
}
GetPushRuleJob::GetPushRuleJob(const QString& scope, const QString& kind,
const QString& ruleId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId)
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId))
{}
QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
@@ -43,21 +39,20 @@ QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
const QString& ruleId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/pushrules/" % scope % "/" % kind
- % "/" % ruleId);
+ makePath("/_matrix/client/r0", "/pushrules/",
+ scope, "/", kind, "/", ruleId));
}
DeletePushRuleJob::DeletePushRuleJob(const QString& scope, const QString& kind,
const QString& ruleId)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeletePushRuleJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId)
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId))
{}
auto queryToSetPushRule(const QString& before, const QString& after)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("before"), before);
addParam<IfNotEmpty>(_q, QStringLiteral("after"), after);
return _q;
@@ -70,8 +65,8 @@ SetPushRuleJob::SetPushRuleJob(const QString& scope, const QString& kind,
const QVector<PushCondition>& conditions,
const QString& pattern)
: BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId,
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId),
queryToSetPushRule(before, after))
{
QJsonObject _data;
@@ -86,17 +81,17 @@ QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
const QString& ruleId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/pushrules/" % scope % "/" % kind
- % "/" % ruleId % "/enabled");
+ makePath("/_matrix/client/r0", "/pushrules/",
+ scope, "/", kind, "/", ruleId,
+ "/enabled"));
}
IsPushRuleEnabledJob::IsPushRuleEnabledJob(const QString& scope,
const QString& kind,
const QString& ruleId)
: BaseJob(HttpVerb::Get, QStringLiteral("IsPushRuleEnabledJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId % "/enabled")
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId, "/enabled"))
{
addExpectedKey("enabled");
}
@@ -105,8 +100,8 @@ SetPushRuleEnabledJob::SetPushRuleEnabledJob(const QString& scope,
const QString& kind,
const QString& ruleId, bool enabled)
: BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleEnabledJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId % "/enabled")
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId, "/enabled"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("enabled"), enabled);
@@ -118,17 +113,17 @@ QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
const QString& ruleId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/pushrules/" % scope % "/" % kind
- % "/" % ruleId % "/actions");
+ makePath("/_matrix/client/r0", "/pushrules/",
+ scope, "/", kind, "/", ruleId,
+ "/actions"));
}
GetPushRuleActionsJob::GetPushRuleActionsJob(const QString& scope,
const QString& kind,
const QString& ruleId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleActionsJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId % "/actions")
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId, "/actions"))
{
addExpectedKey("actions");
}
@@ -138,8 +133,8 @@ SetPushRuleActionsJob::SetPushRuleActionsJob(const QString& scope,
const QString& ruleId,
const QVector<QVariant>& actions)
: BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleActionsJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules/" % scope % "/"
- % kind % "/" % ruleId % "/actions")
+ makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ "/", ruleId, "/actions"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("actions"), actions);
diff --git a/lib/csapi/pushrules.h b/lib/csapi/pushrules.h
index 1c6d5c2d..a5eb48f0 100644
--- a/lib/csapi/pushrules.h
+++ b/lib/csapi/pushrules.h
@@ -15,9 +15,9 @@ namespace Quotient {
/*! \brief Retrieve all push rulesets.
*
* Retrieve all push rulesets for this user. Clients can "drill-down" on
- * the rulesets by suffixing a ``scope`` to this path e.g.
- * ``/pushrules/global/``. This will return a subset of this data under the
- * specified key e.g. the ``global`` key.
+ * the rulesets by suffixing a `scope` to this path e.g.
+ * `/pushrules/global/`. This will return a subset of this data under the
+ * specified key e.g. the `global` key.
*/
class GetPushRulesJob : public BaseJob {
public:
@@ -49,7 +49,7 @@ public:
/*! \brief Retrieve a push rule.
*
* \param scope
- * ``global`` to specify global rules.
+ * `global` to specify global rules.
*
* \param kind
* The kind of rule
@@ -71,7 +71,7 @@ public:
// Result properties
/// The specific push rule. This will also include keys specific to the
- /// rule itself such as the rule's ``actions`` and ``conditions`` if set.
+ /// rule itself such as the rule's `actions` and `conditions` if set.
PushRule pushRule() const { return fromJson<PushRule>(jsonData()); }
};
@@ -84,7 +84,7 @@ public:
/*! \brief Delete a push rule.
*
* \param scope
- * ``global`` to specify global rules.
+ * `global` to specify global rules.
*
* \param kind
* The kind of rule
@@ -117,7 +117,7 @@ public:
/*! \brief Add or change a push rule.
*
* \param scope
- * ``global`` to specify global rules.
+ * `global` to specify global rules.
*
* \param kind
* The kind of rule
@@ -129,7 +129,7 @@ public:
* The action(s) to perform when the conditions for this rule are met.
*
* \param before
- * Use 'before' with a ``rule_id`` as its value to make the new rule the
+ * Use 'before' with a `rule_id` as its value to make the new rule the
* next-most important rule with respect to the given user defined rule.
* It is not possible to add a rule relative to a predefined server rule.
*
@@ -141,10 +141,10 @@ public:
* \param conditions
* The conditions that must hold true for an event in order for a
* rule to be applied to an event. A rule with no conditions
- * always matches. Only applicable to ``underride`` and ``override`` rules.
+ * always matches. Only applicable to `underride` and `override` rules.
*
* \param pattern
- * Only applicable to ``content`` rules. The glob-style pattern to match
+ * Only applicable to `content` rules. The glob-style pattern to match
* against.
*/
explicit SetPushRuleJob(const QString& scope, const QString& kind,
@@ -165,8 +165,8 @@ public:
/*! \brief Get whether a push rule is enabled
*
* \param scope
- * Either ``global`` or ``device/<profile_tag>`` to specify global
- * rules or device rules for the given ``profile_tag``.
+ * Either `global` or `device/<profile_tag>` to specify global
+ * rules or device rules for the given `profile_tag`.
*
* \param kind
* The kind of rule
@@ -200,7 +200,7 @@ public:
/*! \brief Enable or disable a push rule.
*
* \param scope
- * ``global`` to specify global rules.
+ * `global` to specify global rules.
*
* \param kind
* The kind of rule
@@ -224,8 +224,8 @@ public:
/*! \brief The actions for a push rule
*
* \param scope
- * Either ``global`` or ``device/<profile_tag>`` to specify global
- * rules or device rules for the given ``profile_tag``.
+ * Either `global` or `device/<profile_tag>` to specify global
+ * rules or device rules for the given `profile_tag`.
*
* \param kind
* The kind of rule
@@ -263,7 +263,7 @@ public:
/*! \brief Set the actions for a push rule.
*
* \param scope
- * ``global`` to specify global rules.
+ * `global` to specify global rules.
*
* \param kind
* The kind of rule
diff --git a/lib/csapi/read_markers.cpp b/lib/csapi/read_markers.cpp
index 39e4d148..f2edb71e 100644
--- a/lib/csapi/read_markers.cpp
+++ b/lib/csapi/read_markers.cpp
@@ -4,16 +4,13 @@
#include "read_markers.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetReadMarkerJob::SetReadMarkerJob(const QString& roomId,
const QString& mFullyRead,
const QString& mRead)
: BaseJob(HttpVerb::Post, QStringLiteral("SetReadMarkerJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/read_markers")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/read_markers"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("m.fully_read"), mFullyRead);
diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h
index 0e122c63..00a2aa0d 100644
--- a/lib/csapi/read_markers.h
+++ b/lib/csapi/read_markers.h
@@ -26,7 +26,7 @@ public:
*
* \param mRead
* The event ID to set the read receipt location at. This is
- * equivalent to calling ``/receipt/m.read/$elsewhere:example.org``
+ * equivalent to calling `/receipt/m.read/$elsewhere:example.org`
* and is provided here to save that extra call.
*/
explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead,
diff --git a/lib/csapi/receipts.cpp b/lib/csapi/receipts.cpp
index 00d1c28a..401c3bfe 100644
--- a/lib/csapi/receipts.cpp
+++ b/lib/csapi/receipts.cpp
@@ -4,16 +4,14 @@
#include "receipts.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType,
const QString& eventId,
const QJsonObject& receipt)
: BaseJob(HttpVerb::Post, QStringLiteral("PostReceiptJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/receipt/" % receiptType % "/" % eventId)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/receipt/",
+ receiptType, "/", eventId))
{
- setRequestData(Data(toJson(receipt)));
+ setRequestData(RequestData(toJson(receipt)));
}
diff --git a/lib/csapi/receipts.h b/lib/csapi/receipts.h
index 1fac0acf..7ac093cd 100644
--- a/lib/csapi/receipts.h
+++ b/lib/csapi/receipts.h
@@ -27,8 +27,8 @@ public:
* The event ID to acknowledge up to.
*
* \param receipt
- * Extra receipt information to attach to ``content`` if any. The
- * server will automatically set the ``ts`` field.
+ * Extra receipt information to attach to `content` if any. The
+ * server will automatically set the `ts` field.
*/
explicit PostReceiptJob(const QString& roomId, const QString& receiptType,
const QString& eventId,
diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp
index 91497064..acf1b0e4 100644
--- a/lib/csapi/redaction.cpp
+++ b/lib/csapi/redaction.cpp
@@ -4,15 +4,13 @@
#include "redaction.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId,
const QString& txnId, const QString& reason)
: BaseJob(HttpVerb::Put, QStringLiteral("RedactEventJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/redact/" % eventId % "/" % txnId)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/redact/",
+ eventId, "/", txnId))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
diff --git a/lib/csapi/redaction.h b/lib/csapi/redaction.h
index 541e433a..f0db9f9f 100644
--- a/lib/csapi/redaction.h
+++ b/lib/csapi/redaction.h
@@ -15,9 +15,12 @@ namespace Quotient {
*
* This cannot be undone.
*
- * Users may redact their own events, and any user with a power level
- * greater than or equal to the ``redact`` power level of the room may
- * redact events there.
+ * Any user with a power level greater than or equal to the `m.room.redaction`
+ * event power level may send redaction events in the room. If the user's power
+ * level greater is also greater than or equal to the `redact` power level
+ * of the room, the user may redact events sent by other users.
+ *
+ * Server administrators may redact events sent by users on their server.
*/
class RedactEventJob : public BaseJob {
public:
diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp
index 33f61265..153abcee 100644
--- a/lib/csapi/registration.cpp
+++ b/lib/csapi/registration.cpp
@@ -4,13 +4,11 @@
#include "registration.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToRegister(const QString& kind)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("kind"), kind);
return _q;
}
@@ -22,7 +20,7 @@ RegisterJob::RegisterJob(const QString& kind,
const QString& initialDeviceDisplayName,
Omittable<bool> inhibitLogin)
: BaseJob(HttpVerb::Post, QStringLiteral("RegisterJob"),
- QStringLiteral("/_matrix/client/r0") % "/register",
+ makePath("/_matrix/client/r0", "/register"),
queryToRegister(kind), {}, false)
{
QJsonObject _data;
@@ -40,28 +38,26 @@ RegisterJob::RegisterJob(const QString& kind,
RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob(
const EmailValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterEmailJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/register/email/requestToken",
+ makePath("/_matrix/client/r0", "/register/email/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob(
const MsisdnValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterMSISDNJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/register/msisdn/requestToken",
+ makePath("/_matrix/client/r0", "/register/msisdn/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
ChangePasswordJob::ChangePasswordJob(const QString& newPassword,
bool logoutDevices,
const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/password")
+ makePath("/_matrix/client/r0", "/account/password"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("new_password"), newPassword);
@@ -74,28 +70,28 @@ RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob(
const EmailValidationData& body)
: BaseJob(HttpVerb::Post,
QStringLiteral("RequestTokenToResetPasswordEmailJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/account/password/email/requestToken",
+ makePath("/_matrix/client/r0",
+ "/account/password/email/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob(
const MsisdnValidationData& body)
: BaseJob(HttpVerb::Post,
QStringLiteral("RequestTokenToResetPasswordMSISDNJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/account/password/msisdn/requestToken",
+ makePath("/_matrix/client/r0",
+ "/account/password/msisdn/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
}
DeactivateAccountJob::DeactivateAccountJob(
const Omittable<AuthenticationData>& auth, const QString& idServer)
: BaseJob(HttpVerb::Post, QStringLiteral("DeactivateAccountJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/deactivate")
+ makePath("/_matrix/client/r0", "/account/deactivate"))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
@@ -106,7 +102,7 @@ DeactivateAccountJob::DeactivateAccountJob(
auto queryToCheckUsernameAvailability(const QString& username)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("username"), username);
return _q;
}
@@ -115,13 +111,13 @@ QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl,
const QString& username)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/register/available",
+ makePath("/_matrix/client/r0",
+ "/register/available"),
queryToCheckUsernameAvailability(username));
}
CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& username)
: BaseJob(HttpVerb::Get, QStringLiteral("CheckUsernameAvailabilityJob"),
- QStringLiteral("/_matrix/client/r0") % "/register/available",
+ makePath("/_matrix/client/r0", "/register/available"),
queryToCheckUsernameAvailability(username), {}, false)
{}
diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h
index 6864fd47..c1614f20 100644
--- a/lib/csapi/registration.h
+++ b/lib/csapi/registration.h
@@ -15,8 +15,9 @@ namespace Quotient {
/*! \brief Register for an account on this homeserver.
*
- * This API endpoint uses the `User-Interactive Authentication API`_, except in
- * the cases where a guest account is being registered.
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api), except in the
+ * cases where a guest account is being registered.
*
* Register for an account on this homeserver.
*
@@ -31,45 +32,46 @@ namespace Quotient {
* If registration is successful, this endpoint will issue an access token
* the client can use to authorize itself in subsequent requests.
*
- * If the client does not supply a ``device_id``, the server must
+ * If the client does not supply a `device_id`, the server must
* auto-generate one.
*
* The server SHOULD register an account with a User ID based on the
- * ``username`` provided, if any. Note that the grammar of Matrix User ID
+ * `username` provided, if any. Note that the grammar of Matrix User ID
* localparts is restricted, so the server MUST either map the provided
- * ``username`` onto a ``user_id`` in a logical manner, or reject
- * ``username``\s which do not comply to the grammar, with
- * ``M_INVALID_USERNAME``.
+ * `username` onto a `user_id` in a logical manner, or reject
+ * `username`\s which do not comply to the grammar, with
+ * `M_INVALID_USERNAME`.
*
* Matrix clients MUST NOT assume that localpart of the registered
- * ``user_id`` matches the provided ``username``.
+ * `user_id` matches the provided `username`.
*
- * The returned access token must be associated with the ``device_id``
+ * The returned access token must be associated with the `device_id`
* supplied by the client or generated by the server. The server may
* invalidate any access token previously associated with that device. See
- * `Relationship between access tokens and devices`_.
+ * [Relationship between access tokens and
+ * devices](/client-server-api/#relationship-between-access-tokens-and-devices).
*
* When registering a guest account, all parameters in the request body
- * with the exception of ``initial_device_display_name`` MUST BE ignored
- * by the server. The server MUST pick a ``device_id`` for the account
+ * with the exception of `initial_device_display_name` MUST BE ignored
+ * by the server. The server MUST pick a `device_id` for the account
* regardless of input.
*
* Any user ID returned by this API must conform to the grammar given in the
- * `Matrix specification <../appendices.html#user-identifiers>`_.
+ * [Matrix specification](/appendices/#user-identifiers).
*/
class RegisterJob : public BaseJob {
public:
/*! \brief Register for an account on this homeserver.
*
* \param kind
- * The kind of account to register. Defaults to ``user``.
+ * The kind of account to register. Defaults to `user`.
*
* \param auth
* Additional authentication information for the
* user-interactive authentication API. Note that this
* information is *not* used to define how the registered user
* should be authenticated, but is instead used to
- * authenticate the ``register`` call itself.
+ * authenticate the `register` call itself.
*
* \param username
* The basis for the localpart of the desired Matrix ID. If omitted,
@@ -85,10 +87,10 @@ public:
*
* \param initialDeviceDisplayName
* A display name to assign to the newly-created device. Ignored
- * if ``device_id`` corresponds to a known device.
+ * if `device_id` corresponds to a known device.
*
* \param inhibitLogin
- * If true, an ``access_token`` and ``device_id`` should not be
+ * If true, an `access_token` and `device_id` should not be
* returned from this call, therefore preventing an automatic
* login. Defaults to false.
*/
@@ -105,12 +107,12 @@ public:
/// The fully-qualified Matrix user ID (MXID) that has been registered.
///
/// Any user ID returned by this API must conform to the grammar given in
- /// the `Matrix specification <../appendices.html#user-identifiers>`_.
+ /// the [Matrix specification](/appendices/#user-identifiers).
QString userId() const { return loadFromJson<QString>("user_id"_ls); }
/// An access token for the account.
/// This access token can then be used to authorize other requests.
- /// Required if the ``inhibit_login`` option is false.
+ /// Required if the `inhibit_login` option is false.
QString accessToken() const
{
return loadFromJson<QString>("access_token"_ls);
@@ -120,8 +122,8 @@ public:
/// been registered.
///
/// **Deprecated**. Clients should extract the server_name from
- /// ``user_id`` (by splitting at the first colon) if they require
- /// it. Note also that ``homeserver`` is not spelt this way.
+ /// `user_id` (by splitting at the first colon) if they require
+ /// it. Note also that `homeserver` is not spelt this way.
QString homeServer() const
{
return loadFromJson<QString>("home_server"_ls);
@@ -129,7 +131,7 @@ public:
/// ID of the registered device. Will be the same as the
/// corresponding parameter in the request, if one was specified.
- /// Required if the ``inhibit_login`` option is false.
+ /// Required if the `inhibit_login` option is false.
QString deviceId() const { return loadFromJson<QString>("device_id"_ls); }
};
@@ -201,9 +203,9 @@ public:
*
* Changes the password for an account on this homeserver.
*
- * This API endpoint uses the `User-Interactive Authentication API`_ to
- * ensure the user changing the password is actually the owner of the
- * account.
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api) to ensure the
+ * user changing the password is actually the owner of the account.
*
* An access token should be submitted to this endpoint if the client has
* an active session.
@@ -224,8 +226,8 @@ public:
* Whether the user's other access tokens, and their associated devices,
* should be revoked if the request succeeds.
*
- * When ``false``, the server can still take advantage of `the soft logout
- * method <#soft-logout>`_ for the user's remaining devices.
+ * When `false`, the server can still take advantage of the [soft logout
+ * method](/client-server-api/#soft-logout) for the user's remaining devices.
*
* \param auth
* Additional authentication information for the user-interactive
@@ -242,23 +244,18 @@ public:
* The homeserver must check that the given email address **is
* associated** with an account on this homeserver. This API should be
* used to request validation tokens when authenticating for the
- * ``/account/password`` endpoint.
+ * `/account/password` endpoint.
*
* This API's parameters and response are identical to that of the
- * |/register/email/requestToken|_ endpoint, except that
- * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the
+ * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken)
+ * endpoint, except that
+ * `M_THREEPID_NOT_FOUND` may be returned if no account matching the
* given email address could be found. The server may instead send an
* email to the given address prompting the user to create an account.
- * ``M_THREEPID_IN_USE`` may not be returned.
+ * `M_THREEPID_IN_USE` may not be returned.
*
* The homeserver should validate the email itself, either by sending a
* validation email itself or by using a service it has control over.
- *
- *
- * .. |/register/email/requestToken| replace:: ``/register/email/requestToken``
- *
- * .. _/register/email/requestToken:
- * #post-matrix-client-r0-register-email-requesttoken
*/
class RequestTokenToResetPasswordEmailJob : public BaseJob {
public:
@@ -269,24 +266,18 @@ public:
* The homeserver must check that the given email address **is
* associated** with an account on this homeserver. This API should be
* used to request validation tokens when authenticating for the
- * ``/account/password`` endpoint.
+ * `/account/password` endpoint.
*
* This API's parameters and response are identical to that of the
- * |/register/email/requestToken|_ endpoint, except that
- * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the
+ * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken)
+ * endpoint, except that
+ * `M_THREEPID_NOT_FOUND` may be returned if no account matching the
* given email address could be found. The server may instead send an
* email to the given address prompting the user to create an account.
- * ``M_THREEPID_IN_USE`` may not be returned.
+ * `M_THREEPID_IN_USE` may not be returned.
*
* The homeserver should validate the email itself, either by sending a
* validation email itself or by using a service it has control over.
- *
- *
- * .. |/register/email/requestToken| replace::
- * ``/register/email/requestToken``
- *
- * .. _/register/email/requestToken:
- * #post-matrix-client-r0-register-email-requesttoken
*/
explicit RequestTokenToResetPasswordEmailJob(const EmailValidationData& body);
@@ -305,22 +296,18 @@ public:
* The homeserver must check that the given phone number **is
* associated** with an account on this homeserver. This API should be
* used to request validation tokens when authenticating for the
- * ``/account/password`` endpoint.
+ * `/account/password` endpoint.
*
* This API's parameters and response are identical to that of the
- * |/register/msisdn/requestToken|_ endpoint, except that
- * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the
+ * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken)
+ * endpoint, except that
+ * `M_THREEPID_NOT_FOUND` may be returned if no account matching the
* given phone number could be found. The server may instead send the SMS
* to the given phone number prompting the user to create an account.
- * ``M_THREEPID_IN_USE`` may not be returned.
+ * `M_THREEPID_IN_USE` may not be returned.
*
* The homeserver should validate the phone number itself, either by sending a
* validation message itself or by using a service it has control over.
- *
- * .. |/register/msisdn/requestToken| replace:: ``/register/msisdn/requestToken``
- *
- * .. _/register/msisdn/requestToken:
- * #post-matrix-client-r0-register-email-requesttoken
*/
class RequestTokenToResetPasswordMSISDNJob : public BaseJob {
public:
@@ -331,23 +318,18 @@ public:
* The homeserver must check that the given phone number **is
* associated** with an account on this homeserver. This API should be
* used to request validation tokens when authenticating for the
- * ``/account/password`` endpoint.
+ * `/account/password` endpoint.
*
* This API's parameters and response are identical to that of the
- * |/register/msisdn/requestToken|_ endpoint, except that
- * ``M_THREEPID_NOT_FOUND`` may be returned if no account matching the
+ * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken)
+ * endpoint, except that
+ * `M_THREEPID_NOT_FOUND` may be returned if no account matching the
* given phone number could be found. The server may instead send the SMS
* to the given phone number prompting the user to create an account.
- * ``M_THREEPID_IN_USE`` may not be returned.
+ * `M_THREEPID_IN_USE` may not be returned.
*
* The homeserver should validate the phone number itself, either by sending
* a validation message itself or by using a service it has control over.
- *
- * .. |/register/msisdn/requestToken| replace::
- * ``/register/msisdn/requestToken``
- *
- * .. _/register/msisdn/requestToken:
- * #post-matrix-client-r0-register-email-requesttoken
*/
explicit RequestTokenToResetPasswordMSISDNJob(
const MsisdnValidationData& body);
@@ -366,7 +348,8 @@ public:
* Deactivate the user's account, removing all ability for the user to
* login again.
*
- * This API endpoint uses the `User-Interactive Authentication API`_.
+ * This API endpoint uses the [User-Interactive Authentication
+ * API](/client-server-api/#user-interactive-authentication-api).
*
* An access token should be submitted to this endpoint if the client has
* an active session.
@@ -374,7 +357,7 @@ public:
* The homeserver may change the flows available depending on whether a
* valid access token is provided.
*
- * Unlike other endpoints, this endpoint does not take an ``id_access_token``
+ * Unlike other endpoints, this endpoint does not take an `id_access_token`
* parameter because the homeserver is expected to sign the request to the
* identity server instead.
*/
@@ -388,11 +371,11 @@ public:
*
* \param idServer
* The identity server to unbind all of the user's 3PIDs from.
- * If not provided, the homeserver MUST use the ``id_server``
+ * If not provided, the homeserver MUST use the `id_server`
* that was originally use to bind each identifier. If the
- * homeserver does not know which ``id_server`` that was,
- * it must return an ``id_server_unbind_result`` of
- * ``no-support``.
+ * homeserver does not know which `id_server` that was,
+ * it must return an `id_server_unbind_result` of
+ * `no-support`.
*/
explicit DeactivateAccountJob(const Omittable<AuthenticationData>& auth = none,
const QString& idServer = {});
@@ -400,12 +383,12 @@ public:
// Result properties
/// An indicator as to whether or not the homeserver was able to unbind
- /// the user's 3PIDs from the identity server(s). ``success`` indicates
+ /// the user's 3PIDs from the identity server(s). `success` indicates
/// that all identifiers have been unbound from the identity server while
- /// ``no-support`` indicates that one or more identifiers failed to unbind
+ /// `no-support` indicates that one or more identifiers failed to unbind
/// due to the identity server refusing the request or the homeserver
/// being unable to determine an identity server to unbind from. This
- /// must be ``success`` if the homeserver has no identifiers to unbind
+ /// must be `success` if the homeserver has no identifiers to unbind
/// for the user.
QString idServerUnbindResult() const
{
@@ -447,7 +430,7 @@ public:
// Result properties
/// A flag to indicate that the username is available. This should always
- /// be ``true`` when the server replies with 200 OK.
+ /// be `true` when the server replies with 200 OK.
Omittable<bool> available() const
{
return loadFromJson<Omittable<bool>>("available"_ls);
diff --git a/lib/csapi/report_content.cpp b/lib/csapi/report_content.cpp
index 0a41625f..0a76d5b8 100644
--- a/lib/csapi/report_content.cpp
+++ b/lib/csapi/report_content.cpp
@@ -4,18 +4,16 @@
#include "report_content.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId,
- int score, const QString& reason)
+ Omittable<int> score, const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("ReportContentJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/report/" % eventId)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/report/",
+ eventId))
{
QJsonObject _data;
- addParam<>(_data, QStringLiteral("score"), score);
- addParam<>(_data, QStringLiteral("reason"), reason);
+ addParam<IfNotEmpty>(_data, QStringLiteral("score"), score);
+ addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
setRequestData(std::move(_data));
}
diff --git a/lib/csapi/report_content.h b/lib/csapi/report_content.h
index 375e1829..e401c2e1 100644
--- a/lib/csapi/report_content.h
+++ b/lib/csapi/report_content.h
@@ -31,7 +31,8 @@ public:
* The reason the content is being reported. May be blank.
*/
explicit ReportContentJob(const QString& roomId, const QString& eventId,
- int score, const QString& reason);
+ Omittable<int> score = none,
+ const QString& reason = {});
};
} // namespace Quotient
diff --git a/lib/csapi/room_send.cpp b/lib/csapi/room_send.cpp
index 63986c56..f80f9300 100644
--- a/lib/csapi/room_send.cpp
+++ b/lib/csapi/room_send.cpp
@@ -4,16 +4,14 @@
#include "room_send.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SendMessageJob::SendMessageJob(const QString& roomId, const QString& eventType,
const QString& txnId, const QJsonObject& body)
: BaseJob(HttpVerb::Put, QStringLiteral("SendMessageJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/send/" % eventType % "/" % txnId)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/send/",
+ eventType, "/", txnId))
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
addExpectedKey("event_id");
}
diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h
index 39460aca..96f5beca 100644
--- a/lib/csapi/room_send.h
+++ b/lib/csapi/room_send.h
@@ -16,7 +16,7 @@ namespace Quotient {
*
* The body of the request should be the content object of the event; the
* fields in this object will vary depending on the type of event. See
- * `Room Events`_ for the m. event specification.
+ * [Room Events](/client-server-api/#room-events) for the m. event specification.
*/
class SendMessageJob : public BaseJob {
public:
@@ -40,7 +40,8 @@ public:
*
* The body of the request should be the content object of the event; the
* fields in this object will vary depending on the type of event. See
- * `Room Events`_ for the m. event specification.
+ * [Room Events](/client-server-api/#room-events) for the m. event
+ * specification.
*/
explicit SendMessageJob(const QString& roomId, const QString& eventType,
const QString& txnId, const QJsonObject& body = {});
diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp
index e18108ac..f6d2e6ec 100644
--- a/lib/csapi/room_state.cpp
+++ b/lib/csapi/room_state.cpp
@@ -4,8 +4,6 @@
#include "room_state.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId,
@@ -13,9 +11,9 @@ SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId,
const QString& stateKey,
const QJsonObject& body)
: BaseJob(HttpVerb::Put, QStringLiteral("SetRoomStateWithKeyJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/state/" % eventType % "/" % stateKey)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/",
+ eventType, "/", stateKey))
{
- setRequestData(Data(toJson(body)));
+ setRequestData(RequestData(toJson(body)));
addExpectedKey("event_id");
}
diff --git a/lib/csapi/room_state.h b/lib/csapi/room_state.h
index 447605ff..f95af223 100644
--- a/lib/csapi/room_state.h
+++ b/lib/csapi/room_state.h
@@ -10,22 +10,20 @@ namespace Quotient {
/*! \brief Send a state event to the given room.
*
- * .. For backwards compatibility with older links...
- * .. _`put-matrix-client-r0-rooms-roomid-state-eventtype`:
- *
* State events can be sent using this endpoint. These events will be
- * overwritten if ``<room id>``, ``<event type>`` and ``<state key>`` all
+ * overwritten if `<room id>`, `<event type>` and `<state key>` all
* match.
*
* Requests to this endpoint **cannot use transaction IDs**
- * like other ``PUT`` paths because they cannot be differentiated from the
- * ``state_key``. Furthermore, ``POST`` is unsupported on state paths.
+ * like other `PUT` paths because they cannot be differentiated from the
+ * `state_key`. Furthermore, `POST` is unsupported on state paths.
*
* The body of the request should be the content object of the event; the
* fields in this object will vary depending on the type of event. See
- * `Room Events`_ for the ``m.`` event specification.
+ * [Room Events](/client-server-api/#room-events) for the `m.` event
+ * specification.
*
- * If the event type being sent is ``m.room.canonical_alias`` servers
+ * If the event type being sent is `m.room.canonical_alias` servers
* SHOULD ensure that any new aliases being listed in the event are valid
* per their grammar/syntax and that they point to the room ID where the
* state event is to be sent. Servers do not validate aliases which are
@@ -46,22 +44,20 @@ public:
* an empty string, the trailing slash on this endpoint is optional.
*
* \param body
- * .. For backwards compatibility with older links...
- * .. _`put-matrix-client-r0-rooms-roomid-state-eventtype`:
- *
* State events can be sent using this endpoint. These events will be
- * overwritten if ``<room id>``, ``<event type>`` and ``<state key>`` all
+ * overwritten if `<room id>`, `<event type>` and `<state key>` all
* match.
*
* Requests to this endpoint **cannot use transaction IDs**
- * like other ``PUT`` paths because they cannot be differentiated from the
- * ``state_key``. Furthermore, ``POST`` is unsupported on state paths.
+ * like other `PUT` paths because they cannot be differentiated from the
+ * `state_key`. Furthermore, `POST` is unsupported on state paths.
*
* The body of the request should be the content object of the event; the
* fields in this object will vary depending on the type of event. See
- * `Room Events`_ for the ``m.`` event specification.
+ * [Room Events](/client-server-api/#room-events) for the `m.` event
+ * specification.
*
- * If the event type being sent is ``m.room.canonical_alias`` servers
+ * If the event type being sent is `m.room.canonical_alias` servers
* SHOULD ensure that any new aliases being listed in the event are valid
* per their grammar/syntax and that they point to the room ID where the
* state event is to be sent. Servers do not validate aliases which are
diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp
index e3791b08..d4129cfb 100644
--- a/lib/csapi/room_upgrades.cpp
+++ b/lib/csapi/room_upgrades.cpp
@@ -4,14 +4,11 @@
#include "room_upgrades.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion)
: BaseJob(HttpVerb::Post, QStringLiteral("UpgradeRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/upgrade")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/upgrade"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("new_version"), newVersion);
diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp
index 724d941f..5310aa32 100644
--- a/lib/csapi/rooms.cpp
+++ b/lib/csapi/rooms.cpp
@@ -4,24 +4,21 @@
#include "rooms.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetOneRoomEventJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
const QString& eventId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/event/"
- % eventId);
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/event/", eventId));
}
GetOneRoomEventJob::GetOneRoomEventJob(const QString& roomId,
const QString& eventId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetOneRoomEventJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/event/" % eventId)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/event/",
+ eventId))
{}
QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
@@ -29,36 +26,35 @@ QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
const QString& stateKey)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/state/"
- % eventType % "/" % stateKey);
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/state/", eventType, "/",
+ stateKey));
}
GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId,
const QString& eventType,
const QString& stateKey)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateWithKeyJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/state/" % eventType % "/" % stateKey)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/",
+ eventType, "/", stateKey))
{}
QUrl GetRoomStateJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/state");
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/state"));
}
GetRoomStateJob::GetRoomStateJob(const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/state")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/state"))
{}
auto queryToGetMembersByRoom(const QString& at, const QString& membership,
const QString& notMembership)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("at"), at);
addParam<IfNotEmpty>(_q, QStringLiteral("membership"), membership);
addParam<IfNotEmpty>(_q, QStringLiteral("not_membership"), notMembership);
@@ -72,7 +68,7 @@ QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
{
return BaseJob::makeRequestUrl(
std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/members",
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"),
queryToGetMembersByRoom(at, membership, notMembership));
}
@@ -81,8 +77,7 @@ GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId,
const QString& membership,
const QString& notMembership)
: BaseJob(HttpVerb::Get, QStringLiteral("GetMembersByRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/members",
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"),
queryToGetMembersByRoom(at, membership, notMembership))
{}
@@ -90,12 +85,12 @@ QUrl GetJoinedMembersByRoomJob::makeRequestUrl(QUrl baseUrl,
const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/joined_members");
+ makePath("/_matrix/client/r0", "/rooms/",
+ roomId, "/joined_members"));
}
GetJoinedMembersByRoomJob::GetJoinedMembersByRoomJob(const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedMembersByRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/joined_members")
+ makePath("/_matrix/client/r0", "/rooms/", roomId,
+ "/joined_members"))
{}
diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h
index f0bfa349..2620582b 100644
--- a/lib/csapi/rooms.h
+++ b/lib/csapi/rooms.h
@@ -12,7 +12,7 @@ namespace Quotient {
/*! \brief Get a single event by event ID.
*
- * Get a single event based on ``roomId/eventId``. You must have permission to
+ * Get a single event based on `roomId/eventId`. You must have permission to
* retrieve this event e.g. by being a member in the room for this event.
*/
class GetOneRoomEventJob : public BaseJob {
@@ -43,9 +43,6 @@ public:
/*! \brief Get the state identified by the type and key.
*
- * .. For backwards compatibility with older links...
- * .. _`get-matrix-client-r0-rooms-roomid-state-eventtype`:
- *
* Looks up the contents of a state event in a room. If the user is
* joined to the room then the state is taken from the current
* state of the room. If the user has left the room then the state is
@@ -118,16 +115,15 @@ public:
*
* \param at
* The point in time (pagination token) to return members for in the room.
- * This token can be obtained from a ``prev_batch`` token returned for
+ * This token can be obtained from a `prev_batch` token returned for
* each room by the sync API. Defaults to the current state of the room,
* as determined by the server.
*
* \param membership
* The kind of membership to filter for. Defaults to no filtering if
- * unspecified. When specified alongside ``not_membership``, the two
+ * unspecified. When specified alongside `not_membership`, the two
* parameters create an 'or' condition: either the membership *is*
- * the same as ``membership`` **or** *is not* the same as
- * ``not_membership``.
+ * the same as `membership` **or** *is not* the same as `not_membership`.
*
* \param notMembership
* The kind of membership to exclude from the results. Defaults to no
@@ -162,7 +158,7 @@ public:
* room. The current user must be in the room for it to work, unless it is an
* Application Service in which case any of the AS's users must be in the room.
* This API is primarily for Application Services and should be faster to
- * respond than ``/members`` as it can be implemented more efficiently on the
+ * respond than `/members` as it can be implemented more efficiently on the
* server.
*/
class GetJoinedMembersByRoomJob : public BaseJob {
@@ -173,13 +169,13 @@ public:
/// the room. The current user must be in the room for it to work, unless it
/// is an Application Service in which case any of the AS's users must be in
/// the room. This API is primarily for Application Services and should be
- /// faster to respond than ``/members`` as it can be implemented more
+ /// faster to respond than `/members` as it can be implemented more
/// efficiently on the server.
struct RoomMember {
/// The display name of the user this object is representing.
QString displayName;
/// The mxc avatar url of the user this object is representing.
- QString avatarUrl;
+ QUrl avatarUrl;
};
// Construction/destruction
diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp
index 5649d52a..295dd1cc 100644
--- a/lib/csapi/search.cpp
+++ b/lib/csapi/search.cpp
@@ -4,13 +4,11 @@
#include "search.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToSearch(const QString& nextBatch)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("next_batch"), nextBatch);
return _q;
}
@@ -18,7 +16,7 @@ auto queryToSearch(const QString& nextBatch)
SearchJob::SearchJob(const Categories& searchCategories,
const QString& nextBatch)
: BaseJob(HttpVerb::Post, QStringLiteral("SearchJob"),
- QStringLiteral("/_matrix/client/r0") % "/search",
+ makePath("/_matrix/client/r0", "/search"),
queryToSearch(nextBatch))
{
QJsonObject _data;
diff --git a/lib/csapi/search.h b/lib/csapi/search.h
index c009ded6..3d02752a 100644
--- a/lib/csapi/search.h
+++ b/lib/csapi/search.h
@@ -23,15 +23,15 @@ public:
/// returned are included in the response.
struct IncludeEventContext {
/// How many events before the result are
- /// returned. By default, this is ``5``.
+ /// returned. By default, this is `5`.
Omittable<int> beforeLimit;
/// How many events after the result are
- /// returned. By default, this is ``5``.
+ /// returned. By default, this is `5`.
Omittable<int> afterLimit;
/// Requests that the server returns the
/// historic profile information for the users
/// that sent the events that were returned.
- /// By default, this is ``false``.
+ /// By default, this is `false`.
Omittable<bool> includeProfile;
};
@@ -54,10 +54,10 @@ public:
QString searchTerm;
/// The keys to search. Defaults to all.
QStringList keys;
- /// This takes a `filter`_.
+ /// This takes a [filter](/client-server-api/#filtering).
RoomEventFilter filter;
/// The order in which to search for results.
- /// By default, this is ``"rank"``.
+ /// By default, this is `"rank"`.
QString orderBy;
/// Configures whether any context for the events
/// returned are included in the response.
@@ -81,7 +81,7 @@ public:
/// Performs a full text search across different categories.
QString displayname;
/// Performs a full text search across different categories.
- QString avatarUrl;
+ QUrl avatarUrl;
};
/// Context for result, if requested.
@@ -93,7 +93,7 @@ public:
/// The historic profile information of the
/// users that sent the events returned.
///
- /// The ``string`` key is the user ID for which
+ /// The `string` key is the user ID for which
/// the profile belongs to.
QHash<QString, UserProfile> profileInfo;
/// Events just before the result.
@@ -139,15 +139,15 @@ public:
std::vector<Result> results;
/// The current state for every room in the results.
/// This is included if the request had the
- /// ``include_state`` key set with a value of ``true``.
+ /// `include_state` key set with a value of `true`.
///
- /// The ``string`` key is the room ID for which the ``State
- /// Event`` array belongs to.
+ /// The `string` key is the room ID for which the `State
+ /// Event` array belongs to.
UnorderedMap<QString, StateEvents> state;
/// Any groups that were requested.
///
- /// The outer ``string`` key is the group key requested (eg: ``room_id``
- /// or ``sender``). The inner ``string`` key is the grouped value (eg:
+ /// The outer `string` key is the group key requested (eg: `room_id`
+ /// or `sender`). The inner `string` key is the grouped value (eg:
/// a room's ID or a user's ID).
QHash<QString, QHash<QString, GroupValue>> groups;
/// Token that can be used to get the next batch of
@@ -172,7 +172,7 @@ public:
*
* \param nextBatch
* The point to return events from. If given, this should be a
- * ``next_batch`` result from a previous call to this endpoint.
+ * `next_batch` result from a previous call to this endpoint.
*/
explicit SearchJob(const Categories& searchCategories,
const QString& nextBatch = {});
diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp
index 85a18560..871d6ff6 100644
--- a/lib/csapi/sso_login_redirect.cpp
+++ b/lib/csapi/sso_login_redirect.cpp
@@ -4,13 +4,11 @@
#include "sso_login_redirect.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
auto queryToRedirectToSSO(const QString& redirectUrl)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl);
return _q;
}
@@ -18,13 +16,36 @@ auto queryToRedirectToSSO(const QString& redirectUrl)
QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/login/sso/redirect",
+ makePath("/_matrix/client/r0",
+ "/login/sso/redirect"),
queryToRedirectToSSO(redirectUrl));
}
RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl)
: BaseJob(HttpVerb::Get, QStringLiteral("RedirectToSSOJob"),
- QStringLiteral("/_matrix/client/r0") % "/login/sso/redirect",
+ makePath("/_matrix/client/r0", "/login/sso/redirect"),
queryToRedirectToSSO(redirectUrl), {}, false)
{}
+
+auto queryToRedirectToIdP(const QString& redirectUrl)
+{
+ QUrlQuery _q;
+ addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl);
+ return _q;
+}
+
+QUrl RedirectToIdPJob::makeRequestUrl(QUrl baseUrl, const QString& idpId,
+ const QString& redirectUrl)
+{
+ return BaseJob::makeRequestUrl(std::move(baseUrl),
+ makePath("/_matrix/client/r0",
+ "/login/sso/redirect/", idpId),
+ queryToRedirectToIdP(redirectUrl));
+}
+
+RedirectToIdPJob::RedirectToIdPJob(const QString& idpId,
+ const QString& redirectUrl)
+ : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToIdPJob"),
+ makePath("/_matrix/client/r0", "/login/sso/redirect/", idpId),
+ queryToRedirectToIdP(redirectUrl), {}, false)
+{}
diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h
index d6330e38..ade1eb7d 100644
--- a/lib/csapi/sso_login_redirect.h
+++ b/lib/csapi/sso_login_redirect.h
@@ -13,7 +13,9 @@ namespace Quotient {
* A web-based Matrix client should instruct the user's browser to
* navigate to this endpoint in order to log in via SSO.
*
- * The server MUST respond with an HTTP redirect to the SSO interface.
+ * The server MUST respond with an HTTP redirect to the SSO interface,
+ * or present a page which lets the user select an IdP to continue
+ * with in the event multiple are supported by the server.
*/
class RedirectToSSOJob : public BaseJob {
public:
@@ -33,4 +35,36 @@ public:
static QUrl makeRequestUrl(QUrl baseUrl, const QString& redirectUrl);
};
+/*! \brief Redirect the user's browser to the SSO interface for an IdP.
+ *
+ * This endpoint is the same as `/login/sso/redirect`, though with an
+ * IdP ID from the original `identity_providers` array to inform the
+ * server of which IdP the client/user would like to continue with.
+ *
+ * The server MUST respond with an HTTP redirect to the SSO interface
+ * for that IdP.
+ */
+class RedirectToIdPJob : public BaseJob {
+public:
+ /*! \brief Redirect the user's browser to the SSO interface for an IdP.
+ *
+ * \param idpId
+ * The `id` of the IdP from the `m.login.sso` `identity_providers`
+ * array denoting the user's selection.
+ *
+ * \param redirectUrl
+ * URI to which the user will be redirected after the homeserver has
+ * authenticated the user with SSO.
+ */
+ explicit RedirectToIdPJob(const QString& idpId, const QString& redirectUrl);
+
+ /*! \brief Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for RedirectToIdPJob
+ * is necessary but the job itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& idpId,
+ const QString& redirectUrl);
+};
+
} // namespace Quotient
diff --git a/lib/csapi/tags.cpp b/lib/csapi/tags.cpp
index dc22dc18..f717de6e 100644
--- a/lib/csapi/tags.cpp
+++ b/lib/csapi/tags.cpp
@@ -4,30 +4,28 @@
#include "tags.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetRoomTagsJob::makeRequestUrl(QUrl baseUrl, const QString& userId,
const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0") % "/user/"
- % userId % "/rooms/" % roomId % "/tags");
+ makePath("/_matrix/client/r0", "/user/",
+ userId, "/rooms/", roomId, "/tags"));
}
GetRoomTagsJob::GetRoomTagsJob(const QString& userId, const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomTagsJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/rooms/" % roomId % "/tags")
+ makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ roomId, "/tags"))
{}
SetRoomTagJob::SetRoomTagJob(const QString& userId, const QString& roomId,
const QString& tag, Omittable<float> order,
const QVariantHash& additionalProperties)
: BaseJob(HttpVerb::Put, QStringLiteral("SetRoomTagJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/rooms/" % roomId % "/tags/" % tag)
+ makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ roomId, "/tags/", tag))
{
QJsonObject _data;
fillJson(_data, additionalProperties);
@@ -39,14 +37,14 @@ QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId,
const QString& roomId, const QString& tag)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/user/" % userId % "/rooms/" % roomId
- % "/tags/" % tag);
+ makePath("/_matrix/client/r0", "/user/",
+ userId, "/rooms/", roomId, "/tags/",
+ tag));
}
DeleteRoomTagJob::DeleteRoomTagJob(const QString& userId, const QString& roomId,
const QString& tag)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomTagJob"),
- QStringLiteral("/_matrix/client/r0") % "/user/" % userId
- % "/rooms/" % roomId % "/tags/" % tag)
+ makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ roomId, "/tags/", tag))
{}
diff --git a/lib/csapi/tags.h b/lib/csapi/tags.h
index a815d9b3..a854531a 100644
--- a/lib/csapi/tags.h
+++ b/lib/csapi/tags.h
@@ -18,7 +18,7 @@ public:
/// List the tags set by a user on a room.
struct Tag {
- /// A number in a range ``[0,1]`` describing a relative
+ /// A number in a range `[0,1]` describing a relative
/// position of the room under the given tag.
Omittable<float> order;
/// List the tags set by a user on a room.
@@ -83,7 +83,7 @@ public:
* The tag to add.
*
* \param order
- * A number in a range ``[0,1]`` describing a relative
+ * A number in a range `[0,1]` describing a relative
* position of the room under the given tag.
*
* \param additionalProperties
diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp
index baf1fab5..4c930668 100644
--- a/lib/csapi/third_party_lookup.cpp
+++ b/lib/csapi/third_party_lookup.cpp
@@ -4,39 +4,36 @@
#include "third_party_lookup.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetProtocolsJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/thirdparty/protocols");
+ makePath("/_matrix/client/r0",
+ "/thirdparty/protocols"));
}
GetProtocolsJob::GetProtocolsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolsJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocols")
+ makePath("/_matrix/client/r0", "/thirdparty/protocols"))
{}
QUrl GetProtocolMetadataJob::makeRequestUrl(QUrl baseUrl,
const QString& protocol)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/thirdparty/protocol/" % protocol);
+ makePath("/_matrix/client/r0",
+ "/thirdparty/protocol/", protocol));
}
GetProtocolMetadataJob::GetProtocolMetadataJob(const QString& protocol)
: BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolMetadataJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocol/"
- % protocol)
+ makePath("/_matrix/client/r0", "/thirdparty/protocol/", protocol))
{}
auto queryToQueryLocationByProtocol(const QString& searchFields)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("searchFields"), searchFields);
return _q;
}
@@ -46,22 +43,21 @@ QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl,
const QString& searchFields)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/thirdparty/location/" % protocol,
+ makePath("/_matrix/client/r0",
+ "/thirdparty/location/", protocol),
queryToQueryLocationByProtocol(searchFields));
}
QueryLocationByProtocolJob::QueryLocationByProtocolJob(
const QString& protocol, const QString& searchFields)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByProtocolJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/location/"
- % protocol,
+ makePath("/_matrix/client/r0", "/thirdparty/location/", protocol),
queryToQueryLocationByProtocol(searchFields))
{}
auto queryToQueryUserByProtocol(const QString& fields)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<IfNotEmpty>(_q, QStringLiteral("fields..."), fields);
return _q;
}
@@ -71,22 +67,21 @@ QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl,
const QString& fields)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/thirdparty/user/" % protocol,
+ makePath("/_matrix/client/r0",
+ "/thirdparty/user/", protocol),
queryToQueryUserByProtocol(fields));
}
QueryUserByProtocolJob::QueryUserByProtocolJob(const QString& protocol,
const QString& fields)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByProtocolJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/user/"
- % protocol,
+ makePath("/_matrix/client/r0", "/thirdparty/user/", protocol),
queryToQueryUserByProtocol(fields))
{}
auto queryToQueryLocationByAlias(const QString& alias)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("alias"), alias);
return _q;
}
@@ -94,20 +89,20 @@ auto queryToQueryLocationByAlias(const QString& alias)
QUrl QueryLocationByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& alias)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/thirdparty/location",
+ makePath("/_matrix/client/r0",
+ "/thirdparty/location"),
queryToQueryLocationByAlias(alias));
}
QueryLocationByAliasJob::QueryLocationByAliasJob(const QString& alias)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByAliasJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/location",
+ makePath("/_matrix/client/r0", "/thirdparty/location"),
queryToQueryLocationByAlias(alias))
{}
auto queryToQueryUserByID(const QString& userid)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("userid"), userid);
return _q;
}
@@ -115,13 +110,13 @@ auto queryToQueryUserByID(const QString& userid)
QUrl QueryUserByIDJob::makeRequestUrl(QUrl baseUrl, const QString& userid)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/thirdparty/user",
+ makePath("/_matrix/client/r0",
+ "/thirdparty/user"),
queryToQueryUserByID(userid));
}
QueryUserByIDJob::QueryUserByIDJob(const QString& userid)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByIDJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/user",
+ makePath("/_matrix/client/r0", "/thirdparty/user"),
queryToQueryUserByID(userid))
{}
diff --git a/lib/csapi/third_party_membership.cpp b/lib/csapi/third_party_membership.cpp
index fda772d2..59275e41 100644
--- a/lib/csapi/third_party_membership.cpp
+++ b/lib/csapi/third_party_membership.cpp
@@ -4,16 +4,13 @@
#include "third_party_membership.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer,
const QString& idAccessToken,
const QString& medium, const QString& address)
: BaseJob(HttpVerb::Post, QStringLiteral("InviteBy3PIDJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/invite")
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/invite"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("id_server"), idServer);
diff --git a/lib/csapi/third_party_membership.h b/lib/csapi/third_party_membership.h
index 55cab370..a424678f 100644
--- a/lib/csapi/third_party_membership.h
+++ b/lib/csapi/third_party_membership.h
@@ -10,14 +10,13 @@ namespace Quotient {
/*! \brief Invite a user to participate in a particular room.
*
- * .. _invite-by-third-party-id-endpoint:
- *
* *Note that there are two forms of this API, which are documented separately.
* This version of the API does not require that the inviter know the Matrix
* identifier of the invitee, and instead relies on third party identifiers.
* The homeserver uses an identity server to perform the mapping from
* third party identifier to a Matrix identifier. The other is documented in
- * the* `joining rooms section`_.
+ * the* [joining rooms
+ * section](/client-server-api/#post_matrixclientr0roomsroomidinvite).
*
* This API invites a user to participate in a particular room.
* They do not start participating in the room until they actually join the
@@ -27,7 +26,7 @@ namespace Quotient {
* join that room.
*
* If the identity server did know the Matrix user identifier for the
- * third party identifier, the homeserver will append a ``m.room.member``
+ * third party identifier, the homeserver will append a `m.room.member`
* event to the room.
*
* If the identity server does not know a Matrix user identifier for the
@@ -35,7 +34,7 @@ namespace Quotient {
* which can be accepted upon providing proof of ownership of the third
* party identifier. This is achieved by the identity server generating a
* token, which it gives to the inviting homeserver. The homeserver will
- * add an ``m.room.third_party_invite`` event into the graph for the room,
+ * add an `m.room.third_party_invite` event into the graph for the room,
* containing that token.
*
* When the invitee binds the invited third party identifier to a Matrix
@@ -51,9 +50,7 @@ namespace Quotient {
* - The matrix user ID who invited them to the room
*
* If a token is requested from the identity server, the homeserver will
- * append a ``m.room.third_party_invite`` event to the room.
- *
- * .. _joining rooms section: `invite-by-user-id-endpoint`_
+ * append a `m.room.third_party_invite` event to the room.
*/
class InviteBy3PIDJob : public BaseJob {
public:
@@ -73,7 +70,7 @@ public:
*
* \param medium
* The kind of address being passed in the address field, for example
- * ``email``.
+ * `email`.
*
* \param address
* The invitee's third party identifier.
diff --git a/lib/csapi/to_device.cpp b/lib/csapi/to_device.cpp
index 28c4115a..628e8314 100644
--- a/lib/csapi/to_device.cpp
+++ b/lib/csapi/to_device.cpp
@@ -4,18 +4,16 @@
#include "to_device.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SendToDeviceJob::SendToDeviceJob(
const QString& eventType, const QString& txnId,
const QHash<QString, QHash<QString, QJsonObject>>& messages)
: BaseJob(HttpVerb::Put, QStringLiteral("SendToDeviceJob"),
- QStringLiteral("/_matrix/client/r0") % "/sendToDevice/"
- % eventType % "/" % txnId)
+ makePath("/_matrix/client/r0", "/sendToDevice/", eventType, "/",
+ txnId))
{
QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("messages"), messages);
+ addParam<>(_data, QStringLiteral("messages"), messages);
setRequestData(std::move(_data));
}
diff --git a/lib/csapi/to_device.h b/lib/csapi/to_device.h
index f5d69d65..7a237195 100644
--- a/lib/csapi/to_device.h
+++ b/lib/csapi/to_device.h
@@ -32,7 +32,7 @@ public:
*/
explicit SendToDeviceJob(
const QString& eventType, const QString& txnId,
- const QHash<QString, QHash<QString, QJsonObject>>& messages = {});
+ const QHash<QString, QHash<QString, QJsonObject>>& messages);
};
} // namespace Quotient
diff --git a/lib/csapi/typing.cpp b/lib/csapi/typing.cpp
index 8e214053..c9673118 100644
--- a/lib/csapi/typing.cpp
+++ b/lib/csapi/typing.cpp
@@ -4,15 +4,13 @@
#include "typing.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId,
bool typing, Omittable<int> timeout)
: BaseJob(HttpVerb::Put, QStringLiteral("SetTypingJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/typing/" % userId)
+ makePath("/_matrix/client/r0", "/rooms/", roomId, "/typing/",
+ userId))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("typing"), typing);
diff --git a/lib/csapi/typing.h b/lib/csapi/typing.h
index 2c953949..64a310d0 100644
--- a/lib/csapi/typing.h
+++ b/lib/csapi/typing.h
@@ -11,8 +11,8 @@ namespace Quotient {
/*! \brief Informs the server that the user has started or stopped typing.
*
* This tells the server that the user is typing for the next N
- * milliseconds where N is the value specified in the ``timeout`` key.
- * Alternatively, if ``typing`` is ``false``, it tells the server that the
+ * milliseconds where N is the value specified in the `timeout` key.
+ * Alternatively, if `typing` is `false`, it tells the server that the
* user has stopped typing.
*/
class SetTypingJob : public BaseJob {
@@ -26,7 +26,7 @@ public:
* The room in which the user is typing.
*
* \param typing
- * Whether the user is typing or not. If ``false``, the ``timeout``
+ * Whether the user is typing or not. If `false`, the `timeout`
* key can be omitted.
*
* \param timeout
diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp
index a0279d7e..48b727f0 100644
--- a/lib/csapi/users.cpp
+++ b/lib/csapi/users.cpp
@@ -4,14 +4,12 @@
#include "users.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SearchUserDirectoryJob::SearchUserDirectoryJob(const QString& searchTerm,
Omittable<int> limit)
: BaseJob(HttpVerb::Post, QStringLiteral("SearchUserDirectoryJob"),
- QStringLiteral("/_matrix/client/r0") % "/user_directory/search")
+ makePath("/_matrix/client/r0", "/user_directory/search"))
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("search_term"), searchTerm);
diff --git a/lib/csapi/users.h b/lib/csapi/users.h
index 6fc26f57..ec186592 100644
--- a/lib/csapi/users.h
+++ b/lib/csapi/users.h
@@ -19,7 +19,7 @@ namespace Quotient {
*
* The search is performed case-insensitively on user IDs and display
* names preferably using a collation determined based upon the
- * ``Accept-Language`` header provided in the request, if present.
+ * `Accept-Language` header provided in the request, if present.
*/
class SearchUserDirectoryJob : public BaseJob {
public:
@@ -34,14 +34,14 @@ public:
///
/// The search is performed case-insensitively on user IDs and display
/// names preferably using a collation determined based upon the
- /// ``Accept-Language`` header provided in the request, if present.
+ /// `Accept-Language` header provided in the request, if present.
struct User {
/// The user's matrix user ID.
QString userId;
/// The display name of the user, if one exists.
QString displayName;
/// The avatar url, as an MXC, if one exists.
- QString avatarUrl;
+ QUrl avatarUrl;
};
// Construction/destruction
diff --git a/lib/csapi/versions.cpp b/lib/csapi/versions.cpp
index 9003e27f..a1efc33e 100644
--- a/lib/csapi/versions.cpp
+++ b/lib/csapi/versions.cpp
@@ -4,20 +4,17 @@
#include "versions.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client")
- % "/versions");
+ makePath("/_matrix/client", "/versions"));
}
GetVersionsJob::GetVersionsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetVersionsJob"),
- QStringLiteral("/_matrix/client") % "/versions", false)
+ makePath("/_matrix/client", "/versions"), false)
{
addExpectedKey("versions");
}
diff --git a/lib/csapi/versions.h b/lib/csapi/versions.h
index 828a7eb9..896e2ea9 100644
--- a/lib/csapi/versions.h
+++ b/lib/csapi/versions.h
@@ -12,14 +12,14 @@ namespace Quotient {
*
* Gets the versions of the specification supported by the server.
*
- * Values will take the form ``rX.Y.Z``.
+ * Values will take the form `rX.Y.Z`.
*
- * Only the latest ``Z`` value will be reported for each supported ``X.Y``
- * value. i.e. if the server implements ``r0.0.0``, ``r0.0.1``, and ``r1.2.0``,
- * it will report ``r0.0.1`` and ``r1.2.0``.
+ * Only the latest `Z` value will be reported for each supported `X.Y` value.
+ * i.e. if the server implements `r0.0.0`, `r0.0.1`, and `r1.2.0`, it will
+ * report `r0.0.1` and `r1.2.0`.
*
* The server may additionally advertise experimental features it supports
- * through ``unstable_features``. These features should be namespaced and
+ * through `unstable_features`. These features should be namespaced and
* may optionally include version information within their name if desired.
* Features listed here are not for optionally toggling parts of the Matrix
* specification and should only be used to advertise support for a feature
diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp
index 43170057..c748ad94 100644
--- a/lib/csapi/voip.cpp
+++ b/lib/csapi/voip.cpp
@@ -4,18 +4,15 @@
#include "voip.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetTurnServerJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/voip/turnServer");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/voip/turnServer"));
}
GetTurnServerJob::GetTurnServerJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetTurnServerJob"),
- QStringLiteral("/_matrix/client/r0") % "/voip/turnServer")
+ makePath("/_matrix/client/r0", "/voip/turnServer"))
{}
diff --git a/lib/csapi/wellknown.cpp b/lib/csapi/wellknown.cpp
index 1aa0a90b..0b441279 100644
--- a/lib/csapi/wellknown.cpp
+++ b/lib/csapi/wellknown.cpp
@@ -4,18 +4,15 @@
#include "wellknown.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetWellknownJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/.well-known")
- % "/matrix/client");
+ makePath("/.well-known", "/matrix/client"));
}
GetWellknownJob::GetWellknownJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetWellknownJob"),
- QStringLiteral("/.well-known") % "/matrix/client", false)
+ makePath("/.well-known", "/matrix/client"), false)
{}
diff --git a/lib/csapi/wellknown.h b/lib/csapi/wellknown.h
index b21d9fc7..c707d232 100644
--- a/lib/csapi/wellknown.h
+++ b/lib/csapi/wellknown.h
@@ -14,7 +14,7 @@ namespace Quotient {
*
* Gets discovery information about the domain. The file may include
* additional keys, which MUST follow the Java package naming convention,
- * e.g. ``com.example.myapp.property``. This ensures property names are
+ * e.g. `com.example.myapp.property`. This ensures property names are
* suitably namespaced for each application and reduces the risk of
* clashes.
*
diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp
index 73f0298e..ed8a9817 100644
--- a/lib/csapi/whoami.cpp
+++ b/lib/csapi/whoami.cpp
@@ -4,20 +4,17 @@
#include "whoami.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
QUrl GetTokenOwnerJob::makeRequestUrl(QUrl baseUrl)
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/account/whoami");
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl), makePath("/_matrix/client/r0", "/account/whoami"));
}
GetTokenOwnerJob::GetTokenOwnerJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetTokenOwnerJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/whoami")
+ makePath("/_matrix/client/r0", "/account/whoami"))
{
addExpectedKey("user_id");
}
diff --git a/lib/csapi/whoami.h b/lib/csapi/whoami.h
index af8f1e8a..319f82c5 100644
--- a/lib/csapi/whoami.h
+++ b/lib/csapi/whoami.h
@@ -14,8 +14,8 @@ namespace Quotient {
*
* Note that, as with the rest of the Client-Server API,
* Application Services may masquerade as users within their
- * namespace by giving a ``user_id`` query parameter. In this
- * situation, the server should verify that the given ``user_id``
+ * namespace by giving a `user_id` query parameter. In this
+ * situation, the server should verify that the given `user_id`
* is registered by the appservice, and return it in the response
* body.
*/
@@ -33,8 +33,14 @@ public:
// Result properties
- /// The user id that owns the access token.
+ /// The user ID that owns the access token.
QString userId() const { return loadFromJson<QString>("user_id"_ls); }
+
+ /// Device ID associated with the access token. If no device
+ /// is associated with the access token (such as in the case
+ /// of application services) then this field can be omitted.
+ /// Otherwise this is required.
+ QString deviceId() const { return loadFromJson<QString>("device_id"_ls); }
};
} // namespace Quotient
diff --git a/lib/eventitem.h b/lib/eventitem.h
index 1986ba77..0ab1a01d 100644
--- a/lib/eventitem.h
+++ b/lib/eventitem.h
@@ -9,11 +9,10 @@
#include <utility>
namespace Quotient {
-class StateEventBase;
-class EventStatus {
- Q_GADGET
-public:
+namespace EventStatus {
+ Q_NAMESPACE
+
/** Special marks an event can assume
*
* This is used to hint at a special status of some events in UI.
@@ -31,9 +30,8 @@ public:
Replaced = 0x10, //< The event has been replaced
Hidden = 0x100, //< The event should not be shown in the timeline
};
- Q_DECLARE_FLAGS(Status, Code)
- Q_FLAG(Status)
-};
+ Q_ENUM_NS(Code)
+} // namespace EventStatus
class EventItemBase {
public:
@@ -106,7 +104,6 @@ inline const CallEventBase* EventItemBase::viewAs<CallEventBase>() const
}
class PendingEventItem : public EventItemBase {
- Q_GADGET
public:
using EventItemBase::EventItemBase;
@@ -148,4 +145,3 @@ inline QDebug& operator<<(QDebug& d, const TimelineItem& ti)
return d;
}
} // namespace Quotient
-Q_DECLARE_METATYPE(Quotient::EventStatus)
diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h
index 8cea0ec8..9cf77be3 100644
--- a/lib/events/accountdataevents.h
+++ b/lib/events/accountdataevents.h
@@ -4,7 +4,6 @@
#pragma once
#include "event.h"
-#include "eventcontent.h"
namespace Quotient {
constexpr const char* FavouriteTag = "m.favourite";
@@ -16,12 +15,12 @@ struct TagRecord {
order_type order;
- TagRecord(order_type order = none) : order(std::move(order)) {}
+ TagRecord(order_type order = none) : order(order) {}
bool operator<(const TagRecord& other) const
{
// Per The Spec, rooms with no order should be after those with order,
- // against optional<>::operator<() convention.
+ // against std::optional<>::operator<() convention.
return order && (!other.order || *order < *other.order);
}
};
@@ -55,15 +54,15 @@ using TagsMap = QHash<QString, TagRecord>;
public: \
using content_type = _ContentType; \
DEFINE_EVENT_TYPEID(_TypeId, _Name) \
- explicit _Name(QJsonObject obj) : Event(typeId(), std::move(obj)) {} \
- explicit _Name(_ContentType content) \
+ explicit _Name(const QJsonObject& obj) : Event(typeId(), obj) {} \
+ explicit _Name(const content_type& content) \
: Event(typeId(), matrixTypeId(), \
- QJsonObject { { QStringLiteral(#_ContentKey), \
- toJson(std::move(content)) } }) \
+ QJsonObject { \
+ { QStringLiteral(#_ContentKey), toJson(content) } }) \
{} \
auto _ContentKey() const \
{ \
- return content<content_type>(#_ContentKey##_ls); \
+ return contentPart<content_type>(#_ContentKey##_ls); \
} \
}; \
REGISTER_EVENT_TYPE(_Name) \
diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h
index 6132cb44..4c01c941 100644
--- a/lib/events/callanswerevent.h
+++ b/lib/events/callanswerevent.h
@@ -19,11 +19,11 @@ public:
int lifetime() const
{
- return content<int>("lifetime"_ls);
+ return contentPart<int>("lifetime"_ls);
} // FIXME: Omittable<>?
QString sdp() const
{
- return contentJson()["answer"_ls].toObject().value("sdp"_ls).toString();
+ return contentPart<QJsonObject>("answer"_ls).value("sdp"_ls).toString();
}
};
diff --git a/lib/events/callcandidatesevent.cpp b/lib/events/callcandidatesevent.cpp
deleted file mode 100644
index b87c8e9b..00000000
--- a/lib/events/callcandidatesevent.cpp
+++ /dev/null
@@ -1,27 +0,0 @@
-// SPDX-FileCopyrightText: 2017 Marius Gripsgard <marius@ubports.com>
-// SPDX-FileCopyrightText: 2018 Josip Delic <delijati@googlemail.com>
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#include "callcandidatesevent.h"
-
-/*
-m.call.candidates
-{
- "age": 242352,
- "content": {
- "call_id": "12345",
- "candidates": [
- {
- "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156
-43670 typ host generation 0", "sdpMLineIndex": 0, "sdpMid": "audio"
- }
- ],
- "version": 0
- },
- "event_id": "$WLGTSEFSEF:localhost",
- "origin_server_ts": 1431961217939,
- "room_id": "!Cuyf34gef24t:localhost",
- "sender": "@example:localhost",
- "type": "m.call.candidates"
-}
-*/
diff --git a/lib/events/callcandidatesevent.h b/lib/events/callcandidatesevent.h
index c2ccac3b..74c38f2c 100644
--- a/lib/events/callcandidatesevent.h
+++ b/lib/events/callcandidatesevent.h
@@ -25,17 +25,17 @@ public:
QJsonArray candidates() const
{
- return content<QJsonArray>("candidates"_ls);
+ return contentPart<QJsonArray>("candidates"_ls);
}
QString callId() const
{
- return content<QString>("call_id");
+ return contentPart<QString>("call_id");
}
int version() const
{
- return content<int>("version");
+ return contentPart<int>("version");
}
};
diff --git a/lib/events/callhangupevent.cpp b/lib/events/callhangupevent.cpp
deleted file mode 100644
index 43bc4db0..00000000
--- a/lib/events/callhangupevent.cpp
+++ /dev/null
@@ -1,36 +0,0 @@
-/******************************************************************************
- * SPDX-FileCopyrightText: 2017 Marius Gripsgard <marius@ubports.com>
- * SPDX-FileCopyrightText: 2018 Josip Delic <delijati@googlemail.com>
- *
- * SPDX-License-Identifier: LGPL-2.1-or-later
- */
-
-#include "callhangupevent.h"
-
-/*
-m.call.hangup
-{
- "age": 242352,
- "content": {
- "call_id": "12345",
- "version": 0
- },
- "event_id": "$WLGTSEFSEF:localhost",
- "origin_server_ts": 1431961217939,
- "room_id": "!Cuyf34gef24t:localhost",
- "sender": "@example:localhost",
- "type": "m.call.hangup"
-}
-*/
-
-using namespace Quotient;
-
-CallHangupEvent::CallHangupEvent(const QJsonObject& obj)
- : CallEventBase(typeId(), obj)
-{
- qCDebug(EVENTS) << "Call Hangup event";
-}
-
-CallHangupEvent::CallHangupEvent(const QString& callId)
- : CallEventBase(typeId(), matrixTypeId(), callId, 0)
-{}
diff --git a/lib/events/callhangupevent.h b/lib/events/callhangupevent.h
index 24382ac2..f3f82833 100644
--- a/lib/events/callhangupevent.h
+++ b/lib/events/callhangupevent.h
@@ -11,8 +11,12 @@ class CallHangupEvent : public CallEventBase {
public:
DEFINE_EVENT_TYPEID("m.call.hangup", CallHangupEvent)
- explicit CallHangupEvent(const QJsonObject& obj);
- explicit CallHangupEvent(const QString& callId);
+ explicit CallHangupEvent(const QJsonObject& obj)
+ : CallEventBase(typeId(), obj)
+ {}
+ explicit CallHangupEvent(const QString& callId)
+ : CallEventBase(typeId(), matrixTypeId(), callId, 0)
+ {}
};
REGISTER_EVENT_TYPE(CallHangupEvent)
diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h
index d3454c4f..80b7d651 100644
--- a/lib/events/callinviteevent.h
+++ b/lib/events/callinviteevent.h
@@ -18,11 +18,11 @@ public:
int lifetime() const
{
- return content<int>("lifetime"_ls);
+ return contentPart<int>("lifetime"_ls);
} // FIXME: Omittable<>?
QString sdp() const
{
- return contentJson()["offer"_ls].toObject().value("sdp"_ls).toString();
+ return contentPart<QJsonObject>("offer"_ls).value("sdp"_ls).toString();
}
};
diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h
index eb7123eb..de89a7c6 100644
--- a/lib/events/encryptedevent.h
+++ b/lib/events/encryptedevent.h
@@ -7,7 +7,6 @@
#include "roomevent.h"
namespace Quotient {
-class Room;
/*
* While the specification states:
*
@@ -27,7 +26,6 @@ class Room;
* one and doesn't add new restrictions, just provides additional features.
*/
class EncryptedEvent : public RoomEvent {
- Q_GADGET
public:
DEFINE_EVENT_TYPEID("m.room.encrypted", EncryptedEvent)
@@ -43,7 +41,7 @@ public:
QString algorithm() const
{
- QString algo = content<QString>(AlgorithmKeyL);
+ QString algo = contentPart<QString>(AlgorithmKeyL);
if (!SupportedAlgorithms.contains(algo)) {
qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo
<< "is not supported";
@@ -52,17 +50,17 @@ public:
}
QByteArray ciphertext() const
{
- return content<QString>(CiphertextKeyL).toLatin1();
+ return contentPart<QString>(CiphertextKeyL).toLatin1();
}
QJsonObject ciphertext(const QString& identityKey) const
{
- return content<QJsonObject>(CiphertextKeyL).value(identityKey).toObject();
+ return contentPart<QJsonObject>(CiphertextKeyL).value(identityKey).toObject();
}
- QString senderKey() const { return content<QString>(SenderKeyKeyL); }
+ QString senderKey() const { return contentPart<QString>(SenderKeyKeyL); }
/* device_id and session_id are required with Megolm */
- QString deviceId() const { return content<QString>(DeviceIdKeyL); }
- QString sessionId() const { return content<QString>(SessionIdKeyL); }
+ QString deviceId() const { return contentPart<QString>(DeviceIdKeyL); }
+ QString sessionId() const { return contentPart<QString>(SessionIdKeyL); }
};
REGISTER_EVENT_TYPE(EncryptedEvent)
diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h
new file mode 100644
index 00000000..24ac9de1
--- /dev/null
+++ b/lib/events/encryptedfile.h
@@ -0,0 +1,88 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPl-2.1-or-later
+
+#pragma once
+
+#include "converters.h"
+
+namespace Quotient {
+/**
+ * JSON Web Key object as specified in
+ * https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes
+ * The only currently relevant member is `k`, the rest needs to be set to the defaults specified in the spec.
+ */
+struct JWK
+{
+ Q_GADGET
+ Q_PROPERTY(QString kty MEMBER kty CONSTANT)
+ Q_PROPERTY(QStringList keyOps MEMBER keyOps CONSTANT)
+ Q_PROPERTY(QString alg MEMBER alg CONSTANT)
+ Q_PROPERTY(QString k MEMBER k CONSTANT)
+ Q_PROPERTY(bool ext MEMBER ext CONSTANT)
+
+public:
+ QString kty;
+ QStringList keyOps;
+ QString alg;
+ QString k;
+ bool ext;
+};
+
+struct EncryptedFile
+{
+ Q_GADGET
+ Q_PROPERTY(QUrl url MEMBER url CONSTANT)
+ Q_PROPERTY(JWK key MEMBER key CONSTANT)
+ Q_PROPERTY(QString iv MEMBER iv CONSTANT)
+ Q_PROPERTY(QHash<QString, QString> hashes MEMBER hashes CONSTANT)
+ Q_PROPERTY(QString v MEMBER v CONSTANT)
+
+public:
+ QUrl url;
+ JWK key;
+ QString iv;
+ QHash<QString, QString> hashes;
+ QString v;
+};
+
+template <>
+struct JsonObjectConverter<EncryptedFile> {
+ static void dumpTo(QJsonObject& jo, const EncryptedFile& pod)
+ {
+ addParam<>(jo, QStringLiteral("url"), pod.url);
+ addParam<>(jo, QStringLiteral("key"), pod.key);
+ addParam<>(jo, QStringLiteral("iv"), pod.iv);
+ addParam<>(jo, QStringLiteral("hashes"), pod.hashes);
+ addParam<>(jo, QStringLiteral("v"), pod.v);
+ }
+ static void fillFrom(const QJsonObject& jo, EncryptedFile& pod)
+ {
+ fromJson(jo.value("url"_ls), pod.url);
+ fromJson(jo.value("key"_ls), pod.key);
+ fromJson(jo.value("iv"_ls), pod.iv);
+ fromJson(jo.value("hashes"_ls), pod.hashes);
+ fromJson(jo.value("v"_ls), pod.v);
+ }
+};
+
+template <>
+struct JsonObjectConverter<JWK> {
+ static void dumpTo(QJsonObject& jo, const JWK& pod)
+ {
+ addParam<>(jo, QStringLiteral("kty"), pod.kty);
+ addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps);
+ addParam<>(jo, QStringLiteral("alg"), pod.alg);
+ addParam<>(jo, QStringLiteral("k"), pod.k);
+ addParam<>(jo, QStringLiteral("ext"), pod.ext);
+ }
+ static void fillFrom(const QJsonObject& jo, JWK& pod)
+ {
+ fromJson(jo.value("kty"_ls), pod.kty);
+ fromJson(jo.value("key_ops"_ls), pod.keyOps);
+ fromJson(jo.value("alg"_ls), pod.alg);
+ fromJson(jo.value("k"_ls), pod.k);
+ fromJson(jo.value("ext"_ls), pod.ext);
+ }
+};
+} // namespace Quotient
diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp
index 490a5e8a..aa05a96e 100644
--- a/lib/events/encryptionevent.cpp
+++ b/lib/events/encryptionevent.cpp
@@ -39,6 +39,14 @@ EncryptionEventContent::EncryptionEventContent(const QJsonObject& json)
, rotationPeriodMsgs(json[RotationPeriodMsgsKeyL].toInt(100))
{}
+EncryptionEventContent::EncryptionEventContent(EncryptionType et)
+ : encryption(et)
+{
+ if(encryption != Undefined) {
+ algorithm = encryptionStrings[encryption];
+ }
+}
+
void EncryptionEventContent::fillJson(QJsonObject* o) const
{
Q_ASSERT(o);
diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h
index f9bbab12..14439fcc 100644
--- a/lib/events/encryptionevent.h
+++ b/lib/events/encryptionevent.h
@@ -12,9 +12,7 @@ class EncryptionEventContent : public EventContent::Base {
public:
enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined };
- explicit EncryptionEventContent(EncryptionType et = Undefined)
- : encryption(et)
- {}
+ explicit EncryptionEventContent(EncryptionType et = Undefined);
explicit EncryptionEventContent(const QJsonObject& json);
EncryptionType encryption;
@@ -40,6 +38,7 @@ public:
// default value
: StateEvent(typeId(), obj)
{}
+ EncryptionEvent(EncryptionEvent&&) = delete;
template <typename... ArgTs>
EncryptionEvent(ArgTs&&... contentArgs)
: StateEvent(typeId(), matrixTypeId(), QString(),
diff --git a/lib/events/event.cpp b/lib/events/event.cpp
index 3d66ab55..96be717c 100644
--- a/lib/events/event.cpp
+++ b/lib/events/event.cpp
@@ -46,14 +46,11 @@ QString Event::matrixType() const { return fullJson()[TypeKeyL].toString(); }
QByteArray Event::originalJson() const { return QJsonDocument(_json).toJson(); }
-// On const below: this is to catch accidental attempts to change event JSON
-// NOLINTNEXTLINE(readability-const-return-type)
const QJsonObject Event::contentJson() const
{
return fullJson()[ContentKeyL].toObject();
}
-// NOLINTNEXTLINE(readability-const-return-type)
const QJsonObject Event::unsignedJson() const
{
return fullJson()[UnsignedKeyL].toObject();
diff --git a/lib/events/event.h b/lib/events/event.h
index f8f8311d..8f62872d 100644
--- a/lib/events/event.h
+++ b/lib/events/event.h
@@ -5,6 +5,7 @@
#include "converters.h"
#include "logging.h"
+#include "function_traits.h"
namespace Quotient {
// === event_ptr_tt<> and type casting facilities ===
@@ -28,31 +29,30 @@ inline TargetEventT* weakPtrCast(const event_ptr_tt<EventT>& ptr)
// === Standard Matrix key names and basicEventJson() ===
-static const auto TypeKey = QStringLiteral("type");
-static const auto BodyKey = QStringLiteral("body");
-static const auto ContentKey = QStringLiteral("content");
-static const auto EventIdKey = QStringLiteral("event_id");
-static const auto SenderKey = QStringLiteral("sender");
-static const auto RoomIdKey = QStringLiteral("room_id");
-static const auto UnsignedKey = QStringLiteral("unsigned");
-static const auto StateKeyKey = QStringLiteral("state_key");
-static const auto TypeKeyL = "type"_ls;
-static const auto BodyKeyL = "body"_ls;
-static const auto ContentKeyL = "content"_ls;
-static const auto EventIdKeyL = "event_id"_ls;
-static const auto SenderKeyL = "sender"_ls;
-static const auto RoomIdKeyL = "room_id"_ls;
-static const auto UnsignedKeyL = "unsigned"_ls;
-static const auto RedactedCauseKeyL = "redacted_because"_ls;
-static const auto PrevContentKeyL = "prev_content"_ls;
-static const auto StateKeyKeyL = "state_key"_ls;
+constexpr auto TypeKeyL = "type"_ls;
+constexpr auto BodyKeyL = "body"_ls;
+constexpr auto ContentKeyL = "content"_ls;
+constexpr auto EventIdKeyL = "event_id"_ls;
+constexpr auto SenderKeyL = "sender"_ls;
+constexpr auto RoomIdKeyL = "room_id"_ls;
+constexpr auto UnsignedKeyL = "unsigned"_ls;
+constexpr auto RedactedCauseKeyL = "redacted_because"_ls;
+constexpr auto PrevContentKeyL = "prev_content"_ls;
+constexpr auto StateKeyKeyL = "state_key"_ls;
+const QString TypeKey { TypeKeyL };
+const QString BodyKey { BodyKeyL };
+const QString ContentKey { ContentKeyL };
+const QString EventIdKey { EventIdKeyL };
+const QString SenderKey { SenderKeyL };
+const QString RoomIdKey { RoomIdKeyL };
+const QString UnsignedKey { UnsignedKeyL };
+const QString StateKeyKey { StateKeyKeyL };
/// Make a minimal correct Matrix event JSON
-template <typename StrT>
-inline QJsonObject basicEventJson(StrT matrixType, const QJsonObject& content)
+inline QJsonObject basicEventJson(const QString& matrixType,
+ const QJsonObject& content)
{
- return { { TypeKey, std::forward<StrT>(matrixType) },
- { ContentKey, content } };
+ return { { TypeKey, matrixType }, { ContentKey, content } };
}
// === Event types and event types registry ===
@@ -76,8 +76,7 @@ public:
private:
EventTypeRegistry() = default;
- Q_DISABLE_COPY(EventTypeRegistry)
- DISABLE_MOVE(EventTypeRegistry)
+ Q_DISABLE_COPY_MOVE(EventTypeRegistry)
static EventTypeRegistry& get()
{
@@ -111,97 +110,90 @@ inline event_type_t typeId()
inline event_type_t unknownEventTypeId() { return typeId<void>(); }
-// === EventFactory ===
+// === Event creation facilities ===
-/** Create an event of arbitrary type from its arguments */
+//! Create an event of arbitrary type from its arguments
template <typename EventT, typename... ArgTs>
inline event_ptr_tt<EventT> makeEvent(ArgTs&&... args)
{
return std::make_unique<EventT>(std::forward<ArgTs>(args)...);
}
-template <typename BaseEventT>
-class EventFactory {
-public:
- template <typename FnT>
- static auto addMethod(FnT&& method)
- {
- factories().emplace_back(std::forward<FnT>(method));
- return 0;
- }
-
- /** Chain two type factories
- * Adds the factory class of EventT2 (EventT2::factory_t) to
- * the list in factory class of EventT1 (EventT1::factory_t) so
- * that when EventT1::factory_t::make() is invoked, types of
- * EventT2 factory are looked through as well. This is used
- * to include RoomEvent types into the more general Event factory,
- * and state event types into the RoomEvent factory.
- */
- template <typename EventT>
- static auto chainFactory()
- {
- return addMethod(&EventT::factory_t::make);
- }
-
- static event_ptr_tt<BaseEventT> make(const QJsonObject& json,
- const QString& matrixType)
- {
- for (const auto& f : factories())
- if (auto e = f(json, matrixType))
- return e;
- return nullptr;
- }
-
-private:
- static auto& factories()
+namespace _impl {
+ template <class EventT, class BaseEventT>
+ event_ptr_tt<BaseEventT> makeIfMatches(const QJsonObject& json,
+ const QString& matrixType)
{
- using inner_factory_tt = std::function<event_ptr_tt<BaseEventT>(
- const QJsonObject&, const QString&)>;
- static std::vector<inner_factory_tt> _factories {};
- return _factories;
+ return QLatin1String(EventT::matrixTypeId()) == matrixType
+ ? makeEvent<EventT>(json)
+ : nullptr;
}
-};
-/** Add a type to its default factory
- * Adds a standard factory method (via makeEvent<>) for a given
- * type to EventT::factory_t factory class so that it can be
- * created dynamically from loadEvent<>().
- *
- * \tparam EventT the type to enable dynamic creation of
- * \return the registered type id
- * \sa loadEvent, Event::type
- */
-template <typename EventT>
-inline auto setupFactory()
-{
- qDebug(EVENTS) << "Adding factory method for" << EventT::matrixTypeId();
- return EventT::factory_t::addMethod([](const QJsonObject& json,
- const QString& jsonMatrixType) {
- return EventT::matrixTypeId() == jsonMatrixType ? makeEvent<EventT>(json)
- : nullptr;
- });
-}
-
-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
-}
+ //! \brief A family of event factories to create events from CS API responses
+ //!
+ //! Each of these factories, as instantiated by event base types (Event,
+ //! RoomEvent etc.) is capable of producing an event object derived from
+ //! \p BaseEventT, using the JSON payload and the event type passed to its
+ //! make() method. Don't use these directly to make events; use loadEvent()
+ //! overloads as the frontend for these. Never instantiate new factories
+ //! outside of base event classes.
+ //! \sa loadEvent, setupFactory, Event::factory, RoomEvent::factory,
+ //! StateEventBase::factory
+ template <typename BaseEventT>
+ class EventFactory
+ : private std::vector<event_ptr_tt<BaseEventT> (*)(const QJsonObject&,
+ const QString&)> {
+ // Actual makeIfMatches specialisations will differ in the first
+ // template parameter but that doesn't affect the function type
+ public:
+ explicit EventFactory(const char* name = "")
+ : name(name)
+ {
+ static auto yetToBeConstructed = true;
+ Q_ASSERT(yetToBeConstructed);
+ if (!yetToBeConstructed) // For Release builds that pass Q_ASSERT
+ qCritical(EVENTS)
+ << "Another EventFactory for the same base type is being "
+ "created - event creation logic will be splintered";
+ yetToBeConstructed = false;
+ }
+ EventFactory(const EventFactory&) = delete;
+
+ //! \brief Add a method to create events of a given type
+ //!
+ //! Adds a standard factory method (makeIfMatches) for \p EventT so that
+ //! event objects of this type can be created dynamically by loadEvent.
+ //! The caller is responsible for ensuring this method is called only
+ //! once per type.
+ //! \sa makeIfMatches, loadEvent, Quotient::loadEvent
+ template <class EventT>
+ bool addMethod()
+ {
+ this->emplace_back(&makeIfMatches<EventT, BaseEventT>);
+ qDebug(EVENTS) << "Added factory method for"
+ << EventT::matrixTypeId() << "events;" << this->size()
+ << "methods in the" << name << "chain by now";
+ return true;
+ }
+
+ auto loadEvent(const QJsonObject& json, const QString& matrixType)
+ {
+ for (const auto& f : *this)
+ if (auto e = f(json, matrixType))
+ return e;
+ return makeEvent<BaseEventT>(unknownEventTypeId(), json);
+ }
+
+ const char* const name;
+ };
+} // namespace _impl
// === Event ===
class Event {
- Q_GADGET
- Q_PROPERTY(Type type READ type CONSTANT)
- Q_PROPERTY(QJsonObject contentJson READ contentJson CONSTANT)
public:
using Type = event_type_t;
- using factory_t = EventFactory<Event>;
+ static inline _impl::EventFactory<Event> factory { "Event" };
explicit Event(Type type, const QJsonObject& json);
explicit Event(Type type, event_mtype_t matrixType,
@@ -213,7 +205,10 @@ public:
Type type() const { return _type; }
QString matrixType() const;
+ [[deprecated("Use fullJson() and stringify it with QJsonDocument::toJson() "
+ "or by other means")]]
QByteArray originalJson() const;
+ [[deprecated("Use fullJson() instead")]] //
QJsonObject originalJsonObject() const { return fullJson(); }
const QJsonObject& fullJson() const { return _json; }
@@ -222,19 +217,30 @@ public:
// a "content" object; but since its structure is different for
// different types, we're implementing it per-event type.
+ // NB: const return types below are meant to catch accidental attempts
+ // to change event JSON (e.g., consider contentJson()["inexistentKey"]).
+
const QJsonObject contentJson() const;
- const QJsonObject unsignedJson() const;
+
+ template <typename T = QJsonValue, typename KeyT>
+ const T contentPart(KeyT&& key) const
+ {
+ return fromJson<T>(contentJson()[std::forward<KeyT>(key)]);
+ }
template <typename T>
+ [[deprecated("Use contentPart() to get a part of the event content")]] //
T content(const QString& key) const
{
- return fromJson<T>(contentJson()[key]);
+ return contentPart<T>(key);
}
- template <typename T>
- T content(QLatin1String key) const
+ const QJsonObject unsignedJson() const;
+
+ template <typename T = QJsonValue, typename KeyT>
+ const T unsignedPart(KeyT&& key) const
{
- return fromJson<T>(contentJson()[key]);
+ return fromJson<T>(unsignedJson()[std::forward<KeyT>(key)]);
}
friend QDebug operator<<(QDebug dbg, const Event& e)
@@ -262,7 +268,7 @@ template <typename EventT>
using EventsArray = std::vector<event_ptr_tt<EventT>>;
using Events = EventsArray<Event>;
-// === Macros used with event class definitions ===
+// === Facilities for event class definitions ===
// This macro should be used in a public section of an event class to
// provide matrixTypeId() and typeId().
@@ -274,14 +280,28 @@ using Events = EventsArray<Event>;
// This macro should be put after an event class definition (in .h or .cpp)
// to enable its deserialisation from a /sync and other
// polymorphic event arrays
-#define REGISTER_EVENT_TYPE(_Type) \
- namespace { \
- [[maybe_unused]] static const auto _factoryAdded##_Type = \
- registerEventType<_Type>(); \
- } \
+#define REGISTER_EVENT_TYPE(_Type) \
+ [[maybe_unused]] inline const auto _factoryAdded##_Type = \
+ _Type::factory.addMethod<_Type>(); \
// End of macro
-// === is<>(), eventCast<>() and visit<>() ===
+// === Event loading ===
+// (see also event_loader.h)
+
+//! \brief Point of customisation to dynamically load events
+//!
+//! The default specialisation of this calls BaseEventT::factory and if that
+//! fails (i.e. returns nullptr) creates an unknown event of BaseEventT.
+//! Other specialisations may reuse other factories, add validations common to
+//! BaseEventT, and so on
+template <class BaseEventT>
+event_ptr_tt<BaseEventT> doLoadEvent(const QJsonObject& json,
+ const QString& matrixType)
+{
+ return BaseEventT::factory.loadEvent(json, matrixType);
+}
+
+// === is<>(), eventCast<>() and switchOnType<>() ===
template <class EventT>
inline bool is(const Event& e)
@@ -303,12 +323,12 @@ inline auto eventCast(const BasePtrT& eptr)
: nullptr;
}
-// A single generic catch-all visitor
+// A trivial generic catch-all "switch"
template <class BaseEventT, typename FnT>
-inline auto visit(const BaseEventT& event, FnT&& visitor)
- -> decltype(visitor(event))
+inline auto switchOnType(const BaseEventT& event, FnT&& fn)
+ -> decltype(fn(event))
{
- return visitor(event);
+ return fn(event);
}
namespace _impl {
@@ -319,52 +339,60 @@ namespace _impl {
&& !std::is_same_v<BaseT, std::decay_t<fn_arg_t<FnT>>>;
}
-// A single type-specific void visitor
+// A trivial type-specific "switch" for a void function
template <class BaseT, typename FnT>
-inline auto visit(const BaseT& event, FnT&& visitor)
+inline auto switchOnType(const BaseT& event, FnT&& fn)
-> std::enable_if_t<_impl::needs_downcast<BaseT, FnT>
&& std::is_void_v<fn_return_t<FnT>>>
{
using event_type = fn_arg_t<FnT>;
if (is<std::decay_t<event_type>>(event))
- visitor(static_cast<event_type>(event));
+ fn(static_cast<event_type>(event));
}
-// A single type-specific non-void visitor with an optional default value
-// non-voidness is guarded by defaultValue type
+// A trivial type-specific "switch" for non-void functions with an optional
+// default value; non-voidness is guarded by defaultValue type
template <class BaseT, typename FnT>
-inline auto visit(const BaseT& event, FnT&& visitor,
- fn_return_t<FnT>&& defaultValue = {})
+inline auto switchOnType(const BaseT& event, FnT&& fn,
+ fn_return_t<FnT>&& defaultValue = {})
-> std::enable_if_t<_impl::needs_downcast<BaseT, FnT>, fn_return_t<FnT>>
{
using event_type = fn_arg_t<FnT>;
if (is<std::decay_t<event_type>>(event))
- return visitor(static_cast<event_type>(event));
- return std::forward<fn_return_t<FnT>>(defaultValue);
+ return fn(static_cast<event_type>(event));
+ return std::move(defaultValue);
}
-// A chain of 2 or more visitors
+// A switch for a chain of 2 or more functions
template <class BaseT, typename FnT1, typename FnT2, typename... FnTs>
-inline std::common_type_t<fn_return_t<FnT1>, fn_return_t<FnT2>> visit(
- const BaseT& event, FnT1&& visitor1, FnT2&& visitor2,
- FnTs&&... visitors)
+inline std::common_type_t<fn_return_t<FnT1>, fn_return_t<FnT2>>
+switchOnType(const BaseT& event, FnT1&& fn1, FnT2&& fn2, FnTs&&... fns)
{
using event_type1 = fn_arg_t<FnT1>;
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)...);
+ return fn1(static_cast<event_type1&>(event));
+ return switchOnType(event, std::forward<FnT2>(fn2),
+ std::forward<FnTs>(fns)...);
+}
+
+template <class BaseT, typename... FnTs>
+[[deprecated("The new name for visit() is switchOnType()")]] //
+inline std::common_type_t<fn_return_t<FnTs>...>
+visit(const BaseT& event, FnTs&&... fns)
+{
+ return switchOnType(event, std::forward<FnTs>(fns)...);
}
-// A facility overload that calls void-returning visit() on each event
+ // A facility overload that calls void-returning switchOnType() on each event
// over a range of event pointers
+// TODO: replace with ranges::for_each once all standard libraries have it
template <typename RangeT, typename... FnTs>
-inline auto visitEach(RangeT&& events, FnTs&&... visitors)
+inline auto visitEach(RangeT&& events, FnTs&&... fns)
-> std::enable_if_t<std::is_void_v<
- decltype(visit(**begin(events), std::forward<FnTs>(visitors)...))>>
+ decltype(switchOnType(**begin(events), std::forward<FnTs>(fns)...))>>
{
for (auto&& evtPtr: events)
- visit(*evtPtr, std::forward<FnTs>(visitors)...);
+ switchOnType(*evtPtr, std::forward<FnTs>(fns)...);
}
} // namespace Quotient
Q_DECLARE_METATYPE(Quotient::Event*)
diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp
index b249b160..22878d4c 100644
--- a/lib/events/eventcontent.cpp
+++ b/lib/events/eventcontent.cpp
@@ -5,10 +5,13 @@
#include "converters.h"
#include "util.h"
+#include "logging.h"
#include <QtCore/QMimeDatabase>
+#include <QtCore/QFileInfo>
using namespace Quotient::EventContent;
+using std::move;
QJsonObject Base::toJson() const
{
@@ -17,23 +20,44 @@ QJsonObject Base::toJson() const
return o;
}
-FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType,
- const QString& originalFilename)
+FileInfo::FileInfo(const QFileInfo &fi)
+ : mimeType(QMimeDatabase().mimeTypeForFile(fi))
+ , url(QUrl::fromLocalFile(fi.filePath()))
+ , payloadSize(fi.size())
+ , originalName(fi.fileName())
+{
+ Q_ASSERT(fi.isFile());
+}
+
+FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType,
+ Omittable<EncryptedFile> file, QString originalFilename)
: mimeType(mimeType)
- , url(u)
+ , url(move(u))
, payloadSize(payloadSize)
- , originalName(originalFilename)
-{}
+ , originalName(move(originalFilename))
+ , file(file)
+{
+ if (!isValid())
+ qCWarning(MESSAGES)
+ << "To client developers: using FileInfo(QUrl, qint64, ...) "
+ "constructor for non-mxc resources is deprecated since Quotient "
+ "0.7; for local resources, use FileInfo(QFileInfo) instead";
+}
-FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson,
- const QString& originalFilename)
+FileInfo::FileInfo(QUrl mxcUrl, const QJsonObject& infoJson,
+ const Omittable<EncryptedFile> &file,
+ QString originalFilename)
: originalInfoJson(infoJson)
, mimeType(
QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString()))
- , url(u)
+ , url(move(mxcUrl))
, payloadSize(fromJson<qint64>(infoJson["size"_ls]))
- , originalName(originalFilename)
+ , originalName(move(originalFilename))
+ , file(file)
{
+ if(url.isEmpty() && file.has_value()) {
+ url = file->url;
+ }
if (!mimeType.isValid())
mimeType = QMimeDatabase().mimeTypeForData(QByteArray());
}
@@ -53,14 +77,20 @@ void FileInfo::fillInfoJson(QJsonObject* infoJson) const
infoJson->insert(QStringLiteral("mimetype"), mimeType.name());
}
-ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType,
- const QSize& imageSize, const QString& originalFilename)
- : FileInfo(u, fileSize, mimeType, originalFilename), imageSize(imageSize)
+ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize)
+ : FileInfo(fi), imageSize(imageSize)
+{}
+
+ImageInfo::ImageInfo(const QUrl& mxcUrl, qint64 fileSize, const QMimeType& type,
+ QSize imageSize, const Omittable<EncryptedFile> &file, const QString& originalFilename)
+ : FileInfo(mxcUrl, fileSize, type, file, originalFilename)
+ , imageSize(imageSize)
{}
-ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson,
+ImageInfo::ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson,
+ const Omittable<EncryptedFile> &file,
const QString& originalFilename)
- : FileInfo(u, infoJson, originalFilename)
+ : FileInfo(mxcUrl, infoJson, file, originalFilename)
, imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt())
{}
@@ -73,9 +103,10 @@ void ImageInfo::fillInfoJson(QJsonObject* infoJson) const
infoJson->insert(QStringLiteral("h"), imageSize.height());
}
-Thumbnail::Thumbnail(const QJsonObject& infoJson)
+Thumbnail::Thumbnail(const QJsonObject& infoJson, const Omittable<EncryptedFile> &file)
: ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()),
- infoJson["thumbnail_info"_ls].toObject())
+ infoJson["thumbnail_info"_ls].toObject(),
+ file)
{}
void Thumbnail::fillInfoJson(QJsonObject* infoJson) const
diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h
index 60d1f7b7..f609a603 100644
--- a/lib/events/eventcontent.h
+++ b/lib/events/eventcontent.h
@@ -12,6 +12,10 @@
#include <QtCore/QUrl>
#include <QtCore/QMetaType>
+#include "encryptedfile.h"
+
+class QFileInfo;
+
namespace Quotient {
namespace EventContent {
/**
@@ -47,13 +51,14 @@ namespace EventContent {
// but specific aggregation structure is altered. See doc comments to
// each type for the list of available attributes.
- // A quick classes inheritance structure follows:
+ // A quick classes inheritance structure follows (the definitions are
+ // spread across eventcontent.h and roommessageevent.h):
// FileInfo
- // FileContent : UrlBasedContent<FileInfo, Thumbnail>
- // AudioContent : UrlBasedContent<FileInfo, Duration>
+ // FileContent : UrlWithThumbnailContent<FileInfo>
+ // AudioContent : PlayableContent<UrlBasedContent<FileInfo>>
// ImageInfo : FileInfo + imageSize attribute
- // ImageContent : UrlBasedContent<ImageInfo, Thumbnail>
- // VideoContent : UrlBasedContent<ImageInfo, Thumbnail, Duration>
+ // ImageContent : UrlWithThumbnailContent<ImageInfo>
+ // VideoContent : PlayableContent<UrlWithThumbnailContent<ImageInfo>>
/**
* A base/mixin class for structures representing an "info" object for
@@ -73,11 +78,15 @@ namespace EventContent {
*/
class FileInfo {
public:
- explicit FileInfo(const QUrl& u, qint64 payloadSize = -1,
+ FileInfo() = default;
+ explicit FileInfo(const QFileInfo& fi);
+ explicit FileInfo(QUrl mxcUrl, qint64 payloadSize = -1,
const QMimeType& mimeType = {},
- const QString& originalFilename = {});
- FileInfo(const QUrl& u, const QJsonObject& infoJson,
- const QString& originalFilename = {});
+ Omittable<EncryptedFile> file = none,
+ QString originalFilename = {});
+ FileInfo(QUrl mxcUrl, const QJsonObject& infoJson,
+ const Omittable<EncryptedFile> &file,
+ QString originalFilename = {});
bool isValid() const;
@@ -98,6 +107,7 @@ namespace EventContent {
QUrl url;
qint64 payloadSize;
QString originalName;
+ Omittable<EncryptedFile> file = none;
};
template <typename InfoT>
@@ -113,10 +123,14 @@ namespace EventContent {
*/
class ImageInfo : public FileInfo {
public:
- explicit ImageInfo(const QUrl& u, qint64 fileSize = -1,
- QMimeType mimeType = {}, const QSize& imageSize = {},
+ ImageInfo() = default;
+ explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {});
+ explicit ImageInfo(const QUrl& mxcUrl, qint64 fileSize = -1,
+ const QMimeType& type = {}, QSize imageSize = {},
+ const Omittable<EncryptedFile> &file = none,
const QString& originalFilename = {});
- ImageInfo(const QUrl& u, const QJsonObject& infoJson,
+ ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson,
+ const Omittable<EncryptedFile> &encryptedFile,
const QString& originalFilename = {});
void fillInfoJson(QJsonObject* infoJson) const;
@@ -134,8 +148,8 @@ namespace EventContent {
*/
class Thumbnail : public ImageInfo {
public:
- Thumbnail() : ImageInfo(QUrl()) {} // To allow empty thumbnails
- Thumbnail(const QJsonObject& infoJson);
+ Thumbnail() = default; // Allow empty thumbnails
+ Thumbnail(const QJsonObject& infoJson, const Omittable<EncryptedFile> &file = none);
Thumbnail(const ImageInfo& info) : ImageInfo(info) {}
using ImageInfo::ImageInfo;
@@ -148,13 +162,13 @@ namespace EventContent {
class TypedBase : public Base {
public:
- explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {}
virtual QMimeType type() const = 0;
virtual const FileInfo* fileInfo() const { return nullptr; }
virtual FileInfo* fileInfo() { return nullptr; }
virtual const Thumbnail* thumbnailInfo() const { return nullptr; }
protected:
+ explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {}
using Base::Base;
};
@@ -175,7 +189,7 @@ namespace EventContent {
explicit UrlBasedContent(const QJsonObject& json)
: TypedBase(json)
, InfoT(QUrl(json["url"].toString()), json["info"].toObject(),
- json["filename"].toString())
+ fromJson<Omittable<EncryptedFile>>(json["file"]), json["filename"].toString())
{
// A small hack to facilitate links creation in QML.
originalJson.insert("mediaId", InfoT::mediaId());
@@ -189,7 +203,11 @@ namespace EventContent {
void fillJson(QJsonObject* json) const override
{
Q_ASSERT(json);
- json->insert("url", InfoT::url.toString());
+ if (!InfoT::file.has_value()) {
+ json->insert("url", InfoT::url.toString());
+ } else {
+ json->insert("file", Quotient::toJson(*InfoT::file));
+ }
if (!InfoT::originalName.isEmpty())
json->insert("filename", InfoT::originalName);
json->insert("info", toInfoJson<InfoT>(*this));
diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h
index 978668f2..fe624d70 100644
--- a/lib/events/eventloader.h
+++ b/lib/events/eventloader.h
@@ -6,16 +6,6 @@
#include "stateevent.h"
namespace Quotient {
-namespace _impl {
- template <typename BaseEventT>
- static inline auto loadEvent(const QJsonObject& json,
- const QString& matrixType)
- {
- if (auto e = EventFactory<BaseEventT>::make(json, matrixType))
- return e;
- return makeEvent<BaseEventT>(unknownEventTypeId(), json);
- }
-} // namespace _impl
/*! Create an event with proper type from a JSON object
*
@@ -26,7 +16,7 @@ namespace _impl {
template <typename BaseEventT>
inline event_ptr_tt<BaseEventT> loadEvent(const QJsonObject& fullJson)
{
- return _impl::loadEvent<BaseEventT>(fullJson, fullJson[TypeKeyL].toString());
+ return doLoadEvent<BaseEventT>(fullJson, fullJson[TypeKeyL].toString());
}
/*! Create an event from a type string and content JSON
@@ -39,8 +29,8 @@ template <typename BaseEventT>
inline event_ptr_tt<BaseEventT> loadEvent(const QString& matrixType,
const QJsonObject& content)
{
- return _impl::loadEvent<BaseEventT>(basicEventJson(matrixType, content),
- matrixType);
+ return doLoadEvent<BaseEventT>(basicEventJson(matrixType, content),
+ matrixType);
}
/*! Create a state event from a type string, content JSON and state key
@@ -53,7 +43,7 @@ inline StateEventPtr loadStateEvent(const QString& matrixType,
const QJsonObject& content,
const QString& stateKey = {})
{
- return _impl::loadEvent<StateEventBase>(
+ return doLoadEvent<StateEventBase>(
basicStateEventJson(matrixType, content, stateKey), matrixType);
}
diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h
index 777905f2..5a2b98c4 100644
--- a/lib/events/reactionevent.h
+++ b/lib/events/reactionevent.h
@@ -47,7 +47,7 @@ public:
explicit ReactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) {}
EventRelation relation() const
{
- return content<EventRelation>(QStringLiteral("m.relates_to"));
+ return contentPart<EventRelation>("m.relates_to"_ls);
}
};
REGISTER_EVENT_TYPE(ReactionEvent)
diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp
index 4185d92d..72dbf2e3 100644
--- a/lib/events/receiptevent.cpp
+++ b/lib/events/receiptevent.cpp
@@ -25,6 +25,27 @@ Example of a Receipt Event:
using namespace Quotient;
+// The library loads the event-ids-to-receipts JSON map into a vector because
+// map lookups are not used and vectors are massively faster. Same goes for
+// de-/serialization of ReceiptsForEvent::receipts.
+// (XXX: would this be generally preferred across CS API JSON maps?..)
+QJsonObject toJson(const EventsWithReceipts& ewrs)
+{
+ QJsonObject json;
+ for (const auto& e : ewrs) {
+ QJsonObject receiptsJson;
+ for (const auto& r : e.receipts)
+ receiptsJson.insert(r.userId,
+ QJsonObject { { "ts"_ls, toJson(r.timestamp) } });
+ json.insert(e.evtId, QJsonObject { { "m.read"_ls, receiptsJson } });
+ }
+ return json;
+}
+
+ReceiptEvent::ReceiptEvent(const EventsWithReceipts &ewrs)
+ : Event(typeId(), matrixTypeId(), toJson(ewrs))
+{}
+
EventsWithReceipts ReceiptEvent::eventsWithReceipts() const
{
EventsWithReceipts result;
@@ -39,14 +60,14 @@ EventsWithReceipts ReceiptEvent::eventsWithReceipts() const
}
const auto reads =
eventIt.value().toObject().value("m.read"_ls).toObject();
- QVector<Receipt> receipts;
- receipts.reserve(reads.size());
+ QVector<UserTimestamp> usersAtEvent;
+ usersAtEvent.reserve(reads.size());
for (auto userIt = reads.begin(); userIt != reads.end(); ++userIt) {
const auto user = userIt.value().toObject();
- receipts.push_back(
+ usersAtEvent.push_back(
{ userIt.key(), fromJson<QDateTime>(user["ts"_ls]) });
}
- result.push_back({ eventIt.key(), std::move(receipts) });
+ result.push_back({ eventIt.key(), std::move(usersAtEvent) });
}
return result;
}
diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h
index 4feec9ea..9683deef 100644
--- a/lib/events/receiptevent.h
+++ b/lib/events/receiptevent.h
@@ -9,19 +9,20 @@
#include <QtCore/QVector>
namespace Quotient {
-struct Receipt {
+struct UserTimestamp {
QString userId;
QDateTime timestamp;
};
struct ReceiptsForEvent {
QString evtId;
- QVector<Receipt> receipts;
+ QVector<UserTimestamp> receipts;
};
using EventsWithReceipts = QVector<ReceiptsForEvent>;
class ReceiptEvent : public Event {
public:
DEFINE_EVENT_TYPEID("m.receipt", ReceiptEvent)
+ explicit ReceiptEvent(const EventsWithReceipts& ewrs);
explicit ReceiptEvent(const QJsonObject& obj) : Event(typeId(), obj) {}
EventsWithReceipts eventsWithReceipts() const;
diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h
index ed560331..be20bf52 100644
--- a/lib/events/redactionevent.h
+++ b/lib/events/redactionevent.h
@@ -17,7 +17,7 @@ public:
{
return fullJson()["redacts"_ls].toString();
}
- QString reason() const { return contentJson()["reason"_ls].toString(); }
+ QString reason() const { return contentPart<QString>("reason"_ls); }
};
REGISTER_EVENT_TYPE(RedactionEvent)
} // namespace Quotient
diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h
index a4257895..8618ba31 100644
--- a/lib/events/roomavatarevent.h
+++ b/lib/events/roomavatarevent.h
@@ -20,12 +20,12 @@ public:
: StateEvent(typeId(), matrixTypeId(), QString(), avatar)
{}
// A replica of EventContent::ImageInfo constructor
- explicit RoomAvatarEvent(const QUrl& u, qint64 fileSize = -1,
+ explicit RoomAvatarEvent(const QUrl& mxcUrl, qint64 fileSize = -1,
QMimeType mimeType = {},
const QSize& imageSize = {},
const QString& originalFilename = {})
: RoomAvatarEvent(EventContent::ImageContent {
- u, fileSize, mimeType, imageSize, originalFilename })
+ mxcUrl, fileSize, mimeType, imageSize, none, originalFilename })
{}
QUrl url() const { return content().url; }
diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp
index 6558bade..bb6de648 100644
--- a/lib/events/roomcreateevent.cpp
+++ b/lib/events/roomcreateevent.cpp
@@ -5,19 +5,35 @@
using namespace Quotient;
+template <>
+struct Quotient::JsonConverter<RoomType> {
+ static RoomType load(const QJsonValue& jv)
+ {
+ const auto& roomTypeString = jv.toString();
+ for (auto it = RoomTypeStrings.begin(); it != RoomTypeStrings.end();
+ ++it)
+ if (roomTypeString == *it)
+ return RoomType(it - RoomTypeStrings.begin());
+
+ if (!roomTypeString.isEmpty())
+ qCWarning(EVENTS) << "Unknown Room Type: " << roomTypeString;
+ return RoomType::Undefined;
+ }
+};
+
bool RoomCreateEvent::isFederated() const
{
- return fromJson<bool>(contentJson()["m.federate"_ls]);
+ return contentPart<bool>("m.federate"_ls);
}
QString RoomCreateEvent::version() const
{
- return fromJson<QString>(contentJson()["room_version"_ls]);
+ return contentPart<QString>("room_version"_ls);
}
RoomCreateEvent::Predecessor RoomCreateEvent::predecessor() const
{
- const auto predJson = contentJson()["predecessor"_ls].toObject();
+ const auto predJson = contentPart<QJsonObject>("predecessor"_ls);
return { fromJson<QString>(predJson[RoomIdKeyL]),
fromJson<QString>(predJson[EventIdKeyL]) };
}
@@ -26,3 +42,8 @@ bool RoomCreateEvent::isUpgrade() const
{
return contentJson().contains("predecessor"_ls);
}
+
+RoomType RoomCreateEvent::roomType() const
+{
+ return contentPart<RoomType>("type"_ls);
+}
diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h
index 05e623ed..b3ad287c 100644
--- a/lib/events/roomcreateevent.h
+++ b/lib/events/roomcreateevent.h
@@ -4,6 +4,7 @@
#pragma once
#include "stateevent.h"
+#include "quotient_common.h"
namespace Quotient {
class RoomCreateEvent : public StateEventBase {
@@ -24,6 +25,7 @@ public:
QString version() const;
Predecessor predecessor() const;
bool isUpgrade() const;
+ RoomType roomType() const;
};
REGISTER_EVENT_TYPE(RoomCreateEvent)
} // namespace Quotient
diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp
index 4fec9d2b..b728e0bf 100644
--- a/lib/events/roomevent.cpp
+++ b/lib/events/roomevent.cpp
@@ -9,9 +9,6 @@
using namespace Quotient;
-[[maybe_unused]] static auto roomEventTypeInitialised =
- Event::factory_t::chainFactory<RoomEvent>();
-
RoomEvent::RoomEvent(Type type, event_mtype_t matrixType,
const QJsonObject& contentJson)
: Event(type, matrixType, contentJson)
@@ -19,7 +16,7 @@ RoomEvent::RoomEvent(Type type, event_mtype_t matrixType,
RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json)
{
- if (const auto redaction = unsignedJson()[RedactedCauseKeyL];
+ if (const auto redaction = unsignedPart(RedactedCauseKeyL);
redaction.isObject())
_redactedBecause = makeEvent<RedactionEvent>(redaction.toObject());
}
@@ -45,14 +42,14 @@ QString RoomEvent::senderId() const
bool RoomEvent::isReplaced() const
{
- return unsignedJson()["m.relations"_ls].toObject().contains("m.replace");
+ return unsignedPart<QJsonObject>("m.relations"_ls).contains("m.replace");
}
QString RoomEvent::replacedBy() const
{
// clang-format off
- return unsignedJson()["m.relations"_ls].toObject()
- .value("m.replace").toObject()
+ return unsignedPart<QJsonObject>("m.relations"_ls)
+ .value("m.replace"_ls).toObject()
.value(EventIdKeyL).toString();
// clang-format on
}
@@ -64,7 +61,7 @@ QString RoomEvent::redactionReason() const
QString RoomEvent::transactionId() const
{
- return unsignedJson()["transaction_id"_ls].toString();
+ return unsignedPart<QString>("transaction_id"_ls);
}
QString RoomEvent::stateKey() const
diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h
index fea509c0..8be58481 100644
--- a/lib/events/roomevent.h
+++ b/lib/events/roomevent.h
@@ -12,16 +12,8 @@ class RedactionEvent;
/** This class corresponds to m.room.* events */
class RoomEvent : public Event {
- Q_GADGET
- Q_PROPERTY(QString id READ id)
- Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT)
- Q_PROPERTY(QString roomId READ roomId CONSTANT)
- Q_PROPERTY(QString senderId READ senderId CONSTANT)
- Q_PROPERTY(QString redactionReason READ redactionReason)
- Q_PROPERTY(bool isRedacted READ isRedacted)
- Q_PROPERTY(QString transactionId READ transactionId WRITE setTransactionId)
public:
- using factory_t = EventFactory<RoomEvent>;
+ static inline _impl::EventFactory<RoomEvent> factory { "RoomEvent" };
// RedactionEvent is an incomplete type here so we cannot inline
// constructors and destructors and we cannot use 'using'.
@@ -32,11 +24,12 @@ public:
QString id() const;
QDateTime originTimestamp() const;
- [[deprecated("Use originTimestamp()")]] QDateTime timestamp() const {
- return originTimestamp();
- }
QString roomId() const;
QString senderId() const;
+ //! \brief Determine whether the event has been replaced
+ //!
+ //! \return true if this event has been overridden by another event
+ //! with `"rel_type": "m.replace"`; false otherwise
bool isReplaced() const;
QString replacedBy() const;
bool isRedacted() const { return bool(_redactedBecause); }
@@ -48,28 +41,23 @@ public:
QString transactionId() const;
QString stateKey() const;
+ //! \brief Fill the pending event object with the room id
void setRoomId(const QString& roomId);
+ //! \brief Fill the pending event object with the sender id
void setSender(const QString& senderId);
-
- /**
- * Sets the transaction id for locally created events. This should be
- * done before the event is exposed to any code using the respective
- * Q_PROPERTY.
- *
- * \param txnId - transaction id, normally obtained from
- * Connection::generateTxnId()
- */
+ //! \brief Fill the pending event object with the transaction id
+ //! \param txnId - transaction id, normally obtained from
+ //! Connection::generateTxnId()
void setTransactionId(const QString& txnId);
- /**
- * Sets event id for locally created events
- *
- * When a new event is created locally, it has no server id yet.
- * This function allows to add the id once the confirmation from
- * the server is received. There should be no id set previously
- * in the event. It's the responsibility of the code calling addId()
- * to notify clients that use Q_PROPERTY(id) about its change
- */
+ //! \brief Add an event id to locally created events after they are sent
+ //!
+ //! When a new event is created locally, it has no id; the homeserver
+ //! assigns it once the event is sent. This function allows to add the id
+ //! once the confirmation from the server is received. There should be no id
+ //! set previously in the event. It's the responsibility of the code calling
+ //! addId() to notify clients about the change; there's no signal or
+ //! callback for that in RoomEvent.
void addId(const QString& newId);
protected:
@@ -90,8 +78,8 @@ public:
~CallEventBase() override = default;
bool isCallEvent() const override { return true; }
- QString callId() const { return content<QString>("call_id"_ls); }
- int version() const { return content<int>("version"_ls); }
+ QString callId() const { return contentPart<QString>("call_id"_ls); }
+ int version() const { return contentPart<int>("version"_ls); }
};
} // namespace Quotient
Q_DECLARE_METATYPE(Quotient::RoomEvent*)
diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h
index 14e80324..d021fbec 100644
--- a/lib/events/roomkeyevent.h
+++ b/lib/events/roomkeyevent.h
@@ -13,10 +13,10 @@ public:
explicit RoomKeyEvent(const QJsonObject& obj);
- QString algorithm() const { return content<QString>("algorithm"_ls); }
- QString roomId() const { return content<QString>(RoomIdKeyL); }
- QString sessionId() const { return content<QString>("session_id"_ls); }
- QString sessionKey() const { return content<QString>("session_key"_ls); }
+ QString algorithm() const { return contentPart<QString>("algorithm"_ls); }
+ QString roomId() const { return contentPart<QString>(RoomIdKeyL); }
+ QString sessionId() const { return contentPart<QString>("session_id"_ls); }
+ QString sessionKey() const { return contentPart<QString>("session_key"_ls); }
};
REGISTER_EVENT_TYPE(RoomKeyEvent)
} // namespace Quotient
diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp
index 9634ca3a..3141f6b5 100644
--- a/lib/events/roommemberevent.cpp
+++ b/lib/events/roommemberevent.cpp
@@ -7,27 +7,26 @@
#include "converters.h"
#include "logging.h"
-#include <array>
-
-static const std::array<QString, 5> membershipStrings = {
- { QStringLiteral("invite"), QStringLiteral("join"), QStringLiteral("knock"),
- QStringLiteral("leave"), QStringLiteral("ban") }
-};
+#include <QtCore/QtAlgorithms>
namespace Quotient {
template <>
-struct JsonConverter<MembershipType> {
- static MembershipType load(const QJsonValue& jv)
+struct JsonConverter<Membership> {
+ static Membership load(const QJsonValue& jv)
{
- const auto& membershipString = jv.toString();
- for (auto it = membershipStrings.begin(); it != membershipStrings.end();
- ++it)
- if (membershipString == *it)
- return MembershipType(it - membershipStrings.begin());
-
- if (!membershipString.isEmpty())
- qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString;
- return MembershipType::Undefined;
+ const auto& ms = jv.toString();
+ if (ms.isEmpty())
+ {
+ qCWarning(EVENTS) << "Empty membership state";
+ return Membership::Invalid;
+ }
+ const auto it =
+ std::find(MembershipStrings.begin(), MembershipStrings.end(), ms);
+ if (it != MembershipStrings.end())
+ return Membership(1U << (it - MembershipStrings.begin()));
+
+ qCWarning(EVENTS) << "Unknown Membership value: " << ms;
+ return Membership::Invalid;
}
};
} // namespace Quotient
@@ -35,7 +34,7 @@ struct JsonConverter<MembershipType> {
using namespace Quotient;
MemberEventContent::MemberEventContent(const QJsonObject& json)
- : membership(fromJson<MembershipType>(json["membership"_ls]))
+ : membership(fromJson<Membership>(json["membership"_ls]))
, isDirect(json["is_direct"_ls].toBool())
, displayName(fromJson<Omittable<QString>>(json["displayname"_ls]))
, avatarUrl(fromJson<Omittable<QString>>(json["avatar_url"_ls]))
@@ -48,10 +47,10 @@ MemberEventContent::MemberEventContent(const QJsonObject& json)
void MemberEventContent::fillJson(QJsonObject* o) const
{
Q_ASSERT(o);
- Q_ASSERT_X(membership != MembershipType::Undefined, __FUNCTION__,
- "The key 'membership' must be explicit in MemberEventContent");
- if (membership != MembershipType::Undefined)
- o->insert(QStringLiteral("membership"), membershipStrings[membership]);
+ if (membership != Membership::Invalid)
+ o->insert(QStringLiteral("membership"),
+ MembershipStrings[qCountTrailingZeroBits(
+ std::underlying_type_t<Membership>(membership))]);
if (displayName)
o->insert(QStringLiteral("displayname"), *displayName);
if (avatarUrl && avatarUrl->isValid())
@@ -67,51 +66,49 @@ bool RoomMemberEvent::changesMembership() const
bool RoomMemberEvent::isInvite() const
{
- return membership() == MembershipType::Invite && changesMembership();
+ return membership() == Membership::Invite && changesMembership();
}
bool RoomMemberEvent::isRejectedInvite() const
{
- return membership() == MembershipType::Leave && prevContent()
- && prevContent()->membership == MembershipType::Invite;
+ return membership() == Membership::Leave && prevContent()
+ && prevContent()->membership == Membership::Invite;
}
bool RoomMemberEvent::isJoin() const
{
- return membership() == MembershipType::Join && changesMembership();
+ return membership() == Membership::Join && changesMembership();
}
bool RoomMemberEvent::isLeave() const
{
- return membership() == MembershipType::Leave && prevContent()
+ return membership() == Membership::Leave && prevContent()
&& prevContent()->membership != membership()
- && prevContent()->membership != MembershipType::Ban
- && prevContent()->membership != MembershipType::Invite;
+ && prevContent()->membership != Membership::Ban
+ && prevContent()->membership != Membership::Invite;
}
bool RoomMemberEvent::isBan() const
{
- return membership() == MembershipType::Ban && changesMembership();
+ return membership() == Membership::Ban && changesMembership();
}
bool RoomMemberEvent::isUnban() const
{
- return membership() == MembershipType::Leave && prevContent()
- && prevContent()->membership == MembershipType::Ban;
+ return membership() == Membership::Leave && prevContent()
+ && prevContent()->membership == Membership::Ban;
}
bool RoomMemberEvent::isRename() const
{
- auto prevName = prevContent() && prevContent()->displayName
- ? *prevContent()->displayName
- : QString();
- return newDisplayName() != prevName;
+ return prevContent() && prevContent()->displayName
+ ? newDisplayName() != *prevContent()->displayName
+ : newDisplayName().has_value();
}
bool RoomMemberEvent::isAvatarUpdate() const
{
- auto prevAvatarUrl = prevContent() && prevContent()->avatarUrl
- ? *prevContent()->avatarUrl
- : QUrl();
- return newAvatarUrl() != prevAvatarUrl;
+ return prevContent() && prevContent()->avatarUrl
+ ? newAvatarUrl() != *prevContent()->avatarUrl
+ : newAvatarUrl().has_value();
}
diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h
index f2fbe689..0fb464d4 100644
--- a/lib/events/roommemberevent.h
+++ b/lib/events/roommemberevent.h
@@ -7,23 +7,21 @@
#include "eventcontent.h"
#include "stateevent.h"
+#include "quotient_common.h"
namespace Quotient {
class MemberEventContent : public EventContent::Base {
public:
- enum MembershipType : unsigned char {
- Invite = 0,
- Join,
- Knock,
- Leave,
- Ban,
- Undefined
- };
+ using MembershipType
+ [[deprecated("Use Quotient::Membership instead")]] = Membership;
- explicit MemberEventContent(MembershipType mt = Join) : membership(mt) {}
+ explicit MemberEventContent(Membership ms = Membership::Join)
+ : membership(ms)
+ {}
explicit MemberEventContent(const QJsonObject& json);
- MembershipType membership;
+ Membership membership;
+ /// (Only for invites) Whether the invite is to a direct chat
bool isDirect = false;
Omittable<QString> displayName;
Omittable<QUrl> avatarUrl;
@@ -33,15 +31,15 @@ protected:
void fillJson(QJsonObject* o) const override;
};
-using MembershipType = MemberEventContent::MembershipType;
+using MembershipType [[deprecated("Use Membership instead")]] = Membership;
class RoomMemberEvent : public StateEvent<MemberEventContent> {
Q_GADGET
public:
DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent)
- using MembershipType = MemberEventContent::MembershipType;
- Q_ENUM(MembershipType)
+ using MembershipType
+ [[deprecated("Use Quotient::Membership instead")]] = Membership;
explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj)
{}
@@ -51,21 +49,20 @@ public:
std::forward<ArgTs>(contentArgs)...)
{}
- /// A special constructor to create unknown RoomMemberEvents
- /**
- * This is needed in order to use RoomMemberEvent as a "base event
- * class" in cases like GetMembersByRoomJob when RoomMemberEvents
- * (rather than RoomEvents or StateEvents) are resolved from JSON.
- * For such cases loadEvent<> requires an underlying class to be
- * constructible with unknownTypeId() instead of its genuine id.
- * Don't use it directly.
- * \sa GetMembersByRoomJob, loadEvent, unknownTypeId
- */
+ //! \brief A special constructor to create unknown RoomMemberEvents
+ //!
+ //! This is needed in order to use RoomMemberEvent as a "base event class"
+ //! in cases like GetMembersByRoomJob when RoomMemberEvents (rather than
+ //! RoomEvents or StateEvents) are resolved from JSON. For such cases
+ //! loadEvent\<> requires an underlying class to have a specialisation of
+ //! EventFactory\<> and be constructible with unknownTypeId() instead of
+ //! its genuine id. Don't use directly.
+ //! \sa EventFactory, loadEvent, GetMembersByRoomJob
RoomMemberEvent(Type type, const QJsonObject& fullJson)
: StateEvent(type, fullJson)
{}
- MembershipType membership() const { return content().membership; }
+ Membership membership() const { return content().membership; }
QString userId() const { return stateKey(); }
bool isDirect() const { return content().isDirect; }
Omittable<QString> newDisplayName() const { return content().displayName; }
@@ -91,14 +88,13 @@ public:
};
template <>
-class EventFactory<RoomMemberEvent> {
-public:
- static event_ptr_tt<RoomMemberEvent> make(const QJsonObject& json,
- const QString&)
- {
+inline event_ptr_tt<RoomMemberEvent>
+doLoadEvent<RoomMemberEvent>(const QJsonObject& json, const QString& matrixType)
+{
+ if (matrixType == QLatin1String(RoomMemberEvent::matrixTypeId()))
return makeEvent<RoomMemberEvent>(json);
- }
-};
+ return makeEvent<RoomMemberEvent>(unknownEventTypeId(), json);
+}
REGISTER_EVENT_TYPE(RoomMemberEvent)
} // namespace Quotient
diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp
index 31c0fd9e..2b7b4166 100644
--- a/lib/events/roommessageevent.cpp
+++ b/lib/events/roommessageevent.cpp
@@ -10,7 +10,9 @@
#include <QtCore/QFileInfo>
#include <QtCore/QMimeDatabase>
#include <QtGui/QImageReader>
-#include <QtMultimedia/QMediaResource>
+#if QT_VERSION_MAJOR < 6
+# include <QtMultimedia/QMediaResource>
+#endif
using namespace Quotient;
using namespace EventContent;
@@ -133,6 +135,7 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody, MsgType msgType,
: RoomMessageEvent(plainBody, msgTypeToJson(msgType), content)
{}
+#if QT_VERSION_MAJOR < 6
TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile)
{
auto filePath = file.absoluteFilePath();
@@ -142,21 +145,21 @@ TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile)
auto mimeTypeName = mimeType.name();
if (mimeTypeName.startsWith("image/"))
return new ImageContent(localUrl, file.size(), mimeType,
- QImageReader(filePath).size(),
+ QImageReader(filePath).size(), none,
file.fileName());
// duration can only be obtained asynchronously and can only be reliably
// done by starting to play the file. Left for a future implementation.
if (mimeTypeName.startsWith("video/"))
return new VideoContent(localUrl, file.size(), mimeType,
- QMediaResource(localUrl).resolution(),
+ QMediaResource(localUrl).resolution(), none,
file.fileName());
if (mimeTypeName.startsWith("audio/"))
- return new AudioContent(localUrl, file.size(), mimeType,
+ return new AudioContent(localUrl, file.size(), mimeType, none,
file.fileName());
}
- return new FileContent(localUrl, file.size(), mimeType, file.fileName());
+ return new FileContent(localUrl, file.size(), mimeType, none, file.fileName());
}
RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
@@ -166,6 +169,7 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
: rawMsgTypeForFile(file),
contentFromFile(file, asGenericFile))
{}
+#endif
RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj)
: RoomEvent(typeId(), obj), _content(nullptr)
@@ -200,12 +204,12 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const
QString RoomMessageEvent::rawMsgtype() const
{
- return contentJson()[MsgTypeKeyL].toString();
+ return contentPart<QString>(MsgTypeKeyL);
}
QString RoomMessageEvent::plainBody() const
{
- return contentJson()[BodyKeyL].toString();
+ return contentPart<QString>(BodyKeyL);
}
QMimeType RoomMessageEvent::mimeType() const
diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h
index 8303ce4e..56597ddc 100644
--- a/lib/events/roommessageevent.h
+++ b/lib/events/roommessageevent.h
@@ -18,10 +18,6 @@ namespace MessageEventContent = EventContent; // Back-compatibility
*/
class RoomMessageEvent : public RoomEvent {
Q_GADGET
- Q_PROPERTY(QString msgType READ rawMsgtype CONSTANT)
- Q_PROPERTY(QString plainBody READ plainBody CONSTANT)
- Q_PROPERTY(QMimeType mimeType READ mimeType STORED false CONSTANT)
- Q_PROPERTY(const EventContent::TypedBase* content READ content CONSTANT)
public:
DEFINE_EVENT_TYPEID("m.room.message", RoomMessageEvent)
@@ -42,8 +38,12 @@ public:
explicit RoomMessageEvent(const QString& plainBody,
MsgType msgType = MsgType::Text,
EventContent::TypedBase* content = nullptr);
+#if QT_VERSION_MAJOR < 6
+ [[deprecated("Create an EventContent object on the client side"
+ " and pass it to other constructors")]] //
explicit RoomMessageEvent(const QString& plainBody, const QFileInfo& file,
bool asGenericFile = false);
+#endif
explicit RoomMessageEvent(const QJsonObject& obj);
MsgType msgtype() const;
@@ -58,9 +58,26 @@ public:
_content.data());
}
QMimeType mimeType() const;
+ //! \brief Determine whether the message has text content
+ //!
+ //! \return true, if the message type is one of m.text, m.notice, m.emote,
+ //! or the message type is unspecified (in which case plainBody()
+ //! can still be examined); false otherwise
bool hasTextContent() const;
+ //! \brief Determine whether the message has a file/attachment
+ //!
+ //! \return true, if the message has a data structure corresponding to
+ //! a file (such as m.file or m.audio); false otherwise
bool hasFileContent() const;
+ //! \brief Determine whether the message has a thumbnail
+ //!
+ //! \return true, if the message has a data structure corresponding to
+ //! a thumbnail (the message type may be one for visual content,
+ //! such as m.image, or generic binary content, i.e. m.file);
+ //! false otherwise
bool hasThumbnail() const;
+ //! \brief Obtain id of an event replaced by the current one
+ //! \sa RoomEvent::isReplaced, RoomEvent::replacedBy
QString replacedEvent() const;
static QString rawMsgTypeForUrl(const QUrl& url);
diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp
index 080d269c..2c3492d6 100644
--- a/lib/events/roomtombstoneevent.cpp
+++ b/lib/events/roomtombstoneevent.cpp
@@ -7,10 +7,10 @@ using namespace Quotient;
QString RoomTombstoneEvent::serverMessage() const
{
- return fromJson<QString>(contentJson()["body"_ls]);
+ return contentPart<QString>("body"_ls);
}
QString RoomTombstoneEvent::successorRoomId() const
{
- return fromJson<QString>(contentJson()["replacement_room"_ls]);
+ return contentPart<QString>("replacement_room"_ls);
}
diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h
index c977cb6e..13597979 100644
--- a/lib/events/simplestateevents.h
+++ b/lib/events/simplestateevents.h
@@ -8,8 +8,7 @@
namespace Quotient {
namespace EventContent {
template <typename T>
- class SimpleContent {
- public:
+ struct SimpleContent {
using value_type = T;
// The constructor is templated to enable perfect forwarding
@@ -25,11 +24,8 @@ namespace EventContent {
return { { key, Quotient::toJson(value) } };
}
- public:
T value;
-
- protected:
- QString key;
+ const QString key;
};
} // namespace EventContent
@@ -57,19 +53,16 @@ DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name)
DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic)
DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", QStringList, pinnedEvents)
-class RoomAliasesEvent
- : public StateEvent<EventContent::SimpleContent<QStringList>> {
+class [[deprecated(
+ "m.room.aliases events are deprecated by the Matrix spec; use"
+ " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases")]] //
+RoomAliasesEvent : public StateEvent<EventContent::SimpleContent<QStringList>> {
public:
DEFINE_EVENT_TYPEID("m.room.aliases", RoomAliasesEvent)
explicit RoomAliasesEvent(const QJsonObject& obj)
: StateEvent(typeId(), obj, QStringLiteral("aliases"))
{}
- RoomAliasesEvent(const QString& server, const QStringList& aliases)
- : StateEvent(typeId(), matrixTypeId(), server,
- QStringLiteral("aliases"), aliases)
- {}
QString server() const { return stateKey(); }
QStringList aliases() const { return content().value; }
};
-REGISTER_EVENT_TYPE(RoomAliasesEvent)
} // namespace Quotient
diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp
index 42fc9054..e53d47d4 100644
--- a/lib/events/stateevent.cpp
+++ b/lib/events/stateevent.cpp
@@ -5,20 +5,13 @@
using namespace Quotient;
-// Aside from the normal factory to instantiate StateEventBase inheritors
-// StateEventBase itself can be instantiated if there's a state_key JSON key
-// but the event type is unknown.
-[[maybe_unused]] static auto stateEventTypeInitialised =
- RoomEvent::factory_t::addMethod(
- [](const QJsonObject& json, const QString& matrixType) -> StateEventPtr {
- if (!json.contains(StateKeyKeyL))
- return nullptr;
-
- if (auto e = StateEventBase::factory_t::make(json, matrixType))
- return e;
-
- return makeEvent<StateEventBase>(unknownEventTypeId(), json);
- });
+StateEventBase::StateEventBase(Type type, const QJsonObject& json)
+ : RoomEvent(json.contains(StateKeyKeyL) ? type : unknownEventTypeId(), json)
+{
+ if (Event::type() == unknownEventTypeId() && !json.contains(StateKeyKeyL))
+ qWarning(EVENTS) << "Attempt to create a state event with no stateKey -"
+ "forcing the event type to unknown to avoid damage";
+}
StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType,
const QString& stateKey,
@@ -28,22 +21,22 @@ StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType,
bool StateEventBase::repeatsState() const
{
- const auto prevContentJson = unsignedJson().value(PrevContentKeyL);
+ const auto prevContentJson = unsignedPart(PrevContentKeyL);
return fullJson().value(ContentKeyL) == prevContentJson;
}
QString StateEventBase::replacedState() const
{
- return unsignedJson().value("replaces_state"_ls).toString();
+ return unsignedPart<QString>("replaces_state"_ls);
}
void StateEventBase::dumpTo(QDebug dbg) const
{
if (!stateKey().isEmpty())
dbg << '<' << stateKey() << "> ";
- if (unsignedJson().contains(PrevContentKeyL))
- dbg << QJsonDocument(unsignedJson()[PrevContentKeyL].toObject())
- .toJson(QJsonDocument::Compact)
+ if (const auto prevContentJson = unsignedPart<QJsonObject>(PrevContentKeyL);
+ !prevContentJson.isEmpty())
+ dbg << QJsonDocument(prevContentJson).toJson(QJsonDocument::Compact)
<< " -> ";
RoomEvent::dumpTo(dbg);
}
diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h
index 1415f709..c37965aa 100644
--- a/lib/events/stateevent.h
+++ b/lib/events/stateevent.h
@@ -18,13 +18,10 @@ inline QJsonObject basicStateEventJson(const QString& matrixTypeId,
}
class StateEventBase : public RoomEvent {
- Q_GADGET
- Q_PROPERTY(QString stateKey READ stateKey CONSTANT)
public:
- using factory_t = EventFactory<StateEventBase>;
+ static inline _impl::EventFactory<StateEventBase> factory { "StateEvent" };
- StateEventBase(Type type, const QJsonObject& json) : RoomEvent(type, json)
- {}
+ StateEventBase(Type type, const QJsonObject& json);
StateEventBase(Type type, event_mtype_t matrixType,
const QString& stateKey = {},
const QJsonObject& contentJson = {});
@@ -39,6 +36,22 @@ public:
using StateEventPtr = event_ptr_tt<StateEventBase>;
using StateEvents = EventsArray<StateEventBase>;
+//! \brief Override RoomEvent factory with that from StateEventBase if JSON has
+//! stateKey
+//!
+//! This means in particular that an event with a type known to RoomEvent but
+//! having stateKey set (even to an empty value) will be treated as a state
+//! event and most likely end up as unknown (consider, e.g., m.room.message
+//! that has stateKey set).
+template <>
+inline RoomEventPtr doLoadEvent(const QJsonObject& json,
+ const QString& matrixType)
+{
+ if (json.contains(StateKeyKeyL))
+ return StateEventBase::factory.loadEvent(json, matrixType);
+ return RoomEvent::factory.loadEvent(json, matrixType);
+}
+
template <>
inline bool is<StateEventBase>(const Event& e)
{
@@ -100,10 +113,6 @@ public:
visitor(_content);
editJson()[ContentKeyL] = _content.toJson();
}
- [[deprecated("Use prevContent instead")]] const ContentT* prev_content() const
- {
- return prevContent();
- }
const ContentT* prevContent() const
{
return _prev ? &_prev->content : nullptr;
diff --git a/lib/events/stickerevent.cpp b/lib/events/stickerevent.cpp
index ea4dff3f..628fd154 100644
--- a/lib/events/stickerevent.cpp
+++ b/lib/events/stickerevent.cpp
@@ -12,7 +12,7 @@ StickerEvent::StickerEvent(const QJsonObject &obj)
QString StickerEvent::body() const
{
- return content<QString>("body"_ls);
+ return contentPart<QString>("body"_ls);
}
const EventContent::ImageContent &StickerEvent::image() const
diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp
index e97e978f..7e5d7ee6 100644
--- a/lib/events/typingevent.cpp
+++ b/lib/events/typingevent.cpp
@@ -7,5 +7,5 @@ using namespace Quotient;
QStringList TypingEvent::users() const
{
- return fromJson<QStringList>(contentJson()["user_ids"_ls]);
+ return contentPart<QStringList>("user_ids"_ls);
}
diff --git a/lib/eventstats.cpp b/lib/eventstats.cpp
new file mode 100644
index 00000000..9fa7f5ff
--- /dev/null
+++ b/lib/eventstats.cpp
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: 2021 Quotient contributors
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "eventstats.h"
+
+using namespace Quotient;
+
+EventStats EventStats::fromRange(const Room* room, const Room::rev_iter_t& from,
+ const Room::rev_iter_t& to,
+ const EventStats& init)
+{
+ Q_ASSERT(to <= room->historyEdge());
+ Q_ASSERT(from >= Room::rev_iter_t(room->syncEdge()));
+ Q_ASSERT(from <= to);
+ QElapsedTimer et;
+ et.start();
+ const auto result =
+ accumulate(from, to, init,
+ [room](EventStats acc, const TimelineItem& ti) {
+ acc.notableCount += room->isEventNotable(ti);
+ acc.highlightCount += room->notificationFor(ti).type
+ == Notification::Highlight;
+ return acc;
+ });
+ if (et.nsecsElapsed() > profilerMinNsecs() / 10)
+ qCDebug(PROFILER).nospace()
+ << "Event statistics collection over index range [" << from->index()
+ << "," << (to - 1)->index() << "] took " << et;
+ return result;
+}
+
+EventStats EventStats::fromMarker(const Room* room,
+ const EventStats::marker_t& marker)
+{
+ const auto s = fromRange(room, marker_t(room->syncEdge()), marker,
+ { 0, 0, marker == room->historyEdge() });
+ Q_ASSERT(s.isValidFor(room, marker));
+ return s;
+}
+
+EventStats EventStats::fromCachedCounters(Omittable<int> notableCount,
+ Omittable<int> highlightCount)
+{
+ const auto hCount = std::max(0, highlightCount.value_or(0));
+ if (!notableCount.has_value())
+ return { 0, hCount, true };
+ auto nCount = notableCount.value_or(0);
+ return { std::max(0, nCount), hCount, nCount != -1 };
+}
+
+bool EventStats::updateOnMarkerMove(const Room* room, const marker_t& oldMarker,
+ const marker_t& newMarker)
+{
+ if (newMarker == oldMarker)
+ return false;
+
+ // Double-check consistency between the old marker and the old stats
+ Q_ASSERT(isValidFor(room, oldMarker));
+ Q_ASSERT(oldMarker > newMarker);
+
+ // A bit of optimisation: only calculate the difference if the marker moved
+ // less than half the remaining timeline ahead; otherwise, recalculation
+ // over the remaining timeline will very likely be faster.
+ if (oldMarker != room->historyEdge()
+ && oldMarker - newMarker < newMarker - marker_t(room->syncEdge())) {
+ const auto removedStats = fromRange(room, newMarker, oldMarker);
+ Q_ASSERT(notableCount >= removedStats.notableCount
+ && highlightCount >= removedStats.highlightCount);
+ notableCount -= removedStats.notableCount;
+ highlightCount -= removedStats.highlightCount;
+ return removedStats.notableCount > 0 || removedStats.highlightCount > 0;
+ }
+
+ const auto newStats = EventStats::fromMarker(room, newMarker);
+ if (!isEstimate && newStats == *this)
+ return false;
+ *this = newStats;
+ return true;
+}
+
+bool EventStats::isValidFor(const Room* room, const marker_t& marker) const
+{
+ const auto markerAtHistoryEdge = marker == room->historyEdge();
+ // Either markerAtHistoryEdge and isEstimate are in the same state, or it's
+ // a special case of no notable events and the marker at history edge
+ // (then isEstimate can assume any value).
+ return markerAtHistoryEdge == isEstimate
+ || (markerAtHistoryEdge && notableCount == 0);
+}
+
+QDebug Quotient::operator<<(QDebug dbg, const EventStats& es)
+{
+ QDebugStateSaver _(dbg);
+ dbg.nospace() << es.notableCount << '/' << es.highlightCount;
+ if (es.isEstimate)
+ dbg << " (estimated)";
+ return dbg;
+}
diff --git a/lib/eventstats.h b/lib/eventstats.h
new file mode 100644
index 00000000..77c661a7
--- /dev/null
+++ b/lib/eventstats.h
@@ -0,0 +1,114 @@
+// SPDX-FileCopyrightText: 2021 Quotient contributors
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "room.h"
+
+namespace Quotient {
+
+//! \brief Counters of unread events and highlights with a precision flag
+//!
+//! This structure contains a static snapshot with values of unread counters
+//! returned by Room::partiallyReadStats and Room::unreadStats (properties
+//! or methods).
+//!
+//! \note It's just a simple grouping of counters and is not automatically
+//! updated from the room as subsequent syncs arrive.
+//! \sa Room::unreadStats, Room::partiallyReadStats, Room::isEventNotable
+struct EventStats {
+ Q_GADGET
+ Q_PROPERTY(qsizetype notableCount MEMBER notableCount CONSTANT)
+ Q_PROPERTY(qsizetype highlightCount MEMBER highlightCount CONSTANT)
+ Q_PROPERTY(bool isEstimate MEMBER isEstimate CONSTANT)
+public:
+ //! The number of "notable" events in an events range
+ //! \sa Room::isEventNotable
+ qsizetype notableCount = 0;
+ qsizetype highlightCount = 0;
+ //! \brief Whether the counter values above are exact
+ //!
+ //! This is false when the end marker (m.read receipt or m.fully_read) used
+ //! to collect the stats points to an event loaded locally and the counters
+ //! can therefore be calculated exactly using the locally available segment
+ //! of the timeline; true when the marker points to an event outside of
+ //! the local timeline (in which case the estimation is made basing on
+ //! the data supplied by the homeserver as well as counters saved from
+ //! the previous run of the client).
+ bool isEstimate = true;
+
+ // TODO: replace with = default once C++20 becomes a requirement on clients
+ bool operator==(const EventStats& rhs) const
+ {
+ return notableCount == rhs.notableCount
+ && highlightCount == rhs.highlightCount
+ && isEstimate == rhs.isEstimate;
+ }
+ bool operator!=(const EventStats& rhs) const { return !operator==(rhs); }
+
+ //! \brief Check whether the event statistics are empty
+ //!
+ //! Empty statistics have notable and highlight counters of zero and
+ //! isEstimate set to false.
+ Q_INVOKABLE bool empty() const
+ {
+ return notableCount == 0 && !isEstimate && highlightCount == 0;
+ }
+
+ using marker_t = Room::rev_iter_t;
+
+ //! \brief Build event statistics on a range of events
+ //!
+ //! This is a factory that returns an EventStats instance with counts of
+ //! notable and highlighted events between \p from and \p to reverse
+ //! timeline iterators; the \p init parameter allows to override
+ //! the initial statistics object and start from other values.
+ static EventStats fromRange(const Room* room, const marker_t& from,
+ const marker_t& to,
+ const EventStats& init = { 0, 0, false });
+
+ //! \brief Build event statistics on a range from sync edge to marker
+ //!
+ //! This is mainly a shortcut for \code
+ //! <tt>fromRange(room, marker_t(room->syncEdge()), marker)</tt>
+ //! \endcode except that it also sets isEstimate to true if (and only if)
+ //! <tt>to == room->historyEdge()</tt>.
+ static EventStats fromMarker(const Room* room, const marker_t& marker);
+
+ //! \brief Loads a statistics object from the cached counters
+ //!
+ //! Sets isEstimate to `true` unless both notableCount and highlightCount
+ //! are equal to -1.
+ static EventStats fromCachedCounters(Omittable<int> notableCount,
+ Omittable<int> highlightCount = none);
+
+ //! \brief Update statistics when a read marker moves down the timeline
+ //!
+ //! Removes events between oldMarker and newMarker from statistics
+ //! calculation if \p oldMarker points to an existing event in the timeline,
+ //! or recalculates the statistics entirely if \p oldMarker points
+ //! to <tt>room->historyEdge()</tt>. Always results in exact statistics
+ //! (<tt>isEstimate == false</tt>.
+ //! \param oldMarker Must point correspond to the _current_ statistics
+ //! isEstimate state, i.e. it should point to
+ //! <tt>room->historyEdge()</tt> if <tt>isEstimate == true</tt>, or
+ //! to a valid position within the timeline otherwise
+ //! \param newMarker Must point to a valid position in the timeline (not to
+ //! <tt>room->historyEdge()</tt> that is equal to or closer to
+ //! the sync edge than \p oldMarker
+ //! \return true if either notableCount or highlightCount changed, or if
+ //! the statistics was completely recalculated; false otherwise
+ bool updateOnMarkerMove(const Room* room, const marker_t& oldMarker,
+ const marker_t& newMarker);
+
+ //! \brief Validate the statistics object against the given marker
+ //!
+ //! Checks whether the statistics object data are valid for a given marker.
+ //! No stats recalculation takes place, only isEstimate and zero-ness
+ //! of notableCount are checked.
+ bool isValidFor(const Room* room, const marker_t& marker) const;
+};
+
+QDebug operator<<(QDebug dbg, const EventStats& es);
+
+}
diff --git a/lib/function_traits.cpp b/lib/function_traits.cpp
new file mode 100644
index 00000000..20bcf30e
--- /dev/null
+++ b/lib/function_traits.cpp
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "function_traits.h"
+
+// Tests for function_traits<>
+
+using namespace Quotient;
+
+int f_();
+static_assert(std::is_same_v<fn_return_t<decltype(f_)>, int>,
+ "Test fn_return_t<>");
+
+void f1_(int, float);
+static_assert(std::is_same_v<fn_arg_t<decltype(f1_), 1>, float>,
+ "Test fn_arg_t<>");
+
+struct Fo {
+ int operator()();
+ static constexpr auto l = [] { return 0.0f; };
+ bool memFn();
+ void constMemFn() const&;
+ double field;
+ const double field2;
+};
+static_assert(std::is_same_v<fn_return_t<Fo>, int>,
+ "Test return type of function object");
+static_assert(std::is_same_v<fn_return_t<decltype(Fo::l)>, float>,
+ "Test return type of lambda");
+static_assert(std::is_same_v<fn_arg_t<decltype(&Fo::memFn)>, Fo>,
+ "Test first argument type of member function");
+static_assert(std::is_same_v<fn_return_t<decltype(&Fo::memFn)>, bool>,
+ "Test return type of member function");
+static_assert(std::is_same_v<fn_arg_t<decltype(&Fo::constMemFn)>, const Fo&>,
+ "Test first argument type of const member function");
+static_assert(std::is_void_v<fn_return_t<decltype(&Fo::constMemFn)>>,
+ "Test return type of const member function");
+static_assert(std::is_same_v<fn_return_t<decltype(&Fo::field)>, double&>,
+ "Test return type of a class member");
+static_assert(std::is_same_v<fn_return_t<decltype(&Fo::field2)>, const double&>,
+ "Test return type of a const class member");
+
+struct Fo1 {
+ void operator()(int);
+};
+static_assert(std::is_same_v<fn_arg_t<Fo1>, int>,
+ "Test fn_arg_t defaulting to first argument");
+
+template <typename T>
+static void ft(const std::vector<T>&);
+static_assert(
+ std::is_same<fn_arg_t<decltype(ft<double>)>, const std::vector<double>&>(),
+ "Test function templates");
diff --git a/lib/function_traits.h b/lib/function_traits.h
new file mode 100644
index 00000000..83b8e425
--- /dev/null
+++ b/lib/function_traits.h
@@ -0,0 +1,93 @@
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <functional>
+
+namespace Quotient {
+
+namespace _impl {
+ template <typename AlwaysVoid, typename>
+ struct fn_traits {};
+}
+
+/// Determine traits of an arbitrary function/lambda/functor
+/*!
+ * Doesn't work with generic lambdas and function objects that have
+ * operator() overloaded.
+ * \sa
+ * https://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda#7943765
+ */
+template <typename T>
+struct function_traits
+ : public _impl::fn_traits<void, std::remove_reference_t<T>> {};
+
+// Specialisation for a function
+template <typename ReturnT, typename... ArgTs>
+struct function_traits<ReturnT(ArgTs...)> {
+ using return_type = ReturnT;
+ using arg_types = std::tuple<ArgTs...>;
+ // See also the comment for wrap_in_function() in qt_connection_util.h
+ using function_type = std::function<ReturnT(ArgTs...)>;
+};
+
+namespace _impl {
+ template <typename AlwaysVoid, typename>
+ struct fn_object_traits;
+
+ // Specialisation for a lambda function
+ template <typename ReturnT, typename ClassT, typename... ArgTs>
+ struct fn_object_traits<void, ReturnT (ClassT::*)(ArgTs...)>
+ : function_traits<ReturnT(ArgTs...)> {};
+
+ // Specialisation for a const lambda function
+ template <typename ReturnT, typename ClassT, typename... ArgTs>
+ struct fn_object_traits<void, ReturnT (ClassT::*)(ArgTs...) const>
+ : function_traits<ReturnT(ArgTs...)> {};
+
+ // Specialisation for function objects with (non-overloaded) operator()
+ // (this includes non-generic lambdas)
+ template <typename T>
+ struct fn_traits<decltype(void(&T::operator())), T>
+ : public fn_object_traits<void, decltype(&T::operator())> {};
+
+ // Specialisation for a member function in a non-functor class
+ template <typename ReturnT, typename ClassT, typename... ArgTs>
+ struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...)>
+ : function_traits<ReturnT(ClassT, 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(const ClassT&, ArgTs...)> {};
+
+ // Specialisation for a constref member function
+ template <typename ReturnT, typename ClassT, typename... ArgTs>
+ struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) const&>
+ : function_traits<ReturnT(const ClassT&, ArgTs...)> {};
+
+ // Specialisation for a prvalue member function
+ template <typename ReturnT, typename ClassT, typename... ArgTs>
+ struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) &&>
+ : function_traits<ReturnT(ClassT&&, ArgTs...)> {};
+
+ // Specialisation for a pointer-to-member
+ template <typename ReturnT, typename ClassT>
+ struct fn_traits<void, ReturnT ClassT::*>
+ : function_traits<ReturnT&(ClassT)> {};
+
+ // Specialisation for a const pointer-to-member
+ template <typename ReturnT, typename ClassT>
+ struct fn_traits<void, const ReturnT ClassT::*>
+ : function_traits<const ReturnT&(ClassT)> {};
+} // namespace _impl
+
+template <typename FnT>
+using fn_return_t = typename function_traits<FnT>::return_type;
+
+template <typename FnT, int ArgN = 0>
+using fn_arg_t =
+ std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>;
+
+} // namespace Quotient
diff --git a/lib/identity/definitions/request_email_validation.h b/lib/identity/definitions/request_email_validation.h
index 079da953..87549505 100644
--- a/lib/identity/definitions/request_email_validation.h
+++ b/lib/identity/definitions/request_email_validation.h
@@ -11,16 +11,16 @@ namespace Quotient {
struct RequestEmailValidation {
/// A unique string generated by the client, and used to identify the
/// validation attempt. It must be a string consisting of the characters
- /// ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it
+ /// `[0-9a-zA-Z.=_-]`. Its length must not exceed 255 characters and it
/// must not be empty.
QString clientSecret;
/// The email address to validate.
QString email;
- /// The server will only send an email if the ``send_attempt``
+ /// The server will only send an email if the `send_attempt`
/// is a number greater than the most recent one which it has seen,
- /// scoped to that ``email`` + ``client_secret`` pair. This is to
+ /// scoped to that `email` + `client_secret` pair. This is to
/// avoid repeatedly sending the same email in the case of request
/// retries between the POSTing user and the identity server.
/// The client should increment this value if they desire a new
diff --git a/lib/identity/definitions/request_msisdn_validation.h b/lib/identity/definitions/request_msisdn_validation.h
index a29fd0de..d2ea463f 100644
--- a/lib/identity/definitions/request_msisdn_validation.h
+++ b/lib/identity/definitions/request_msisdn_validation.h
@@ -11,20 +11,20 @@ namespace Quotient {
struct RequestMsisdnValidation {
/// A unique string generated by the client, and used to identify the
/// validation attempt. It must be a string consisting of the characters
- /// ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it
+ /// `[0-9a-zA-Z.=_-]`. Its length must not exceed 255 characters and it
/// must not be empty.
QString clientSecret;
/// The two-letter uppercase ISO-3166-1 alpha-2 country code that the
- /// number in ``phone_number`` should be parsed as if it were dialled from.
+ /// number in `phone_number` should be parsed as if it were dialled from.
QString country;
/// The phone number to validate.
QString phoneNumber;
- /// The server will only send an SMS if the ``send_attempt`` is a
+ /// The server will only send an SMS if the `send_attempt` is a
/// number greater than the most recent one which it has seen,
- /// scoped to that ``country`` + ``phone_number`` + ``client_secret``
+ /// scoped to that `country` + `phone_number` + `client_secret`
/// triple. This is to avoid repeatedly sending the same SMS in
/// the case of request retries between the POSTing user and the
/// identity server. The client should increment this value if
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp
index 48c2996d..971fea7b 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -5,6 +5,7 @@
#include "basejob.h"
#include "connectiondata.h"
+#include "quotient_common.h"
#include <QtCore/QRegularExpression>
#include <QtCore/QTimer>
@@ -15,8 +16,6 @@
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkRequest>
-#include <array>
-
using namespace Quotient;
using std::chrono::seconds, std::chrono::milliseconds;
using namespace std::chrono_literals;
@@ -25,7 +24,7 @@ BaseJob::StatusCode BaseJob::Status::fromHttpCode(int httpCode)
{
// Based on https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
if (httpCode / 10 == 41) // 41x errors
- return httpCode == 410 ? IncorrectRequestError : NotFoundError;
+ return httpCode == 410 ? IncorrectRequest : NotFound;
switch (httpCode) {
case 401:
return Unauthorised;
@@ -33,19 +32,19 @@ BaseJob::StatusCode BaseJob::Status::fromHttpCode(int httpCode)
case 403: case 407: // clang-format on
return ContentAccessError;
case 404:
- return NotFoundError;
+ return NotFound;
// clang-format off
case 400: case 405: case 406: case 426: case 428: case 505: // clang-format on
case 494: // Unofficial nginx "Request header too large"
case 497: // Unofficial nginx "HTTP request sent to HTTPS port"
- return IncorrectRequestError;
+ return IncorrectRequest;
case 429:
- return TooManyRequestsError;
+ return TooManyRequests;
case 501:
case 510:
- return RequestNotImplementedError;
+ return RequestNotImplemented;
case 511:
- return NetworkAuthRequiredError;
+ return NetworkAuthRequired;
default:
return NetworkError;
}
@@ -63,12 +62,6 @@ QDebug BaseJob::Status::dumpToLog(QDebug dbg) const
return dbg << ": " << message;
}
-template <typename... Ts>
-constexpr auto make_array(Ts&&... items)
-{
- return std::array<std::common_type_t<Ts...>, sizeof...(Ts)>({items...});
-}
-
class BaseJob::Private {
public:
struct JobTimeoutConfig {
@@ -78,8 +71,8 @@ public:
// Using an idiom from clang-tidy:
// http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html
- Private(HttpVerb v, QString endpoint, const QUrlQuery& q, Data&& data,
- bool nt)
+ Private(HttpVerb v, QByteArray endpoint, const QUrlQuery& q,
+ RequestData&& data, bool nt)
: verb(v)
, apiEndpoint(std::move(endpoint))
, requestQuery(q)
@@ -113,10 +106,10 @@ public:
// Contents for the network request
HttpVerb verb;
- QString apiEndpoint;
+ QByteArray apiEndpoint;
QHash<QByteArray, QByteArray> requestHeaders;
QUrlQuery requestQuery;
- Data requestData;
+ RequestData requestData;
bool needsToken;
bool inBackground = false;
@@ -173,14 +166,36 @@ public:
}
};
-BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
+inline bool isHex(QChar c)
+{
+ return c.isDigit() || (c >= u'A' && c <= u'F') || (c >= u'a' && c <= u'f');
+}
+
+QByteArray BaseJob::encodeIfParam(const QString& paramPart)
+{
+ const auto percentIndex = paramPart.indexOf('%');
+ if (percentIndex != -1 && paramPart.size() > percentIndex + 2
+ && isHex(paramPart[percentIndex + 1])
+ && isHex(paramPart[percentIndex + 2])) {
+ qCWarning(JOBS)
+ << "Developers, upfront percent-encoding of job parameters is "
+ "deprecated since libQuotient 0.7; the string involved is"
+ << paramPart;
+ return QUrl(paramPart, QUrl::TolerantMode).toEncoded();
+ }
+ return QUrl::toPercentEncoding(paramPart);
+}
+
+BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
bool needsToken)
- : BaseJob(verb, name, endpoint, Query {}, Data {}, needsToken)
+ : BaseJob(verb, name, std::move(endpoint), QUrlQuery {}, RequestData {},
+ needsToken)
{}
-BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
- const Query& query, Data&& data, bool needsToken)
- : d(new Private(verb, endpoint, query, std::move(data), needsToken))
+BaseJob::BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
+ const QUrlQuery& query, RequestData&& data, bool needsToken)
+ : d(new Private(verb, std::move(endpoint), query, std::move(data),
+ needsToken))
{
setObjectName(name);
connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout);
@@ -201,13 +216,6 @@ QUrl BaseJob::requestUrl() const { return d->reply ? d->reply->url() : QUrl(); }
bool BaseJob::isBackground() const { return d->inBackground; }
-const QString& BaseJob::apiEndpoint() const { return d->apiEndpoint; }
-
-void BaseJob::setApiEndpoint(const QString& apiEndpoint)
-{
- d->apiEndpoint = apiEndpoint;
-}
-
const BaseJob::headers_t& BaseJob::requestHeaders() const
{
return d->requestHeaders;
@@ -224,16 +232,19 @@ void BaseJob::setRequestHeaders(const BaseJob::headers_t& headers)
d->requestHeaders = headers;
}
-const QUrlQuery& BaseJob::query() const { return d->requestQuery; }
+QUrlQuery BaseJob::query() const { return d->requestQuery; }
void BaseJob::setRequestQuery(const QUrlQuery& query)
{
d->requestQuery = query;
}
-const BaseJob::Data& BaseJob::requestData() const { return d->requestData; }
+const RequestData& BaseJob::requestData() const { return d->requestData; }
-void BaseJob::setRequestData(Data&& data) { std::swap(d->requestData, data); }
+void BaseJob::setRequestData(RequestData&& data)
+{
+ std::swap(d->requestData, data);
+}
const QByteArrayList& BaseJob::expectedContentTypes() const
{
@@ -263,17 +274,17 @@ const QNetworkReply* BaseJob::reply() const { return d->reply.data(); }
QNetworkReply* BaseJob::reply() { return d->reply.data(); }
-QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QString& path,
+QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QByteArray& encodedPath,
const QUrlQuery& query)
{
- auto pathBase = baseUrl.path();
- // QUrl::adjusted(QUrl::StripTrailingSlashes) doesn't help with root '/'
- while (pathBase.endsWith('/'))
- pathBase.chop(1);
- if (!path.startsWith('/')) // Normally API files do start with '/'
- pathBase.push_back('/'); // so this shouldn't be needed these days
-
- baseUrl.setPath(pathBase + path, QUrl::TolerantMode);
+ // Make sure the added path is relative even if it's not (the official
+ // API definitions have the leading slash though it's not really correct).
+ const auto pathUrl =
+ QUrl::fromEncoded(encodedPath.mid(encodedPath.startsWith('/')),
+ QUrl::StrictMode);
+ Q_ASSERT_X(pathUrl.isValid(), __FUNCTION__,
+ qPrintable(pathUrl.errorString()));
+ baseUrl = baseUrl.resolved(pathUrl);
baseUrl.setQuery(query);
return baseUrl;
}
@@ -288,7 +299,8 @@ void BaseJob::Private::sendRequest()
req.setRawHeader("Authorization",
QByteArray("Bearer ") + connection->accessToken());
req.setAttribute(QNetworkRequest::BackgroundRequestAttribute, inBackground);
- req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ req.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
+ QNetworkRequest::NoLessSafeRedirectPolicy);
req.setMaximumRedirectsAllowed(10);
req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);
req.setAttribute(
@@ -353,7 +365,7 @@ void BaseJob::initiate(ConnectionData* connData, bool inBackground)
qCCritical(d->logCat)
<< "Developers, ensure the Connection is valid before using it";
Q_ASSERT(false);
- setStatus(IncorrectRequestError, tr("Invalid server connection"));
+ setStatus(IncorrectRequest, tr("Invalid server connection"));
}
// The status is no good, finalise
QTimer::singleShot(0, this, &BaseJob::finishJob);
@@ -403,42 +415,42 @@ BaseJob::Status BaseJob::Private::parseJson()
void BaseJob::gotReply()
{
- setStatus(checkReply(reply()));
-
- if (status().good()
- && d->expectedContentTypes == QByteArrayList { "application/json" }) {
+ // Defer actually updating the status until it's finalised
+ auto statusSoFar = checkReply(reply());
+ if (statusSoFar.good()
+ && d->expectedContentTypes == QByteArrayList { "application/json" }) //
+ {
d->rawResponse = reply()->readAll();
- setStatus(d->parseJson());
- if (status().good() && !expectedKeys().empty()) {
+ statusSoFar = d->parseJson();
+ if (statusSoFar.good() && !expectedKeys().empty()) {
const auto& responseObject = jsonData();
QByteArrayList missingKeys;
for (const auto& k: expectedKeys())
if (!responseObject.contains(k))
missingKeys.push_back(k);
if (!missingKeys.empty())
- setStatus(IncorrectResponse, tr("Required JSON keys missing: ")
- + missingKeys.join());
+ statusSoFar = { IncorrectResponse,
+ tr("Required JSON keys missing: ")
+ + missingKeys.join() };
}
+ setStatus(statusSoFar);
if (!status().good()) // Bad JSON in a "good" reply: bail out
return;
- } // else {
+ }
// If the endpoint expects anything else than just (API-related) JSON
// reply()->readAll() is not performed and the whole reply processing
// is left to derived job classes: they may read it piecemeal or customise
// per content type in prepareResult(), or even have read it already
// (see, e.g., DownloadFileJob).
- // }
-
- if (status().good())
+ if (statusSoFar.good()) {
setStatus(prepareResult());
- else {
- d->rawResponse = reply()->readAll();
- qCDebug(d->logCat).noquote()
- << "Error body (truncated if long):" << rawDataSample(500);
- // Parse the error payload and update the status if needed
- if (const auto newStatus = prepareError(); !newStatus.good())
- setStatus(newStatus);
+ return;
}
+
+ d->rawResponse = reply()->readAll();
+ qCDebug(d->logCat).noquote()
+ << "Error body (truncated if long):" << rawDataSample(500);
+ setStatus(prepareError(statusSoFar));
}
bool checkContentType(const QByteArray& type, const QByteArrayList& patterns)
@@ -503,7 +515,7 @@ BaseJob::Status BaseJob::checkReply(const QNetworkReply* reply) const
BaseJob::Status BaseJob::prepareResult() { return Success; }
-BaseJob::Status BaseJob::prepareError()
+BaseJob::Status BaseJob::prepareError(Status currentStatus)
{
// Try to make sense of the error payload but be prepared for all kinds
// of unexpected stuff (raw HTML, plain text, foreign JSON among those)
@@ -513,10 +525,10 @@ BaseJob::Status BaseJob::prepareError()
// By now, if d->parseJson() above succeeded then jsonData() will return
// a valid JSON object - or an empty object otherwise (in which case most
- // of if's below will fall through to `return NoError` at the end
+ // of if's below will fall through retaining the current status)
const auto& errorJson = jsonData();
const auto errCode = errorJson.value("errcode"_ls).toString();
- if (error() == TooManyRequestsError || errCode == "M_LIMIT_EXCEEDED") {
+ if (error() == TooManyRequests || errCode == "M_LIMIT_EXCEEDED") {
QString msg = tr("Too many requests");
int64_t retryAfterMs = errorJson.value("retry_after_ms"_ls).toInt(-1);
if (retryAfterMs >= 0)
@@ -526,16 +538,16 @@ BaseJob::Status BaseJob::prepareError()
d->connection->limitRate(milliseconds(retryAfterMs));
- return { TooManyRequestsError, msg };
+ return { TooManyRequests, msg };
}
if (errCode == "M_CONSENT_NOT_GIVEN") {
d->errorUrl = QUrl(errorJson.value("consent_uri"_ls).toString());
- return { UserConsentRequiredError };
+ return { UserConsentRequired };
}
if (errCode == "M_UNSUPPORTED_ROOM_VERSION"
|| errCode == "M_INCOMPATIBLE_ROOM_VERSION")
- return { UnsupportedRoomVersionError,
+ return { UnsupportedRoomVersion,
errorJson.contains("room_version"_ls)
? tr("Requested room version: %1")
.arg(errorJson.value("room_version"_ls).toString())
@@ -548,9 +560,9 @@ BaseJob::Status BaseJob::prepareError()
// Not localisable on the client side
if (errorJson.contains("error"_ls)) // Keep the code, update the message
- return { d->status.code, errorJson.value("error"_ls).toString() };
+ return { currentStatus.code, errorJson.value("error"_ls).toString() };
- return NoError; // Retain the status if the error payload is not recognised
+ return currentStatus; // The error payload is not recognised
}
QJsonValue BaseJob::takeValueFromJson(const QString& key)
@@ -717,27 +729,27 @@ QString BaseJob::statusCaption() const
return tr("Request was abandoned");
case NetworkError:
return tr("Network problems");
- case TimeoutError:
+ case Timeout:
return tr("Request timed out");
case Unauthorised:
return tr("Unauthorised request");
case ContentAccessError:
return tr("Access error");
- case NotFoundError:
+ case NotFound:
return tr("Not found");
- case IncorrectRequestError:
+ case IncorrectRequest:
return tr("Invalid request");
- case IncorrectResponseError:
+ case IncorrectResponse:
return tr("Response could not be parsed");
- case TooManyRequestsError:
+ case TooManyRequests:
return tr("Too many requests");
- case RequestNotImplementedError:
+ case RequestNotImplemented:
return tr("Function not implemented by the server");
- case NetworkAuthRequiredError:
+ case NetworkAuthRequired:
return tr("Network authentication required");
- case UserConsentRequiredError:
+ case UserConsentRequired:
return tr("User consent required");
- case UnsupportedRoomVersionError:
+ case UnsupportedRoomVersion:
return tr("The server does not support the needed room version");
default:
return tr("Request failed");
@@ -799,7 +811,7 @@ void BaseJob::abandon()
void BaseJob::timeout()
{
- setStatus(TimeoutError, "The job has timed out");
+ setStatus(Timeout, "The job has timed out");
finishJob();
}
diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h
index ca91a781..ddf243ed 100644
--- a/lib/jobs/basejob.h
+++ b/lib/jobs/basejob.h
@@ -7,8 +7,10 @@
#include "requestdata.h"
#include "../logging.h"
#include "../converters.h"
+#include "../quotient_common.h"
#include <QtCore/QObject>
+#include <QtCore/QStringBuilder>
class QNetworkReply;
class QSslError;
@@ -23,7 +25,18 @@ class BaseJob : public QObject {
Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT)
Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries)
Q_PROPERTY(int statusCode READ error NOTIFY statusChanged)
+
+ static QByteArray encodeIfParam(const QString& paramPart);
+ template <int N>
+ static inline auto encodeIfParam(const char (&constPart)[N])
+ {
+ return constPart;
+ }
+
public:
+#define WITH_DEPRECATED_ERROR_VERSION(Recommended) \
+ Recommended, DECL_DEPRECATED_ENUMERATOR(Recommended##Error, Recommended)
+
/*! The status code of a job
*
* Every job is created in Unprepared status; upon calling prepare()
@@ -34,7 +47,7 @@ public:
*/
enum StatusCode {
Success = 0,
- NoError = Success, // To be compatible with Qt conventions
+ NoError = Success,
Pending = 1,
WarningLevel = 20, //< Warnings have codes starting from this
UnexpectedResponseType = 21,
@@ -43,28 +56,18 @@ public:
Abandoned = 50, //< A tiny period between abandoning and object deletion
ErrorLevel = 100, //< Errors have codes starting from this
NetworkError = 101,
- Timeout,
- TimeoutError = Timeout,
+ WITH_DEPRECATED_ERROR_VERSION(Timeout),
Unauthorised,
ContentAccessError,
- NotFoundError,
- IncorrectRequest,
- IncorrectRequestError = IncorrectRequest,
- IncorrectResponse,
- IncorrectResponseError = IncorrectResponse,
- JsonParseError //< \deprecated Use IncorrectResponse instead
- = IncorrectResponse,
- TooManyRequests,
- TooManyRequestsError = TooManyRequests,
+ WITH_DEPRECATED_ERROR_VERSION(NotFound),
+ WITH_DEPRECATED_ERROR_VERSION(IncorrectRequest),
+ WITH_DEPRECATED_ERROR_VERSION(IncorrectResponse),
+ WITH_DEPRECATED_ERROR_VERSION(TooManyRequests),
RateLimited = TooManyRequests,
- RequestNotImplemented,
- RequestNotImplementedError = RequestNotImplemented,
- UnsupportedRoomVersion,
- UnsupportedRoomVersionError = UnsupportedRoomVersion,
- NetworkAuthRequired,
- NetworkAuthRequiredError = NetworkAuthRequired,
- UserConsentRequired,
- UserConsentRequiredError = UserConsentRequired,
+ WITH_DEPRECATED_ERROR_VERSION(RequestNotImplemented),
+ WITH_DEPRECATED_ERROR_VERSION(UnsupportedRoomVersion),
+ WITH_DEPRECATED_ERROR_VERSION(NetworkAuthRequired),
+ WITH_DEPRECATED_ERROR_VERSION(UserConsentRequired),
CannotLeaveRoom,
UserDeactivated,
FileError,
@@ -72,21 +75,19 @@ public:
};
Q_ENUM(StatusCode)
- /**
- * A simple wrapper around QUrlQuery that allows its creation from
- * a list of string pairs
- */
- class Query : public QUrlQuery {
- public:
- using QUrlQuery::QUrlQuery;
- Query() = default;
- Query(const std::initializer_list<QPair<QString, QString>>& l)
- {
- setQueryItems(l);
- }
- };
+#undef WITH_DEPRECATED_ERROR_VERSION
- using Data = RequestData;
+ template <typename... StrTs>
+ static QByteArray makePath(StrTs&&... parts)
+ {
+ return (QByteArray() % ... % encodeIfParam(parts));
+ }
+
+ using Data
+#ifndef Q_CC_MSVC
+ Q_DECL_DEPRECATED_X("Use Quotient::RequestData instead")
+#endif
+ = RequestData;
/*!
* This structure stores the status of a server call job. The status
@@ -136,10 +137,11 @@ public:
};
public:
- BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
+ BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
+ bool needsToken = true);
+ BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
+ const QUrlQuery& query, RequestData&& data = {},
bool needsToken = true);
- BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
- const Query& query, Data&& data = {}, bool needsToken = true);
QUrl requestUrl() const;
bool isBackground() const;
@@ -194,7 +196,7 @@ public:
* If there's no top-level JSON object in the response or if there's
* no node with the key \p keyName, \p defaultValue is returned.
*/
- template <typename T, typename StrT> // Waiting for QStringViews...
+ template <typename T, typename StrT>
T loadFromJson(const StrT& keyName, T&& defaultValue = {}) const
{
const auto& jv = jsonData().value(keyName);
@@ -336,16 +338,18 @@ Q_SIGNALS:
protected:
using headers_t = QHash<QByteArray, QByteArray>;
+ Q_DECL_DEPRECATED_X("Deprecated due to being unused")
const QString& apiEndpoint() const;
+ Q_DECL_DEPRECATED_X("Deprecated due to being unused")
void setApiEndpoint(const QString& apiEndpoint);
const headers_t& requestHeaders() const;
void setRequestHeader(const headers_t::key_type& headerName,
const headers_t::mapped_type& headerValue);
void setRequestHeaders(const headers_t& headers);
- const QUrlQuery& query() const;
+ QUrlQuery query() const;
void setRequestQuery(const QUrlQuery& query);
- const Data& requestData() const;
- void setRequestData(Data&& data);
+ const RequestData& requestData() const;
+ void setRequestData(RequestData&& data);
const QByteArrayList& expectedContentTypes() const;
void addExpectedContentType(const QByteArray& contentType);
void setExpectedContentTypes(const QByteArrayList& contentTypes);
@@ -361,7 +365,7 @@ protected:
* The function ensures exactly one '/' between the path component of
* \p baseUrl and \p path. The query component of \p baseUrl is ignored.
*/
- static QUrl makeRequestUrl(QUrl baseUrl, const QString& path,
+ static QUrl makeRequestUrl(QUrl baseUrl, const QByteArray &encodedPath,
const QUrlQuery& query = {});
/*! Prepares the job for execution
@@ -395,10 +399,12 @@ protected:
* was not good (usually because of an unsuccessful HTTP code).
* The base implementation assumes Matrix JSON error object in the body;
* overrides are strongly recommended to call it for all stock Matrix
- * responses as early as possible but in addition can process custom errors,
+ * responses as early as possible and only then process custom errors,
* with JSON or non-JSON payload.
+ *
+ * \return updated (if necessary) job status
*/
- virtual Status prepareError();
+ virtual Status prepareError(Status currentStatus);
/*! \brief Get direct access to the JSON response object in the job
*
diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp
index 7dbf4ab3..6fe8ef26 100644
--- a/lib/jobs/mediathumbnailjob.cpp
+++ b/lib/jobs/mediathumbnailjob.cpp
@@ -17,13 +17,17 @@ MediaThumbnailJob::MediaThumbnailJob(const QString& serverName,
const QString& mediaId, QSize requestedSize)
: GetContentThumbnailJob(serverName, mediaId, requestedSize.width(),
requestedSize.height(), "scale")
-{}
+{
+ setLoggingCategory(THUMBNAILJOB);
+}
MediaThumbnailJob::MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize)
: MediaThumbnailJob(mxcUri.authority(),
mxcUri.path().mid(1), // sans leading '/'
requestedSize)
-{}
+{
+ setLoggingCategory(THUMBNAILJOB);
+}
QImage MediaThumbnailJob::thumbnail() const { return _thumbnail; }
diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp
index 047e2920..2c001ccc 100644
--- a/lib/jobs/requestdata.cpp
+++ b/lib/jobs/requestdata.cpp
@@ -3,6 +3,7 @@
#include "requestdata.h"
+#include <QtCore/QIODevice>
#include <QtCore/QBuffer>
#include <QtCore/QByteArray>
#include <QtCore/QJsonArray>
@@ -31,4 +32,8 @@ RequestData::RequestData(const QJsonObject& jo) : _source(fromJson(jo)) {}
RequestData::RequestData(const QJsonArray& ja) : _source(fromJson(ja)) {}
+RequestData::RequestData(QIODevice* source)
+ : _source(std::unique_ptr<QIODevice>(source))
+{}
+
RequestData::~RequestData() = default;
diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h
index 4958e0f9..4f05e5ff 100644
--- a/lib/jobs/requestdata.h
+++ b/lib/jobs/requestdata.h
@@ -24,8 +24,7 @@ public:
RequestData(const QByteArray& a = {});
RequestData(const QJsonObject& jo);
RequestData(const QJsonArray& ja);
- RequestData(QIODevice* source) : _source(std::unique_ptr<QIODevice>(source))
- {}
+ RequestData(QIODevice* source);
RequestData(RequestData&&) = default;
RequestData& operator=(RequestData&&) = default;
~RequestData();
@@ -36,5 +35,3 @@ private:
std::unique_ptr<QIODevice> _source;
};
} // namespace Quotient
-/// \deprecated Use namespace Quotient instead
-namespace QMatrixClient = Quotient;
diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp
index 59a34ef3..9b1b46f0 100644
--- a/lib/jobs/syncjob.cpp
+++ b/lib/jobs/syncjob.cpp
@@ -10,7 +10,7 @@ static size_t jobId = 0;
SyncJob::SyncJob(const QString& since, const QString& filter, int timeout,
const QString& presence)
: BaseJob(HttpVerb::Get, QStringLiteral("SyncJob-%1").arg(++jobId),
- QStringLiteral("_matrix/client/r0/sync"))
+ "_matrix/client/r0/sync")
{
setLoggingCategory(SYNCJOB);
QUrlQuery query;
diff --git a/lib/joinstate.h b/lib/joinstate.h
deleted file mode 100644
index 805ce73a..00000000
--- a/lib/joinstate.h
+++ /dev/null
@@ -1,32 +0,0 @@
-// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#pragma once
-
-#include <QtCore/QFlags>
-
-#include <array>
-
-namespace Quotient {
-enum class JoinState : unsigned int {
- Join = 0x1,
- Invite = 0x2,
- Leave = 0x4,
-};
-
-Q_DECLARE_FLAGS(JoinStates, JoinState)
-
-// We cannot use Q_ENUM outside of a Q_OBJECT and besides, we want
-// to use strings that match respective JSON keys.
-static const std::array<const char*, 3> JoinStateStrings { { "join", "invite",
- "leave" } };
-
-inline const char* toCString(JoinState js)
-{
- size_t state = size_t(js), index = 0;
- while (state >>= 1u)
- ++index;
- return JoinStateStrings[index];
-}
-} // namespace Quotient
-Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::JoinStates)
diff --git a/lib/logging.cpp b/lib/logging.cpp
index af229684..15eac69d 100644
--- a/lib/logging.cpp
+++ b/lib/logging.cpp
@@ -16,4 +16,6 @@ LOGGING_CATEGORY(EPHEMERAL, "quotient.events.ephemeral")
LOGGING_CATEGORY(E2EE, "quotient.e2ee")
LOGGING_CATEGORY(JOBS, "quotient.jobs")
LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync")
+LOGGING_CATEGORY(THUMBNAILJOB, "quotient.jobs.thumbnail")
+LOGGING_CATEGORY(NETWORK, "quotient.network")
LOGGING_CATEGORY(PROFILER, "quotient.profiler")
diff --git a/lib/logging.h b/lib/logging.h
index 432ed16f..5bf050a9 100644
--- a/lib/logging.h
+++ b/lib/logging.h
@@ -16,6 +16,8 @@ Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL)
Q_DECLARE_LOGGING_CATEGORY(E2EE)
Q_DECLARE_LOGGING_CATEGORY(JOBS)
Q_DECLARE_LOGGING_CATEGORY(SYNCJOB)
+Q_DECLARE_LOGGING_CATEGORY(THUMBNAILJOB)
+Q_DECLARE_LOGGING_CATEGORY(NETWORK)
Q_DECLARE_LOGGING_CATEGORY(PROFILER)
namespace Quotient {
@@ -35,24 +37,18 @@ using QDebugManip = QDebug (*)(QDebug);
*/
inline QDebug formatJson(QDebug debug_object)
{
-#if QT_VERSION < QT_VERSION_CHECK(5, 4, 0)
- return debug_object;
-#else
return debug_object.noquote();
-#endif
}
-/**
- * @brief A helper operator to facilitate usage of formatJson (and possibly
- * other manipulators)
- *
- * @param debug_object to output the json to
- * @param qdm a QDebug manipulator
- * @return a copy of debug_object that has its mode altered by qdm
- */
-inline QDebug operator<<(QDebug debug_object, QDebugManip qdm)
+//! Suppress full qualification of enums/QFlags when logging
+inline QDebug terse(QDebug dbg)
{
- return qdm(debug_object);
+ return
+#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
+ dbg.setVerbosity(0), dbg;
+#else
+ dbg.verbosity(QDebug::MinimumVerbosity);
+#endif
}
inline qint64 profilerMinNsecs()
@@ -66,8 +62,19 @@ inline qint64 profilerMinNsecs()
* 1000;
}
} // namespace Quotient
-/// \deprecated Use namespace Quotient instead
-namespace QMatrixClient = Quotient;
+
+/**
+ * @brief A helper operator to facilitate usage of formatJson (and possibly
+ * other manipulators)
+ *
+ * @param debug_object to output the json to
+ * @param qdm a QDebug manipulator
+ * @return a copy of debug_object that has its mode altered by qdm
+ */
+inline QDebug operator<<(QDebug debug_object, Quotient::QDebugManip qdm)
+{
+ return qdm(debug_object);
+}
inline QDebug operator<<(QDebug debug_object, const QElapsedTimer& et)
{
diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp
new file mode 100644
index 00000000..0b6643fc
--- /dev/null
+++ b/lib/mxcreply.cpp
@@ -0,0 +1,70 @@
+// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "mxcreply.h"
+
+#include "room.h"
+
+using namespace Quotient;
+
+class MxcReply::Private
+{
+public:
+ explicit Private(QNetworkReply* r = nullptr)
+ : m_reply(r)
+ {}
+ QNetworkReply* m_reply;
+};
+
+MxcReply::MxcReply(QNetworkReply* reply)
+ : d(std::make_unique<Private>(reply))
+{
+ reply->setParent(this);
+ connect(d->m_reply, &QNetworkReply::finished, this, [this]() {
+ setError(d->m_reply->error(), d->m_reply->errorString());
+ setOpenMode(ReadOnly);
+ Q_EMIT finished();
+ });
+}
+
+MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId)
+ : d(std::make_unique<Private>(reply))
+{
+ reply->setParent(this);
+ connect(d->m_reply, &QNetworkReply::finished, this, [this, room, eventId]() {
+ setError(d->m_reply->error(), d->m_reply->errorString());
+ setOpenMode(ReadOnly);
+ emit finished();
+ });
+}
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
+#define ERROR_SIGNAL errorOccurred
+#else
+#define ERROR_SIGNAL error
+#endif
+
+MxcReply::MxcReply()
+{
+ static const auto BadRequestPhrase = tr("Bad Request");
+ QMetaObject::invokeMethod(this, [this]() {
+ setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 400);
+ setAttribute(QNetworkRequest::HttpReasonPhraseAttribute,
+ BadRequestPhrase);
+ setError(QNetworkReply::ProtocolInvalidOperationError,
+ BadRequestPhrase);
+ setFinished(true);
+ emit ERROR_SIGNAL(QNetworkReply::ProtocolInvalidOperationError);
+ emit finished();
+ }, Qt::QueuedConnection);
+}
+
+qint64 MxcReply::readData(char *data, qint64 maxSize)
+{
+ return d->m_reply->read(data, maxSize);
+}
+
+void MxcReply::abort()
+{
+ d->m_reply->abort();
+}
diff --git a/lib/mxcreply.h b/lib/mxcreply.h
new file mode 100644
index 00000000..efaf01c6
--- /dev/null
+++ b/lib/mxcreply.h
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QtNetwork/QNetworkReply>
+#include <memory>
+
+namespace Quotient {
+class Room;
+
+class MxcReply : public QNetworkReply
+{
+public:
+ explicit MxcReply();
+ explicit MxcReply(QNetworkReply *reply);
+ MxcReply(QNetworkReply* reply, Room* room, const QString& eventId);
+
+public Q_SLOTS:
+ void abort() override;
+
+protected:
+ qint64 readData(char *data, qint64 maxSize) override;
+
+private:
+ class Private;
+ std::unique_ptr<Private> d;
+};
+}
diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp
index a94ead34..57618329 100644
--- a/lib/networkaccessmanager.cpp
+++ b/lib/networkaccessmanager.cpp
@@ -3,18 +3,43 @@
#include "networkaccessmanager.h"
+#include "connection.h"
+#include "room.h"
+#include "accountregistry.h"
+#include "mxcreply.h"
+
#include <QtCore/QCoreApplication>
+#include <QtCore/QThreadStorage>
+#include <QtCore/QSettings>
#include <QtNetwork/QNetworkReply>
using namespace Quotient;
class NetworkAccessManager::Private {
public:
+ explicit Private(NetworkAccessManager* q)
+ : q(q)
+ {}
+
+ QNetworkReply* createImplRequest(Operation op,
+ const QNetworkRequest& outerRequest,
+ Connection* connection)
+ {
+ Q_ASSERT(outerRequest.url().scheme() == "mxc");
+ QNetworkRequest r(outerRequest);
+ r.setUrl(QUrl(QStringLiteral("%1/_matrix/media/r0/download/%2")
+ .arg(connection->homeserver().toString(),
+ outerRequest.url().authority()
+ + outerRequest.url().path())));
+ return q->createRequest(op, r);
+ }
+
+ NetworkAccessManager* q;
QList<QSslError> ignoredSslErrors;
};
NetworkAccessManager::NetworkAccessManager(QObject* parent)
- : QNetworkAccessManager(parent), d(std::make_unique<Private>())
+ : QNetworkAccessManager(parent), d(std::make_unique<Private>(this))
{}
QList<QSslError> NetworkAccessManager::ignoredSslErrors() const
@@ -34,7 +59,7 @@ void NetworkAccessManager::clearIgnoredSslErrors()
static NetworkAccessManager* createNam()
{
- auto nam = new NetworkAccessManager(QCoreApplication::instance());
+ auto nam = new NetworkAccessManager();
#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
// See #109; in newer Qt, bearer management is deprecated altogether
NetworkAccessManager::connect(nam,
@@ -47,8 +72,11 @@ static NetworkAccessManager* createNam()
NetworkAccessManager* NetworkAccessManager::instance()
{
- static auto* nam = createNam();
- return nam;
+ static QThreadStorage<NetworkAccessManager*> storage;
+ if(!storage.hasLocalData()) {
+ storage.setLocalData(createNam());
+ }
+ return storage.localData();
}
NetworkAccessManager::~NetworkAccessManager() = default;
@@ -56,7 +84,49 @@ NetworkAccessManager::~NetworkAccessManager() = default;
QNetworkReply* NetworkAccessManager::createRequest(
Operation op, const QNetworkRequest& request, QIODevice* outgoingData)
{
+ const auto& mxcUrl = request.url();
+ if (mxcUrl.scheme() == "mxc") {
+ const QUrlQuery query(mxcUrl.query());
+ const auto accountId = query.queryItemValue(QStringLiteral("user_id"));
+ if (accountId.isEmpty()) {
+ // Using QSettings here because Quotient::NetworkSettings
+ // doesn't provide multithreading guarantees
+ static thread_local QSettings s;
+ if (!s.value("Network/allow_direct_media_requests").toBool()) {
+ qCWarning(NETWORK) << "No connection specified";
+ return new MxcReply();
+ }
+ // TODO: Make the best effort with a direct unauthenticated request
+ // to the media server
+ } else {
+ auto* const connection = AccountRegistry::instance().get(accountId);
+ if (!connection) {
+ qCWarning(NETWORK) << "Connection" << accountId << "not found";
+ return new MxcReply();
+ }
+ const auto roomId = query.queryItemValue(QStringLiteral("room_id"));
+ if (!roomId.isEmpty()) {
+ auto room = connection->room(roomId);
+ if (!room) {
+ qCWarning(NETWORK) << "Room" << roomId << "not found";
+ return new MxcReply();
+ }
+ return new MxcReply(
+ d->createImplRequest(op, request, connection), room,
+ query.queryItemValue(QStringLiteral("event_id")));
+ }
+ return new MxcReply(
+ d->createImplRequest(op, request, connection));
+ }
+ }
auto reply = QNetworkAccessManager::createRequest(op, request, outgoingData);
reply->ignoreSslErrors(d->ignoredSslErrors);
return reply;
}
+
+QStringList NetworkAccessManager::supportedSchemesImplementation() const
+{
+ auto schemes = QNetworkAccessManager::supportedSchemesImplementation();
+ schemes += QStringLiteral("mxc");
+ return schemes;
+}
diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h
index 47729a1b..87bc12a1 100644
--- a/lib/networkaccessmanager.h
+++ b/lib/networkaccessmanager.h
@@ -8,6 +8,8 @@
#include <memory>
namespace Quotient {
+class Room;
+class Connection;
class NetworkAccessManager : public QNetworkAccessManager {
Q_OBJECT
public:
@@ -21,6 +23,9 @@ public:
/** Get a pointer to the singleton */
static NetworkAccessManager* instance();
+public Q_SLOTS:
+ QStringList supportedSchemesImplementation() const;
+
private:
QNetworkReply* createRequest(Operation op, const QNetworkRequest& request,
QIODevice* outgoingData = Q_NULLPTR) override;
diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h
index c6fa037a..86593cc8 100644
--- a/lib/qt_connection_util.h
+++ b/lib/qt_connection_util.h
@@ -3,7 +3,7 @@
#pragma once
-#include "util.h"
+#include "function_traits.h"
#include <QtCore/QPointer>
@@ -19,12 +19,7 @@ namespace _impl {
decorated_slot_tt<ArgTs...> decoratedSlot,
Qt::ConnectionType connType)
{
- // See https://bugreports.qt.io/browse/QTBUG-60339
-#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
- auto pc = std::make_shared<QMetaObject::Connection>();
-#else
auto pc = std::make_unique<QMetaObject::Connection>();
-#endif
auto& c = *pc; // Resolve a reference before pc is moved to lambda
// Perfect forwarding doesn't work through signal-slot connections -
@@ -73,6 +68,17 @@ namespace _impl {
}),
connType);
}
+
+ // TODO: get rid of it as soon as Apple Clang gets proper deduction guides
+ // for std::function<>
+ // ...or consider using QtPrivate magic used by QObject::connect()
+ // ...for inspiration, also check a possible std::not_fn implementation
+ // at https://en.cppreference.com/w/cpp/utility/functional/not_fn
+ template <typename FnT>
+ inline auto wrap_in_function(FnT&& f)
+ {
+ return typename function_traits<FnT>::function_type(std::forward<FnT>(f));
+ }
} // namespace _impl
/*! \brief Create a connection that self-disconnects when its "slot" returns true
@@ -90,7 +96,7 @@ inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context,
const FunctorT& slot,
Qt::ConnectionType connType = Qt::AutoConnection)
{
- return _impl::connectUntil(sender, signal, context, wrap_in_function(slot),
+ return _impl::connectUntil(sender, signal, context, _impl::wrap_in_function(slot),
connType);
}
@@ -100,8 +106,13 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal,
ContextT* context, const FunctorT& slot,
Qt::ConnectionType connType = Qt::AutoConnection)
{
+#if QT_VERSION_MAJOR >= 6
+ return QObject::connect(sender, signal, context, slot,
+ Qt::ConnectionType(connType
+ | Qt::SingleShotConnection));
+#else
return _impl::connectSingleShot(
- sender, signal, context, wrap_in_function(slot), connType);
+ sender, signal, context, _impl::wrap_in_function(slot), connType);
}
// Specialisation for usual Qt slots passed as pointers-to-members.
@@ -114,11 +125,12 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal,
{
// TODO: when switching to C++20, use std::bind_front() instead
return _impl::connectSingleShot(sender, signal, receiver,
- wrap_in_function(
+ _impl::wrap_in_function(
[receiver, slot](const ArgTs&... args) {
(receiver->*slot)(args...);
}),
connType);
+#endif
}
/*! \brief A guard pointer that disconnects an interested object upon destruction
diff --git a/lib/quotient_common.h b/lib/quotient_common.h
index 22fdbe94..0e3e2a40 100644
--- a/lib/quotient_common.h
+++ b/lib/quotient_common.h
@@ -5,18 +5,95 @@
#include <qobjectdefs.h>
+#include <array>
+
+// See https://bugreports.qt.io/browse/QTBUG-82295 - despite the comment that
+// Q_FLAG[_NS] "should" be applied to the enum only, Qt doesn't allow to wrap
+// a flag type into a QVariant then. The macros below define Q_FLAG[_NS] and on
+// top of that add Q_ENUM[_NS]_IMPL which is a part of Q_ENUM() macro that
+// enables the metatype data but goes under the moc radar to avoid double
+// registration of the same data in the map defined in moc_*.cpp
+#define QUO_DECLARE_FLAGS(Flags, Enum) \
+ Q_DECLARE_FLAGS(Flags, Enum) \
+ Q_ENUM_IMPL(Enum) \
+ Q_FLAG(Flags)
+
+#define QUO_DECLARE_FLAGS_NS(Flags, Enum) \
+ Q_DECLARE_FLAGS(Flags, Enum) \
+ Q_ENUM_NS_IMPL(Enum) \
+ Q_FLAG_NS(Flags)
+
+#define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \
+ Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended
+
namespace Quotient {
Q_NAMESPACE
-/** Enumeration with flags defining the network job running policy
- * So far only background/foreground flags are available.
- *
- * \sa Connection::callApi, Connection::run
- */
-enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 };
+// std::array {} needs explicit template parameters on macOS because
+// Apple stdlib doesn't have deduction guides for std::array. C++20 has
+// to_array() but that can't be borrowed, this time because of MSVC:
+// https://developercommunity.visualstudio.com/t/vc-ice-p1-initc-line-3652-from-stdto-array/1464038
+// Therefore a simpler (but also slightly more wobbly - it resolves the element
+// type using std::common_type<>) make_array facility is implemented here.
+template <typename... Ts>
+constexpr auto make_array(Ts&&... items)
+{
+ return std::array<std::common_type_t<Ts...>, sizeof...(items)>(
+ { std::forward<Ts>(items)... });
+}
+
+// TODO: code like this should be generated from the CS API definition
+
+//! \brief Membership states
+//!
+//! These are used for member events. The names here are case-insensitively
+//! equal to state names used on the wire.
+//! \sa MemberEventContent, RoomMemberEvent
+enum class Membership : unsigned int {
+ // Specific power-of-2 values (1,2,4,...) are important here as syncdata.cpp
+ // depends on that, as well as Join being the first in line
+ Invalid = 0x0,
+ Join = 0x1,
+ Leave = 0x2,
+ Invite = 0x4,
+ Knock = 0x8,
+ Ban = 0x10,
+ Undefined = Invalid
+};
+QUO_DECLARE_FLAGS_NS(MembershipMask, Membership)
+constexpr inline auto MembershipStrings = make_array(
+ // The order MUST be the same as the order in the original enum
+ "join", "leave", "invite", "knock", "ban");
+
+//! \brief Local user join-state names
+//!
+//! This represents a subset of Membership values that may arrive as the local
+//! user's state grouping for the sync response.
+//! \sa SyncData
+enum class JoinState : std::underlying_type_t<Membership> {
+ Invalid = std::underlying_type_t<Membership>(Membership::Invalid),
+ Join = std::underlying_type_t<Membership>(Membership::Join),
+ Leave = std::underlying_type_t<Membership>(Membership::Leave),
+ Invite = std::underlying_type_t<Membership>(Membership::Invite),
+ Knock = std::underlying_type_t<Membership>(Membership::Knock),
+};
+QUO_DECLARE_FLAGS_NS(JoinStates, JoinState)
+
+constexpr inline auto JoinStateStrings = make_array(
+ MembershipStrings[0], MembershipStrings[1], MembershipStrings[2],
+ MembershipStrings[3] /* same as MembershipStrings, sans "ban" */
+);
+
+//! \brief Network job running policy flags
+//!
+//! So far only background/foreground flags are available.
+//! \sa Connection::callApi, Connection::run
+enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 };
Q_ENUM_NS(RunningPolicy)
+//! \brief The result of URI resolution using UriResolver
+//! \sa UriResolver
enum UriResolveResult : short {
StillResolving = -1,
UriResolved = 0,
@@ -27,6 +104,16 @@ enum UriResolveResult : short {
};
Q_ENUM_NS(UriResolveResult)
+enum RoomType {
+ Space,
+ Undefined,
+};
+Q_ENUM_NS(RoomType)
+
+constexpr inline auto RoomTypeStrings = make_array(
+ "m.space"
+);
+
} // namespace Quotient
-/// \deprecated Use namespace Quotient instead
-namespace QMatrixClient = Quotient;
+Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask)
+Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::JoinStates)
diff --git a/lib/room.cpp b/lib/room.cpp
index 2a9cc0d8..6854879c 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -15,6 +15,10 @@
#include "e2ee.h"
#include "syncdata.h"
#include "user.h"
+#include "eventstats.h"
+
+// NB: since Qt 6, moc_room.cpp needs User fully defined
+#include "moc_room.cpp"
#include "csapi/account-data.h"
#include "csapi/banning.h"
@@ -51,7 +55,6 @@
#include <QtCore/QDir>
#include <QtCore/QHash>
-#include <QtCore/QMimeDatabase>
#include <QtCore/QPointer>
#include <QtCore/QRegularExpression>
#include <QtCore/QStringBuilder> // for efficient string concats (operator%)
@@ -98,8 +101,8 @@ public:
UnorderedMap<StateEventKey, StateEventPtr> baseState;
/// State event stubs - events without content, just type and state key
static decltype(baseState) stubbedState;
- /// The state of the room at timeline position after-maxTimelineIndex()
- /// \sa Room::syncEdge
+ /// The state of the room at syncEdge()
+ /// \sa syncEdge
QHash<StateEventKey, const StateEventBase*> currentState;
/// Servers with aliases for this room except the one of the local user
/// \sa Room::remoteAliases
@@ -114,19 +117,21 @@ public:
QHash<QPair<QString, QString>, RelatedEvents> relations;
QString displayname;
Avatar avatar;
- int highlightCount = 0;
- int notificationCount = 0;
+ QHash<QString, Notification> notifications;
+ qsizetype serverHighlightCount = 0;
+ // Starting up with estimate event statistics as there's zero knowledge
+ // about the timeline.
+ EventStats partiallyReadStats {}, unreadStats {};
members_map_t membersMap;
QList<User*> usersTyping;
- QMultiHash<QString, User*> eventIdReadUsers;
+ QHash<QString, QSet<QString>> eventIdReadUsers;
QList<User*> usersInvited;
QList<User*> membersLeft;
- int unreadMessages = 0;
bool displayed = false;
QString firstDisplayedEventId;
QString lastDisplayedEventId;
- QHash<const User*, QString> lastReadEventIds;
- QString serverReadMarker;
+ QHash<QString, ReadReceipt> lastReadReceipts;
+ QString fullyReadUntilEventId;
TagsMap tags;
UnorderedMap<QString, EventPtr> accountData;
QString prevBatch;
@@ -195,6 +200,8 @@ public:
/// A point in the timeline corresponding to baseState
rev_iter_t timelineBase() const { return q->findInTimeline(-1); }
+ rev_iter_t historyEdge() const { return timeline.crend(); }
+ Timeline::const_iterator syncEdge() const { return timeline.cend(); }
void getPreviousContent(int limit = 10, const QString &filter = {});
@@ -210,6 +217,7 @@ public:
evtKey.second));
qCDebug(STATE) << "A new stub event created for key {"
<< evtKey.first << evtKey.second << "}";
+ qCDebug(STATE) << "Stubbed state size:" << stubbedState.size();
}
evt = stubbedState[evtKey].get();
Q_ASSERT(evt);
@@ -219,6 +227,16 @@ public:
return evt;
}
+ QVector<const StateEventBase*> stateEventsOfType(const QString& evtType) const
+ {
+ auto vals = QVector<const StateEventBase*>();
+ for (auto it = currentState.cbegin(); it != currentState.cend(); ++it)
+ if (it.key().first == evtType)
+ vals.append(it.value());
+
+ return vals;
+ }
+
template <typename EventT>
const EventT* getCurrentState(const QString& stateKey = {}) const
{
@@ -237,39 +255,34 @@ public:
// return EventT::content_type()
// }
- bool isEventNotable(const TimelineItem& ti) const
- {
- return !ti->isRedacted() && ti->senderId() != connection->userId()
- && is<RoomMessageEvent>(*ti)
- && ti.viewAs<RoomMessageEvent>()->replacedEvent().isEmpty();
- }
-
template <typename EventArrayT>
Changes updateStateFrom(EventArrayT&& events)
{
- Changes changes = NoChange;
+ Changes changes {};
if (!events.empty()) {
QElapsedTimer et;
et.start();
for (auto&& eptr : events) {
const auto& evt = *eptr;
Q_ASSERT(evt.isStateEvent());
- auto change = q->processStateEvent(evt);
- if (change != NoChange) {
+ if (auto change = q->processStateEvent(evt); change) {
changes |= change;
baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr);
}
}
if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
qCDebug(PROFILER)
- << "*** Room::Private::updateStateFrom():" << events.size()
- << "event(s)," << et;
+ << "Updated" << q->objectName() << "room state from"
+ << events.size() << "event(s) in" << et;
}
return changes;
}
Changes addNewMessageEvents(RoomEvents&& events);
void addHistoricalMessageEvents(RoomEvents&& events);
+ Changes updateStatsFromSyncData(const SyncRoomData &data, bool fromCache);
+ void postprocessChanges(Changes changes, bool saveState = true);
+
/** Move events into the timeline
*
* Insert events into the timeline, either new or historical.
@@ -287,11 +300,12 @@ public:
*/
void dropDuplicateEvents(RoomEvents& events) const;
- Changes setLastReadEvent(User* u, QString eventId);
- void updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to);
- Changes promoteReadMarker(User* u, const rev_iter_t& newMarker, bool force = false);
-
- Changes markMessagesAsRead(rev_iter_t upToMarker);
+ Changes setLastReadReceipt(const QString& userId, rev_iter_t newMarker,
+ ReadReceipt newReceipt = {},
+ bool deferStatsUpdate = false);
+ Changes setFullyReadMarker(const QString &eventId);
+ Changes updateStats(const rev_iter_t& from, const rev_iter_t& to);
+ bool markMessagesAsRead(const rev_iter_t& upToMarker);
void getAllMembers();
@@ -303,6 +317,8 @@ public:
return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...));
}
+ QString doPostFile(RoomEventPtr &&msgEvent, const QUrl &localUrl);
+
RoomEvent* addAsPending(RoomEventPtr&& event);
QString doSendEvent(const RoomEvent* pEvent);
@@ -455,7 +471,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
emit baseStateLoaded();
return this == r; // loadedRoomState fires only once per room
});
- qCDebug(STATE) << "New" << toCString(initialJoinState) << "Room:" << id;
+ qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id;
}
Room::~Room() { delete d; }
@@ -535,21 +551,6 @@ QStringList Room::altAliases() const
return d->getCurrentState<RoomCanonicalAliasEvent>()->altAliases();
}
-QStringList Room::localAliases() const
-{
- return d->getCurrentState<RoomAliasesEvent>(
- connection()->domain())
- ->aliases();
-}
-
-QStringList Room::remoteAliases() const
-{
- QStringList result;
- for (const auto& s : std::as_const(d->aliasServers))
- result += d->getCurrentState<RoomAliasesEvent>(s)->aliases();
- return result;
-}
-
QString Room::canonicalAlias() const
{
return d->getCurrentState<RoomCanonicalAliasEvent>()->alias();
@@ -574,6 +575,11 @@ QVector< const Quotient::RoomEvent* > Quotient::Room::pinnedEvents() const
return pinnedEvents;
}
+QString Room::displayNameForHtml() const
+{
+ return displayName().toHtmlEscaped();
+}
+
void Room::refreshDisplayName() { d->updateDisplayname(); }
QString Room::topic() const
@@ -593,13 +599,13 @@ QImage Room::avatar(int width, int height)
{
if (!d->avatar.url().isEmpty())
return d->avatar.get(connection(), width, height,
- [=] { emit avatarChanged(); });
+ [this] { emit avatarChanged(); });
// Use the first (excluding self) user's avatar for direct chats
const auto dcUsers = directChatUsers();
for (auto* u : dcUsers)
if (u != localUser())
- return u->avatar(width, height, this, [=] { emit avatarChanged(); });
+ return u->avatar(width, height, this, [this] { emit avatarChanged(); });
return {};
}
@@ -615,6 +621,16 @@ JoinState Room::memberJoinState(User* user) const
: JoinState::Leave;
}
+Membership Room::memberState(const QString& userId) const
+{
+ return d->getCurrentState<RoomMemberEvent>(userId)->membership();
+}
+
+bool Room::isMember(const QString& userId) const
+{
+ return memberState(userId) == Membership::Join;
+}
+
JoinState Room::joinState() const { return d->joinState; }
void Room::setJoinState(JoinState state)
@@ -623,156 +639,261 @@ void Room::setJoinState(JoinState state)
if (state == oldState)
return;
d->joinState = state;
- qCDebug(STATE) << "Room" << id() << "changed state: " << int(oldState)
- << "->" << int(state);
- emit changed(Change::JoinStateChange);
+ qCDebug(STATE) << "Room" << id() << "changed state: " << terse << oldState
+ << "->" << state;
emit joinStateChanged(oldState, state);
}
-Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId)
-{
- auto& storedId = lastReadEventIds[u];
- if (storedId == eventId)
- return Change::NoChange;
- eventIdReadUsers.remove(storedId, u);
- eventIdReadUsers.insert(eventId, u);
- swap(storedId, eventId);
- emit q->lastReadEventChanged(u);
- emit q->readMarkerForUserMoved(u, eventId, storedId);
- if (isLocalUser(u)) {
- if (storedId != serverReadMarker)
- connection->callApi<SetReadMarkerJob>(BackgroundRequest, id,
- storedId);
- emit q->readMarkerMoved(eventId, storedId);
- return Change::ReadMarkerChange;
+Room::Changes Room::Private::setLastReadReceipt(const QString& userId,
+ rev_iter_t newMarker,
+ ReadReceipt newReceipt,
+ bool deferStatsUpdate)
+{
+ if (newMarker == historyEdge() && !newReceipt.eventId.isEmpty())
+ newMarker = q->findInTimeline(newReceipt.eventId);
+ if (newMarker != historyEdge()) {
+ // Try to auto-promote the read marker over the user's own messages
+ // (switch to direct iterators for that).
+ const auto eagerMarker = find_if(newMarker.base(), syncEdge(),
+ [=](const TimelineItem& ti) {
+ return ti->senderId() != userId;
+ });
+ // eagerMarker is now just after the desired event for newMarker
+ if (eagerMarker != newMarker.base()) {
+ newMarker = rev_iter_t(eagerMarker);
+ qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId
+ << "to" << *newMarker;
+ }
+ // Fill newReceipt with the event (and, if needed, timestamp) from
+ // eagerMarker
+ newReceipt.eventId = (eagerMarker - 1)->event()->id();
+ if (newReceipt.timestamp.isNull())
+ newReceipt.timestamp = QDateTime::currentDateTime();
+ }
+ auto& storedReceipt =
+ lastReadReceipts[userId]; // clazy:exclude=detaching-member
+ const auto prevEventId = storedReceipt.eventId;
+ // Check that either the new marker is actually "newer" than the current one
+ // or, if both markers are at historyEdge(), event ids are different.
+ // NB: with reverse iterators, timeline history edge >= sync edge
+ if (prevEventId == newReceipt.eventId
+ || newMarker > q->findInTimeline(prevEventId))
+ return Change::None;
+
+ // Finally make the change
+
+ Changes changes = Change::Other;
+ auto oldEventReadUsersIt =
+ eventIdReadUsers.find(prevEventId); // clazy:exclude=detaching-member
+ if (oldEventReadUsersIt != eventIdReadUsers.end()) {
+ oldEventReadUsersIt->remove(userId);
+ if (oldEventReadUsersIt->isEmpty())
+ eventIdReadUsers.erase(oldEventReadUsersIt);
+ }
+ eventIdReadUsers[newReceipt.eventId].insert(userId);
+ storedReceipt = move(newReceipt);
+
+ {
+ auto dbg = qDebug(EPHEMERAL); // This trick needs qDebug, not qCDebug
+ dbg << "The new read receipt for" << userId << "is now at";
+ if (newMarker == historyEdge())
+ dbg << storedReceipt.eventId;
+ else
+ dbg << *newMarker;
+ }
+
+ // TODO: use Room::member() when it becomes a thing and only emit signals
+ // for actual members, not just any user
+ const auto member = q->user(userId);
+ Q_ASSERT(member != nullptr);
+ if (isLocalUser(member) && !deferStatsUpdate) {
+ if (unreadStats.updateOnMarkerMove(q, q->findInTimeline(prevEventId),
+ newMarker)) {
+ qCDebug(MESSAGES)
+ << "Updated unread event statistics in" << q->objectName()
+ << "after moving the local read receipt:" << unreadStats;
+ changes |= Change::UnreadStats;
+ }
+ Q_ASSERT(unreadStats.isValidFor(q, newMarker)); // post-check
}
- return Change::NoChange;
+ emit q->lastReadEventChanged(member);
+ // TODO: remove in 0.8
+ if (!isLocalUser(member))
+ emit q->readMarkerForUserMoved(member, prevEventId,
+ storedReceipt.eventId);
+ return changes;
}
-void Room::Private::updateUnreadCount(const rev_iter_t& from,
- const rev_iter_t& to)
+Room::Changes Room::Private::updateStats(const rev_iter_t& from,
+ const rev_iter_t& to)
{
Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend());
Q_ASSERT(to >= from && to <= timeline.crend());
- // Catch a special case when the last read event id refers to an event
- // that has just arrived. In this case we should recalculate
- // unreadMessages and might need to promote the read marker further
- // over local-origin messages.
- auto readMarker = q->readMarker();
- if (readMarker == timeline.crend() && q->allHistoryLoaded())
- --readMarker; // Read marker not found in the timeline, initialise it
- if (readMarker >= from && readMarker < to) {
- promoteReadMarker(q->localUser(), readMarker, true);
- return;
+ const auto fullyReadMarker = q->fullyReadMarker();
+ auto readReceiptMarker = q->localReadReceiptMarker();
+ Changes changes = Change::None;
+ // Correct the read receipt to never be behind the fully read marker
+ if (readReceiptMarker > fullyReadMarker
+ && setLastReadReceipt(connection->userId(), fullyReadMarker, {}, true)) {
+ changes |= Change::Other;
+ readReceiptMarker = q->localReadReceiptMarker();
+ qCInfo(MESSAGES) << "The local m.read receipt was behind m.fully_read "
+ "marker - it's now corrected to be at index"
+ << readReceiptMarker->index();
+ }
+
+ if (fullyReadMarker < from)
+ return Change::None; // What's arrived is already fully read
+
+ // If there's no read marker in the whole room, initialise it
+ if (fullyReadMarker == historyEdge() && q->allHistoryLoaded())
+ return setFullyReadMarker(timeline.front()->id());
+
+ // Catch a case when the id in the last fully read marker or the local read
+ // receipt refers to an event that has just arrived. In this case either
+ // one (unreadStats) or both statistics should be recalculated to get
+ // an exact number instead of an estimation (see documentation on
+ // EventStats::isEstimate). For the same reason (switching from the
+ // estimate to the exact number) this branch forces returning
+ // Change::UnreadStats and also possibly Change::PartiallyReadStats, even if
+ // the estimation luckily matched the exact result.
+ if (readReceiptMarker < to || changes /*i.e. read receipt was corrected*/) {
+ unreadStats = EventStats::fromMarker(q, readReceiptMarker);
+ Q_ASSERT(!unreadStats.isEstimate);
+ qCDebug(MESSAGES).nospace() << "Recalculated unread event statistics in"
+ << q->objectName() << ": " << unreadStats;
+ changes |= Change::UnreadStats;
+ if (fullyReadMarker < to) {
+ // Add up to unreadStats instead of counting same events again
+ partiallyReadStats = EventStats::fromRange(q, readReceiptMarker,
+ q->fullyReadMarker(),
+ unreadStats);
+ Q_ASSERT(!partiallyReadStats.isEstimate);
+
+ qCDebug(MESSAGES).nospace()
+ << "Recalculated partially read event statistics in "
+ << q->objectName() << ": " << partiallyReadStats;
+ return changes | Change::PartiallyReadStats;
+ }
}
- Q_ASSERT(to <= readMarker);
-
- QElapsedTimer et;
- et.start();
- const auto newUnreadMessages =
- count_if(from, to, std::bind(&Room::Private::isEventNotable, this, _1));
- if (et.nsecsElapsed() > profilerMinNsecs() / 10)
- qCDebug(PROFILER) << "Counting gained unread messages took" << et;
-
- if (newUnreadMessages > 0) {
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- if (unreadMessages < 0)
- unreadMessages = 0;
-
- unreadMessages += newUnreadMessages;
- qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained"
- << newUnreadMessages << "unread message(s),"
- << (q->readMarker() == timeline.crend()
- ? "in total at least"
- : "in total")
- << unreadMessages << "unread message(s)";
- emit q->unreadMessagesChanged(q);
- }
-}
-
-Room::Changes Room::Private::promoteReadMarker(User* u,
- const rev_iter_t& newMarker,
- bool force)
-{
- Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr");
- Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend());
-
- const auto prevMarker = q->readMarker(u);
- if (!force && prevMarker <= newMarker) // Remember, we deal with reverse
- // iterators
- return Change::NoChange;
-
- Q_ASSERT(newMarker < timeline.crend());
-
- // Try to auto-promote the read marker over the user's own messages
- // (switch to direct iterators for that).
- auto eagerMarker =
- find_if(newMarker.base(), timeline.cend(), [=](const TimelineItem& ti) {
- return ti->senderId() != u->id();
- });
+ // As of here, at least the fully read marker (but maybe also read receipt)
+ // points to somewhere beyond the "oldest" message from the arrived batch -
+ // add up newly arrived messages to the current stats, instead of a complete
+ // recalculation.
+ Q_ASSERT(fullyReadMarker >= to);
- auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id());
- if (isLocalUser(u)) {
- const auto oldUnreadCount = unreadMessages;
- QElapsedTimer et;
- et.start();
- unreadMessages =
- int(count_if(eagerMarker, timeline.cend(),
- [this](const auto& ti) { return isEventNotable(ti); }));
- if (et.nsecsElapsed() > profilerMinNsecs() / 10)
- qCDebug(PROFILER) << "Recounting unread messages took" << et;
+ const auto newStats = EventStats::fromRange(q, from, to);
+ Q_ASSERT(!newStats.isEstimate);
+ if (newStats.empty())
+ return changes;
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- if (unreadMessages == 0)
- unreadMessages = -1;
+ const auto doAddStats = [this, &changes, newStats](EventStats& s,
+ const rev_iter_t& marker,
+ Change c) {
+ s.notableCount += newStats.notableCount;
+ s.highlightCount += newStats.highlightCount;
+ if (!s.isEstimate)
+ s.isEstimate = marker == historyEdge();
+ changes |= c;
+ };
- if (force || unreadMessages != oldUnreadCount) {
- if (unreadMessages == -1) {
- qCDebug(MESSAGES)
- << "Room" << displayname << "has no more unread messages";
- } else
- qCDebug(MESSAGES) << "Room" << displayname << "still has"
- << unreadMessages << "unread message(s)";
- emit q->unreadMessagesChanged(q);
- changes |= Change::UnreadNotifsChange;
- }
+ doAddStats(partiallyReadStats, fullyReadMarker, Change::PartiallyReadStats);
+ if (readReceiptMarker >= to) {
+ // readReceiptMarker < to branch shouldn't have been entered
+ Q_ASSERT(!changes.testFlag(Change::UnreadStats));
+ doAddStats(unreadStats, readReceiptMarker, Change::UnreadStats);
}
+ qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" << newStats
+ << "notable/highlighted event(s); total statistics:"
+ << partiallyReadStats << "since the fully read marker,"
+ << unreadStats << "since read receipt";
+
+ // Check invariants
+ Q_ASSERT(partiallyReadStats.isValidFor(q, fullyReadMarker));
+ Q_ASSERT(unreadStats.isValidFor(q, readReceiptMarker));
return changes;
}
-Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
+Room::Changes Room::Private::setFullyReadMarker(const QString& eventId)
{
- const auto prevMarker = q->readMarker();
- auto changes = promoteReadMarker(q->localUser(), upToMarker);
- if (prevMarker != upToMarker)
- qCDebug(MESSAGES) << "Marked messages as read until" << *q->readMarker();
+ if (fullyReadUntilEventId == eventId)
+ return Change::None;
- // We shouldn't send read receipts for the local user's own messages - so
- // search earlier messages for the latest message not from the local user
- // until the previous last-read message, whichever comes first.
- for (; upToMarker < prevMarker; ++upToMarker) {
- if ((*upToMarker)->senderId() != q->localUser()->id()) {
- connection->callApi<PostReceiptJob>(BackgroundRequest,
- id, QStringLiteral("m.read"),
- QUrl::toPercentEncoding(
- (*upToMarker)->id()));
- break;
+ const auto prevReadMarker = q->fullyReadMarker();
+ const auto newReadMarker = q->findInTimeline(eventId);
+ if (newReadMarker > prevReadMarker)
+ return Change::None;
+
+ const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId);
+ qCDebug(MESSAGES) << "Fully read marker in" << q->objectName() //
+ << "set to" << fullyReadUntilEventId;
+
+ QT_IGNORE_DEPRECATIONS(Changes changes = Change::ReadMarker|Change::Other;)
+ if (const auto rm = q->fullyReadMarker(); rm != historyEdge()) {
+ // Pull read receipt if it's behind, and update statistics
+ changes |= setLastReadReceipt(connection->userId(), rm);
+ if (partiallyReadStats.updateOnMarkerMove(q, prevReadMarker, rm)) {
+ changes |= Change::PartiallyReadStats;
+ qCDebug(MESSAGES)
+ << "Updated partially read event statistics in"
+ << q->objectName()
+ << "after moving m.fully_read marker: " << partiallyReadStats;
}
+ Q_ASSERT(partiallyReadStats.isValidFor(q, rm)); // post-check
}
+ emit q->fullyReadMarkerMoved(prevFullyReadId, fullyReadUntilEventId);
+ // TODO: Remove in 0.8
+ emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId);
return changes;
}
-void Room::markMessagesAsRead(QString uptoEventId)
+void Room::setReadReceipt(const QString& atEventId)
+{
+ if (const auto changes = d->setLastReadReceipt(localUser()->id(),
+ historyEdge(),
+ { atEventId })) {
+ connection()->callApi<PostReceiptJob>(BackgroundRequest, id(),
+ QStringLiteral("m.read"),
+ QUrl::toPercentEncoding(atEventId));
+ d->postprocessChanges(changes);
+ } else
+ qCDebug(EPHEMERAL) << "The new read receipt for" << localUser()->id()
+ << "in" << objectName()
+ << "is at or behind the old one, skipping";
+}
+
+bool Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker)
+{
+ if (upToMarker == q->historyEdge())
+ qCWarning(MESSAGES) << "Cannot mark an unknown event in"
+ << q->objectName() << "as fully read";
+ else if (const auto changes = setFullyReadMarker(upToMarker->event()->id())) {
+ // The assumption below is that if a read receipt was sent on a newer
+ // event, the homeserver will keep it there instead of reverting to
+ // m.fully_read
+ connection->callApi<SetReadMarkerJob>(BackgroundRequest, id,
+ fullyReadUntilEventId,
+ fullyReadUntilEventId);
+ postprocessChanges(changes);
+ return true;
+ } else
+ qCDebug(MESSAGES) << "Event" << *upToMarker << "in" << q->objectName()
+ << "is behind the current fully read marker at"
+ << *q->fullyReadMarker()
+ << "- won't move fully read marker back in timeline";
+ return false;
+}
+
+void Room::markMessagesAsRead(const QString& uptoEventId)
{
d->markMessagesAsRead(findInTimeline(uptoEventId));
}
void Room::markAllMessagesAsRead()
{
- if (!d->timeline.empty())
- d->markMessagesAsRead(d->timeline.crbegin());
+ d->markMessagesAsRead(d->timeline.crbegin());
}
bool Room::canSwitchVersions() const
@@ -789,18 +910,44 @@ bool Room::canSwitchVersions() const
return true;
}
-bool Room::hasUnreadMessages() const { return unreadCount() >= 0; }
+bool Room::isEventNotable(const TimelineItem &ti) const
+{
+ const auto& evt = *ti;
+ const auto* rme = ti.viewAs<RoomMessageEvent>();
+ return !evt.isRedacted()
+ && (is<RoomTopicEvent>(evt) || is<RoomNameEvent>(evt)
+ || is<RoomAvatarEvent>(evt) || is<RoomTombstoneEvent>(evt)
+ || (rme && rme->msgtype() != MessageEventType::Notice
+ && rme->replacedEvent().isEmpty()))
+ && evt.senderId() != localUser()->id();
+}
+
+Notification Room::notificationFor(const TimelineItem &ti) const
+{
+ return d->notifications.value(ti->id());
+}
-int Room::unreadCount() const { return d->unreadMessages; }
+Notification Room::checkForNotifications(const TimelineItem &ti)
+{
+ return { Notification::None };
+}
-Room::rev_iter_t Room::historyEdge() const { return d->timeline.crend(); }
+bool Room::hasUnreadMessages() const { return !d->partiallyReadStats.empty(); }
-Room::Timeline::const_iterator Room::syncEdge() const
+int countFromStats(const EventStats& s)
{
- return d->timeline.cend();
+ return s.empty() ? -1 : int(s.notableCount);
}
-Room::rev_iter_t Room::timelineEdge() const { return historyEdge(); }
+int Room::unreadCount() const { return countFromStats(partiallyReadStats()); }
+
+EventStats Room::partiallyReadStats() const { return d->partiallyReadStats; }
+
+EventStats Room::unreadStats() const { return d->unreadStats; }
+
+Room::rev_iter_t Room::historyEdge() const { return d->historyEdge(); }
+
+Room::Timeline::const_iterator Room::syncEdge() const { return d->syncEdge(); }
TimelineItem::index_t Room::minTimelineIndex() const
{
@@ -820,7 +967,7 @@ bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const
Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const
{
- return timelineEdge()
+ return historyEdge()
- (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0);
}
@@ -872,18 +1019,17 @@ void Room::Private::getAllMembers()
allMembersJob = connection->callApi<GetMembersByRoomJob>(
id, connection->nextBatchToken(), "join");
auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1;
- connect(allMembersJob, &BaseJob::success, q, [=] {
+ connect(allMembersJob, &BaseJob::success, q, [this, nextIndex] {
Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1);
auto roomChanges = updateStateFrom(allMembersJob->chunk());
// Replay member events that arrived after the point for which
// the full members list was requested.
if (!timeline.empty())
for (auto it = q->findInTimeline(nextIndex).base();
- it != timeline.cend(); ++it)
+ it != syncEdge(); ++it)
if (is<RoomMemberEvent>(**it))
roomChanges |= q->processStateEvent(**it);
- if (roomChanges & MembersChange)
- emit q->memberListChanged();
+ postprocessChanges(roomChanges);
emit q->allMembersLoaded();
});
}
@@ -897,11 +1043,8 @@ void Room::setDisplayed(bool displayed)
d->displayed = displayed;
emit displayedChanged(displayed);
- if (displayed) {
- resetHighlightCount();
- resetNotificationCount();
+ if (displayed)
d->getAllMembers();
- }
}
QString Room::firstDisplayedEventId() const { return d->firstDisplayedEventId; }
@@ -916,6 +1059,11 @@ void Room::setFirstDisplayedEventId(const QString& eventId)
if (d->firstDisplayedEventId == eventId)
return;
+ if (!eventId.isEmpty() && findInTimeline(eventId) == historyEdge())
+ qCWarning(MESSAGES)
+ << eventId
+ << "is marked as first displayed but doesn't seem to be loaded";
+
d->firstDisplayedEventId = eventId;
emit firstDisplayedEventChanged();
}
@@ -938,6 +1086,12 @@ void Room::setLastDisplayedEventId(const QString& eventId)
if (d->lastDisplayedEventId == eventId)
return;
+ const auto marker = findInTimeline(eventId);
+ if (!eventId.isEmpty() && marker == historyEdge())
+ qCWarning(MESSAGES)
+ << eventId
+ << "is marked as last displayed but doesn't seem to be loaded";
+
d->lastDisplayedEventId = eventId;
emit lastDisplayedEventChanged();
}
@@ -951,38 +1105,70 @@ void Room::setLastDisplayedEvent(TimelineItem::index_t index)
Room::rev_iter_t Room::readMarker(const User* user) const
{
Q_ASSERT(user);
- return findInTimeline(d->lastReadEventIds.value(user));
+ return findInTimeline(lastReadReceipt(user->id()).eventId);
}
-Room::rev_iter_t Room::readMarker() const { return readMarker(localUser()); }
+Room::rev_iter_t Room::readMarker() const { return fullyReadMarker(); }
+
+QString Room::readMarkerEventId() const { return lastFullyReadEventId(); }
+
+ReadReceipt Room::lastReadReceipt(const QString& userId) const
+{
+ return d->lastReadReceipts.value(userId);
+}
-QString Room::readMarkerEventId() const
+ReadReceipt Room::lastLocalReadReceipt() const
{
- return d->lastReadEventIds.value(localUser());
+ return d->lastReadReceipts.value(localUser()->id());
}
-QList<User*> Room::usersAtEventId(const QString& eventId)
+Room::rev_iter_t Room::localReadReceiptMarker() const
{
- return d->eventIdReadUsers.values(eventId);
+ return findInTimeline(lastLocalReadReceipt().eventId);
}
-int Room::notificationCount() const { return d->notificationCount; }
+QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; }
+
+Room::rev_iter_t Room::fullyReadMarker() const
+{
+ return findInTimeline(d->fullyReadUntilEventId);
+}
+
+QSet<QString> Room::userIdsAtEvent(const QString& eventId)
+{
+ return d->eventIdReadUsers.value(eventId);
+}
+
+QSet<User*> Room::usersAtEventId(const QString& eventId)
+{
+ const auto& userIds = d->eventIdReadUsers.value(eventId);
+ QSet<User*> users;
+ users.reserve(userIds.size());
+ for (const auto& uId : userIds)
+ users.insert(user(uId));
+ return users;
+}
+
+qsizetype Room::notificationCount() const
+{
+ return d->unreadStats.notableCount;
+}
void Room::resetNotificationCount()
{
- if (d->notificationCount == 0)
+ if (d->unreadStats.notableCount == 0)
return;
- d->notificationCount = 0;
+ d->unreadStats.notableCount = 0;
emit notificationCountChanged();
}
-int Room::highlightCount() const { return d->highlightCount; }
+qsizetype Room::highlightCount() const { return d->serverHighlightCount; }
void Room::resetHighlightCount()
{
- if (d->highlightCount == 0)
+ if (d->serverHighlightCount == 0)
return;
- d->highlightCount = 0;
+ d->serverHighlightCount = 0;
emit highlightCountChanged();
}
@@ -1120,6 +1306,17 @@ QList<User*> Room::directChatUsers() const
return connection()->directChatUsers(this);
}
+QUrl Room::makeMediaUrl(const QString& eventId, const QUrl& mxcUrl) const
+{
+ auto url = connection()->makeMediaUrl(mxcUrl);
+ QUrlQuery q(url.query());
+ Q_ASSERT(q.hasQueryItem("user_id"));
+ q.addQueryItem("room_id", id());
+ q.addQueryItem("event_id", eventId);
+ url.setQuery(q);
+ return url;
+}
+
QString safeFileName(QString rawName)
{
return rawName.replace(QRegularExpression("[/\\<>|\"*?:]"), "_");
@@ -1173,9 +1370,8 @@ QUrl Room::urlToThumbnail(const QString& eventId) const
if (event->hasThumbnail()) {
auto* thumbnail = event->content()->thumbnailInfo();
Q_ASSERT(thumbnail != nullptr);
- return MediaThumbnailJob::makeRequestUrl(connection()->homeserver(),
- thumbnail->url,
- thumbnail->imageSize);
+ return connection()->getUrlForApi<MediaThumbnailJob>(
+ thumbnail->url, thumbnail->imageSize);
}
qCDebug(MAIN) << "Event" << eventId << "has no thumbnail";
return {};
@@ -1186,8 +1382,7 @@ QUrl Room::urlToDownload(const QString& eventId) const
if (auto* event = d->getEventWithFile(eventId)) {
auto* fileInfo = event->content()->fileInfo();
Q_ASSERT(fileInfo != nullptr);
- return DownloadFileJob::makeRequestUrl(connection()->homeserver(),
- fileInfo->url);
+ return connection()->getUrlForApi<DownloadFileJob>(fileInfo->url);
}
return {};
}
@@ -1250,7 +1445,7 @@ QList<User*> Room::membersLeft() const { return d->membersLeft; }
QList<User*> Room::users() const { return d->membersMap.values(); }
-[[deprecated]] QStringList Room::memberNames() const
+QStringList Room::memberNames() const
{
return safeMemberNames();
}
@@ -1275,8 +1470,6 @@ QStringList Room::htmlSafeMemberNames() const
return res;
}
-int Room::memberCount() const { return d->membersMap.size(); }
-
int Room::timelineSize() const { return int(d->timeline.size()); }
bool Room::usesEncryption() const
@@ -1290,6 +1483,17 @@ const StateEventBase* Room::getCurrentState(const QString& evtType,
return d->getCurrentState({ evtType, stateKey });
}
+const QVector<const StateEventBase*>
+Room::stateEventsOfType(const QString& evtType) const
+{
+ return d->stateEventsOfType(evtType);
+}
+
+const QHash<StateEventKey, const StateEventBase*>& Room::currentState() const
+{
+ return d->currentState;
+}
+
RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent)
{
#ifndef Quotient_E2EE_ENABLED
@@ -1353,11 +1557,10 @@ GetRoomEventsJob* Room::eventsHistoryJob() const { return d->eventsHistoryJob; }
Room::Changes Room::Private::setSummary(RoomSummary&& newSummary)
{
if (!summary.merge(newSummary))
- return Change::NoChange;
+ return Change::None;
qCDebug(STATE).nospace().noquote()
<< "Updated room summary for " << q->objectName() << ": " << summary;
- emit q->memberListChanged();
- return Change::SummaryChange;
+ return Change::Summary;
}
void Room::Private::insertMemberIntoMap(User* u)
@@ -1435,7 +1638,8 @@ void Room::Private::removeMemberFromMap(User* u)
inline auto makeErrorStr(const Event& e, QByteArray msg)
{
- return msg.append("; event dump follows:\n").append(e.originalJson());
+ return msg.append("; event dump follows:\n")
+ .append(QJsonDocument(e.fullJson()).toJson());
}
Room::Timeline::size_type
@@ -1461,11 +1665,12 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events,
!eventsIndex.contains(eId), __FUNCTION__,
makeErrorStr(*e, "Event is already in the timeline; "
"incoming events were not properly deduplicated"));
- if (placement == Older)
- timeline.emplace_front(move(e), --index);
- else
- timeline.emplace_back(move(e), ++index);
+ const auto& ti = placement == Older
+ ? timeline.emplace_front(move(e), --index)
+ : timeline.emplace_back(move(e), ++index);
eventsIndex.insert(eId, index);
+ if (auto n = q->checkForNotifications(ti); n.type != Notification::None)
+ notifications.insert(e->id(), n);
Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId);
}
const auto insertedSize = (index - baseIndex) * placement;
@@ -1523,7 +1728,7 @@ QString Room::disambiguatedMemberName(const QString& mxId) const
QString Room::safeMemberName(const QString& userId) const
{
- return sanitized(roomMembername(userId));
+ return sanitized(disambiguatedMemberName(userId));
}
QString Room::htmlSafeMemberName(const QString& userId) const
@@ -1540,63 +1745,132 @@ QUrl Room::memberAvatarUrl(const QString &mxId) const
: QUrl();
}
+Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data,
+ bool fromCache)
+{
+ Changes changes {};
+ if (fromCache) {
+ // Initial load of cached statistics
+ partiallyReadStats =
+ EventStats::fromCachedCounters(data.partiallyReadCount);
+ unreadStats = EventStats::fromCachedCounters(data.unreadCount,
+ data.highlightCount);
+ // Migrate from lib 0.6: -1 in the old unread counter overrides 0
+ // (which loads to an estimate) in notification_count. Next caching will
+ // save -1 in both places, completing the migration.
+ if (data.unreadCount == 0 && data.partiallyReadCount == -1)
+ unreadStats.isEstimate = false;
+ changes |= Change::PartiallyReadStats | Change::UnreadStats;
+ qCDebug(MESSAGES) << "Loaded" << q->objectName()
+ << "event statistics from cache:" << partiallyReadStats
+ << "since m.fully_read," << unreadStats
+ << "since m.read";
+ } else if (timeline.empty()) {
+ // In absence of actual events use statistics from the homeserver
+ if (merge(unreadStats.notableCount, data.unreadCount))
+ changes |= Change::PartiallyReadStats;
+ if (merge(unreadStats.highlightCount, data.highlightCount))
+ changes |= Change::UnreadStats;
+ unreadStats.isEstimate = !data.unreadCount.has_value()
+ || *data.unreadCount > 0;
+ qCDebug(MESSAGES)
+ << "Using server-side unread event statistics while the"
+ << q->objectName() << "timeline is empty:" << unreadStats;
+ }
+ bool correctedStats = false;
+ if (unreadStats.highlightCount > partiallyReadStats.highlightCount) {
+ correctedStats = true;
+ partiallyReadStats.highlightCount = unreadStats.highlightCount;
+ partiallyReadStats.isEstimate |= unreadStats.isEstimate;
+ }
+ if (unreadStats.notableCount > partiallyReadStats.notableCount) {
+ correctedStats = true;
+ partiallyReadStats.notableCount = unreadStats.notableCount;
+ partiallyReadStats.isEstimate |= unreadStats.isEstimate;
+ }
+ if (!unreadStats.isEstimate && partiallyReadStats.isEstimate) {
+ correctedStats = true;
+ partiallyReadStats.isEstimate = true;
+ }
+ if (correctedStats)
+ qCDebug(MESSAGES) << "Partially read event statistics in"
+ << q->objectName() << "were adjusted to"
+ << partiallyReadStats
+ << "to be consistent with the m.read receipt";
+ Q_ASSERT(partiallyReadStats.isValidFor(q, q->fullyReadMarker()));
+ Q_ASSERT(unreadStats.isValidFor(q, q->localReadReceiptMarker()));
+
+ // TODO: Once the library learns to count highlights, drop
+ // serverHighlightCount and only use the server-side counter when
+ // the timeline is empty (see the code above).
+ if (merge(serverHighlightCount, data.highlightCount)) {
+ qCDebug(MESSAGES) << "Updated highlights number in" << q->objectName()
+ << "to" << serverHighlightCount;
+ changes |= Change::Highlights;
+ }
+ return changes;
+}
+
void Room::updateData(SyncRoomData&& data, bool fromCache)
{
if (d->prevBatch.isEmpty())
d->prevBatch = data.timelinePrevBatch;
setJoinState(data.joinState);
- Changes roomChanges = Change::NoChange;
- QElapsedTimer et;
- et.start();
+ Changes roomChanges {};
+ // The order of calculation is important - don't merge the lines!
+ roomChanges |= d->updateStateFrom(data.state);
+ roomChanges |= d->setSummary(move(data.summary));
+ roomChanges |= d->addNewMessageEvents(move(data.timeline));
+
+ for (auto&& ephemeralEvent : data.ephemeral)
+ roomChanges |= processEphemeralEvent(move(ephemeralEvent));
+
for (auto&& event : data.accountData)
roomChanges |= processAccountDataEvent(move(event));
- roomChanges |= d->updateStateFrom(data.state);
+ roomChanges |= d->updateStatsFromSyncData(data, fromCache);
- if (!data.timeline.empty()) {
- et.restart();
- roomChanges |= d->addNewMessageEvents(move(data.timeline));
- if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER)
- << "*** Room::addNewMessageEvents():" << data.timeline.size()
- << "event(s)," << et;
- }
- if (roomChanges & TopicChange)
+ if (roomChanges & Change::Topic)
emit topicChanged();
- if (roomChanges & (NameChange | AliasesChange))
+ if (roomChanges & (Change::Name | Change::Aliases))
emit namesChanged(this);
- if (roomChanges & MembersChange)
- emit memberListChanged();
+ d->postprocessChanges(roomChanges, !fromCache);
+}
- roomChanges |= d->setSummary(move(data.summary));
+void Room::Private::postprocessChanges(Changes changes, bool saveState)
+{
+ if (!changes)
+ return;
- for (auto&& ephemeralEvent : data.ephemeral)
- roomChanges |= processEphemeralEvent(move(ephemeralEvent));
+ if (changes & Change::Members)
+ emit q->memberListChanged();
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) {
- qCDebug(MESSAGES) << "Setting unread_count to" << data.unreadCount;
- d->unreadMessages = data.unreadCount;
- emit unreadMessagesChanged(this);
- }
+ if (changes
+ & (Change::Name | Change::Aliases | Change::Members | Change::Summary))
+ updateDisplayname();
- if (data.highlightCount != d->highlightCount) {
- d->highlightCount = data.highlightCount;
- emit highlightCountChanged();
- }
- if (data.notificationCount != d->notificationCount) {
- d->notificationCount = data.notificationCount;
- emit notificationCountChanged();
- }
- if (roomChanges != Change::NoChange) {
- d->updateDisplayname();
- emit changed(roomChanges);
- if (!fromCache)
- connection()->saveRoomState(this);
+ if (changes & Change::PartiallyReadStats) {
+ emit q->unreadMessagesChanged(q); // TODO: remove in 0.8
+ emit q->partiallyReadStatsChanged();
}
+
+ if (changes & Change::UnreadStats)
+ emit q->unreadStatsChanged();
+
+ if (changes & Change::Highlights)
+ emit q->highlightCountChanged();
+
+ qCDebug(MAIN) << terse << changes << "= hex" <<
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ Qt::
+#endif
+ hex << uint(changes) << "in" << q->objectName();
+ emit q->changed(changes);
+ if (saveState)
+ connection->saveRoomState(q);
}
RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event)
@@ -1716,6 +1990,12 @@ QString Room::retryMessage(const QString& txnId)
return d->doSendEvent(it->event());
}
+// Lambda defers actual tr() invocation to the moment when translations are
+// initialised
+const auto FileTransferCancelledMsg = [] {
+ return Room::tr("File transfer cancelled");
+};
+
void Room::discardMessage(const QString& txnId)
{
auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(),
@@ -1730,7 +2010,7 @@ void Room::discardMessage(const QString& txnId)
if (isJobPending(transferIt->job)) {
transferIt->status = FileTransferInfo::Cancelled;
transferIt->job->abandon();
- emit fileTransferFailed(txnId, tr("File upload cancelled"));
+ emit fileTransferFailed(txnId, FileTransferCancelledMsg());
} else if (transferIt->status == FileTransferInfo::Completed) {
qCWarning(MAIN)
<< "File for transaction" << txnId
@@ -1770,57 +2050,80 @@ QString Room::postReaction(const QString& eventId, const QString& key)
return d->sendEvent<ReactionEvent>(EventRelation::annotate(eventId, key));
}
-QString Room::postFile(const QString& plainText, const QUrl& localPath,
- bool asGenericFile)
+QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl)
{
- QFileInfo localFile { localPath.toLocalFile() };
- Q_ASSERT(localFile.isFile());
-
- const auto txnId =
- d->addAsPending(
- makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile))
- ->transactionId();
+ const auto txnId = addAsPending(move(msgEvent))->transactionId();
// Remote URL will only be known after upload; fill in the local path
// to enable the preview while the event is pending.
- uploadFile(txnId, localPath);
+ q->uploadFile(txnId, localUrl);
// Below, the upload job is used as a context object to clean up connections
- const auto& transferJob = d->fileTransfers.value(txnId).job;
- connect(this, &Room::fileTransferCompleted, transferJob,
- [this, txnId](const QString& id, const QUrl&, const QUrl& mxcUri) {
- if (id == txnId) {
- auto it = findPendingEvent(txnId);
- if (it != d->unsyncedEvents.end()) {
- it->setFileUploaded(mxcUri);
- emit pendingEventChanged(
- int(it - d->unsyncedEvents.begin()));
- d->doSendEvent(it->get());
- } else {
- // Normally in this situation we should instruct
- // the media server to delete the file; alas, there's no
- // API specced for that.
- qCWarning(MAIN) << "File uploaded to" << mxcUri
- << "but the event referring to it was "
- "cancelled";
- }
+ const auto& transferJob = fileTransfers.value(txnId).job;
+ connect(q, &Room::fileTransferCompleted, transferJob,
+ [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri) {
+ if (tId != txnId)
+ return;
+
+ const auto it = q->findPendingEvent(txnId);
+ if (it != unsyncedEvents.end()) {
+ it->setFileUploaded(mxcUri);
+ emit q->pendingEventChanged(
+ int(it - unsyncedEvents.begin()));
+ doSendEvent(it->get());
+ } else {
+ // Normally in this situation we should instruct
+ // the media server to delete the file; alas, there's no
+ // API specced for that.
+ qCWarning(MAIN) << "File uploaded to" << mxcUri
+ << "but the event referring to it was "
+ "cancelled";
}
});
- connect(this, &Room::fileTransferCancelled, transferJob,
- [this, txnId](const QString& id) {
- if (id == txnId) {
- auto it = findPendingEvent(txnId);
- if (it != d->unsyncedEvents.end()) {
- const auto idx = int(it - d->unsyncedEvents.begin());
- emit pendingEventAboutToDiscard(idx);
- // See #286 on why iterator may not be valid here.
- d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx);
- emit pendingEventDiscarded();
- }
- }
+ connect(q, &Room::fileTransferFailed, transferJob,
+ [this, txnId](const QString& tId) {
+ if (tId != txnId)
+ return;
+
+ const auto it = q->findPendingEvent(txnId);
+ if (it == unsyncedEvents.end())
+ return;
+
+ const auto idx = int(it - unsyncedEvents.begin());
+ emit q->pendingEventAboutToDiscard(idx);
+ // See #286 on why `it` may not be valid here.
+ unsyncedEvents.erase(unsyncedEvents.begin() + idx);
+ emit q->pendingEventDiscarded();
});
return txnId;
}
+QString Room::postFile(const QString& plainText,
+ EventContent::TypedBase* content)
+{
+ Q_ASSERT(content != nullptr && content->fileInfo() != nullptr);
+ const auto* const fileInfo = content->fileInfo();
+ Q_ASSERT(fileInfo != nullptr);
+ QFileInfo localFile { fileInfo->url.toLocalFile() };
+ Q_ASSERT(localFile.isFile());
+
+ return d->doPostFile(
+ makeEvent<RoomMessageEvent>(
+ plainText, RoomMessageEvent::rawMsgTypeForFile(localFile), content),
+ fileInfo->url);
+}
+
+#if QT_VERSION_MAJOR < 6
+QString Room::postFile(const QString& plainText, const QUrl& localPath,
+ bool asGenericFile)
+{
+ QFileInfo localFile { localPath.toLocalFile() };
+ Q_ASSERT(localFile.isFile());
+ return d->doPostFile(makeEvent<RoomMessageEvent>(plainText, localFile,
+ asGenericFile),
+ localPath);
+}
+#endif
+
QString Room::postEvent(RoomEvent* event)
{
return d->sendEvent(RoomEventPtr(event));
@@ -1943,7 +2246,7 @@ void Room::Private::getPreviousContent(int limit, const QString &filter)
eventsHistoryJob =
connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit, filter);
emit q->eventsHistoryJobChanged();
- connect(eventsHistoryJob, &BaseJob::success, q, [=] {
+ connect(eventsHistoryJob, &BaseJob::success, q, [this] {
prevBatch = eventsHistoryJob->end();
addHistoricalMessageEvents(eventsHistoryJob->chunk());
});
@@ -2068,16 +2371,16 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
void Room::cancelFileTransfer(const QString& id)
{
- const auto it = d->fileTransfers.constFind(id);
- if (it == d->fileTransfers.cend()) {
+ const auto it = d->fileTransfers.find(id);
+ if (it == d->fileTransfers.end()) {
qCWarning(MAIN) << "No information on file transfer" << id << "in room"
<< d->id;
return;
}
if (isJobPending(it->job))
it->job->abandon();
- d->fileTransfers.remove(id);
- emit fileTransferCancelled(id);
+ it->status = FileTransferInfo::Cancelled;
+ emit fileTransferFailed(id, FileTransferCancelledMsg());
}
void Room::Private::dropDuplicateEvents(RoomEvents& events) const
@@ -2114,7 +2417,7 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const
RoomEventPtr makeRedacted(const RoomEvent& target,
const RedactionEvent& redaction)
{
- auto originalJson = target.originalJsonObject();
+ auto originalJson = target.fullJson();
// clang-format off
static const QStringList keepKeys {
EventIdKey, TypeKey, RoomIdKey, SenderKey, StateKeyKey,
@@ -2125,15 +2428,14 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
QStringLiteral("membership") };
// clang-format on
- std::vector<std::pair<event_type_t, QStringList>> keepContentKeysMap {
+ static const std::pair<event_type_t, QStringList> keepContentKeysMap[] {
{ RoomMemberEvent::typeId(), { QStringLiteral("membership") } },
{ RoomCreateEvent::typeId(), { QStringLiteral("creator") } },
{ RoomPowerLevelsEvent::typeId(),
{ QStringLiteral("ban"), QStringLiteral("events"),
QStringLiteral("events_default"), QStringLiteral("kick"),
QStringLiteral("redact"), QStringLiteral("state_default"),
- QStringLiteral("users"), QStringLiteral("users_default") } },
- { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } }
+ QStringLiteral("users"), QStringLiteral("users_default") } }
// , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } }
// , { RoomHistoryVisibility::typeId(),
// { QStringLiteral("history_visibility") } }
@@ -2145,9 +2447,9 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
++it;
}
auto keepContentKeys =
- find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(),
+ find_if(begin(keepContentKeysMap), end(keepContentKeysMap),
[&target](const auto& t) { return target.type() == t.first; });
- if (keepContentKeys == keepContentKeysMap.end()) {
+ if (keepContentKeys == end(keepContentKeysMap)) {
originalJson.remove(ContentKeyL);
originalJson.remove(PrevContentKeyL);
} else {
@@ -2161,7 +2463,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
originalJson.insert(ContentKey, content);
}
auto unsignedData = originalJson.take(UnsignedKeyL).toObject();
- unsignedData[RedactedCauseKeyL] = redaction.originalJsonObject();
+ unsignedData[RedactedCauseKeyL] = redaction.fullJson();
originalJson.insert(QStringLiteral("unsigned"), unsignedData);
return loadEvent<RoomEvent>(originalJson);
@@ -2226,8 +2528,13 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
RoomEventPtr makeReplaced(const RoomEvent& target,
const RoomMessageEvent& replacement)
{
- auto originalJson = target.originalJsonObject();
- originalJson[ContentKeyL] = replacement.contentJson().value("m.new_content"_ls);
+ const auto& targetReply = target.contentPart<QJsonObject>("m.relates_to");
+ auto newContent = replacement.contentPart<QJsonObject>("m.new_content"_ls);
+ if (!targetReply.empty()) {
+ newContent["m.relates_to"] = targetReply;
+ }
+ auto originalJson = target.fullJson();
+ originalJson[ContentKeyL] = newContent;
auto unsignedData = originalJson.take(UnsignedKeyL).toObject();
auto relations = unsignedData.take("m.relations"_ls).toObject();
@@ -2287,8 +2594,10 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
{
dropDuplicateEvents(events);
if (events.empty())
- return Change::NoChange;
+ return Change::None;
+ QElapsedTimer et;
+ et.start();
{
// Pre-process redactions and edits so that events that get
// redacted/replaced in the same batch landed in the timeline already
@@ -2338,7 +2647,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
// clients historically expect. This may eventually change though if we
// postulate that the current state is only current between syncs but not
// within a sync.
- Changes roomChanges = Change::NoChange;
+ Changes roomChanges {};
for (const auto& eptr : events)
roomChanges |= q->processStateEvent(*eptr);
@@ -2356,7 +2665,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
emit q->aboutToAddNewMessages(eventsSpan);
auto insertedSize = moveEventsToTimeline(eventsSpan, Newer);
totalInserted += insertedSize;
- auto firstInserted = timeline.cend() - insertedSize;
+ auto firstInserted = syncEdge() - insertedSize;
q->onAddNewTimelineEvents(firstInserted);
emit q->addedMessages(firstInserted->index(),
timeline.back().index());
@@ -2386,20 +2695,20 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx);
if (auto insertedSize = moveEventsToTimeline({ remoteEcho, it }, Newer)) {
totalInserted += insertedSize;
- q->onAddNewTimelineEvents(timeline.cend() - insertedSize);
+ q->onAddNewTimelineEvents(syncEdge() - insertedSize);
}
emit q->pendingEventMerged();
}
// Events merged and transferred from `events` to `timeline` now.
- const auto from = timeline.cend() - totalInserted;
+ const auto from = syncEdge() - totalInserted;
if (q->supportsCalls())
- for (auto it = from; it != timeline.cend(); ++it)
+ for (auto it = from; it != syncEdge(); ++it)
if (const auto* evt = it->viewAs<CallEventBase>())
emit q->callEvent(q, evt);
if (totalInserted > 0) {
- for (auto it = from; it != timeline.cend(); ++it) {
+ for (auto it = from; it != syncEdge(); ++it) {
if (const auto* reaction = it->viewAs<ReactionEvent>()) {
const auto& relation = reaction->relation();
relations[{ relation.eventId, relation.type }] << reaction;
@@ -2411,28 +2720,22 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
<< totalInserted << "new events; the last event is now"
<< timeline.back();
- // The first event in the just-added batch (referred to by `from`)
- // defines whose read marker can possibly be promoted any further over
- // the same author's events newly arrived. Others will need explicit
- // read receipts from the server (or, for the local user,
- // markMessagesAsRead() invocation) to promote their read markers over
- // the new message events.
- if (const auto senderId = (*from)->senderId(); !senderId.isEmpty()) {
- auto* const firstWriter = q->user(senderId);
- if (q->readMarker(firstWriter) != timeline.crend()) {
- roomChanges |=
- promoteReadMarker(firstWriter, rev_iter_t(from) - 1);
- qCDebug(MESSAGES)
- << "Auto-promoted read marker for" << senderId
- << "to" << *q->readMarker(firstWriter);
- }
- }
+ roomChanges |= updateStats(timeline.crbegin(), rev_iter_t(from));
- updateUnreadCount(timeline.crbegin(), rev_iter_t(from));
- roomChanges |= Change::UnreadNotifsChange;
+ // If the local user's message(s) is/are first in the batch
+ // and the fully read marker was right before it, promote
+ // the fully read marker to the same event as the read receipt.
+ const auto& firstWriterId = (*from)->senderId();
+ if (firstWriterId == connection->userId()
+ && q->fullyReadMarker().base() == from)
+ roomChanges |=
+ setFullyReadMarker(q->lastReadReceipt(firstWriterId).eventId);
}
Q_ASSERT(timeline.size() == timelineSize + totalInserted);
+ if (totalInserted > 9 || et.nsecsElapsed() >= profilerMinNsecs())
+ qCDebug(PROFILER) << "Added" << totalInserted << "new event(s) to"
+ << q->objectName() << "in" << et;
return roomChanges;
}
@@ -2446,6 +2749,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
if (events.empty())
return;
+ Changes changes {};
// In case of lazy-loading new members may be loaded with historical
// messages. Also, the cache doesn't store events with empty content;
// so when such events show up in the timeline they should be properly
@@ -2454,39 +2758,40 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
const auto& e = *eptr;
if (e.isStateEvent()
&& !currentState.contains({ e.matrixType(), e.stateKey() })) {
- q->processStateEvent(e);
+ changes |= q->processStateEvent(e);
}
}
emit q->aboutToAddHistoricalMessages(events);
const auto insertedSize = moveEventsToTimeline(events, Older);
- const auto from = timeline.crend() - insertedSize;
+ const auto from = historyEdge() - insertedSize;
qCDebug(STATE) << "Room" << displayname << "received" << insertedSize
<< "past events; the oldest event is now" << timeline.front();
q->onAddHistoricalTimelineEvents(from);
emit q->addedMessages(timeline.front().index(), from->index());
- for (auto it = from; it != timeline.crend(); ++it) {
+ for (auto it = from; it != historyEdge(); ++it) {
if (const auto* reaction = it->viewAs<ReactionEvent>()) {
const auto& relation = reaction->relation();
relations[{ relation.eventId, relation.type }] << reaction;
emit q->updatedEvent(relation.eventId);
}
}
- if (from <= q->readMarker())
- updateUnreadCount(from, timeline.crend());
-
Q_ASSERT(timeline.size() == timelineSize + insertedSize);
if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** Room::addHistoricalMessageEvents():"
- << insertedSize << "event(s)," << et;
+ qCDebug(PROFILER) << "Added" << insertedSize << "historical event(s) to"
+ << q->objectName() << "in" << et;
+
+ changes |= updateStats(from, historyEdge());
+ if (changes)
+ postprocessChanges(changes);
}
Room::Changes Room::processStateEvent(const RoomEvent& e)
{
if (!e.isStateEvent())
- return NoChange;
+ return Change::None;
// Find a value (create an empty one if necessary) and get a reference
// to it. Can't use getCurrentState<>() because it (creates and) returns
@@ -2495,7 +2800,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }];
// Prepare for the state change
// clang-format off
- const bool proceed = visit(e
+ const bool proceed = switchOnType(e
, [this, curStateEvent](const RoomMemberEvent& rme) {
// clang-format on
auto* oldRme = static_cast<const RoomMemberEvent*>(curStateEvent);
@@ -2506,16 +2811,16 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return false; // Stay low and hope for the best...
}
const auto prevMembership = oldRme ? oldRme->membership()
- : MembershipType::Leave;
+ : Membership::Leave;
switch (prevMembership) {
- case MembershipType::Invite:
+ case Membership::Invite:
if (rme.membership() != prevMembership) {
d->usersInvited.removeOne(u);
Q_ASSERT(!d->usersInvited.contains(u));
}
break;
- case MembershipType::Join:
- if (rme.membership() == MembershipType::Join) {
+ case Membership::Join:
+ if (rme.membership() == Membership::Join) {
// rename/avatar change or no-op
if (rme.newDisplayName()) {
emit memberAboutToRename(u, *rme.newDisplayName());
@@ -2529,7 +2834,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return false;
}
} else {
- if (rme.membership() == MembershipType::Invite)
+ if (rme.membership() == Membership::Invite)
qCWarning(MAIN)
<< "Membership change from Join to Invite:" << rme;
// whatever the new membership, it's no more Join
@@ -2537,16 +2842,16 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
emit userRemoved(u);
}
break;
- case MembershipType::Ban:
- case MembershipType::Knock:
- case MembershipType::Leave:
- if (rme.membership() == MembershipType::Invite
- || rme.membership() == MembershipType::Join) {
+ case Membership::Ban:
+ case Membership::Knock:
+ case Membership::Leave:
+ if (rme.membership() == Membership::Invite
+ || rme.membership() == Membership::Join) {
d->membersLeft.removeOne(u);
Q_ASSERT(!d->membersLeft.contains(u));
}
break;
- case MembershipType::Undefined:
+ case Membership::Undefined:
; // A warning will be dropped in the post-processing block below
}
return true;
@@ -2573,8 +2878,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
}
, true); // By default, go forward with the state change
// clang-format on
- if (!proceed)
- return NoChange;
+ if (!proceed) {
+ if (!curStateEvent) // Remove the empty placeholder if one was created
+ d->currentState.remove({ e.matrixType(), e.stateKey() });
+ return Change::None;
+ }
// Change the state
const auto* const oldStateEvent =
@@ -2590,15 +2898,15 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
// Update internal structures as per the change and work out the return value
// clang-format off
- const auto result = visit(e
+ const auto result = switchOnType(e
, [] (const RoomNameEvent&) {
- return NameChange;
+ return Change::Name;
}
, [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) {
// clang-format on
setObjectName(cae.alias().isEmpty() ? d->id : cae.alias());
const auto* oldCae =
- static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent);
+ static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent);
QStringList previousAltAliases {};
if (oldCae) {
previousAltAliases = oldCae->altAliases();
@@ -2610,8 +2918,9 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
if (!cae.alias().isEmpty())
newAliases.push_front(cae.alias());
- connection()->updateRoomAliases(id(), previousAltAliases, newAliases);
- return AliasesChange;
+ connection()->updateRoomAliases(id(), previousAltAliases,
+ newAliases);
+ return Change::Aliases;
// clang-format off
}
, [this] (const RoomPinnedEvent&) {
@@ -2619,12 +2928,12 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return OtherChange;
}
, [] (const RoomTopicEvent&) {
- return TopicChange;
+ return Change::Topic;
}
, [this] (const RoomAvatarEvent& evt) {
if (d->avatar.updateUrl(evt.url()))
emit avatarChanged();
- return AvatarChange;
+ return Change::Avatar;
}
, [this,oldStateEvent] (const RoomMemberEvent& evt) {
// clang-format on
@@ -2633,10 +2942,10 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
static_cast<const RoomMemberEvent*>(oldStateEvent);
const auto prevMembership = oldMemberEvent
? oldMemberEvent->membership()
- : MembershipType::Leave;
+ : Membership::Leave;
switch (evt.membership()) {
- case MembershipType::Join:
- if (prevMembership != MembershipType::Join) {
+ case Membership::Join:
+ if (prevMembership != Membership::Join) {
d->insertMemberIntoMap(u);
emit userAdded(u);
} else {
@@ -2648,29 +2957,29 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
emit memberAvatarChanged(u);
}
break;
- case MembershipType::Invite:
+ case Membership::Invite:
if (!d->usersInvited.contains(u))
d->usersInvited.push_back(u);
if (u == localUser() && evt.isDirect())
connection()->addToDirectChats(this, user(evt.senderId()));
break;
- case MembershipType::Knock:
- case MembershipType::Ban:
- case MembershipType::Leave:
+ case Membership::Knock:
+ case Membership::Ban:
+ case Membership::Leave:
if (!d->membersLeft.contains(u))
d->membersLeft.append(u);
break;
- case MembershipType::Undefined:
+ case Membership::Undefined:
qCWarning(MEMBERS) << "Ignored undefined membership type";
}
- return MembersChange;
+ return Change::Members;
// clang-format off
}
, [this] (const EncryptionEvent&) {
// As encryption can only be switched on once, emit the signal here
// instead of aggregating and emitting in updateData()
emit encryption();
- return OtherChange;
+ return Change::Other;
}
, [this] (const RoomTombstoneEvent& evt) {
const auto successorId = evt.successorRoomId();
@@ -2686,30 +2995,31 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return true;
});
- return OtherChange;
+ return Change::Other;
// clang-format off
}
- , OtherChange);
+ , Change::Other);
// clang-format on
- Q_ASSERT(result != NoChange);
+ Q_ASSERT(result != Change::None);
return result;
}
Room::Changes Room::processEphemeralEvent(EventPtr&& event)
{
- Changes changes = NoChange;
+ Changes changes {};
QElapsedTimer et;
et.start();
if (auto* evt = eventCast<TypingEvent>(event)) {
d->usersTyping.clear();
- for (const auto& userId : evt->users()) {
- auto u = user(userId);
- if (memberJoinState(u) == JoinState::Join)
- d->usersTyping.append(u);
- }
+ d->usersTyping.reserve(evt->users().size()); // Assume all are members
+ for (const auto& userId : evt->users())
+ if (isMember(userId))
+ d->usersTyping.append(user(userId));
+
if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):"
- << evt->users().size() << "users," << et;
+ qCDebug(PROFILER)
+ << "Processing typing events from" << evt->users().size()
+ << "user(s) in" << objectName() << "took" << et;
emit typingChanged();
}
if (auto* evt = eventCast<ReceiptEvent>(event)) {
@@ -2717,67 +3027,54 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event)
const auto& eventsWithReceipts = evt->eventsWithReceipts();
for (const auto& p : eventsWithReceipts) {
totalReceipts += p.receipts.size();
- {
- if (p.receipts.size() == 1)
- qCDebug(EPHEMERAL) << "Marking" << p.evtId << "as read for"
- << p.receipts[0].userId;
- else
- qCDebug(EPHEMERAL) << "Marking" << p.evtId << "as read for"
- << p.receipts.size() << "users";
- }
const auto newMarker = findInTimeline(p.evtId);
- if (newMarker != timelineEdge()) {
- for (const Receipt& r : p.receipts) {
- if (r.userId == connection()->userId())
- continue; // FIXME, #185
- auto u = user(r.userId);
- if (memberJoinState(u) == JoinState::Join)
- changes |= d->promoteReadMarker(u, newMarker);
- }
- } else {
- qCDebug(EPHEMERAL) << "Event" << p.evtId
- << "not found; saving read receipts anyway";
- // If the event is not found (most likely, because it's too old
- // and hasn't been fetched from the server yet), but there is
- // a previous marker for a user, keep the previous marker.
- // Otherwise, blindly store the event id for this user.
- for (const Receipt& r : p.receipts) {
- if (r.userId == connection()->userId())
- continue; // FIXME, #185
- auto u = user(r.userId);
- if (memberJoinState(u) == JoinState::Join
- && readMarker(u) == timelineEdge())
- changes |= d->setLastReadEvent(u, p.evtId);
- }
- }
+ if (newMarker == historyEdge())
+ qCDebug(EPHEMERAL)
+ << "Event" << p.evtId
+ << "is not found; saving read receipt(s) anyway";
+ // If the event is not found (most likely, because it's too old and
+ // hasn't been fetched from the server yet) but there is a previous
+ // marker for a user, keep the previous marker because read receipts
+ // are not supposed to move backwards. Otherwise, blindly store
+ // the event id for this user and update the read marker when/if
+ // the event is fetched later on.
+ const auto updatedCount = std::count_if(
+ p.receipts.cbegin(), p.receipts.cend(),
+ [this, &changes, &newMarker, &evtId = p.evtId](const auto& r) {
+ const auto change =
+ d->setLastReadReceipt(r.userId, newMarker,
+ { evtId, r.timestamp });
+ changes |= change;
+ return change & Change::Any;
+ });
+
+ if (p.receipts.size() > 1)
+ qCDebug(EPHEMERAL) << p.evtId << "marked as read for"
+ << updatedCount << "user(s)";
+ if (updatedCount < p.receipts.size())
+ qCDebug(EPHEMERAL) << p.receipts.size() - updatedCount
+ << "receipts were skipped";
}
if (eventsWithReceipts.size() > 3 || totalReceipts > 10
|| et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER)
- << "*** Room::processEphemeralEvent(receipts):"
- << eventsWithReceipts.size() << "event(s) with"
- << totalReceipts << "receipt(s)," << et;
+ qCDebug(PROFILER) << "Processing" << totalReceipts
+ << "receipt(s) on" << eventsWithReceipts.size()
+ << "event(s) in" << objectName() << "took" << et;
}
return changes;
}
Room::Changes Room::processAccountDataEvent(EventPtr&& event)
{
- Changes changes = NoChange;
+ Changes changes {};
if (auto* evt = eventCast<TagEvent>(event)) {
d->setTags(evt->tags());
- changes |= Change::TagsChange;
+ changes |= Change::Tags;
}
- if (auto* evt = eventCast<ReadMarkerEvent>(event)) {
- auto readEventId = evt->event_id();
- qCDebug(STATE) << "Server-side read marker at" << readEventId;
- d->serverReadMarker = readEventId;
- const auto newMarker = findInTimeline(readEventId);
- changes |= newMarker != timelineEdge()
- ? d->markMessagesAsRead(newMarker)
- : d->setLastReadEvent(localUser(), readEventId);
- }
+ if (auto* evt = eventCast<const ReadMarkerEvent>(event))
+ changes |= d->setFullyReadMarker(evt->event_id());
+
// For all account data events
auto& currentData = d->accountData[event->matrixType()];
// A polymorphic event-specific comparison might be a bit more
@@ -2788,9 +3085,12 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event)
qCDebug(STATE) << "Updated account data of type"
<< currentData->matrixType();
emit accountDataChanged(currentData->matrixType());
- return Change::AccountDataChange;
+ // TODO: Drop AccountDataChange in 0.8
+ // NB: GCC (at least 10) only accepts QT_IGNORE_DEPRECATIONS around
+ // a statement, not within a statement
+ QT_IGNORE_DEPRECATIONS(changes |= Change::AccountData | Change::Other;)
}
- return Change::NoChange;
+ return changes;
}
template <typename ContT>
@@ -2958,19 +3258,28 @@ QJsonObject Room::Private::toJson() const
{ QStringLiteral("events"), accountDataEvents } });
}
- QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey,
- unreadMessages } };
-
- if (highlightCount > 0)
- unreadNotifObj.insert(QStringLiteral("highlight_count"), highlightCount);
- if (notificationCount > 0)
- unreadNotifObj.insert(QStringLiteral("notification_count"),
- notificationCount);
-
- result.insert(QStringLiteral("unread_notifications"), unreadNotifObj);
+ if (const auto& readReceipt = q->lastReadReceipt(connection->userId());
+ !readReceipt.eventId.isEmpty()) //
+ {
+ result.insert(
+ QStringLiteral("ephemeral"),
+ QJsonObject {
+ { QStringLiteral("events"),
+ QJsonArray { ReceiptEvent({ { readReceipt.eventId,
+ { { connection->userId(),
+ readReceipt.timestamp } } } })
+ .fullJson() } } });
+ }
+
+ result.insert(UnreadNotificationsKey,
+ QJsonObject { { PartiallyReadCountKey,
+ countFromStats(partiallyReadStats) },
+ { HighlightCountKey, serverHighlightCount } });
+ result.insert(NewUnreadCountKey, countFromStats(unreadStats));
if (et.elapsed() > 30)
- qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et;
+ qCDebug(PROFILER) << "Room::toJson() for" << q->objectName() << "took"
+ << et;
return result;
}
@@ -2984,12 +3293,25 @@ bool MemberSorter::operator()(User* u1, User* u2) const
return operator()(u1, room->disambiguatedMemberName(u2->id()));
}
-bool MemberSorter::operator()(User* u1, const QString& u2name) const
+bool MemberSorter::operator()(User* u1, QStringView u2name) const
{
auto n1 = room->disambiguatedMemberName(u1->id());
if (n1.startsWith('@'))
n1.remove(0, 1);
- auto n2 = u2name.midRef(u2name.startsWith('@') ? 1 : 0);
+ const auto n2 = u2name.mid(u2name.startsWith('@') ? 1 : 0)
+#if QT_VERSION_MAJOR < 6
+ .toString() // Qt 5 doesn't have QStringView::localeAwareCompare
+#endif
+ ;
return n1.localeAwareCompare(n2) < 0;
}
+
+void Room::activateEncryption()
+{
+ if(usesEncryption()) {
+ qCWarning(E2EE) << "Room" << objectName() << "is already encrypted";
+ return;
+ }
+ setState<EncryptionEvent>(EncryptionEventContent::MegolmV1AesSha2);
+}
diff --git a/lib/room.h b/lib/room.h
index 23c0c846..cbe3d1ad 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -11,7 +11,7 @@
#include "connection.h"
#include "eventitem.h"
-#include "joinstate.h"
+#include "quotient_common.h"
#include "csapi/message_pagination.h"
@@ -71,6 +71,45 @@ public:
bool failed() const { return status == Failed; }
};
+//! \brief Data structure for a room member's read receipt
+//! \sa Room::lastReadReceipt
+class ReadReceipt {
+ Q_GADGET
+ Q_PROPERTY(QString eventId MEMBER eventId CONSTANT)
+ Q_PROPERTY(QDateTime timestamp MEMBER timestamp CONSTANT)
+public:
+ QString eventId;
+ QDateTime timestamp = {};
+
+ bool operator==(const ReadReceipt& other) const
+ {
+ return eventId == other.eventId && timestamp == other.timestamp;
+ }
+ bool operator!=(const ReadReceipt& other) const
+ {
+ return !operator==(other);
+ }
+};
+inline void swap(ReadReceipt& lhs, ReadReceipt& rhs)
+{
+ swap(lhs.eventId, rhs.eventId);
+ swap(lhs.timestamp, rhs.timestamp);
+}
+
+struct EventStats;
+
+struct Notification
+{
+ enum Type { None = 0, Basic, Highlight };
+ Q_ENUM(Notification)
+
+ Type type = None;
+
+private:
+ Q_GADGET
+ Q_PROPERTY(Type type MEMBER type CONSTANT)
+};
+
class Room : public QObject {
Q_OBJECT
Q_PROPERTY(Connection* connection READ connection CONSTANT)
@@ -87,6 +126,7 @@ class Room : public QObject {
Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged)
Q_PROPERTY(QStringList pinnedEventIds READ pinnedEventIds WRITE setPinnedEvents
NOTIFY pinnedEventsChanged)
+ Q_PROPERTY(QString displayNameForHtml READ displayNameForHtml NOTIFY displaynameChanged)
Q_PROPERTY(QString topic READ topic NOTIFY topicChanged)
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged
STORED false)
@@ -95,7 +135,6 @@ class Room : public QObject {
Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages)
Q_PROPERTY(QStringList memberNames READ safeMemberNames NOTIFY memberListChanged)
- Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged)
Q_PROPERTY(int joinedCount READ joinedCount NOTIFY memberListChanged)
Q_PROPERTY(int invitedCount READ invitedCount NOTIFY memberListChanged)
Q_PROPERTY(int totalMemberCount READ totalMemberCount NOTIFY memberListChanged)
@@ -106,21 +145,28 @@ class Room : public QObject {
setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged)
Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE
setLastDisplayedEventId NOTIFY lastDisplayedEventChanged)
-
+ //! \deprecated since 0.7
Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE
markMessagesAsRead NOTIFY readMarkerMoved)
+ Q_PROPERTY(QString lastFullyReadEventId READ lastFullyReadEventId WRITE
+ markMessagesAsRead NOTIFY fullyReadMarkerMoved)
+ //! \deprecated since 0.7
Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY
- unreadMessagesChanged)
- Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged)
- Q_PROPERTY(int highlightCount READ highlightCount NOTIFY
- highlightCountChanged RESET resetHighlightCount)
- Q_PROPERTY(int notificationCount READ notificationCount NOTIFY
- notificationCountChanged RESET resetNotificationCount)
+ partiallyReadStatsChanged STORED false)
+ //! \deprecated since 0.7
+ Q_PROPERTY(int unreadCount READ unreadCount NOTIFY partiallyReadStatsChanged
+ STORED false)
+ Q_PROPERTY(qsizetype highlightCount READ highlightCount
+ NOTIFY highlightCountChanged)
+ Q_PROPERTY(qsizetype notificationCount READ notificationCount
+ NOTIFY notificationCountChanged)
+ Q_PROPERTY(EventStats partiallyReadStats READ partiallyReadStats NOTIFY partiallyReadStatsChanged)
+ Q_PROPERTY(EventStats unreadStats READ unreadStats NOTIFY unreadStatsChanged)
Q_PROPERTY(bool allHistoryLoaded READ allHistoryLoaded NOTIFY addedMessages
STORED false)
Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged)
- Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged)
- Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged)
+ Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged STORED false)
+ Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged STORED false)
Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY
eventsHistoryJobChanged)
@@ -132,26 +178,49 @@ public:
using rev_iter_t = Timeline::const_reverse_iterator;
using timeline_iter_t = Timeline::const_iterator;
- enum Change : uint {
- NoChange = 0x0,
- NameChange = 0x1,
- AliasesChange = 0x2,
- CanonicalAliasChange = AliasesChange,
- TopicChange = 0x4,
- UnreadNotifsChange = 0x8,
- AvatarChange = 0x10,
- JoinStateChange = 0x20,
- TagsChange = 0x40,
- MembersChange = 0x80,
- /* = 0x100, */
- AccountDataChange = 0x200,
- SummaryChange = 0x400,
- ReadMarkerChange = 0x800,
- OtherChange = 0x8000,
- AnyChange = 0xFFFF
+ //! \brief Room changes that can be tracked using Room::changed() signal
+ //!
+ //! This enumeration lists kinds of changes that can be tracked with
+ //! a "cumulative" changed() signal instead of using individual signals for
+ //! each change. Specific enumerators mention these individual signals.
+ //! \sa changed
+ enum class Change : uint {
+ None = 0x0, //< No changes occurred in the room
+ Name = 0x1, //< \sa namesChanged, displaynameChanged
+ Aliases = 0x2, //< \sa namesChanged, displaynameChanged
+ CanonicalAlias = Aliases,
+ Topic = 0x4, //< \sa topicChanged
+ PartiallyReadStats = 0x8, //< \sa partiallyReadStatsChanged
+ DECL_DEPRECATED_ENUMERATOR(UnreadNotifs, PartiallyReadStats),
+ Avatar = 0x10, //< \sa avatarChanged
+ JoinState = 0x20, //< \sa joinStateChanged
+ Tags = 0x40, //< \sa tagsChanged
+ //! \sa userAdded, userRemoved, memberRenamed, memberListChanged,
+ //! displaynameChanged
+ Members = 0x80,
+ UnreadStats = 0x100, //< \sa unreadStatsChanged
+ AccountData Q_DECL_ENUMERATOR_DEPRECATED_X(
+ "Change::AccountData will be merged into Change::Other in 0.8") =
+ 0x200,
+ Summary = 0x400, //< \sa summaryChanged, displaynameChanged
+ ReadMarker Q_DECL_ENUMERATOR_DEPRECATED_X(
+ "Change::ReadMarker will be merged into Change::Other in 0.8") =
+ 0x800,
+ Highlights = 0x1000, //< \sa highlightCountChanged
+ //! A catch-all value that covers changes not listed above (such as
+ //! encryption turned on or the room having been upgraded), as well as
+ //! changes in the room state that the library is not aware of (e.g.,
+ //! custom state events) and m.read/m.fully_read position changes.
+ //! \sa encryptionChanged, upgraded, accountDataChanged
+ Other = 0x8000,
+ //! This is intended to test a Change/Changes value for non-emptiness;
+ //! adding <tt>& Change::Any</tt> has the same meaning as
+ //! !testFlag(Change::None) or adding <tt>!= Change::None</tt>
+ //! \note testFlag(Change::Any) tests that _all_ bits are on and
+ //! will always return false.
+ Any = 0xFFFF
};
- Q_DECLARE_FLAGS(Changes, Change)
- Q_FLAG(Changes)
+ QUO_DECLARE_FLAGS(Changes, Change)
Room(Connection* connection, QString id, JoinState initialJoinState);
~Room() override;
@@ -181,21 +250,15 @@ public:
Room* successor(JoinStates statesFilter = JoinState::Invite
| JoinState::Join) const;
QString name() const;
- /// Room aliases defined on the current user's server
- /// \sa remoteAliases, setLocalAliases
- [[deprecated("Use aliases()")]]
- QStringList localAliases() const;
- /// Room aliases defined on other servers
- /// \sa localAliases
- [[deprecated("Use aliases()")]]
- QStringList remoteAliases() const;
QString canonicalAlias() const;
QStringList altAliases() const;
+ //! Get a list of both canonical and alternative aliases
QStringList aliases() const;
QString displayName() const;
QStringList pinnedEventIds() const;
// Returns events available locally, use pinnedEventIds() for full list
QVector<const RoomEvent*> pinnedEvents() const;
+ QString displayNameForHtml() const;
QString topic() const;
QString avatarMediaId() const;
QUrl avatarUrl() const;
@@ -205,12 +268,10 @@ public:
QList<User*> membersLeft() const;
Q_INVOKABLE QList<Quotient::User*> users() const;
- [[deprecated("Use safeMemberNames() or htmlSafeMemberNames() instead")]]
+ Q_DECL_DEPRECATED_X("Use safeMemberNames() or htmlSafeMemberNames() instead") //
QStringList memberNames() const;
QStringList safeMemberNames() const;
QStringList htmlSafeMemberNames() const;
- [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]]
- int memberCount() const;
int timelineSize() const;
bool usesEncryption() const;
RoomEventPtr decryptMessage(const EncryptedEvent& encryptedEvent);
@@ -255,24 +316,27 @@ public:
*
* \return Join if the user is a room member; Leave otherwise
*/
+ Q_DECL_DEPRECATED_X("Use isMember() instead")
Q_INVOKABLE Quotient::JoinState memberJoinState(Quotient::User* user) const;
+ //! \brief Check the join state of a given user in this room
+ //!
+ //! \return the given user's state with respect to the room
+ Q_INVOKABLE Quotient::Membership memberState(const QString& userId) const;
+
+ //! Check whether a user with the given id is a member of the room
+ Q_INVOKABLE bool isMember(const QString& userId) const;
+
//! \brief Get a display name (without disambiguation) for the given member
//!
//! \sa safeMemberName, htmlSafeMemberName
Q_INVOKABLE QString memberName(const QString& mxId) const;
- /*!
- * \brief Get a disambiguated name for the given user in the room context
- *
- * \deprecated use safeMemberName() instead
- */
+ //! \brief Get a disambiguated name for the given user in the room context
+ Q_DECL_DEPRECATED_X("Use safeMemberName() instead")
Q_INVOKABLE QString roomMembername(const Quotient::User* u) const;
- /*!
- * \brief Get a disambiguated name for a user with this id in the room context
- *
- * \deprecated use safeMemberName() instead
- */
+ //! \brief Get a disambiguated name for a user with this id in the room
+ Q_DECL_DEPRECATED_X("Use safeMemberName() instead")
Q_INVOKABLE QString roomMembername(const QString& userId) const;
/*!
@@ -319,8 +383,6 @@ public:
* arrived event; same as messageEvents().cend()
*/
Timeline::const_iterator syncEdge() const;
- /// \deprecated Use historyEdge instead
- rev_iter_t timelineEdge() const;
Q_INVOKABLE Quotient::TimelineItem::index_t minTimelineIndex() const;
Q_INVOKABLE Quotient::TimelineItem::index_t maxTimelineIndex() const;
Q_INVOKABLE bool
@@ -337,9 +399,13 @@ public:
const char* relType) const;
const RoomCreateEvent* creation() const
- { return getCurrentState<RoomCreateEvent>(); }
+ {
+ return getCurrentState<RoomCreateEvent>();
+ }
const RoomTombstoneEvent* tombstone() const
- { return getCurrentState<RoomTombstoneEvent>(); }
+ {
+ return getCurrentState<RoomTombstoneEvent>();
+ }
bool displayed() const;
/// Mark the room as currently displayed to the user
@@ -359,44 +425,223 @@ public:
void setLastDisplayedEventId(const QString& eventId);
void setLastDisplayedEvent(TimelineItem::index_t index);
+ //! \brief Obtain a read receipt of any user
+ //! \deprecated Use lastReadReceipt or fullyReadMarker instead.
+ //!
+ //! Historically, readMarker was returning a "converged" read marker
+ //! representing both the read receipt and the fully read marker, as
+ //! Quotient managed them together. Since 0.6.8, a single-argument call of
+ //! readMarker returns the last read receipt position (for any room member)
+ //! and a call without arguments returns the last _fully read_ position,
+ //! to provide access to both positions separately while maintaining API
+ //! stability guarantees. 0.7 has separate methods to return read receipts
+ //! and the fully read marker - use them instead.
+ //! \sa lastReadReceipt
+ [[deprecated("Use lastReadReceipt() to get m.read receipt or"
+ " fullyReadMarker() to get m.fully_read marker")]] //
rev_iter_t readMarker(const User* user) const;
+ //! \brief Obtain the local user's fully-read marker
+ //! \deprecated Use fullyReadMarker instead
+ //!
+ //! See the documentation for the single-argument overload.
+ //! \sa fullyReadMarker
+ [[deprecated("Use localReadReceiptMarker() or fullyReadMarker()")]] //
rev_iter_t readMarker() const;
+ //! \brief Get the event id for the local user's fully-read marker
+ //! \deprecated Use lastFullyReadEventId instead
+ //!
+ //! See the readMarker documentation
+ [[deprecated("Use lastReadReceipt() to get m.read receipt or"
+ " lastFullyReadEventId() to get an event id that"
+ " m.fully_read marker points to")]] //
QString readMarkerEventId() const;
- QList<User*> usersAtEventId(const QString& eventId);
- /**
- * \brief Mark the event with uptoEventId as read
- *
- * Finds in the timeline and marks as read the event with
- * the specified id; also posts a read receipt to the server either
- * for this message or, if it's from the local user, for
- * the nearest non-local message before. uptoEventId must be non-empty.
- */
- void markMessagesAsRead(QString uptoEventId);
- /// Check whether there are unread messages in the room
+ //! \brief Get the latest read receipt from a user
+ //!
+ //! The user id must be valid. A read receipt with an empty event id
+ //! is returned if the user id is valid but there was no read receipt
+ //! from them.
+ //! \sa usersAtEventId
+ ReadReceipt lastReadReceipt(const QString& userId) const;
+
+ //! \brief Get the latest read receipt from the local user
+ //!
+ //! This is a shortcut for <tt>lastReadReceipt(localUserId)</tt>.
+ //! \sa lastReadReceipt
+ ReadReceipt lastLocalReadReceipt() const;
+
+ //! \brief Find the timeline item the local read receipt is at
+ //!
+ //! This is a shortcut for \code
+ //! room->findInTimeline(room->lastLocalReadReceipt().eventId);
+ //! \endcode
+ rev_iter_t localReadReceiptMarker() const;
+
+ //! \brief Get the latest event id marked as fully read
+ //!
+ //! This can be either the event id pointed to by the actual latest
+ //! m.fully_read event, or the latest event id marked locally as fully read
+ //! if markMessagesAsRead or markAllMessagesAsRead has been called and
+ //! the homeserver didn't return an updated m.fully_read event yet.
+ //! \sa markMessagesAsRead, markAllMessagesAsRead, fullyReadMarker
+ QString lastFullyReadEventId() const;
+
+ //! \brief Get the iterator to the latest timeline item marked as fully read
+ //!
+ //! This method calls findInTimeline on the result of lastFullyReadEventId.
+ //! If the fully read marker turns out to be outside the timeline (because
+ //! the event marked as fully read is too far back in the history) the
+ //! returned value will be equal to historyEdge.
+ //!
+ //! Be sure to read the caveats on iterators returned by findInTimeline.
+ //! \sa lastFullyReadEventId, findInTimeline
+ rev_iter_t fullyReadMarker() const;
+
+ //! \brief Get users whose latest read receipts point to the event
+ //!
+ //! This method is for cases when you need to show users who have read
+ //! an event. Calling it on inexistent or empty event id will return
+ //! an empty set.
+ //! \note The returned list may contain ids resolving to users that are
+ //! not loaded as room members yet (in particular, if members are not
+ //! yet lazy-loaded). For now this merely means that the user's
+ //! room-specific name and avatar will not be there; but generally
+ //! it's recommended to ensure that all room members are loaded
+ //! before operating on the result of this function.
+ //! \sa lastReadReceipt, allMembersLoaded
+ QSet<QString> userIdsAtEvent(const QString& eventId);
+
+ [[deprecated("Use userIdsAtEvent instead")]]
+ QSet<User*> usersAtEventId(const QString& eventId);
+
+ //! \brief Mark the event with uptoEventId as fully read
+ //!
+ //! Marks the event with the specified id as fully read locally and also
+ //! sends an update to m.fully_read account data to the server either
+ //! for this message or, if it's from the local user, for
+ //! the nearest non-local message before. uptoEventId must point to a known
+ //! event in the timeline; the method will do nothing if the event is behind
+ //! the current m.fully_read marker or is not loaded, to prevent
+ //! accidentally trying to move the marker back in the timeline.
+ //! \sa markAllMessagesAsRead, fullyReadMarker
+ Q_INVOKABLE void markMessagesAsRead(const QString& uptoEventId);
+
+ //! \brief Determine whether an event should be counted as unread
+ //!
+ //! The criteria of including an event in unread counters are described in
+ //! [MSC2654](https://github.com/matrix-org/matrix-doc/pull/2654); according
+ //! to these, the event should be counted as unread (or, in libQuotient
+ //! parlance, is "notable") if it is:
+ //! - either
+ //! - a message event that is not m.notice, or
+ //! - a state event with type being one of:
+ //! `m.room.topic`, `m.room.name`, `m.room.avatar`, `m.room.tombstone`;
+ //! - neither redacted, nor an edit (redactions cause the redacted event
+ //! to stop being notable, while edits are not notable themselves while
+ //! the original event usually is);
+ //! - from a non-local user (events from other devices of the local
+ //! user are not notable).
+ //! \sa partiallyReadStats, unreadStats
+ virtual bool isEventNotable(const TimelineItem& ti) const;
+
+ //! \brief Get notification details for an event
+ //!
+ //! This allows to get details on the kind of notification that should
+ //! generated for \p evt.
+ Notification notificationFor(const TimelineItem& ti) const;
+
+ //! \brief Get event statistics since the fully read marker
+ //!
+ //! This call returns a structure containing:
+ //! - the number of notable unread events since the fully read marker;
+ //! depending on the fully read marker state with respect to the local
+ //! timeline, this number may be either exact or estimated
+ //! (see EventStats::isEstimate);
+ //! - the number of highlights (TODO).
+ //!
+ //! Note that this is different from the unread count defined by MSC2654
+ //! and from the notification/highlight numbers defined by the spec in that
+ //! it counts events since the fully read marker, not since the last
+ //! read receipt position.
+ //!
+ //! As E2EE is not supported in the library, the returned result will always
+ //! be an estimate (<tt>isEstimate == true</tt>) for encrypted rooms;
+ //! moreover, since the library doesn't know how to tackle push rules yet
+ //! the number of highlights returned here will always be zero (there's no
+ //! good substitute for that now).
+ //!
+ //! \sa isEventNotable, fullyReadMarker, unreadStats, EventStats
+ EventStats partiallyReadStats() const;
+
+ //! \brief Get event statistics since the last read receipt
+ //!
+ //! This call returns a structure that contains the following three numbers,
+ //! all counted on the timeline segment between the event pointed to by
+ //! the m.fully_read marker and the sync edge:
+ //! - the number of unread events - depending on the read receipt state
+ //! with respect to the local timeline, this number may be either precise
+ //! or estimated (see EventStats::isEstimate);
+ //! - the number of highlights (TODO).
+ //!
+ //! As E2EE is not supported in the library, the returned result will always
+ //! be an estimate (<tt>isEstimate == true</tt>) for encrypted rooms;
+ //! moreover, since the library doesn't know how to tackle push rules yet
+ //! the number of highlights returned here will always be zero - use
+ //! highlightCount() for now.
+ //!
+ //! \sa isEventNotable, lastLocalReadReceipt, partiallyReadStats,
+ //! highlightCount
+ EventStats unreadStats() const;
+
+ [[deprecated(
+ "Use partiallyReadStats/unreadStats() and EventStats::empty()")]]
bool hasUnreadMessages() const;
- /** Get the number of unread messages in the room
- * Depending on the read marker state, this call may return either
- * a precise or an estimate number of unread events. Only "notable"
- * events (non-redacted message events from users other than local)
- * are counted.
- *
- * In a case when readMarker() == timelineEdge() (the local read
- * marker is beyond the local timeline) only the bottom limit of
- * the unread messages number can be estimated (and even that may
- * be slightly off due to, e.g., redactions of events not loaded
- * to the local timeline).
- *
- * If all messages are read, this function will return -1 (_not_ 0,
- * as zero may mean "zero or more unread messages" in a situation
- * when the read marker is outside the local timeline.
- */
+ //! \brief Get the number of notable events since the fully read marker
+ //!
+ //! \deprecated Since 0.7 there are two ways to count unread events: since
+ //! the fully read marker (used by libQuotient pre-0.7) and since the last
+ //! read receipt (as used by most of Matrix ecosystem, including the spec
+ //! and MSCs). This function currently returns a value derived from
+ //! partiallyReadStats() for compatibility with libQuotient 0.6; it will be
+ //! removed due to ambiguity. Use unreadStats() to obtain the spec-compliant
+ //! count of unread events and the highlight count; partiallyReadStats() to
+ //! obtain the unread events count since the fully read marker.
+ //!
+ //! \return -1 (_not 0_) when all messages are known to have been fully read,
+ //! i.e. the fully read marker points to _the latest notable_ event
+ //! loaded in the local timeline (which may be different from
+ //! the latest event in the local timeline as that might not be
+ //! notable);
+ //! 0 when there may be unread messages but the current local
+ //! timeline doesn't have any notable ones (often but not always
+ //! because it's entirely empty yet);
+ //! a positive integer when there is (or estimated to be) a number
+ //! of unread notable events as described above.
+ //!
+ //! \sa partiallyReadStats, unreadStats
+ [[deprecated("Use partiallyReadStats() or unreadStats() instead")]] //
int unreadCount() const;
- Q_INVOKABLE int notificationCount() const;
+ //! \brief Get the number of notifications since the last read receipt
+ //!
+ //! This is the same as <tt>unreadStats().notableCount</tt>.
+ //!
+ //! \sa unreadStats, lastLocalReadReceipt
+ qsizetype notificationCount() const;
+
+ //! \deprecated Use setReadReceipt() to drive changes in notification count
Q_INVOKABLE void resetNotificationCount();
- Q_INVOKABLE int highlightCount() const;
+
+ //! \brief Get the number of highlights since the last read receipt
+ //!
+ //! As of 0.7, this is defined by the homeserver as Quotient doesn't process
+ //! push rules.
+ //!
+ //! \sa unreadStats, lastLocalReadReceipt
+ qsizetype highlightCount() const;
+
+ //! \deprecated Use setReadReceipt() to drive changes in highlightCount
Q_INVOKABLE void resetHighlightCount();
/** Check whether the room has account data of the given type
@@ -466,6 +711,9 @@ public:
/// Get the list of users this room is a direct chat with
QList<User*> directChatUsers() const;
+ Q_INVOKABLE QUrl makeMediaUrl(const QString& eventId,
+ const QUrl &mxcUrl) const;
+
Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const;
Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const;
@@ -514,6 +762,19 @@ public:
Q_INVOKABLE const Quotient::StateEventBase*
getCurrentState(const QString& evtType, const QString& stateKey = {}) const;
+ /// Get all state events in the room.
+ /*! This method returns all known state events that have occured in
+ * the room, as a mapping from the event type and state key to value.
+ */
+ const QHash<StateEventKey, const StateEventBase*>& currentState() const;
+
+ /// Get all state events in the room of a certain type.
+ /*! This method returns all known state events that have occured in
+ * the room of the given type.
+ */
+ Q_INVOKABLE const QVector<const StateEventBase*>
+ stateEventsOfType(const QString& evtType) const;
+
/// Get a state event with the given event type and state key
/*! This is a typesafe overload that accepts a C++ event type instead of
* its Matrix name.
@@ -554,8 +815,13 @@ public Q_SLOTS:
QString postHtmlText(const QString& plainText, const QString& html);
/// Send a reaction on a given event with a given key
QString postReaction(const QString& eventId, const QString& key);
+
+ QString postFile(const QString& plainText, EventContent::TypedBase* content);
+#if QT_VERSION_MAJOR < 6
+ Q_DECL_DEPRECATED_X("Use postFile(QString, MessageEventType, EventContent)") //
QString postFile(const QString& plainText, const QUrl& localPath,
bool asGenericFile = false);
+#endif
/** Post a pre-created room message event
*
* Takes ownership of the event, deleting it once the matching one
@@ -594,7 +860,12 @@ public Q_SLOTS:
void downloadFile(const QString& eventId, const QUrl& localFilename = {});
void cancelFileTransfer(const QString& id);
- /// Mark all messages in the room as read
+ //! \brief Set a given event as last read and post a read receipt on it
+ //!
+ //! Does nothing if the event is behind the current read receipt.
+ //! \sa lastReadReceipt, markMessagesAsRead, markAllMessagesAsRead
+ void setReadReceipt(const QString& atEventId);
+ //! Put the fully-read marker at the latest message in the room
void markAllMessagesAsRead();
/// Switch the room's version (aka upgrade)
@@ -608,6 +879,12 @@ public Q_SLOTS:
void answerCall(const QString& callId, const QString& sdp);
void hangupCall(const QString& callId);
+ /**
+ * Activates encryption for this room.
+ * Warning: Cannot be undone
+ */
+ void activateEncryption();
+
Q_SIGNALS:
/// Initial set of state events has been loaded
/**
@@ -619,11 +896,11 @@ Q_SIGNALS:
*/
void baseStateLoaded();
void eventsHistoryJobChanged();
- void aboutToAddHistoricalMessages(RoomEventsRange events);
- void aboutToAddNewMessages(RoomEventsRange events);
+ void aboutToAddHistoricalMessages(Quotient::RoomEventsRange events);
+ void aboutToAddNewMessages(Quotient::RoomEventsRange events);
void addedMessages(int fromIndex, int toIndex);
/// The event is about to be appended to the list of pending events
- void pendingEventAboutToAdd(RoomEvent* event);
+ void pendingEventAboutToAdd(Quotient::RoomEvent* event);
/// An event has been appended to the list of pending events
void pendingEventAdded();
/// The remote echo has arrived with the sync and will be merged
@@ -692,17 +969,26 @@ Q_SIGNALS:
Quotient::JoinState newState);
void typingChanged();
- void highlightCountChanged();
- void notificationCountChanged();
+ void highlightCountChanged(); //< \sa highlightCount
+ void notificationCountChanged(); //< \sa notificationCount
void displayedChanged(bool displayed);
void firstDisplayedEventChanged();
void lastDisplayedEventChanged();
+ //! The event that m.read receipt points to has changed
+ //! \sa lastReadReceipt
void lastReadEventChanged(Quotient::User* user);
+ void fullyReadMarkerMoved(QString fromEventId, QString toEventId);
+ //! \deprecated since 0.7 - use fullyReadMarkerMoved
void readMarkerMoved(QString fromEventId, QString toEventId);
+ //! \deprecated since 0.7 - use lastReadEventChanged
void readMarkerForUserMoved(Quotient::User* user, QString fromEventId,
QString toEventId);
+ //! \deprecated since 0.7 - use either partiallyReadStatsChanged
+ //! or unreadStatsChanged
void unreadMessagesChanged(Quotient::Room* room);
+ void partiallyReadStatsChanged();
+ void unreadStatsChanged();
void accountDataAboutToChange(QString type);
void accountDataChanged(QString type);
@@ -717,7 +1003,8 @@ Q_SIGNALS:
void fileTransferProgress(QString id, qint64 progress, qint64 total);
void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl);
void fileTransferFailed(QString id, QString errorMessage = {});
- void fileTransferCancelled(QString id);
+ // fileTransferCancelled() is no more here; use fileTransferFailed() and
+ // check the transfer status instead
void callEvent(Quotient::Room* room, const Quotient::RoomEvent* event);
@@ -743,6 +1030,7 @@ protected:
{}
virtual QJsonObject toJson() const;
virtual void updateData(SyncRoomData&& data, bool fromCache = false);
+ virtual Notification checkForNotifications(const TimelineItem& ti);
private:
friend class Connection;
@@ -761,7 +1049,7 @@ public:
explicit MemberSorter(const Room* r) : room(r) {}
bool operator()(User* u1, User* u2) const;
- bool operator()(User* u1, const QString& u2name) const;
+ bool operator()(User* u1, QStringView u2name) const;
template <typename ContT, typename ValT>
typename ContT::size_type lowerBoundIndex(const ContT& c, const ValT& v) const
@@ -774,4 +1062,5 @@ private:
};
} // namespace Quotient
Q_DECLARE_METATYPE(Quotient::FileTransferInfo)
+Q_DECLARE_METATYPE(Quotient::ReadReceipt)
Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::Room::Changes)
diff --git a/lib/settings.cpp b/lib/settings.cpp
index 703f4320..ed9082b0 100644
--- a/lib/settings.cpp
+++ b/lib/settings.cpp
@@ -21,7 +21,9 @@ void Settings::setLegacyNames(const QString& organizationName,
Settings::Settings(QObject* parent) : QSettings(parent)
{
+#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
setIniCodec("UTF-8");
+#endif
}
void Settings::setValue(const QString& key, const QVariant& value)
@@ -124,19 +126,6 @@ void AccountSettings::setHomeserver(const QUrl& url)
QString AccountSettings::userId() const { return group().section('/', -1); }
-QString AccountSettings::accessToken() const
-{
- return value(AccessTokenKey).toString();
-}
-
-void AccountSettings::setAccessToken(const QString& accessToken)
-{
- qCWarning(MAIN) << "Saving access_token to QSettings is insecure."
- " Developers, do it manually or contribute to share "
- "QtKeychain logic to libQuotient.";
- setValue(AccessTokenKey, accessToken);
-}
-
void AccountSettings::clearAccessToken()
{
legacySettings.remove(AccessTokenKey);
diff --git a/lib/settings.h b/lib/settings.h
index 84c54802..efd0d714 100644
--- a/lib/settings.h
+++ b/lib/settings.h
@@ -78,9 +78,8 @@ protected:
class SettingsGroup : public Settings {
public:
- template <typename... ArgTs>
- explicit SettingsGroup(QString path, ArgTs&&... qsettingsArgs)
- : Settings(std::forward<ArgTs>(qsettingsArgs)...)
+ explicit SettingsGroup(QString path, QObject* parent = nullptr)
+ : Settings(parent)
, groupPath(std::move(path))
{}
@@ -131,15 +130,11 @@ class AccountSettings : public SettingsGroup {
QTNT_DECLARE_SETTING(QString, deviceId, setDeviceId)
QTNT_DECLARE_SETTING(QString, deviceName, setDeviceName)
QTNT_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn)
- /** \deprecated \sa setAccessToken */
- Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken)
Q_PROPERTY(QByteArray encryptionAccountPickle READ encryptionAccountPickle
WRITE setEncryptionAccountPickle)
public:
- template <typename... ArgTs>
- explicit AccountSettings(const QString& accountId, ArgTs&&... qsettingsArgs)
- : SettingsGroup("Accounts/" + accountId,
- std::forward<ArgTs>(qsettingsArgs)...)
+ explicit AccountSettings(const QString& accountId, QObject* parent = nullptr)
+ : SettingsGroup("Accounts/" + accountId, parent)
{}
QString userId() const;
@@ -147,11 +142,7 @@ public:
QUrl homeserver() const;
void setHomeserver(const QUrl& url);
- /** \deprecated \sa setToken */
- QString accessToken() const;
- /** \deprecated Storing accessToken in QSettings is unsafe,
- * see quotient-im/Quaternion#181 */
- void setAccessToken(const QString& accessToken);
+ Q_DECL_DEPRECATED_X("Access tokens are not stored in QSettings any more")
Q_INVOKABLE void clearAccessToken();
QByteArray encryptionAccountPickle();
diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp
index adcba5cd..396e77eb 100644
--- a/lib/syncdata.cpp
+++ b/lib/syncdata.cpp
@@ -10,9 +10,6 @@
using namespace Quotient;
-const QString SyncRoomData::UnreadCountKey =
- QStringLiteral("x-quotient.unread_count");
-
bool RoomSummary::isEmpty() const
{
return !joinedMemberCount && !invitedMemberCount && !heroes;
@@ -64,23 +61,23 @@ inline EventsArrayT load(const QJsonObject& batches, StrT keyName)
return fromJson<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
+SyncRoomData::SyncRoomData(QString roomId_, JoinState joinState,
+ const QJsonObject& roomJson)
+ : roomId(std::move(roomId_))
+ , joinState(joinState)
+ , summary(fromJson<RoomSummary>(roomJson["summary"_ls]))
+ , state(load<StateEvents>(roomJson, joinState == JoinState::Invite
? "invite_state"_ls
: "state"_ls))
{
switch (joinState) {
case JoinState::Join:
- ephemeral = load<Events>(room_, "ephemeral"_ls);
+ ephemeral = load<Events>(roomJson, "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();
+ accountData = load<Events>(roomJson, "account_data"_ls);
+ timeline = load<RoomEvents>(roomJson, "timeline"_ls);
+ const auto timelineJson = roomJson.value("timeline"_ls).toObject();
timelineLimited = timelineJson.value("limited"_ls).toBool();
timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString();
@@ -89,21 +86,24 @@ SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
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;
+ const auto unreadJson = roomJson.value(UnreadNotificationsKey).toObject();
+
+ fromJson(unreadJson.value(PartiallyReadCountKey), partiallyReadCount);
+ if (!partiallyReadCount.has_value())
+ fromJson(unreadJson.value("x-quotient.unread_count"_ls),
+ partiallyReadCount);
+
+ fromJson(roomJson.value(NewUnreadCountKey), unreadCount);
+ if (!unreadCount.has_value())
+ fromJson(unreadJson.value("notification_count"_ls), unreadCount);
+ fromJson(unreadJson.value(HighlightCountKey), highlightCount);
}
SyncData::SyncData(const QString& cacheFileName)
{
QFileInfo cacheFileInfo { cacheFileName };
auto json = loadJson(cacheFileName);
- auto requiredVersion = std::get<0>(cacheVersion());
+ auto requiredVersion = MajorCacheVersion;
auto actualVersion =
json.value("cache_version"_ls).toObject().value("major"_ls).toInt();
if (actualVersion == requiredVersion)
@@ -128,6 +128,11 @@ Events&& SyncData::takeAccountData() { return std::move(accountData); }
Events&& SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); }
+std::pair<int, int> SyncData::cacheVersion()
+{
+ return { MajorCacheVersion, 2 };
+}
+
QJsonObject SyncData::loadJson(const QString& fileName)
{
QFile roomFile { fileName };
@@ -171,13 +176,14 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir)
deviceOneTimeKeysCount_);
auto rooms = json.value("rooms"_ls).toObject();
- JoinStates::Int ii = 1; // ii is used to make a JoinState value
auto totalRooms = 0;
auto totalEvents = 0;
- for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) {
+ for (size_t i = 0; i < JoinStateStrings.size(); ++i) {
+ // This assumes that MemberState values go over powers of 2: 1,2,4,...
+ const auto joinState = JoinState(1U << i);
const auto rs = rooms.value(JoinStateStrings[i]).toObject();
// We have a Qt container on the right and an STL one on the left
- roomData.reserve(static_cast<size_t>(rs.size()));
+ roomData.reserve(roomData.size() + static_cast<size_t>(rs.size()));
for (auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) {
auto roomJson =
roomIt->isObject()
@@ -187,7 +193,7 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir)
unresolvedRoomIds.push_back(roomIt.key());
continue;
}
- roomData.emplace_back(roomIt.key(), JoinState(ii), roomJson);
+ roomData.emplace_back(roomIt.key(), joinState, roomJson);
const auto& r = roomData.back();
totalEvents += r.state.size() + r.ephemeral.size()
+ r.accountData.size() + r.timeline.size();
diff --git a/lib/syncdata.h b/lib/syncdata.h
index e69bac17..36d2e0bf 100644
--- a/lib/syncdata.h
+++ b/lib/syncdata.h
@@ -3,11 +3,17 @@
#pragma once
-#include "joinstate.h"
+#include "quotient_common.h"
#include "events/stateevent.h"
namespace Quotient {
+
+constexpr auto UnreadNotificationsKey = "unread_notifications"_ls;
+constexpr auto PartiallyReadCountKey = "x-quotient.since_fully_read_count"_ls;
+constexpr auto NewUnreadCountKey = "org.matrix.msc2654.unread_count"_ls;
+constexpr auto HighlightCountKey = "highlight_count"_ls;
+
/// Room summary, as defined in MSC688
/**
* Every member of this structure is an Omittable; as per the MSC, only
@@ -29,7 +35,6 @@ struct RoomSummary {
};
QDebug operator<<(QDebug dbg, const RoomSummary& rs);
-
template <>
struct JsonObjectConverter<RoomSummary> {
static void dumpTo(QJsonObject& jo, const RoomSummary& rs);
@@ -48,16 +53,14 @@ public:
bool timelineLimited;
QString timelinePrevBatch;
- int unreadCount;
- int highlightCount;
- int notificationCount;
+ Omittable<int> partiallyReadCount;
+ Omittable<int> unreadCount;
+ Omittable<int> highlightCount;
- SyncRoomData(const QString& roomId, JoinState joinState_,
- const QJsonObject& room_);
+ SyncRoomData(QString roomId, JoinState joinState,
+ const QJsonObject& roomJson);
SyncRoomData(SyncRoomData&&) = default;
SyncRoomData& operator=(SyncRoomData&&) = default;
-
- static const QString UnreadCountKey;
};
// QVector cannot work with non-copyable objects, std::vector can.
@@ -87,7 +90,8 @@ public:
QStringList unresolvedRooms() const { return unresolvedRoomIds; }
- static std::pair<int, int> cacheVersion() { return { 11, 0 }; }
+ static constexpr int MajorCacheVersion = 11;
+ static std::pair<int, int> cacheVersion();
static QString fileNameForRoom(QString roomId);
private:
diff --git a/lib/uri.cpp b/lib/uri.cpp
index 291bfcae..c8843dda 100644
--- a/lib/uri.cpp
+++ b/lib/uri.cpp
@@ -70,7 +70,7 @@ static QString pathSegment(const QUrl& url, int which)
encodedPath(url).section('/', which, which).toUtf8());
}
-static auto decodeFragmentPart(const QStringRef& part)
+static auto decodeFragmentPart(QStringView part)
{
return QUrl::fromPercentEncoding(part.toLatin1()).toUtf8();
}
@@ -98,7 +98,7 @@ Uri::Uri(QUrl url) : QUrl(std::move(url))
if (scheme() == "matrix") {
// Check sanity as per https://github.com/matrix-org/matrix-doc/pull/2312
const auto& urlPath = encodedPath(*this);
- const auto& splitPath = urlPath.splitRef('/');
+ const auto& splitPath = urlPath.split('/');
switch (splitPath.size()) {
case 2:
break;
@@ -128,9 +128,9 @@ Uri::Uri(QUrl url) : QUrl(std::move(url))
// so force QUrl to decode everything.
auto f = fragment(QUrl::EncodeUnicode);
if (auto&& m = MatrixToUrlRE.match(f); m.hasMatch())
- *this = Uri { decodeFragmentPart(m.capturedRef("main")),
- decodeFragmentPart(m.capturedRef("sec")),
- decodeFragmentPart(m.capturedRef("query")) };
+ *this = Uri { decodeFragmentPart(m.capturedView(u"main")),
+ decodeFragmentPart(m.capturedView(u"sec")),
+ decodeFragmentPart(m.capturedView(u"query")) };
}
}
@@ -186,14 +186,18 @@ QString Uri::primaryId() const
if (primaryType_ == Empty || primaryType_ == Invalid)
return {};
- const auto& idStem = pathSegment(*this, 1);
- return idStem.isEmpty() ? idStem : primaryType_ + idStem;
+ auto idStem = pathSegment(*this, 1);
+ if (!idStem.isEmpty())
+ idStem.push_front(char(primaryType_));
+ return idStem;
}
QString Uri::secondaryId() const
{
- const auto& idStem = pathSegment(*this, 3);
- return idStem.isEmpty() ? idStem : secondaryType() + idStem;
+ auto idStem = pathSegment(*this, 3);
+ if (!idStem.isEmpty())
+ idStem.push_front(char(secondaryType()));
+ return idStem;
}
static const auto ActionKey = QStringLiteral("action");
diff --git a/lib/uriresolver.cpp b/lib/uriresolver.cpp
index 287e0552..681e3842 100644
--- a/lib/uriresolver.cpp
+++ b/lib/uriresolver.cpp
@@ -8,6 +8,8 @@
using namespace Quotient;
+UriResolverBase::~UriResolverBase() = default;
+
UriResolveResult UriResolverBase::visitResource(Connection* account,
const Uri& uri)
{
diff --git a/lib/uriresolver.h b/lib/uriresolver.h
index f290e58b..ff97324a 100644
--- a/lib/uriresolver.h
+++ b/lib/uriresolver.h
@@ -42,23 +42,29 @@ public:
UriResolveResult visitResource(Connection* account, const Uri& uri);
protected:
+ virtual ~UriResolverBase() = 0;
+
/// Called by visitResource() when the passed URI identifies a Matrix user
/*!
* \return IncorrectAction if the action is not correct or not supported;
* UriResolved if it is accepted; other values are disallowed
*/
- virtual UriResolveResult visitUser(User* user, const QString& action)
+ virtual UriResolveResult visitUser(User* user [[maybe_unused]],
+ const QString& action [[maybe_unused]])
{
return IncorrectAction;
}
/// Called by visitResource() when the passed URI identifies a room or
/// an event in a room
- virtual void visitRoom(Room* room, const QString& eventId) {}
+ virtual void visitRoom(Room* room [[maybe_unused]],
+ const QString& eventId [[maybe_unused]])
+ {}
/// Called by visitResource() when the passed URI has `action() == "join"`
/// and identifies a room that the user defined by the Connection argument
/// is not a member of
- virtual void joinRoom(Connection* account, const QString& roomAliasOrId,
- const QStringList& viaServers = {})
+ virtual void joinRoom(Connection* account [[maybe_unused]],
+ const QString& roomAliasOrId [[maybe_unused]],
+ const QStringList& viaServers [[maybe_unused]] = {})
{}
/// Called by visitResource() when the passed URI has `type() == NonMatrix`
/*!
@@ -67,7 +73,10 @@ protected:
* `return QDesktopServices::openUrl(url);` but it's strongly advised to
* ask for a user confirmation beforehand.
*/
- virtual bool visitNonMatrix(const QUrl& url) { return false; }
+ virtual bool visitNonMatrix(const QUrl& url [[maybe_unused]])
+ {
+ return false;
+ }
};
/*! \brief Resolve the resource and invoke an action on it, via function objects
diff --git a/lib/user.cpp b/lib/user.cpp
index 7933c5d9..7da71dba 100644
--- a/lib/user.cpp
+++ b/lib/user.cpp
@@ -65,7 +65,8 @@ User::~User() = default;
void User::load()
{
- auto *profileJob = connection()->callApi<GetUserProfileJob>(id());
+ auto* profileJob =
+ connection()->callApi<GetUserProfileJob>(id());
connect(profileJob, &BaseJob::result, this, [this, profileJob] {
d->defaultName = profileJob->displayname();
d->defaultAvatar = Avatar(QUrl(profileJob->avatarUrl()));
@@ -81,7 +82,7 @@ bool User::isGuest() const
Q_ASSERT(!d->id.isEmpty() && d->id.startsWith('@'));
auto it = std::find_if_not(d->id.cbegin() + 1, d->id.cend(),
[](QChar c) { return c.isDigit(); });
- Q_ASSERT(it != d->id.end());
+ Q_ASSERT(it != d->id.cend());
return *it == ':';
}
@@ -92,8 +93,6 @@ QString User::name(const Room* room) const
return room ? room->memberName(id()) : d->defaultName;
}
-QString User::rawName(const Room* room) const { return name(room); }
-
void User::rename(const QString& newName)
{
const auto actualNewName = sanitized(newName);
@@ -121,11 +120,11 @@ void User::rename(const QString& newName, const Room* r)
rename(newName);
return;
}
- Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__,
+ // #481: take the current state and update it with the new name
+ auto evtC = r->getCurrentState<RoomMemberEvent>(id())->content();
+ Q_ASSERT_X(evtC.membership == Membership::Join, __FUNCTION__,
"Attempt to rename a user that's not a room member");
- const auto actualNewName = sanitized(newName);
- MemberEventContent evtC;
- evtC.displayName = actualNewName;
+ evtC.displayName = sanitized(newName);
r->setState<RoomMemberEvent>(id(), move(evtC));
// The state will be updated locally after it arrives with sync
}
@@ -134,17 +133,17 @@ template <typename SourceT>
inline bool User::doSetAvatar(SourceT&& source)
{
return d->defaultAvatar.upload(
- connection(), source, [this](const QString& contentUri) {
+ connection(), source, [this](const QUrl& contentUri) {
auto* j = connection()->callApi<SetAvatarUrlJob>(id(), contentUri);
connect(j, &BaseJob::success, this,
- [this, newUrl = QUrl(contentUri)] {
- if (newUrl == d->defaultAvatar.url()) {
- d->defaultAvatar.updateUrl(newUrl);
+ [this, contentUri] {
+ if (contentUri == d->defaultAvatar.url()) {
+ d->defaultAvatar.updateUrl(contentUri);
emit defaultAvatarChanged();
} else
qCWarning(MAIN) << "User" << id()
<< "already has avatar URL set to"
- << newUrl.toDisplayString();
+ << contentUri.toDisplayString();
});
});
}
@@ -161,7 +160,7 @@ bool User::setAvatar(QIODevice* source)
void User::removeAvatar()
{
- connection()->callApi<SetAvatarUrlJob>(id(), "");
+ connection()->callApi<SetAvatarUrlJob>(id(), QUrl());
}
void User::requestDirectChat() { connection()->requestDirectChat(this); }
@@ -184,8 +183,6 @@ QString User::fullName(const Room* room) const
return displayName.isEmpty() ? id() : (displayName % " (" % id() % ')');
}
-QString User::bridged() const { return {}; }
-
const Avatar& User::avatarObject(const Room* room) const
{
if (!room)
@@ -196,18 +193,18 @@ const Avatar& User::avatarObject(const Room* room) const
return d->otherAvatars.try_emplace(mediaId, url).first->second;
}
-QImage User::avatar(int dimension, const Room* room)
+QImage User::avatar(int dimension, const Room* room) const
{
return avatar(dimension, dimension, room);
}
-QImage User::avatar(int width, int height, const Room* room)
+QImage User::avatar(int width, int height, const Room* room) const
{
return avatar(width, height, room, [] {});
}
QImage User::avatar(int width, int height, const Room* room,
- const Avatar::get_callback_t& callback)
+ const Avatar::get_callback_t& callback) const
{
return avatarObject(room).get(connection(), width, height, callback);
}
diff --git a/lib/user.h b/lib/user.h
index e4560843..78b72bf2 100644
--- a/lib/user.h
+++ b/lib/user.h
@@ -22,7 +22,6 @@ class User : public QObject {
Q_PROPERTY(QString name READ name NOTIFY defaultNameChanged)
Q_PROPERTY(QString displayName READ displayname NOTIFY defaultNameChanged STORED false)
Q_PROPERTY(QString fullName READ fullName NOTIFY defaultNameChanged STORED false)
- Q_PROPERTY(QString bridgeName READ bridged NOTIFY defaultNameChanged STORED false)
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY defaultAvatarChanged STORED false)
Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY defaultAvatarChanged)
public:
@@ -40,18 +39,10 @@ public:
* This may be empty if the user didn't choose the name or cleared
* it. If the user is bridged, the bridge postfix (such as '(IRC)')
* is stripped out. No disambiguation for the room is done.
- * \sa displayName, rawName
+ * \sa displayName
*/
QString name(const Room* room = nullptr) const;
- /** Get the user name along with the bridge postfix
- * This function is similar to name() but appends the bridge postfix
- * (such as '(IRC)') to the user name. No disambiguation is done.
- * \sa name, displayName
- */
- [[deprecated("Bridge postfixes exist no more, use name() instead")]]
- QString rawName(const Room* room = nullptr) const;
-
/** Get the displayed user name
* When \p room is null, this method returns result of name() if
* the name is non-empty; otherwise it returns user id.
@@ -70,13 +61,6 @@ public:
*/
QString fullName(const Room* room = nullptr) const;
- /**
- * Returns the name of bridge the user is connected from or empty.
- */
- [[deprecated("Bridged status is no more supported; this always returns"
- " an empty string")]]
- QString bridged() const;
-
/** Whether the user is a guest
* As of now, the function relies on the convention used in Synapse
* that guests and only guests have all-numeric IDs. This may or
@@ -99,11 +83,11 @@ public:
*/
const Avatar& avatarObject(const Room* room = nullptr) const;
Q_INVOKABLE QImage avatar(int dimension,
- const Quotient::Room* room = nullptr);
+ const Quotient::Room* room = nullptr) const;
Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight,
- const Quotient::Room* room = nullptr);
+ const Quotient::Room* room = nullptr) const;
QImage avatar(int width, int height, const Room* room,
- const Avatar::get_callback_t& callback);
+ const Avatar::get_callback_t& callback) const;
QString avatarMediaId(const Room* room = nullptr) const;
QUrl avatarUrl(const Room* room = nullptr) const;
diff --git a/lib/util.cpp b/lib/util.cpp
index 904bfd5a..03ebf325 100644
--- a/lib/util.cpp
+++ b/lib/util.cpp
@@ -14,9 +14,6 @@
static const auto RegExpOptions =
QRegularExpression::CaseInsensitiveOption
-#if QT_VERSION < QT_VERSION_CHECK(5, 12, 0)
- | QRegularExpression::OptimizeOnFirstUsageOption // Default since 5.12
-#endif
| QRegularExpression::UseUnicodePropertiesOption;
// Converts all that looks like a URL into HTML links
@@ -33,7 +30,7 @@ void Quotient::linkifyUrls(QString& htmlEscapedText)
// comma or dot
static const QRegularExpression FullUrlRegExp(
QStringLiteral(
- R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet|matrix):(//)?)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"),
+ R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp):(//)?\w|(magnet|matrix):)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"),
RegExpOptions);
// email address:
// [word chars, dots or dashes]@[word chars, dots or dashes].[word chars]
@@ -44,7 +41,7 @@ void Quotient::linkifyUrls(QString& htmlEscapedText)
// https://matrix.org/docs/spec/appendices.html#identifier-grammar
static const QRegularExpression MxIdRegExp(
QStringLiteral(
- R"((^|[^<>/])([!#@][-a-z0-9_=#/.]{1,252}:(?:\w|\.|-)+\.\w+(?::\d{1,5})?))"),
+ R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"),
RegExpOptions);
Q_ASSERT(FullUrlRegExp.isValid() && EmailAddressRegExp.isValid()
&& MxIdRegExp.isValid());
@@ -119,34 +116,22 @@ QString Quotient::serverPart(const QString& mxId)
return parser.match(mxId).captured(1);
}
-// Tests for function_traits<>
-
-using namespace Quotient;
-
-int f_();
-static_assert(std::is_same<fn_return_t<decltype(f_)>, int>::value,
- "Test fn_return_t<>");
-
-void f1_(int, QString);
-static_assert(std::is_same<fn_arg_t<decltype(f1_), 1>, QString>::value,
- "Test fn_arg_t<>");
-
-struct Fo {
- int operator()();
- static constexpr auto l = [] { return 0.0f; };
-};
-static_assert(std::is_same<fn_return_t<Fo>, int>::value,
- "Test return type of function object");
-static_assert(std::is_same<fn_return_t<decltype(Fo::l)>, float>::value,
- "Test return type of lambda");
-
-struct Fo1 {
- void operator()(int);
-};
-static_assert(std::is_same<fn_arg_t<Fo1>, int>(),
- "Test fn_arg_t defaulting to first argument");
-
-template <typename T>
-static QString ft(T&&);
-static_assert(std::is_same<fn_arg_t<decltype(ft<QString>)>, QString&&>(),
- "Test function templates");
+QString Quotient::versionString()
+{
+ return QStringLiteral(Quotient_VERSION_STRING);
+}
+
+int Quotient::majorVersion()
+{
+ return Quotient_VERSION_MAJOR;
+}
+
+int Quotient::minorVersion()
+{
+ return Quotient_VERSION_MINOR;
+}
+
+int Quotient::patchVersion()
+{
+ return Quotient_VERSION_PATCH;
+}
diff --git a/lib/util.h b/lib/util.h
index cb0ff44a..97f0ecbc 100644
--- a/lib/util.h
+++ b/lib/util.h
@@ -12,10 +12,30 @@
#include <unordered_map>
#include <optional>
-// Along the lines of Q_DISABLE_COPY - the upstream version comes in Qt 5.13
-#define DISABLE_MOVE(_ClassName) \
- _ClassName(_ClassName&&) Q_DECL_EQ_DELETE; \
- _ClassName& operator=(_ClassName&&) Q_DECL_EQ_DELETE;
+#ifndef Q_DISABLE_MOVE
+// Q_DISABLE_MOVE was introduced in Q_VERSION_CHECK(5,13,0)
+# define Q_DISABLE_MOVE(_ClassName) \
+ _ClassName(_ClassName&&) Q_DECL_EQ_DELETE; \
+ _ClassName& operator=(_ClassName&&) Q_DECL_EQ_DELETE;
+#endif
+
+#ifndef Q_DISABLE_COPY_MOVE
+#define Q_DISABLE_COPY_MOVE(Class) \
+ Q_DISABLE_COPY(Class) \
+ Q_DISABLE_MOVE(Class)
+#endif
+
+#define DISABLE_MOVE(_ClassName) \
+static_assert(false, "Use Q_DISABLE_MOVE instead; Quotient enables it across all used versions of Qt");
+
+#ifndef QT_IGNORE_DEPRECATIONS
+// QT_IGNORE_DEPRECATIONS was introduced in Q_VERSION_CHECK(5,15,0)
+# define QT_IGNORE_DEPRECATIONS(statement) \
+ QT_WARNING_PUSH \
+ QT_WARNING_DISABLE_DEPRECATED \
+ statement \
+ QT_WARNING_POP
+#endif
namespace Quotient {
/// An equivalent of std::hash for QTypes to enable std::unordered_map<QType, ...>
@@ -30,6 +50,13 @@ struct HashQ {
template <typename KeyT, typename ValT>
using UnorderedMap = std::unordered_map<KeyT, ValT, HashQ<KeyT>>;
+namespace _impl {
+ template <typename TT>
+ constexpr inline auto IsOmittableValue = false;
+ template <typename TT>
+ constexpr inline auto IsOmittable = IsOmittableValue<std::decay_t<TT>>;
+}
+
constexpr auto none = std::nullopt;
/** `std::optional` with tweaks
@@ -107,18 +134,18 @@ public:
return !this->has_value();
}
- /// Merge the value from another Omittable
- /// \return true if \p other is not omitted and the value of
- /// the current Omittable was different (or omitted);
- /// in other words, if the current Omittable has changed;
- /// false otherwise
+ //! Merge the value from another Omittable
+ //! \return true if \p other is not omitted and the value of
+ //! the current Omittable was different (or omitted),
+ //! in other words, if the current Omittable has changed;
+ //! false otherwise
template <typename T1>
auto merge(const Omittable<T1>& other)
- -> std::enable_if_t<std::is_convertible<T1, T>::value, bool>
+ -> std::enable_if_t<std::is_convertible_v<T1, T>, bool>
{
if (!other || (this->has_value() && **this == *other))
return false;
- *this = other;
+ emplace(*other);
return true;
}
@@ -131,72 +158,38 @@ public:
const value_type& operator*() const& { return base_type::operator*(); }
value_type& operator*() && { return base_type::operator*(); }
};
+template <typename T>
+Omittable(T&&) -> Omittable<T>;
namespace _impl {
- template <typename AlwaysVoid, typename>
- struct fn_traits {};
+ template <typename T>
+ constexpr inline auto IsOmittableValue<Omittable<T>> = true;
}
-/// Determine traits of an arbitrary function/lambda/functor
-/*!
- * Doesn't work with generic lambdas and function objects that have
- * operator() overloaded.
- * \sa
- * https://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda#7943765
- */
-template <typename T>
-struct function_traits
- : public _impl::fn_traits<void, std::remove_reference_t<T>> {};
-
-// Specialisation for a function
-template <typename ReturnT, typename... ArgTs>
-struct function_traits<ReturnT(ArgTs...)> {
- using return_type = ReturnT;
- using arg_types = std::tuple<ArgTs...>;
- // Doesn't (and there's no plan to make it) work for "classic"
- // member functions (i.e. outside of functors).
- // See also the comment for wrap_in_function() below
- using function_type = std::function<ReturnT(ArgTs...)>;
-};
+template <typename T1, typename T2>
+inline auto merge(Omittable<T1>& lhs, T2&& rhs)
+{
+ return lhs.merge(std::forward<T2>(rhs));
+}
-namespace _impl {
- // Specialisation for function objects with (non-overloaded) operator()
- // (this includes non-generic lambdas)
- template <typename T>
- struct fn_traits<decltype(void(&T::operator())), T>
- : public fn_traits<void, decltype(&T::operator())> {};
-
- // 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, int ArgN = 0>
-using fn_arg_t =
- std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>;
-
-// TODO: get rid of it as soon as Apple Clang gets proper deduction guides
-// for std::function<>
-// ...or consider using QtPrivate magic used by QObject::connect()
-// since wrap_in_function() is actually made for qt_connection_util.h
-// ...for inspiration, also check a possible std::not_fn implementation at
-// https://en.cppreference.com/w/cpp/utility/functional/not_fn
-template <typename FnT>
-inline auto wrap_in_function(FnT&& f)
+//! \brief Merge the value from an Omittable
+//! This is an adaptation of Omittable::merge() to the case when the value
+//! on the left hand side is not an Omittable.
+//! \return true if \p rhs is not omitted and the \p lhs value was different,
+//! in other words, if \p lhs has changed;
+//! false otherwise
+template <typename T1, typename T2>
+inline auto merge(T1& lhs, const Omittable<T2>& rhs)
+ -> std::enable_if_t<!_impl::IsOmittable<T1>
+ && std::is_convertible_v<T2, T1>, bool>
{
- return typename function_traits<FnT>::function_type(std::forward<FnT>(f));
+ if (!rhs || lhs == *rhs)
+ return false;
+ lhs = *rhs;
+ return true;
}
-inline auto operator"" _ls(const char* s, std::size_t size)
+inline constexpr auto operator"" _ls(const char* s, std::size_t size)
{
return QLatin1String(s, int(size));
}
@@ -283,4 +276,9 @@ qreal stringToHueF(const QString& s);
/** Extract the serverpart from MXID */
QString serverPart(const QString& mxId);
+
+QString versionString();
+int majorVersion();
+int minorVersion();
+int patchVersion();
} // namespace Quotient