aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/accountregistry.cpp137
-rw-r--r--lib/accountregistry.h92
-rw-r--r--lib/avatar.cpp53
-rw-r--r--lib/avatar.h33
-rw-r--r--lib/connection.cpp1167
-rw-r--r--lib/connection.h275
-rw-r--r--lib/connectiondata.cpp34
-rw-r--r--lib/connectiondata.h29
-rw-r--r--lib/converters.cpp42
-rw-r--r--lib/converters.h408
-rw-r--r--lib/csapi/account-data.cpp32
-rw-r--r--lib/csapi/account-data.h69
-rw-r--r--lib/csapi/admin.cpp8
-rw-r--r--lib/csapi/admin.h2
-rw-r--r--lib/csapi/administrative_contact.cpp79
-rw-r--r--lib/csapi/administrative_contact.h42
-rw-r--r--lib/csapi/appservice_room_directory.cpp20
-rw-r--r--lib/csapi/appservice_room_directory.h8
-rw-r--r--lib/csapi/banning.cpp25
-rw-r--r--lib/csapi/banning.h11
-rw-r--r--lib/csapi/capabilities.cpp9
-rw-r--r--lib/csapi/capabilities.h2
-rw-r--r--lib/csapi/content-repo.cpp64
-rw-r--r--lib/csapi/content-repo.h26
-rw-r--r--lib/csapi/create_room.cpp34
-rw-r--r--lib/csapi/create_room.h48
-rw-r--r--lib/csapi/cross_signing.cpp33
-rw-r--r--lib/csapi/cross_signing.h78
-rw-r--r--lib/csapi/definitions/auth_data.h7
-rw-r--r--lib/csapi/definitions/cross_signing_key.h47
-rw-r--r--lib/csapi/definitions/openid_token.h8
-rw-r--r--lib/csapi/definitions/public_rooms_response.h61
-rw-r--r--lib/csapi/definitions/push_condition.h4
-rw-r--r--lib/csapi/definitions/request_token_response.h2
-rw-r--r--lib/csapi/definitions/room_event_filter.h9
-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.cpp39
-rw-r--r--lib/csapi/device_management.h13
-rw-r--r--lib/csapi/directory.cpp32
-rw-r--r--lib/csapi/directory.h8
-rw-r--r--lib/csapi/event_context.cpp13
-rw-r--r--lib/csapi/event_context.h5
-rw-r--r--lib/csapi/filter.cpp15
-rw-r--r--lib/csapi/filter.h4
-rw-r--r--lib/csapi/inviting.cpp15
-rw-r--r--lib/csapi/inviting.h11
-rw-r--r--lib/csapi/joining.cpp28
-rw-r--r--lib/csapi/joining.h28
-rw-r--r--lib/csapi/keys.cpp48
-rw-r--r--lib/csapi/keys.h74
-rw-r--r--lib/csapi/kicking.cpp12
-rw-r--r--lib/csapi/kicking.h2
-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.h15
-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.cpp44
-rw-r--r--lib/csapi/list_public_rooms.h18
-rw-r--r--lib/csapi/login.cpp34
-rw-r--r--lib/csapi/login.h34
-rw-r--r--lib/csapi/logout.cpp14
-rw-r--r--lib/csapi/logout.h4
-rw-r--r--lib/csapi/message_pagination.cpp22
-rw-r--r--lib/csapi/message_pagination.h59
-rw-r--r--lib/csapi/notifications.cpp10
-rw-r--r--lib/csapi/notifications.h9
-rw-r--r--lib/csapi/openid.cpp8
-rw-r--r--lib/csapi/openid.h7
-rw-r--r--lib/csapi/peeking_events.cpp9
-rw-r--r--lib/csapi/peeking_events.h8
-rw-r--r--lib/csapi/presence.cpp20
-rw-r--r--lib/csapi/presence.h4
-rw-r--r--lib/csapi/profile.cpp44
-rw-r--r--lib/csapi/profile.h16
-rw-r--r--lib/csapi/pusher.cpp32
-rw-r--r--lib/csapi/pusher.h8
-rw-r--r--lib/csapi/pushrules.cpp83
-rw-r--r--lib/csapi/pushrules.h16
-rw-r--r--lib/csapi/read_markers.cpp18
-rw-r--r--lib/csapi/read_markers.h13
-rw-r--r--lib/csapi/receipts.cpp8
-rw-r--r--lib/csapi/receipts.h10
-rw-r--r--lib/csapi/redaction.cpp12
-rw-r--r--lib/csapi/redaction.h8
-rw-r--r--lib/csapi/refresh.cpp18
-rw-r--r--lib/csapi/refresh.h65
-rw-r--r--lib/csapi/registration.cpp87
-rw-r--r--lib/csapi/registration.h71
-rw-r--r--lib/csapi/registration_tokens.cpp33
-rw-r--r--lib/csapi/registration_tokens.h44
-rw-r--r--lib/csapi/relations.cpp118
-rw-r--r--lib/csapi/relations.h298
-rw-r--r--lib/csapi/report_content.cpp16
-rw-r--r--lib/csapi/report_content.h5
-rw-r--r--lib/csapi/room_send.cpp8
-rw-r--r--lib/csapi/room_send.h12
-rw-r--r--lib/csapi/room_state.cpp8
-rw-r--r--lib/csapi/room_state.h2
-rw-r--r--lib/csapi/room_upgrades.cpp11
-rw-r--r--lib/csapi/room_upgrades.h2
-rw-r--r--lib/csapi/rooms.cpp43
-rw-r--r--lib/csapi/rooms.h28
-rw-r--r--lib/csapi/search.cpp26
-rw-r--r--lib/csapi/search.h307
-rw-r--r--lib/csapi/space_hierarchy.cpp43
-rw-r--r--lib/csapi/space_hierarchy.h152
-rw-r--r--lib/csapi/sso_login_redirect.cpp33
-rw-r--r--lib/csapi/sso_login_redirect.h34
-rw-r--r--lib/csapi/tags.cpp32
-rw-r--r--lib/csapi/tags.h6
-rw-r--r--lib/csapi/third_party_lookup.cpp49
-rw-r--r--lib/csapi/third_party_lookup.h12
-rw-r--r--lib/csapi/third_party_membership.cpp17
-rw-r--r--lib/csapi/third_party_membership.h4
-rw-r--r--lib/csapi/threads_list.cpp37
-rw-r--r--lib/csapi/threads_list.h76
-rw-r--r--lib/csapi/to_device.cpp12
-rw-r--r--lib/csapi/to_device.h11
-rw-r--r--lib/csapi/typing.cpp14
-rw-r--r--lib/csapi/typing.h2
-rw-r--r--lib/csapi/users.cpp12
-rw-r--r--lib/csapi/users.h4
-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/voip.h2
-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.h16
-rw-r--r--lib/database.cpp419
-rw-r--r--lib/database.h81
-rw-r--r--lib/e2ee.h31
-rw-r--r--lib/e2ee/e2ee.h139
-rw-r--r--lib/e2ee/qolmaccount.cpp275
-rw-r--r--lib/e2ee/qolmaccount.h123
-rw-r--r--lib/e2ee/qolminboundsession.cpp192
-rw-r--r--lib/e2ee/qolminboundsession.h60
-rw-r--r--lib/e2ee/qolmmessage.cpp31
-rw-r--r--lib/e2ee/qolmmessage.h42
-rw-r--r--lib/e2ee/qolmoutboundsession.cpp152
-rw-r--r--lib/e2ee/qolmoutboundsession.h63
-rw-r--r--lib/e2ee/qolmsession.cpp231
-rw-r--r--lib/e2ee/qolmsession.h84
-rw-r--r--lib/e2ee/qolmutility.cpp53
-rw-r--r--lib/e2ee/qolmutility.h41
-rw-r--r--lib/e2ee/qolmutils.cpp22
-rw-r--r--lib/e2ee/qolmutils.h55
-rw-r--r--lib/encryptionmanager.cpp369
-rw-r--r--lib/encryptionmanager.h47
-rw-r--r--lib/eventitem.cpp34
-rw-r--r--lib/eventitem.h86
-rw-r--r--lib/events/accountdataevents.h91
-rw-r--r--lib/events/callanswerevent.cpp71
-rw-r--r--lib/events/callanswerevent.h45
-rw-r--r--lib/events/callcandidatesevent.cpp41
-rw-r--r--lib/events/callcandidatesevent.h45
-rw-r--r--lib/events/callevents.cpp82
-rw-r--r--lib/events/callevents.h99
-rw-r--r--lib/events/callhangupevent.cpp52
-rw-r--r--lib/events/callhangupevent.h33
-rw-r--r--lib/events/callinviteevent.cpp63
-rw-r--r--lib/events/callinviteevent.h44
-rw-r--r--lib/events/directchatevent.cpp21
-rw-r--r--lib/events/directchatevent.h26
-rw-r--r--lib/events/encryptedevent.cpp73
-rw-r--r--lib/events/encryptedevent.h50
-rw-r--r--lib/events/encryptionevent.cpp74
-rw-r--r--lib/events/encryptionevent.h74
-rw-r--r--lib/events/event.cpp81
-rw-r--r--lib/events/event.h740
-rw-r--r--lib/events/eventcontent.cpp133
-rw-r--r--lib/events/eventcontent.h506
-rw-r--r--lib/events/eventloader.h85
-rw-r--r--lib/events/eventrelation.cpp38
-rw-r--r--lib/events/eventrelation.h52
-rw-r--r--lib/events/filesourceinfo.cpp163
-rw-r--r--lib/events/filesourceinfo.h90
-rw-r--r--lib/events/keyverificationevent.h258
-rw-r--r--lib/events/reactionevent.cpp44
-rw-r--r--lib/events/reactionevent.h69
-rw-r--r--lib/events/receiptevent.cpp61
-rw-r--r--lib/events/receiptevent.h44
-rw-r--r--lib/events/redactionevent.cpp1
-rw-r--r--lib/events/redactionevent.h29
-rw-r--r--lib/events/roomavatarevent.h44
-rw-r--r--lib/events/roomcanonicalaliasevent.h88
-rw-r--r--lib/events/roomcreateevent.cpp41
-rw-r--r--lib/events/roomcreateevent.h31
-rw-r--r--lib/events/roomevent.cpp82
-rw-r--r--lib/events/roomevent.h106
-rw-r--r--lib/events/roomkeyevent.cpp9
-rw-r--r--lib/events/roomkeyevent.h30
-rw-r--r--lib/events/roommemberevent.cpp107
-rw-r--r--lib/events/roommemberevent.h115
-rw-r--r--lib/events/roommessageevent.cpp178
-rw-r--r--lib/events/roommessageevent.h112
-rw-r--r--lib/events/roompowerlevelsevent.cpp69
-rw-r--r--lib/events/roompowerlevelsevent.h35
-rw-r--r--lib/events/roomtombstoneevent.cpp23
-rw-r--r--lib/events/roomtombstoneevent.h29
-rw-r--r--lib/events/simplestateevents.h108
-rw-r--r--lib/events/single_key_value.h36
-rw-r--r--lib/events/stateevent.cpp62
-rw-r--r--lib/events/stateevent.h198
-rw-r--r--lib/events/stickerevent.h48
-rw-r--r--lib/events/typingevent.cpp31
-rw-r--r--lib/events/typingevent.h32
-rw-r--r--lib/eventstats.cpp98
-rw-r--r--lib/eventstats.h114
-rw-r--r--lib/expected.h78
-rw-r--r--lib/function_traits.cpp56
-rw-r--r--lib/function_traits.h93
-rw-r--r--lib/jobs/basejob.cpp226
-rw-r--r--lib/jobs/basejob.h144
-rw-r--r--lib/jobs/downloadfilejob.cpp93
-rw-r--r--lib/jobs/downloadfilejob.h12
-rw-r--r--lib/jobs/mediathumbnailjob.cpp27
-rw-r--r--lib/jobs/mediathumbnailjob.h21
-rw-r--r--lib/jobs/postreadmarkersjob.h38
-rw-r--r--lib/jobs/requestdata.cpp10
-rw-r--r--lib/jobs/requestdata.h44
-rw-r--r--lib/jobs/syncjob.cpp27
-rw-r--r--lib/jobs/syncjob.h21
-rw-r--r--lib/joinstate.h47
-rw-r--r--lib/keyverificationsession.cpp501
-rw-r--r--lib/keyverificationsession.h153
-rw-r--r--lib/logging.cpp24
-rw-r--r--lib/logging.h66
-rw-r--r--lib/mxcreply.cpp99
-rw-r--r--lib/mxcreply.h31
-rw-r--r--lib/networkaccessmanager.cpp120
-rw-r--r--lib/networkaccessmanager.h33
-rw-r--r--lib/networksettings.cpp25
-rw-r--r--lib/networksettings.h27
-rw-r--r--lib/omittable.h217
-rw-r--r--lib/qt_connection_util.h192
-rw-r--r--lib/quotient_common.h117
-rw-r--r--lib/quotient_export.h25
-rw-r--r--lib/room.cpp2164
-rw-r--r--lib/room.h632
-rw-r--r--lib/roomstateview.cpp35
-rw-r--r--lib/roomstateview.h211
-rw-r--r--lib/settings.cpp40
-rw-r--r--lib/settings.h56
-rw-r--r--lib/ssosession.cpp49
-rw-r--r--lib/ssosession.h17
-rw-r--r--lib/syncdata.cpp144
-rw-r--r--lib/syncdata.h76
-rw-r--r--lib/uri.cpp52
-rw-r--r--lib/uri.h5
-rw-r--r--lib/uriresolver.cpp11
-rw-r--r--lib/uriresolver.h30
-rw-r--r--lib/user.cpp256
-rw-r--r--lib/user.h101
-rw-r--r--lib/util.cpp84
-rw-r--r--lib/util.h349
260 files changed, 13590 insertions, 6990 deletions
diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp
new file mode 100644
index 00000000..ad7c5f99
--- /dev/null
+++ b/lib/accountregistry.cpp
@@ -0,0 +1,137 @@
+// 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"
+#include <QtCore/QCoreApplication>
+
+using namespace Quotient;
+
+void AccountRegistry::add(Connection* a)
+{
+ if (contains(a))
+ return;
+ beginInsertRows(QModelIndex(), size(), size());
+ push_back(a);
+ endInsertRows();
+ emit accountCountChanged();
+}
+
+void AccountRegistry::drop(Connection* a)
+{
+ if (const auto idx = indexOf(a); idx != -1) {
+ beginRemoveRows(QModelIndex(), idx, idx);
+ remove(idx);
+ endRemoveRows();
+ }
+ Q_ASSERT(!contains(a));
+}
+
+bool AccountRegistry::isLoggedIn(const QString &userId) const
+{
+ return std::any_of(cbegin(), cend(), [&userId](const Connection* a) {
+ return a->userId() == userId;
+ });
+}
+
+QVariant AccountRegistry::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid() || index.row() >= count())
+ return {};
+
+ if (role == AccountRole)
+ return QVariant::fromValue(at(index.row()));
+
+ return {};
+}
+
+int AccountRegistry::rowCount(const QModelIndex& parent) const
+{
+ return parent.isValid() ? 0 : count();
+}
+
+QHash<int, QByteArray> AccountRegistry::roleNames() const
+{
+ return { { AccountRole, "connection" } };
+}
+
+Connection* AccountRegistry::get(const QString& userId)
+{
+ for (const auto &connection : *this) {
+ if (connection->userId() == userId)
+ return connection;
+ }
+ return nullptr;
+}
+
+QKeychain::ReadPasswordJob* AccountRegistry::loadAccessTokenFromKeychain(const QString& userId)
+{
+ qCDebug(MAIN) << "Reading access token from keychain for" << userId;
+ auto job = new QKeychain::ReadPasswordJob(qAppName(), this);
+ job->setKey(userId);
+ job->start();
+
+ return job;
+}
+
+void AccountRegistry::invokeLogin()
+{
+ const auto accounts = SettingsGroup("Accounts").childGroups();
+ for (const auto& accountId : accounts) {
+ AccountSettings account { accountId };
+ m_accountsLoading += accountId;
+ emit accountsLoadingChanged();
+
+ if (account.homeserver().isEmpty())
+ continue;
+
+ auto accessTokenLoadingJob =
+ loadAccessTokenFromKeychain(account.userId());
+ connect(accessTokenLoadingJob, &QKeychain::Job::finished, this,
+ [accountId, this, accessTokenLoadingJob]() {
+ if (accessTokenLoadingJob->error()
+ != QKeychain::Error::NoError) {
+ emit keychainError(accessTokenLoadingJob->error());
+ return;
+ }
+
+ AccountSettings account { accountId };
+ auto connection = new Connection(account.homeserver());
+ connect(connection, &Connection::connected, this,
+ [connection, this, accountId] {
+ connection->loadState();
+ connection->setLazyLoading(true);
+
+ connection->syncLoop();
+
+ m_accountsLoading.removeAll(accountId);
+ emit accountsLoadingChanged();
+ });
+ connect(connection, &Connection::loginError, this,
+ [this, connection, accountId](const QString& error,
+ const QString& details) {
+ emit loginError(connection, error, details);
+
+ m_accountsLoading.removeAll(accountId);
+ emit accountsLoadingChanged();
+ });
+ connect(connection, &Connection::resolveError, this,
+ [this, connection, accountId](const QString& error) {
+ emit resolveError(connection, error);
+
+ m_accountsLoading.removeAll(accountId);
+ emit accountsLoadingChanged();
+ });
+ connection->assumeIdentity(
+ account.userId(), accessTokenLoadingJob->binaryData(),
+ account.deviceId());
+ });
+ }
+}
+
+QStringList AccountRegistry::accountsLoading() const
+{
+ return m_accountsLoading;
+}
diff --git a/lib/accountregistry.h b/lib/accountregistry.h
new file mode 100644
index 00000000..9560688e
--- /dev/null
+++ b/lib/accountregistry.h
@@ -0,0 +1,92 @@
+// 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 "quotient_export.h"
+#include "settings.h"
+
+#include <QtCore/QAbstractListModel>
+
+#if QT_VERSION_MAJOR >= 6
+# include <qt6keychain/keychain.h>
+#else
+# include <qt5keychain/keychain.h>
+#endif
+
+namespace QKeychain {
+class ReadPasswordJob;
+}
+
+namespace Quotient {
+class Connection;
+
+class QUOTIENT_API AccountRegistry : public QAbstractListModel,
+ private QVector<Connection*> {
+ Q_OBJECT
+ /// Number of accounts that are currently fully loaded
+ Q_PROPERTY(int accountCount READ rowCount NOTIFY accountCountChanged)
+ /// List of accounts that are currently in some stage of being loaded (Reading token from keychain, trying to contact server, etc).
+ /// Can be used to inform the user or to show a login screen if size() == 0 and no accounts are loaded
+ Q_PROPERTY(QStringList accountsLoading READ accountsLoading NOTIFY accountsLoadingChanged)
+public:
+ using vector_t = QVector<Connection*>;
+ using const_iterator = vector_t::const_iterator;
+ using const_reference = vector_t::const_reference;
+
+ enum EventRoles {
+ AccountRole = Qt::UserRole + 1,
+ ConnectionRole = AccountRole
+ };
+
+ [[deprecated("Use Accounts variable instead")]] //
+ static AccountRegistry& instance();
+
+ // Expose most of vector_t's const-API but only provide add() and drop()
+ // for changing it. In theory other changing operations could be supported
+ // too; but then boilerplate begin/end*() calls has to be tucked into each
+ // and this class gives no guarantees on the order of entries, so why care.
+
+ const vector_t& accounts() const { return *this; }
+ void add(Connection* a);
+ void drop(Connection* a);
+ const_iterator begin() const { return vector_t::begin(); }
+ const_iterator end() const { return vector_t::end(); }
+ const_reference front() const { return vector_t::front(); }
+ const_reference back() const { return vector_t::back(); }
+ bool isLoggedIn(const QString& userId) const;
+ Connection* get(const QString& userId);
+
+ using vector_t::isEmpty, vector_t::empty;
+ using vector_t::size, vector_t::count, vector_t::capacity;
+ using vector_t::cbegin, vector_t::cend, vector_t::contains;
+
+ // QAbstractItemModel interface implementation
+
+ [[nodiscard]] QVariant data(const QModelIndex& index,
+ int role) const override;
+ [[nodiscard]] int rowCount(
+ const QModelIndex& parent = QModelIndex()) const override;
+ [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
+
+ QStringList accountsLoading() const;
+
+ void invokeLogin();
+Q_SIGNALS:
+ void accountCountChanged();
+ void accountsLoadingChanged();
+
+ void keychainError(QKeychain::Error error);
+ void loginError(Connection* connection, QString message, QString details);
+ void resolveError(Connection* connection, QString error);
+
+private:
+ QKeychain::ReadPasswordJob* loadAccessTokenFromKeychain(const QString &userId);
+ QStringList m_accountsLoading;
+};
+
+inline QUOTIENT_API AccountRegistry Accounts {};
+
+inline AccountRegistry& AccountRegistry::instance() { return Accounts; }
+} // namespace Quotient
diff --git a/lib/avatar.cpp b/lib/avatar.cpp
index c65aa25c..13de99bf 100644
--- a/lib/avatar.cpp
+++ b/lib/avatar.cpp
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "avatar.h"
@@ -37,9 +22,9 @@ public:
explicit Private(QUrl url = {}) : _url(move(url)) {}
~Private()
{
- if (isJobRunning(_thumbnailRequest))
+ if (isJobPending(_thumbnailRequest))
_thumbnailRequest->abandon();
- if (isJobRunning(_uploadRequest))
+ if (isJobPending(_uploadRequest))
_uploadRequest->abandon();
}
@@ -54,7 +39,7 @@ public:
// The below are related to image caching, hence mutable
mutable QImage _originalImage;
- mutable std::vector<QPair<QSize, QImage>> _scaledImages;
+ mutable std::vector<std::pair<QSize, QImage>> _scaledImages;
mutable QSize _requestedSize;
mutable enum { Unknown, Cache, Network, Banned } _imageSource = Unknown;
mutable QPointer<MediaThumbnailJob> _thumbnailRequest = nullptr;
@@ -62,15 +47,11 @@ public:
mutable std::vector<get_callback_t> callbacks;
};
-Avatar::Avatar() : d(std::make_unique<Private>()) {}
+Avatar::Avatar()
+ : d(makeImpl<Private>())
+{}
-Avatar::Avatar(QUrl url) : d(std::make_unique<Private>(std::move(url))) {}
-
-Avatar::Avatar(Avatar&&) = default;
-
-Avatar::~Avatar() = default;
-
-Avatar& Avatar::operator=(Avatar&&) = default;
+Avatar::Avatar(QUrl url) : d(makeImpl<Private>(std::move(url))) {}
QImage Avatar::get(Connection* connection, int dimension,
get_callback_t callback) const
@@ -87,7 +68,7 @@ QImage Avatar::get(Connection* connection, int width, int height,
bool Avatar::upload(Connection* connection, const QString& fileName,
upload_callback_t callback) const
{
- if (isJobRunning(d->_uploadRequest))
+ if (isJobPending(d->_uploadRequest))
return false;
return d->upload(connection->uploadFile(fileName), move(callback));
}
@@ -95,7 +76,7 @@ bool Avatar::upload(Connection* connection, const QString& fileName,
bool Avatar::upload(Connection* connection, QIODevice* source,
upload_callback_t callback) const
{
- if (isJobRunning(d->_uploadRequest) || !source->isReadable())
+ if (isJobPending(d->_uploadRequest) || !source->isReadable())
return false;
return d->upload(connection->uploadContent(source), move(callback));
}
@@ -125,7 +106,7 @@ QImage Avatar::Private::get(Connection* connection, QSize size,
&& checkUrl(_url)) {
qCDebug(MAIN) << "Getting avatar from" << _url.toString();
_requestedSize = size;
- if (isJobRunning(_thumbnailRequest))
+ if (isJobPending(_thumbnailRequest))
_thumbnailRequest->abandon();
if (callback)
callbacks.emplace_back(move(callback));
@@ -143,9 +124,9 @@ QImage Avatar::Private::get(Connection* connection, QSize size,
});
}
- for (const auto& p : _scaledImages)
- if (p.first == size)
- return p.second;
+ for (const auto& [scaledSize, scaledImage] : _scaledImages)
+ if (scaledSize == size)
+ return scaledImage;
auto result = _originalImage.isNull()
? QImage()
: _originalImage.scaled(size, Qt::KeepAspectRatio,
@@ -157,7 +138,7 @@ QImage Avatar::Private::get(Connection* connection, QSize size,
bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t &&callback)
{
_uploadRequest = job;
- if (!isJobRunning(_uploadRequest))
+ if (!isJobPending(_uploadRequest))
return false;
_uploadRequest->connect(_uploadRequest, &BaseJob::success, _uploadRequest,
[job, callback] { callback(job->contentUri()); });
@@ -194,7 +175,7 @@ bool Avatar::updateUrl(const QUrl& newUrl)
d->_url = newUrl;
d->_imageSource = Private::Unknown;
- if (isJobRunning(d->_thumbnailRequest))
+ if (isJobPending(d->_thumbnailRequest))
d->_thumbnailRequest->abandon();
return true;
}
diff --git a/lib/avatar.h b/lib/avatar.h
index 7a566bfa..c94dc369 100644
--- a/lib/avatar.h
+++ b/lib/avatar.h
@@ -1,42 +1,25 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
+#include "util.h"
+
#include <QtCore/QUrl>
#include <QtGui/QIcon>
#include <functional>
-#include <memory>
namespace Quotient {
class Connection;
-class Avatar {
+class QUOTIENT_API Avatar {
public:
explicit Avatar();
explicit Avatar(QUrl url);
- Avatar(Avatar&&);
- ~Avatar();
- 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;
@@ -54,8 +37,6 @@ public:
private:
class Private;
- std::unique_ptr<Private> d;
+ ImplPtr<Private> d;
};
} // namespace Quotient
-/// \deprecated Use namespace Quotient instead
-namespace QMatrixClient = Quotient; \ No newline at end of file
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 853053bd..4547474a 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -1,55 +1,55 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name>
+// SPDX-FileCopyrightText: 2019 Ville Ranki <ville.ranki@iki.fi>
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "connection.h"
+#include "accountregistry.h"
#include "connectiondata.h"
-#ifdef Quotient_E2EE_ENABLED
-# include "encryptionmanager.h"
-#endif // Quotient_E2EE_ENABLED
+#include "qt_connection_util.h"
#include "room.h"
#include "settings.h"
#include "user.h"
+// NB: since Qt 6, moc_connection.cpp needs Room and User fully defined
+#include "moc_connection.cpp"
+
#include "csapi/account-data.h"
#include "csapi/capabilities.h"
#include "csapi/joining.h"
#include "csapi/leaving.h"
#include "csapi/logout.h"
-#include "csapi/receipts.h"
#include "csapi/room_send.h"
#include "csapi/to_device.h"
-#include "csapi/versions.h"
#include "csapi/voip.h"
#include "csapi/wellknown.h"
+#include "csapi/whoami.h"
#include "events/directchatevent.h"
-#include "events/eventloader.h"
#include "jobs/downloadfilejob.h"
#include "jobs/mediathumbnailjob.h"
#include "jobs/syncjob.h"
+#include <variant>
#ifdef Quotient_E2EE_ENABLED
-# include "account.h" // QtOlm
+# include "database.h"
+# include "keyverificationsession.h"
+
+# include "e2ee/qolmaccount.h"
+# include "e2ee/qolminboundsession.h"
+# include "e2ee/qolmsession.h"
+# include "e2ee/qolmutility.h"
+# include "e2ee/qolmutils.h"
+
+# include "events/keyverificationevent.h"
#endif // Quotient_E2EE_ENABLED
-#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
-# include <QtCore/QCborValue>
+#if QT_VERSION_MAJOR >= 6
+# include <qt6keychain/keychain.h>
+#else
+# include <qt5keychain/keychain.h>
#endif
#include <QtCore/QCoreApplication>
@@ -66,7 +66,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();) {
@@ -84,8 +84,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;
@@ -93,12 +91,11 @@ public:
// state is Invited. The spec mandates to keep Invited room state
// separately; specifically, we should keep objects for Invite and
// Leave state of the same room if the two happen to co-exist.
- QHash<QPair<QString, bool>, Room*> roomMap;
+ QHash<std::pair<QString, bool>, Room*> roomMap;
/// Mapping from serverparts to alias/room id mappings,
/// as of the last sync
QHash<QString, QString> roomAliasMap;
QVector<QString> roomIdsToForget;
- QVector<Room*> firstTimeRooms;
QVector<QString> pendingStateRoomIds;
QMap<QString, User*> userMap;
DirectChatsMap directChats;
@@ -111,13 +108,34 @@ public:
QMetaObject::Connection syncLoopConnection {};
int syncTimeout = -1;
+#ifdef Quotient_E2EE_ENABLED
+ QSet<QString> trackedUsers;
+ QSet<QString> outdatedUsers;
+ QHash<QString, QHash<QString, DeviceKeys>> deviceKeys;
+ QueryKeysJob *currentQueryKeysJob = nullptr;
+ bool encryptionUpdateRequired = false;
+ PicklingMode picklingMode = Unencrypted {};
+ Database *database = nullptr;
+ QHash<QString, int> oneTimeKeysCount;
+ std::vector<std::unique_ptr<EncryptedEvent>> pendingEncryptedEvents;
+ void handleEncryptedToDeviceEvent(const EncryptedEvent& event);
+ bool processIfVerificationEvent(const Event &evt, bool encrypted);
+
+ // A map from SenderKey to vector of InboundSession
+ UnorderedMap<QString, std::vector<QOlmSessionPtr>> olmSessions;
+
+ QHash<QString, KeyVerificationSession*> verificationSessions;
+#endif
+
GetCapabilitiesJob* capabilitiesJob = nullptr;
GetCapabilitiesJob::Capabilities capabilities;
QVector<GetLoginFlowsJob::LoginFlow> loginFlows;
#ifdef Quotient_E2EE_ENABLED
- QScopedPointer<EncryptionManager> encryptionManager;
+ std::unique_ptr<QOlmAccount> olmAccount;
+ bool isUploadingKeys = false;
+ bool firstSync = true;
#endif // Quotient_E2EE_ENABLED
QPointer<GetWellknownJob> resolverJob = nullptr;
@@ -133,11 +151,6 @@ public:
!= "json";
bool lazyLoading = false;
- /// \brief Stop resolving and login flows jobs, and clear login flows
- ///
- /// Prepares the class to set or resolve a new homeserver
- void clearResolvingContext();
-
/** \brief Check the homeserver and resolve it if needed, before connecting
*
* A single entry for functions that need to check whether the homeserver
@@ -155,25 +168,17 @@ public:
*/
void checkAndConnect(const QString &userId,
const std::function<void ()> &connectFn,
- const std::optional<LoginFlows::LoginFlow> &flow = none);
+ const std::optional<LoginFlow> &flow = none);
template <typename... LoginArgTs>
void loginToServer(LoginArgTs&&... loginArgs);
- void completeSetup(const QString& mxId);
+ void completeSetup(const QString &mxId);
void removeRoom(const QString& roomId);
void consumeRoomData(SyncDataList&& roomDataList, bool fromCache);
void consumeAccountData(Events&& accountDataEvents);
void consumePresenceData(Events&& presenceData);
void consumeToDeviceEvents(Events&& toDeviceEvents);
-
- template <typename EventT>
- EventT* unpackAccountData() const
- {
- const auto& eventIt = accountData.find(EventT::matrixTypeId());
- return eventIt == accountData.end()
- ? nullptr
- : weakPtrCast<EventT>(eventIt->second);
- }
+ void consumeDevicesList(DevicesList&& devicesList);
void packAndSendAccountData(EventPtr&& event)
{
@@ -184,7 +189,7 @@ public:
emit q->accountDataChanged(eventType);
}
- template <typename EventT, typename ContentT>
+ template <EventClass EventT, typename ContentT>
void packAndSendAccountData(ContentT&& content)
{
packAndSendAccountData(
@@ -195,36 +200,115 @@ public:
return q->stateCacheDir().filePath("state.json");
}
- EventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent)
+#ifdef Quotient_E2EE_ENABLED
+ void loadSessions() {
+ olmSessions = q->database()->loadOlmSessions(picklingMode);
+ }
+ void saveSession(const QOlmSession& session, const QString& senderKey) const
+ {
+ q->database()->saveOlmSession(senderKey, session.sessionId(),
+ session.pickle(picklingMode),
+ QDateTime::currentDateTime());
+ }
+
+ template <typename FnT>
+ std::pair<QString, QString> doDecryptMessage(const QOlmSession& session,
+ const QOlmMessage& message,
+ FnT&& andThen) const
+ {
+ const auto expectedMessage = session.decrypt(message);
+ if (expectedMessage) {
+ const auto result =
+ std::make_pair(*expectedMessage, session.sessionId());
+ andThen();
+ return result;
+ }
+ const auto errorLine = message.type() == QOlmMessage::PreKey
+ ? "Failed to decrypt prekey message:"
+ : "Failed to decrypt message:";
+ qCDebug(E2EE) << errorLine << expectedMessage.error();
+ return {};
+ }
+
+ std::pair<QString, QString> sessionDecryptMessage(
+ const QJsonObject& personalCipherObject, const QByteArray& senderKey)
+ {
+ const auto msgType = static_cast<QOlmMessage::Type>(
+ personalCipherObject.value(TypeKeyL).toInt(-1));
+ if (msgType != QOlmMessage::General && msgType != QOlmMessage::PreKey) {
+ qCWarning(E2EE) << "Olm message has incorrect type" << msgType;
+ return {};
+ }
+ QOlmMessage message {
+ personalCipherObject.value(BodyKeyL).toString().toLatin1(), msgType
+ };
+ for (const auto& session : olmSessions[senderKey])
+ if (msgType == QOlmMessage::General
+ || session->matchesInboundSessionFrom(senderKey, message)) {
+ return doDecryptMessage(*session, message, [this, &session] {
+ q->database()->setOlmSessionLastReceived(
+ session->sessionId(), QDateTime::currentDateTime());
+ });
+ }
+
+ if (msgType == QOlmMessage::General) {
+ qCWarning(E2EE) << "Failed to decrypt message";
+ return {};
+ }
+
+ qCDebug(E2EE) << "Creating new inbound session"; // Pre-key messages only
+ auto newSessionResult =
+ olmAccount->createInboundSessionFrom(senderKey, message);
+ if (!newSessionResult) {
+ qCWarning(E2EE)
+ << "Failed to create inbound session for" << senderKey
+ << "with error" << newSessionResult.error();
+ return {};
+ }
+ auto newSession = std::move(*newSessionResult);
+ if (olmAccount->removeOneTimeKeys(*newSession) != OLM_SUCCESS) {
+ qWarning(E2EE) << "Failed to remove one time key for session"
+ << newSession->sessionId();
+ // Keep going though
+ }
+ return doDecryptMessage(
+ *newSession, message, [this, &senderKey, &newSession] {
+ saveSession(*newSession, senderKey);
+ olmSessions[senderKey].push_back(std::move(newSession));
+ });
+ }
+#endif
+
+ std::pair<EventPtr, QString> sessionDecryptMessage(const EncryptedEvent& encryptedEvent)
{
#ifndef Quotient_E2EE_ENABLED
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
return {};
-#else // Quotient_E2EE_ENABLED
+#else
if (encryptedEvent.algorithm() != OlmV1Curve25519AesSha2AlgoKey)
return {};
- const auto identityKey =
- encryptionManager->account()->curve25519IdentityKey();
+ const auto identityKey = olmAccount->identityKeys().curve25519;
const auto personalCipherObject =
encryptedEvent.ciphertext(identityKey);
if (personalCipherObject.isEmpty()) {
qCDebug(E2EE) << "Encrypted event is not for the current device";
return {};
}
- const auto decrypted = encryptionManager->sessionDecryptMessage(
- personalCipherObject, encryptedEvent.senderKey().toLatin1());
+ const auto [decrypted, olmSessionId] =
+ sessionDecryptMessage(personalCipherObject,
+ encryptedEvent.senderKey().toLatin1());
if (decrypted.isEmpty()) {
qCDebug(E2EE) << "Problem with new session from senderKey:"
<< encryptedEvent.senderKey()
- << encryptionManager->account()->oneTimeKeys();
+ << olmAccount->oneTimeKeys().keys;
return {};
}
auto&& decryptedEvent =
fromJson<EventPtr>(QJsonDocument::fromJson(decrypted.toUtf8()));
- if (auto sender = decryptedEvent->fullJson()["sender"_ls].toString();
+ if (auto sender = decryptedEvent->fullJson()[SenderKeyL].toString();
sender != encryptedEvent.senderId()) {
qCWarning(E2EE) << "Found user" << sender
<< "instead of sender" << encryptedEvent.senderId()
@@ -232,36 +316,97 @@ public:
return {};
}
+ auto query = database->prepareQuery(QStringLiteral("SELECT edKey FROM tracked_devices WHERE curveKey=:curveKey;"));
+ query.bindValue(":curveKey", encryptedEvent.contentJson()["sender_key"].toString());
+ database->execute(query);
+ if (!query.next()) {
+ qCWarning(E2EE) << "Received olm message from unknown device" << encryptedEvent.contentJson()["sender_key"].toString();
+ return {};
+ }
+ auto edKey = decryptedEvent->fullJson()["keys"]["ed25519"].toString();
+ if (edKey.isEmpty() || query.value(QStringLiteral("edKey")).toString() != edKey) {
+ qCDebug(E2EE) << "Received olm message with invalid ed key";
+ return {};
+ }
+
// TODO: keys to constants
const auto decryptedEventObject = decryptedEvent->fullJson();
- const auto recipient =
- decryptedEventObject.value("recipient"_ls).toString();
+ const auto recipient = decryptedEventObject.value("recipient"_ls).toString();
if (recipient != data->userId()) {
qCDebug(E2EE) << "Found user" << recipient << "instead of us"
<< data->userId() << "in Olm plaintext";
return {};
}
- const auto ourKey =
- decryptedEventObject.value("recipient_keys"_ls).toObject()
- .value(Ed25519Key).toString();
- if (ourKey
- != QString::fromUtf8(
- encryptionManager->account()->ed25519IdentityKey())) {
+ const auto ourKey = decryptedEventObject.value("recipient_keys"_ls).toObject()
+ .value(Ed25519Key).toString();
+ if (ourKey != QString::fromUtf8(olmAccount->identityKeys().ed25519)) {
qCDebug(E2EE) << "Found key" << ourKey
<< "instead of ours own ed25519 key"
- << encryptionManager->account()->ed25519IdentityKey()
+ << olmAccount->identityKeys().ed25519
<< "in Olm plaintext";
return {};
}
- return std::move(decryptedEvent);
+ return { std::move(decryptedEvent), olmSessionId };
#endif // Quotient_E2EE_ENABLED
}
+#ifdef Quotient_E2EE_ENABLED
+ bool isKnownCurveKey(const QString& userId, const QString& curveKey) const;
+
+ void loadOutdatedUserDevices();
+ void saveDevicesList();
+ void loadDevicesList();
+
+ // This function assumes that an olm session with (user, device) exists
+ std::pair<QOlmMessage::Type, QByteArray> olmEncryptMessage(
+ const QString& userId, const QString& device,
+ const QByteArray& message) const;
+ bool createOlmSession(const QString& targetUserId,
+ const QString& targetDeviceId,
+ const OneTimeKeys &oneTimeKeyObject);
+ QString curveKeyForUserDevice(const QString& userId,
+ const QString& device) const;
+ QJsonObject assembleEncryptedContent(QJsonObject payloadJson,
+ const QString& targetUserId,
+ const QString& targetDeviceId) const;
+#endif
+
+ void saveAccessTokenToKeychain() const
+ {
+ qCDebug(MAIN) << "Saving access token to keychain for" << q->userId();
+ auto job = new QKeychain::WritePasswordJob(qAppName());
+ job->setAutoDelete(true);
+ job->setKey(q->userId());
+ job->setBinaryData(data->accessToken());
+ job->start();
+ //TODO error handling
+ }
+
+ void dropAccessToken()
+ {
+ qCDebug(MAIN) << "Removing access token from keychain for" << q->userId();
+ auto job = new QKeychain::DeletePasswordJob(qAppName());
+ job->setAutoDelete(true);
+ job->setKey(q->userId());
+ job->start();
+
+ auto pickleJob = new QKeychain::DeletePasswordJob(qAppName());
+ pickleJob->setAutoDelete(true);
+ pickleJob->setKey(q->userId() + "-Pickle"_ls);
+ pickleJob->start();
+ //TODO error handling
+
+ data->setToken({});
+ }
};
Connection::Connection(const QUrl& server, QObject* parent)
- : QObject(parent), d(new Private(std::make_unique<ConnectionData>(server)))
+ : QObject(parent)
+ , d(makeImpl<Private>(std::make_unique<ConnectionData>(server)))
{
+#ifdef Quotient_E2EE_ENABLED
+ //connect(qApp, &QCoreApplication::aboutToQuit, this, &Connection::saveOlmAccount);
+#endif
d->q = this; // All d initialization should occur before this line
}
@@ -271,11 +416,13 @@ Connection::~Connection()
{
qCDebug(MAIN) << "deconstructing connection object for" << userId();
stopSync();
+ Accounts.drop(this);
}
void Connection::resolveServer(const QString& mxid)
{
- d->clearResolvingContext();
+ if (isJobPending(d->resolverJob))
+ d->resolverJob->abandon();
auto maybeBaseUrl = QUrl::fromUserInput(serverPart(mxid));
maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http"
@@ -298,7 +445,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";
@@ -326,12 +473,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"));
- });
});
}
@@ -353,7 +494,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);
@@ -380,8 +521,18 @@ void Connection::assumeIdentity(const QString& mxId, const QString& accessToken,
{
d->checkAndConnect(mxId, [this, mxId, accessToken, deviceId] {
d->data->setToken(accessToken.toLatin1());
- d->data->setDeviceId(deviceId);
- d->completeSetup(mxId);
+ d->data->setDeviceId(deviceId); // Can't we deduce this from access_token?
+ auto* job = callApi<GetTokenOwnerJob>();
+ connect(job, &BaseJob::success, this, [this, job, mxId] {
+ if (mxId != job->userId())
+ qCWarning(MAIN).nospace()
+ << "The access_token owner (" << job->userId()
+ << ") is different from passed MXID (" << mxId << ")!";
+ d->completeSetup(job->userId());
+ });
+ connect(job, &BaseJob::failure, this, [this, job] {
+ emit loginError(job->errorString(), job->rawDataSample());
+ });
});
}
@@ -403,7 +554,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";
});
@@ -425,12 +576,10 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs)
data->setToken(loginJob->accessToken().toLatin1());
data->setDeviceId(loginJob->deviceId());
completeSetup(loginJob->userId());
-#ifndef Quotient_E2EE_ENABLED
- qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
-#else // Quotient_E2EE_ENABLED
- encryptionManager->uploadIdentityKeys(q);
- encryptionManager->uploadOneTimeKeys(q);
-#endif // Quotient_E2EE_ENABLED
+ saveAccessTokenToKeychain();
+#ifdef Quotient_E2EE_ENABLED
+ database->clear();
+#endif
});
connect(loginJob, &BaseJob::failure, q, [this, loginJob] {
emit q->loginError(loginJob->errorString(), loginJob->rawDataSample());
@@ -445,15 +594,63 @@ void Connection::Private::completeSetup(const QString& mxId)
qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString()
<< "by user" << data->userId()
<< "from device" << data->deviceId();
+ Accounts.add(q);
+ connect(qApp, &QCoreApplication::aboutToQuit, q, &Connection::saveState);
#ifndef Quotient_E2EE_ENABLED
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
#else // Quotient_E2EE_ENABLED
AccountSettings accountSettings(data->userId());
- encryptionManager.reset(
- new EncryptionManager(accountSettings.encryptionAccountPickle()));
- if (accountSettings.encryptionAccountPickle().isEmpty()) {
- accountSettings.setEncryptionAccountPickle(
- encryptionManager->olmAccountPickle());
+
+ QKeychain::ReadPasswordJob job(qAppName());
+ job.setAutoDelete(false);
+ job.setKey(accountSettings.userId() + QStringLiteral("-Pickle"));
+ QEventLoop loop;
+ QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+ job.start();
+ loop.exec();
+
+ if (job.error() == QKeychain::Error::EntryNotFound) {
+ picklingMode = Encrypted { RandomBuffer(128) };
+ QKeychain::WritePasswordJob job(qAppName());
+ job.setAutoDelete(false);
+ job.setKey(accountSettings.userId() + QStringLiteral("-Pickle"));
+ job.setBinaryData(std::get<Encrypted>(picklingMode).key);
+ QEventLoop loop;
+ QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+ job.start();
+ loop.exec();
+
+ if (job.error()) {
+ qCWarning(E2EE) << "Could not save pickling key to keychain: " << job.errorString();
+ }
+ } else if(job.error() != QKeychain::Error::NoError) {
+ //TODO Error, do something
+ qCWarning(E2EE) << "Error loading pickling key from keychain:" << job.error();
+ } else {
+ qCDebug(E2EE) << "Successfully loaded pickling key from keychain";
+ picklingMode = Encrypted { job.binaryData() };
+ }
+
+ database = new Database(data->userId(), data->deviceId(), q);
+
+ // init olmAccount
+ olmAccount = std::make_unique<QOlmAccount>(data->userId(), data->deviceId(), q);
+ connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount);
+
+ loadSessions();
+
+ if (database->accountPickle().isEmpty()) {
+ // create new account and save unpickle data
+ olmAccount->createNewAccount();
+ auto job = q->callApi<UploadKeysJob>(olmAccount->deviceKeys());
+ connect(job, &BaseJob::failure, q, [job]{
+ qCWarning(E2EE) << "Failed to upload device keys:" << job->errorString();
+ });
+ } else {
+ // account already existing
+ if (!olmAccount->unpickle(database->accountPickle(), picklingMode))
+ qWarning(E2EE)
+ << "Could not unpickle Olm account, E2EE won't be available";
}
#endif // Quotient_E2EE_ENABLED
emit q->stateChanged();
@@ -463,7 +660,7 @@ void Connection::Private::completeSetup(const QString& mxId)
void Connection::Private::checkAndConnect(const QString& userId,
const std::function<void()>& connectFn,
- const std::optional<LoginFlows::LoginFlow>& flow)
+ const std::optional<LoginFlow>& flow)
{
if (data->baseUrl().isValid() && (!flow || loginFlows.contains(*flow))) {
connectFn();
@@ -479,10 +676,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);
@@ -513,8 +711,10 @@ void Connection::logout()
|| d->logoutJob->error() == BaseJob::ContentAccessError) {
if (d->syncLoopConnection)
disconnect(d->syncLoopConnection);
- d->data->setToken({});
+ SettingsGroup("Accounts").remove(userId());
+ d->dropAccessToken();
emit loggedOut();
+ deleteLater();
} else { // logout() somehow didn't proceed - restore the session state
emit stateChanged();
if (wasSyncing)
@@ -606,24 +806,39 @@ QJsonObject toJson(const DirectChatsMap& directChats)
void Connection::onSyncSuccess(SyncData&& data, bool fromCache)
{
+#ifdef Quotient_E2EE_ENABLED
+ d->oneTimeKeysCount = data.deviceOneTimeKeysCount();
+ if (d->oneTimeKeysCount[SignedCurve25519Key] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys()
+ && !d->isUploadingKeys) {
+ d->isUploadingKeys = true;
+ d->olmAccount->generateOneTimeKeys(
+ d->olmAccount->maxNumberOfOneTimeKeys() / 2 - d->oneTimeKeysCount[SignedCurve25519Key]);
+ auto keys = d->olmAccount->oneTimeKeys();
+ auto job = d->olmAccount->createUploadKeyRequest(keys);
+ run(job, ForegroundRequest);
+ connect(job, &BaseJob::success, this,
+ [this] { d->olmAccount->markKeysAsPublished(); });
+ connect(job, &BaseJob::result, this,
+ [this] { d->isUploadingKeys = false; });
+ }
+ if(d->firstSync) {
+ d->loadDevicesList();
+ d->firstSync = false;
+ }
+
+ d->consumeDevicesList(data.takeDevicesList());
+#endif // Quotient_E2EE_ENABLED
+ d->consumeToDeviceEvents(data.takeToDeviceEvents());
d->data->setLastEvent(data.nextBatch());
d->consumeRoomData(data.takeRoomData(), fromCache);
d->consumeAccountData(data.takeAccountData());
d->consumePresenceData(data.takePresenceData());
- d->consumeToDeviceEvents(data.takeToDeviceEvents());
#ifdef Quotient_E2EE_ENABLED
- // handling device_one_time_keys_count
- if (!d->encryptionManager)
- {
- qCDebug(E2EE) << "Encryption manager is not there yet, updating "
- "one-time key counts will be skipped";
- return;
+ if(d->encryptionUpdateRequired) {
+ d->loadOutdatedUserDevices();
+ d->encryptionUpdateRequired = false;
}
- if (const auto deviceOneTimeKeysCount = data.deviceOneTimeKeysCount();
- !deviceOneTimeKeysCount.isEmpty())
- d->encryptionManager->updateOneTimeKeyCounts(this,
- deviceOneTimeKeysCount);
-#endif // Quotient_E2EE_ENABLED
+#endif
}
void Connection::Private::consumeRoomData(SyncDataList&& roomDataList,
@@ -641,21 +856,19 @@ 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)) {
pendingStateRoomIds.removeOne(roomData.roomId);
- r->updateData(std::move(roomData), fromCache);
- if (firstTimeRooms.removeOne(r)) {
- emit q->loadedRoomState(r);
- if (capabilities.roomVersions)
- r->checkVersion();
- // Otherwise, the version will be checked in reloadCapabilities()
- }
+ // Update rooms one by one, giving time to update the UI.
+ QMetaObject::invokeMethod(
+ r,
+ [r, rd = std::move(roomData), fromCache] () mutable {
+ r->updateData(std::move(rd), fromCache);
+ },
+ Qt::QueuedConnection);
}
- // Let UI update itself after updating each room
- QCoreApplication::processEvents();
}
}
@@ -664,21 +877,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())
@@ -691,7 +904,7 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents)
DirectChatsMap remoteAdditions;
for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) {
- if (auto* const u = q->user(it.key())) {
+ if (auto* u = q->user(it.key())) {
if (!directChats.contains(u, it.value())
&& !dcLocalRemovals.contains(u, it.value())) {
Q_ASSERT(!directChatUsers.contains(it.value(), u));
@@ -706,7 +919,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())
@@ -751,34 +964,105 @@ void Connection::Private::consumePresenceData(Events&& presenceData)
void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents)
{
#ifdef Quotient_E2EE_ENABLED
- // handling m.room_key to-device encrypted event
- visitEach(toDeviceEvents, [this](const EncryptedEvent& ee) {
- if (ee.algorithm() != OlmV1Curve25519AesSha2AlgoKey) {
- qCDebug(E2EE) << "Encrypted event" << ee.id() << "algorithm"
- << ee.algorithm() << "is not supported";
- return;
+ if (!toDeviceEvents.empty()) {
+ qCDebug(E2EE) << "Consuming" << toDeviceEvents.size()
+ << "to-device events";
+ for (auto&& tdEvt : toDeviceEvents) {
+ if (processIfVerificationEvent(*tdEvt, false))
+ continue;
+ if (auto&& event = eventCast<EncryptedEvent>(std::move(tdEvt))) {
+ if (event->algorithm() != OlmV1Curve25519AesSha2AlgoKey) {
+ qCDebug(E2EE) << "Unsupported algorithm" << event->id()
+ << "for event" << event->algorithm();
+ return;
+ }
+ if (isKnownCurveKey(event->senderId(), event->senderKey())) {
+ handleEncryptedToDeviceEvent(*event);
+ return;
+ }
+ trackedUsers += event->senderId();
+ outdatedUsers += event->senderId();
+ encryptionUpdateRequired = true;
+ pendingEncryptedEvents.push_back(std::move(event));
+ }
}
+ }
+#endif
+}
- // TODO: full maintaining of the device keys
- // with device_lists sync extention and /keys/query
- qCDebug(E2EE) << "Getting device keys for the m.room_key sender:"
- << ee.senderId();
- // encryptionManager->updateDeviceKeys();
-
- visit(*sessionDecryptMessage(ee),
- [this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) {
- if (auto* detectedRoom = q->room(roomKeyEvent.roomId()))
- detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey);
- else
- qCDebug(E2EE)
- << "Encrypted event room id" << roomKeyEvent.roomId()
- << "is not found at the connection" << q->objectName();
- },
- [](const Event& evt) {
- qCDebug(E2EE) << "Skipping encrypted to_device event, type"
- << evt.matrixType();
- });
- });
+#ifdef Quotient_E2EE_ENABLED
+bool Connection::Private::processIfVerificationEvent(const Event& evt,
+ bool encrypted)
+{
+ return switchOnType(evt,
+ [this, encrypted](const KeyVerificationRequestEvent& reqEvt) {
+ const auto sessionIter = verificationSessions.insert(
+ reqEvt.transactionId(),
+ new KeyVerificationSession(q->userId(), reqEvt, q, encrypted));
+ emit q->newKeyVerificationSession(*sessionIter);
+ return true;
+ },
+ [](const KeyVerificationDoneEvent&) {
+ return true;
+ },
+ [this](const KeyVerificationEvent& kvEvt) {
+ if (auto* const session =
+ verificationSessions.value(kvEvt.transactionId())) {
+ session->handleEvent(kvEvt);
+ emit q->keyVerificationStateChanged(session, session->state());
+ }
+ return true;
+ },
+ false);
+}
+
+void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event)
+{
+ const auto [decryptedEvent, olmSessionId] = sessionDecryptMessage(event);
+ if(!decryptedEvent) {
+ qCWarning(E2EE) << "Failed to decrypt event" << event.id();
+ return;
+ }
+
+ if (processIfVerificationEvent(*decryptedEvent, true))
+ return;
+ switchOnType(*decryptedEvent,
+ [this, &event,
+ olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) {
+ if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) {
+ detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(),
+ olmSessionId);
+ } else {
+ qCDebug(E2EE)
+ << "Encrypted event room id" << roomKeyEvent.roomId()
+ << "is not found at the connection" << q->objectName();
+ }
+ },
+ [](const Event& evt) {
+ qCWarning(E2EE) << "Skipping encrypted to_device event, type"
+ << evt.matrixType();
+ });
+}
+#endif
+
+void Connection::Private::consumeDevicesList(DevicesList&& devicesList)
+{
+#ifdef Quotient_E2EE_ENABLED
+ bool hasNewOutdatedUser = false;
+ for(const auto &changed : devicesList.changed) {
+ if(trackedUsers.contains(changed)) {
+ outdatedUsers += changed;
+ hasNewOutdatedUser = true;
+ }
+ }
+ for(const auto &left : devicesList.left) {
+ trackedUsers -= left;
+ outdatedUsers -= left;
+ deviceKeys.remove(left);
+ }
+ if(hasNewOutdatedUser) {
+ loadOutdatedUserDevices();
+ }
#endif
}
@@ -796,11 +1080,6 @@ 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)
{
@@ -844,6 +1123,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)
@@ -914,6 +1202,18 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url,
return job;
}
+#ifdef Quotient_E2EE_ENABLED
+DownloadFileJob* Connection::downloadFile(
+ const QUrl& url, const EncryptedFileMetadata& fileMetadata,
+ const QString& localFilename)
+{
+ auto mediaId = url.authority() + url.path();
+ auto idParts = splitMediaId(mediaId);
+ return callApi<DownloadFileJob>(idParts.front(), idParts.back(),
+ fileMetadata, localFilename);
+}
+#endif
+
CreateRoomJob*
Connection::createRoom(RoomVisibility visibility, const QString& alias,
const QString& name, const QString& topic,
@@ -924,12 +1224,6 @@ Connection::createRoom(RoomVisibility visibility, const QString& alias,
const QJsonObject& creationContent)
{
invites.removeOne(userId()); // The creator is by definition in the room
- for (const auto& i : invites)
- if (!user(i)) {
- qCWarning(MAIN) << "Won't create a room with malformed invitee ids";
- return nullptr;
- }
-
auto job = callApi<CreateRoomJob>(visibility == PublishRoom
? QStringLiteral("public")
: QStringLiteral("private"),
@@ -964,7 +1258,7 @@ void Connection::requestDirectChat(User* u)
void Connection::doInDirectChat(const QString& userId,
const std::function<void(Room*)>& operation)
{
- if (auto* const u = user(userId))
+ if (auto* u = user(userId))
doInDirectChat(u, operation);
else
qCCritical(MAIN)
@@ -1060,7 +1354,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
@@ -1079,7 +1373,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 << ": "
@@ -1088,26 +1382,11 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id)
return forgetJob;
}
-SendToDeviceJob*
-Connection::sendToDevices(const QString& eventType,
- const UsersToDevicesToEvents& eventsMap)
-{
- QHash<QString, QHash<QString, QJsonObject>> json;
- json.reserve(int(eventsMap.size()));
- std::for_each(eventsMap.begin(), eventsMap.end(),
- [&json](const auto& userTodevicesToEvents) {
- auto& jsonUser = json[userTodevicesToEvents.first];
- const auto& devicesToEvents = userTodevicesToEvents.second;
- std::for_each(devicesToEvents.begin(),
- devicesToEvents.end(),
- [&jsonUser](const auto& deviceToEvents) {
- jsonUser.insert(
- deviceToEvents.first,
- deviceToEvents.second.contentJson());
- });
- });
+SendToDeviceJob* Connection::sendToDevices(
+ const QString& eventType, const UsersToDevicesToContent& contents)
+{
return callApi<SendToDeviceJob>(BackgroundRequest, eventType,
- generateTxnId(), json);
+ generateTxnId(), contents);
}
SendMessageJob* Connection::sendMessage(const QString& roomId,
@@ -1200,12 +1479,14 @@ User* Connection::user(const QString& uId)
{
if (uId.isEmpty())
return nullptr;
+ if (const auto v = d->userMap.value(uId, nullptr))
+ return v;
+ // Before creating a user object, check that the user id is well-formed
+ // (it's faster to just do a lookup above before validation)
if (!uId.startsWith('@') || serverPart(uId).isEmpty()) {
qCCritical(MAIN) << "Malformed userId:" << uId;
return nullptr;
}
- if (d->userMap.contains(uId))
- return d->userMap.value(uId);
auto* user = userFactory()(this, uId);
d->userMap.insert(uId, user);
emit newUser(user);
@@ -1227,15 +1508,15 @@ QByteArray Connection::accessToken() const
{
// The logout job needs access token to do its job; so the token is
// kept inside d->data but no more exposed to the outside world.
- return isJobRunning(d->logoutJob) ? QByteArray() : d->data->accessToken();
+ return isJobPending(d->logoutJob) ? QByteArray() : d->data->accessToken();
}
bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); }
#ifdef Quotient_E2EE_ENABLED
-QtOlm::Account* Connection::olmAccount() const
+QOlmAccount *Connection::olmAccount() const
{
- return d->encryptionManager->account();
+ return d->olmAccount.get();
}
#endif // Quotient_E2EE_ENABLED
@@ -1246,20 +1527,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;
@@ -1362,8 +1629,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();
}
@@ -1395,7 +1662,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;
@@ -1421,8 +1688,8 @@ bool Connection::isIgnored(const User* user) const
IgnoredUsersList Connection::ignoredUsers() const
{
- const auto* event = d->unpackAccountData<IgnoredUsersEvent>();
- return event ? event->ignored_users() : IgnoredUsersList();
+ const auto* event = accountData<IgnoredUsersEvent>();
+ return event ? event->ignoredUsers() : IgnoredUsersList();
}
void Connection::addToIgnoredUsers(const User* user)
@@ -1461,7 +1728,7 @@ Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState)
Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id");
// If joinState is empty, all joinState == comparisons below are false.
- const auto roomKey = qMakePair(id, joinState == JoinState::Invite);
+ const std::pair roomKey { id, joinState == JoinState::Invite };
auto* room = d->roomMap.value(roomKey, nullptr);
if (room) {
// Leave is a special case because in transition (5a) (see the .h file)
@@ -1486,9 +1753,14 @@ Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState)
return nullptr;
}
d->roomMap.insert(roomKey, room);
- d->firstTimeRooms.push_back(room);
connect(room, &Room::beforeDestruction, this,
&Connection::aboutToDeleteRoom);
+ connect(room, &Room::baseStateLoaded, this, [this, room] {
+ emit loadedRoomState(room);
+ if (d->capabilities.roomVersions)
+ room->checkVersion();
+ // Otherwise, the version will be checked in reloadCapabilities()
+ });
emit newRoom(room);
}
if (!joinState)
@@ -1534,27 +1806,21 @@ 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
{
return d->data->generateTxnId();
}
-void Connection::Private::clearResolvingContext()
-{
- if (isJobRunning(resolverJob))
- resolverJob->abandon();
- if (isJobRunning(loginFlowsJob))
- loginFlowsJob->abandon();
- loginFlows.clear();
-
-}
-
void Connection::setHomeserver(const QUrl& url)
{
- d->clearResolvingContext();
+ if (isJobPending(d->resolverJob))
+ d->resolverJob->abandon();
+ if (isJobPending(d->loginFlowsJob))
+ d->loginFlowsJob->abandon();
+ d->loginFlows.clear();
if (homeserver() != url) {
d->data->setBaseUrl(url);
@@ -1581,16 +1847,10 @@ void Connection::saveRoomState(Room* r) const
QFile outRoomFile { stateCacheDir().filePath(
SyncData::fileNameForRoom(r->id())) };
if (outRoomFile.open(QFile::WriteOnly)) {
-#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
const auto data =
d->cacheToBinary
? QCborValue::fromJsonValue(r->toJson()).toCbor()
: QJsonDocument(r->toJson()).toJson(QJsonDocument::Compact);
-#else
- QJsonDocument json { r->toJson() };
- const auto data = d->cacheToBinary ? json.toBinaryData()
- : json.toJson(QJsonDocument::Compact);
-#endif
outRoomFile.write(data.data(), data.size());
qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName();
} else {
@@ -1643,26 +1903,26 @@ void Connection::saveState() const
}
{
QJsonArray accountDataEvents {
- basicEventJson(QStringLiteral("m.direct"), toJson(d->directChats))
+ Event::basicJson(QStringLiteral("m.direct"), toJson(d->directChats))
};
for (const auto& e : d->accountData)
accountDataEvents.append(
- basicEventJson(e.first, e.second->contentJson()));
+ Event::basicJson(e.first, e.second->contentJson()));
rootObj.insert(QStringLiteral("account_data"),
QJsonObject {
{ QStringLiteral("events"), accountDataEvents } });
}
+#ifdef Quotient_E2EE_ENABLED
+ {
+ QJsonObject keysJson = toJson(d->oneTimeKeysCount);
+ rootObj.insert(QStringLiteral("device_one_time_keys_count"), keysJson);
+ }
+#endif
-#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
const auto data =
d->cacheToBinary ? QCborValue::fromJsonValue(rootObj).toCbor()
: QJsonDocument(rootObj).toJson(QJsonDocument::Compact);
-#else
- QJsonDocument json { rootObj };
- const auto data = d->cacheToBinary ? json.toBinaryData()
- : json.toJson(QJsonDocument::Compact);
-#endif
qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et;
outFile.write(data.data(), data.size());
@@ -1738,7 +1998,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 =
@@ -1763,6 +2023,14 @@ QStringList Connection::stableRoomVersions() const
return l;
}
+bool Connection::canChangePassword() const
+{
+ // By default assume we can
+ return d->capabilities.changePassword
+ ? d->capabilities.changePassword->enabled
+ : true;
+}
+
inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1,
const Connection::SupportedRoomVersion& v2)
{
@@ -1790,3 +2058,430 @@ QVector<Connection::SupportedRoomVersion> Connection::availableRoomVersions() co
}
return result;
}
+
+#ifdef Quotient_E2EE_ENABLED
+void Connection::Private::loadOutdatedUserDevices()
+{
+ QHash<QString, QStringList> users;
+ for(const auto &user : outdatedUsers) {
+ users[user] += QStringList();
+ }
+ if(currentQueryKeysJob) {
+ currentQueryKeysJob->abandon();
+ currentQueryKeysJob = nullptr;
+ }
+ auto queryKeysJob = q->callApi<QueryKeysJob>(users);
+ currentQueryKeysJob = queryKeysJob;
+ connect(queryKeysJob, &BaseJob::success, q, [this, queryKeysJob](){
+ currentQueryKeysJob = nullptr;
+ const auto data = queryKeysJob->deviceKeys();
+ for(const auto &[user, keys] : asKeyValueRange(data)) {
+ QHash<QString, Quotient::DeviceKeys> oldDevices = deviceKeys[user];
+ deviceKeys[user].clear();
+ for(const auto &device : keys) {
+ if(device.userId != user) {
+ qCWarning(E2EE)
+ << "mxId mismatch during device key verification:"
+ << device.userId << user;
+ continue;
+ }
+ if (!std::all_of(device.algorithms.cbegin(),
+ device.algorithms.cend(),
+ isSupportedAlgorithm)) {
+ qCWarning(E2EE) << "Unsupported encryption algorithms found"
+ << device.algorithms;
+ continue;
+ }
+ if (!verifyIdentitySignature(device, device.deviceId,
+ device.userId)) {
+ qCWarning(E2EE) << "Failed to verify devicekeys signature. "
+ "Skipping this device";
+ continue;
+ }
+ if (oldDevices.contains(device.deviceId)) {
+ if (oldDevices[device.deviceId].keys["ed25519:" % device.deviceId] != device.keys["ed25519:" % device.deviceId]) {
+ qCDebug(E2EE) << "Device reuse detected. Skipping this device";
+ continue;
+ }
+ }
+ deviceKeys[user][device.deviceId] = SLICE(device, DeviceKeys);
+ }
+ outdatedUsers -= user;
+ }
+ saveDevicesList();
+
+ for(size_t i = 0; i < pendingEncryptedEvents.size();) {
+ if (isKnownCurveKey(
+ pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(),
+ pendingEncryptedEvents[i]->contentPart<QString>("sender_key"_ls))) {
+ handleEncryptedToDeviceEvent(*pendingEncryptedEvents[i]);
+ pendingEncryptedEvents.erase(pendingEncryptedEvents.begin() + i);
+ } else
+ ++i;
+ }
+ });
+}
+
+void Connection::Private::saveDevicesList()
+{
+ q->database()->transaction();
+ auto query = q->database()->prepareQuery(
+ QStringLiteral("DELETE FROM tracked_users"));
+ q->database()->execute(query);
+ query.prepare(QStringLiteral(
+ "INSERT INTO tracked_users(matrixId) VALUES(:matrixId);"));
+ for (const auto& user : trackedUsers) {
+ query.bindValue(":matrixId", user);
+ q->database()->execute(query);
+ }
+
+ query.prepare(QStringLiteral("DELETE FROM outdated_users"));
+ q->database()->execute(query);
+ query.prepare(QStringLiteral(
+ "INSERT INTO outdated_users(matrixId) VALUES(:matrixId);"));
+ for (const auto& user : outdatedUsers) {
+ query.bindValue(":matrixId", user);
+ q->database()->execute(query);
+ }
+
+ query.prepare(QStringLiteral(
+ "INSERT INTO tracked_devices"
+ "(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey, verified) "
+ "SELECT :matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey, :verified WHERE NOT EXISTS(SELECT 1 FROM tracked_devices WHERE matrixId=:matrixId AND deviceId=:deviceId);"
+ ));
+ for (const auto& user : deviceKeys.keys()) {
+ for (const auto& device : deviceKeys[user]) {
+ auto keys = device.keys.keys();
+ auto curveKeyId = keys[0].startsWith(QLatin1String("curve")) ? keys[0] : keys[1];
+ auto edKeyId = keys[0].startsWith(QLatin1String("ed")) ? keys[0] : keys[1];
+
+ query.bindValue(":matrixId", user);
+ query.bindValue(":deviceId", device.deviceId);
+ query.bindValue(":curveKeyId", curveKeyId);
+ query.bindValue(":curveKey", device.keys[curveKeyId]);
+ query.bindValue(":edKeyId", edKeyId);
+ query.bindValue(":edKey", device.keys[edKeyId]);
+ // If the device gets saved here, it can't be verified
+ query.bindValue(":verified", false);
+
+ q->database()->execute(query);
+ }
+ }
+ q->database()->commit();
+}
+
+void Connection::Private::loadDevicesList()
+{
+ auto query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_users;"));
+ q->database()->execute(query);
+ while(query.next()) {
+ trackedUsers += query.value(0).toString();
+ }
+
+ query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM outdated_users;"));
+ q->database()->execute(query);
+ while(query.next()) {
+ outdatedUsers += query.value(0).toString();
+ }
+
+ query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices;"));
+ q->database()->execute(query);
+ while(query.next()) {
+ deviceKeys[query.value("matrixId").toString()][query.value("deviceId").toString()] = DeviceKeys {
+ query.value("matrixId").toString(),
+ query.value("deviceId").toString(),
+ { "m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"},
+ {{query.value("curveKeyId").toString(), query.value("curveKey").toString()},
+ {query.value("edKeyId").toString(), query.value("edKey").toString()}},
+ {} // Signatures are not saved/loaded as they are not needed after initial validation
+ };
+ }
+
+}
+
+void Connection::encryptionUpdate(Room *room)
+{
+ for(const auto &user : room->users()) {
+ if(!d->trackedUsers.contains(user->id())) {
+ d->trackedUsers += user->id();
+ d->outdatedUsers += user->id();
+ d->encryptionUpdateRequired = true;
+ }
+ }
+}
+
+PicklingMode Connection::picklingMode() const
+{
+ return d->picklingMode;
+}
+#endif
+
+void Connection::saveOlmAccount()
+{
+#ifdef Quotient_E2EE_ENABLED
+ qCDebug(E2EE) << "Saving olm account";
+ d->database->setAccountPickle(d->olmAccount->pickle(d->picklingMode));
+#endif
+}
+
+#ifdef Quotient_E2EE_ENABLED
+QJsonObject Connection::decryptNotification(const QJsonObject &notification)
+{
+ if (auto r = room(notification["room_id"].toString()))
+ if (auto event =
+ loadEvent<EncryptedEvent>(notification["event"].toObject()))
+ if (const auto decrypted = r->decryptMessage(*event))
+ return decrypted->fullJson();
+ return QJsonObject();
+}
+
+Database* Connection::database() const
+{
+ return d->database;
+}
+
+UnorderedMap<QString, QOlmInboundGroupSessionPtr>
+Connection::loadRoomMegolmSessions(const Room* room) const
+{
+ return database()->loadMegolmSessions(room->id(), picklingMode());
+}
+
+void Connection::saveMegolmSession(const Room* room,
+ const QOlmInboundGroupSession& session) const
+{
+ database()->saveMegolmSession(room->id(), session.sessionId(),
+ session.pickle(picklingMode()),
+ session.senderId(), session.olmSessionId());
+}
+
+QStringList Connection::devicesForUser(const QString& userId) const
+{
+ return d->deviceKeys[userId].keys();
+}
+
+QString Connection::Private::curveKeyForUserDevice(const QString& userId,
+ const QString& device) const
+{
+ return deviceKeys[userId][device].keys["curve25519:" % device];
+}
+
+QString Connection::edKeyForUserDevice(const QString& userId,
+ const QString& deviceId) const
+{
+ return d->deviceKeys[userId][deviceId].keys["ed25519:" % deviceId];
+}
+
+bool Connection::Private::isKnownCurveKey(const QString& userId,
+ const QString& curveKey) const
+{
+ auto query = database->prepareQuery(
+ QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId "
+ "AND curveKey=:curveKey"));
+ query.bindValue(":matrixId", userId);
+ query.bindValue(":curveKey", curveKey);
+ database->execute(query);
+ return query.next();
+}
+
+bool Connection::hasOlmSession(const QString& user,
+ const QString& deviceId) const
+{
+ const auto& curveKey = d->curveKeyForUserDevice(user, deviceId);
+ return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty();
+}
+
+std::pair<QOlmMessage::Type, QByteArray> Connection::Private::olmEncryptMessage(
+ const QString& userId, const QString& device,
+ const QByteArray& message) const
+{
+ const auto& curveKey = curveKeyForUserDevice(userId, device);
+ const auto& olmSession = olmSessions.at(curveKey).front();
+ const auto result = olmSession->encrypt(message);
+ database->updateOlmSession(curveKey, olmSession->sessionId(),
+ olmSession->pickle(picklingMode));
+ return { result.type(), result.toCiphertext() };
+}
+
+bool Connection::Private::createOlmSession(const QString& targetUserId,
+ const QString& targetDeviceId,
+ const OneTimeKeys& oneTimeKeyObject)
+{
+ static QOlmUtility verifier;
+ qDebug(E2EE) << "Creating a new session for" << targetUserId
+ << targetDeviceId;
+ if (oneTimeKeyObject.isEmpty()) {
+ qWarning(E2EE) << "No one time key for" << targetUserId
+ << targetDeviceId;
+ return false;
+ }
+ auto* signedOneTimeKey =
+ std::get_if<SignedOneTimeKey>(&*oneTimeKeyObject.begin());
+ if (!signedOneTimeKey) {
+ qWarning(E2EE) << "No signed one time key for" << targetUserId
+ << targetDeviceId;
+ return false;
+ }
+ // Verify contents of signedOneTimeKey - for that, drop `signatures` and
+ // `unsigned` and then verify the object against the respective signature
+ const auto signature =
+ signedOneTimeKey->signature(targetUserId, targetDeviceId);
+ if (!verifier.ed25519Verify(
+ q->edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(),
+ signedOneTimeKey->toJsonForVerification(),
+ signature)) {
+ qWarning(E2EE) << "Failed to verify one-time-key signature for"
+ << targetUserId << targetDeviceId
+ << ". Skipping this device.";
+ return false;
+ }
+ const auto recipientCurveKey =
+ curveKeyForUserDevice(targetUserId, targetDeviceId).toLatin1();
+ auto session =
+ QOlmSession::createOutboundSession(olmAccount.get(), recipientCurveKey,
+ signedOneTimeKey->key());
+ if (!session) {
+ qCWarning(E2EE) << "Failed to create olm session for "
+ << recipientCurveKey << session.error();
+ return false;
+ }
+ saveSession(**session, recipientCurveKey);
+ olmSessions[recipientCurveKey].push_back(std::move(*session));
+ return true;
+}
+
+QJsonObject Connection::Private::assembleEncryptedContent(
+ QJsonObject payloadJson, const QString& targetUserId,
+ const QString& targetDeviceId) const
+{
+ payloadJson.insert(SenderKeyL, data->userId());
+// eventJson.insert("sender_device"_ls, data->deviceId());
+ payloadJson.insert("keys"_ls,
+ QJsonObject{
+ { Ed25519Key,
+ QString(olmAccount->identityKeys().ed25519) } });
+ payloadJson.insert("recipient"_ls, targetUserId);
+ payloadJson.insert(
+ "recipient_keys"_ls,
+ QJsonObject{ { Ed25519Key,
+ q->edKeyForUserDevice(targetUserId, targetDeviceId) } });
+ const auto [type, cipherText] = olmEncryptMessage(
+ targetUserId, targetDeviceId,
+ QJsonDocument(payloadJson).toJson(QJsonDocument::Compact));
+ QJsonObject encrypted {
+ { curveKeyForUserDevice(targetUserId, targetDeviceId),
+ QJsonObject { { "type"_ls, type },
+ { "body"_ls, QString(cipherText) } } }
+ };
+ return EncryptedEvent(encrypted, olmAccount->identityKeys().curve25519)
+ .contentJson();
+}
+
+void Connection::sendSessionKeyToDevices(
+ const QString& roomId, const QByteArray& sessionId,
+ const QByteArray& sessionKey, const QMultiHash<QString, QString>& devices,
+ int index)
+{
+ qDebug(E2EE) << "Sending room key to devices:" << sessionId
+ << sessionKey.toHex();
+ QHash<QString, QHash<QString, QString>> hash;
+ for (const auto& [userId, deviceId] : asKeyValueRange(devices))
+ if (!hasOlmSession(userId, deviceId)) {
+ hash[userId].insert(deviceId, "signed_curve25519"_ls);
+ qDebug(E2EE) << "Adding" << userId << deviceId
+ << "to keys to claim";
+ }
+
+ if (hash.isEmpty())
+ return;
+
+ auto job = callApi<ClaimKeysJob>(hash);
+ connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, sessionKey, devices, index] {
+ QHash<QString, QHash<QString, QJsonObject>> usersToDevicesToContent;
+ for (const auto oneTimeKeys = job->oneTimeKeys();
+ const auto& [targetUserId, targetDeviceId] :
+ asKeyValueRange(devices)) {
+ if (!hasOlmSession(targetUserId, targetDeviceId)
+ && !d->createOlmSession(
+ targetUserId, targetDeviceId,
+ oneTimeKeys[targetUserId][targetDeviceId]))
+ continue;
+
+ // Noisy but nice for debugging
+// qDebug(E2EE) << "Creating the payload for" << targetUserId
+// << targetDeviceId << sessionId << sessionKey.toHex();
+ const auto keyEventJson = RoomKeyEvent(MegolmV1AesSha2AlgoKey,
+ roomId, sessionId, sessionKey)
+ .fullJson();
+
+ usersToDevicesToContent[targetUserId][targetDeviceId] =
+ d->assembleEncryptedContent(keyEventJson, targetUserId,
+ targetDeviceId);
+ }
+ if (!usersToDevicesToContent.empty()) {
+ sendToDevices(EncryptedEvent::TypeId, usersToDevicesToContent);
+ QVector<std::tuple<QString, QString, QString>> receivedDevices;
+ receivedDevices.reserve(devices.size());
+ for (const auto& [user, device] : asKeyValueRange(devices))
+ receivedDevices.push_back(
+ { user, device, d->curveKeyForUserDevice(user, device) });
+
+ database()->setDevicesReceivedKey(roomId, receivedDevices,
+ sessionId, index);
+ }
+ });
+}
+
+QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(
+ const QString& roomId) const
+{
+ return d->database->loadCurrentOutboundMegolmSession(roomId,
+ d->picklingMode);
+}
+
+void Connection::saveCurrentOutboundMegolmSession(
+ const QString& roomId, const QOlmOutboundGroupSession& session) const
+{
+ d->database->saveCurrentOutboundMegolmSession(roomId, d->picklingMode,
+ session);
+}
+
+void Connection::startKeyVerificationSession(const QString& deviceId)
+{
+ auto* const session = new KeyVerificationSession(userId(), deviceId, this);
+ emit newKeyVerificationSession(session);
+}
+
+void Connection::sendToDevice(const QString& targetUserId,
+ const QString& targetDeviceId, const Event& event,
+ bool encrypted)
+{
+ const auto contentJson =
+ encrypted ? d->assembleEncryptedContent(event.fullJson(), targetUserId,
+ targetDeviceId)
+ : event.contentJson();
+ sendToDevices(encrypted ? EncryptedEvent::TypeId : event.type(),
+ { { targetUserId, { { targetDeviceId, contentJson } } } });
+}
+
+bool Connection::isVerifiedSession(const QString& megolmSessionId) const
+{
+ auto query = database()->prepareQuery("SELECT olmSessionId FROM inbound_megolm_sessions WHERE sessionId=:sessionId;"_ls);
+ query.bindValue(":sessionId", megolmSessionId);
+ database()->execute(query);
+ if (!query.next()) {
+ return false;
+ }
+ auto olmSessionId = query.value("olmSessionId").toString();
+ query.prepare("SELECT senderKey FROM olm_sessions WHERE sessionId=:sessionId;"_ls);
+ query.bindValue(":sessionId", olmSessionId);
+ database()->execute(query);
+ if (!query.next()) {
+ return false;
+ }
+ auto curveKey = query.value("senderKey"_ls).toString();
+ query.prepare("SELECT verified FROM tracked_devices WHERE curveKey=:curveKey;"_ls);
+ query.bindValue(":curveKey", curveKey);
+ database()->execute(query);
+ return query.next() && query.value("verified").toBool();
+}
+#endif
diff --git a/lib/connection.h b/lib/connection.h
index c90cb892..75faf370 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -1,30 +1,16 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name>
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
-#include "ssosession.h"
-#include "joinstate.h"
-#include "qt_connection_util.h"
#include "quotient_common.h"
+#include "ssosession.h"
+#include "util.h"
-#include "csapi/login.h"
#include "csapi/create_room.h"
+#include "csapi/login.h"
#include "events/accountdataevents.h"
@@ -35,9 +21,12 @@
#include <functional>
-namespace QtOlm {
-class Account;
-}
+#ifdef Quotient_E2EE_ENABLED
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmoutboundsession.h"
+#include "keyverificationsession.h"
+#include "events/keyverificationevent.h"
+#endif
Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow)
@@ -61,29 +50,33 @@ class DownloadFileJob;
class SendToDeviceJob;
class SendMessageJob;
class LeaveRoomJob;
+class Database;
+struct EncryptedFileMetadata;
+
+class QOlmAccount;
+class QOlmInboundGroupSession;
+
+using LoginFlow = GetLoginFlowsJob::LoginFlow;
+
+/// Predefined login flows
+namespace LoginFlows {
+ inline const LoginFlow Password { "m.login.password" };
+ inline const LoginFlow SSO { "m.login.sso" };
+ inline const LoginFlow Token { "m.login.token" };
+}
// To simplify comparisons of LoginFlows
-inline bool operator==(const GetLoginFlowsJob::LoginFlow& lhs,
- const GetLoginFlowsJob::LoginFlow& rhs)
+inline bool operator==(const LoginFlow& lhs, const LoginFlow& rhs)
{
return lhs.type == rhs.type;
}
-inline bool operator!=(const GetLoginFlowsJob::LoginFlow& lhs,
- const GetLoginFlowsJob::LoginFlow& rhs)
+inline bool operator!=(const LoginFlow& lhs, const LoginFlow& rhs)
{
return !(lhs == rhs);
}
-/// Predefined login flows
-struct LoginFlows {
- using LoginFlow = GetLoginFlowsJob::LoginFlow;
- static inline const LoginFlow Password { "m.login.password" };
- static inline const LoginFlow SSO { "m.login.sso" };
- static inline const LoginFlow Token { "m.login.token" };
-};
-
class Connection;
using room_factory_t =
@@ -96,11 +89,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
@@ -109,9 +100,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
@@ -120,9 +111,9 @@ static inline user_factory_t defaultUserFactory()
// are stored with no regard to their state.
using DirectChatsMap = QMultiHash<const User*, QString>;
using DirectChatUsersMap = QMultiHash<QString, User*>;
-using IgnoredUsersList = IgnoredUsersEvent::content_type;
+using IgnoredUsersList = IgnoredUsersEvent::value_type;
-class Connection : public QObject {
+class QUOTIENT_API Connection : public QObject {
Q_OBJECT
Q_PROPERTY(User* localUser READ user NOTIFY stateChanged)
@@ -139,10 +130,10 @@ 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 =
- UnorderedMap<QString, UnorderedMap<QString, const Event&>>;
+ using UsersToDevicesToContent = QHash<QString, QHash<QString, QJsonObject>>;
enum RoomVisibility {
PublishRoom,
@@ -153,15 +144,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.
@@ -192,24 +174,25 @@ public:
*/
bool hasAccountData(const QString& type) const;
- /** Get a generic account data event of the given type
- * This returns an account data event of the given type
- * stored on the server. Direct chats map cannot be retrieved
- * using this method _yet_; use directChats() instead.
- */
+ //! \brief Get a generic account data event of the given type
+ //!
+ //! \return an account data event of the given type stored on the server,
+ //! or nullptr if there's none of that type.
+ //! \note Direct chats map cannot be retrieved using this method _yet_;
+ //! use directChats() instead.
const EventPtr& accountData(const QString& type) const;
- /** Get a generic account data event of the given type
- * This returns an account data event of the given type
- * stored on the server. Direct chats map cannot be retrieved
- * using this method _yet_; use directChats() instead.
- */
- template <typename EventT>
- const typename EventT::content_type accountData() const
+ //! \brief Get an account data event of the given type
+ //!
+ //! \return the account data content for the given event type stored
+ //! on the server, or a default-constructed object if there's none
+ //! of that type.
+ //! \note Direct chats map cannot be retrieved using this method _yet_;
+ //! use directChats() instead.
+ template <EventClass EventT>
+ const EventT* accountData() const
{
- if (const auto& eventPtr = accountData(EventT::matrixTypeId()))
- return eventPtr->content();
- return {};
+ return eventCast<EventT>(accountData(EventT::TypeId));
}
/** Get account data as a JSON object
@@ -334,7 +317,38 @@ public:
QByteArray accessToken() const;
bool isLoggedIn() const;
#ifdef Quotient_E2EE_ENABLED
- QtOlm::Account* olmAccount() const;
+ QOlmAccount* olmAccount() const;
+ Database* database() const;
+ PicklingMode picklingMode() const;
+
+ UnorderedMap<QString, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(
+ const Room* room) const;
+ void saveMegolmSession(const Room* room,
+ const QOlmInboundGroupSession& session) const;
+ QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(
+ const QString& roomId) const;
+ void saveCurrentOutboundMegolmSession(
+ const QString& roomId, const QOlmOutboundGroupSession& session) const;
+
+ QString edKeyForUserDevice(const QString& userId,
+ const QString& deviceId) const;
+ bool hasOlmSession(const QString& user, const QString& deviceId) const;
+
+ // This assumes that an olm session already exists. If it doesn't, no message is sent.
+ void sendToDevice(const QString& targetUserId, const QString& targetDeviceId,
+ const Event& event, bool encrypted);
+
+ /// Returns true if this megolm session comes from a verified device
+ bool isVerifiedSession(const QString& megolmSessionId) const;
+
+ void sendSessionKeyToDevices(const QString& roomId,
+ const QByteArray& sessionId,
+ const QByteArray& sessionKey,
+ const QMultiHash<QString, QString>& devices,
+ int index);
+
+ QJsonObject decryptNotification(const QJsonObject &notification);
+ QStringList devicesForUser(const QString& userId) const;
#endif // Quotient_E2EE_ENABLED
Q_INVOKABLE Quotient::SyncJob* syncJob() const;
Q_INVOKABLE int millisToReconnect() const;
@@ -368,22 +382,19 @@ public:
* \sa loadingCapabilities */
QVector<SupportedRoomVersion> availableRoomVersions() const;
+ /// Indicate if the user can change its password from the client.
+ /// This is often not the case when SSO is enabled.
+ /// \sa loadingCapabilities
+ bool canChangePassword() const;
+
/**
* Call this before first sync to load from previously saved file.
- *
- * \param fromFile A local path to read the state from. Uses QUrl
- * to be QML-friendly. Empty parameter means saving to the directory
- * defined by stateCachePath() / stateCacheDir().
*/
Q_INVOKABLE void loadState();
/**
* This method saves the current state of rooms (but not messages
* in them) to a local cache file, so that it could be loaded by
* loadState() on a next run of the client.
- *
- * \param toFile A local path to save the state to. Uses QUrl to be
- * QML-friendly. Empty parameter means saving to the directory
- * defined by stateCachePath() / stateCacheDir().
*/
Q_INVOKABLE void saveState() const;
@@ -418,7 +429,7 @@ public:
/*! Start a pre-created job object on this connection */
Q_INVOKABLE BaseJob* run(BaseJob* job,
- RunningPolicy runningPolicy = ForegroundRequest);
+ RunningPolicy runningPolicy = ForegroundRequest);
/*! Start a job of a specified type with specified arguments and policy
*
@@ -463,6 +474,17 @@ public:
std::forward<JobArgTs>(jobArgs)...);
}
+ //! \brief Start a local HTTP server and generate a single sign-on URL
+ //!
+ //! This call does the preparatory steps to carry out single sign-on
+ //! sequence
+ //! \sa https://matrix.org/docs/guides/sso-for-client-developers
+ //! \return A proxy object holding two URLs: one for SSO on the chosen
+ //! homeserver and another for the local callback address. Normally
+ //! you won't need the callback URL unless you proxy the response
+ //! with a custom UI. You do not need to delete the SsoSession
+ //! object; the Connection that issued it will dispose of it once
+ //! the login sequence completes (with any outcome).
Q_INVOKABLE SsoSession* prepareForSso(const QString& initialDeviceName,
const QString& deviceId = {});
@@ -487,21 +509,37 @@ 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 slots:
- /** Set the homeserver base URL */
+ /// Saves the olm account data to disk. Usually doesn't need to be called manually.
+ void saveOlmAccount();
+
+public Q_SLOTS:
+ /// \brief Set the homeserver base URL and retrieve its login flows
+ ///
+ /// \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
@@ -534,30 +572,12 @@ public 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);
@@ -566,6 +586,8 @@ public slots:
void stopSync();
QString nextBatchToken() const;
+ Q_INVOKABLE QUrl makeMediaUrl(QUrl mxcUrl) const;
+
virtual MediaThumbnailJob*
getThumbnail(const QString& mediaId, QSize requestedSize,
RunningPolicy policy = BackgroundRequest);
@@ -587,6 +609,11 @@ public slots:
DownloadFileJob* downloadFile(const QUrl& url,
const QString& localFilename = {});
+#ifdef Quotient_E2EE_ENABLED
+ DownloadFileJob* downloadFile(const QUrl& url,
+ const EncryptedFileMetadata& fileMetadata,
+ const QString& localFilename = {});
+#endif
/**
* \brief Create a room (generic method)
* This method allows to customize room entirely to your liking,
@@ -664,7 +691,7 @@ public slots:
ForgetRoomJob* forgetRoom(const QString& id);
SendToDeviceJob* sendToDevices(const QString& eventType,
- const UsersToDevicesToEvents& eventsMap);
+ const UsersToDevicesToContent& contents);
/** \deprecated This method is experimental and may be removed any time */
SendMessageJob* sendMessage(const QString& roomId, const RoomEvent& event);
@@ -672,24 +699,18 @@ public 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.
+#ifdef Quotient_E2EE_ENABLED
+ void startKeyVerificationSession(const QString& deviceId);
- /** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead
- */
- virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event);
+ void encryptionUpdate(Room *room);
+#endif
-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();
+Q_SIGNALS:
+ /// \brief Initial server resolution has failed
+ ///
+ /// This signal is emitted when resolveServer() did not manage to resolve
+ /// the homeserver using its .well-known/client record or otherwise.
+ /// \sa resolveServer
void resolveError(QString error);
void homeserverChanged(QUrl baseUrl);
@@ -697,7 +718,6 @@ signals:
void capabilitiesLoaded();
void connected();
- void reconnected(); //< \deprecated Use connected() instead
void loggedOut();
/** Login data or state have changed
*
@@ -841,6 +861,15 @@ signals:
void cacheStateChanged();
void lazyLoadingChanged();
void turnServersChanged(const QJsonObject& servers);
+ void devicesListLoaded();
+
+#ifdef Quotient_E2EE_ENABLED
+ void newKeyVerificationSession(KeyVerificationSession* session);
+ void keyVerificationStateChanged(
+ const KeyVerificationSession* session,
+ Quotient::KeyVerificationSession::State state);
+ void sessionVerified(const QString& userId, const QString& deviceId);
+#endif
protected:
/**
@@ -875,12 +904,12 @@ protected:
*/
void onSyncSuccess(SyncData&& data, bool fromCache = false);
-protected slots:
+protected Q_SLOTS:
void syncLoopIteration();
private:
class Private;
- QScopedPointer<Private> d;
+ ImplPtr<Private> d;
static room_factory_t _roomFactory;
static user_factory_t _userFactory;
diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp
index d57363d0..aca218be 100644
--- a/lib/connectiondata.cpp
+++ b/lib/connectiondata.cpp
@@ -1,20 +1,6 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "connectiondata.h"
@@ -55,7 +41,7 @@ public:
};
ConnectionData::ConnectionData(QUrl baseUrl)
- : d(std::make_unique<Private>(std::move(baseUrl)))
+ : d(makeImpl<Private>(std::move(baseUrl)))
{
// Each lambda invocation below takes no more than one job from the
// queues (first foreground, then background) and resumes it; then
@@ -132,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 000099d1..75fc332f 100644
--- a/lib/connectiondata.h
+++ b/lib/connectiondata.h
@@ -1,26 +1,13 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
+#include "util.h"
+
#include <QtCore/QUrl>
-#include <memory>
#include <chrono>
class QNetworkAccessManager;
@@ -45,10 +32,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);
@@ -60,6 +43,6 @@ public:
private:
class Private;
- std::unique_ptr<Private> d;
+ ImplPtr<Private> d;
};
} // namespace Quotient
diff --git a/lib/converters.cpp b/lib/converters.cpp
index 9f570087..b0e3a4b6 100644
--- a/lib/converters.cpp
+++ b/lib/converters.cpp
@@ -1,43 +1,41 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "converters.h"
+#include "logging.h"
#include <QtCore/QVariant>
-using namespace Quotient;
+void Quotient::_impl::warnUnknownEnumValue(const QString& stringValue,
+ const char* enumTypeName)
+{
+ qWarning(EVENTS).noquote()
+ << "Unknown" << enumTypeName << "value:" << stringValue;
+}
+
+void Quotient::_impl::reportEnumOutOfBounds(uint32_t v, const char* enumTypeName)
+{
+ qCritical(MAIN).noquote()
+ << "Value" << v << "is out of bounds for enumeration" << enumTypeName;
+}
-QJsonValue 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 543e9496..0fb36320 100644
--- a/lib/converters.h
+++ b/lib/converters.h
@@ -1,23 +1,9 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
+#include "omittable.h"
#include "util.h"
#include <QtCore/QDate>
@@ -28,163 +14,312 @@
#include <QtCore/QUrlQuery>
#include <QtCore/QVector>
+#include <type_traits>
#include <vector>
+#include <variant>
class QVariant;
namespace Quotient {
template <typename T>
struct JsonObjectConverter {
- static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod.toJson(); }
- static void fillFrom(const QJsonObject& jo, T& pod) { pod = T(jo); }
+ // To be implemented in specialisations
+ static void dumpTo(QJsonObject&, const T&) = delete;
+ static void fillFrom(const QJsonObject&, T&) = delete;
+};
+
+template <typename PodT, typename JsonT>
+PodT fromJson(const JsonT&);
+
+template <typename T>
+struct JsonObjectUnpacker {
+ // By default, revert to fromJson() so that one could provide a single
+ // fromJson<T, QJsonObject> specialisation instead of specialising
+ // the entire JsonConverter; if a different type of JSON value is needed
+ // (e.g., an array), specialising JsonConverter is inevitable
+ static T load(const QJsonValue& jv) { return fromJson<T>(jv.toObject()); }
+ static T load(const QJsonDocument& jd) { return fromJson<T>(jd.object()); }
};
+//! \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)
+struct JsonConverter : JsonObjectUnpacker<T> {
+ // Unfortunately, if constexpr doesn't work with dump() and T::toJson
+ // because trying to check invocability of T::toJson hits a hard
+ // (non-SFINAE) compilation error if the member is not there. Hence a bit
+ // more verbose SFINAE construct in _impl::JsonExporter.
+ static auto dump(const T& data)
{
- QJsonObject jo;
- JsonObjectConverter<T>::dumpTo(jo, pod);
- return jo;
+ if constexpr (requires() { data.toJson(); })
+ return data.toJson();
+ else {
+ QJsonObject jo;
+ JsonObjectConverter<T>::dumpTo(jo, data);
+ return jo;
+ }
}
- static T doLoad(const QJsonObject& jo)
+
+ using JsonObjectUnpacker<T>::load;
+ static T load(const QJsonObject& jo)
{
- T pod;
- JsonObjectConverter<T>::fillFrom(jo, pod);
- return pod;
+ // 'else' below are required to suppress code generation for unused
+ // branches - 'return' is not enough
+ if constexpr (std::is_same_v<T, QJsonObject>)
+ return jo;
+ else if constexpr (std::is_constructible_v<T, QJsonObject>)
+ return T(jo);
+ else {
+ T pod;
+ JsonObjectConverter<T>::fillFrom(jo, pod);
+ return pod;
+ }
}
- static T load(const QJsonValue& jv) { return doLoad(jv.toObject()); }
- static T load(const QJsonDocument& jd) { return doLoad(jd.object()); }
};
template <typename T>
inline auto toJson(const T& pod)
+// -> can return anything from which QJsonValue or, in some cases, QJsonDocument
+// is constructible
{
- return JsonConverter<T>::dump(pod);
+ if constexpr (std::is_constructible_v<QJsonValue, T>)
+ return pod; // No-op if QJsonValue can be directly constructed
+ else
+ return JsonConverter<T>::dump(pod);
}
-inline auto toJson(const QJsonObject& jo) { return jo; }
-
template <typename T>
inline void fillJson(QJsonObject& json, const T& data)
{
JsonObjectConverter<T>::dumpTo(json, data);
}
-template <typename T>
-inline T fromJson(const QJsonValue& jv)
+template <typename PodT, typename JsonT>
+inline PodT fromJson(const JsonT& json)
+{
+ // JsonT here can be whatever the respective JsonConverter specialisation
+ // accepts but by default it's QJsonValue, QJsonDocument, or QJsonObject
+ return JsonConverter<PodT>::load(json);
+}
+
+// Convenience fromJson() overload that deduces PodT instead of requiring
+// the coder to explicitly type it. It still enforces the
+// overwrite-everything semantics of fromJson(), unlike fillFromJson()
+
+template <typename JsonT, typename PodT>
+inline void fromJson(const JsonT& json, PodT& pod)
{
- return JsonConverter<T>::load(jv);
+ pod = fromJson<PodT>(json);
}
template <typename T>
-inline T fromJson(const QJsonDocument& jd)
+inline void fillFromJson(const QJsonValue& jv, T& pod)
{
- return JsonConverter<T>::load(jd);
+ if constexpr (requires() { JsonObjectConverter<T>::fillFrom({}, pod); }) {
+ JsonObjectConverter<T>::fillFrom(jv.toObject(), pod);
+ return;
+ } else if (!jv.isUndefined())
+ pod = fromJson<T>(jv);
}
-// Convenience fromJson() overloads that deduce T instead of requiring
-// the coder to explicitly type it. They still enforce the
-// overwrite-everything semantics of fromJson(), unlike fillFromJson()
+namespace _impl {
+ void warnUnknownEnumValue(const QString& stringValue,
+ const char* enumTypeName);
+ void reportEnumOutOfBounds(uint32_t v, const char* enumTypeName);
+}
-template <typename T>
-inline void fromJson(const QJsonValue& jv, T& pod)
+//! \brief Facility string-to-enum converter
+//!
+//! This is to simplify enum loading from JSON - just specialise
+//! Quotient::fromJson() and call this function from it, passing (aside from
+//! the JSON value for the enum - that must be a string, not an int) any
+//! iterable container of string'y values (const char*, QLatin1String, etc.)
+//! matching respective enum values, 0-based.
+//! \sa enumToJsonString
+template <typename EnumT, typename EnumStringValuesT>
+EnumT enumFromJsonString(const QString& s, const EnumStringValuesT& enumValues,
+ EnumT defaultValue)
{
- pod = jv.isUndefined() ? T() : fromJson<T>(jv);
+ static_assert(std::is_unsigned_v<std::underlying_type_t<EnumT>>);
+ if (const auto it = std::find(cbegin(enumValues), cend(enumValues), s);
+ it != cend(enumValues))
+ return EnumT(it - cbegin(enumValues));
+
+ if (!s.isEmpty())
+ _impl::warnUnknownEnumValue(s, qt_getEnumName(EnumT()));
+ return defaultValue;
}
-template <typename T>
-inline void fromJson(const QJsonDocument& jd, T& pod)
+//! \brief Facility enum-to-string converter
+//!
+//! This does the same as enumFromJsonString, the other way around.
+//! \note The source enumeration must not have gaps in values, or \p enumValues
+//! has to match those gaps (i.e., if the source enumeration is defined
+//! as <tt>{ Value1 = 1, Value2 = 3, Value3 = 5 }</tt> then \p enumValues
+//! should be defined as <tt>{ "", "Value1", "", "Value2", "", "Value3"
+//! }</tt> (mind the gap at value 0, in particular).
+//! \sa enumFromJsonString
+template <typename EnumT, typename EnumStringValuesT>
+QString enumToJsonString(EnumT v, const EnumStringValuesT& enumValues)
{
- pod = fromJson<T>(jd);
+ static_assert(std::is_unsigned_v<std::underlying_type_t<EnumT>>);
+ if (v < size(enumValues))
+ return enumValues[v];
+
+ _impl::reportEnumOutOfBounds(static_cast<uint32_t>(v),
+ qt_getEnumName(EnumT()));
+ Q_ASSERT(false);
+ return {};
}
-template <typename T>
-inline void fillFromJson(const QJsonValue& jv, T& pod)
+//! \brief Facility converter for flags
+//!
+//! This is very similar to enumFromJsonString, except that the target
+//! enumeration is assumed to be of a 'flag' kind - i.e. its values must be
+//! a power-of-two sequence starting from 1, without gaps, so exactly 1,2,4,8,16
+//! and so on.
+//! \note Unlike enumFromJsonString, the values start from 1 and not from 0,
+//! with 0 being used for an invalid value by default.
+//! \note This function does not support flag combinations.
+//! \sa QUO_DECLARE_FLAGS, QUO_DECLARE_FLAGS_NS
+template <typename FlagT, typename FlagStringValuesT>
+FlagT flagFromJsonString(const QString& s, const FlagStringValuesT& flagValues,
+ FlagT defaultValue = FlagT(0U))
{
- if (jv.isObject())
- JsonObjectConverter<T>::fillFrom(jv.toObject(), pod);
- else if (!jv.isUndefined())
- pod = fromJson<T>(jv);
+ // Enums based on signed integers don't make much sense for flag types
+ static_assert(std::is_unsigned_v<std::underlying_type_t<FlagT>>);
+ if (const auto it = std::find(cbegin(flagValues), cend(flagValues), s);
+ it != cend(flagValues))
+ return FlagT(1U << (it - cbegin(flagValues)));
+
+ if (!s.isEmpty())
+ _impl::warnUnknownEnumValue(s, qt_getEnumName(FlagT()));
+ return defaultValue;
}
-// JsonConverter<> specialisations
+template <typename FlagT, typename FlagStringValuesT>
+QString flagToJsonString(FlagT v, const FlagStringValuesT& flagValues)
+{
+ static_assert(std::is_unsigned_v<std::underlying_type_t<FlagT>>);
+ if (const auto offset =
+ qCountTrailingZeroBits(std::underlying_type_t<FlagT>(v));
+ offset < size(flagValues)) //
+ {
+ return flagValues[offset];
+ }
-template <typename T>
-struct TrivialJsonDumper {
- // Works for: QJsonValue (and all things it can consume),
- // QJsonObject, QJsonArray
- static auto dump(const T& val) { return val; }
-};
+ _impl::reportEnumOutOfBounds(static_cast<uint32_t>(v),
+ qt_getEnumName(FlagT()));
+ Q_ASSERT(false);
+ return {};
+}
+
+// Specialisations
+
+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(); }
+//! Use fromJson<QString> and then toLatin1()/toUtf8()/... to make QByteArray
+//!
+//! QJsonValue can only convert to QString and there's ambiguity whether
+//! conversion to QByteArray should use (fast but very limited) toLatin1() or
+//! (all encompassing and conforming to the JSON spec but slow) toUtf8().
template <>
-struct JsonConverter<QString> : public TrivialJsonDumper<QString> {
- static auto load(const QJsonValue& jv) { return jv.toString(); }
-};
+inline QByteArray fromJson(const QJsonValue& jv) = delete;
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 QJsonValue& jv) { return jv.toArray(); }
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 QJsonArray fromJson(const QJsonDocument& jd) { return jd.array(); }
+inline QJsonValue toJson(const QDateTime& val)
+{
+ return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue();
+}
template <>
-struct JsonConverter<QJsonArray> : public TrivialJsonDumper<QJsonArray> {
- static auto load(const QJsonValue& jv) { return jv.toArray(); }
-};
+inline QDateTime fromJson(const QJsonValue& jv)
+{
+ return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC);
+}
+inline QJsonValue toJson(const QDate& val) { return toJson(val.startOfDay()); }
template <>
-struct JsonConverter<QByteArray> {
- static QString dump(const QByteArray& ba) { return ba.constData(); }
+inline QDate fromJson(const QJsonValue& jv)
+{
+ return fromJson<QDateTime>(jv).date();
+}
+
+// Insulate QVariant and QUrl conversions into JsonConverter so that they don't
+// interfere with toJson(const QJsonValue&) over QString, since both types are
+// constructible from QString (even if QUrl requires explicit construction).
+
+template <>
+struct JsonConverter<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);
}
};
template <>
-struct JsonConverter<QVariant> {
+struct QUOTIENT_API JsonConverter<QVariant> {
static QJsonValue dump(const QVariant& v);
static QVariant load(const QJsonValue& jv);
};
+template <typename... Ts>
+inline QJsonValue toJson(const std::variant<Ts...>& v)
+{
+ // std::visit requires all overloads to return the same type - and
+ // QJsonValue is a perfect candidate for that same type (assuming that
+ // variants never occur on the top level in Matrix API)
+ return std::visit(
+ [](const auto& value) { return QJsonValue { toJson(value) }; }, v);
+}
+
+template <typename T>
+struct JsonConverter<std::variant<QString, T>> {
+ static std::variant<QString, T> load(const QJsonValue& jv)
+ {
+ if (jv.isString())
+ return fromJson<QString>(jv);
+ return fromJson<T>(jv);
+ }
+};
+
template <typename T>
struct JsonConverter<Omittable<T>> {
static QJsonValue dump(const Omittable<T>& from)
@@ -201,23 +336,23 @@ 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)
{
VectorT vect;
vect.reserve(typename VectorT::size_type(ja.size()));
- for (const auto& i : ja)
- vect.push_back(fromJson<T>(i));
+ // NB: Make sure to pass QJsonValue to fromJson<> so that it could
+ // hit the correct overload and not fall back to the generic fromJson
+ // that treats everything as an object. See also the explanation in
+ // the commit introducing these lines.
+ for (const QJsonValue v : ja)
+ vect.push_back(fromJson<T>(v));
return vect;
}
static auto load(const QJsonValue& jv) { return load(jv.toArray()); }
@@ -228,14 +363,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);
@@ -247,14 +384,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;
}
};
@@ -267,9 +403,12 @@ struct HashMapFromJson {
}
static void fillFrom(const QJsonObject& jo, HashMapT& h)
{
- h.reserve(jo.size());
+ h.reserve(h.size() + jo.size());
+ // NB: the QJsonValue cast below is for the same reason as in
+ // JsonArrayConverter
for (auto it = jo.begin(); it != jo.end(); ++it)
- h[it.key()] = fromJson<typename HashMapT::mapped_type>(it.value());
+ h[it.key()] = fromJson<typename HashMapT::mapped_type>(
+ QJsonValue(it.value()));
}
};
@@ -281,14 +420,14 @@ template <typename T>
struct JsonObjectConverter<QHash<QString, T>>
: public HashMapFromJson<QHash<QString, T>> {};
+QJsonObject QUOTIENT_API toJson(const QVariantHash& vh);
template <>
-struct JsonConverter<QVariantHash> {
- static QJsonObject dump(const QVariantHash& vh);
- static QVariantHash load(const QJsonValue& jv);
-};
+QVariantHash QUOTIENT_API fromJson(const QJsonValue& jv);
// Conditional insertion into a QJsonObject
+constexpr bool IfNotEmpty = false;
+
namespace _impl {
template <typename ValT>
inline void addTo(QJsonObject& o, const QString& k, ValT&& v)
@@ -309,16 +448,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
@@ -335,7 +473,7 @@ namespace _impl {
// This one is for types that have isEmpty() when Force is false
template <typename ValT>
- struct AddNode<ValT, false, decltype(std::declval<ValT>().isEmpty())> {
+ struct AddNode<ValT, IfNotEmpty, decltype(std::declval<ValT>().isEmpty())> {
template <typename ContT, typename ForwardedT>
static void impl(ContT& container, const QString& key,
ForwardedT&& value)
@@ -345,9 +483,9 @@ namespace _impl {
}
};
- // This one unfolds Omittable<> (also only when Force is false)
+ // This one unfolds Omittable<> (also only when IfNotEmpty is requested)
template <typename ValT>
- struct AddNode<Omittable<ValT>, false> {
+ struct AddNode<Omittable<ValT>, IfNotEmpty> {
template <typename ContT, typename OmittableT>
static void impl(ContT& container, const QString& key,
const OmittableT& value)
@@ -358,8 +496,6 @@ namespace _impl {
};
} // namespace _impl
-static constexpr bool IfNotEmpty = false;
-
/*! Add a key-value pair to QJsonObject or QUrlQuery
*
* Adds a key-value pair(s) specified by \p key and \p value to
@@ -389,4 +525,20 @@ inline void addParam(ContT& container, const QString& key, ValT&& value)
_impl::AddNode<std::decay_t<ValT>, Force>::impl(container, key,
std::forward<ValT>(value));
}
+
+// This is a facility function to convert camelCase method/variable names
+// used throughout Quotient to snake_case JSON keys - see usage in
+// single_key_value.h and event.h (DEFINE_CONTENT_GETTER macro).
+inline auto toSnakeCase(QLatin1String s)
+{
+ QString result { s };
+ for (auto it = result.begin(); it != result.end(); ++it)
+ if (it->isUpper()) {
+ const auto offset = static_cast<int>(it - result.begin());
+ result.insert(offset, '_'); // NB: invalidates iterators
+ it = result.begin() + offset + 1;
+ *it = it->toLower();
+ }
+ return result;
+}
} // namespace Quotient
diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp
index 6a40e908..8c71f6c5 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/v3", "/user/", userId, "/account_data/",
+ type))
{
- setRequestData(Data(toJson(content)));
+ setRequestData({ 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/v3", "/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/v3", "/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/v3", "/user/", userId, "/rooms/",
+ roomId, "/account_data/", type))
{
- setRequestData(Data(toJson(content)));
+ setRequestData({ 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/v3", "/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/v3", "/user/", userId, "/rooms/",
+ roomId, "/account_data/", type))
{}
diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h
index 0c760e80..70d4e492 100644
--- a/lib/csapi/account-data.h
+++ b/lib/csapi/account-data.h
@@ -8,46 +8,47 @@
namespace Quotient {
-/*! \brief Set some account_data for the user.
+/*! \brief Set some account data for the user.
*
- * Set some account_data for the client. This config is only visible to the user
- * that set the account_data. The config will be synced to clients in the
- * top-level `account_data`.
+ * Set some account data for the client. This config is only visible to the user
+ * that set the account data. The config will be available to clients through
+ * the top-level `account_data` field in the homeserver response to
+ * [/sync](#get_matrixclientv3sync).
*/
-class SetAccountDataJob : public BaseJob {
+class QUOTIENT_API SetAccountDataJob : public BaseJob {
public:
- /*! \brief Set some account_data for the user.
+ /*! \brief Set some account data for the user.
*
* \param userId
- * The ID of the user to set account_data for. The access token must be
+ * The ID of the user to set account data for. The access token must be
* authorized to make requests for this user ID.
*
* \param type
- * The event type of the account_data to set. Custom types should be
+ * The event type of the account data to set. Custom types should be
* namespaced to avoid clashes.
*
* \param content
- * The content of the account_data
+ * The content of the account data.
*/
explicit SetAccountDataJob(const QString& userId, const QString& type,
const QJsonObject& content = {});
};
-/*! \brief Get some account_data for the user.
+/*! \brief Get some account data for the user.
*
- * Get some account_data for the client. This config is only visible to the user
- * that set the account_data.
+ * Get some account data for the client. This config is only visible to the user
+ * that set the account data.
*/
-class GetAccountDataJob : public BaseJob {
+class QUOTIENT_API GetAccountDataJob : public BaseJob {
public:
- /*! \brief Get some account_data for the user.
+ /*! \brief Get some account data for the user.
*
* \param userId
- * The ID of the user to get account_data for. The access token must be
+ * The ID of the user to get account data for. The access token must be
* authorized to make requests for this user ID.
*
* \param type
- * The event type of the account_data to get. Custom types should be
+ * The event type of the account data to get. Custom types should be
* namespaced to avoid clashes.
*/
explicit GetAccountDataJob(const QString& userId, const QString& type);
@@ -61,53 +62,53 @@ public:
const QString& type);
};
-/*! \brief Set some account_data for the user.
+/*! \brief Set some account data for the user that is specific to a room.
*
- * Set some account_data for the client on a given room. This config is only
- * visible to the user that set the account_data. The config will be synced to
- * clients in the per-room `account_data`.
+ * Set some account data for the client on a given room. This config is only
+ * visible to the user that set the account data. The config will be delivered
+ * to clients in the per-room entries via [/sync](#get_matrixclientv3sync).
*/
-class SetAccountDataPerRoomJob : public BaseJob {
+class QUOTIENT_API SetAccountDataPerRoomJob : public BaseJob {
public:
- /*! \brief Set some account_data for the user.
+ /*! \brief Set some account data for the user that is specific to a room.
*
* \param userId
- * The ID of the user to set account_data for. The access token must be
+ * The ID of the user to set account data for. The access token must be
* authorized to make requests for this user ID.
*
* \param roomId
- * The ID of the room to set account_data on.
+ * The ID of the room to set account data on.
*
* \param type
- * The event type of the account_data to set. Custom types should be
+ * The event type of the account data to set. Custom types should be
* namespaced to avoid clashes.
*
* \param content
- * The content of the account_data
+ * The content of the account data.
*/
explicit SetAccountDataPerRoomJob(const QString& userId,
const QString& roomId, const QString& type,
const QJsonObject& content = {});
};
-/*! \brief Get some account_data for the user.
+/*! \brief Get some account data for the user that is specific to a room.
*
- * Get some account_data for the client on a given room. This config is only
- * visible to the user that set the account_data.
+ * Get some account data for the client on a given room. This config is only
+ * visible to the user that set the account data.
*/
-class GetAccountDataPerRoomJob : public BaseJob {
+class QUOTIENT_API GetAccountDataPerRoomJob : public BaseJob {
public:
- /*! \brief Get some account_data for the user.
+ /*! \brief Get some account data for the user that is specific to a room.
*
* \param userId
- * The ID of the user to set account_data for. The access token must be
+ * The ID of the user to get account data for. The access token must be
* authorized to make requests for this user ID.
*
* \param roomId
- * The ID of the room to get account_data for.
+ * The ID of the room to get account data for.
*
* \param type
- * The event type of the account_data to get. Custom types should be
+ * The event type of the account data to get. Custom types should be
* namespaced to avoid clashes.
*/
explicit GetAccountDataPerRoomJob(const QString& userId,
diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp
index 9619c441..322212db 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/v3",
+ "/admin/whois/", userId));
}
GetWhoIsJob::GetWhoIsJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetWhoIsJob"),
- QStringLiteral("/_matrix/client/r0") % "/admin/whois/" % userId)
+ makePath("/_matrix/client/v3", "/admin/whois/", userId))
{}
diff --git a/lib/csapi/admin.h b/lib/csapi/admin.h
index 570bf24a..c53ddd7e 100644
--- a/lib/csapi/admin.h
+++ b/lib/csapi/admin.h
@@ -16,7 +16,7 @@ namespace Quotient {
* up, or by a server admin. Server-local administrator privileges are not
* specified in this document.
*/
-class GetWhoIsJob : public BaseJob {
+class QUOTIENT_API GetWhoIsJob : public BaseJob {
public:
// Inner data structures
diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp
index fa4f475a..aa55d934 100644
--- a/lib/csapi/administrative_contact.cpp
+++ b/lib/csapi/administrative_contact.cpp
@@ -4,67 +4,64 @@
#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/v3", "/account/3pid"));
}
GetAccount3PIDsJob::GetAccount3PIDsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetAccount3PIDsJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid")
+ makePath("/_matrix/client/v3", "/account/3pid"))
{}
Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds)
: BaseJob(HttpVerb::Post, QStringLiteral("Post3PIDsJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid")
+ makePath("/_matrix/client/v3", "/account/3pid"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("three_pid_creds"), threePidCreds);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("three_pid_creds"), threePidCreds);
+ setRequestData({ _dataJson });
}
Add3PIDJob::Add3PIDJob(const QString& clientSecret, const QString& sid,
const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Post, QStringLiteral("Add3PIDJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid/add")
+ makePath("/_matrix/client/v3", "/account/3pid/add"))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
- addParam<>(_data, QStringLiteral("client_secret"), clientSecret);
- addParam<>(_data, QStringLiteral("sid"), sid);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth);
+ addParam<>(_dataJson, QStringLiteral("client_secret"), clientSecret);
+ addParam<>(_dataJson, QStringLiteral("sid"), sid);
+ setRequestData({ _dataJson });
}
Bind3PIDJob::Bind3PIDJob(const QString& clientSecret, const QString& idServer,
const QString& idAccessToken, const QString& sid)
: BaseJob(HttpVerb::Post, QStringLiteral("Bind3PIDJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid/bind")
+ makePath("/_matrix/client/v3", "/account/3pid/bind"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("client_secret"), clientSecret);
- addParam<>(_data, QStringLiteral("id_server"), idServer);
- addParam<>(_data, QStringLiteral("id_access_token"), idAccessToken);
- addParam<>(_data, QStringLiteral("sid"), sid);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("client_secret"), clientSecret);
+ addParam<>(_dataJson, QStringLiteral("id_server"), idServer);
+ addParam<>(_dataJson, QStringLiteral("id_access_token"), idAccessToken);
+ addParam<>(_dataJson, QStringLiteral("sid"), sid);
+ setRequestData({ _dataJson });
}
Delete3pidFromAccountJob::Delete3pidFromAccountJob(const QString& medium,
const QString& address,
const QString& idServer)
: BaseJob(HttpVerb::Post, QStringLiteral("Delete3pidFromAccountJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/3pid/delete")
+ makePath("/_matrix/client/v3", "/account/3pid/delete"))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer);
- addParam<>(_data, QStringLiteral("medium"), medium);
- addParam<>(_data, QStringLiteral("address"), address);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("id_server"), idServer);
+ addParam<>(_dataJson, QStringLiteral("medium"), medium);
+ addParam<>(_dataJson, QStringLiteral("address"), address);
+ setRequestData({ _dataJson });
addExpectedKey("id_server_unbind_result");
}
@@ -72,32 +69,32 @@ 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/v3", "/account/3pid/unbind"))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer);
- addParam<>(_data, QStringLiteral("medium"), medium);
- addParam<>(_data, QStringLiteral("address"), address);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("id_server"), idServer);
+ addParam<>(_dataJson, QStringLiteral("medium"), medium);
+ addParam<>(_dataJson, QStringLiteral("address"), address);
+ setRequestData({ _dataJson });
addExpectedKey("id_server_unbind_result");
}
RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob(
const EmailValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDEmailJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/account/3pid/email/requestToken",
+ makePath("/_matrix/client/v3",
+ "/account/3pid/email/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData({ toJson(body) });
}
RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob(
const MsisdnValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDMSISDNJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/account/3pid/msisdn/requestToken",
+ makePath("/_matrix/client/v3",
+ "/account/3pid/msisdn/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData({ toJson(body) });
}
diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h
index e436971d..27334850 100644
--- a/lib/csapi/administrative_contact.h
+++ b/lib/csapi/administrative_contact.h
@@ -24,7 +24,7 @@ namespace Quotient {
* Identifiers in this list may be used by the homeserver as, for example,
* identifiers that it will accept to reset the user's account password.
*/
-class GetAccount3PIDsJob : public BaseJob {
+class QUOTIENT_API GetAccount3PIDsJob : public BaseJob {
public:
// Inner data structures
@@ -102,7 +102,7 @@ struct JsonObjectConverter<GetAccount3PIDsJob::ThirdPartyIdentifier> {
* This results in this endpoint being an equivalent to `/3pid/bind` rather
* than dual-purpose.
*/
-class Post3PIDsJob : public BaseJob {
+class QUOTIENT_API Post3PIDsJob : public BaseJob {
public:
// Inner data structures
@@ -128,6 +128,22 @@ public:
* The third party credentials to associate with the account.
*/
explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds);
+
+ // Result properties
+
+ /// An optional field containing a URL where the client must
+ /// submit the validation token to, with identical parameters
+ /// to the Identity Service API's `POST
+ /// /validate/email/submitToken` endpoint (without the requirement
+ /// for an access token). The homeserver must send this token to the
+ /// user (if applicable), who should then be prompted to provide it
+ /// to the client.
+ ///
+ /// If this field is not present, the client can assume that
+ /// verification will happen without the client's involvement
+ /// provided the homeserver advertises this specification version
+ /// in the `/versions` response (ie: r0.5.0).
+ QUrl submitUrl() const { return loadFromJson<QUrl>("submit_url"_ls); }
};
template <>
@@ -154,7 +170,7 @@ struct JsonObjectConverter<Post3PIDsJob::ThreePidCredentials> {
* Homeservers should prevent the caller from adding a 3PID to their account if
* it has already been added to another user's account on the homeserver.
*/
-class Add3PIDJob : public BaseJob {
+class QUOTIENT_API Add3PIDJob : public BaseJob {
public:
/*! \brief Adds contact information to the user's account.
*
@@ -182,7 +198,7 @@ public:
*
* Homeservers should track successful binds so they can be unbound later.
*/
-class Bind3PIDJob : public BaseJob {
+class QUOTIENT_API Bind3PIDJob : public BaseJob {
public:
/*! \brief Binds a 3PID to the user's account through an Identity Service.
*
@@ -211,7 +227,7 @@ public:
* parameter because the homeserver is expected to sign the request to the
* identity server instead.
*/
-class Delete3pidFromAccountJob : public BaseJob {
+class QUOTIENT_API Delete3pidFromAccountJob : public BaseJob {
public:
/*! \brief Deletes a third party identifier from the user's account
*
@@ -235,7 +251,7 @@ public:
/// An indicator as to whether or not the homeserver was able to unbind
/// the 3PID from the identity server. `success` indicates that the
- /// indentity server has unbound the identifier whereas `no-support`
+ /// identity server has unbound the identifier whereas `no-support`
/// indicates that the identity server refuses to support the request
/// or the homeserver was not able to determine an identity server to
/// unbind from.
@@ -254,7 +270,7 @@ public:
* parameter because the homeserver is expected to sign the request to the
* identity server instead.
*/
-class Unbind3pidFromAccountJob : public BaseJob {
+class QUOTIENT_API Unbind3pidFromAccountJob : public BaseJob {
public:
/*! \brief Removes a user's third party identifier from an identity server.
*
@@ -295,12 +311,12 @@ public:
* be used to request validation tokens when adding an email address to an
* account. This API's parameters and response are identical to that of
* the
- * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken)
+ * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken)
* endpoint. The homeserver should validate
* the email itself, either by sending a validation email itself or by using
* a service it has control over.
*/
-class RequestTokenTo3PIDEmailJob : public BaseJob {
+class QUOTIENT_API RequestTokenTo3PIDEmailJob : public BaseJob {
public:
/*! \brief Begins the validation process for an email address for
* association with the user's account.
@@ -311,7 +327,7 @@ public:
* be used to request validation tokens when adding an email address to an
* account. This API's parameters and response are identical to that of
* the
- * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken)
+ * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken)
* endpoint. The homeserver should validate
* the email itself, either by sending a validation email itself or by
* using a service it has control over.
@@ -337,12 +353,12 @@ public:
* be used to request validation tokens when adding a phone number to an
* account. This API's parameters and response are identical to that of
* the
- * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken)
+ * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken)
* endpoint. The homeserver should validate
* the phone number itself, either by sending a validation message itself or by
* using a service it has control over.
*/
-class RequestTokenTo3PIDMSISDNJob : public BaseJob {
+class QUOTIENT_API RequestTokenTo3PIDMSISDNJob : public BaseJob {
public:
/*! \brief Begins the validation process for a phone number for association
* with the user's account.
@@ -353,7 +369,7 @@ public:
* be used to request validation tokens when adding a phone number to an
* account. This API's parameters and response are identical to that of
* the
- * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken)
+ * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken)
* endpoint. The homeserver should validate
* the phone number itself, either by sending a validation message itself
* or by using a service it has control over.
diff --git a/lib/csapi/appservice_room_directory.cpp b/lib/csapi/appservice_room_directory.cpp
index e8ec55bf..dff7e032 100644
--- a/lib/csapi/appservice_room_directory.cpp
+++ b/lib/csapi/appservice_room_directory.cpp
@@ -4,18 +4,18 @@
#include "appservice_room_directory.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
-UpdateAppserviceRoomDirectoryVsibilityJob::UpdateAppserviceRoomDirectoryVsibilityJob(
- const QString& networkId, const QString& roomId, const QString& visibility)
+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/v3", "/directory/list/appservice/",
+ networkId, "/", roomId))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("visibility"), visibility);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("visibility"), visibility);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/appservice_room_directory.h b/lib/csapi/appservice_room_directory.h
index 2631f38c..d6268979 100644
--- a/lib/csapi/appservice_room_directory.h
+++ b/lib/csapi/appservice_room_directory.h
@@ -21,7 +21,7 @@ namespace Quotient {
* instead of a typical client's access_token. This API cannot be invoked by
* users who are not identified as application services.
*/
-class UpdateAppserviceRoomDirectoryVsibilityJob : public BaseJob {
+class QUOTIENT_API 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..e04075b7 100644
--- a/lib/csapi/banning.cpp
+++ b/lib/csapi/banning.cpp
@@ -4,27 +4,26 @@
#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/v3", "/rooms/", roomId, "/ban"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("user_id"), userId);
- addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("user_id"), userId);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason);
+ setRequestData({ _dataJson });
}
-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/v3", "/rooms/", roomId, "/unban"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("user_id"), userId);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("user_id"), userId);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/banning.h b/lib/csapi/banning.h
index 48c054c2..e4c60ce3 100644
--- a/lib/csapi/banning.h
+++ b/lib/csapi/banning.h
@@ -18,7 +18,7 @@ namespace Quotient {
* The caller must have the required power level in order to perform this
* operation.
*/
-class BanJob : public BaseJob {
+class QUOTIENT_API BanJob : public BaseJob {
public:
/*! \brief Ban a user in the room.
*
@@ -46,7 +46,7 @@ public:
* The caller must have the required power level in order to perform this
* operation.
*/
-class UnbanJob : public BaseJob {
+class QUOTIENT_API UnbanJob : public BaseJob {
public:
/*! \brief Unban a user from the room.
*
@@ -55,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..ca2a543f 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/v3", "/capabilities"));
}
GetCapabilitiesJob::GetCapabilitiesJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetCapabilitiesJob"),
- QStringLiteral("/_matrix/client/r0") % "/capabilities")
+ makePath("/_matrix/client/v3", "/capabilities"))
{
addExpectedKey("capabilities");
}
diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h
index da50c8c1..81b47cd4 100644
--- a/lib/csapi/capabilities.h
+++ b/lib/csapi/capabilities.h
@@ -13,7 +13,7 @@ namespace Quotient {
* Gets information about the server's supported feature set
* and other relevant capabilities.
*/
-class GetCapabilitiesJob : public BaseJob {
+class QUOTIENT_API GetCapabilitiesJob : public BaseJob {
public:
// Inner data structures
diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp
index 7ae89739..6f6738af 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/v3", "/upload"),
queryToUploadContent(filename))
{
setRequestHeader("Content-Type", contentType.toLatin1());
- setRequestData(Data(content));
+ setRequestData({ 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/v3", "/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/v3", "/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/v3", "/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/v3", "/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,55 +101,54 @@ QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl,
{
return BaseJob::makeRequestUrl(
std::move(baseUrl),
- QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName % "/"
- % mediaId,
+ makePath("/_matrix/media/v3", "/thumbnail/", serverName, "/", mediaId),
queryToGetContentThumbnail(width, height, method, allowRemote));
}
GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName,
- const QString& mediaId, int width,
- int height, const QString& method,
+ const QString& mediaId,
+ int width, int height,
+ const QString& method,
bool allowRemote)
: BaseJob(HttpVerb::Get, QStringLiteral("GetContentThumbnailJob"),
- QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName
- % "/" % mediaId,
+ makePath("/_matrix/media/v3", "/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/v3",
+ "/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/v3", "/preview_url"),
queryToGetUrlPreview(url, ts))
{}
QUrl GetConfigJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/media/r0")
- % "/config");
+ makePath("/_matrix/media/v3", "/config"));
}
GetConfigJob::GetConfigJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetConfigJob"),
- QStringLiteral("/_matrix/media/r0") % "/config")
+ makePath("/_matrix/media/v3", "/config"))
{}
diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h
index f3d7309a..2ba66a35 100644
--- a/lib/csapi/content-repo.h
+++ b/lib/csapi/content-repo.h
@@ -14,7 +14,7 @@ namespace Quotient {
/*! \brief Upload some content to the content repository.
*
*/
-class UploadContentJob : public BaseJob {
+class QUOTIENT_API UploadContentJob : public BaseJob {
public:
/*! \brief Upload some content to the content repository.
*
@@ -34,16 +34,13 @@ public:
/// The [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the
/// uploaded content.
- QString contentUri() const
- {
- return loadFromJson<QString>("content_uri"_ls);
- }
+ QUrl contentUri() const { return loadFromJson<QUrl>("content_uri"_ls); }
};
/*! \brief Download content from the content repository.
*
*/
-class GetContentJob : public BaseJob {
+class QUOTIENT_API GetContentJob : public BaseJob {
public:
/*! \brief Download content from the content repository.
*
@@ -90,7 +87,7 @@ public:
* the previous endpoint) but replace the target file name with the one
* provided by the caller.
*/
-class GetContentOverrideNameJob : public BaseJob {
+class QUOTIENT_API GetContentOverrideNameJob : public BaseJob {
public:
/*! \brief Download content from the content repository overriding the file
* name
@@ -145,7 +142,7 @@ public:
* See the [Thumbnails](/client-server-api/#thumbnails) section for more
* information.
*/
-class GetContentThumbnailJob : public BaseJob {
+class QUOTIENT_API GetContentThumbnailJob : public BaseJob {
public:
/*! \brief Download a thumbnail of content from the content repository
*
@@ -165,7 +162,8 @@ public:
*
* \param method
* The desired resizing method. See the
- * [Thumbnails](/client-server-api/#thumbnails) section for more information.
+ * [Thumbnails](/client-server-api/#thumbnails) section for more
+ * information.
*
* \param allowRemote
* Indicates to the server that it should not attempt to fetch
@@ -207,7 +205,7 @@ public:
* do not want to share with the homeserver, and this can mean that the URLs
* being shared should also not be shared with the homeserver.
*/
-class GetUrlPreviewJob : public BaseJob {
+class QUOTIENT_API GetUrlPreviewJob : public BaseJob {
public:
/*! \brief Get information about a URL for a client
*
@@ -219,14 +217,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
@@ -239,7 +237,7 @@ public:
/// An [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the image.
/// Omitted if there is no image.
- QString ogImage() const { return loadFromJson<QString>("og:image"_ls); }
+ QUrl ogImage() const { return loadFromJson<QUrl>("og:image"_ls); }
};
/*! \brief Get the configuration for the content repository.
@@ -255,7 +253,7 @@ public:
* content repository APIs, for example, proxies may enforce a lower upload size
* limit than is advertised by the server on this endpoint.
*/
-class GetConfigJob : public BaseJob {
+class QUOTIENT_API GetConfigJob : public BaseJob {
public:
/// Get the configuration for the content repository.
explicit GetConfigJob();
diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp
index a94f9951..afae80af 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,24 +16,26 @@ 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/v3", "/createRoom"))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility);
- addParam<IfNotEmpty>(_data, QStringLiteral("room_alias_name"),
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("visibility"), visibility);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("room_alias_name"),
roomAliasName);
- addParam<IfNotEmpty>(_data, QStringLiteral("name"), name);
- addParam<IfNotEmpty>(_data, QStringLiteral("topic"), topic);
- addParam<IfNotEmpty>(_data, QStringLiteral("invite"), invite);
- addParam<IfNotEmpty>(_data, QStringLiteral("invite_3pid"), invite3pid);
- addParam<IfNotEmpty>(_data, QStringLiteral("room_version"), roomVersion);
- addParam<IfNotEmpty>(_data, QStringLiteral("creation_content"),
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("name"), name);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("topic"), topic);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("invite"), invite);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("invite_3pid"), invite3pid);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("room_version"), roomVersion);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("creation_content"),
creationContent);
- addParam<IfNotEmpty>(_data, QStringLiteral("initial_state"), initialState);
- addParam<IfNotEmpty>(_data, QStringLiteral("preset"), preset);
- addParam<IfNotEmpty>(_data, QStringLiteral("is_direct"), isDirect);
- addParam<IfNotEmpty>(_data, QStringLiteral("power_level_content_override"),
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("initial_state"),
+ initialState);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("preset"), preset);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("is_direct"), isDirect);
+ addParam<IfNotEmpty>(_dataJson,
+ QStringLiteral("power_level_content_override"),
powerLevelContentOverride);
- setRequestData(std::move(_data));
+ setRequestData({ _dataJson });
addExpectedKey("room_id");
}
diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h
index 81dfbffc..336b9767 100644
--- a/lib/csapi/create_room.h
+++ b/lib/csapi/create_room.h
@@ -26,16 +26,18 @@ namespace Quotient {
* (and not other members) permission to send state events. Overridden
* by the `power_level_content_override` parameter.
*
- * 4. Events set by the `preset`. Currently these are the `m.room.join_rules`,
+ * 4. An `m.room.canonical_alias` event if `room_alias_name` is given.
+ *
+ * 5. Events set by the `preset`. Currently these are the `m.room.join_rules`,
* `m.room.history_visibility`, and `m.room.guest_access` state events.
*
- * 5. Events listed in `initial_state`, in the order that they are
+ * 6. Events listed in `initial_state`, in the order that they are
* listed.
*
- * 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic`
+ * 7. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic`
* state events).
*
- * 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member` with
+ * 8. Invite events implied by `invite` and `invite_3pid` (`m.room.member` with
* `membership: invite` and `m.room.third_party_invite`).
*
* The available presets do the following with respect to room state:
@@ -53,7 +55,7 @@ namespace Quotient {
* requesting user as the creator, alongside other keys provided in the
* `creation_content`.
*/
-class CreateRoomJob : public BaseJob {
+class QUOTIENT_API CreateRoomJob : public BaseJob {
public:
// Inner data structures
@@ -73,17 +75,20 @@ public:
/// (and not other members) permission to send state events. Overridden
/// by the `power_level_content_override` parameter.
///
- /// 4. Events set by the `preset`. Currently these are the
+ /// 4. An `m.room.canonical_alias` event if `room_alias_name` is given.
+ ///
+ /// 5. Events set by the `preset`. Currently these are the
/// `m.room.join_rules`,
/// `m.room.history_visibility`, and `m.room.guest_access` state events.
///
- /// 5. Events listed in `initial_state`, in the order that they are
+ /// 6. Events listed in `initial_state`, in the order that they are
/// listed.
///
- /// 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic`
+ /// 7. Events implied by `name` and `topic` (`m.room.name` and
+ /// `m.room.topic`
/// state events).
///
- /// 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member`
+ /// 8. Invite events implied by `invite` and `invite_3pid` (`m.room.member`
/// with
/// `membership: invite` and `m.room.third_party_invite`).
///
@@ -132,17 +137,20 @@ public:
/// (and not other members) permission to send state events. Overridden
/// by the `power_level_content_override` parameter.
///
- /// 4. Events set by the `preset`. Currently these are the
+ /// 4. An `m.room.canonical_alias` event if `room_alias_name` is given.
+ ///
+ /// 5. Events set by the `preset`. Currently these are the
/// `m.room.join_rules`,
/// `m.room.history_visibility`, and `m.room.guest_access` state events.
///
- /// 5. Events listed in `initial_state`, in the order that they are
+ /// 6. Events listed in `initial_state`, in the order that they are
/// listed.
///
- /// 6. Events implied by `name` and `topic` (`m.room.name` and `m.room.topic`
+ /// 7. Events implied by `name` and `topic` (`m.room.name` and
+ /// `m.room.topic`
/// state events).
///
- /// 7. Invite events implied by `invite` and `invite_3pid` (`m.room.member`
+ /// 8. Invite events implied by `invite` and `invite_3pid` (`m.room.member`
/// with
/// `membership: invite` and `m.room.third_party_invite`).
///
@@ -190,7 +198,8 @@ public:
* would be `#foo:example.com`.
*
* The complete room alias will become the canonical alias for
- * the room.
+ * the room and an `m.room.canonical_alias` event will be sent
+ * into the room.
*
* \param name
* If this is included, an `m.room.name` event will be sent
@@ -218,9 +227,10 @@ public:
*
* \param creationContent
* Extra keys, such as `m.federate`, to be added to the content
- * of the [`m.room.create`](client-server-api/#mroomcreate) event. The
- * server will clobber the following keys: `creator`, `room_version`. Future
- * versions of the specification may allow the server to clobber other keys.
+ * of the [`m.room.create`](/client-server-api/#mroomcreate) event. The
+ * server will overwrite the following keys: `creator`, `room_version`.
+ * Future versions of the specification may allow the server to overwrite
+ * other keys.
*
* \param initialState
* A list of state events to set in the new room. This allows
@@ -229,7 +239,7 @@ public:
* with type, state_key and content keys set.
*
* Takes precedence over events set by `preset`, but gets
- * overriden by `name` and `topic` keys.
+ * overridden by `name` and `topic` keys.
*
* \param preset
* Convenience parameter for setting various default state events
@@ -249,7 +259,7 @@ public:
* \param powerLevelContentOverride
* The power level content to override in the default power level
* event. This object is applied on top of the generated
- * [`m.room.power_levels`](client-server-api/#mroompower_levels)
+ * [`m.room.power_levels`](/client-server-api/#mroompower_levels)
* event content prior to it being sent to the room. Defaults to
* overriding nothing.
*/
diff --git a/lib/csapi/cross_signing.cpp b/lib/csapi/cross_signing.cpp
new file mode 100644
index 00000000..83136d71
--- /dev/null
+++ b/lib/csapi/cross_signing.cpp
@@ -0,0 +1,33 @@
+/******************************************************************************
+ * 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,
+ const Omittable<AuthenticationData>& auth)
+ : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningKeysJob"),
+ makePath("/_matrix/client/v3", "/keys/device_signing/upload"))
+{
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("master_key"), masterKey);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("self_signing_key"),
+ selfSigningKey);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("user_signing_key"),
+ userSigningKey);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth);
+ setRequestData({ _dataJson });
+}
+
+UploadCrossSigningSignaturesJob::UploadCrossSigningSignaturesJob(
+ const QHash<QString, QHash<QString, QJsonObject>>& signatures)
+ : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningSignaturesJob"),
+ makePath("/_matrix/client/v3", "/keys/signatures/upload"))
+{
+ setRequestData({ toJson(signatures) });
+}
diff --git a/lib/csapi/cross_signing.h b/lib/csapi/cross_signing.h
new file mode 100644
index 00000000..6cea73e6
--- /dev/null
+++ b/lib/csapi/cross_signing.h
@@ -0,0 +1,78 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "csapi/definitions/auth_data.h"
+#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 QUOTIENT_API 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.
+ *
+ * \param auth
+ * Additional authentication information for the
+ * user-interactive authentication API.
+ */
+ explicit UploadCrossSigningKeysJob(
+ const Omittable<CrossSigningKey>& masterKey = none,
+ const Omittable<CrossSigningKey>& selfSigningKey = none,
+ const Omittable<CrossSigningKey>& userSigningKey = none,
+ const Omittable<AuthenticationData>& auth = 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 QUOTIENT_API 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/auth_data.h b/lib/csapi/definitions/auth_data.h
index e92596d0..a9972323 100644
--- a/lib/csapi/definitions/auth_data.h
+++ b/lib/csapi/definitions/auth_data.h
@@ -10,7 +10,10 @@ namespace Quotient {
/// Used by clients to submit authentication information to the
/// interactive-authentication API
struct AuthenticationData {
- /// The login type that the client is attempting to complete.
+ /// The authentication type that the client is attempting to complete.
+ /// May be omitted if `session` is given, and the client is reissuing a
+ /// request which it believes has been completed out-of-band (for example,
+ /// via the [fallback mechanism](#fallback)).
QString type;
/// The value of the session key given by the homeserver.
@@ -25,7 +28,7 @@ struct JsonObjectConverter<AuthenticationData> {
static void dumpTo(QJsonObject& jo, const AuthenticationData& pod)
{
fillJson(jo, pod.authInfo);
- addParam<>(jo, QStringLiteral("type"), pod.type);
+ addParam<IfNotEmpty>(jo, QStringLiteral("type"), pod.type);
addParam<IfNotEmpty>(jo, QStringLiteral("session"), pod.session);
}
static void fillFrom(QJsonObject jo, AuthenticationData& pod)
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/openid_token.h b/lib/csapi/definitions/openid_token.h
index 3c447321..9b026dea 100644
--- a/lib/csapi/definitions/openid_token.h
+++ b/lib/csapi/definitions/openid_token.h
@@ -8,7 +8,7 @@
namespace Quotient {
-struct OpenidToken {
+struct OpenIdCredentials {
/// An access token the consumer may use to verify the identity of
/// the person who generated the token. This is given to the federation
/// API `GET /openid/userinfo` to verify the user's identity.
@@ -27,8 +27,8 @@ struct OpenidToken {
};
template <>
-struct JsonObjectConverter<OpenidToken> {
- static void dumpTo(QJsonObject& jo, const OpenidToken& pod)
+struct JsonObjectConverter<OpenIdCredentials> {
+ static void dumpTo(QJsonObject& jo, const OpenIdCredentials& pod)
{
addParam<>(jo, QStringLiteral("access_token"), pod.accessToken);
addParam<>(jo, QStringLiteral("token_type"), pod.tokenType);
@@ -36,7 +36,7 @@ struct JsonObjectConverter<OpenidToken> {
pod.matrixServerName);
addParam<>(jo, QStringLiteral("expires_in"), pod.expiresIn);
}
- static void fillFrom(const QJsonObject& jo, OpenidToken& pod)
+ static void fillFrom(const QJsonObject& jo, OpenIdCredentials& pod)
{
fromJson(jo.value("access_token"_ls), pod.accessToken);
fromJson(jo.value("token_type"_ls), pod.tokenType);
diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h
index 8f30e607..7c7d9cc6 100644
--- a/lib/csapi/definitions/public_rooms_response.h
+++ b/lib/csapi/definitions/public_rooms_response.h
@@ -9,9 +9,6 @@
namespace Quotient {
struct PublicRoomsChunk {
- /// Aliases of the room. May be empty.
- QStringList aliases;
-
/// The canonical alias of the room, if any.
QString canonicalAlias;
@@ -36,14 +33,23 @@ struct PublicRoomsChunk {
bool guestCanJoin;
/// The URL for the room's avatar, if one is set.
- QString avatarUrl;
+ QUrl avatarUrl;
+
+ /// The `type` of room (from
+ /// [`m.room.create`](/client-server-api/#mroomcreate)), if any.
+ QString roomType;
+
+ /// The room's join rule. When not present, the room is assumed to
+ /// be `public`. Note that rooms with `invite` join rules are not
+ /// expected here, but rooms with `knock` rules are given their
+ /// near-public nature.
+ QString joinRule;
};
template <>
struct JsonObjectConverter<PublicRoomsChunk> {
static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod)
{
- addParam<IfNotEmpty>(jo, QStringLiteral("aliases"), pod.aliases);
addParam<IfNotEmpty>(jo, QStringLiteral("canonical_alias"),
pod.canonicalAlias);
addParam<IfNotEmpty>(jo, QStringLiteral("name"), pod.name);
@@ -54,10 +60,11 @@ 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("room_type"), pod.roomType);
+ addParam<IfNotEmpty>(jo, QStringLiteral("join_rule"), pod.joinRule);
}
static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod)
{
- fromJson(jo.value("aliases"_ls), pod.aliases);
fromJson(jo.value("canonical_alias"_ls), pod.canonicalAlias);
fromJson(jo.value("name"_ls), pod.name);
fromJson(jo.value("num_joined_members"_ls), pod.numJoinedMembers);
@@ -66,46 +73,8 @@ 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);
- }
-};
-
-/// A list of the rooms on the server.
-struct PublicRoomsResponse {
- /// A paginated chunk of public rooms.
- QVector<PublicRoomsChunk> chunk;
-
- /// A pagination token for the response. The absence of this token
- /// means there are no more results to fetch and the client should
- /// stop paginating.
- QString nextBatch;
-
- /// A pagination token that allows fetching previous results. The
- /// absence of this token means there are no results before this
- /// batch, i.e. this is the first batch.
- QString prevBatch;
-
- /// An estimate on the total number of public rooms, if the
- /// server has an estimate.
- Omittable<int> totalRoomCountEstimate;
-};
-
-template <>
-struct JsonObjectConverter<PublicRoomsResponse> {
- static void dumpTo(QJsonObject& jo, const PublicRoomsResponse& pod)
- {
- addParam<>(jo, QStringLiteral("chunk"), pod.chunk);
- addParam<IfNotEmpty>(jo, QStringLiteral("next_batch"), pod.nextBatch);
- addParam<IfNotEmpty>(jo, QStringLiteral("prev_batch"), pod.prevBatch);
- addParam<IfNotEmpty>(jo, QStringLiteral("total_room_count_estimate"),
- pod.totalRoomCountEstimate);
- }
- static void fillFrom(const QJsonObject& jo, PublicRoomsResponse& pod)
- {
- fromJson(jo.value("chunk"_ls), pod.chunk);
- fromJson(jo.value("next_batch"_ls), pod.nextBatch);
- fromJson(jo.value("prev_batch"_ls), pod.prevBatch);
- fromJson(jo.value("total_room_count_estimate"_ls),
- pod.totalRoomCountEstimate);
+ fromJson(jo.value("room_type"_ls), pod.roomType);
+ 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 ce66d075..6a048ba8 100644
--- a/lib/csapi/definitions/push_condition.h
+++ b/lib/csapi/definitions/push_condition.h
@@ -24,9 +24,7 @@ struct PushCondition {
QString key;
/// Required for `event_match` conditions. The glob-style pattern to
- /// match against. Patterns with no special glob characters should be
- /// treated as having asterisks prepended and appended when testing the
- /// condition.
+ /// match against.
QString pattern;
/// Required for `room_member_count` conditions. A decimal integer
diff --git a/lib/csapi/definitions/request_token_response.h b/lib/csapi/definitions/request_token_response.h
index f9981100..d5fbbadb 100644
--- a/lib/csapi/definitions/request_token_response.h
+++ b/lib/csapi/definitions/request_token_response.h
@@ -25,7 +25,7 @@ struct RequestTokenResponse {
/// will happen without the client's involvement provided the homeserver
/// advertises this specification version in the `/versions` response
/// (ie: r0.5.0).
- QString submitUrl;
+ QUrl submitUrl;
};
template <>
diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h
index 91caf667..293e5492 100644
--- a/lib/csapi/definitions/room_event_filter.h
+++ b/lib/csapi/definitions/room_event_filter.h
@@ -11,6 +11,11 @@
namespace Quotient {
struct RoomEventFilter : EventFilter {
+ /// If `true`, enables per-[thread](/client-server-api/#threading)
+ /// notification counts. Only applies to the `/sync` endpoint. Defaults to
+ /// `false`.
+ Omittable<bool> unreadThreadNotifications;
+
/// If `true`, enables lazy-loading of membership events. See
/// [Lazy-loading room
/// members](/client-server-api/#lazy-loading-room-members) for more
@@ -44,6 +49,8 @@ struct JsonObjectConverter<RoomEventFilter> {
static void dumpTo(QJsonObject& jo, const RoomEventFilter& pod)
{
fillJson<EventFilter>(jo, pod);
+ addParam<IfNotEmpty>(jo, QStringLiteral("unread_thread_notifications"),
+ pod.unreadThreadNotifications);
addParam<IfNotEmpty>(jo, QStringLiteral("lazy_load_members"),
pod.lazyLoadMembers);
addParam<IfNotEmpty>(jo, QStringLiteral("include_redundant_members"),
@@ -56,6 +63,8 @@ struct JsonObjectConverter<RoomEventFilter> {
static void fillFrom(const QJsonObject& jo, RoomEventFilter& pod)
{
fillFromJson<EventFilter>(jo, pod);
+ fromJson(jo.value("unread_thread_notifications"_ls),
+ pod.unreadThreadNotifications);
fromJson(jo.value("lazy_load_members"_ls), pod.lazyLoadMembers);
fromJson(jo.value("include_redundant_members"_ls),
pod.includeRedundantMembers);
diff --git a/lib/csapi/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..6f2badee 100644
--- a/lib/csapi/device_management.cpp
+++ b/lib/csapi/device_management.cpp
@@ -4,61 +4,58 @@
#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/v3", "/devices"));
}
GetDevicesJob::GetDevicesJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetDevicesJob"),
- QStringLiteral("/_matrix/client/r0") % "/devices")
+ makePath("/_matrix/client/v3", "/devices"))
{}
QUrl GetDeviceJob::makeRequestUrl(QUrl baseUrl, const QString& deviceId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/devices/" % deviceId);
+ makePath("/_matrix/client/v3", "/devices/",
+ deviceId));
}
GetDeviceJob::GetDeviceJob(const QString& deviceId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetDeviceJob"),
- QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId)
+ makePath("/_matrix/client/v3", "/devices/", deviceId))
{}
UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId,
const QString& displayName)
: BaseJob(HttpVerb::Put, QStringLiteral("UpdateDeviceJob"),
- QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId)
+ makePath("/_matrix/client/v3", "/devices/", deviceId))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("display_name"), displayName);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("display_name"), displayName);
+ setRequestData({ _dataJson });
}
DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId,
const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"),
- QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId)
+ makePath("/_matrix/client/v3", "/devices/", deviceId))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth);
+ setRequestData({ _dataJson });
}
DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices,
const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DeleteDevicesJob"),
- QStringLiteral("/_matrix/client/r0") % "/delete_devices")
+ makePath("/_matrix/client/v3", "/delete_devices"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("devices"), devices);
- addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("devices"), devices);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/device_management.h b/lib/csapi/device_management.h
index 7fb69873..c10389b3 100644
--- a/lib/csapi/device_management.h
+++ b/lib/csapi/device_management.h
@@ -15,7 +15,7 @@ namespace Quotient {
*
* Gets information about all devices for the current user.
*/
-class GetDevicesJob : public BaseJob {
+class QUOTIENT_API GetDevicesJob : public BaseJob {
public:
/// List registered devices for the current user
explicit GetDevicesJob();
@@ -40,7 +40,7 @@ public:
*
* Gets information on a single device, by device id.
*/
-class GetDeviceJob : public BaseJob {
+class QUOTIENT_API GetDeviceJob : public BaseJob {
public:
/*! \brief Get a single device
*
@@ -66,7 +66,7 @@ public:
*
* Updates the metadata on the given device.
*/
-class UpdateDeviceJob : public BaseJob {
+class QUOTIENT_API UpdateDeviceJob : public BaseJob {
public:
/*! \brief Update a device
*
@@ -86,9 +86,10 @@ public:
* This API endpoint uses the [User-Interactive Authentication
* API](/client-server-api/#user-interactive-authentication-api).
*
- * Deletes the given device, and invalidates any access token associated with it.
+ * Deletes the given device, and invalidates any access token associated with
+ * it.
*/
-class DeleteDeviceJob : public BaseJob {
+class QUOTIENT_API DeleteDeviceJob : public BaseJob {
public:
/*! \brief Delete a device
*
@@ -111,7 +112,7 @@ public:
* Deletes the given devices, and invalidates any access token associated with
* them.
*/
-class DeleteDevicesJob : public BaseJob {
+class QUOTIENT_API DeleteDevicesJob : public BaseJob {
public:
/*! \brief Bulk deletion of devices
*
diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp
index 25ea82e2..c1255bb1 100644
--- a/lib/csapi/directory.cpp
+++ b/lib/csapi/directory.cpp
@@ -4,58 +4,52 @@
#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/v3", "/directory/room/", roomAlias))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("room_id"), roomId);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("room_id"), roomId);
+ setRequestData({ _dataJson });
}
QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/directory/room/" % roomAlias);
+ makePath("/_matrix/client/v3",
+ "/directory/room/", roomAlias));
}
GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomIdByAliasJob"),
- QStringLiteral("/_matrix/client/r0") % "/directory/room/"
- % roomAlias,
+ makePath("/_matrix/client/v3", "/directory/room/", roomAlias),
false)
{}
QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/directory/room/" % roomAlias);
+ makePath("/_matrix/client/v3",
+ "/directory/room/", roomAlias));
}
DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomAliasJob"),
- QStringLiteral("/_matrix/client/r0") % "/directory/room/"
- % roomAlias)
+ makePath("/_matrix/client/v3", "/directory/room/", roomAlias))
{}
QUrl GetLocalAliasesJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/aliases");
+ makePath("/_matrix/client/v3", "/rooms/",
+ roomId, "/aliases"));
}
GetLocalAliasesJob::GetLocalAliasesJob(const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetLocalAliasesJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/aliases")
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/aliases"))
{
addExpectedKey("aliases");
}
diff --git a/lib/csapi/directory.h b/lib/csapi/directory.h
index 93a31595..0bd13a76 100644
--- a/lib/csapi/directory.h
+++ b/lib/csapi/directory.h
@@ -11,7 +11,7 @@ namespace Quotient {
/*! \brief Create a new mapping from room alias to room ID.
*
*/
-class SetRoomAliasJob : public BaseJob {
+class QUOTIENT_API SetRoomAliasJob : public BaseJob {
public:
/*! \brief Create a new mapping from room alias to room ID.
*
@@ -32,7 +32,7 @@ public:
* domain part of the alias does not correspond to the server's own
* domain.
*/
-class GetRoomIdByAliasJob : public BaseJob {
+class QUOTIENT_API GetRoomIdByAliasJob : public BaseJob {
public:
/*! \brief Get the room ID corresponding to this room alias.
*
@@ -76,7 +76,7 @@ public:
* return a successful response even if the user does not have permission to
* update the `m.room.canonical_alias` event.
*/
-class DeleteRoomAliasJob : public BaseJob {
+class QUOTIENT_API DeleteRoomAliasJob : public BaseJob {
public:
/*! \brief Remove a mapping of room alias to room ID.
*
@@ -112,7 +112,7 @@ public:
* as they are not curated, unlike those listed in the `m.room.canonical_alias`
* state event.
*/
-class GetLocalAliasesJob : public BaseJob {
+class QUOTIENT_API GetLocalAliasesJob : public BaseJob {
public:
/*! \brief Get a list of local aliases on a given room.
*
diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp
index d2a5f522..4ebbbf98 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/v3", "/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/v3", "/rooms/", roomId, "/context/",
+ eventId),
queryToGetEventContext(limit, filter))
{}
diff --git a/lib/csapi/event_context.h b/lib/csapi/event_context.h
index 4e50edf3..1614c7ed 100644
--- a/lib/csapi/event_context.h
+++ b/lib/csapi/event_context.h
@@ -4,7 +4,8 @@
#pragma once
-#include "events/eventloader.h"
+#include "events/roomevent.h"
+#include "events/stateevent.h"
#include "jobs/basejob.h"
namespace Quotient {
@@ -19,7 +20,7 @@ namespace Quotient {
* [Lazy-loading room members](/client-server-api/#lazy-loading-room-members)
* for more information.
*/
-class GetEventContextJob : public BaseJob {
+class QUOTIENT_API GetEventContextJob : public BaseJob {
public:
/*! \brief Get events and state around the specified event.
*
diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp
index bb3a893f..2469fbd1 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/v3", "/user/", userId, "/filter"))
{
- setRequestData(Data(toJson(filter)));
+ setRequestData({ 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/v3", "/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/v3", "/user/", userId, "/filter/",
+ filterId))
{}
diff --git a/lib/csapi/filter.h b/lib/csapi/filter.h
index 01bec36b..9518a461 100644
--- a/lib/csapi/filter.h
+++ b/lib/csapi/filter.h
@@ -16,7 +16,7 @@ namespace Quotient {
* Returns a filter ID that may be used in future requests to
* restrict which events are returned to the client.
*/
-class DefineFilterJob : public BaseJob {
+class QUOTIENT_API DefineFilterJob : public BaseJob {
public:
/*! \brief Upload a new filter.
*
@@ -41,7 +41,7 @@ public:
/*! \brief Download a filter
*
*/
-class GetFilterJob : public BaseJob {
+class QUOTIENT_API GetFilterJob : public BaseJob {
public:
/*! \brief Download a filter
*
diff --git a/lib/csapi/inviting.cpp b/lib/csapi/inviting.cpp
index 01620f9e..41a8b5be 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/v3", "/rooms/", roomId, "/invite"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("user_id"), userId);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("user_id"), userId);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/inviting.h b/lib/csapi/inviting.h
index 1e65ecff..cb9d052b 100644
--- a/lib/csapi/inviting.h
+++ b/lib/csapi/inviting.h
@@ -14,7 +14,7 @@ namespace Quotient {
* This version of the API requires that the inviter knows the Matrix
* identifier of the invitee. The other is documented in the*
* [third party invites
- * section](/client-server-api/#post_matrixclientr0roomsroomidinvite-1).
+ * section](/client-server-api/#post_matrixclientv3roomsroomidinvite-1).
*
* This API invites a user to participate in a particular room.
* They do not start participating in the room until they actually join the
@@ -26,7 +26,7 @@ namespace Quotient {
* If the user was invited to the room, the homeserver will append a
* `m.room.member` event to the room.
*/
-class InviteUserJob : public BaseJob {
+class QUOTIENT_API InviteUserJob : public BaseJob {
public:
/*! \brief Invite a user to participate in a particular room.
*
@@ -35,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..cdba95e9 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/v3", "/rooms/", roomId, "/join"))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"),
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("third_party_signed"),
thirdPartySigned);
- setRequestData(std::move(_data));
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason);
+ setRequestData({ _dataJson });
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/v3", "/join/", roomIdOrAlias),
queryToJoinRoom(serverName))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"),
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("third_party_signed"),
thirdPartySigned);
- setRequestData(std::move(_data));
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason);
+ setRequestData({ _dataJson });
addExpectedKey("room_id");
}
diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h
index 1b6f99e4..c86baa90 100644
--- a/lib/csapi/joining.h
+++ b/lib/csapi/joining.h
@@ -22,10 +22,10 @@ namespace Quotient {
*
* After a user has joined a room, the room will appear as an entry in the
* response of the
- * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and
- * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs.
+ * [`/initialSync`](/client-server-api/#get_matrixclientv3initialsync) and
+ * [`/sync`](/client-server-api/#get_matrixclientv3sync) APIs.
*/
-class JoinRoomByIdJob : public BaseJob {
+class QUOTIENT_API JoinRoomByIdJob : public BaseJob {
public:
/*! \brief Start the requesting user participating in a particular room.
*
@@ -36,10 +36,15 @@ public:
* If 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 JoinRoomByIdJob(
const QString& roomId,
- const Omittable<ThirdPartySigned>& thirdPartySigned = none);
+ const Omittable<ThirdPartySigned>& thirdPartySigned = none,
+ const QString& reason = {});
// Result properties
@@ -50,7 +55,7 @@ public:
/*! \brief Start the requesting user participating in a particular room.
*
* *Note that this API takes either a room ID or alias, unlike*
- * `/room/{roomId}/join`.
+ * `/rooms/{roomId}/join`.
*
* This API starts a user participating in a particular room, if that user
* is allowed to participate in that room. After this call, the client is
@@ -59,10 +64,10 @@ public:
*
* After a user has joined a room, the room will appear as an entry in the
* response of the
- * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and
- * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs.
+ * [`/initialSync`](/client-server-api/#get_matrixclientv3initialsync) and
+ * [`/sync`](/client-server-api/#get_matrixclientv3sync) APIs.
*/
-class JoinRoomJob : public BaseJob {
+class QUOTIENT_API JoinRoomJob : public BaseJob {
public:
/*! \brief Start the requesting user participating in a particular room.
*
@@ -77,10 +82,15 @@ public:
* 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..2e4978f2 100644
--- a/lib/csapi/keys.cpp
+++ b/lib/csapi/keys.cpp
@@ -4,50 +4,52 @@
#include "keys.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
UploadKeysJob::UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys,
- const QHash<QString, QVariant>& oneTimeKeys)
+ const OneTimeKeys& oneTimeKeys,
+ const OneTimeKeys& fallbackKeys)
: BaseJob(HttpVerb::Post, QStringLiteral("UploadKeysJob"),
- QStringLiteral("/_matrix/client/r0") % "/keys/upload")
+ makePath("/_matrix/client/v3", "/keys/upload"))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("device_keys"), deviceKeys);
- addParam<IfNotEmpty>(_data, QStringLiteral("one_time_keys"), oneTimeKeys);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("device_keys"), deviceKeys);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("one_time_keys"),
+ oneTimeKeys);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("fallback_keys"),
+ fallbackKeys);
+ setRequestData({ _dataJson });
addExpectedKey("one_time_key_counts");
}
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/v3", "/keys/query"))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout);
- addParam<>(_data, QStringLiteral("device_keys"), deviceKeys);
- addParam<IfNotEmpty>(_data, QStringLiteral("token"), token);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("timeout"), timeout);
+ addParam<>(_dataJson, QStringLiteral("device_keys"), deviceKeys);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("token"), token);
+ setRequestData({ _dataJson });
}
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/v3", "/keys/claim"))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout);
- addParam<>(_data, QStringLiteral("one_time_keys"), oneTimeKeys);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("timeout"), timeout);
+ addParam<>(_dataJson, QStringLiteral("one_time_keys"), oneTimeKeys);
+ setRequestData({ _dataJson });
addExpectedKey("one_time_keys");
}
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 +59,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/v3",
+ "/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/v3", "/keys/changes"),
queryToGetKeysChanges(from, to))
{}
diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h
index 621945eb..b28de305 100644
--- a/lib/csapi/keys.h
+++ b/lib/csapi/keys.h
@@ -4,8 +4,11 @@
#pragma once
+#include "csapi/definitions/cross_signing_key.h"
#include "csapi/definitions/device_keys.h"
+#include "e2ee/e2ee.h"
+
#include "jobs/basejob.h"
namespace Quotient {
@@ -14,7 +17,7 @@ namespace Quotient {
*
* Publishes end-to-end encryption keys for the device.
*/
-class UploadKeysJob : public BaseJob {
+class QUOTIENT_API UploadKeysJob : public BaseJob {
public:
/*! \brief Upload end-to-end encryption keys.
*
@@ -29,14 +32,32 @@ public:
* by the [key algorithm](/client-server-api/#key-algorithms).
*
* May be absent if no new one-time keys are required.
+ *
+ * \param fallbackKeys
+ * The public key which should be used if the device's one-time keys
+ * are exhausted. The fallback key is not deleted once used, but should
+ * be replaced when additional one-time keys are being uploaded. The
+ * server will notify the client of the fallback key being used through
+ * `/sync`.
+ *
+ * There can only be at most one key per algorithm uploaded, and the
+ * server will only persist one key per algorithm.
+ *
+ * When uploading a signed key, an additional `fallback: true` key should
+ * be included to denote that the key is a fallback key.
+ *
+ * May be absent if a new fallback key is not required.
*/
explicit UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys = none,
- const QHash<QString, QVariant>& oneTimeKeys = {});
+ const OneTimeKeys& oneTimeKeys = {},
+ const OneTimeKeys& fallbackKeys = {});
// Result properties
/// For each key algorithm, the number of unclaimed one-time keys
/// of that type currently held on the server for this device.
+ /// If an algorithm is not listed, the count for that algorithm
+ /// is to be assumed zero.
QHash<QString, int> oneTimeKeyCounts() const
{
return loadFromJson<QHash<QString, int>>("one_time_key_counts"_ls);
@@ -47,7 +68,7 @@ public:
*
* Returns the current devices and identity keys for the given users.
*/
-class QueryKeysJob : public BaseJob {
+class QUOTIENT_API QueryKeysJob : public BaseJob {
public:
// Inner data structures
@@ -114,6 +135,38 @@ public:
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 <>
@@ -139,7 +192,7 @@ struct JsonObjectConverter<QueryKeysJob::DeviceInformation> {
*
* Claims one-time keys for use in pre-key messages.
*/
-class ClaimKeysJob : public BaseJob {
+class QUOTIENT_API ClaimKeysJob : public BaseJob {
public:
/*! \brief Claim one-time encryption keys.
*
@@ -174,9 +227,12 @@ public:
///
/// See the [key algorithms](/client-server-api/#key-algorithms) section for
/// information on the Key Object format.
- QHash<QString, QHash<QString, QVariant>> oneTimeKeys() const
+ ///
+ /// If necessary, the claimed key might be a fallback key. Fallback
+ /// keys are re-used by the server until replaced by the device.
+ QHash<QString, QHash<QString, OneTimeKeys>> oneTimeKeys() const
{
- return loadFromJson<QHash<QString, QHash<QString, QVariant>>>(
+ return loadFromJson<QHash<QString, QHash<QString, OneTimeKeys>>>(
"one_time_keys"_ls);
}
};
@@ -193,14 +249,14 @@ public:
* * added new device identity keys or removed an existing device with
* identity keys, between `from` and `to`.
*/
-class GetKeysChangesJob : public BaseJob {
+class QUOTIENT_API GetKeysChangesJob : public BaseJob {
public:
/*! \brief Query users with recent device key updates.
*
* \param from
* The desired start point of the list. Should be the `next_batch` field
* from a response to an earlier call to
- * [`/sync`](/client-server-api/#get_matrixclientr0sync). Users who have not
+ * [`/sync`](/client-server-api/#get_matrixclientv3sync). Users who have not
* uploaded new device identity keys since this point, nor deleted
* existing devices with identity keys since then, will be excluded
* from the results.
@@ -208,7 +264,7 @@ public:
* \param to
* The desired end point of the list. Should be the `next_batch`
* field from a recent call to
- * [`/sync`](/client-server-api/#get_matrixclientr0sync) - typically the
+ * [`/sync`](/client-server-api/#get_matrixclientv3sync) - typically the
* most recent such call. This may be used by the server as a hint to check
* its caches are up to date.
*/
diff --git a/lib/csapi/kicking.cpp b/lib/csapi/kicking.cpp
index 7de5ce01..4ca39c4c 100644
--- a/lib/csapi/kicking.cpp
+++ b/lib/csapi/kicking.cpp
@@ -4,17 +4,15 @@
#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/v3", "/rooms/", roomId, "/kick"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("user_id"), userId);
- addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("user_id"), userId);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h
index 11018368..6ac106e2 100644
--- a/lib/csapi/kicking.h
+++ b/lib/csapi/kicking.h
@@ -20,7 +20,7 @@ namespace Quotient {
* directly adjust the target member's state by making a request to
* `/rooms/<room id>/state/m.room.member/<user id>`.
*/
-class KickJob : public BaseJob {
+class QUOTIENT_API KickJob : public BaseJob {
public:
/*! \brief Kick a user from the room.
*
diff --git a/lib/csapi/knocking.cpp b/lib/csapi/knocking.cpp
new file mode 100644
index 00000000..b9da4b9b
--- /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/v3", "/knock/", roomIdOrAlias),
+ queryToKnockRoom(serverName))
+{
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason);
+ setRequestData({ _dataJson });
+ addExpectedKey("room_id");
+}
diff --git a/lib/csapi/knocking.h b/lib/csapi/knocking.h
new file mode 100644
index 00000000..f43033a8
--- /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_matrixclientv3sync) API.
+ */
+class QUOTIENT_API 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..ba91f26a 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/v3", "/rooms/", roomId, "/leave"))
{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/rooms/" % roomId % "/leave");
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason);
+ setRequestData({ _dataJson });
}
-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/v3", "/rooms/",
+ roomId, "/forget"));
}
ForgetRoomJob::ForgetRoomJob(const QString& roomId)
: BaseJob(HttpVerb::Post, QStringLiteral("ForgetRoomJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/forget")
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/forget"))
{}
diff --git a/lib/csapi/leaving.h b/lib/csapi/leaving.h
index 1bea7e41..19cac3f0 100644
--- a/lib/csapi/leaving.h
+++ b/lib/csapi/leaving.h
@@ -22,21 +22,18 @@ namespace Quotient {
* The user will still be allowed to retrieve history from the room which
* they were previously allowed to see.
*/
-class LeaveRoomJob : public BaseJob {
+class QUOTIENT_API LeaveRoomJob : public BaseJob {
public:
/*! \brief Stop the requesting user participating in a particular room.
*
* \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.
@@ -51,7 +48,7 @@ public:
* If the user is currently joined to the room, they must leave the room
* before calling this API.
*/
-class ForgetRoomJob : public BaseJob {
+class QUOTIENT_API ForgetRoomJob : public BaseJob {
public:
/*! \brief Stop the requesting user remembering about a particular room.
*
diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp
index 8d7e267f..cdcf3eb2 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/v3", "/joined_rooms"));
}
GetJoinedRoomsJob::GetJoinedRoomsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedRoomsJob"),
- QStringLiteral("/_matrix/client/r0") % "/joined_rooms")
+ makePath("/_matrix/client/v3", "/joined_rooms"))
{
addExpectedKey("joined_rooms");
}
diff --git a/lib/csapi/list_joined_rooms.h b/lib/csapi/list_joined_rooms.h
index 59a24a49..aea68afd 100644
--- a/lib/csapi/list_joined_rooms.h
+++ b/lib/csapi/list_joined_rooms.h
@@ -12,7 +12,7 @@ namespace Quotient {
*
* This API returns a list of the user's current rooms.
*/
-class GetJoinedRoomsJob : public BaseJob {
+class QUOTIENT_API GetJoinedRoomsJob : public BaseJob {
public:
/// Lists the user's current rooms.
explicit GetJoinedRoomsJob();
diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp
index 415d816c..4deecfc2 100644
--- a/lib/csapi/list_public_rooms.cpp
+++ b/lib/csapi/list_public_rooms.cpp
@@ -4,41 +4,37 @@
#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/v3",
+ "/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/v3", "/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/v3", "/directory/list/room/", roomId))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("visibility"), visibility);
+ setRequestData({ _dataJson });
}
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/v3",
+ "/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/v3", "/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,17 +74,17 @@ QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server,
Omittable<bool> includeAllNetworks,
const QString& thirdPartyInstanceId)
: BaseJob(HttpVerb::Post, QStringLiteral("QueryPublicRoomsJob"),
- QStringLiteral("/_matrix/client/r0") % "/publicRooms",
+ makePath("/_matrix/client/v3", "/publicRooms"),
queryToQueryPublicRooms(server))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("limit"), limit);
- addParam<IfNotEmpty>(_data, QStringLiteral("since"), since);
- addParam<IfNotEmpty>(_data, QStringLiteral("filter"), filter);
- addParam<IfNotEmpty>(_data, QStringLiteral("include_all_networks"),
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("limit"), limit);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("since"), since);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("filter"), filter);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("include_all_networks"),
includeAllNetworks);
- addParam<IfNotEmpty>(_data, QStringLiteral("third_party_instance_id"),
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("third_party_instance_id"),
thirdPartyInstanceId);
- setRequestData(std::move(_data));
+ setRequestData({ _dataJson });
addExpectedKey("chunk");
}
diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h
index 963c8b56..3b6b91b9 100644
--- a/lib/csapi/list_public_rooms.h
+++ b/lib/csapi/list_public_rooms.h
@@ -14,7 +14,7 @@ namespace Quotient {
*
* Gets the visibility of a given room on the server's public room directory.
*/
-class GetRoomVisibilityOnDirectoryJob : public BaseJob {
+class QUOTIENT_API GetRoomVisibilityOnDirectoryJob : public BaseJob {
public:
/*! \brief Gets the visibility of a room in the directory
*
@@ -48,7 +48,7 @@ public:
* here, for instance that room visibility can only be changed by
* the room creator or a server administrator.
*/
-class SetRoomVisibilityOnDirectoryJob : public BaseJob {
+class QUOTIENT_API SetRoomVisibilityOnDirectoryJob : public BaseJob {
public:
/*! \brief Sets the visibility of a room in the room directory
*
@@ -70,7 +70,7 @@ public:
* This API returns paginated responses. The rooms are ordered by the number
* of joined members, with the largest rooms first.
*/
-class GetPublicRoomsJob : public BaseJob {
+class QUOTIENT_API GetPublicRoomsJob : public BaseJob {
public:
/*! \brief Lists the public rooms on the server.
*
@@ -133,15 +133,20 @@ public:
* This API returns paginated responses. The rooms are ordered by the number
* of joined members, with the largest rooms first.
*/
-class QueryPublicRoomsJob : public BaseJob {
+class QUOTIENT_API QueryPublicRoomsJob : public BaseJob {
public:
// Inner data structures
/// Filter to apply to the results.
struct Filter {
- /// A string to search for in the room metadata, e.g. name,
- /// topic, canonical alias etc. (Optional).
+ /// An optional string to search for in the room metadata, e.g. name,
+ /// topic, canonical alias, etc.
QString genericSearchTerm;
+ /// An optional list of [room types](/client-server-api/#types) to
+ /// search for. To include rooms without a room type, specify `null`
+ /// within this list. When not specified, all applicable rooms
+ /// (regardless of type) are returned.
+ QStringList roomTypes;
};
// Construction/destruction
@@ -211,6 +216,7 @@ struct JsonObjectConverter<QueryPublicRoomsJob::Filter> {
{
addParam<IfNotEmpty>(jo, QStringLiteral("generic_search_term"),
pod.genericSearchTerm);
+ addParam<IfNotEmpty>(jo, QStringLiteral("room_types"), pod.roomTypes);
}
};
diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp
index a5bac9ea..7bb74e29 100644
--- a/lib/csapi/login.cpp
+++ b/lib/csapi/login.cpp
@@ -4,37 +4,41 @@
#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/v3", "/login"));
}
GetLoginFlowsJob::GetLoginFlowsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetLoginFlowsJob"),
- QStringLiteral("/_matrix/client/r0") % "/login", false)
+ makePath("/_matrix/client/v3", "/login"), false)
{}
LoginJob::LoginJob(const QString& type,
const Omittable<UserIdentifier>& identifier,
const QString& password, const QString& token,
const QString& deviceId,
- const QString& initialDeviceDisplayName)
+ const QString& initialDeviceDisplayName,
+ Omittable<bool> refreshToken)
: BaseJob(HttpVerb::Post, QStringLiteral("LoginJob"),
- QStringLiteral("/_matrix/client/r0") % "/login", false)
+ makePath("/_matrix/client/v3", "/login"), false)
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("type"), type);
- addParam<IfNotEmpty>(_data, QStringLiteral("identifier"), identifier);
- addParam<IfNotEmpty>(_data, QStringLiteral("password"), password);
- addParam<IfNotEmpty>(_data, QStringLiteral("token"), token);
- addParam<IfNotEmpty>(_data, QStringLiteral("device_id"), deviceId);
- addParam<IfNotEmpty>(_data, QStringLiteral("initial_device_display_name"),
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("type"), type);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("identifier"), identifier);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("password"), password);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("token"), token);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("device_id"), deviceId);
+ addParam<IfNotEmpty>(_dataJson,
+ QStringLiteral("initial_device_display_name"),
initialDeviceDisplayName);
- setRequestData(std::move(_data));
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("refresh_token"),
+ refreshToken);
+ setRequestData({ _dataJson });
+ addExpectedKey("user_id");
+ addExpectedKey("access_token");
+ addExpectedKey("device_id");
}
diff --git a/lib/csapi/login.h b/lib/csapi/login.h
index b35db1eb..b9f14266 100644
--- a/lib/csapi/login.h
+++ b/lib/csapi/login.h
@@ -16,7 +16,7 @@ namespace Quotient {
* Gets the homeserver's supported login types to authenticate users. Clients
* should pick one of these and supply it as the `type` when logging in.
*/
-class GetLoginFlowsJob : public BaseJob {
+class QUOTIENT_API GetLoginFlowsJob : public BaseJob {
public:
// Inner data structures
@@ -73,7 +73,7 @@ struct JsonObjectConverter<GetLoginFlowsJob::LoginFlow> {
* [Relationship between access tokens and
* devices](/client-server-api/#relationship-between-access-tokens-and-devices).
*/
-class LoginJob : public BaseJob {
+class QUOTIENT_API LoginJob : public BaseJob {
public:
/*! \brief Authenticates the user.
*
@@ -111,12 +111,16 @@ public:
* \param initialDeviceDisplayName
* A display name to assign to the newly-created device. Ignored
* if `device_id` corresponds to a known device.
+ *
+ * \param refreshToken
+ * If true, the client supports refresh tokens.
*/
explicit LoginJob(const QString& type,
const Omittable<UserIdentifier>& identifier = none,
const QString& password = {}, const QString& token = {},
const QString& deviceId = {},
- const QString& initialDeviceDisplayName = {});
+ const QString& initialDeviceDisplayName = {},
+ Omittable<bool> refreshToken = none);
// Result properties
@@ -130,15 +134,23 @@ public:
return loadFromJson<QString>("access_token"_ls);
}
- /// The server_name of the homeserver on which the account has
- /// been registered.
- ///
- /// **Deprecated**. Clients should extract the server_name from
- /// `user_id` (by splitting at the first colon) if they require
- /// it. Note also that `homeserver` is not spelt this way.
- QString homeServer() const
+ /// A refresh token for the account. This token can be used to
+ /// obtain a new access token when it expires by calling the
+ /// `/refresh` endpoint.
+ QString refreshToken() const
+ {
+ return loadFromJson<QString>("refresh_token"_ls);
+ }
+
+ /// The lifetime of the access token, in milliseconds. Once
+ /// the access token has expired a new access token can be
+ /// obtained by using the provided refresh token. If no
+ /// refresh token is provided, the client will need to re-log in
+ /// to obtain a new access token. If not given, the client can
+ /// assume that the access token will not expire.
+ Omittable<int> expiresInMs() const
{
- return loadFromJson<QString>("home_server"_ls);
+ return loadFromJson<Omittable<int>>("expires_in_ms"_ls);
}
/// ID of the logged-in device. Will be the same as the
diff --git a/lib/csapi/logout.cpp b/lib/csapi/logout.cpp
index 9583b8ec..9ec54c71 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/v3", "/logout"));
}
LogoutJob::LogoutJob()
: BaseJob(HttpVerb::Post, QStringLiteral("LogoutJob"),
- QStringLiteral("/_matrix/client/r0") % "/logout")
+ makePath("/_matrix/client/v3", "/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/v3", "/logout/all"));
}
LogoutAllJob::LogoutAllJob()
: BaseJob(HttpVerb::Post, QStringLiteral("LogoutAllJob"),
- QStringLiteral("/_matrix/client/r0") % "/logout/all")
+ makePath("/_matrix/client/v3", "/logout/all"))
{}
diff --git a/lib/csapi/logout.h b/lib/csapi/logout.h
index 2e4c2692..3f1ac7fa 100644
--- a/lib/csapi/logout.h
+++ b/lib/csapi/logout.h
@@ -15,7 +15,7 @@ namespace Quotient {
* [Device keys](/client-server-api/#device-keys) for the device are deleted
* alongside the device.
*/
-class LogoutJob : public BaseJob {
+class QUOTIENT_API LogoutJob : public BaseJob {
public:
/// Invalidates a user access token
explicit LogoutJob();
@@ -44,7 +44,7 @@ public:
* used in the request, and therefore the attacker is unable to take over the
* account in this way.
*/
-class LogoutAllJob : public BaseJob {
+class QUOTIENT_API LogoutAllJob : public BaseJob {
public:
/// Invalidates all access tokens for a user
explicit LogoutAllJob();
diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp
index 855c051f..0b2c99ce 100644
--- a/lib/csapi/message_pagination.cpp
+++ b/lib/csapi/message_pagination.cpp
@@ -4,16 +4,14 @@
#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;
- addParam<>(_q, QStringLiteral("from"), from);
+ QUrlQuery _q;
+ addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
addParam<IfNotEmpty>(_q, QStringLiteral("to"), to);
addParam<>(_q, QStringLiteral("dir"), dir);
addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
@@ -22,21 +20,23 @@ auto queryToGetRoomEvents(const QString& from, const QString& to,
}
QUrl GetRoomEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
- const QString& from, const QString& dir,
+ const QString& dir, const QString& from,
const QString& to, Omittable<int> limit,
const QString& filter)
{
return BaseJob::makeRequestUrl(
std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/messages",
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/messages"),
queryToGetRoomEvents(from, to, dir, limit, filter));
}
-GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& from,
- const QString& dir, const QString& to,
+GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& dir,
+ const QString& from, const QString& to,
Omittable<int> limit, const QString& filter)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomEventsJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/messages",
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/messages"),
queryToGetRoomEvents(from, to, dir, limit, filter))
-{}
+{
+ addExpectedKey("start");
+ addExpectedKey("chunk");
+}
diff --git a/lib/csapi/message_pagination.h b/lib/csapi/message_pagination.h
index 363e4d99..b4f3a38a 100644
--- a/lib/csapi/message_pagination.h
+++ b/lib/csapi/message_pagination.h
@@ -4,7 +4,7 @@
#pragma once
-#include "events/eventloader.h"
+#include "events/roomevent.h"
#include "jobs/basejob.h"
namespace Quotient {
@@ -18,27 +18,37 @@ namespace Quotient {
* [Lazy-loading room members](/client-server-api/#lazy-loading-room-members)
* for more information.
*/
-class GetRoomEventsJob : public BaseJob {
+class QUOTIENT_API GetRoomEventsJob : public BaseJob {
public:
/*! \brief Get a list of events for this room
*
* \param roomId
* The room to get events from.
*
+ * \param dir
+ * The direction to return events from. If this is set to `f`, events
+ * will be returned in chronological order starting at `from`. If it
+ * is set to `b`, events will be returned in *reverse* chronological
+ * order, again starting at `from`.
+ *
* \param from
* The token to start returning events from. This token can be obtained
- * from a `prev_batch` token returned for each room by the sync API,
- * or from a `start` or `end` token returned by a previous request
- * to this endpoint.
+ * from a `prev_batch` or `next_batch` token returned by the `/sync`
+ * endpoint, or from an `end` token returned by a previous request to this
+ * endpoint.
*
- * \param dir
- * The direction to return events from.
+ * This endpoint can also accept a value returned as a `start` token
+ * by a previous request to this endpoint, though servers are not
+ * required to support this. Clients should not rely on the behaviour.
+ *
+ * If it is not provided, the homeserver shall return a list of messages
+ * from the first or last (per the value of the `dir` parameter) visible
+ * event in the room history for the requesting user.
*
* \param to
* The token to stop returning events at. This token can be obtained from
- * a `prev_batch` token returned for each room by the sync endpoint,
- * or from a `start` or `end` token returned by a previous request to
- * this endpoint.
+ * a `prev_batch` or `next_batch` token returned by the `/sync` endpoint,
+ * or from an `end` token returned by a previous request to this endpoint.
*
* \param limit
* The maximum number of events to return. Default: 10.
@@ -46,8 +56,8 @@ public:
* \param filter
* A JSON RoomEventFilter to filter returned events with.
*/
- explicit GetRoomEventsJob(const QString& roomId, const QString& from,
- const QString& dir, const QString& to = {},
+ explicit GetRoomEventsJob(const QString& roomId, const QString& dir,
+ const QString& from = {}, const QString& to = {},
Omittable<int> limit = none,
const QString& filter = {});
@@ -57,25 +67,34 @@ public:
* is necessary but the job itself isn't.
*/
static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId,
- const QString& from, const QString& dir,
+ const QString& dir, const QString& from = {},
const QString& to = {},
Omittable<int> limit = none,
const QString& filter = {});
// Result properties
- /// The token the pagination starts from. If `dir=b` this will be
- /// the token supplied in `from`.
+ /// A token corresponding to the start of `chunk`. This will be the same as
+ /// the value given in `from`.
QString begin() const { return loadFromJson<QString>("start"_ls); }
- /// The token the pagination ends at. If `dir=b` this token should
- /// be used again to request even earlier events.
+ /// A token corresponding to the end of `chunk`. This token can be passed
+ /// back to this endpoint to request further events.
+ ///
+ /// If no further events are available (either because we have
+ /// reached the start of the timeline, or because the user does
+ /// not have permission to see any more events), this property
+ /// is omitted from the response.
QString end() const { return loadFromJson<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.
+ /// for `dir=f` in chronological order. (The exact definition of
+ /// `chronological` is dependent on the server implementation.)
+ ///
+ /// Note that an empty `chunk` does not *necessarily* imply that no more
+ /// events are available. Clients should continue to paginate until no `end`
+ /// property is returned.
RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); }
/// A list of state events relevant to showing the `chunk`. For example, if
@@ -86,7 +105,7 @@ public:
/// may remove membership events which would have already been
/// sent to the client in prior calls to this endpoint, assuming
/// the membership of those members has not changed.
- StateEvents state() { return takeFromJson<StateEvents>("state"_ls); }
+ RoomEvents state() { return takeFromJson<RoomEvents>("state"_ls); }
};
} // namespace Quotient
diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp
index a479d500..38aed174 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/v3",
+ "/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/v3", "/notifications"),
queryToGetNotifications(from, limit, only))
{
addExpectedKey("notifications");
diff --git a/lib/csapi/notifications.h b/lib/csapi/notifications.h
index 0999fece..ff8aa47f 100644
--- a/lib/csapi/notifications.h
+++ b/lib/csapi/notifications.h
@@ -4,7 +4,7 @@
#pragma once
-#include "events/eventloader.h"
+#include "events/event.h"
#include "jobs/basejob.h"
namespace Quotient {
@@ -14,7 +14,7 @@ namespace Quotient {
* This API is used to paginate through the list of events that the
* user has been, or would have been notified about.
*/
-class GetNotificationsJob : public BaseJob {
+class QUOTIENT_API GetNotificationsJob : public BaseJob {
public:
// Inner data structures
@@ -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
@@ -43,7 +43,8 @@ public:
/*! \brief Gets a list of events that the user has been notified about
*
* \param from
- * Pagination token given to retrieve the next set of events.
+ * Pagination token to continue from. This should be the `next_token`
+ * returned from an earlier call to this endpoint.
*
* \param limit
* Limit on the number of events to return in this request.
diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp
index 3941e9c0..7e89b8a6 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/v3", "/user/", userId,
+ "/openid/request_token"))
{
- setRequestData(Data(toJson(body)));
+ setRequestData({ toJson(body) });
}
diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h
index 0be39c8c..b3f72a25 100644
--- a/lib/csapi/openid.h
+++ b/lib/csapi/openid.h
@@ -21,7 +21,7 @@ namespace Quotient {
* be used to request another OpenID access token or call `/sync`, for
* example.
*/
-class RequestOpenIdTokenJob : public BaseJob {
+class QUOTIENT_API RequestOpenIdTokenJob : public BaseJob {
public:
/*! \brief Get an OpenID token object to verify the requester's identity.
*
@@ -43,7 +43,10 @@ public:
/// Specification](http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse)
/// with the only difference being the lack of an `id_token`. Instead,
/// the Matrix homeserver's name is provided.
- OpenidToken tokenData() const { return fromJson<OpenidToken>(jsonData()); }
+ OpenIdCredentials tokenData() const
+ {
+ return fromJson<OpenIdCredentials>(jsonData());
+ }
};
} // namespace Quotient
diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp
index 70a5b6f3..9dd1445e 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/v3", "/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/v3", "/events"),
queryToPeekEvents(from, timeout, roomId))
{}
diff --git a/lib/csapi/peeking_events.h b/lib/csapi/peeking_events.h
index 885ff340..a67d2e4a 100644
--- a/lib/csapi/peeking_events.h
+++ b/lib/csapi/peeking_events.h
@@ -4,12 +4,12 @@
#pragma once
-#include "events/eventloader.h"
+#include "events/roomevent.h"
#include "jobs/basejob.h"
namespace Quotient {
-/*! \brief Listen on the event stream.
+/*! \brief Listen on the event stream of a particular room.
*
* This will listen for new events related to a particular room and return
* them to the caller. This will block until an event is received, or until
@@ -22,9 +22,9 @@ namespace Quotient {
* API will also be deprecated at some point, but its replacement is not
* yet known.
*/
-class PeekEventsJob : public BaseJob {
+class QUOTIENT_API PeekEventsJob : public BaseJob {
public:
- /*! \brief Listen on the event stream.
+ /*! \brief Listen on the event stream of a particular room.
*
* \param from
* The token to stream from. This token is either from a previous
diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp
index 58d0d157..828ccfb7 100644
--- a/lib/csapi/presence.cpp
+++ b/lib/csapi/presence.cpp
@@ -4,33 +4,29 @@
#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/v3", "/presence/", userId, "/status"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("presence"), presence);
- addParam<IfNotEmpty>(_data, QStringLiteral("status_msg"), statusMsg);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("presence"), presence);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("status_msg"), statusMsg);
+ setRequestData({ _dataJson });
}
QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/presence/" % userId % "/status");
+ makePath("/_matrix/client/v3", "/presence/",
+ userId, "/status"));
}
GetPresenceJob::GetPresenceJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetPresenceJob"),
- QStringLiteral("/_matrix/client/r0") % "/presence/" % userId
- % "/status")
+ makePath("/_matrix/client/v3", "/presence/", userId, "/status"))
{
addExpectedKey("presence");
}
diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h
index 4ab50e25..52445205 100644
--- a/lib/csapi/presence.h
+++ b/lib/csapi/presence.h
@@ -15,7 +15,7 @@ namespace Quotient {
* not need to specify the `last_active_ago` field. You cannot set the
* presence state of another user.
*/
-class SetPresenceJob : public BaseJob {
+class QUOTIENT_API SetPresenceJob : public BaseJob {
public:
/*! \brief Update this user's presence state.
*
@@ -36,7 +36,7 @@ public:
*
* Get the given user's presence state.
*/
-class GetPresenceJob : public BaseJob {
+class QUOTIENT_API GetPresenceJob : public BaseJob {
public:
/*! \brief Get this user's presence state.
*
diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp
index 8436b8e6..f024ed82 100644
--- a/lib/csapi/profile.cpp
+++ b/lib/csapi/profile.cpp
@@ -4,67 +4,63 @@
#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/v3", "/profile/", userId,
+ "/displayname"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("displayname"), displayname);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("displayname"), displayname);
+ setRequestData({ _dataJson });
}
QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/profile/" % userId % "/displayname");
+ makePath("/_matrix/client/v3", "/profile/",
+ userId, "/displayname"));
}
GetDisplayNameJob::GetDisplayNameJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetDisplayNameJob"),
- QStringLiteral("/_matrix/client/r0") % "/profile/" % userId
- % "/displayname",
+ makePath("/_matrix/client/v3", "/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/v3", "/profile/", userId, "/avatar_url"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("avatar_url"), avatarUrl);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("avatar_url"), avatarUrl);
+ setRequestData({ _dataJson });
}
QUrl GetAvatarUrlJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/profile/" % userId % "/avatar_url");
+ makePath("/_matrix/client/v3", "/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/v3", "/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/v3", "/profile/",
+ userId));
}
GetUserProfileJob::GetUserProfileJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetUserProfileJob"),
- QStringLiteral("/_matrix/client/r0") % "/profile/" % userId, false)
+ makePath("/_matrix/client/v3", "/profile/", userId), false)
{}
diff --git a/lib/csapi/profile.h b/lib/csapi/profile.h
index 8bbe4f8c..b00c944b 100644
--- a/lib/csapi/profile.h
+++ b/lib/csapi/profile.h
@@ -13,7 +13,7 @@ namespace Quotient {
* This API sets the given user's display name. You must have permission to
* set this user's display name, e.g. you need to have their `access_token`.
*/
-class SetDisplayNameJob : public BaseJob {
+class QUOTIENT_API SetDisplayNameJob : public BaseJob {
public:
/*! \brief Set the user's display name.
*
@@ -33,7 +33,7 @@ public:
* own displayname or to query the name of other users; either locally or
* on remote homeservers.
*/
-class GetDisplayNameJob : public BaseJob {
+class QUOTIENT_API GetDisplayNameJob : public BaseJob {
public:
/*! \brief Get the user's display name.
*
@@ -63,7 +63,7 @@ public:
* This API sets the given user's avatar URL. You must have permission to
* set this user's avatar URL, e.g. you need to have their `access_token`.
*/
-class SetAvatarUrlJob : public BaseJob {
+class QUOTIENT_API SetAvatarUrlJob : public BaseJob {
public:
/*! \brief Set the user's avatar URL.
*
@@ -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.
@@ -82,7 +82,7 @@ public:
* own avatar URL or to query the URL of other users; either locally or
* on remote homeservers.
*/
-class GetAvatarUrlJob : public BaseJob {
+class QUOTIENT_API GetAvatarUrlJob : public BaseJob {
public:
/*! \brief Get the user's avatar URL.
*
@@ -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.
@@ -111,7 +111,7 @@ public:
* locally or on remote homeservers. This API may return keys which are not
* limited to `displayname` or `avatar_url`.
*/
-class GetUserProfileJob : public BaseJob {
+class QUOTIENT_API GetUserProfileJob : public BaseJob {
public:
/*! \brief Get this user's profile information.
*
@@ -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..fb6595fc 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/v3", "/pushers"));
}
GetPushersJob::GetPushersJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushersJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushers")
+ makePath("/_matrix/client/v3", "/pushers"))
{}
PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind,
@@ -26,17 +23,18 @@ 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/v3", "/pushers/set"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("pushkey"), pushkey);
- addParam<>(_data, QStringLiteral("kind"), kind);
- addParam<>(_data, QStringLiteral("app_id"), appId);
- addParam<>(_data, QStringLiteral("app_display_name"), appDisplayName);
- addParam<>(_data, QStringLiteral("device_display_name"), deviceDisplayName);
- addParam<IfNotEmpty>(_data, QStringLiteral("profile_tag"), profileTag);
- addParam<>(_data, QStringLiteral("lang"), lang);
- addParam<>(_data, QStringLiteral("data"), data);
- addParam<IfNotEmpty>(_data, QStringLiteral("append"), append);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("pushkey"), pushkey);
+ addParam<>(_dataJson, QStringLiteral("kind"), kind);
+ addParam<>(_dataJson, QStringLiteral("app_id"), appId);
+ addParam<>(_dataJson, QStringLiteral("app_display_name"), appDisplayName);
+ addParam<>(_dataJson, QStringLiteral("device_display_name"),
+ deviceDisplayName);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("profile_tag"), profileTag);
+ addParam<>(_dataJson, QStringLiteral("lang"), lang);
+ addParam<>(_dataJson, QStringLiteral("data"), data);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("append"), append);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h
index 13c9ec25..d859ffc4 100644
--- a/lib/csapi/pusher.h
+++ b/lib/csapi/pusher.h
@@ -12,7 +12,7 @@ namespace Quotient {
*
* Gets all currently active pushers for the authenticated user.
*/
-class GetPushersJob : public BaseJob {
+class QUOTIENT_API GetPushersJob : public BaseJob {
public:
// Inner data structures
@@ -21,7 +21,7 @@ public:
struct PusherData {
/// Required if `kind` is `http`. The URL to use to send
/// notifications to.
- QString url;
+ QUrl url;
/// The format to use when sending notifications to the Push
/// Gateway.
QString format;
@@ -108,7 +108,7 @@ struct JsonObjectConverter<GetPushersJob::Pusher> {
* [pushers](/client-server-api/#push-notifications) for this user ID. The
* behaviour of this endpoint varies depending on the values in the JSON body.
*/
-class PostPusherJob : public BaseJob {
+class QUOTIENT_API PostPusherJob : public BaseJob {
public:
// Inner data structures
@@ -119,7 +119,7 @@ public:
/// Required if `kind` is `http`. The URL to use to send
/// notifications to. MUST be an HTTPS URL with a path of
/// `/_matrix/push/v1/notify`.
- QString url;
+ QUrl url;
/// The format to send notifications in to Push Gateways if the
/// `kind` is `http`. The details about what fields the
/// homeserver should send to the push gateway are defined in the
diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp
index 86165744..2376654a 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/v3", "/pushrules"));
}
GetPushRulesJob::GetPushRulesJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushRulesJob"),
- QStringLiteral("/_matrix/client/r0") % "/pushrules")
+ makePath("/_matrix/client/v3", "/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/v3", "/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/v3", "/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/v3", "/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/v3", "/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,15 +65,15 @@ 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/v3", "/pushrules/", scope, "/", kind,
+ "/", ruleId),
queryToSetPushRule(before, after))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("actions"), actions);
- addParam<IfNotEmpty>(_data, QStringLiteral("conditions"), conditions);
- addParam<IfNotEmpty>(_data, QStringLiteral("pattern"), pattern);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("actions"), actions);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("conditions"), conditions);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("pattern"), pattern);
+ setRequestData({ _dataJson });
}
QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
@@ -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/v3", "/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/v3", "/pushrules/", scope, "/", kind,
+ "/", ruleId, "/enabled"))
{
addExpectedKey("enabled");
}
@@ -105,12 +100,12 @@ 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/v3", "/pushrules/", scope, "/", kind,
+ "/", ruleId, "/enabled"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("enabled"), enabled);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("enabled"), enabled);
+ setRequestData({ _dataJson });
}
QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
@@ -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/v3", "/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/v3", "/pushrules/", scope, "/", kind,
+ "/", ruleId, "/actions"))
{
addExpectedKey("actions");
}
@@ -138,10 +133,10 @@ 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/v3", "/pushrules/", scope, "/", kind,
+ "/", ruleId, "/actions"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("actions"), actions);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("actions"), actions);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/pushrules.h b/lib/csapi/pushrules.h
index a5eb48f0..d6c57efd 100644
--- a/lib/csapi/pushrules.h
+++ b/lib/csapi/pushrules.h
@@ -19,7 +19,7 @@ namespace Quotient {
* `/pushrules/global/`. This will return a subset of this data under the
* specified key e.g. the `global` key.
*/
-class GetPushRulesJob : public BaseJob {
+class QUOTIENT_API GetPushRulesJob : public BaseJob {
public:
/// Retrieve all push rulesets.
explicit GetPushRulesJob();
@@ -44,7 +44,7 @@ public:
*
* Retrieve a single specified push rule.
*/
-class GetPushRuleJob : public BaseJob {
+class QUOTIENT_API GetPushRuleJob : public BaseJob {
public:
/*! \brief Retrieve a push rule.
*
@@ -79,7 +79,7 @@ public:
*
* This endpoint removes the push rule defined in the path.
*/
-class DeletePushRuleJob : public BaseJob {
+class QUOTIENT_API DeletePushRuleJob : public BaseJob {
public:
/*! \brief Delete a push rule.
*
@@ -112,7 +112,7 @@ public:
*
* When creating push rules, they MUST be enabled by default.
*/
-class SetPushRuleJob : public BaseJob {
+class QUOTIENT_API SetPushRuleJob : public BaseJob {
public:
/*! \brief Add or change a push rule.
*
@@ -160,7 +160,7 @@ public:
*
* This endpoint gets whether the specified push rule is enabled.
*/
-class IsPushRuleEnabledJob : public BaseJob {
+class QUOTIENT_API IsPushRuleEnabledJob : public BaseJob {
public:
/*! \brief Get whether a push rule is enabled
*
@@ -195,7 +195,7 @@ public:
*
* This endpoint allows clients to enable or disable the specified push rule.
*/
-class SetPushRuleEnabledJob : public BaseJob {
+class QUOTIENT_API SetPushRuleEnabledJob : public BaseJob {
public:
/*! \brief Enable or disable a push rule.
*
@@ -219,7 +219,7 @@ public:
*
* This endpoint get the actions for the specified push rule.
*/
-class GetPushRuleActionsJob : public BaseJob {
+class QUOTIENT_API GetPushRuleActionsJob : public BaseJob {
public:
/*! \brief The actions for a push rule
*
@@ -258,7 +258,7 @@ public:
* This endpoint allows clients to change the actions of a push rule.
* This can be used to change the actions of builtin rules.
*/
-class SetPushRuleActionsJob : public BaseJob {
+class QUOTIENT_API SetPushRuleActionsJob : public BaseJob {
public:
/*! \brief Set the actions for a push rule.
*
diff --git a/lib/csapi/read_markers.cpp b/lib/csapi/read_markers.cpp
index 39e4d148..febd6d3a 100644
--- a/lib/csapi/read_markers.cpp
+++ b/lib/csapi/read_markers.cpp
@@ -4,19 +4,19 @@
#include "read_markers.h"
-#include <QtCore/QStringBuilder>
-
using namespace Quotient;
SetReadMarkerJob::SetReadMarkerJob(const QString& roomId,
const QString& mFullyRead,
- const QString& mRead)
+ const QString& mRead,
+ const QString& mReadPrivate)
: BaseJob(HttpVerb::Post, QStringLiteral("SetReadMarkerJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/read_markers")
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/read_markers"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("m.fully_read"), mFullyRead);
- addParam<IfNotEmpty>(_data, QStringLiteral("m.read"), mRead);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("m.fully_read"), mFullyRead);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("m.read"), mRead);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("m.read.private"),
+ mReadPrivate);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h
index 00a2aa0d..1024076f 100644
--- a/lib/csapi/read_markers.h
+++ b/lib/csapi/read_markers.h
@@ -13,7 +13,7 @@ namespace Quotient {
* Sets the position of the read marker for a given room, and optionally
* the read receipt's location.
*/
-class SetReadMarkerJob : public BaseJob {
+class QUOTIENT_API SetReadMarkerJob : public BaseJob {
public:
/*! \brief Set the position of the read marker for a room.
*
@@ -28,9 +28,16 @@ public:
* The event ID to set the read receipt location at. This is
* equivalent to calling `/receipt/m.read/$elsewhere:example.org`
* and is provided here to save that extra call.
+ *
+ * \param mReadPrivate
+ * The event ID to set the *private* read receipt location at. This
+ * equivalent to calling `/receipt/m.read.private/$elsewhere:example.org`
+ * and is provided here to save that extra call.
*/
- explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead,
- const QString& mRead = {});
+ explicit SetReadMarkerJob(const QString& roomId,
+ const QString& mFullyRead = {},
+ const QString& mRead = {},
+ const QString& mReadPrivate = {});
};
} // namespace Quotient
diff --git a/lib/csapi/receipts.cpp b/lib/csapi/receipts.cpp
index 00d1c28a..0194603d 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/v3", "/rooms/", roomId, "/receipt/",
+ receiptType, "/", eventId))
{
- setRequestData(Data(toJson(receipt)));
+ setRequestData({ toJson(receipt) });
}
diff --git a/lib/csapi/receipts.h b/lib/csapi/receipts.h
index 7ac093cd..98bc5004 100644
--- a/lib/csapi/receipts.h
+++ b/lib/csapi/receipts.h
@@ -13,7 +13,7 @@ namespace Quotient {
* This API updates the marker for the given receipt type to the event ID
* specified.
*/
-class PostReceiptJob : public BaseJob {
+class QUOTIENT_API PostReceiptJob : public BaseJob {
public:
/*! \brief Send a receipt for the given event ID.
*
@@ -21,7 +21,13 @@ public:
* The room in which to send the event.
*
* \param receiptType
- * The type of receipt to send.
+ * The type of receipt to send. This can also be `m.fully_read` as an
+ * alternative to
+ * [`/read_makers`](/client-server-api/#post_matrixclientv3roomsroomidread_markers).
+ *
+ * Note that `m.fully_read` does not appear under `m.receipt`: this
+ * endpoint effectively calls `/read_markers` internally when presented with
+ * a receipt type of `m.fully_read`.
*
* \param eventId
* The event ID to acknowledge up to.
diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp
index 91497064..154abd9b 100644
--- a/lib/csapi/redaction.cpp
+++ b/lib/csapi/redaction.cpp
@@ -4,17 +4,15 @@
#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/v3", "/rooms/", roomId, "/redact/",
+ eventId, "/", txnId))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/redaction.h b/lib/csapi/redaction.h
index f0db9f9f..2f85793e 100644
--- a/lib/csapi/redaction.h
+++ b/lib/csapi/redaction.h
@@ -22,7 +22,7 @@ namespace Quotient {
*
* Server administrators may redact events sent by users on their server.
*/
-class RedactEventJob : public BaseJob {
+class QUOTIENT_API RedactEventJob : public BaseJob {
public:
/*! \brief Strips all non-integrity-critical information out of an event.
*
@@ -33,9 +33,9 @@ public:
* The ID of the event to redact
*
* \param txnId
- * The transaction ID for this event. Clients should generate a
- * unique ID; it will be used by the server to ensure idempotency of
- * requests.
+ * The [transaction ID](/client-server-api/#transaction-identifiers) for
+ * this event. Clients should generate a unique ID; it will be used by the
+ * server to ensure idempotency of requests.
*
* \param reason
* The reason for the event being redacted.
diff --git a/lib/csapi/refresh.cpp b/lib/csapi/refresh.cpp
new file mode 100644
index 00000000..284ae4ff
--- /dev/null
+++ b/lib/csapi/refresh.cpp
@@ -0,0 +1,18 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "refresh.h"
+
+using namespace Quotient;
+
+RefreshJob::RefreshJob(const QString& refreshToken)
+ : BaseJob(HttpVerb::Post, QStringLiteral("RefreshJob"),
+ makePath("/_matrix/client/v3", "/refresh"), false)
+{
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("refresh_token"),
+ refreshToken);
+ setRequestData({ _dataJson });
+ addExpectedKey("access_token");
+}
diff --git a/lib/csapi/refresh.h b/lib/csapi/refresh.h
new file mode 100644
index 00000000..d432802c
--- /dev/null
+++ b/lib/csapi/refresh.h
@@ -0,0 +1,65 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "jobs/basejob.h"
+
+namespace Quotient {
+
+/*! \brief Refresh an access token
+ *
+ * Refresh an access token. Clients should use the returned access token
+ * when making subsequent API calls, and store the returned refresh token
+ * (if given) in order to refresh the new access token when necessary.
+ *
+ * After an access token has been refreshed, a server can choose to
+ * invalidate the old access token immediately, or can choose not to, for
+ * example if the access token would expire soon anyways. Clients should
+ * not make any assumptions about the old access token still being valid,
+ * and should use the newly provided access token instead.
+ *
+ * The old refresh token remains valid until the new access token or refresh
+ * token is used, at which point the old refresh token is revoked.
+ *
+ * Note that this endpoint does not require authentication via an
+ * access token. Authentication is provided via the refresh token.
+ *
+ * Application Service identity assertion is disabled for this endpoint.
+ */
+class QUOTIENT_API RefreshJob : public BaseJob {
+public:
+ /*! \brief Refresh an access token
+ *
+ * \param refreshToken
+ * The refresh token
+ */
+ explicit RefreshJob(const QString& refreshToken = {});
+
+ // Result properties
+
+ /// The new access token to use.
+ QString accessToken() const
+ {
+ return loadFromJson<QString>("access_token"_ls);
+ }
+
+ /// The new refresh token to use when the access token needs to
+ /// be refreshed again. If not given, the old refresh token can
+ /// be re-used.
+ QString refreshToken() const
+ {
+ return loadFromJson<QString>("refresh_token"_ls);
+ }
+
+ /// The lifetime of the access token, in milliseconds. If not
+ /// given, the client can assume that the access token will not
+ /// expire.
+ Omittable<int> expiresInMs() const
+ {
+ return loadFromJson<Omittable<int>>("expires_in_ms"_ls);
+ }
+};
+
+} // namespace Quotient
diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp
index b80abc84..04c0fe12 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;
}
@@ -20,93 +18,97 @@ RegisterJob::RegisterJob(const QString& kind,
const QString& username, const QString& password,
const QString& deviceId,
const QString& initialDeviceDisplayName,
- Omittable<bool> inhibitLogin)
+ Omittable<bool> inhibitLogin,
+ Omittable<bool> refreshToken)
: BaseJob(HttpVerb::Post, QStringLiteral("RegisterJob"),
- QStringLiteral("/_matrix/client/r0") % "/register",
+ makePath("/_matrix/client/v3", "/register"),
queryToRegister(kind), {}, false)
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
- addParam<IfNotEmpty>(_data, QStringLiteral("username"), username);
- addParam<IfNotEmpty>(_data, QStringLiteral("password"), password);
- addParam<IfNotEmpty>(_data, QStringLiteral("device_id"), deviceId);
- addParam<IfNotEmpty>(_data, QStringLiteral("initial_device_display_name"),
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("username"), username);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("password"), password);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("device_id"), deviceId);
+ addParam<IfNotEmpty>(_dataJson,
+ QStringLiteral("initial_device_display_name"),
initialDeviceDisplayName);
- addParam<IfNotEmpty>(_data, QStringLiteral("inhibit_login"), inhibitLogin);
- setRequestData(std::move(_data));
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("inhibit_login"),
+ inhibitLogin);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("refresh_token"),
+ refreshToken);
+ setRequestData({ _dataJson });
addExpectedKey("user_id");
}
RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob(
const EmailValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterEmailJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/register/email/requestToken",
+ makePath("/_matrix/client/v3", "/register/email/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData({ toJson(body) });
}
RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob(
const MsisdnValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterMSISDNJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/register/msisdn/requestToken",
+ makePath("/_matrix/client/v3", "/register/msisdn/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData({ toJson(body) });
}
ChangePasswordJob::ChangePasswordJob(const QString& newPassword,
- Omittable<bool> logoutDevices,
+ bool logoutDevices,
const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/password")
+ makePath("/_matrix/client/v3", "/account/password"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("new_password"), newPassword);
- addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices);
- addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("new_password"), newPassword);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("logout_devices"),
+ logoutDevices);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth);
+ setRequestData({ _dataJson });
}
RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob(
const EmailValidationData& body)
: BaseJob(HttpVerb::Post,
QStringLiteral("RequestTokenToResetPasswordEmailJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/account/password/email/requestToken",
+ makePath("/_matrix/client/v3",
+ "/account/password/email/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData({ toJson(body) });
}
RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob(
const MsisdnValidationData& body)
: BaseJob(HttpVerb::Post,
QStringLiteral("RequestTokenToResetPasswordMSISDNJob"),
- QStringLiteral("/_matrix/client/r0")
- % "/account/password/msisdn/requestToken",
+ makePath("/_matrix/client/v3",
+ "/account/password/msisdn/requestToken"),
false)
{
- setRequestData(Data(toJson(body)));
+ setRequestData({ 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/v3", "/account/deactivate"))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
- addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("id_server"), idServer);
+ setRequestData({ _dataJson });
addExpectedKey("id_server_unbind_result");
}
auto queryToCheckUsernameAvailability(const QString& username)
{
- BaseJob::Query _q;
+ QUrlQuery _q;
addParam<>(_q, QStringLiteral("username"), username);
return _q;
}
@@ -115,13 +117,14 @@ QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl,
const QString& username)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/register/available",
+ makePath("/_matrix/client/v3",
+ "/register/available"),
queryToCheckUsernameAvailability(username));
}
-CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& username)
+CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(
+ const QString& username)
: BaseJob(HttpVerb::Get, QStringLiteral("CheckUsernameAvailabilityJob"),
- QStringLiteral("/_matrix/client/r0") % "/register/available",
+ makePath("/_matrix/client/v3", "/register/available"),
queryToCheckUsernameAvailability(username), {}, false)
{}
diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h
index ae8fc162..21d7f9d7 100644
--- a/lib/csapi/registration.h
+++ b/lib/csapi/registration.h
@@ -59,7 +59,7 @@ namespace Quotient {
* Any user ID returned by this API must conform to the grammar given in the
* [Matrix specification](/appendices/#user-identifiers).
*/
-class RegisterJob : public BaseJob {
+class QUOTIENT_API RegisterJob : public BaseJob {
public:
/*! \brief Register for an account on this homeserver.
*
@@ -93,6 +93,9 @@ public:
* If true, an `access_token` and `device_id` should not be
* returned from this call, therefore preventing an automatic
* login. Defaults to false.
+ *
+ * \param refreshToken
+ * If true, the client supports refresh tokens.
*/
explicit RegisterJob(const QString& kind = QStringLiteral("user"),
const Omittable<AuthenticationData>& auth = none,
@@ -100,7 +103,8 @@ public:
const QString& password = {},
const QString& deviceId = {},
const QString& initialDeviceDisplayName = {},
- Omittable<bool> inhibitLogin = none);
+ Omittable<bool> inhibitLogin = none,
+ Omittable<bool> refreshToken = none);
// Result properties
@@ -118,15 +122,27 @@ public:
return loadFromJson<QString>("access_token"_ls);
}
- /// The server_name of the homeserver on which the account has
- /// been registered.
+ /// A refresh token for the account. This token can be used to
+ /// obtain a new access token when it expires by calling the
+ /// `/refresh` endpoint.
+ ///
+ /// Omitted if the `inhibit_login` option is true.
+ QString refreshToken() const
+ {
+ return loadFromJson<QString>("refresh_token"_ls);
+ }
+
+ /// The lifetime of the access token, in milliseconds. Once
+ /// the access token has expired a new access token can be
+ /// obtained by using the provided refresh token. If no
+ /// refresh token is provided, the client will need to re-log in
+ /// to obtain a new access token. If not given, the client can
+ /// assume that the access token will not expire.
///
- /// **Deprecated**. Clients should extract the server_name from
- /// `user_id` (by splitting at the first colon) if they require
- /// it. Note also that `homeserver` is not spelt this way.
- QString homeServer() const
+ /// Omitted if the `inhibit_login` option is true.
+ Omittable<int> expiresInMs() const
{
- return loadFromJson<QString>("home_server"_ls);
+ return loadFromJson<Omittable<int>>("expires_in_ms"_ls);
}
/// ID of the registered device. Will be the same as the
@@ -143,7 +159,7 @@ public:
* should validate the email itself, either by sending a validation email
* itself or by using a service it has control over.
*/
-class RequestTokenToRegisterEmailJob : public BaseJob {
+class QUOTIENT_API RequestTokenToRegisterEmailJob : public BaseJob {
public:
/*! \brief Begins the validation process for an email to be used during
* registration.
@@ -175,7 +191,7 @@ public:
* should validate the phone number itself, either by sending a validation
* message itself or by using a service it has control over.
*/
-class RequestTokenToRegisterMSISDNJob : public BaseJob {
+class QUOTIENT_API RequestTokenToRegisterMSISDNJob : public BaseJob {
public:
/*! \brief Requests a validation token be sent to the given phone number for
* the purpose of registering an account
@@ -215,7 +231,7 @@ public:
* access token provided in the request. Whether other access tokens for
* the user are revoked depends on the request parameters.
*/
-class ChangePasswordJob : public BaseJob {
+class QUOTIENT_API ChangePasswordJob : public BaseJob {
public:
/*! \brief Changes a user's password.
*
@@ -227,14 +243,15 @@ public:
* should be revoked if the request succeeds.
*
* When `false`, the server can still take advantage of the [soft logout
- * method](/client-server-api/#soft-logout) for the user's remaining devices.
+ * method](/client-server-api/#soft-logout) for the user's remaining
+ * devices.
*
* \param auth
* Additional authentication information for the user-interactive
* authentication API.
*/
explicit ChangePasswordJob(const QString& newPassword,
- Omittable<bool> logoutDevices = none,
+ bool logoutDevices = true,
const Omittable<AuthenticationData>& auth = none);
};
@@ -247,7 +264,7 @@ public:
* `/account/password` endpoint.
*
* This API's parameters and response are identical to that of the
- * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken)
+ * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken)
* endpoint, except that
* `M_THREEPID_NOT_FOUND` may be returned if no account matching the
* given email address could be found. The server may instead send an
@@ -257,7 +274,7 @@ public:
* The homeserver should validate the email itself, either by sending a
* validation email itself or by using a service it has control over.
*/
-class RequestTokenToResetPasswordEmailJob : public BaseJob {
+class QUOTIENT_API RequestTokenToResetPasswordEmailJob : public BaseJob {
public:
/*! \brief Requests a validation token be sent to the given email address
* for the purpose of resetting a user's password
@@ -269,7 +286,7 @@ public:
* `/account/password` endpoint.
*
* This API's parameters and response are identical to that of the
- * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken)
+ * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken)
* endpoint, except that
* `M_THREEPID_NOT_FOUND` may be returned if no account matching the
* given email address could be found. The server may instead send an
@@ -299,7 +316,7 @@ public:
* `/account/password` endpoint.
*
* This API's parameters and response are identical to that of the
- * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken)
+ * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken)
* endpoint, except that
* `M_THREEPID_NOT_FOUND` may be returned if no account matching the
* given phone number could be found. The server may instead send the SMS
@@ -309,7 +326,7 @@ public:
* The homeserver should validate the phone number itself, either by sending a
* validation message itself or by using a service it has control over.
*/
-class RequestTokenToResetPasswordMSISDNJob : public BaseJob {
+class QUOTIENT_API RequestTokenToResetPasswordMSISDNJob : public BaseJob {
public:
/*! \brief Requests a validation token be sent to the given phone number for
* the purpose of resetting a user's password.
@@ -321,15 +338,16 @@ public:
* `/account/password` endpoint.
*
* This API's parameters and response are identical to that of the
- * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken)
+ * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken)
* endpoint, except that
* `M_THREEPID_NOT_FOUND` may be returned if no account matching the
* given phone number could be found. The server may instead send the SMS
* to the given phone number prompting the user to create an account.
* `M_THREEPID_IN_USE` may not be returned.
*
- * The homeserver should validate the phone number itself, either by sending
- * a validation message itself or by using a service it has control over.
+ * The homeserver should validate the phone number itself, either by
+ * sending a validation message itself or by using a service it has control
+ * over.
*/
explicit RequestTokenToResetPasswordMSISDNJob(
const MsisdnValidationData& body);
@@ -361,7 +379,7 @@ public:
* parameter because the homeserver is expected to sign the request to the
* identity server instead.
*/
-class DeactivateAccountJob : public BaseJob {
+class QUOTIENT_API DeactivateAccountJob : public BaseJob {
public:
/*! \brief Deactivate a user's account.
*
@@ -377,8 +395,9 @@ public:
* it must return an `id_server_unbind_result` of
* `no-support`.
*/
- explicit DeactivateAccountJob(const Omittable<AuthenticationData>& auth = none,
- const QString& idServer = {});
+ explicit DeactivateAccountJob(
+ const Omittable<AuthenticationData>& auth = none,
+ const QString& idServer = {});
// Result properties
@@ -411,7 +430,7 @@ public:
* reserve the username. This can mean that the username becomes unavailable
* between checking its availability and attempting to register it.
*/
-class CheckUsernameAvailabilityJob : public BaseJob {
+class QUOTIENT_API CheckUsernameAvailabilityJob : public BaseJob {
public:
/*! \brief Checks to see if a username is available on the server.
*
diff --git a/lib/csapi/registration_tokens.cpp b/lib/csapi/registration_tokens.cpp
new file mode 100644
index 00000000..9c1f0587
--- /dev/null
+++ b/lib/csapi/registration_tokens.cpp
@@ -0,0 +1,33 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "registration_tokens.h"
+
+using namespace Quotient;
+
+auto queryToRegistrationTokenValidity(const QString& token)
+{
+ QUrlQuery _q;
+ addParam<>(_q, QStringLiteral("token"), token);
+ return _q;
+}
+
+QUrl RegistrationTokenValidityJob::makeRequestUrl(QUrl baseUrl,
+ const QString& token)
+{
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl),
+ makePath("/_matrix/client/v1",
+ "/register/m.login.registration_token/validity"),
+ queryToRegistrationTokenValidity(token));
+}
+
+RegistrationTokenValidityJob::RegistrationTokenValidityJob(const QString& token)
+ : BaseJob(HttpVerb::Get, QStringLiteral("RegistrationTokenValidityJob"),
+ makePath("/_matrix/client/v1",
+ "/register/m.login.registration_token/validity"),
+ queryToRegistrationTokenValidity(token), {}, false)
+{
+ addExpectedKey("valid");
+}
diff --git a/lib/csapi/registration_tokens.h b/lib/csapi/registration_tokens.h
new file mode 100644
index 00000000..e3008dd4
--- /dev/null
+++ b/lib/csapi/registration_tokens.h
@@ -0,0 +1,44 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "jobs/basejob.h"
+
+namespace Quotient {
+
+/*! \brief Query if a given registration token is still valid.
+ *
+ * Queries the server to determine if a given registration token is still
+ * valid at the time of request. This is a point-in-time check where the
+ * token might still expire by the time it is used.
+ *
+ * Servers should be sure to rate limit this endpoint to avoid brute force
+ * attacks.
+ */
+class QUOTIENT_API RegistrationTokenValidityJob : public BaseJob {
+public:
+ /*! \brief Query if a given registration token is still valid.
+ *
+ * \param token
+ * The token to check validity of.
+ */
+ explicit RegistrationTokenValidityJob(const QString& token);
+
+ /*! \brief Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for RegistrationTokenValidityJob
+ * is necessary but the job itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& token);
+
+ // Result properties
+
+ /// True if the token is still valid, false otherwise. This should
+ /// additionally be false if the token is not a recognised token by
+ /// the server.
+ bool valid() const { return loadFromJson<bool>("valid"_ls); }
+};
+
+} // namespace Quotient
diff --git a/lib/csapi/relations.cpp b/lib/csapi/relations.cpp
new file mode 100644
index 00000000..1d8febcc
--- /dev/null
+++ b/lib/csapi/relations.cpp
@@ -0,0 +1,118 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "relations.h"
+
+using namespace Quotient;
+
+auto queryToGetRelatingEvents(const QString& from, const QString& to,
+ Omittable<int> limit, const QString& dir)
+{
+ QUrlQuery _q;
+ addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
+ addParam<IfNotEmpty>(_q, QStringLiteral("to"), to);
+ addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
+ addParam<IfNotEmpty>(_q, QStringLiteral("dir"), dir);
+ return _q;
+}
+
+QUrl GetRelatingEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
+ const QString& eventId,
+ const QString& from, const QString& to,
+ Omittable<int> limit,
+ const QString& dir)
+{
+ return BaseJob::makeRequestUrl(std::move(baseUrl),
+ makePath("/_matrix/client/v1", "/rooms/",
+ roomId, "/relations/", eventId),
+ queryToGetRelatingEvents(from, to, limit,
+ dir));
+}
+
+GetRelatingEventsJob::GetRelatingEventsJob(
+ const QString& roomId, const QString& eventId, const QString& from,
+ const QString& to, Omittable<int> limit, const QString& dir)
+ : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsJob"),
+ makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/",
+ eventId),
+ queryToGetRelatingEvents(from, to, limit, dir))
+{
+ addExpectedKey("chunk");
+}
+
+auto queryToGetRelatingEventsWithRelType(const QString& from, const QString& to,
+ Omittable<int> limit,
+ const QString& dir)
+{
+ QUrlQuery _q;
+ addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
+ addParam<IfNotEmpty>(_q, QStringLiteral("to"), to);
+ addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
+ addParam<IfNotEmpty>(_q, QStringLiteral("dir"), dir);
+ return _q;
+}
+
+QUrl GetRelatingEventsWithRelTypeJob::makeRequestUrl(
+ QUrl baseUrl, const QString& roomId, const QString& eventId,
+ const QString& relType, const QString& from, const QString& to,
+ Omittable<int> limit, const QString& dir)
+{
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl),
+ makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/",
+ eventId, "/", relType),
+ queryToGetRelatingEventsWithRelType(from, to, limit, dir));
+}
+
+GetRelatingEventsWithRelTypeJob::GetRelatingEventsWithRelTypeJob(
+ const QString& roomId, const QString& eventId, const QString& relType,
+ const QString& from, const QString& to, Omittable<int> limit,
+ const QString& dir)
+ : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsWithRelTypeJob"),
+ makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/",
+ eventId, "/", relType),
+ queryToGetRelatingEventsWithRelType(from, to, limit, dir))
+{
+ addExpectedKey("chunk");
+}
+
+auto queryToGetRelatingEventsWithRelTypeAndEventType(const QString& from,
+ const QString& to,
+ Omittable<int> limit,
+ const QString& dir)
+{
+ QUrlQuery _q;
+ addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
+ addParam<IfNotEmpty>(_q, QStringLiteral("to"), to);
+ addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
+ addParam<IfNotEmpty>(_q, QStringLiteral("dir"), dir);
+ return _q;
+}
+
+QUrl GetRelatingEventsWithRelTypeAndEventTypeJob::makeRequestUrl(
+ QUrl baseUrl, const QString& roomId, const QString& eventId,
+ const QString& relType, const QString& eventType, const QString& from,
+ const QString& to, Omittable<int> limit, const QString& dir)
+{
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl),
+ makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/",
+ eventId, "/", relType, "/", eventType),
+ queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit, dir));
+}
+
+GetRelatingEventsWithRelTypeAndEventTypeJob::
+ GetRelatingEventsWithRelTypeAndEventTypeJob(
+ const QString& roomId, const QString& eventId, const QString& relType,
+ const QString& eventType, const QString& from, const QString& to,
+ Omittable<int> limit, const QString& dir)
+ : BaseJob(HttpVerb::Get,
+ QStringLiteral("GetRelatingEventsWithRelTypeAndEventTypeJob"),
+ makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/",
+ eventId, "/", relType, "/", eventType),
+ queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit,
+ dir))
+{
+ addExpectedKey("chunk");
+}
diff --git a/lib/csapi/relations.h b/lib/csapi/relations.h
new file mode 100644
index 00000000..5d6efd1c
--- /dev/null
+++ b/lib/csapi/relations.h
@@ -0,0 +1,298 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "events/roomevent.h"
+#include "jobs/basejob.h"
+
+namespace Quotient {
+
+/*! \brief Get the child events for a given parent event.
+ *
+ * Retrieve all of the child events for a given parent event.
+ *
+ * Note that when paginating the `from` token should be "after" the `to` token
+ * in terms of topological ordering, because it is only possible to paginate
+ * "backwards" through events, starting at `from`.
+ *
+ * For example, passing a `from` token from page 2 of the results, and a `to`
+ * token from page 1, would return the empty set. The caller can use a `from`
+ * token from page 1 and a `to` token from page 2 to paginate over the same
+ * range, however.
+ */
+class QUOTIENT_API GetRelatingEventsJob : public BaseJob {
+public:
+ /*! \brief Get the child events for a given parent event.
+ *
+ * \param roomId
+ * The ID of the room containing the parent event.
+ *
+ * \param eventId
+ * The ID of the parent event whose child events are to be returned.
+ *
+ * \param from
+ * The pagination token to start returning results from. If not supplied,
+ * results start at the most recent topological event known to the server.
+ *
+ * Can be a `next_batch` or `prev_batch` token from a previous call, or a
+ * returned `start` token from
+ * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages),
+ * or a `next_batch` token from
+ * [`/sync`](/client-server-api/#get_matrixclientv3sync).
+ *
+ * \param to
+ * The pagination token to stop returning results at. If not supplied,
+ * results continue up to `limit` or until there are no more events.
+ *
+ * Like `from`, this can be a previous token from a prior call to this
+ * endpoint or from `/messages` or `/sync`.
+ *
+ * \param limit
+ * The maximum number of results to return in a single `chunk`. The server
+ * can and should apply a maximum value to this parameter to avoid large
+ * responses.
+ *
+ * Similarly, the server should apply a default value when not supplied.
+ *
+ * \param dir
+ * Optional (default `b`) direction to return events from. If this is set
+ * to `f`, events will be returned in chronological order starting at
+ * `from`. If it is set to `b`, events will be returned in *reverse*
+ * chronological order, again starting at `from`.
+ */
+ explicit GetRelatingEventsJob(const QString& roomId, const QString& eventId,
+ const QString& from = {},
+ const QString& to = {},
+ Omittable<int> limit = none,
+ const QString& dir = {});
+
+ /*! \brief Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for GetRelatingEventsJob
+ * is necessary but the job itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId,
+ const QString& eventId, const QString& from = {},
+ const QString& to = {},
+ Omittable<int> limit = none,
+ const QString& dir = {});
+
+ // Result properties
+
+ /// The child events of the requested event, ordered topologically
+ /// most-recent first.
+ RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); }
+
+ /// An opaque string representing a pagination token. The absence of this
+ /// token means there are no more results to fetch and the client should
+ /// stop paginating.
+ QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); }
+
+ /// An opaque string representing a pagination token. The absence of this
+ /// token means this is the start of the result set, i.e. this is the first
+ /// batch/page.
+ QString prevBatch() const { return loadFromJson<QString>("prev_batch"_ls); }
+};
+
+/*! \brief Get the child events for a given parent event, with a given
+ * `relType`.
+ *
+ * Retrieve all of the child events for a given parent event which relate to the
+ * parent using the given `relType`.
+ *
+ * Note that when paginating the `from` token should be "after" the `to` token
+ * in terms of topological ordering, because it is only possible to paginate
+ * "backwards" through events, starting at `from`.
+ *
+ * For example, passing a `from` token from page 2 of the results, and a `to`
+ * token from page 1, would return the empty set. The caller can use a `from`
+ * token from page 1 and a `to` token from page 2 to paginate over the same
+ * range, however.
+ */
+class QUOTIENT_API GetRelatingEventsWithRelTypeJob : public BaseJob {
+public:
+ /*! \brief Get the child events for a given parent event, with a given
+ * `relType`.
+ *
+ * \param roomId
+ * The ID of the room containing the parent event.
+ *
+ * \param eventId
+ * The ID of the parent event whose child events are to be returned.
+ *
+ * \param relType
+ * The [relationship type](/client-server-api/#relationship-types) to
+ * search for.
+ *
+ * \param from
+ * The pagination token to start returning results from. If not supplied,
+ * results start at the most recent topological event known to the server.
+ *
+ * Can be a `next_batch` or `prev_batch` token from a previous call, or a
+ * returned `start` token from
+ * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages),
+ * or a `next_batch` token from
+ * [`/sync`](/client-server-api/#get_matrixclientv3sync).
+ *
+ * \param to
+ * The pagination token to stop returning results at. If not supplied,
+ * results continue up to `limit` or until there are no more events.
+ *
+ * Like `from`, this can be a previous token from a prior call to this
+ * endpoint or from `/messages` or `/sync`.
+ *
+ * \param limit
+ * The maximum number of results to return in a single `chunk`. The server
+ * can and should apply a maximum value to this parameter to avoid large
+ * responses.
+ *
+ * Similarly, the server should apply a default value when not supplied.
+ *
+ * \param dir
+ * Optional (default `b`) direction to return events from. If this is set
+ * to `f`, events will be returned in chronological order starting at
+ * `from`. If it is set to `b`, events will be returned in *reverse*
+ * chronological order, again starting at `from`.
+ */
+ explicit GetRelatingEventsWithRelTypeJob(
+ const QString& roomId, const QString& eventId, const QString& relType,
+ const QString& from = {}, const QString& to = {},
+ Omittable<int> limit = none, const QString& dir = {});
+
+ /*! \brief Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for GetRelatingEventsWithRelTypeJob
+ * is necessary but the job itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId,
+ const QString& eventId, const QString& relType,
+ const QString& from = {}, const QString& to = {},
+ Omittable<int> limit = none,
+ const QString& dir = {});
+
+ // Result properties
+
+ /// The child events of the requested event, ordered topologically
+ /// most-recent first. The events returned will match the `relType`
+ /// supplied in the URL.
+ RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); }
+
+ /// An opaque string representing a pagination token. The absence of this
+ /// token means there are no more results to fetch and the client should
+ /// stop paginating.
+ QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); }
+
+ /// An opaque string representing a pagination token. The absence of this
+ /// token means this is the start of the result set, i.e. this is the first
+ /// batch/page.
+ QString prevBatch() const { return loadFromJson<QString>("prev_batch"_ls); }
+};
+
+/*! \brief Get the child events for a given parent event, with a given `relType`
+ * and `eventType`.
+ *
+ * Retrieve all of the child events for a given parent event which relate to the
+ * parent using the given `relType` and have the given `eventType`.
+ *
+ * Note that when paginating the `from` token should be "after" the `to` token
+ * in terms of topological ordering, because it is only possible to paginate
+ * "backwards" through events, starting at `from`.
+ *
+ * For example, passing a `from` token from page 2 of the results, and a `to`
+ * token from page 1, would return the empty set. The caller can use a `from`
+ * token from page 1 and a `to` token from page 2 to paginate over the same
+ * range, however.
+ */
+class QUOTIENT_API GetRelatingEventsWithRelTypeAndEventTypeJob
+ : public BaseJob {
+public:
+ /*! \brief Get the child events for a given parent event, with a given
+ * `relType` and `eventType`.
+ *
+ * \param roomId
+ * The ID of the room containing the parent event.
+ *
+ * \param eventId
+ * The ID of the parent event whose child events are to be returned.
+ *
+ * \param relType
+ * The [relationship type](/client-server-api/#relationship-types) to
+ * search for.
+ *
+ * \param eventType
+ * The event type of child events to search for.
+ *
+ * Note that in encrypted rooms this will typically always be
+ * `m.room.encrypted` regardless of the event type contained within the
+ * encrypted payload.
+ *
+ * \param from
+ * The pagination token to start returning results from. If not supplied,
+ * results start at the most recent topological event known to the server.
+ *
+ * Can be a `next_batch` or `prev_batch` token from a previous call, or a
+ * returned `start` token from
+ * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages),
+ * or a `next_batch` token from
+ * [`/sync`](/client-server-api/#get_matrixclientv3sync).
+ *
+ * \param to
+ * The pagination token to stop returning results at. If not supplied,
+ * results continue up to `limit` or until there are no more events.
+ *
+ * Like `from`, this can be a previous token from a prior call to this
+ * endpoint or from `/messages` or `/sync`.
+ *
+ * \param limit
+ * The maximum number of results to return in a single `chunk`. The server
+ * can and should apply a maximum value to this parameter to avoid large
+ * responses.
+ *
+ * Similarly, the server should apply a default value when not supplied.
+ *
+ * \param dir
+ * Optional (default `b`) direction to return events from. If this is set
+ * to `f`, events will be returned in chronological order starting at
+ * `from`. If it is set to `b`, events will be returned in *reverse*
+ * chronological order, again starting at `from`.
+ */
+ explicit GetRelatingEventsWithRelTypeAndEventTypeJob(
+ const QString& roomId, const QString& eventId, const QString& relType,
+ const QString& eventType, const QString& from = {},
+ const QString& to = {}, Omittable<int> limit = none,
+ const QString& dir = {});
+
+ /*! \brief Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for
+ * GetRelatingEventsWithRelTypeAndEventTypeJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId,
+ const QString& eventId, const QString& relType,
+ const QString& eventType,
+ const QString& from = {}, const QString& to = {},
+ Omittable<int> limit = none,
+ const QString& dir = {});
+
+ // Result properties
+
+ /// The child events of the requested event, ordered topologically
+ /// most-recent first. The events returned will match the `relType` and
+ /// `eventType` supplied in the URL.
+ RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); }
+
+ /// An opaque string representing a pagination token. The absence of this
+ /// token means there are no more results to fetch and the client should
+ /// stop paginating.
+ QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); }
+
+ /// An opaque string representing a pagination token. The absence of this
+ /// token means this is the start of the result set, i.e. this is the first
+ /// batch/page.
+ QString prevBatch() const { return loadFromJson<QString>("prev_batch"_ls); }
+};
+
+} // namespace Quotient
diff --git a/lib/csapi/report_content.cpp b/lib/csapi/report_content.cpp
index 0a41625f..bc52208f 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/v3", "/rooms/", roomId, "/report/",
+ eventId))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("score"), score);
- addParam<>(_data, QStringLiteral("reason"), reason);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("score"), score);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/report_content.h b/lib/csapi/report_content.h
index 375e1829..8c533c19 100644
--- a/lib/csapi/report_content.h
+++ b/lib/csapi/report_content.h
@@ -13,7 +13,7 @@ namespace Quotient {
* Reports an event as inappropriate to the server, which may then notify
* the appropriate people.
*/
-class ReportContentJob : public BaseJob {
+class QUOTIENT_API ReportContentJob : public BaseJob {
public:
/*! \brief Reports an event as inappropriate.
*
@@ -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..2319496f 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/v3", "/rooms/", roomId, "/send/",
+ eventType, "/", txnId))
{
- setRequestData(Data(toJson(body)));
+ setRequestData({ toJson(body) });
addExpectedKey("event_id");
}
diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h
index 96f5beca..abe5f207 100644
--- a/lib/csapi/room_send.h
+++ b/lib/csapi/room_send.h
@@ -16,9 +16,10 @@ namespace Quotient {
*
* The body of the request should be the content object of the event; the
* fields in this object will vary depending on the type of event. See
- * [Room Events](/client-server-api/#room-events) for the m. event specification.
+ * [Room Events](/client-server-api/#room-events) for the m. event
+ * specification.
*/
-class SendMessageJob : public BaseJob {
+class QUOTIENT_API SendMessageJob : public BaseJob {
public:
/*! \brief Send a message event to the given room.
*
@@ -29,9 +30,10 @@ public:
* The type of event to send.
*
* \param txnId
- * The transaction ID for this event. Clients should generate an
- * ID unique across requests with the same access token; it will be
- * used by the server to ensure idempotency of requests.
+ * The [transaction ID](/client-server-api/#transaction-identifiers) for
+ * this event. Clients should generate an ID unique across requests with the
+ * same access token; it will be used by the server to ensure idempotency of
+ * requests.
*
* \param body
* This endpoint is used to send a message event to a room. Message events
diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp
index e18108ac..b4adb739 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/v3", "/rooms/", roomId, "/state/",
+ eventType, "/", stateKey))
{
- setRequestData(Data(toJson(body)));
+ setRequestData({ toJson(body) });
addExpectedKey("event_id");
}
diff --git a/lib/csapi/room_state.h b/lib/csapi/room_state.h
index f95af223..a00b0947 100644
--- a/lib/csapi/room_state.h
+++ b/lib/csapi/room_state.h
@@ -29,7 +29,7 @@ namespace Quotient {
* state event is to be sent. Servers do not validate aliases which are
* being removed or are already present in the state event.
*/
-class SetRoomStateWithKeyJob : public BaseJob {
+class QUOTIENT_API SetRoomStateWithKeyJob : public BaseJob {
public:
/*! \brief Send a state event to the given room.
*
diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp
index e3791b08..b03fb6e8 100644
--- a/lib/csapi/room_upgrades.cpp
+++ b/lib/csapi/room_upgrades.cpp
@@ -4,17 +4,14 @@
#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/v3", "/rooms/", roomId, "/upgrade"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("new_version"), newVersion);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("new_version"), newVersion);
+ setRequestData({ _dataJson });
addExpectedKey("replacement_room");
}
diff --git a/lib/csapi/room_upgrades.h b/lib/csapi/room_upgrades.h
index 58327587..0432f667 100644
--- a/lib/csapi/room_upgrades.h
+++ b/lib/csapi/room_upgrades.h
@@ -12,7 +12,7 @@ namespace Quotient {
*
* Upgrades the given room to a particular room version.
*/
-class UpgradeRoomJob : public BaseJob {
+class QUOTIENT_API UpgradeRoomJob : public BaseJob {
public:
/*! \brief Upgrades a room to a new room version.
*
diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp
index 724d941f..563f4fa5 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/v3", "/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/v3", "/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/v3", "/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/v3", "/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/v3", "/rooms/",
+ roomId, "/state"));
}
GetRoomStateJob::GetRoomStateJob(const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateJob"),
- QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId
- % "/state")
+ makePath("/_matrix/client/v3", "/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/v3", "/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/v3", "/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/v3", "/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/v3", "/rooms/", roomId,
+ "/joined_members"))
{}
diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h
index 51af2c65..7823a1b0 100644
--- a/lib/csapi/rooms.h
+++ b/lib/csapi/rooms.h
@@ -4,8 +4,8 @@
#pragma once
-#include "events/eventloader.h"
-#include "events/roommemberevent.h"
+#include "events/roomevent.h"
+#include "events/stateevent.h"
#include "jobs/basejob.h"
namespace Quotient {
@@ -15,7 +15,7 @@ namespace Quotient {
* Get a single event based on `roomId/eventId`. You must have permission to
* retrieve this event e.g. by being a member in the room for this event.
*/
-class GetOneRoomEventJob : public BaseJob {
+class QUOTIENT_API GetOneRoomEventJob : public BaseJob {
public:
/*! \brief Get a single event by event ID.
*
@@ -38,7 +38,7 @@ public:
// Result properties
/// The full event.
- EventPtr event() { return fromJson<EventPtr>(jsonData()); }
+ RoomEventPtr event() { return fromJson<RoomEventPtr>(jsonData()); }
};
/*! \brief Get the state identified by the type and key.
@@ -48,7 +48,7 @@ public:
* state of the room. If the user has left the room then the state is
* taken from the state of the room when they left.
*/
-class GetRoomStateWithKeyJob : public BaseJob {
+class QUOTIENT_API GetRoomStateWithKeyJob : public BaseJob {
public:
/*! \brief Get the state identified by the type and key.
*
@@ -80,7 +80,7 @@ public:
*
* Get the state events for the current state of a room.
*/
-class GetRoomStateJob : public BaseJob {
+class QUOTIENT_API GetRoomStateJob : public BaseJob {
public:
/*! \brief Get all state events in the current state of a room.
*
@@ -106,7 +106,7 @@ public:
*
* Get the list of members for this room.
*/
-class GetMembersByRoomJob : public BaseJob {
+class QUOTIENT_API GetMembersByRoomJob : public BaseJob {
public:
/*! \brief Get the m.room.member events for the room.
*
@@ -146,10 +146,7 @@ public:
// Result properties
/// Get the list of members for this room.
- EventsArray<RoomMemberEvent> chunk()
- {
- return takeFromJson<EventsArray<RoomMemberEvent>>("chunk"_ls);
- }
+ StateEvents chunk() { return takeFromJson<StateEvents>("chunk"_ls); }
};
/*! \brief Gets the list of currently joined users and their profile data.
@@ -157,11 +154,10 @@ public:
* This API returns a map of MXIDs to member info objects for members of the
* room. The current user must be in the room for it to work, unless it is an
* Application Service in which case any of the AS's users must be in the room.
- * This API is primarily for Application Services and should be faster to
- * respond than `/members` as it can be implemented more efficiently on the
- * server.
+ * This API is primarily for Application Services and should be faster to respond
+ * than `/members` as it can be implemented more efficiently on the server.
*/
-class GetJoinedMembersByRoomJob : public BaseJob {
+class QUOTIENT_API GetJoinedMembersByRoomJob : public BaseJob {
public:
// Inner data structures
@@ -175,7 +171,7 @@ public:
/// The display name of the user this object is representing.
QString displayName;
/// The mxc avatar url of the user this object is representing.
- QString avatarUrl;
+ QUrl avatarUrl;
};
// Construction/destruction
diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp
new file mode 100644
index 00000000..4e2c9e92
--- /dev/null
+++ b/lib/csapi/search.cpp
@@ -0,0 +1,26 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "search.h"
+
+using namespace Quotient;
+
+auto queryToSearch(const QString& nextBatch)
+{
+ QUrlQuery _q;
+ addParam<IfNotEmpty>(_q, QStringLiteral("next_batch"), nextBatch);
+ return _q;
+}
+
+SearchJob::SearchJob(const Categories& searchCategories,
+ const QString& nextBatch)
+ : BaseJob(HttpVerb::Post, QStringLiteral("SearchJob"),
+ makePath("/_matrix/client/v3", "/search"),
+ queryToSearch(nextBatch))
+{
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("search_categories"), searchCategories);
+ setRequestData({ _dataJson });
+ addExpectedKey("search_categories");
+}
diff --git a/lib/csapi/search.h b/lib/csapi/search.h
new file mode 100644
index 00000000..30095f32
--- /dev/null
+++ b/lib/csapi/search.h
@@ -0,0 +1,307 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "csapi/definitions/room_event_filter.h"
+
+#include "events/roomevent.h"
+#include "events/stateevent.h"
+#include "jobs/basejob.h"
+
+namespace Quotient {
+
+/*! \brief Perform a server-side search.
+ *
+ * Performs a full text search across different categories.
+ */
+class QUOTIENT_API SearchJob : public BaseJob {
+public:
+ // Inner data structures
+
+ /// Configures whether any context for the events
+ /// returned are included in the response.
+ struct IncludeEventContext {
+ /// How many events before the result are
+ /// returned. By default, this is `5`.
+ Omittable<int> beforeLimit;
+ /// How many events after the result are
+ /// 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`.
+ Omittable<bool> includeProfile;
+ };
+
+ /// Configuration for group.
+ struct Group {
+ /// Key that defines the group.
+ QString key;
+ };
+
+ /// Requests that the server partitions the result set
+ /// based on the provided list of keys.
+ struct Groupings {
+ /// List of groups to request.
+ QVector<Group> groupBy;
+ };
+
+ /// Mapping of category name to search criteria.
+ struct RoomEventsCriteria {
+ /// The string to search events for
+ QString searchTerm;
+ /// The keys to search. Defaults to all.
+ QStringList keys;
+ /// This takes a [filter](/client-server-api/#filtering).
+ RoomEventFilter filter;
+ /// The order in which to search for results.
+ /// By default, this is `"rank"`.
+ QString orderBy;
+ /// Configures whether any context for the events
+ /// returned are included in the response.
+ Omittable<IncludeEventContext> eventContext;
+ /// Requests the server return the current state for
+ /// each room returned.
+ Omittable<bool> includeState;
+ /// Requests that the server partitions the result set
+ /// based on the provided list of keys.
+ Omittable<Groupings> groupings;
+ };
+
+ /// Describes which categories to search in and their criteria.
+ struct Categories {
+ /// Mapping of category name to search criteria.
+ Omittable<RoomEventsCriteria> roomEvents;
+ };
+
+ /// Performs a full text search across different categories.
+ struct UserProfile {
+ /// Performs a full text search across different categories.
+ QString displayname;
+ /// Performs a full text search across different categories.
+ QUrl avatarUrl;
+ };
+
+ /// Context for result, if requested.
+ struct EventContext {
+ /// Pagination token for the start of the chunk
+ QString begin;
+ /// Pagination token for the end of the chunk
+ QString end;
+ /// The historic profile information of the
+ /// users that sent the events returned.
+ ///
+ /// The `string` key is the user ID for which
+ /// the profile belongs to.
+ QHash<QString, UserProfile> profileInfo;
+ /// Events just before the result.
+ RoomEvents eventsBefore;
+ /// Events just after the result.
+ RoomEvents eventsAfter;
+ };
+
+ /// The result object.
+ struct Result {
+ /// A number that describes how closely this result matches the search.
+ /// Higher is closer.
+ Omittable<double> rank;
+ /// The event that matched.
+ RoomEventPtr result;
+ /// Context for result, if requested.
+ Omittable<EventContext> context;
+ };
+
+ /// The results for a particular group value.
+ struct GroupValue {
+ /// Token that can be used to get the next batch
+ /// of results in the group, by passing as the
+ /// `next_batch` parameter to the next call. If
+ /// this field is absent, there are no more
+ /// results in this group.
+ QString nextBatch;
+ /// Key that can be used to order different
+ /// groups.
+ Omittable<int> order;
+ /// Which results are in this group.
+ QStringList results;
+ };
+
+ /// Mapping of category name to search criteria.
+ struct ResultRoomEvents {
+ /// An approximate count of the total number of results found.
+ Omittable<int> count;
+ /// List of words which should be highlighted, useful for stemming which
+ /// may change the query terms.
+ QStringList highlights;
+ /// List of results in the requested order.
+ std::vector<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`.
+ ///
+ /// 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:
+ /// 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
+ /// results, by passing as the `next_batch` parameter to
+ /// the next call. If this field is absent, there are no
+ /// more results.
+ QString nextBatch;
+ };
+
+ /// Describes which categories to search in and their criteria.
+ struct ResultCategories {
+ /// Mapping of category name to search criteria.
+ Omittable<ResultRoomEvents> roomEvents;
+ };
+
+ // Construction/destruction
+
+ /*! \brief Perform a server-side search.
+ *
+ * \param searchCategories
+ * Describes which categories to search in and their criteria.
+ *
+ * \param nextBatch
+ * The point to return events from. If given, this should be a
+ * `next_batch` result from a previous call to this endpoint.
+ */
+ explicit SearchJob(const Categories& searchCategories,
+ const QString& nextBatch = {});
+
+ // Result properties
+
+ /// Describes which categories to search in and their criteria.
+ ResultCategories searchCategories() const
+ {
+ return loadFromJson<ResultCategories>("search_categories"_ls);
+ }
+};
+
+template <>
+struct JsonObjectConverter<SearchJob::IncludeEventContext> {
+ static void dumpTo(QJsonObject& jo,
+ const SearchJob::IncludeEventContext& pod)
+ {
+ addParam<IfNotEmpty>(jo, QStringLiteral("before_limit"),
+ pod.beforeLimit);
+ addParam<IfNotEmpty>(jo, QStringLiteral("after_limit"), pod.afterLimit);
+ addParam<IfNotEmpty>(jo, QStringLiteral("include_profile"),
+ pod.includeProfile);
+ }
+};
+
+template <>
+struct JsonObjectConverter<SearchJob::Group> {
+ static void dumpTo(QJsonObject& jo, const SearchJob::Group& pod)
+ {
+ addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key);
+ }
+};
+
+template <>
+struct JsonObjectConverter<SearchJob::Groupings> {
+ static void dumpTo(QJsonObject& jo, const SearchJob::Groupings& pod)
+ {
+ addParam<IfNotEmpty>(jo, QStringLiteral("group_by"), pod.groupBy);
+ }
+};
+
+template <>
+struct JsonObjectConverter<SearchJob::RoomEventsCriteria> {
+ static void dumpTo(QJsonObject& jo, const SearchJob::RoomEventsCriteria& pod)
+ {
+ addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm);
+ addParam<IfNotEmpty>(jo, QStringLiteral("keys"), pod.keys);
+ addParam<IfNotEmpty>(jo, QStringLiteral("filter"), pod.filter);
+ addParam<IfNotEmpty>(jo, QStringLiteral("order_by"), pod.orderBy);
+ addParam<IfNotEmpty>(jo, QStringLiteral("event_context"),
+ pod.eventContext);
+ addParam<IfNotEmpty>(jo, QStringLiteral("include_state"),
+ pod.includeState);
+ addParam<IfNotEmpty>(jo, QStringLiteral("groupings"), pod.groupings);
+ }
+};
+
+template <>
+struct JsonObjectConverter<SearchJob::Categories> {
+ static void dumpTo(QJsonObject& jo, const SearchJob::Categories& pod)
+ {
+ addParam<IfNotEmpty>(jo, QStringLiteral("room_events"), pod.roomEvents);
+ }
+};
+
+template <>
+struct JsonObjectConverter<SearchJob::UserProfile> {
+ static void fillFrom(const QJsonObject& jo, SearchJob::UserProfile& result)
+ {
+ fromJson(jo.value("displayname"_ls), result.displayname);
+ fromJson(jo.value("avatar_url"_ls), result.avatarUrl);
+ }
+};
+
+template <>
+struct JsonObjectConverter<SearchJob::EventContext> {
+ static void fillFrom(const QJsonObject& jo, SearchJob::EventContext& result)
+ {
+ fromJson(jo.value("start"_ls), result.begin);
+ fromJson(jo.value("end"_ls), result.end);
+ fromJson(jo.value("profile_info"_ls), result.profileInfo);
+ fromJson(jo.value("events_before"_ls), result.eventsBefore);
+ fromJson(jo.value("events_after"_ls), result.eventsAfter);
+ }
+};
+
+template <>
+struct JsonObjectConverter<SearchJob::Result> {
+ static void fillFrom(const QJsonObject& jo, SearchJob::Result& result)
+ {
+ fromJson(jo.value("rank"_ls), result.rank);
+ fromJson(jo.value("result"_ls), result.result);
+ fromJson(jo.value("context"_ls), result.context);
+ }
+};
+
+template <>
+struct JsonObjectConverter<SearchJob::GroupValue> {
+ static void fillFrom(const QJsonObject& jo, SearchJob::GroupValue& result)
+ {
+ fromJson(jo.value("next_batch"_ls), result.nextBatch);
+ fromJson(jo.value("order"_ls), result.order);
+ fromJson(jo.value("results"_ls), result.results);
+ }
+};
+
+template <>
+struct JsonObjectConverter<SearchJob::ResultRoomEvents> {
+ static void fillFrom(const QJsonObject& jo,
+ SearchJob::ResultRoomEvents& result)
+ {
+ fromJson(jo.value("count"_ls), result.count);
+ fromJson(jo.value("highlights"_ls), result.highlights);
+ fromJson(jo.value("results"_ls), result.results);
+ fromJson(jo.value("state"_ls), result.state);
+ fromJson(jo.value("groups"_ls), result.groups);
+ fromJson(jo.value("next_batch"_ls), result.nextBatch);
+ }
+};
+
+template <>
+struct JsonObjectConverter<SearchJob::ResultCategories> {
+ static void fillFrom(const QJsonObject& jo,
+ SearchJob::ResultCategories& result)
+ {
+ fromJson(jo.value("room_events"_ls), result.roomEvents);
+ }
+};
+
+} // namespace Quotient
diff --git a/lib/csapi/space_hierarchy.cpp b/lib/csapi/space_hierarchy.cpp
new file mode 100644
index 00000000..7b5c7eac
--- /dev/null
+++ b/lib/csapi/space_hierarchy.cpp
@@ -0,0 +1,43 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "space_hierarchy.h"
+
+using namespace Quotient;
+
+auto queryToGetSpaceHierarchy(Omittable<bool> suggestedOnly,
+ Omittable<int> limit, Omittable<int> maxDepth,
+ const QString& from)
+{
+ QUrlQuery _q;
+ addParam<IfNotEmpty>(_q, QStringLiteral("suggested_only"), suggestedOnly);
+ addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
+ addParam<IfNotEmpty>(_q, QStringLiteral("max_depth"), maxDepth);
+ addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
+ return _q;
+}
+
+QUrl GetSpaceHierarchyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
+ Omittable<bool> suggestedOnly,
+ Omittable<int> limit,
+ Omittable<int> maxDepth,
+ const QString& from)
+{
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl),
+ makePath("/_matrix/client/v1", "/rooms/", roomId, "/hierarchy"),
+ queryToGetSpaceHierarchy(suggestedOnly, limit, maxDepth, from));
+}
+
+GetSpaceHierarchyJob::GetSpaceHierarchyJob(const QString& roomId,
+ Omittable<bool> suggestedOnly,
+ Omittable<int> limit,
+ Omittable<int> maxDepth,
+ const QString& from)
+ : BaseJob(HttpVerb::Get, QStringLiteral("GetSpaceHierarchyJob"),
+ makePath("/_matrix/client/v1", "/rooms/", roomId, "/hierarchy"),
+ queryToGetSpaceHierarchy(suggestedOnly, limit, maxDepth, from))
+{
+ addExpectedKey("rooms");
+}
diff --git a/lib/csapi/space_hierarchy.h b/lib/csapi/space_hierarchy.h
new file mode 100644
index 00000000..e5da6df2
--- /dev/null
+++ b/lib/csapi/space_hierarchy.h
@@ -0,0 +1,152 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "events/stateevent.h"
+#include "jobs/basejob.h"
+
+namespace Quotient {
+
+/*! \brief Retrieve a portion of a space tree.
+ *
+ * Paginates over the space tree in a depth-first manner to locate child rooms
+ * of a given space.
+ *
+ * Where a child room is unknown to the local server, federation is used to fill
+ * in the details. The servers listed in the `via` array should be contacted to
+ * attempt to fill in missing rooms.
+ *
+ * Only [`m.space.child`](#mspacechild) state events of the room are considered.
+ * Invalid child rooms and parent events are not covered by this endpoint.
+ */
+class QUOTIENT_API GetSpaceHierarchyJob : public BaseJob {
+public:
+ // Inner data structures
+
+ /// Paginates over the space tree in a depth-first manner to locate child
+ /// rooms of a given space.
+ ///
+ /// Where a child room is unknown to the local server, federation is used to
+ /// fill in the details. The servers listed in the `via` array should be
+ /// contacted to attempt to fill in missing rooms.
+ ///
+ /// Only [`m.space.child`](#mspacechild) state events of the room are
+ /// considered. Invalid child rooms and parent events are not covered by
+ /// this endpoint.
+ struct ChildRoomsChunk {
+ /// The canonical alias of the room, if any.
+ QString canonicalAlias;
+ /// The name of the room, if any.
+ QString name;
+ /// The number of members joined to the room.
+ int numJoinedMembers;
+ /// The ID of the room.
+ QString roomId;
+ /// The topic of the room, if any.
+ QString topic;
+ /// Whether the room may be viewed by guest users without joining.
+ bool worldReadable;
+ /// Whether guest users may join the room and participate in it.
+ /// If they can, they will be subject to ordinary power level
+ /// rules like any other user.
+ bool guestCanJoin;
+ /// The URL for the room's avatar, if one is set.
+ QUrl avatarUrl;
+ /// The room's join rule. When not present, the room is assumed to
+ /// be `public`.
+ QString joinRule;
+ /// The `type` of room (from
+ /// [`m.room.create`](/client-server-api/#mroomcreate)), if any.
+ QString roomType;
+ /// The [`m.space.child`](#mspacechild) events of the space-room,
+ /// represented as [Stripped State Events](#stripped-state) with an
+ /// added `origin_server_ts` key.
+ ///
+ /// If the room is not a space-room, this should be empty.
+ StateEvents childrenState;
+ };
+
+ // Construction/destruction
+
+ /*! \brief Retrieve a portion of a space tree.
+ *
+ * \param roomId
+ * The room ID of the space to get a hierarchy for.
+ *
+ * \param suggestedOnly
+ * Optional (default `false`) flag to indicate whether or not the server
+ * should only consider suggested rooms. Suggested rooms are annotated in
+ * their [`m.space.child`](#mspacechild) event contents.
+ *
+ * \param limit
+ * Optional limit for the maximum number of rooms to include per response.
+ * Must be an integer greater than zero.
+ *
+ * Servers should apply a default value, and impose a maximum value to
+ * avoid resource exhaustion.
+ *
+ * \param maxDepth
+ * Optional limit for how far to go into the space. Must be a non-negative
+ * integer.
+ *
+ * When reached, no further child rooms will be returned.
+ *
+ * Servers should apply a default value, and impose a maximum value to
+ * avoid resource exhaustion.
+ *
+ * \param from
+ * A pagination token from a previous result. If specified, `max_depth`
+ * and `suggested_only` cannot be changed from the first request.
+ */
+ explicit GetSpaceHierarchyJob(const QString& roomId,
+ Omittable<bool> suggestedOnly = none,
+ Omittable<int> limit = none,
+ Omittable<int> maxDepth = none,
+ const QString& from = {});
+
+ /*! \brief Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for GetSpaceHierarchyJob
+ * is necessary but the job itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId,
+ Omittable<bool> suggestedOnly = none,
+ Omittable<int> limit = none,
+ Omittable<int> maxDepth = none,
+ const QString& from = {});
+
+ // Result properties
+
+ /// The rooms for the current page, with the current filters.
+ std::vector<ChildRoomsChunk> rooms()
+ {
+ return takeFromJson<std::vector<ChildRoomsChunk>>("rooms"_ls);
+ }
+
+ /// A token to supply to `from` to keep paginating the responses. Not
+ /// present when there are no further results.
+ QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); }
+};
+
+template <>
+struct JsonObjectConverter<GetSpaceHierarchyJob::ChildRoomsChunk> {
+ static void fillFrom(const QJsonObject& jo,
+ GetSpaceHierarchyJob::ChildRoomsChunk& result)
+ {
+ fromJson(jo.value("canonical_alias"_ls), result.canonicalAlias);
+ fromJson(jo.value("name"_ls), result.name);
+ fromJson(jo.value("num_joined_members"_ls), result.numJoinedMembers);
+ fromJson(jo.value("room_id"_ls), result.roomId);
+ fromJson(jo.value("topic"_ls), result.topic);
+ fromJson(jo.value("world_readable"_ls), result.worldReadable);
+ fromJson(jo.value("guest_can_join"_ls), result.guestCanJoin);
+ fromJson(jo.value("avatar_url"_ls), result.avatarUrl);
+ fromJson(jo.value("join_rule"_ls), result.joinRule);
+ fromJson(jo.value("room_type"_ls), result.roomType);
+ fromJson(jo.value("children_state"_ls), result.childrenState);
+ }
+};
+
+} // namespace Quotient
diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp
index 85a18560..71f8147c 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/v3",
+ "/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/v3", "/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/v3",
+ "/login/sso/redirect/", idpId),
+ queryToRedirectToIdP(redirectUrl));
+}
+
+RedirectToIdPJob::RedirectToIdPJob(const QString& idpId,
+ const QString& redirectUrl)
+ : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToIdPJob"),
+ makePath("/_matrix/client/v3", "/login/sso/redirect/", idpId),
+ queryToRedirectToIdP(redirectUrl), {}, false)
+{}
diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h
index 6205ca59..f4f81c1e 100644
--- a/lib/csapi/sso_login_redirect.h
+++ b/lib/csapi/sso_login_redirect.h
@@ -17,7 +17,7 @@ namespace Quotient {
* or present a page which lets the user select an IdP to continue
* with in the event multiple are supported by the server.
*/
-class RedirectToSSOJob : public BaseJob {
+class QUOTIENT_API RedirectToSSOJob : public BaseJob {
public:
/*! \brief Redirect the user's browser to the SSO interface.
*
@@ -35,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 QUOTIENT_API 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..2c85842d 100644
--- a/lib/csapi/tags.cpp
+++ b/lib/csapi/tags.cpp
@@ -4,49 +4,47 @@
#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/v3", "/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/v3", "/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/v3", "/user/", userId, "/rooms/",
+ roomId, "/tags/", tag))
{
- QJsonObject _data;
- fillJson(_data, additionalProperties);
- addParam<IfNotEmpty>(_data, QStringLiteral("order"), order);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ fillJson(_dataJson, additionalProperties);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("order"), order);
+ setRequestData({ _dataJson });
}
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/v3", "/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/v3", "/user/", userId, "/rooms/",
+ roomId, "/tags/", tag))
{}
diff --git a/lib/csapi/tags.h b/lib/csapi/tags.h
index a854531a..f4250674 100644
--- a/lib/csapi/tags.h
+++ b/lib/csapi/tags.h
@@ -12,7 +12,7 @@ namespace Quotient {
*
* List the tags set by a user on a room.
*/
-class GetRoomTagsJob : public BaseJob {
+class QUOTIENT_API GetRoomTagsJob : public BaseJob {
public:
// Inner data structures
@@ -68,7 +68,7 @@ struct JsonObjectConverter<GetRoomTagsJob::Tag> {
*
* Add a tag to the room.
*/
-class SetRoomTagJob : public BaseJob {
+class QUOTIENT_API SetRoomTagJob : public BaseJob {
public:
/*! \brief Add a tag to a room.
*
@@ -98,7 +98,7 @@ public:
*
* Remove a tag from the room.
*/
-class DeleteRoomTagJob : public BaseJob {
+class QUOTIENT_API DeleteRoomTagJob : public BaseJob {
public:
/*! \brief Remove a tag from the room.
*
diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp
index baf1fab5..1e5870ce 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/v3",
+ "/thirdparty/protocols"));
}
GetProtocolsJob::GetProtocolsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolsJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocols")
+ makePath("/_matrix/client/v3", "/thirdparty/protocols"))
{}
QUrl GetProtocolMetadataJob::makeRequestUrl(QUrl baseUrl,
const QString& protocol)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- QStringLiteral("/_matrix/client/r0")
- % "/thirdparty/protocol/" % protocol);
+ makePath("/_matrix/client/v3",
+ "/thirdparty/protocol/", protocol));
}
GetProtocolMetadataJob::GetProtocolMetadataJob(const QString& protocol)
: BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolMetadataJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocol/"
- % protocol)
+ makePath("/_matrix/client/v3", "/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/v3",
+ "/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/v3", "/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/v3",
+ "/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/v3", "/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/v3",
+ "/thirdparty/location"),
queryToQueryLocationByAlias(alias));
}
QueryLocationByAliasJob::QueryLocationByAliasJob(const QString& alias)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByAliasJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/location",
+ makePath("/_matrix/client/v3", "/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/v3",
+ "/thirdparty/user"),
queryToQueryUserByID(userid));
}
QueryUserByIDJob::QueryUserByIDJob(const QString& userid)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByIDJob"),
- QStringLiteral("/_matrix/client/r0") % "/thirdparty/user",
+ makePath("/_matrix/client/v3", "/thirdparty/user"),
queryToQueryUserByID(userid))
{}
diff --git a/lib/csapi/third_party_lookup.h b/lib/csapi/third_party_lookup.h
index 969e767c..30c5346e 100644
--- a/lib/csapi/third_party_lookup.h
+++ b/lib/csapi/third_party_lookup.h
@@ -18,7 +18,7 @@ namespace Quotient {
* homeserver. Includes both the available protocols and all fields
* required for queries against each protocol.
*/
-class GetProtocolsJob : public BaseJob {
+class QUOTIENT_API GetProtocolsJob : public BaseJob {
public:
/// Retrieve metadata about all protocols that a homeserver supports.
explicit GetProtocolsJob();
@@ -45,7 +45,7 @@ public:
* Fetches the metadata from the homeserver about a particular third party
* protocol.
*/
-class GetProtocolMetadataJob : public BaseJob {
+class QUOTIENT_API GetProtocolMetadataJob : public BaseJob {
public:
/*! \brief Retrieve metadata about a specific protocol that the homeserver
* supports.
@@ -82,7 +82,7 @@ public:
* identifier. It should attempt to canonicalise the identifier as much
* as reasonably possible given the network type.
*/
-class QueryLocationByProtocolJob : public BaseJob {
+class QUOTIENT_API QueryLocationByProtocolJob : public BaseJob {
public:
/*! \brief Retrieve Matrix-side portals rooms leading to a third party
* location.
@@ -119,7 +119,7 @@ public:
* Retrieve a Matrix User ID linked to a user on the third party service, given
* a set of user parameters.
*/
-class QueryUserByProtocolJob : public BaseJob {
+class QUOTIENT_API QueryUserByProtocolJob : public BaseJob {
public:
/*! \brief Retrieve the Matrix User ID of a corresponding third party user.
*
@@ -155,7 +155,7 @@ public:
* Retrieve an array of third party network locations from a Matrix room
* alias.
*/
-class QueryLocationByAliasJob : public BaseJob {
+class QUOTIENT_API QueryLocationByAliasJob : public BaseJob {
public:
/*! \brief Reverse-lookup third party locations given a Matrix room alias.
*
@@ -184,7 +184,7 @@ public:
*
* Retrieve an array of third party users from a Matrix User ID.
*/
-class QueryUserByIDJob : public BaseJob {
+class QUOTIENT_API QueryUserByIDJob : public BaseJob {
public:
/*! \brief Reverse-lookup third party users given a Matrix User ID.
*
diff --git a/lib/csapi/third_party_membership.cpp b/lib/csapi/third_party_membership.cpp
index fda772d2..3ca986c7 100644
--- a/lib/csapi/third_party_membership.cpp
+++ b/lib/csapi/third_party_membership.cpp
@@ -4,21 +4,18 @@
#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/v3", "/rooms/", roomId, "/invite"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("id_server"), idServer);
- addParam<>(_data, QStringLiteral("id_access_token"), idAccessToken);
- addParam<>(_data, QStringLiteral("medium"), medium);
- addParam<>(_data, QStringLiteral("address"), address);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("id_server"), idServer);
+ addParam<>(_dataJson, QStringLiteral("id_access_token"), idAccessToken);
+ addParam<>(_dataJson, QStringLiteral("medium"), medium);
+ addParam<>(_dataJson, QStringLiteral("address"), address);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/third_party_membership.h b/lib/csapi/third_party_membership.h
index a424678f..1129a9a8 100644
--- a/lib/csapi/third_party_membership.h
+++ b/lib/csapi/third_party_membership.h
@@ -16,7 +16,7 @@ namespace Quotient {
* The homeserver uses an identity server to perform the mapping from
* third party identifier to a Matrix identifier. The other is documented in
* the* [joining rooms
- * section](/client-server-api/#post_matrixclientr0roomsroomidinvite).
+ * section](/client-server-api/#post_matrixclientv3roomsroomidinvite).
*
* This API invites a user to participate in a particular room.
* They do not start participating in the room until they actually join the
@@ -52,7 +52,7 @@ namespace Quotient {
* If a token is requested from the identity server, the homeserver will
* append a `m.room.third_party_invite` event to the room.
*/
-class InviteBy3PIDJob : public BaseJob {
+class QUOTIENT_API InviteBy3PIDJob : public BaseJob {
public:
/*! \brief Invite a user to participate in a particular room.
*
diff --git a/lib/csapi/threads_list.cpp b/lib/csapi/threads_list.cpp
new file mode 100644
index 00000000..26924f24
--- /dev/null
+++ b/lib/csapi/threads_list.cpp
@@ -0,0 +1,37 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "threads_list.h"
+
+using namespace Quotient;
+
+auto queryToGetThreadRoots(const QString& include, Omittable<int> limit,
+ const QString& from)
+{
+ QUrlQuery _q;
+ addParam<IfNotEmpty>(_q, QStringLiteral("include"), include);
+ addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
+ addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
+ return _q;
+}
+
+QUrl GetThreadRootsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
+ const QString& include,
+ Omittable<int> limit, const QString& from)
+{
+ return BaseJob::makeRequestUrl(std::move(baseUrl),
+ makePath("/_matrix/client/v1", "/rooms/",
+ roomId, "/threads"),
+ queryToGetThreadRoots(include, limit, from));
+}
+
+GetThreadRootsJob::GetThreadRootsJob(const QString& roomId,
+ const QString& include,
+ Omittable<int> limit, const QString& from)
+ : BaseJob(HttpVerb::Get, QStringLiteral("GetThreadRootsJob"),
+ makePath("/_matrix/client/v1", "/rooms/", roomId, "/threads"),
+ queryToGetThreadRoots(include, limit, from))
+{
+ addExpectedKey("chunk");
+}
diff --git a/lib/csapi/threads_list.h b/lib/csapi/threads_list.h
new file mode 100644
index 00000000..7041583a
--- /dev/null
+++ b/lib/csapi/threads_list.h
@@ -0,0 +1,76 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "events/roomevent.h"
+#include "jobs/basejob.h"
+
+namespace Quotient {
+
+/*! \brief Retrieve a list of threads in a room, with optional filters.
+ *
+ * Paginates over the thread roots in a room, ordered by the `latest_event` of
+ * each thread root in its bundle.
+ */
+class QUOTIENT_API GetThreadRootsJob : public BaseJob {
+public:
+ /*! \brief Retrieve a list of threads in a room, with optional filters.
+ *
+ * \param roomId
+ * The room ID where the thread roots are located.
+ *
+ * \param include
+ * Optional (default `all`) flag to denote which thread roots are of
+ * interest to the caller. When `all`, all thread roots found in the room
+ * are returned. When `participated`, only thread roots for threads the user
+ * has [participated
+ * in](/client-server-api/#server-side-aggreagtion-of-mthread-relationships)
+ * will be returned.
+ *
+ * \param limit
+ * Optional limit for the maximum number of thread roots to include per
+ * response. Must be an integer greater than zero.
+ *
+ * Servers should apply a default value, and impose a maximum value to
+ * avoid resource exhaustion.
+ *
+ * \param from
+ * A pagination token from a previous result. When not provided, the
+ * server starts paginating from the most recent event visible to the user
+ * (as per history visibility rules; topologically).
+ */
+ explicit GetThreadRootsJob(const QString& roomId,
+ const QString& include = {},
+ Omittable<int> limit = none,
+ const QString& from = {});
+
+ /*! \brief Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for GetThreadRootsJob
+ * is necessary but the job itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId,
+ const QString& include = {},
+ Omittable<int> limit = none,
+ const QString& from = {});
+
+ // Result properties
+
+ /// The thread roots, ordered by the `latest_event` in each event's
+ /// aggregation bundle. All events returned include bundled
+ /// [aggregations](/client-server-api/#aggregations).
+ ///
+ /// If the thread root event was sent by an [ignored
+ /// user](/client-server-api/#ignoring-users), the event is returned
+ /// redacted to the caller. This is to simulate the same behaviour of a
+ /// client doing aggregation locally on the thread.
+ RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); }
+
+ /// A token to supply to `from` to keep paginating the responses. Not
+ /// present when there are no further results.
+ QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); }
+};
+
+} // namespace Quotient
diff --git a/lib/csapi/to_device.cpp b/lib/csapi/to_device.cpp
index 28c4115a..e10fac69 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/v3", "/sendToDevice/", eventType, "/",
+ txnId))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("messages"), messages);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("messages"), messages);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/to_device.h b/lib/csapi/to_device.h
index f5d69d65..54828337 100644
--- a/lib/csapi/to_device.h
+++ b/lib/csapi/to_device.h
@@ -13,7 +13,7 @@ namespace Quotient {
* This endpoint is used to send send-to-device events to a set of
* client devices.
*/
-class SendToDeviceJob : public BaseJob {
+class QUOTIENT_API SendToDeviceJob : public BaseJob {
public:
/*! \brief Send an event to a given set of devices.
*
@@ -21,9 +21,10 @@ public:
* The type of event to send.
*
* \param txnId
- * The transaction ID for this event. Clients should generate an
- * ID unique across requests with the same access token; it will be
- * used by the server to ensure idempotency of requests.
+ * The [transaction ID](/client-server-api/#transaction-identifiers) for
+ * this event. Clients should generate an ID unique across requests with the
+ * same access token; it will be used by the server to ensure idempotency of
+ * requests.
*
* \param messages
* The messages to send. A map from user ID, to a map from
@@ -32,7 +33,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..21bd45ae 100644
--- a/lib/csapi/typing.cpp
+++ b/lib/csapi/typing.cpp
@@ -4,18 +4,16 @@
#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/v3", "/rooms/", roomId, "/typing/",
+ userId))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("typing"), typing);
- addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("typing"), typing);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("timeout"), timeout);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/typing.h b/lib/csapi/typing.h
index 64a310d0..234e91b0 100644
--- a/lib/csapi/typing.h
+++ b/lib/csapi/typing.h
@@ -15,7 +15,7 @@ namespace Quotient {
* Alternatively, if `typing` is `false`, it tells the server that the
* user has stopped typing.
*/
-class SetTypingJob : public BaseJob {
+class QUOTIENT_API SetTypingJob : public BaseJob {
public:
/*! \brief Informs the server that the user has started or stopped typing.
*
diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp
index a0279d7e..c65280ee 100644
--- a/lib/csapi/users.cpp
+++ b/lib/csapi/users.cpp
@@ -4,19 +4,17 @@
#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/v3", "/user_directory/search"))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("search_term"), searchTerm);
- addParam<IfNotEmpty>(_data, QStringLiteral("limit"), limit);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("search_term"), searchTerm);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("limit"), limit);
+ setRequestData({ _dataJson });
addExpectedKey("results");
addExpectedKey("limited");
}
diff --git a/lib/csapi/users.h b/lib/csapi/users.h
index eab18f6c..3c99758b 100644
--- a/lib/csapi/users.h
+++ b/lib/csapi/users.h
@@ -21,7 +21,7 @@ namespace Quotient {
* names preferably using a collation determined based upon the
* `Accept-Language` header provided in the request, if present.
*/
-class SearchUserDirectoryJob : public BaseJob {
+class QUOTIENT_API SearchUserDirectoryJob : public BaseJob {
public:
// Inner data structures
@@ -41,7 +41,7 @@ public:
/// The display name of the user, if one exists.
QString displayName;
/// The avatar url, as an MXC, if one exists.
- QString avatarUrl;
+ QUrl avatarUrl;
};
// Construction/destruction
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 896e2ea9..9f799cb0 100644
--- a/lib/csapi/versions.h
+++ b/lib/csapi/versions.h
@@ -12,11 +12,9 @@ namespace Quotient {
*
* Gets the versions of the specification supported by the server.
*
- * Values will take the form `rX.Y.Z`.
- *
- * Only the latest `Z` value will be reported for each supported `X.Y` value.
- * i.e. if the server implements `r0.0.0`, `r0.0.1`, and `r1.2.0`, it will
- * report `r0.0.1` and `r1.2.0`.
+ * Values will take the form `vX.Y` or `rX.Y.Z` in historical cases. See
+ * [the Specification Versioning](../#specification-versions) for more
+ * information.
*
* The server may additionally advertise experimental features it supports
* through `unstable_features`. These features should be namespaced and
@@ -31,7 +29,7 @@ namespace Quotient {
* upgrade appropriately. Additionally, clients should avoid using unstable
* features in their stable releases.
*/
-class GetVersionsJob : public BaseJob {
+class QUOTIENT_API GetVersionsJob : public BaseJob {
public:
/// Gets the versions of the specification supported by the server.
explicit GetVersionsJob();
diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp
index 43170057..1e1f2441 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/v3", "/voip/turnServer"));
}
GetTurnServerJob::GetTurnServerJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetTurnServerJob"),
- QStringLiteral("/_matrix/client/r0") % "/voip/turnServer")
+ makePath("/_matrix/client/v3", "/voip/turnServer"))
{}
diff --git a/lib/csapi/voip.h b/lib/csapi/voip.h
index 087ebbbd..38904f60 100644
--- a/lib/csapi/voip.h
+++ b/lib/csapi/voip.h
@@ -13,7 +13,7 @@ namespace Quotient {
* This API provides credentials for the client to use when initiating
* calls.
*/
-class GetTurnServerJob : public BaseJob {
+class QUOTIENT_API GetTurnServerJob : public BaseJob {
public:
/// Obtain TURN server credentials.
explicit GetTurnServerJob();
diff --git a/lib/csapi/wellknown.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 c707d232..8615191c 100644
--- a/lib/csapi/wellknown.h
+++ b/lib/csapi/wellknown.h
@@ -21,7 +21,7 @@ namespace Quotient {
* Note that this endpoint is not necessarily handled by the homeserver,
* but by another webserver, to be used for discovering the homeserver URL.
*/
-class GetWellknownJob : public BaseJob {
+class QUOTIENT_API GetWellknownJob : public BaseJob {
public:
/// Gets Matrix server discovery information about the domain.
explicit GetWellknownJob();
diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp
index 73f0298e..af0c5d31 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/v3", "/account/whoami"));
}
GetTokenOwnerJob::GetTokenOwnerJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetTokenOwnerJob"),
- QStringLiteral("/_matrix/client/r0") % "/account/whoami")
+ makePath("/_matrix/client/v3", "/account/whoami"))
{
addExpectedKey("user_id");
}
diff --git a/lib/csapi/whoami.h b/lib/csapi/whoami.h
index 184459ea..3451dbc3 100644
--- a/lib/csapi/whoami.h
+++ b/lib/csapi/whoami.h
@@ -19,7 +19,7 @@ namespace Quotient {
* is registered by the appservice, and return it in the response
* body.
*/
-class GetTokenOwnerJob : public BaseJob {
+class QUOTIENT_API GetTokenOwnerJob : public BaseJob {
public:
/// Gets information about the owner of an access token.
explicit GetTokenOwnerJob();
@@ -35,6 +35,20 @@ public:
/// 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); }
+
+ /// When `true`, the user is a [Guest User](#guest-access). When
+ /// not present or `false`, the user is presumed to be a non-guest
+ /// user.
+ Omittable<bool> isGuest() const
+ {
+ return loadFromJson<Omittable<bool>>("is_guest"_ls);
+ }
};
} // namespace Quotient
diff --git a/lib/database.cpp b/lib/database.cpp
new file mode 100644
index 00000000..2b472648
--- /dev/null
+++ b/lib/database.cpp
@@ -0,0 +1,419 @@
+// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "database.h"
+
+#include <QtSql/QSqlDatabase>
+#include <QtSql/QSqlQuery>
+#include <QtSql/QSqlError>
+#include <QtCore/QStandardPaths>
+#include <QtCore/QDebug>
+#include <QtCore/QDir>
+
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmsession.h"
+#include "e2ee/qolminboundsession.h"
+#include "e2ee/qolmoutboundsession.h"
+
+using namespace Quotient;
+Database::Database(const QString& matrixId, const QString& deviceId, QObject* parent)
+ : QObject(parent)
+ , m_matrixId(matrixId)
+{
+ m_matrixId.replace(':', '_');
+ QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("Quotient_%1").arg(m_matrixId));
+ QString databasePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/%1").arg(m_matrixId);
+ QDir(databasePath).mkpath(databasePath);
+ database().setDatabaseName(databasePath + QStringLiteral("/quotient_%1.db3").arg(deviceId));
+ database().open();
+
+ switch(version()) {
+ case 0: migrateTo1(); [[fallthrough]];
+ case 1: migrateTo2(); [[fallthrough]];
+ case 2: migrateTo3(); [[fallthrough]];
+ case 3: migrateTo4(); [[fallthrough]];
+ case 4: migrateTo5();
+ }
+}
+
+int Database::version()
+{
+ auto query = execute(QStringLiteral("PRAGMA user_version;"));
+ if (query.next()) {
+ bool ok = false;
+ int value = query.value(0).toInt(&ok);
+ qCDebug(DATABASE) << "Database version" << value;
+ if (ok)
+ return value;
+ } else {
+ qCritical() << "Failed to check database version";
+ }
+ return -1;
+}
+
+QSqlQuery Database::execute(const QString &queryString)
+{
+ auto query = database().exec(queryString);
+ if (query.lastError().type() != QSqlError::NoError) {
+ qCritical() << "Failed to execute query";
+ qCritical() << query.lastQuery();
+ qCritical() << query.lastError();
+ }
+ return query;
+}
+
+QSqlQuery Database::execute(QSqlQuery &query)
+{
+ if (!query.exec()) {
+ qCritical() << "Failed to execute query";
+ qCritical() << query.lastQuery();
+ qCritical() << query.lastError();
+ }
+ return query;
+}
+
+void Database::transaction()
+{
+ database().transaction();
+}
+
+void Database::commit()
+{
+ database().commit();
+}
+
+void Database::migrateTo1()
+{
+ qCDebug(DATABASE) << "Migrating database to version 1";
+ transaction();
+ execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);"));
+ execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);"));
+ execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);"));
+ execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);"));
+ execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);"));
+ execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);"));
+ execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);"));
+ execute(QStringLiteral("CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);"));
+
+ execute(QStringLiteral("PRAGMA user_version = 1;"));
+ commit();
+}
+
+void Database::migrateTo2()
+{
+ qCDebug(DATABASE) << "Migrating database to version 2";
+ transaction();
+
+ execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT"));
+ execute(QStringLiteral("ALTER TABLE olm_sessions ADD lastReceived TEXT"));
+
+ // Add indexes for improving queries speed on larger database
+ execute(QStringLiteral("CREATE INDEX sessions_session_idx ON olm_sessions(sessionId)"));
+ execute(QStringLiteral("CREATE INDEX outbound_room_idx ON outbound_megolm_sessions(roomId)"));
+ execute(QStringLiteral("CREATE INDEX inbound_room_idx ON inbound_megolm_sessions(roomId)"));
+ execute(QStringLiteral("CREATE INDEX group_session_idx ON group_session_record_index(roomId, sessionId, i)"));
+ execute(QStringLiteral("PRAGMA user_version = 2;"));
+ commit();
+}
+
+void Database::migrateTo3()
+{
+ qCDebug(DATABASE) << "Migrating database to version 3";
+ transaction();
+
+ execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions_temp AS SELECT roomId, sessionId, pickle FROM inbound_megolm_sessions;"));
+ execute(QStringLiteral("DROP TABLE inbound_megolm_sessions;"));
+ execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions_temp RENAME TO inbound_megolm_sessions;"));
+ execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD olmSessionId TEXT;"));
+ execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD senderId TEXT;"));
+ execute(QStringLiteral("PRAGMA user_version = 3;"));
+ commit();
+}
+
+void Database::migrateTo4()
+{
+ qCDebug(DATABASE) << "Migrating database to version 4";
+ transaction();
+
+ execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);"));
+ execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD creationTime TEXT;"));
+ execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD messageCount INTEGER;"));
+ execute(QStringLiteral("PRAGMA user_version = 4;"));
+ commit();
+}
+
+void Database::migrateTo5()
+{
+ qCDebug(DATABASE) << "Migrating database to version 5";
+ transaction();
+
+ execute(QStringLiteral("ALTER TABLE tracked_devices ADD verified BOOL;"));
+ execute(QStringLiteral("PRAGMA user_version = 5"));
+ commit();
+}
+
+QByteArray Database::accountPickle()
+{
+ auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;"));
+ execute(query);
+ if (query.next()) {
+ return query.value(QStringLiteral("pickle")).toByteArray();
+ }
+ return {};
+}
+
+void Database::setAccountPickle(const QByteArray &pickle)
+{
+ auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM accounts;"));
+ auto query = prepareQuery(QStringLiteral("INSERT INTO accounts(pickle) VALUES(:pickle);"));
+ query.bindValue(":pickle", pickle);
+ transaction();
+ execute(deleteQuery);
+ execute(query);
+ commit();
+}
+
+void Database::clear()
+{
+ auto query = prepareQuery(QStringLiteral("DELETE FROM accounts;"));
+ auto sessionsQuery = prepareQuery(QStringLiteral("DELETE FROM olm_sessions;"));
+ auto megolmSessionsQuery = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions;"));
+ auto groupSessionIndexRecordQuery = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index;"));
+
+ transaction();
+ execute(query);
+ execute(sessionsQuery);
+ execute(megolmSessionsQuery);
+ execute(groupSessionIndexRecordQuery);
+ commit();
+
+}
+
+void Database::saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle, const QDateTime& timestamp)
+{
+ auto query = prepareQuery(QStringLiteral("INSERT INTO olm_sessions(senderKey, sessionId, pickle, lastReceived) VALUES(:senderKey, :sessionId, :pickle, :lastReceived);"));
+ query.bindValue(":senderKey", senderKey);
+ query.bindValue(":sessionId", sessionId);
+ query.bindValue(":pickle", pickle);
+ query.bindValue(":lastReceived", timestamp);
+ transaction();
+ execute(query);
+ commit();
+}
+
+UnorderedMap<QString, std::vector<QOlmSessionPtr>> Database::loadOlmSessions(const PicklingMode& picklingMode)
+{
+ auto query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions ORDER BY lastReceived DESC;"));
+ transaction();
+ execute(query);
+ commit();
+ UnorderedMap<QString, std::vector<QOlmSessionPtr>> sessions;
+ while (query.next()) {
+ if (auto expectedSession =
+ QOlmSession::unpickle(query.value("pickle").toByteArray(),
+ picklingMode)) {
+ sessions[query.value("senderKey").toString()].emplace_back(
+ std::move(*expectedSession));
+ } else
+ qCWarning(E2EE)
+ << "Failed to unpickle olm session:" << expectedSession.error();
+ }
+ return sessions;
+}
+
+UnorderedMap<QString, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode)
+{
+ auto query = prepareQuery(QStringLiteral("SELECT * FROM inbound_megolm_sessions WHERE roomId=:roomId;"));
+ query.bindValue(":roomId", roomId);
+ transaction();
+ execute(query);
+ commit();
+ UnorderedMap<QString, QOlmInboundGroupSessionPtr> sessions;
+ while (query.next()) {
+ if (auto expectedSession = QOlmInboundGroupSession::unpickle(
+ query.value("pickle").toByteArray(), picklingMode)) {
+ auto& sessionPtr = sessions[query.value("sessionId").toString()] =
+ std::move(*expectedSession);
+ sessionPtr->setOlmSessionId(query.value("olmSessionId").toString());
+ sessionPtr->setSenderId(query.value("senderId").toString());
+ } else
+ qCWarning(E2EE) << "Failed to unpickle megolm session:"
+ << expectedSession.error();
+ }
+ return sessions;
+}
+
+void Database::saveMegolmSession(const QString& roomId, const QString& sessionId, const QByteArray& pickle, const QString& senderId, const QString& olmSessionId)
+{
+ auto query = prepareQuery(QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, sessionId, pickle, senderId, olmSessionId) VALUES(:roomId, :sessionId, :pickle, :senderId, :olmSessionId);"));
+ query.bindValue(":roomId", roomId);
+ query.bindValue(":sessionId", sessionId);
+ query.bindValue(":pickle", pickle);
+ query.bindValue(":senderId", senderId);
+ query.bindValue(":olmSessionId", olmSessionId);
+ transaction();
+ execute(query);
+ commit();
+}
+
+void Database::addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts)
+{
+ auto query = prepareQuery("INSERT INTO group_session_record_index(roomId, sessionId, i, eventId, ts) VALUES(:roomId, :sessionId, :index, :eventId, :ts);");
+ query.bindValue(":roomId", roomId);
+ query.bindValue(":sessionId", sessionId);
+ query.bindValue(":index", index);
+ query.bindValue(":eventId", eventId);
+ query.bindValue(":ts", ts);
+ transaction();
+ execute(query);
+ commit();
+}
+
+std::pair<QString, qint64> Database::groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index)
+{
+ auto query = prepareQuery(QStringLiteral("SELECT * FROM group_session_record_index WHERE roomId=:roomId AND sessionId=:sessionId AND i=:index;"));
+ query.bindValue(":roomId", roomId);
+ query.bindValue(":sessionId", sessionId);
+ query.bindValue(":index", index);
+ transaction();
+ execute(query);
+ commit();
+ if (!query.next()) {
+ return {};
+ }
+ return {query.value("eventId").toString(), query.value("ts").toLongLong()};
+}
+
+QSqlDatabase Database::database()
+{
+ return QSqlDatabase::database(QStringLiteral("Quotient_%1").arg(m_matrixId));
+}
+
+QSqlQuery Database::prepareQuery(const QString& queryString)
+{
+ QSqlQuery query(database());
+ query.prepare(queryString);
+ return query;
+}
+
+void Database::clearRoomData(const QString& roomId)
+{
+ auto query = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions WHERE roomId=:roomId;"));
+ auto query2 = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId;"));
+ auto query3 = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index WHERE roomId=:roomId;"));
+ transaction();
+ execute(query);
+ execute(query2);
+ execute(query3);
+ commit();
+}
+
+void Database::setOlmSessionLastReceived(const QString& sessionId, const QDateTime& timestamp)
+{
+ auto query = prepareQuery(QStringLiteral("UPDATE olm_sessions SET lastReceived=:lastReceived WHERE sessionId=:sessionId;"));
+ query.bindValue(":lastReceived", timestamp);
+ query.bindValue(":sessionId", sessionId);
+ transaction();
+ execute(query);
+ commit();
+}
+
+void Database::saveCurrentOutboundMegolmSession(
+ const QString& roomId, const PicklingMode& picklingMode,
+ const QOlmOutboundGroupSession& session)
+{
+ const auto pickle = session.pickle(picklingMode);
+ auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;"));
+ deleteQuery.bindValue(":roomId", roomId);
+ deleteQuery.bindValue(":sessionId", session.sessionId());
+
+ auto insertQuery = prepareQuery(QStringLiteral("INSERT INTO outbound_megolm_sessions(roomId, sessionId, pickle, creationTime, messageCount) VALUES(:roomId, :sessionId, :pickle, :creationTime, :messageCount);"));
+ insertQuery.bindValue(":roomId", roomId);
+ insertQuery.bindValue(":sessionId", session.sessionId());
+ insertQuery.bindValue(":pickle", pickle);
+ insertQuery.bindValue(":creationTime", session.creationTime());
+ insertQuery.bindValue(":messageCount", session.messageCount());
+
+ transaction();
+ execute(deleteQuery);
+ execute(insertQuery);
+ commit();
+}
+
+QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode)
+{
+ auto query = prepareQuery(QStringLiteral("SELECT * FROM outbound_megolm_sessions WHERE roomId=:roomId ORDER BY creationTime DESC;"));
+ query.bindValue(":roomId", roomId);
+ execute(query);
+ if (query.next()) {
+ auto sessionResult = QOlmOutboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode);
+ if (sessionResult) {
+ auto session = std::move(*sessionResult);
+ session->setCreationTime(query.value("creationTime").toDateTime());
+ session->setMessageCount(query.value("messageCount").toInt());
+ return session;
+ }
+ }
+ return nullptr;
+}
+
+void Database::setDevicesReceivedKey(const QString& roomId, const QVector<std::tuple<QString, QString, QString>>& devices, const QString& sessionId, int index)
+{
+ transaction();
+ for (const auto& [user, device, curveKey] : devices) {
+ auto query = prepareQuery(QStringLiteral("INSERT INTO sent_megolm_sessions(roomId, userId, deviceId, identityKey, sessionId, i) VALUES(:roomId, :userId, :deviceId, :identityKey, :sessionId, :i);"));
+ query.bindValue(":roomId", roomId);
+ query.bindValue(":userId", user);
+ query.bindValue(":deviceId", device);
+ query.bindValue(":identityKey", curveKey);
+ query.bindValue(":sessionId", sessionId);
+ query.bindValue(":i", index);
+ execute(query);
+ }
+ commit();
+}
+
+QMultiHash<QString, QString> Database::devicesWithoutKey(
+ const QString& roomId, QMultiHash<QString, QString> devices,
+ const QString& sessionId)
+{
+ auto query = prepareQuery(QStringLiteral("SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId"));
+ query.bindValue(":roomId", roomId);
+ query.bindValue(":sessionId", sessionId);
+ transaction();
+ execute(query);
+ commit();
+ while (query.next()) {
+ devices.remove(query.value("userId").toString(),
+ query.value("deviceId").toString());
+ }
+ return devices;
+}
+
+void Database::updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle)
+{
+ auto query = prepareQuery(QStringLiteral("UPDATE olm_sessions SET pickle=:pickle WHERE senderKey=:senderKey AND sessionId=:sessionId;"));
+ query.bindValue(":pickle", pickle);
+ query.bindValue(":senderKey", senderKey);
+ query.bindValue(":sessionId", sessionId);
+ transaction();
+ execute(query);
+ commit();
+}
+
+void Database::setSessionVerified(const QString& edKeyId)
+{
+ auto query = prepareQuery(QStringLiteral("UPDATE tracked_devices SET verified=true WHERE edKeyId=:edKeyId;"));
+ query.bindValue(":edKeyId", edKeyId);
+ transaction();
+ execute(query);
+ commit();
+}
+
+bool Database::isSessionVerified(const QString& edKey)
+{
+ auto query = prepareQuery(QStringLiteral("SELECT verified FROM tracked_devices WHERE edKey=:edKey"));
+ query.bindValue(":edKey", edKey);
+ execute(query);
+ return query.next() && query.value("verified").toBool();
+}
diff --git a/lib/database.h b/lib/database.h
new file mode 100644
index 00000000..8a133f8e
--- /dev/null
+++ b/lib/database.h
@@ -0,0 +1,81 @@
+// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QtCore/QObject>
+#include <QtSql/QSqlQuery>
+#include <QtCore/QVector>
+
+#include <QtCore/QHash>
+
+#include "e2ee/e2ee.h"
+
+namespace Quotient {
+
+class QUOTIENT_API Database : public QObject
+{
+ Q_OBJECT
+public:
+ Database(const QString& matrixId, const QString& deviceId, QObject* parent);
+
+ int version();
+ void transaction();
+ void commit();
+ QSqlQuery execute(const QString &queryString);
+ QSqlQuery execute(QSqlQuery &query);
+ QSqlDatabase database();
+ QSqlQuery prepareQuery(const QString& quaryString);
+
+ QByteArray accountPickle();
+ void setAccountPickle(const QByteArray &pickle);
+ void clear();
+ void saveOlmSession(const QString& senderKey, const QString& sessionId,
+ const QByteArray& pickle, const QDateTime& timestamp);
+ UnorderedMap<QString, std::vector<QOlmSessionPtr>> loadOlmSessions(
+ const PicklingMode& picklingMode);
+ UnorderedMap<QString, QOlmInboundGroupSessionPtr> loadMegolmSessions(
+ const QString& roomId, const PicklingMode& picklingMode);
+ void saveMegolmSession(const QString& roomId, const QString& sessionId,
+ const QByteArray& pickle, const QString& senderId,
+ const QString& olmSessionId);
+ void addGroupSessionIndexRecord(const QString& roomId,
+ const QString& sessionId, uint32_t index,
+ const QString& eventId, qint64 ts);
+ std::pair<QString, qint64> groupSessionIndexRecord(const QString& roomId,
+ const QString& sessionId,
+ qint64 index);
+ void clearRoomData(const QString& roomId);
+ void setOlmSessionLastReceived(const QString& sessionId,
+ const QDateTime& timestamp);
+ QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(
+ const QString& roomId, const PicklingMode& picklingMode);
+ void saveCurrentOutboundMegolmSession(
+ const QString& roomId, const PicklingMode& picklingMode,
+ const QOlmOutboundGroupSession& session);
+ void updateOlmSession(const QString& senderKey, const QString& sessionId,
+ const QByteArray& pickle);
+
+ // Returns a map UserId -> [DeviceId] that have not received key yet
+ QMultiHash<QString, QString> devicesWithoutKey(
+ const QString& roomId, QMultiHash<QString, QString> devices,
+ const QString& sessionId);
+ // 'devices' contains tuples {userId, deviceId, curveKey}
+ void setDevicesReceivedKey(
+ const QString& roomId,
+ const QVector<std::tuple<QString, QString, QString>>& devices,
+ const QString& sessionId, int index);
+
+ bool isSessionVerified(const QString& edKey);
+ void setSessionVerified(const QString& edKeyId);
+
+private:
+ void migrateTo1();
+ void migrateTo2();
+ void migrateTo3();
+ void migrateTo4();
+ void migrateTo5();
+
+ QString m_matrixId;
+};
+} // namespace Quotient
diff --git a/lib/e2ee.h b/lib/e2ee.h
deleted file mode 100644
index f49b9748..00000000
--- a/lib/e2ee.h
+++ /dev/null
@@ -1,31 +0,0 @@
-#pragma once
-
-#include "util.h"
-
-#include <QtCore/QStringList>
-
-namespace Quotient {
-inline const auto CiphertextKeyL = "ciphertext"_ls;
-inline const auto SenderKeyKeyL = "sender_key"_ls;
-inline const auto DeviceIdKeyL = "device_id"_ls;
-inline const auto SessionIdKeyL = "session_id"_ls;
-
-inline const auto AlgorithmKeyL = "algorithm"_ls;
-inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls;
-inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls;
-
-inline const auto AlgorithmKey = QStringLiteral("algorithm");
-inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms");
-inline const auto RotationPeriodMsgsKey =
- QStringLiteral("rotation_period_msgs");
-
-inline const auto Ed25519Key = QStringLiteral("ed25519");
-inline const auto Curve25519Key = QStringLiteral("curve25519");
-inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519");
-inline const auto OlmV1Curve25519AesSha2AlgoKey =
- QStringLiteral("m.olm.v1.curve25519-aes-sha2");
-inline const auto MegolmV1AesSha2AlgoKey =
- QStringLiteral("m.megolm.v1.aes-sha2");
-inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey,
- MegolmV1AesSha2AlgoKey };
-} // namespace Quotient
diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h
new file mode 100644
index 00000000..5999c0be
--- /dev/null
+++ b/lib/e2ee/e2ee.h
@@ -0,0 +1,139 @@
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "converters.h"
+
+#include <QtCore/QMetaType>
+#include <QtCore/QStringBuilder>
+
+#include <array>
+
+#ifdef Quotient_E2EE_ENABLED
+# include "expected.h"
+
+# include <olm/error.h>
+# include <variant>
+#endif
+
+namespace Quotient {
+
+constexpr auto AlgorithmKeyL = "algorithm"_ls;
+constexpr auto RotationPeriodMsKeyL = "rotation_period_ms"_ls;
+constexpr auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls;
+
+constexpr auto AlgorithmKey = "algorithm"_ls;
+constexpr auto RotationPeriodMsKey = "rotation_period_ms"_ls;
+constexpr auto RotationPeriodMsgsKey = "rotation_period_msgs"_ls;
+
+constexpr auto Ed25519Key = "ed25519"_ls;
+constexpr auto Curve25519Key = "curve25519"_ls;
+constexpr auto SignedCurve25519Key = "signed_curve25519"_ls;
+
+constexpr auto OlmV1Curve25519AesSha2AlgoKey = "m.olm.v1.curve25519-aes-sha2"_ls;
+constexpr auto MegolmV1AesSha2AlgoKey = "m.megolm.v1.aes-sha2"_ls;
+
+constexpr std::array SupportedAlgorithms { OlmV1Curve25519AesSha2AlgoKey,
+ MegolmV1AesSha2AlgoKey };
+
+inline bool isSupportedAlgorithm(const QString& algorithm)
+{
+ return std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(),
+ algorithm)
+ != SupportedAlgorithms.cend();
+}
+
+#ifdef Quotient_E2EE_ENABLED
+struct Unencrypted {};
+struct Encrypted {
+ QByteArray key;
+};
+
+using PicklingMode = std::variant<Unencrypted, Encrypted>;
+
+class QOlmSession;
+using QOlmSessionPtr = std::unique_ptr<QOlmSession>;
+
+class QOlmInboundGroupSession;
+using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>;
+
+class QOlmOutboundGroupSession;
+using QOlmOutboundGroupSessionPtr = std::unique_ptr<QOlmOutboundGroupSession>;
+
+template <typename T>
+using QOlmExpected = Expected<T, OlmErrorCode>;
+#endif
+
+struct IdentityKeys
+{
+ QByteArray curve25519;
+ QByteArray ed25519;
+};
+
+//! Struct representing the one-time keys.
+struct UnsignedOneTimeKeys
+{
+ QHash<QString, QHash<QString, QString>> keys;
+
+ //! Get the HashMap containing the curve25519 one-time keys.
+ QHash<QString, QString> curve25519() const { return keys[Curve25519Key]; }
+};
+
+class SignedOneTimeKey {
+public:
+ explicit SignedOneTimeKey(const QString& unsignedKey, const QString& userId,
+ const QString& deviceId,
+ const QByteArray& signature)
+ : payload { { "key"_ls, unsignedKey },
+ { "signatures"_ls,
+ QJsonObject {
+ { userId, QJsonObject { { "ed25519:"_ls % deviceId,
+ QString(signature) } } } } } }
+ {}
+ explicit SignedOneTimeKey(const QJsonObject& jo = {})
+ : payload(jo)
+ {}
+
+ //! Unpadded Base64-encoded 32-byte Curve25519 public key
+ QByteArray key() const { return payload["key"_ls].toString().toLatin1(); }
+
+ //! \brief Signatures of the key object
+ //!
+ //! The signature is calculated using the process described at
+ //! https://spec.matrix.org/v1.3/appendices/#signing-json
+ auto signatures() const
+ {
+ return fromJson<QHash<QString, QHash<QString, QString>>>(
+ payload["signatures"_ls]);
+ }
+
+ QByteArray signature(QStringView userId, QStringView deviceId) const
+ {
+ return payload["signatures"_ls][userId]["ed25519:"_ls % deviceId]
+ .toString()
+ .toLatin1();
+ }
+
+ //! Whether the key is a fallback key
+ bool isFallback() const { return payload["fallback"_ls].toBool(); }
+ auto toJson() const { return payload; }
+ auto toJsonForVerification() const
+ {
+ auto json = payload;
+ json.remove("signatures"_ls);
+ json.remove("unsigned"_ls);
+ return QJsonDocument(json).toJson(QJsonDocument::Compact);
+ }
+
+private:
+ QJsonObject payload;
+};
+
+using OneTimeKeys = QHash<QString, std::variant<QString, SignedOneTimeKey>>;
+
+} // namespace Quotient
+
+Q_DECLARE_METATYPE(Quotient::SignedOneTimeKey)
diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp
new file mode 100644
index 00000000..345ab16b
--- /dev/null
+++ b/lib/e2ee/qolmaccount.cpp
@@ -0,0 +1,275 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmaccount.h"
+
+#include "connection.h"
+#include "e2ee/qolmsession.h"
+#include "e2ee/qolmutility.h"
+#include "e2ee/qolmutils.h"
+
+#include "csapi/keys.h"
+
+#include <QtCore/QRandomGenerator>
+
+#include <olm/olm.h>
+
+using namespace Quotient;
+
+// Convert olm error to enum
+OlmErrorCode QOlmAccount::lastErrorCode() const {
+ return olm_account_last_error_code(m_account);
+}
+
+const char* QOlmAccount::lastError() const
+{
+ return olm_account_last_error(m_account);
+}
+
+QOlmAccount::QOlmAccount(QStringView userId, QStringView deviceId,
+ QObject* parent)
+ : QObject(parent)
+ , m_userId(userId.toString())
+ , m_deviceId(deviceId.toString())
+{}
+
+QOlmAccount::~QOlmAccount()
+{
+ olm_clear_account(m_account);
+ delete[](reinterpret_cast<uint8_t *>(m_account));
+}
+
+void QOlmAccount::createNewAccount()
+{
+ m_account = olm_account(new uint8_t[olm_account_size()]);
+ if (const auto randomLength = olm_create_account_random_length(m_account);
+ olm_create_account(m_account, RandomBuffer(randomLength), randomLength)
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to create a new account");
+
+ emit needsSave();
+}
+
+OlmErrorCode QOlmAccount::unpickle(QByteArray&& pickled,
+ const PicklingMode& mode)
+{
+ m_account = olm_account(new uint8_t[olm_account_size()]);
+ if (const auto key = toKey(mode);
+ olm_unpickle_account(m_account, key.data(), key.length(),
+ pickled.data(), pickled.size())
+ == olm_error()) {
+ // Probably log the user out since we have no way of getting to the keys
+ return lastErrorCode();
+ }
+ return OLM_SUCCESS;
+}
+
+QByteArray QOlmAccount::pickle(const PicklingMode &mode)
+{
+ const QByteArray key = toKey(mode);
+ const size_t pickleLength = olm_pickle_account_length(m_account);
+ QByteArray pickleBuffer(pickleLength, '\0');
+ if (olm_pickle_account(m_account, key.data(), key.length(),
+ pickleBuffer.data(), pickleLength)
+ == olm_error())
+ QOLM_INTERNAL_ERROR(qPrintable("Failed to pickle Olm account "
+ + accountId()));
+
+ return pickleBuffer;
+}
+
+IdentityKeys QOlmAccount::identityKeys() const
+{
+ const size_t keyLength = olm_account_identity_keys_length(m_account);
+ QByteArray keyBuffer(keyLength, '\0');
+ if (olm_account_identity_keys(m_account, keyBuffer.data(), keyLength)
+ == olm_error()) {
+ QOLM_INTERNAL_ERROR(
+ qPrintable("Failed to get " % accountId() % " identity keys"));
+ }
+ const auto key = QJsonDocument::fromJson(keyBuffer).object();
+ return IdentityKeys {
+ key.value(QStringLiteral("curve25519")).toString().toUtf8(),
+ key.value(QStringLiteral("ed25519")).toString().toUtf8()
+ };
+}
+
+QByteArray QOlmAccount::sign(const QByteArray &message) const
+{
+ QByteArray signatureBuffer(olm_account_signature_length(m_account), '\0');
+
+ if (olm_account_sign(m_account, message.data(), message.length(),
+ signatureBuffer.data(), signatureBuffer.length())
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to sign a message");
+
+ return signatureBuffer;
+}
+
+QByteArray QOlmAccount::sign(const QJsonObject &message) const
+{
+ return sign(QJsonDocument(message).toJson(QJsonDocument::Compact));
+}
+
+QByteArray QOlmAccount::signIdentityKeys() const
+{
+ const auto keys = identityKeys();
+ return sign(QJsonObject{
+ { "algorithms", QJsonArray{ "m.olm.v1.curve25519-aes-sha2",
+ "m.megolm.v1.aes-sha2" } },
+ { "user_id", m_userId },
+ { "device_id", m_deviceId },
+ { "keys", QJsonObject{ { QStringLiteral("curve25519:") + m_deviceId,
+ QString::fromUtf8(keys.curve25519) },
+ { QStringLiteral("ed25519:") + m_deviceId,
+ QString::fromUtf8(keys.ed25519) } } } });
+}
+
+size_t QOlmAccount::maxNumberOfOneTimeKeys() const
+{
+ return olm_account_max_number_of_one_time_keys(m_account);
+}
+
+size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys)
+{
+ const auto randomLength =
+ olm_account_generate_one_time_keys_random_length(m_account,
+ numberOfKeys);
+ const auto result = olm_account_generate_one_time_keys(
+ m_account, numberOfKeys, RandomBuffer(randomLength), randomLength);
+
+ if (result == olm_error())
+ QOLM_INTERNAL_ERROR(qPrintable(
+ "Failed to generate one-time keys for account " + accountId()));
+
+ emit needsSave();
+ return result;
+}
+
+UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const
+{
+ const auto oneTimeKeyLength = olm_account_one_time_keys_length(m_account);
+ QByteArray oneTimeKeysBuffer(static_cast<int>(oneTimeKeyLength), '\0');
+
+ if (olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(),
+ oneTimeKeyLength)
+ == olm_error())
+ QOLM_INTERNAL_ERROR(qPrintable(
+ "Failed to obtain one-time keys for account" % accountId()));
+
+ const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object();
+ UnsignedOneTimeKeys oneTimeKeys;
+ fromJson(json, oneTimeKeys.keys);
+ return oneTimeKeys;
+}
+
+OneTimeKeys QOlmAccount::signOneTimeKeys(const UnsignedOneTimeKeys &keys) const
+{
+ OneTimeKeys signedOneTimeKeys;
+ for (const auto& curveKeys = keys.curve25519();
+ const auto& [keyId, key] : asKeyValueRange(curveKeys))
+ signedOneTimeKeys.insert("signed_curve25519:" % keyId,
+ SignedOneTimeKey {
+ key, m_userId, m_deviceId,
+ sign(QJsonObject { { "key", key } }) });
+ return signedOneTimeKeys;
+}
+
+OlmErrorCode QOlmAccount::removeOneTimeKeys(const QOlmSession& session)
+{
+ if (olm_remove_one_time_keys(m_account, session.raw()) == olm_error()) {
+ qWarning(E2EE).nospace()
+ << "Failed to remove one-time keys for session "
+ << session.sessionId() << ": " << lastError();
+ return lastErrorCode();
+ }
+ emit needsSave();
+ return OLM_SUCCESS;
+}
+
+OlmAccount* QOlmAccount::data() { return m_account; }
+
+DeviceKeys QOlmAccount::deviceKeys() const
+{
+ static QStringList Algorithms(SupportedAlgorithms.cbegin(),
+ SupportedAlgorithms.cend());
+
+ const auto idKeys = identityKeys();
+ return DeviceKeys{
+ .userId = m_userId,
+ .deviceId = m_deviceId,
+ .algorithms = Algorithms,
+ .keys{ { "curve25519:" + m_deviceId, idKeys.curve25519 },
+ { "ed25519:" + m_deviceId, idKeys.ed25519 } },
+ .signatures{
+ { m_userId, { { "ed25519:" + m_deviceId, signIdentityKeys() } } } }
+ };
+}
+
+UploadKeysJob* QOlmAccount::createUploadKeyRequest(
+ const UnsignedOneTimeKeys& oneTimeKeys) const
+{
+ return new UploadKeysJob(deviceKeys(), signOneTimeKeys(oneTimeKeys));
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmAccount::createInboundSession(
+ const QOlmMessage& preKeyMessage)
+{
+ Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey);
+ return QOlmSession::createInboundSession(this, preKeyMessage);
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmAccount::createInboundSessionFrom(
+ const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage)
+{
+ Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey);
+ return QOlmSession::createInboundSessionFrom(this, theirIdentityKey,
+ preKeyMessage);
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmAccount::createOutboundSession(
+ const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey)
+{
+ return QOlmSession::createOutboundSession(this, theirIdentityKey,
+ theirOneTimeKey);
+}
+
+void QOlmAccount::markKeysAsPublished()
+{
+ olm_account_mark_keys_as_published(m_account);
+ emit needsSave();
+}
+
+bool Quotient::verifyIdentitySignature(const DeviceKeys& deviceKeys,
+ const QString& deviceId,
+ const QString& userId)
+{
+ const auto signKeyId = "ed25519:" + deviceId;
+ const auto signingKey = deviceKeys.keys[signKeyId];
+ const auto signature = deviceKeys.signatures[userId][signKeyId];
+
+ return ed25519VerifySignature(signingKey, toJson(deviceKeys), signature);
+}
+
+bool Quotient::ed25519VerifySignature(const QString& signingKey,
+ const QJsonObject& obj,
+ const QString& signature)
+{
+ if (signature.isEmpty())
+ return false;
+
+ QJsonObject obj1 = obj;
+
+ obj1.remove("unsigned");
+ obj1.remove("signatures");
+
+ auto canonicalJson = QJsonDocument(obj1).toJson(QJsonDocument::Compact);
+
+ QByteArray signingKeyBuf = signingKey.toUtf8();
+ QOlmUtility utility;
+ auto signatureBuf = signature.toUtf8();
+ return utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf);
+}
+
+QString QOlmAccount::accountId() const { return m_userId % '/' % m_deviceId; }
diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h
new file mode 100644
index 00000000..a5faa82a
--- /dev/null
+++ b/lib/e2ee/qolmaccount.h
@@ -0,0 +1,123 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+
+#pragma once
+
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmmessage.h"
+
+#include "csapi/keys.h"
+
+#include <QtCore/QObject>
+
+struct OlmAccount;
+
+namespace Quotient {
+
+//! An olm account manages all cryptographic keys used on a device.
+//! \code{.cpp}
+//! const auto olmAccount = new QOlmAccount(this);
+//! \endcode
+class QUOTIENT_API QOlmAccount : public QObject
+{
+ Q_OBJECT
+public:
+ QOlmAccount(QStringView userId, QStringView deviceId,
+ QObject* parent = nullptr);
+ ~QOlmAccount() override;
+
+ //! Creates a new instance of OlmAccount. During the instantiation
+ //! the Ed25519 fingerprint key pair and the Curve25519 identity key
+ //! pair are generated. For more information see <a
+ //! href="https://matrix.org/docs/guides/e2e_implementation.html#keys-used-in-end-to-end-encryption">here</a>.
+ //! This needs to be called before any other action or use unpickle() instead.
+ void createNewAccount();
+
+ //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`.
+ //! This needs to be called before any other action or use createNewAccount() instead.
+ [[nodiscard]] OlmErrorCode unpickle(QByteArray&& pickled,
+ const PicklingMode& mode);
+
+ //! Serialises an OlmAccount to encrypted Base64.
+ QByteArray pickle(const PicklingMode &mode);
+
+ //! Returns the account's public identity keys already formatted as JSON
+ IdentityKeys identityKeys() const;
+
+ //! Returns the signature of the supplied message.
+ QByteArray sign(const QByteArray &message) const;
+ QByteArray sign(const QJsonObject& message) const;
+
+ //! Sign identity keys.
+ QByteArray signIdentityKeys() const;
+
+ //! Maximum number of one time keys that this OlmAccount can
+ //! currently hold.
+ size_t maxNumberOfOneTimeKeys() const;
+
+ //! Generates the supplied number of one time keys.
+ size_t generateOneTimeKeys(size_t numberOfKeys);
+
+ //! Gets the OlmAccount's one time keys formatted as JSON.
+ UnsignedOneTimeKeys oneTimeKeys() const;
+
+ //! Sign all one time keys.
+ OneTimeKeys signOneTimeKeys(const UnsignedOneTimeKeys &keys) const;
+
+ UploadKeysJob* createUploadKeyRequest(const UnsignedOneTimeKeys& oneTimeKeys) const;
+
+ DeviceKeys deviceKeys() const;
+
+ //! Remove the one time key used to create the supplied session.
+ [[nodiscard]] OlmErrorCode removeOneTimeKeys(const QOlmSession& session);
+
+ //! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
+ //!
+ //! \param preKeyMessage An Olm pre-key message that was encrypted for this account.
+ QOlmExpected<QOlmSessionPtr> createInboundSession(
+ const QOlmMessage& preKeyMessage);
+
+ //! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
+ //!
+ //! \param theirIdentityKey - The identity key of the Olm account that
+ //! encrypted this Olm message.
+ QOlmExpected<QOlmSessionPtr> createInboundSessionFrom(
+ const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage);
+
+ //! Creates an outbound session for sending messages to a specific
+ /// identity and one time key.
+ QOlmExpected<QOlmSessionPtr> createOutboundSession(
+ const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey);
+
+ void markKeysAsPublished();
+
+ OlmErrorCode lastErrorCode() const;
+ const char* lastError() const;
+
+ // HACK do not use directly
+ QOlmAccount(OlmAccount *account);
+ OlmAccount *data();
+
+Q_SIGNALS:
+ void needsSave();
+
+private:
+ OlmAccount *m_account = nullptr; // owning
+ QString m_userId;
+ QString m_deviceId;
+
+ QString accountId() const;
+};
+
+QUOTIENT_API bool verifyIdentitySignature(const DeviceKeys& deviceKeys,
+ const QString& deviceId,
+ const QString& userId);
+
+//! checks if the signature is signed by the signing_key
+QUOTIENT_API bool ed25519VerifySignature(const QString& signingKey,
+ const QJsonObject& obj,
+ const QString& signature);
+
+} // namespace Quotient
diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp
new file mode 100644
index 00000000..18275dc0
--- /dev/null
+++ b/lib/e2ee/qolminboundsession.cpp
@@ -0,0 +1,192 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolminboundsession.h"
+#include "qolmutils.h"
+#include "../logging.h"
+
+#include <cstring>
+#include <iostream>
+#include <olm/olm.h>
+
+using namespace Quotient;
+
+OlmErrorCode QOlmInboundGroupSession::lastErrorCode() const {
+ return olm_inbound_group_session_last_error_code(m_groupSession);
+}
+
+const char* QOlmInboundGroupSession::lastError() const
+{
+ return olm_inbound_group_session_last_error(m_groupSession);
+}
+
+QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session)
+ : m_groupSession(session)
+{}
+
+QOlmInboundGroupSession::~QOlmInboundGroupSession()
+{
+ olm_clear_inbound_group_session(m_groupSession);
+ //delete[](reinterpret_cast<uint8_t *>(m_groupSession));
+}
+
+QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::create(
+ const QByteArray& key)
+{
+ const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]);
+ if (olm_init_inbound_group_session(
+ olmInboundGroupSession,
+ reinterpret_cast<const uint8_t*>(key.constData()), key.size())
+ == olm_error()) {
+ // FIXME: create QOlmInboundGroupSession earlier and use lastErrorCode()
+ qWarning(E2EE) << "Failed to create an inbound group session:"
+ << olm_inbound_group_session_last_error(
+ olmInboundGroupSession);
+ return olm_inbound_group_session_last_error_code(olmInboundGroupSession);
+ }
+
+ return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession);
+}
+
+QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::importSession(
+ const QByteArray& key)
+{
+ const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]);
+
+ if (olm_import_inbound_group_session(
+ olmInboundGroupSession,
+ reinterpret_cast<const uint8_t*>(key.data()), key.size())
+ == olm_error()) {
+ // FIXME: create QOlmInboundGroupSession earlier and use lastError()
+ qWarning(E2EE) << "Failed to import an inbound group session:"
+ << olm_inbound_group_session_last_error(
+ olmInboundGroupSession);
+ return olm_inbound_group_session_last_error_code(olmInboundGroupSession);
+ }
+
+ return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession);
+}
+
+QByteArray QOlmInboundGroupSession::pickle(const PicklingMode& mode) const
+{
+ QByteArray pickledBuf(
+ olm_pickle_inbound_group_session_length(m_groupSession), '\0');
+ if (const auto key = toKey(mode);
+ olm_pickle_inbound_group_session(m_groupSession, key.data(),
+ key.length(), pickledBuf.data(),
+ pickledBuf.length())
+ == olm_error()) {
+ QOLM_INTERNAL_ERROR("Failed to pickle the inbound group session");
+ }
+ return pickledBuf;
+}
+
+QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::unpickle(
+ QByteArray&& pickled, const PicklingMode& mode)
+{
+ const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]);
+ auto key = toKey(mode);
+ if (olm_unpickle_inbound_group_session(groupSession, key.data(),
+ key.length(), pickled.data(),
+ pickled.size())
+ == olm_error()) {
+ // FIXME: create QOlmInboundGroupSession earlier and use lastError()
+ qWarning(E2EE) << "Failed to unpickle an inbound group session:"
+ << olm_inbound_group_session_last_error(groupSession);
+ return olm_inbound_group_session_last_error_code(groupSession);
+ }
+ key.clear();
+
+ return std::make_unique<QOlmInboundGroupSession>(groupSession);
+}
+
+QOlmExpected<std::pair<QByteArray, uint32_t>> QOlmInboundGroupSession::decrypt(
+ const QByteArray& message)
+{
+ // This is for capturing the output of olm_group_decrypt
+ uint32_t messageIndex = 0;
+
+ // We need to clone the message because
+ // olm_decrypt_max_plaintext_length destroys the input buffer
+ QByteArray messageBuf(message.length(), '\0');
+ std::copy(message.begin(), message.end(), messageBuf.begin());
+
+ QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length(
+ m_groupSession,
+ reinterpret_cast<uint8_t*>(messageBuf.data()),
+ messageBuf.length()),
+ '\0');
+
+ messageBuf = QByteArray(message.length(), '\0');
+ std::copy(message.begin(), message.end(), messageBuf.begin());
+
+ const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast<uint8_t *>(messageBuf.data()),
+ messageBuf.length(), reinterpret_cast<uint8_t *>(plaintextBuf.data()), plaintextBuf.length(), &messageIndex);
+ if (plaintextLen == olm_error()) {
+ qWarning(E2EE) << "Failed to decrypt the message:" << lastError();
+ return lastErrorCode();
+ }
+
+ QByteArray output(plaintextLen, '\0');
+ std::memcpy(output.data(), plaintextBuf.data(), plaintextLen);
+
+ return std::make_pair(output, messageIndex);
+}
+
+QOlmExpected<QByteArray> QOlmInboundGroupSession::exportSession(
+ uint32_t messageIndex)
+{
+ const auto keyLength = olm_export_inbound_group_session_length(m_groupSession);
+ QByteArray keyBuf(keyLength, '\0');
+ if (olm_export_inbound_group_session(
+ m_groupSession, reinterpret_cast<uint8_t*>(keyBuf.data()),
+ keyLength, messageIndex)
+ == olm_error()) {
+ QOLM_FAIL_OR_LOG(OLM_OUTPUT_BUFFER_TOO_SMALL,
+ "Failed to export the inbound group session");
+ return lastErrorCode();
+ }
+ return keyBuf;
+}
+
+uint32_t QOlmInboundGroupSession::firstKnownIndex() const
+{
+ return olm_inbound_group_session_first_known_index(m_groupSession);
+}
+
+QByteArray QOlmInboundGroupSession::sessionId() const
+{
+ QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession),
+ '\0');
+ if (olm_inbound_group_session_id(
+ m_groupSession, reinterpret_cast<uint8_t*>(sessionIdBuf.data()),
+ sessionIdBuf.length())
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to obtain the group session id");
+
+ return sessionIdBuf;
+}
+
+bool QOlmInboundGroupSession::isVerified() const
+{
+ return olm_inbound_group_session_is_verified(m_groupSession) != 0;
+}
+
+QString QOlmInboundGroupSession::olmSessionId() const
+{
+ return m_olmSessionId;
+}
+void QOlmInboundGroupSession::setOlmSessionId(const QString& newOlmSessionId)
+{
+ m_olmSessionId = newOlmSessionId;
+}
+
+QString QOlmInboundGroupSession::senderId() const
+{
+ return m_senderId;
+}
+void QOlmInboundGroupSession::setSenderId(const QString& senderId)
+{
+ m_senderId = senderId;
+}
diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h
new file mode 100644
index 00000000..b9710354
--- /dev/null
+++ b/lib/e2ee/qolminboundsession.h
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "e2ee/e2ee.h"
+
+struct OlmInboundGroupSession;
+
+namespace Quotient {
+
+//! An in-bound group session is responsible for decrypting incoming
+//! communication in a Megolm session.
+class QUOTIENT_API QOlmInboundGroupSession
+{
+public:
+ ~QOlmInboundGroupSession();
+ //! Creates a new instance of `OlmInboundGroupSession`.
+ static QOlmExpected<QOlmInboundGroupSessionPtr> create(const QByteArray& key);
+ //! Import an inbound group session, from a previous export.
+ static QOlmExpected<QOlmInboundGroupSessionPtr> importSession(const QByteArray& key);
+ //! Serialises an `OlmInboundGroupSession` to encrypted Base64.
+ QByteArray pickle(const PicklingMode& mode) const;
+ //! Deserialises from encrypted Base64 that was previously obtained by pickling
+ //! an `OlmInboundGroupSession`.
+ static QOlmExpected<QOlmInboundGroupSessionPtr> unpickle(
+ QByteArray&& pickled, const PicklingMode& mode);
+ //! Decrypts ciphertext received for this group session.
+ QOlmExpected<std::pair<QByteArray, uint32_t> > decrypt(const QByteArray& message);
+ //! Export the base64-encoded ratchet key for this session, at the given index,
+ //! in a format which can be used by import.
+ QOlmExpected<QByteArray> exportSession(uint32_t messageIndex);
+ //! Get the first message index we know how to decrypt.
+ uint32_t firstKnownIndex() const;
+ //! Get a base64-encoded identifier for this session.
+ QByteArray sessionId() const;
+ bool isVerified() const;
+
+ //! The olm session that this session was received from.
+ //! Required to get the device this session is from.
+ QString olmSessionId() const;
+ void setOlmSessionId(const QString& newOlmSessionId);
+
+ //! The sender of this session.
+ QString senderId() const;
+ void setSenderId(const QString& senderId);
+
+ OlmErrorCode lastErrorCode() const;
+ const char* lastError() const;
+
+ QOlmInboundGroupSession(OlmInboundGroupSession* session);
+private:
+ OlmInboundGroupSession* m_groupSession;
+ QString m_olmSessionId;
+ QString m_senderId;
+};
+
+using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>;
+} // namespace Quotient
diff --git a/lib/e2ee/qolmmessage.cpp b/lib/e2ee/qolmmessage.cpp
new file mode 100644
index 00000000..b9cb8bd2
--- /dev/null
+++ b/lib/e2ee/qolmmessage.cpp
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmmessage.h"
+
+#include "util.h"
+
+using namespace Quotient;
+
+QOlmMessage::QOlmMessage(QByteArray ciphertext, QOlmMessage::Type type)
+ : QByteArray(std::move(ciphertext))
+ , m_messageType(type)
+{
+ Q_ASSERT_X(!isEmpty(), "olm message", "Ciphertext is empty");
+}
+
+QOlmMessage::Type QOlmMessage::type() const
+{
+ return m_messageType;
+}
+
+QByteArray QOlmMessage::toCiphertext() const
+{
+ return SLICE(*this, QByteArray);
+}
+
+QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext)
+{
+ return QOlmMessage(ciphertext, QOlmMessage::General);
+}
diff --git a/lib/e2ee/qolmmessage.h b/lib/e2ee/qolmmessage.h
new file mode 100644
index 00000000..ea73b3e3
--- /dev/null
+++ b/lib/e2ee/qolmmessage.h
@@ -0,0 +1,42 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "quotient_export.h"
+
+#include <QtCore/QByteArray>
+#include <qobjectdefs.h>
+#include <olm/olm.h>
+
+namespace Quotient {
+
+/*! \brief A wrapper around an olm encrypted message
+ *
+ * This class encapsulates a Matrix olm encrypted message,
+ * passed in either of 2 forms: a general message or a pre-key message.
+ *
+ * The class provides functions to get a type and the ciphertext.
+ */
+class QUOTIENT_API QOlmMessage : public QByteArray {
+ Q_GADGET
+public:
+ enum Type {
+ PreKey = OLM_MESSAGE_TYPE_PRE_KEY,
+ General = OLM_MESSAGE_TYPE_MESSAGE,
+ };
+ Q_ENUM(Type)
+
+ explicit QOlmMessage(QByteArray ciphertext, Type type = General);
+
+ static QOlmMessage fromCiphertext(const QByteArray &ciphertext);
+
+ Q_INVOKABLE Type type() const;
+ Q_INVOKABLE QByteArray toCiphertext() const;
+
+private:
+ Type m_messageType = General;
+};
+
+} //namespace Quotient
diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp
new file mode 100644
index 00000000..1176d790
--- /dev/null
+++ b/lib/e2ee/qolmoutboundsession.cpp
@@ -0,0 +1,152 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmoutboundsession.h"
+
+#include "logging.h"
+#include "qolmutils.h"
+
+#include <olm/olm.h>
+
+using namespace Quotient;
+
+OlmErrorCode QOlmOutboundGroupSession::lastErrorCode() const {
+ return olm_outbound_group_session_last_error_code(m_groupSession);
+}
+
+const char* QOlmOutboundGroupSession::lastError() const
+{
+ return olm_outbound_group_session_last_error(m_groupSession);
+}
+
+QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session)
+ : m_groupSession(session)
+{}
+
+QOlmOutboundGroupSession::~QOlmOutboundGroupSession()
+{
+ olm_clear_outbound_group_session(m_groupSession);
+ delete[](reinterpret_cast<uint8_t *>(m_groupSession));
+}
+
+QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create()
+{
+ auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]);
+ if (const auto randomLength = olm_init_outbound_group_session_random_length(
+ olmOutboundGroupSession);
+ olm_init_outbound_group_session(olmOutboundGroupSession,
+ RandomBuffer(randomLength).bytes(),
+ randomLength)
+ == olm_error()) {
+ // FIXME: create the session object earlier
+ QOLM_INTERNAL_ERROR_X("Failed to initialise an outbound group session",
+ olm_outbound_group_session_last_error(
+ olmOutboundGroupSession));
+ }
+
+ return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession);
+}
+
+QByteArray QOlmOutboundGroupSession::pickle(const PicklingMode &mode) const
+{
+ QByteArray pickledBuf(
+ olm_pickle_outbound_group_session_length(m_groupSession), '\0');
+ auto key = toKey(mode);
+ if (olm_pickle_outbound_group_session(m_groupSession, key.data(),
+ key.length(), pickledBuf.data(),
+ pickledBuf.length())
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to pickle the outbound group session");
+
+ key.clear();
+ return pickledBuf;
+}
+
+QOlmExpected<QOlmOutboundGroupSessionPtr> QOlmOutboundGroupSession::unpickle(
+ QByteArray&& pickled, const PicklingMode& mode)
+{
+ auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]);
+ auto key = toKey(mode);
+ if (olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(),
+ key.length(), pickled.data(),
+ pickled.length())
+ == olm_error()) {
+ // FIXME: create the session object earlier and use lastError()
+ qWarning(E2EE) << "Failed to unpickle an outbound group session:"
+ << olm_outbound_group_session_last_error(
+ olmOutboundGroupSession);
+ return olm_outbound_group_session_last_error_code(
+ olmOutboundGroupSession);
+ }
+
+ key.clear();
+ return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession);
+}
+
+QByteArray QOlmOutboundGroupSession::encrypt(const QByteArray& plaintext) const
+{
+ const auto messageMaxLength =
+ olm_group_encrypt_message_length(m_groupSession, plaintext.length());
+ QByteArray messageBuf(messageMaxLength, '\0');
+ if (olm_group_encrypt(m_groupSession,
+ reinterpret_cast<const uint8_t*>(plaintext.data()),
+ plaintext.length(),
+ reinterpret_cast<uint8_t*>(messageBuf.data()),
+ messageBuf.length())
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to encrypt a message");
+
+ return messageBuf;
+}
+
+uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const
+{
+ return olm_outbound_group_session_message_index(m_groupSession);
+}
+
+QByteArray QOlmOutboundGroupSession::sessionId() const
+{
+ const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession);
+ QByteArray idBuffer(idMaxLength, '\0');
+ if (olm_outbound_group_session_id(
+ m_groupSession, reinterpret_cast<uint8_t*>(idBuffer.data()),
+ idBuffer.length())
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to obtain group session id");
+
+ return idBuffer;
+}
+
+QByteArray QOlmOutboundGroupSession::sessionKey() const
+{
+ const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession);
+ QByteArray keyBuffer(keyMaxLength, '\0');
+ if (olm_outbound_group_session_key(
+ m_groupSession, reinterpret_cast<uint8_t*>(keyBuffer.data()),
+ keyMaxLength)
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to obtain group session key");
+
+ return keyBuffer;
+}
+
+int QOlmOutboundGroupSession::messageCount() const
+{
+ return m_messageCount;
+}
+
+void QOlmOutboundGroupSession::setMessageCount(int messageCount)
+{
+ m_messageCount = messageCount;
+}
+
+QDateTime QOlmOutboundGroupSession::creationTime() const
+{
+ return m_creationTime;
+}
+
+void QOlmOutboundGroupSession::setCreationTime(const QDateTime& creationTime)
+{
+ m_creationTime = creationTime;
+}
diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h
new file mode 100644
index 00000000..d36fbf69
--- /dev/null
+++ b/lib/e2ee/qolmoutboundsession.h
@@ -0,0 +1,63 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "e2ee/e2ee.h"
+
+struct OlmOutboundGroupSession;
+
+namespace Quotient {
+
+//! An out-bound group session is responsible for encrypting outgoing
+//! communication in a Megolm session.
+class QUOTIENT_API QOlmOutboundGroupSession
+{
+public:
+ ~QOlmOutboundGroupSession();
+ //! Creates a new instance of `QOlmOutboundGroupSession`.
+ //! Throw OlmError on errors
+ static QOlmOutboundGroupSessionPtr create();
+ //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64.
+ QByteArray pickle(const PicklingMode &mode) const;
+ //! Deserialises from encrypted Base64 that was previously obtained by
+ //! pickling a `QOlmOutboundGroupSession`.
+ static QOlmExpected<QOlmOutboundGroupSessionPtr> unpickle(
+ QByteArray&& pickled, const PicklingMode& mode);
+
+ //! Encrypts a plaintext message using the session.
+ QByteArray encrypt(const QByteArray& plaintext) const;
+
+ //! Get the current message index for this session.
+ //!
+ //! Each message is sent with an increasing index; this returns the
+ //! index for the next message.
+ uint32_t sessionMessageIndex() const;
+
+ //! Get a base64-encoded identifier for this session.
+ QByteArray sessionId() const;
+
+ //! Get the base64-encoded current ratchet key for this session.
+ //!
+ //! Each message is sent with a different ratchet key. This function returns the
+ //! ratchet key that will be used for the next message.
+ QByteArray sessionKey() const;
+ QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession);
+
+ int messageCount() const;
+ void setMessageCount(int messageCount);
+
+ QDateTime creationTime() const;
+ void setCreationTime(const QDateTime& creationTime);
+
+ OlmErrorCode lastErrorCode() const;
+ const char* lastError() const;
+
+private:
+ OlmOutboundGroupSession *m_groupSession;
+ int m_messageCount = 0;
+ QDateTime m_creationTime = QDateTime::currentDateTime();
+};
+
+} // namespace Quotient
diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp
new file mode 100644
index 00000000..e3f69132
--- /dev/null
+++ b/lib/e2ee/qolmsession.cpp
@@ -0,0 +1,231 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmsession.h"
+
+#include "e2ee/qolmutils.h"
+#include "logging.h"
+
+#include <cstring>
+#include <olm/olm.h>
+
+using namespace Quotient;
+
+OlmErrorCode QOlmSession::lastErrorCode() const {
+ return olm_session_last_error_code(m_session);
+}
+
+const char* QOlmSession::lastError() const
+{
+ return olm_session_last_error(m_session);
+}
+
+Quotient::QOlmSession::~QOlmSession()
+{
+ olm_clear_session(m_session);
+ delete[](reinterpret_cast<uint8_t *>(m_session));
+}
+
+OlmSession* QOlmSession::create()
+{
+ return olm_session(new uint8_t[olm_session_size()]);
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmSession::createInbound(
+ QOlmAccount* account, const QOlmMessage& preKeyMessage, bool from,
+ const QString& theirIdentityKey)
+{
+ if (preKeyMessage.type() != QOlmMessage::PreKey) {
+ qCCritical(E2EE) << "The message is not a pre-key; will try to create "
+ "the inbound session anyway";
+ }
+
+ const auto olmSession = create();
+
+ QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext();
+ QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8();
+ size_t error = 0;
+ if (from) {
+ error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
+ } else {
+ error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
+ }
+
+ if (error == olm_error()) {
+ // FIXME: the QOlmSession object should be created earlier
+ const auto lastErr = olm_session_last_error_code(olmSession);
+ qCWarning(E2EE) << "Error when creating inbound session" << lastErr;
+ return lastErr;
+ }
+
+ return std::make_unique<QOlmSession>(olmSession);
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmSession::createInboundSession(
+ QOlmAccount* account, const QOlmMessage& preKeyMessage)
+{
+ return createInbound(account, preKeyMessage);
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmSession::createInboundSessionFrom(
+ QOlmAccount* account, const QString& theirIdentityKey,
+ const QOlmMessage& preKeyMessage)
+{
+ return createInbound(account, preKeyMessage, true, theirIdentityKey);
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmSession::createOutboundSession(
+ QOlmAccount* account, const QByteArray& theirIdentityKey,
+ const QByteArray& theirOneTimeKey)
+{
+ auto* olmOutboundSession = create();
+ if (const auto randomLength =
+ olm_create_outbound_session_random_length(olmOutboundSession);
+ olm_create_outbound_session(
+ olmOutboundSession, account->data(), theirIdentityKey.data(),
+ theirIdentityKey.length(), theirOneTimeKey.data(),
+ theirOneTimeKey.length(), RandomBuffer(randomLength), randomLength)
+ == olm_error()) {
+ // FIXME: the QOlmSession object should be created earlier
+ const auto lastErr = olm_session_last_error_code(olmOutboundSession);
+ QOLM_FAIL_OR_LOG_X(lastErr == OLM_NOT_ENOUGH_RANDOM,
+ "Failed to create an outbound Olm session",
+ olm_session_last_error(olmOutboundSession));
+ return lastErr;
+ }
+
+ return std::make_unique<QOlmSession>(olmOutboundSession);
+}
+
+QByteArray QOlmSession::pickle(const PicklingMode &mode) const
+{
+ QByteArray pickledBuf(olm_pickle_session_length(m_session), '\0');
+ QByteArray key = toKey(mode);
+ if (olm_pickle_session(m_session, key.data(), key.length(),
+ pickledBuf.data(), pickledBuf.length())
+ == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to pickle an Olm session");
+
+ key.clear();
+ return pickledBuf;
+}
+
+QOlmExpected<QOlmSessionPtr> QOlmSession::unpickle(QByteArray&& pickled,
+ const PicklingMode& mode)
+{
+ auto *olmSession = create();
+ auto key = toKey(mode);
+ if (olm_unpickle_session(olmSession, key.data(), key.length(),
+ pickled.data(), pickled.length())
+ == olm_error()) {
+ // FIXME: the QOlmSession object should be created earlier
+ const auto errorCode = olm_session_last_error_code(olmSession);
+ QOLM_FAIL_OR_LOG_X(errorCode == OLM_OUTPUT_BUFFER_TOO_SMALL,
+ "Failed to unpickle an Olm session",
+ olm_session_last_error(olmSession));
+ return errorCode;
+ }
+
+ key.clear();
+ return std::make_unique<QOlmSession>(olmSession);
+}
+
+QOlmMessage QOlmSession::encrypt(const QByteArray& plaintext)
+{
+ const auto messageMaxLength =
+ olm_encrypt_message_length(m_session, plaintext.length());
+ QByteArray messageBuf(messageMaxLength, '\0');
+ // NB: The type has to be calculated before calling olm_encrypt()
+ const auto messageType = olm_encrypt_message_type(m_session);
+ if (const auto randomLength = olm_encrypt_random_length(m_session);
+ olm_encrypt(m_session, plaintext.data(), plaintext.length(),
+ RandomBuffer(randomLength), randomLength, messageBuf.data(),
+ messageBuf.length())
+ == olm_error()) {
+ QOLM_INTERNAL_ERROR("Failed to encrypt the message");
+ }
+
+ return QOlmMessage(messageBuf, QOlmMessage::Type(messageType));
+}
+
+QOlmExpected<QByteArray> QOlmSession::decrypt(const QOlmMessage &message) const
+{
+ const auto ciphertext = message.toCiphertext();
+ const auto messageTypeValue = message.type();
+
+ // We need to clone the message because
+ // olm_decrypt_max_plaintext_length destroys the input buffer
+ QByteArray messageBuf(ciphertext.length(), '\0');
+ std::copy(message.begin(), message.end(), messageBuf.begin());
+
+ const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(
+ m_session, messageTypeValue, messageBuf.data(), messageBuf.length());
+ if (plaintextMaxLen == olm_error()) {
+ qWarning(E2EE) << "Couldn't calculate decrypted message length:"
+ << lastError();
+ return lastErrorCode();
+ }
+
+ QByteArray plaintextBuf(plaintextMaxLen, '\0');
+ QByteArray messageBuf2(ciphertext.length(), '\0');
+ std::copy(message.begin(), message.end(), messageBuf2.begin());
+
+ const auto plaintextResultLen =
+ olm_decrypt(m_session, messageTypeValue, messageBuf2.data(),
+ messageBuf2.length(), plaintextBuf.data(), plaintextMaxLen);
+ if (plaintextResultLen == olm_error()) {
+ QOLM_FAIL_OR_LOG(OLM_OUTPUT_BUFFER_TOO_SMALL,
+ "Failed to decrypt the message");
+ return lastErrorCode();
+ }
+ plaintextBuf.truncate(plaintextResultLen);
+ return plaintextBuf;
+}
+
+QByteArray QOlmSession::sessionId() const
+{
+ const auto idMaxLength = olm_session_id_length(m_session);
+ QByteArray idBuffer(idMaxLength, '\0');
+ if (olm_session_id(m_session, idBuffer.data(), idMaxLength) == olm_error())
+ QOLM_INTERNAL_ERROR("Failed to obtain Olm session id");
+
+ return idBuffer;
+}
+
+bool QOlmSession::hasReceivedMessage() const
+{
+ return olm_session_has_received_message(m_session);
+}
+
+bool QOlmSession::matchesInboundSession(const QOlmMessage& preKeyMessage) const
+{
+ Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey);
+ QByteArray oneTimeKeyBuf(preKeyMessage.data());
+ const auto maybeMatches =
+ olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(),
+ oneTimeKeyBuf.length());
+ if (maybeMatches == olm_error())
+ qWarning(E2EE) << "Error matching an inbound session:" << lastError();
+
+ return maybeMatches == 1; // Any errors are treated as non-match
+}
+
+bool QOlmSession::matchesInboundSessionFrom(
+ const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const
+{
+ const auto theirIdentityKeyBuf = theirIdentityKey.toUtf8();
+ auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext();
+ const auto maybeMatches = olm_matches_inbound_session_from(
+ m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(),
+ oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
+
+ if (maybeMatches == olm_error())
+ qCWarning(E2EE) << "Error matching an inbound session:" << lastError();
+
+ return maybeMatches == 1;
+}
+
+QOlmSession::QOlmSession(OlmSession *session)
+ : m_session(session)
+{}
diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h
new file mode 100644
index 00000000..400fb854
--- /dev/null
+++ b/lib/e2ee/qolmsession.h
@@ -0,0 +1,84 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmmessage.h"
+#include "e2ee/qolmaccount.h"
+
+struct OlmSession;
+
+namespace Quotient {
+
+//! Either an outbound or inbound session for secure communication.
+class QUOTIENT_API QOlmSession
+{
+public:
+ ~QOlmSession();
+ //! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
+ static QOlmExpected<QOlmSessionPtr> createInboundSession(
+ QOlmAccount* account, const QOlmMessage& preKeyMessage);
+
+ static QOlmExpected<QOlmSessionPtr> createInboundSessionFrom(
+ QOlmAccount* account, const QString& theirIdentityKey,
+ const QOlmMessage& preKeyMessage);
+
+ static QOlmExpected<QOlmSessionPtr> createOutboundSession(
+ QOlmAccount* account, const QByteArray& theirIdentityKey,
+ const QByteArray& theirOneTimeKey);
+
+ //! Serialises an `QOlmSession` to encrypted Base64.
+ QByteArray pickle(const PicklingMode &mode) const;
+
+ //! Deserialises from encrypted Base64 previously made with pickle()
+ static QOlmExpected<QOlmSessionPtr> unpickle(QByteArray&& pickled,
+ const PicklingMode& mode);
+
+ //! Encrypts a plaintext message using the session.
+ QOlmMessage encrypt(const QByteArray& plaintext);
+
+ //! Decrypts a message using this session. Decoding is lossy, meaning if
+ //! the decrypted plaintext contains invalid UTF-8 symbols, they will
+ //! be returned as `U+FFFD` (�).
+ QOlmExpected<QByteArray> decrypt(const QOlmMessage &message) const;
+
+ //! Get a base64-encoded identifier for this session.
+ QByteArray sessionId() const;
+
+ //! Checker for any received messages for this session.
+ bool hasReceivedMessage() const;
+
+ //! Checks if the 'prekey' message is for this in-bound session.
+ bool matchesInboundSession(const QOlmMessage& preKeyMessage) const;
+
+ //! Checks if the 'prekey' message is for this in-bound session.
+ bool matchesInboundSessionFrom(
+ const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const;
+
+ friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs)
+ {
+ return lhs.sessionId() < rhs.sessionId();
+ }
+
+ friend bool operator<(const QOlmSessionPtr& lhs, const QOlmSessionPtr& rhs)
+ {
+ return *lhs < *rhs;
+ }
+
+ OlmErrorCode lastErrorCode() const;
+ const char* lastError() const;
+
+ OlmSession* raw() const { return m_session; }
+
+ QOlmSession(OlmSession* session);
+private:
+ //! Helper function for creating new sessions and handling errors.
+ static OlmSession* create();
+ static QOlmExpected<QOlmSessionPtr> createInbound(
+ QOlmAccount* account, const QOlmMessage& preKeyMessage,
+ bool from = false, const QString& theirIdentityKey = "");
+ OlmSession* m_session;
+};
+} //namespace Quotient
diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp
new file mode 100644
index 00000000..46f7f4f3
--- /dev/null
+++ b/lib/e2ee/qolmutility.cpp
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "e2ee/qolmutility.h"
+
+#include <olm/olm.h>
+
+using namespace Quotient;
+
+OlmErrorCode QOlmUtility::lastErrorCode() const {
+ return olm_utility_last_error_code(m_utility);
+}
+
+const char* QOlmUtility::lastError() const
+{
+ return olm_utility_last_error(m_utility);
+}
+
+QOlmUtility::QOlmUtility()
+{
+ auto utility = new uint8_t[olm_utility_size()];
+ m_utility = olm_utility(utility);
+}
+
+QOlmUtility::~QOlmUtility()
+{
+ olm_clear_utility(m_utility);
+ delete[](reinterpret_cast<uint8_t *>(m_utility));
+}
+
+QString QOlmUtility::sha256Bytes(const QByteArray &inputBuf) const
+{
+ const auto outputLen = olm_sha256_length(m_utility);
+ QByteArray outputBuf(outputLen, '\0');
+ olm_sha256(m_utility, inputBuf.data(), inputBuf.length(),
+ outputBuf.data(), outputBuf.length());
+
+ return QString::fromUtf8(outputBuf);
+}
+
+QString QOlmUtility::sha256Utf8Msg(const QString &message) const
+{
+ return sha256Bytes(message.toUtf8());
+}
+
+bool QOlmUtility::ed25519Verify(const QByteArray& key, const QByteArray& message,
+ QByteArray signature)
+{
+ return olm_ed25519_verify(m_utility, key.data(), key.size(), message.data(),
+ message.size(), signature.data(), signature.size())
+ == 0;
+}
diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h
new file mode 100644
index 00000000..508767bf
--- /dev/null
+++ b/lib/e2ee/qolmutility.h
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "e2ee/e2ee.h"
+
+struct OlmUtility;
+
+namespace Quotient {
+
+//! Allows you to make use of crytographic hashing via SHA-2 and
+//! verifying ed25519 signatures.
+class QUOTIENT_API QOlmUtility
+{
+public:
+ QOlmUtility();
+ ~QOlmUtility();
+
+ //! Returns a sha256 of the supplied byte slice.
+ QString sha256Bytes(const QByteArray &inputBuf) const;
+
+ //! Convenience function that converts the UTF-8 message
+ //! to bytes and then calls `sha256Bytes()`, returning its output.
+ QString sha256Utf8Msg(const QString &message) const;
+
+ //! Verify a ed25519 signature.
+ //! \param key QByteArray The public part of the ed25519 key that signed the message.
+ //! \param message QByteArray The message that was signed.
+ //! \param signature QByteArray The signature of the message.
+ bool ed25519Verify(const QByteArray &key,
+ const QByteArray &message, QByteArray signature);
+
+ OlmErrorCode lastErrorCode() const;
+ const char* lastError() const;
+
+private:
+ OlmUtility *m_utility;
+};
+}
diff --git a/lib/e2ee/qolmutils.cpp b/lib/e2ee/qolmutils.cpp
new file mode 100644
index 00000000..c6e51bcd
--- /dev/null
+++ b/lib/e2ee/qolmutils.cpp
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "e2ee/qolmutils.h"
+#include <QtCore/QRandomGenerator>
+
+using namespace Quotient;
+
+QByteArray Quotient::toKey(const Quotient::PicklingMode &mode)
+{
+ if (std::holds_alternative<Quotient::Unencrypted>(mode)) {
+ return {};
+ }
+ return std::get<Quotient::Encrypted>(mode).key;
+}
+
+RandomBuffer::RandomBuffer(size_t size)
+ : QByteArray(static_cast<int>(size), '\0')
+{
+ QRandomGenerator::system()->generate(begin(), end());
+}
diff --git a/lib/e2ee/qolmutils.h b/lib/e2ee/qolmutils.h
new file mode 100644
index 00000000..17eee7a3
--- /dev/null
+++ b/lib/e2ee/qolmutils.h
@@ -0,0 +1,55 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QByteArray>
+
+#include "e2ee/e2ee.h"
+
+namespace Quotient {
+
+// Convert PicklingMode to key
+QUOTIENT_API QByteArray toKey(const PicklingMode &mode);
+
+class QUOTIENT_API RandomBuffer : public QByteArray {
+public:
+ explicit RandomBuffer(size_t size);
+ ~RandomBuffer() { clear(); }
+
+ // NOLINTNEXTLINE(google-explicit-constructor)
+ QUO_IMPLICIT operator void*() { return data(); }
+ char* chars() { return data(); }
+ uint8_t* bytes() { return reinterpret_cast<uint8_t*>(data()); }
+
+ Q_DISABLE_COPY(RandomBuffer)
+ RandomBuffer(RandomBuffer&&) = default;
+ void operator=(RandomBuffer&&) = delete;
+};
+
+[[deprecated("Create RandomBuffer directly")]] inline auto getRandom(
+ size_t bufferSize)
+{
+ return RandomBuffer(bufferSize);
+}
+
+#define QOLM_INTERNAL_ERROR_X(Message_, LastError_) \
+ qFatal("%s, internal error: %s", Message_, LastError_)
+
+#define QOLM_INTERNAL_ERROR(Message_) \
+ QOLM_INTERNAL_ERROR_X(Message_, lastError())
+
+#define QOLM_FAIL_OR_LOG_X(InternalCondition_, Message_, LastErrorText_) \
+ do { \
+ const QString errorMsg{ (Message_) }; \
+ if (InternalCondition_) \
+ QOLM_INTERNAL_ERROR_X(qPrintable(errorMsg), (LastErrorText_)); \
+ qWarning(E2EE).nospace() << errorMsg << ": " << (LastErrorText_); \
+ } while (false) /* End of macro */
+
+#define QOLM_FAIL_OR_LOG(InternalFailureValue_, Message_) \
+ QOLM_FAIL_OR_LOG_X(lastErrorCode() == (InternalFailureValue_), (Message_), \
+ lastError())
+
+} // namespace Quotient
diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp
deleted file mode 100644
index 4a1025b2..00000000
--- a/lib/encryptionmanager.cpp
+++ /dev/null
@@ -1,369 +0,0 @@
-#ifdef Quotient_E2EE_ENABLED
-#include "encryptionmanager.h"
-
-#include "connection.h"
-#include "e2ee.h"
-
-#include "csapi/keys.h"
-
-#include <QtCore/QHash>
-#include <QtCore/QStringBuilder>
-
-#include <account.h> // QtOlm
-#include <session.h> // QtOlm
-#include <message.h> // QtOlm
-#include <errors.h> // QtOlm
-#include <utils.h> // QtOlm
-#include <functional>
-#include <memory>
-
-using namespace Quotient;
-using namespace QtOlm;
-using std::move;
-
-class EncryptionManager::Private {
-public:
- explicit Private(const QByteArray& encryptionAccountPickle,
- float signedKeysProportion, float oneTimeKeyThreshold)
- : q(nullptr)
- , signedKeysProportion(move(signedKeysProportion))
- , oneTimeKeyThreshold(move(oneTimeKeyThreshold))
- {
- Q_ASSERT((0 <= signedKeysProportion) && (signedKeysProportion <= 1));
- Q_ASSERT((0 <= oneTimeKeyThreshold) && (oneTimeKeyThreshold <= 1));
- if (encryptionAccountPickle.isEmpty()) {
- olmAccount.reset(new Account());
- } else {
- olmAccount.reset(
- new Account(encryptionAccountPickle)); // TODO: passphrase even
- // with qtkeychain?
- }
- /*
- * Note about targetKeysNumber:
- *
- * From: https://github.com/Zil0/matrix-python-sdk/
- * File: matrix_client/crypto/olm_device.py
- *
- * Try to maintain half the number of one-time keys libolm can hold
- * uploaded on the HS. This is because some keys will be claimed by
- * peers but not used instantly, and we want them to stay in libolm,
- * until the limit is reached and it starts discarding keys, starting by
- * the oldest.
- */
- targetKeysNumber = olmAccount->maxOneTimeKeys() / 2;
- targetOneTimeKeyCounts = {
- { SignedCurve25519Key,
- qRound(signedKeysProportion * targetKeysNumber) },
- { Curve25519Key,
- qRound((1 - signedKeysProportion) * targetKeysNumber) }
- };
- updateKeysToUpload();
- }
- ~Private() = default;
-
- EncryptionManager* q;
-
- UploadKeysJob* uploadIdentityKeysJob = nullptr;
- UploadKeysJob* uploadOneTimeKeysInitJob = nullptr;
- UploadKeysJob* uploadOneTimeKeysJob = nullptr;
- QueryKeysJob* queryKeysJob = nullptr;
-
- QScopedPointer<Account> olmAccount;
-
- float signedKeysProportion;
- float oneTimeKeyThreshold;
- int targetKeysNumber;
-
- void updateKeysToUpload();
- bool oneTimeKeyShouldUpload();
-
- QHash<QString, int> oneTimeKeyCounts;
- void setOneTimeKeyCounts(const QHash<QString, int> oneTimeKeyCountsNewValue)
- {
- oneTimeKeyCounts = oneTimeKeyCountsNewValue;
- updateKeysToUpload();
- }
- QHash<QString, int> oneTimeKeysToUploadCounts;
- QHash<QString, int> targetOneTimeKeyCounts;
-
- // A map from senderKey to InboundSession
- QMap<QString, InboundSession*> sessions; // TODO: cache
- void updateDeviceKeys(
- const QHash<QString,
- QHash<QString, QueryKeysJob::DeviceInformation>>& deviceKeys)
- {
- for (auto userId : deviceKeys.keys()) {
- for (auto deviceId : deviceKeys.value(userId).keys()) {
- auto info = deviceKeys.value(userId).value(deviceId);
- // TODO: ed25519Verify, etc
- }
- }
- }
- QString sessionDecrypt(Message* message, const QString& senderKey)
- {
- QString decrypted;
- QList<InboundSession*> senderSessions = sessions.values(senderKey);
- // Try to decrypt message body using one of the known sessions for that
- // device
- bool sessionsPassed = false;
- for (auto senderSession : senderSessions) {
- if (senderSession == senderSessions.last()) {
- sessionsPassed = true;
- }
- try {
- decrypted = senderSession->decrypt(message);
- qCDebug(E2EE)
- << "Success decrypting Olm event using existing session"
- << senderSession->id();
- break;
- } catch (OlmError* e) {
- if (message->messageType() == 0) {
- PreKeyMessage preKeyMessage =
- PreKeyMessage(message->cipherText());
- if (senderSession->matches(&preKeyMessage, senderKey)) {
- // We had a matching session for a pre-key message, but
- // it didn't work. This means something is wrong, so we
- // fail now.
- qCDebug(E2EE)
- << "Error decrypting pre-key message with existing "
- "Olm session"
- << senderSession->id() << "reason:" << e->what();
- return QString();
- }
- }
- // Simply keep trying otherwise
- }
- }
- if (sessionsPassed || senderSessions.empty()) {
- if (message->messageType() > 0) {
- // Not a pre-key message, we should have had a matching session
- if (!sessions.empty()) {
- qCDebug(E2EE) << "Error decrypting with existing sessions";
- return QString();
- }
- qCDebug(E2EE) << "No existing sessions";
- return QString();
- }
- // We have a pre-key message without any matching session, in this
- // case we should try to create one.
- InboundSession* newSession;
- qCDebug(E2EE) << "try to establish new InboundSession with" << senderKey;
- PreKeyMessage preKeyMessage = PreKeyMessage(message->cipherText());
- try {
- newSession = new InboundSession(olmAccount.data(),
- &preKeyMessage,
- senderKey.toLatin1(), q);
- } catch (OlmError* e) {
- qCDebug(E2EE) << "Error decrypting pre-key message when trying "
- "to establish a new session:"
- << e->what();
- return QString();
- }
- qCDebug(E2EE) << "Created new Olm session" << newSession->id();
- try {
- decrypted = newSession->decrypt(message);
- } catch (OlmError* e) {
- qCDebug(E2EE)
- << "Error decrypting pre-key message with new session"
- << e->what();
- return QString();
- }
- olmAccount->removeOneTimeKeys(newSession);
- sessions.insert(senderKey, newSession);
- }
- return decrypted;
- }
-};
-
-EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle,
- float signedKeysProportion,
- float oneTimeKeyThreshold, QObject* parent)
- : QObject(parent)
- , d(std::make_unique<Private>(std::move(encryptionAccountPickle),
- std::move(signedKeysProportion),
- std::move(oneTimeKeyThreshold)))
-{
- d->q = this;
-}
-
-EncryptionManager::~EncryptionManager() = default;
-
-void EncryptionManager::uploadIdentityKeys(Connection* connection)
-{
- // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-keys-upload
- DeviceKeys deviceKeys {
- /*
- * The ID of the user the device belongs to. Must match the user ID used
- * when logging in. The ID of the device these keys belong to. Must
- * match the device ID used when logging in. The encryption algorithms
- * supported by this device.
- */
- connection->userId(),
- connection->deviceId(),
- SupportedAlgorithms,
- /*
- * Public identity keys. The names of the properties should be in the
- * format <algorithm>:<device_id>. The keys themselves should be encoded
- * as specified by the key algorithm.
- */
- { { Curve25519Key + QStringLiteral(":") + connection->deviceId(),
- d->olmAccount->curve25519IdentityKey() },
- { Ed25519Key + QStringLiteral(":") + connection->deviceId(),
- d->olmAccount->ed25519IdentityKey() } },
- /* signatures should be provided after the unsigned deviceKeys
- generation */
- {}
- };
-
- QJsonObject deviceKeysJsonObject = toJson(deviceKeys);
- /* additionally removing signatures key,
- * since we could not initialize deviceKeys
- * without an empty signatures value:
- */
- deviceKeysJsonObject.remove(QStringLiteral("signatures"));
- /*
- * Signatures for the device key object.
- * A map from user ID, to a map from <algorithm>:<device_id> to the
- * signature. The signature is calculated using the process called Signing
- * JSON.
- */
- deviceKeys.signatures = {
- { connection->userId(),
- { { Ed25519Key + QStringLiteral(":") + connection->deviceId(),
- d->olmAccount->sign(deviceKeysJsonObject) } } }
- };
-
- d->uploadIdentityKeysJob = connection->callApi<UploadKeysJob>(deviceKeys);
- connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] {
- d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts());
- });
-}
-
-void EncryptionManager::uploadOneTimeKeys(Connection* connection,
- bool forceUpdate)
-{
- if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) {
- d->uploadOneTimeKeysInitJob = connection->callApi<UploadKeysJob>();
- connect(d->uploadOneTimeKeysInitJob, &BaseJob::success, this, [this] {
- d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts());
- });
- }
-
- int signedKeysToUploadCount =
- d->oneTimeKeysToUploadCounts.value(SignedCurve25519Key, 0);
- int unsignedKeysToUploadCount =
- d->oneTimeKeysToUploadCounts.value(Curve25519Key, 0);
-
- d->olmAccount->generateOneTimeKeys(signedKeysToUploadCount
- + unsignedKeysToUploadCount);
-
- QHash<QString, QVariant> oneTimeKeys = {};
- const auto& olmAccountCurve25519OneTimeKeys =
- d->olmAccount->curve25519OneTimeKeys();
-
- int oneTimeKeysCounter = 0;
- for (auto it = olmAccountCurve25519OneTimeKeys.cbegin();
- it != olmAccountCurve25519OneTimeKeys.cend(); ++it) {
- QString keyId = it.key();
- QString keyType;
- QVariant key;
- if (oneTimeKeysCounter < signedKeysToUploadCount) {
- QJsonObject message { { QStringLiteral("key"),
- it.value().toString() } };
-
- QByteArray signedMessage = d->olmAccount->sign(message);
- QJsonObject signatures {
- { connection->userId(),
- QJsonObject { { Ed25519Key + QStringLiteral(":")
- + connection->deviceId(),
- QString::fromUtf8(signedMessage) } } }
- };
- message.insert(QStringLiteral("signatures"), signatures);
- key = message;
- keyType = SignedCurve25519Key;
- } else {
- key = it.value();
- keyType = Curve25519Key;
- }
- ++oneTimeKeysCounter;
- oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key);
- }
- d->uploadOneTimeKeysJob =
- connection->callApi<UploadKeysJob>(none, oneTimeKeys);
- connect(d->uploadOneTimeKeysJob, &BaseJob::success, this, [this] {
- d->setOneTimeKeyCounts(d->uploadOneTimeKeysJob->oneTimeKeyCounts());
- });
- d->olmAccount->markKeysAsPublished();
- qCDebug(E2EE) << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.")
- .arg(signedKeysToUploadCount)
- .arg(unsignedKeysToUploadCount);
-}
-
-void EncryptionManager::updateOneTimeKeyCounts(
- Connection* connection, const QHash<QString, int>& deviceOneTimeKeysCount)
-{
- d->oneTimeKeyCounts = deviceOneTimeKeysCount;
- if (d->oneTimeKeyShouldUpload()) {
- qCDebug(E2EE) << "Uploading new one-time keys.";
- uploadOneTimeKeys(connection);
- }
-}
-
-void Quotient::EncryptionManager::updateDeviceKeys(
- Connection* connection, const QHash<QString, QStringList>& deviceKeys)
-{
- d->queryKeysJob = connection->callApi<QueryKeysJob>(deviceKeys);
- connect(d->queryKeysJob, &BaseJob::success, this,
- [this] { d->updateDeviceKeys(d->queryKeysJob->deviceKeys()); });
-}
-
-QString EncryptionManager::sessionDecryptMessage(
- const QJsonObject& personalCipherObject, const QByteArray& senderKey)
-{
- QString decrypted;
- int type = personalCipherObject.value(TypeKeyL).toInt(-1);
- QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1();
- if (type == 0) {
- PreKeyMessage preKeyMessage { body };
- decrypted = d->sessionDecrypt(reinterpret_cast<Message*>(&preKeyMessage),
- senderKey);
- } else if (type == 1) {
- Message message { body };
- decrypted = d->sessionDecrypt(&message, senderKey);
- }
- return decrypted;
-}
-
-QByteArray EncryptionManager::olmAccountPickle()
-{
- return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain?
-}
-
-QtOlm::Account* EncryptionManager::account() const
-{
- return d->olmAccount.data();
-}
-
-void EncryptionManager::Private::updateKeysToUpload()
-{
- for (auto it = targetOneTimeKeyCounts.cbegin();
- it != targetOneTimeKeyCounts.cend(); ++it) {
- int numKeys = oneTimeKeyCounts.value(it.key(), 0);
- int numToCreate = qMax(it.value() - numKeys, 0);
- oneTimeKeysToUploadCounts.insert(it.key(), numToCreate);
- }
-}
-
-bool EncryptionManager::Private::oneTimeKeyShouldUpload()
-{
- if (oneTimeKeyCounts.empty())
- return true;
- for (auto it = targetOneTimeKeyCounts.cbegin();
- it != targetOneTimeKeyCounts.cend(); ++it) {
- if (oneTimeKeyCounts.value(it.key(), 0)
- < it.value() * oneTimeKeyThreshold)
- return true;
- }
- return false;
-}
-#endif // Quotient_E2EE_ENABLED
diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h
deleted file mode 100644
index 5df15e83..00000000
--- a/lib/encryptionmanager.h
+++ /dev/null
@@ -1,47 +0,0 @@
-#ifdef Quotient_E2EE_ENABLED
-#pragma once
-
-#include <QtCore/QObject>
-
-#include <functional>
-#include <memory>
-
-namespace QtOlm {
-class Account;
-}
-
-namespace Quotient {
-class Connection;
-
-class EncryptionManager : public QObject {
- Q_OBJECT
-
-public:
- // TODO: store constats separately?
- // TODO: 0.5 oneTimeKeyThreshold instead of 0.1?
- explicit EncryptionManager(
- const QByteArray& encryptionAccountPickle = QByteArray(),
- float signedKeysProportion = 1, float oneTimeKeyThreshold = float(0.1),
- QObject* parent = nullptr);
- ~EncryptionManager();
-
- void uploadIdentityKeys(Connection* connection);
- void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false);
- void
- updateOneTimeKeyCounts(Connection* connection,
- const QHash<QString, int>& deviceOneTimeKeysCount);
- void updateDeviceKeys(Connection* connection,
- const QHash<QString, QStringList>& deviceKeys);
- QString sessionDecryptMessage(const QJsonObject& personalCipherObject,
- const QByteArray& senderKey);
- QByteArray olmAccountPickle();
-
- QtOlm::Account* account() const;
-
-private:
- class Private;
- std::unique_ptr<Private> d;
-};
-
-} // namespace Quotient
-#endif // Quotient_E2EE_ENABLED
diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp
index 2e2b11c0..a2e2a156 100644
--- a/lib/eventitem.cpp
+++ b/lib/eventitem.cpp
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "eventitem.h"
@@ -23,20 +8,25 @@
using namespace Quotient;
-void PendingEventItem::setFileUploaded(const QUrl& remoteUrl)
+void PendingEventItem::setFileUploaded(const FileSourceInfo& uploadedFileData)
{
// TODO: eventually we might introduce hasFileContent to RoomEvent,
// and unify the code below.
if (auto* rme = getAs<RoomMessageEvent>()) {
Q_ASSERT(rme->hasFileContent());
- rme->editContent([remoteUrl](EventContent::TypedBase& ec) {
- ec.fileInfo()->url = remoteUrl;
+ rme->editContent([&uploadedFileData](EventContent::TypedBase& ec) {
+ ec.fileInfo()->source = uploadedFileData;
});
}
if (auto* rae = getAs<RoomAvatarEvent>()) {
Q_ASSERT(rae->content().fileInfo());
- rae->editContent(
- [remoteUrl](EventContent::FileInfo& fi) { fi.url = remoteUrl; });
+ rae->editContent([&uploadedFileData](EventContent::FileInfo& fi) {
+ fi.source = uploadedFileData;
+ });
}
setStatus(EventStatus::FileUploaded);
}
+
+// Not exactly sure why but this helps with the linker not finding
+// Quotient::EventStatus::staticMetaObject when building Quaternion
+#include "moc_eventitem.cpp"
diff --git a/lib/eventitem.h b/lib/eventitem.h
index 7b2c3c44..96e45b38 100644
--- a/lib/eventitem.h
+++ b/lib/eventitem.h
@@ -1,55 +1,43 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
+#include "quotient_common.h"
+
+#include "events/callevents.h"
+#include "events/filesourceinfo.h"
#include "events/stateevent.h"
+#include <any>
#include <utility>
namespace Quotient {
-class StateEventBase;
-class EventStatus {
- Q_GADGET
-public:
+namespace EventStatus {
+ Q_NAMESPACE_EXPORT(QUOTIENT_API)
+
/** Special marks an event can assume
*
* This is used to hint at a special status of some events in UI.
* All values except Redacted and Hidden are mutually exclusive.
*/
enum Code {
- Normal = 0x0, //< No special designation
- Submitted = 0x01, //< The event has just been submitted for sending
- FileUploaded = 0x02, //< The file attached to the event has been
- // uploaded to the server
- Departed = 0x03, //< The event has left the client
- ReachedServer = 0x04, //< The server has received the event
- SendingFailed = 0x05, //< The server could not receive the event
- Redacted = 0x08, //< The event has been redacted
- Replaced = 0x10, //< The event has been replaced
- Hidden = 0x100, //< The event should not be shown in the timeline
+ Normal = 0x0, ///< No special designation
+ Submitted = 0x01, ///< The event has just been submitted for sending
+ FileUploaded = 0x02, ///< The file attached to the event has been
+ /// uploaded to the server
+ Departed = 0x03, ///< The event has left the client
+ ReachedServer = 0x04, ///< The server has received the event
+ SendingFailed = 0x05, ///< The server could not receive the event
+ Redacted = 0x08, ///< The event has been redacted
+ Replaced = 0x10, ///< The event has been replaced
+ Hidden = 0x100, ///< The event should not be shown in the timeline
};
- Q_DECLARE_FLAGS(Status, Code)
- Q_FLAG(Status)
-};
+ Q_ENUM_NS(Code)
+} // namespace EventStatus
-class EventItemBase {
+class QUOTIENT_API EventItemBase {
public:
explicit EventItemBase(RoomEventPtr&& e) : evt(std::move(e))
{
@@ -58,7 +46,7 @@ public:
const RoomEvent* event() const { return rawPtr(evt); }
const RoomEvent* get() const { return event(); }
- template <typename EventT>
+ template <EventClass<RoomEvent> EventT>
const EventT* viewAs() const
{
return eventCast<const EventT>(evt);
@@ -72,8 +60,14 @@ public:
return std::exchange(evt, move(other));
}
+ /// Store arbitrary data with the event item
+ void setUserData(std::any userData) { data = std::move(userData); }
+ /// Obtain custom data previously stored with the event item
+ const std::any& userdata() const { return data; }
+ std::any& userData() { return data; }
+
protected:
- template <typename EventT>
+ template <EventClass<RoomEvent> EventT>
EventT* getAs()
{
return eventCast<EventT>(evt);
@@ -81,9 +75,10 @@ protected:
private:
RoomEventPtr evt;
+ std::any data;
};
-class TimelineItem : public EventItemBase {
+class QUOTIENT_API TimelineItem : public EventItemBase {
public:
// For compatibility with Qt containers, even though we use
// a std:: container now for the room timeline
@@ -100,20 +95,18 @@ private:
};
template <>
-inline const StateEventBase* EventItemBase::viewAs<StateEventBase>() const
+inline const StateEvent* EventItemBase::viewAs<StateEvent>() const
{
- return evt->isStateEvent() ? weakPtrCast<const StateEventBase>(evt)
- : nullptr;
+ return evt->isStateEvent() ? weakPtrCast<const StateEvent>(evt) : nullptr;
}
template <>
-inline const CallEventBase* EventItemBase::viewAs<CallEventBase>() const
+inline const CallEvent* EventItemBase::viewAs<CallEvent>() const
{
- return evt->isCallEvent() ? weakPtrCast<const CallEventBase>(evt) : nullptr;
+ return evt->is<CallEvent>() ? weakPtrCast<const CallEvent>(evt) : nullptr;
}
-class PendingEventItem : public EventItemBase {
- Q_GADGET
+class QUOTIENT_API PendingEventItem : public EventItemBase {
public:
using EventItemBase::EventItemBase;
@@ -122,7 +115,7 @@ public:
QString annotation() const { return _annotation; }
void setDeparted() { setStatus(EventStatus::Departed); }
- void setFileUploaded(const QUrl& remoteUrl);
+ void setFileUploaded(const FileSourceInfo &uploadedFileData);
void setReachedServer(const QString& eventId)
{
setStatus(EventStatus::ReachedServer);
@@ -155,4 +148,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 a55016d9..324ce449 100644
--- a/lib/events/accountdataevents.h
+++ b/lib/events/accountdataevents.h
@@ -1,47 +1,26 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
-#include "converters.h"
#include "event.h"
-#include "eventcontent.h"
namespace Quotient {
-constexpr const char* FavouriteTag = "m.favourite";
-constexpr const char* LowPriorityTag = "m.lowpriority";
-constexpr const char* ServerNoticeTag = "m.server_notice";
+constexpr auto FavouriteTag [[maybe_unused]] = "m.favourite"_ls;
+constexpr auto LowPriorityTag [[maybe_unused]] = "m.lowpriority"_ls;
+constexpr auto ServerNoticeTag [[maybe_unused]] = "m.server_notice"_ls;
struct TagRecord {
- using order_type = Omittable<float>;
-
- order_type order;
-
- TagRecord(order_type order = none) : order(std::move(order)) {}
-
- bool operator<(const TagRecord& other) const
- {
- // Per The Spec, rooms with no order should be after those with order,
- // against optional<>::operator<() convention.
- return order && (!other.order || *order < *other.order);
- }
+ Omittable<float> order = none;
};
+inline bool operator<(TagRecord lhs, TagRecord rhs)
+{
+ // Per The Spec, rooms with no order should be after those with order,
+ // against std::optional<>::operator<() convention.
+ return lhs.order && (!rhs.order || *lhs.order < *rhs.order);
+}
+
template <>
struct JsonObjectConverter<TagRecord> {
static void fillFrom(const QJsonObject& jo, TagRecord& rec)
@@ -52,13 +31,13 @@ struct JsonObjectConverter<TagRecord> {
if (orderJv.isDouble())
rec.order = fromJson<float>(orderJv);
if (orderJv.isString()) {
- bool ok;
+ bool ok = false;
rec.order = orderJv.toString().toFloat(&ok);
if (!ok)
rec.order = none;
}
}
- static void dumpTo(QJsonObject& jo, const TagRecord& rec)
+ static void dumpTo(QJsonObject& jo, TagRecord rec)
{
addParam<IfNotEmpty>(jo, QStringLiteral("order"), rec.order);
}
@@ -66,27 +45,21 @@ struct JsonObjectConverter<TagRecord> {
using TagsMap = QHash<QString, TagRecord>;
-#define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \
- class _Name : public Event { \
- public: \
- using content_type = _ContentType; \
- DEFINE_EVENT_TYPEID(_TypeId, _Name) \
- explicit _Name(QJsonObject obj) : Event(typeId(), std::move(obj)) {} \
- explicit _Name(_ContentType content) \
- : Event(typeId(), matrixTypeId(), \
- QJsonObject { { QStringLiteral(#_ContentKey), \
- toJson(std::move(content)) } }) \
- {} \
- auto _ContentKey() const \
- { \
- return content<content_type>(#_ContentKey##_ls); \
- } \
- }; \
- REGISTER_EVENT_TYPE(_Name) \
- // End of macro
-
-DEFINE_SIMPLE_EVENT(TagEvent, "m.tag", TagsMap, tags)
-DEFINE_SIMPLE_EVENT(ReadMarkerEvent, "m.fully_read", QString, event_id)
-DEFINE_SIMPLE_EVENT(IgnoredUsersEvent, "m.ignored_user_list", QSet<QString>,
- ignored_users)
+DEFINE_SIMPLE_EVENT(TagEvent, Event, "m.tag", TagsMap, tags, "tags")
+DEFINE_SIMPLE_EVENT(ReadMarkerEventImpl, Event, "m.fully_read", QString,
+ eventId, "event_id")
+class ReadMarkerEvent : public ReadMarkerEventImpl {
+public:
+ using ReadMarkerEventImpl::ReadMarkerEventImpl;
+ [[deprecated("Use ReadMarkerEvent::eventId() instead")]]
+ auto event_id() const { return eventId(); }
+};
+DEFINE_SIMPLE_EVENT(IgnoredUsersEventImpl, Event, "m.ignored_user_list",
+ QSet<QString>, ignoredUsers, "ignored_users")
+class IgnoredUsersEvent : public IgnoredUsersEventImpl {
+public:
+ using IgnoredUsersEventImpl::IgnoredUsersEventImpl;
+ [[deprecated("Use IgnoredUsersEvent::ignoredUsers() instead")]]
+ auto ignored_users() const { return ignoredUsers(); }
+};
} // namespace Quotient
diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp
deleted file mode 100644
index d6622b30..00000000
--- a/lib/events/callanswerevent.cpp
+++ /dev/null
@@ -1,71 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#include "callanswerevent.h"
-
-#include "event.h"
-#include "logging.h"
-
-#include <QtCore/QJsonDocument>
-
-/*
-m.call.answer
-{
- "age": 242352,
- "content": {
- "answer": {
- "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]",
- "type": "answer"
- },
- "call_id": "12345",
- "lifetime": 60000,
- "version": 0
- },
- "event_id": "$WLGTSEFSEF:localhost",
- "origin_server_ts": 1431961217939,
- "room_id": "!Cuyf34gef24t:localhost",
- "sender": "@example:localhost",
- "type": "m.call.answer"
-}
-*/
-
-using namespace Quotient;
-
-CallAnswerEvent::CallAnswerEvent(const QJsonObject& obj)
- : CallEventBase(typeId(), obj)
-{
- qCDebug(EVENTS) << "Call Answer event";
-}
-
-CallAnswerEvent::CallAnswerEvent(const QString& callId, const int lifetime,
- const QString& sdp)
- : CallEventBase(
- typeId(), matrixTypeId(), callId, 0,
- { { QStringLiteral("lifetime"), lifetime },
- { QStringLiteral("answer"),
- QJsonObject { { QStringLiteral("type"), QStringLiteral("answer") },
- { QStringLiteral("sdp"), sdp } } } })
-{}
-
-CallAnswerEvent::CallAnswerEvent(const QString& callId, const QString& sdp)
- : CallEventBase(
- typeId(), matrixTypeId(), callId, 0,
- { { QStringLiteral("answer"),
- QJsonObject { { QStringLiteral("type"), QStringLiteral("answer") },
- { QStringLiteral("sdp"), sdp } } } })
-{}
diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h
deleted file mode 100644
index 2709882b..00000000
--- a/lib/events/callanswerevent.h
+++ /dev/null
@@ -1,45 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#pragma once
-
-#include "roomevent.h"
-
-namespace Quotient {
-class CallAnswerEvent : public CallEventBase {
-public:
- DEFINE_EVENT_TYPEID("m.call.answer", CallAnswerEvent)
-
- explicit CallAnswerEvent(const QJsonObject& obj);
-
- explicit CallAnswerEvent(const QString& callId, const int lifetime,
- const QString& sdp);
- explicit CallAnswerEvent(const QString& callId, const QString& sdp);
-
- int lifetime() const
- {
- return content<int>("lifetime"_ls);
- } // FIXME: Omittable<>?
- QString sdp() const
- {
- return contentJson()["answer"_ls].toObject().value("sdp"_ls).toString();
- }
-};
-
-REGISTER_EVENT_TYPE(CallAnswerEvent)
-} // namespace Quotient
diff --git a/lib/events/callcandidatesevent.cpp b/lib/events/callcandidatesevent.cpp
deleted file mode 100644
index 24f0dd46..00000000
--- a/lib/events/callcandidatesevent.cpp
+++ /dev/null
@@ -1,41 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#include "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
deleted file mode 100644
index e224f048..00000000
--- a/lib/events/callcandidatesevent.h
+++ /dev/null
@@ -1,45 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#pragma once
-
-#include "roomevent.h"
-
-namespace Quotient {
-class CallCandidatesEvent : public CallEventBase {
-public:
- DEFINE_EVENT_TYPEID("m.call.candidates", CallCandidatesEvent)
-
- explicit CallCandidatesEvent(const QJsonObject& obj)
- : CallEventBase(typeId(), obj)
- {}
-
- explicit CallCandidatesEvent(const QString& callId,
- const QJsonArray& candidates)
- : CallEventBase(typeId(), matrixTypeId(), callId, 0,
- { { QStringLiteral("candidates"), candidates } })
- {}
-
- QJsonArray candidates() const
- {
- return content<QJsonArray>("candidates"_ls);
- }
-};
-
-REGISTER_EVENT_TYPE(CallCandidatesEvent)
-} // namespace Quotient
diff --git a/lib/events/callevents.cpp b/lib/events/callevents.cpp
new file mode 100644
index 00000000..3873614d
--- /dev/null
+++ b/lib/events/callevents.cpp
@@ -0,0 +1,82 @@
+// SPDX-FileCopyrightText: 2022 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "callevents.h"
+
+#include "logging.h"
+
+using namespace Quotient;
+
+QJsonObject CallEvent::basicJson(const QString& matrixType,
+ const QString& callId, int version,
+ QJsonObject contentJson)
+{
+ contentJson.insert(QStringLiteral("call_id"), callId);
+ contentJson.insert(QStringLiteral("version"), version);
+ return RoomEvent::basicJson(matrixType, contentJson);
+}
+
+CallEvent::CallEvent(const QJsonObject& json)
+ : RoomEvent(json)
+{
+ if (callId().isEmpty())
+ qCWarning(EVENTS) << id() << "is a call event with an empty call id";
+}
+
+/*
+m.call.invite
+{
+ "age": 242352,
+ "content": {
+ "call_id": "12345",
+ "lifetime": 60000,
+ "offer": {
+ "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]",
+ "type": "offer"
+ },
+ "version": 0
+ },
+ "event_id": "$WLGTSEFSEF:localhost",
+ "origin_server_ts": 1431961217939,
+ "room_id": "!Cuyf34gef24t:localhost",
+ "sender": "@example:localhost",
+ "type": "m.call.invite"
+}
+*/
+
+CallInviteEvent::CallInviteEvent(const QString& callId, int lifetime,
+ const QString& sdp)
+ : EventTemplate(
+ callId,
+ { { QStringLiteral("lifetime"), lifetime },
+ { QStringLiteral("offer"),
+ QJsonObject{ { QStringLiteral("type"), QStringLiteral("offer") },
+ { QStringLiteral("sdp"), sdp } } } })
+{}
+
+/*
+m.call.answer
+{
+ "age": 242352,
+ "content": {
+ "answer": {
+ "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]",
+ "type": "answer"
+ },
+ "call_id": "12345",
+ "version": 0
+ },
+ "event_id": "$WLGTSEFSEF:localhost",
+ "origin_server_ts": 1431961217939,
+ "room_id": "!Cuyf34gef24t:localhost",
+ "sender": "@example:localhost",
+ "type": "m.call.answer"
+}
+*/
+
+CallAnswerEvent::CallAnswerEvent(const QString& callId, const QString& sdp)
+ : EventTemplate(callId, { { QStringLiteral("answer"),
+ QJsonObject { { QStringLiteral("type"),
+ QStringLiteral("answer") },
+ { QStringLiteral("sdp"), sdp } } } })
+{}
diff --git a/lib/events/callevents.h b/lib/events/callevents.h
new file mode 100644
index 00000000..752e331d
--- /dev/null
+++ b/lib/events/callevents.h
@@ -0,0 +1,99 @@
+// SPDX-FileCopyrightText: 2022 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "roomevent.h"
+
+namespace Quotient {
+
+class QUOTIENT_API CallEvent : public RoomEvent {
+public:
+ QUO_BASE_EVENT(CallEvent, "m.call.*"_ls, RoomEvent::BaseMetaType)
+ static bool matches(const QJsonObject&, const QString& mType)
+ {
+ return mType.startsWith("m.call.");
+ }
+
+ QUO_CONTENT_GETTER(QString, callId)
+ QUO_CONTENT_GETTER(int, version)
+
+protected:
+ explicit CallEvent(const QJsonObject& json);
+
+ static QJsonObject basicJson(const QString& matrixType,
+ const QString& callId, int version,
+ QJsonObject contentJson = {});
+};
+using CallEventBase
+ [[deprecated("CallEventBase is CallEvent now")]] = CallEvent;
+
+template <typename EventT>
+class EventTemplate<EventT, CallEvent> : public CallEvent {
+public:
+ using CallEvent::CallEvent;
+ explicit EventTemplate(const QString& callId,
+ const QJsonObject& contentJson = {})
+ : EventTemplate(basicJson(EventT::TypeId, callId, 0, contentJson))
+ {}
+};
+
+template <typename EventT, typename ContentT>
+class EventTemplate<EventT, CallEvent, ContentT>
+ : public EventTemplate<EventT, CallEvent> {
+public:
+ using EventTemplate<EventT, CallEvent>::EventTemplate;
+ template <typename... ContentParamTs>
+ explicit EventTemplate(const QString& callId,
+ ContentParamTs&&... contentParams)
+ : EventTemplate<EventT, CallEvent>(
+ callId,
+ toJson(ContentT{ std::forward<ContentParamTs>(contentParams)... }))
+ {}
+};
+
+class QUOTIENT_API CallInviteEvent
+ : public EventTemplate<CallInviteEvent, CallEvent> {
+public:
+ QUO_EVENT(CallInviteEvent, "m.call.invite")
+
+ using EventTemplate::EventTemplate;
+
+ explicit CallInviteEvent(const QString& callId, int lifetime,
+ const QString& sdp);
+
+ QUO_CONTENT_GETTER(int, lifetime)
+ QString sdp() const
+ {
+ return contentPart<QJsonObject>("offer"_ls).value("sdp"_ls).toString();
+ }
+};
+
+DEFINE_SIMPLE_EVENT(CallCandidatesEvent, CallEvent, "m.call.candidates",
+ QJsonArray, candidates, "candidates")
+
+class QUOTIENT_API CallAnswerEvent
+ : public EventTemplate<CallAnswerEvent, CallEvent> {
+public:
+ QUO_EVENT(CallAnswerEvent, "m.call.answer")
+
+ using EventTemplate::EventTemplate;
+
+ explicit CallAnswerEvent(const QString& callId, const QString& sdp);
+
+ QString sdp() const
+ {
+ return contentPart<QJsonObject>("answer"_ls).value("sdp"_ls).toString();
+ }
+};
+
+class QUOTIENT_API CallHangupEvent
+ : public EventTemplate<CallHangupEvent, CallEvent> {
+public:
+ QUO_EVENT(CallHangupEvent, "m.call.hangup")
+ using EventTemplate::EventTemplate;
+};
+
+} // namespace Quotient
+Q_DECLARE_METATYPE(Quotient::CallEvent*)
+Q_DECLARE_METATYPE(const Quotient::CallEvent*)
diff --git a/lib/events/callhangupevent.cpp b/lib/events/callhangupevent.cpp
deleted file mode 100644
index d41849c3..00000000
--- a/lib/events/callhangupevent.cpp
+++ /dev/null
@@ -1,52 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#include "callhangupevent.h"
-
-#include "event.h"
-#include "logging.h"
-
-#include <QtCore/QJsonDocument>
-
-/*
-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
deleted file mode 100644
index 5d73fb62..00000000
--- a/lib/events/callhangupevent.h
+++ /dev/null
@@ -1,33 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#pragma once
-
-#include "roomevent.h"
-
-namespace Quotient {
-class CallHangupEvent : public CallEventBase {
-public:
- DEFINE_EVENT_TYPEID("m.call.hangup", CallHangupEvent)
-
- explicit CallHangupEvent(const QJsonObject& obj);
- explicit CallHangupEvent(const QString& callId);
-};
-
-REGISTER_EVENT_TYPE(CallHangupEvent)
-} // namespace Quotient
diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp
deleted file mode 100644
index 54faac8d..00000000
--- a/lib/events/callinviteevent.cpp
+++ /dev/null
@@ -1,63 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#include "callinviteevent.h"
-
-#include "event.h"
-#include "logging.h"
-
-#include <QtCore/QJsonDocument>
-
-/*
-m.call.invite
-{
- "age": 242352,
- "content": {
- "call_id": "12345",
- "lifetime": 60000,
- "offer": {
- "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]",
- "type": "offer"
- },
- "version": 0
- },
- "event_id": "$WLGTSEFSEF:localhost",
- "origin_server_ts": 1431961217939,
- "room_id": "!Cuyf34gef24t:localhost",
- "sender": "@example:localhost",
- "type": "m.call.invite"
-}
-*/
-
-using namespace Quotient;
-
-CallInviteEvent::CallInviteEvent(const QJsonObject& obj)
- : CallEventBase(typeId(), obj)
-{
- qCDebug(EVENTS) << "Call Invite event";
-}
-
-CallInviteEvent::CallInviteEvent(const QString& callId, const int lifetime,
- const QString& sdp)
- : CallEventBase(
- typeId(), matrixTypeId(), callId, lifetime,
- { { QStringLiteral("lifetime"), lifetime },
- { QStringLiteral("offer"),
- QJsonObject { { QStringLiteral("type"), QStringLiteral("offer") },
- { QStringLiteral("sdp"), sdp } } } })
-{}
diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h
deleted file mode 100644
index b067a492..00000000
--- a/lib/events/callinviteevent.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#pragma once
-
-#include "roomevent.h"
-
-namespace Quotient {
-class CallInviteEvent : public CallEventBase {
-public:
- DEFINE_EVENT_TYPEID("m.call.invite", CallInviteEvent)
-
- explicit CallInviteEvent(const QJsonObject& obj);
-
- explicit CallInviteEvent(const QString& callId, const int lifetime,
- const QString& sdp);
-
- int lifetime() const
- {
- return content<int>("lifetime"_ls);
- } // FIXME: Omittable<>?
- QString sdp() const
- {
- return contentJson()["offer"_ls].toObject().value("sdp"_ls).toString();
- }
-};
-
-REGISTER_EVENT_TYPE(CallInviteEvent)
-} // namespace Quotient
diff --git a/lib/events/directchatevent.cpp b/lib/events/directchatevent.cpp
index b4027e16..83bb1e32 100644
--- a/lib/events/directchatevent.cpp
+++ b/lib/events/directchatevent.cpp
@@ -1,25 +1,8 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "directchatevent.h"
-#include <QtCore/QJsonArray>
-
using namespace Quotient;
QMultiHash<QString, QString> DirectChatEvent::usersToDirectChats() const
diff --git a/lib/events/directchatevent.h b/lib/events/directchatevent.h
index bb091c5c..0756d816 100644
--- a/lib/events/directchatevent.h
+++ b/lib/events/directchatevent.h
@@ -1,33 +1,17 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "event.h"
namespace Quotient {
-class DirectChatEvent : public Event {
+class QUOTIENT_API DirectChatEvent : public Event {
public:
- DEFINE_EVENT_TYPEID("m.direct", DirectChatEvent)
+ QUO_EVENT(DirectChatEvent, "m.direct")
- explicit DirectChatEvent(const QJsonObject& obj) : Event(typeId(), obj) {}
+ using Event::Event;
QMultiHash<QString, QString> usersToDirectChats() const;
};
-REGISTER_EVENT_TYPE(DirectChatEvent)
} // namespace Quotient
diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp
index dccfa540..540594d1 100644
--- a/lib/events/encryptedevent.cpp
+++ b/lib/events/encryptedevent.cpp
@@ -1,32 +1,69 @@
-#include "encryptedevent.h"
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-License-Identifier: LGPL-2.1-or-later
-#include "room.h"
+#include "encryptedevent.h"
+#include "e2ee/e2ee.h"
+#include "logging.h"
using namespace Quotient;
-using namespace QtOlm;
-EncryptedEvent::EncryptedEvent(const QJsonObject& ciphertext,
+EncryptedEvent::EncryptedEvent(const QJsonObject& ciphertexts,
const QString& senderKey)
- : RoomEvent(typeId(), matrixTypeId(),
- { { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey },
- { CiphertextKeyL, ciphertext },
- { SenderKeyKeyL, senderKey } })
+ : RoomEvent(basicJson(TypeId, { { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey },
+ { CiphertextKeyL, ciphertexts },
+ { SenderKeyKeyL, senderKey } }))
{}
-EncryptedEvent::EncryptedEvent(QByteArray ciphertext, const QString& senderKey,
+EncryptedEvent::EncryptedEvent(const QByteArray& ciphertext,
+ const QString& senderKey,
const QString& deviceId, const QString& sessionId)
- : RoomEvent(typeId(), matrixTypeId(),
- {
- { AlgorithmKeyL, MegolmV1AesSha2AlgoKey },
- { CiphertextKeyL, QString(ciphertext) },
- { DeviceIdKeyL, deviceId },
- { SenderKeyKeyL, senderKey },
- { SessionIdKeyL, sessionId },
- })
+ : RoomEvent(basicJson(TypeId, { { AlgorithmKeyL, MegolmV1AesSha2AlgoKey },
+ { CiphertextKeyL, QString(ciphertext) },
+ { DeviceIdKeyL, deviceId },
+ { SenderKeyKeyL, senderKey },
+ { SessionIdKeyL, sessionId } }))
{}
EncryptedEvent::EncryptedEvent(const QJsonObject& obj)
- : RoomEvent(typeId(), obj)
+ : RoomEvent(obj)
{
qCDebug(E2EE) << "Encrypted event from" << senderId();
}
+
+QString EncryptedEvent::algorithm() const
+{
+ const auto algo = contentPart<QString>(AlgorithmKeyL);
+ if (!isSupportedAlgorithm(algo))
+ qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo
+ << "is not supported";
+
+ return algo;
+}
+
+RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const
+{
+ auto eventObject = QJsonDocument::fromJson(decrypted.toUtf8()).object();
+ eventObject["event_id"] = id();
+ eventObject["sender"] = senderId();
+ eventObject["origin_server_ts"] = originTimestamp().toMSecsSinceEpoch();
+ if (const auto relatesToJson = contentPart<QJsonObject>("m.relates_to"_ls);
+ !relatesToJson.isEmpty()) {
+ auto content = eventObject["content"].toObject();
+ content["m.relates_to"] = relatesToJson;
+ eventObject["content"] = content;
+ }
+ if (const auto redactsJson = unsignedPart<QString>("redacts"_ls);
+ !redactsJson.isEmpty()) {
+ auto unsign = eventObject["unsigned"].toObject();
+ unsign["redacts"] = redactsJson;
+ eventObject["unsigned"] = unsign;
+ }
+ return loadEvent<RoomEvent>(eventObject);
+}
+
+void EncryptedEvent::setRelation(const QJsonObject& relation)
+{
+ auto content = contentJson();
+ content["m.relates_to"] = relation;
+ editJson()["content"] = content;
+}
diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h
index 235b2aa4..e24e5745 100644
--- a/lib/events/encryptedevent.h
+++ b/lib/events/encryptedevent.h
@@ -1,10 +1,17 @@
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#pragma once
-#include "e2ee.h"
#include "roomevent.h"
namespace Quotient {
-class Room;
+
+constexpr auto CiphertextKeyL = "ciphertext"_ls;
+constexpr auto SenderKeyKeyL = "sender_key"_ls;
+constexpr auto DeviceIdKeyL = "device_id"_ls;
+constexpr auto SessionIdKeyL = "session_id"_ls;
+
/*
* While the specification states:
*
@@ -23,44 +30,39 @@ class Room;
* in general. It's possible, because RoomEvent interface is similar to Event's
* one and doesn't add new restrictions, just provides additional features.
*/
-class EncryptedEvent : public RoomEvent {
- Q_GADGET
+class QUOTIENT_API EncryptedEvent : public RoomEvent {
public:
- DEFINE_EVENT_TYPEID("m.room.encrypted", EncryptedEvent)
+ QUO_EVENT(EncryptedEvent, "m.room.encrypted")
/* In case with Olm, the encrypted content of the event is
* a map from the recipient Curve25519 identity key to ciphertext
* information */
- explicit EncryptedEvent(const QJsonObject& ciphertext,
+ explicit EncryptedEvent(const QJsonObject& ciphertexts,
const QString& senderKey);
/* In case with Megolm, device_id and session_id are required */
- explicit EncryptedEvent(QByteArray ciphertext, const QString& senderKey,
- const QString& deviceId, const QString& sessionId);
+ explicit EncryptedEvent(const QByteArray& ciphertext,
+ const QString& senderKey, const QString& deviceId,
+ const QString& sessionId);
explicit EncryptedEvent(const QJsonObject& obj);
- QString algorithm() const
- {
- QString algo = content<QString>(AlgorithmKeyL);
- if (!SupportedAlgorithms.contains(algo)) {
- qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo
- << "is not supported";
- }
- return algo;
- }
+ QString algorithm() const;
QByteArray ciphertext() const
{
- return content<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); }
-};
-REGISTER_EVENT_TYPE(EncryptedEvent)
+ QString deviceId() const { return contentPart<QString>(DeviceIdKeyL); }
+ QString sessionId() const { return contentPart<QString>(SessionIdKeyL); }
+ RoomEventPtr createDecrypted(const QString &decrypted) const;
+ void setRelation(const QJsonObject& relation);
+};
} // namespace Quotient
diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp
index f1bde621..b1b04984 100644
--- a/lib/events/encryptionevent.cpp
+++ b/lib/events/encryptionevent.cpp
@@ -1,45 +1,53 @@
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#include "encryptionevent.h"
+#include "logging.h"
-#include "e2ee.h"
+#include "e2ee/e2ee.h"
-#include <array>
+using namespace Quotient;
-namespace Quotient {
-static const std::array<QString, 1> encryptionStrings = {
- { MegolmV1AesSha2AlgoKey }
-};
+static constexpr std::array encryptionStrings { MegolmV1AesSha2AlgoKey };
template <>
-struct JsonConverter<EncryptionType> {
- static EncryptionType load(const QJsonValue& jv)
- {
- const auto& encryptionString = jv.toString();
- for (auto it = encryptionStrings.begin(); it != encryptionStrings.end();
- ++it)
- if (encryptionString == *it)
- return EncryptionType(it - encryptionStrings.begin());
-
- if (!encryptionString.isEmpty())
- qCWarning(EVENTS) << "Unknown EncryptionType: " << encryptionString;
- return EncryptionType::Undefined;
- }
-};
-} // namespace Quotient
-
-using namespace Quotient;
+EncryptionType Quotient::fromJson(const QJsonValue& jv)
+{
+ const auto& encryptionString = jv.toString();
+ for (auto it = encryptionStrings.begin(); it != encryptionStrings.end();
+ ++it)
+ if (encryptionString == *it)
+ return EncryptionType(it - encryptionStrings.begin());
+
+ if (!encryptionString.isEmpty())
+ qCWarning(EVENTS) << "Unknown EncryptionType: " << encryptionString;
+ return EncryptionType::Undefined;
+}
EncryptionEventContent::EncryptionEventContent(const QJsonObject& json)
- : encryption(fromJson<EncryptionType>(json[AlgorithmKeyL]))
+ : encryption(fromJson<Quotient::EncryptionType>(json[AlgorithmKeyL]))
, algorithm(sanitized(json[AlgorithmKeyL].toString()))
- , rotationPeriodMs(json[RotationPeriodMsKeyL].toInt(604800000))
- , rotationPeriodMsgs(json[RotationPeriodMsgsKeyL].toInt(100))
-{}
+{
+ // NB: fillFromJson only fills the variable if the JSON key exists
+ fillFromJson<int>(json[RotationPeriodMsKeyL], rotationPeriodMs);
+ fillFromJson<int>(json[RotationPeriodMsgsKeyL], rotationPeriodMsgs);
+}
+
+EncryptionEventContent::EncryptionEventContent(Quotient::EncryptionType et)
+ : encryption(et)
+{
+ if(encryption != Quotient::EncryptionType::Undefined) {
+ algorithm = encryptionStrings[static_cast<size_t>(encryption)];
+ }
+}
-void EncryptionEventContent::fillJson(QJsonObject* o) const
+QJsonObject EncryptionEventContent::toJson() const
{
- Q_ASSERT(o);
- if (encryption != EncryptionType::Undefined)
- o->insert(AlgorithmKey, algorithm);
- o->insert(RotationPeriodMsKey, rotationPeriodMs);
- o->insert(RotationPeriodMsgsKey, rotationPeriodMsgs);
+ QJsonObject o;
+ if (encryption != Quotient::EncryptionType::Undefined)
+ o.insert(AlgorithmKey, algorithm);
+ o.insert(RotationPeriodMsKey, rotationPeriodMs);
+ o.insert(RotationPeriodMsgsKey, rotationPeriodMsgs);
+ return o;
}
diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h
index cbd3ba4a..4bf7459c 100644
--- a/lib/events/encryptionevent.h
+++ b/lib/events/encryptionevent.h
@@ -1,73 +1,47 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
-#include "eventcontent.h"
+#include "quotient_common.h"
#include "stateevent.h"
namespace Quotient {
-class EncryptionEventContent : public EventContent::Base {
+class QUOTIENT_API EncryptionEventContent {
public:
- enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined };
+ using EncryptionType
+ [[deprecated("Use Quotient::EncryptionType instead")]] =
+ Quotient::EncryptionType;
- explicit EncryptionEventContent(EncryptionType et = Undefined)
- : encryption(et)
- {}
+ // NOLINTNEXTLINE(google-explicit-constructor)
+ QUO_IMPLICIT EncryptionEventContent(Quotient::EncryptionType et);
explicit EncryptionEventContent(const QJsonObject& json);
- EncryptionType encryption;
- QString algorithm;
- int rotationPeriodMs;
- int rotationPeriodMsgs;
+ QJsonObject toJson() const;
-protected:
- void fillJson(QJsonObject* o) const override;
+ Quotient::EncryptionType encryption;
+ QString algorithm {};
+ int rotationPeriodMs = 604'800'000;
+ int rotationPeriodMsgs = 100;
};
-using EncryptionType = EncryptionEventContent::EncryptionType;
-
-class EncryptionEvent : public StateEvent<EncryptionEventContent> {
- Q_GADGET
+class QUOTIENT_API EncryptionEvent
+ : public KeylessStateEventBase<EncryptionEvent, EncryptionEventContent> {
public:
- DEFINE_EVENT_TYPEID("m.room.encryption", EncryptionEvent)
-
- using EncryptionType = EncryptionEventContent::EncryptionType;
+ QUO_EVENT(EncryptionEvent, "m.room.encryption")
- explicit EncryptionEvent(const QJsonObject& obj = {}) // TODO: apropriate
- // default value
- : StateEvent(typeId(), obj)
- {}
- template <typename... ArgTs>
- EncryptionEvent(ArgTs&&... contentArgs)
- : StateEvent(typeId(), matrixTypeId(), QString(),
- std::forward<ArgTs>(contentArgs)...)
- {}
+ using EncryptionType
+ [[deprecated("Use Quotient::EncryptionType instead")]] =
+ Quotient::EncryptionType;
- EncryptionType encryption() const { return content().encryption; }
+ using KeylessStateEventBase::KeylessStateEventBase;
+ Quotient::EncryptionType encryption() const { return content().encryption; }
QString algorithm() const { return content().algorithm; }
int rotationPeriodMs() const { return content().rotationPeriodMs; }
int rotationPeriodMsgs() const { return content().rotationPeriodMsgs; }
-private:
- Q_ENUM(EncryptionType)
+ bool useEncryption() const { return !algorithm().isEmpty(); }
};
-
-REGISTER_EVENT_TYPE(EncryptionEvent)
} // namespace Quotient
diff --git a/lib/events/event.cpp b/lib/events/event.cpp
index 7b34114d..da7de919 100644
--- a/lib/events/event.cpp
+++ b/lib/events/event.cpp
@@ -1,48 +1,54 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "event.h"
+#include "callevents.h"
#include "logging.h"
+#include "stateevent.h"
#include <QtCore/QJsonDocument>
using namespace Quotient;
-event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId)
-{
- const auto id = get().eventTypes.size();
- get().eventTypes.push_back(matrixTypeId);
- if (strncmp(matrixTypeId, "", 1) == 0)
- qDebug(EVENTS) << "Initialized unknown event type with id" << id;
- else
- qDebug(EVENTS) << "Initialized event type" << matrixTypeId << "with id"
- << id;
- return id;
-}
+QString EventTypeRegistry::getMatrixType(event_type_t typeId) { return typeId; }
-QString EventTypeRegistry::getMatrixType(event_type_t typeId)
+void AbstractEventMetaType::addDerived(AbstractEventMetaType* newType)
{
- return typeId < get().eventTypes.size() ? get().eventTypes[typeId]
- : QString();
+ if (const auto existing =
+ std::find_if(derivedTypes.cbegin(), derivedTypes.cend(),
+ [&newType](const AbstractEventMetaType* t) {
+ return t->matrixId == newType->matrixId;
+ });
+ existing != derivedTypes.cend())
+ {
+ if (*existing == newType)
+ return;
+ // Two different metatype objects claim the same Matrix type id; this
+ // is not normal, so give as much information as possible to diagnose
+ if ((*existing)->className == newType->className) {
+ qCritical(EVENTS)
+ << newType->className << "claims" << newType->matrixId
+ << "repeatedly; check that it's exported across translation "
+ "units or shared objects";
+ Q_ASSERT(false); // That situation is plain wrong
+ return; // So maybe std::terminate() even?
+ }
+ qWarning(EVENTS).nospace()
+ << newType->matrixId << " is already mapped to "
+ << (*existing)->className << " before " << newType->className
+ << "; unless the two have different isValid() conditions, the "
+ "latter class will never be used";
+ }
+ derivedTypes.emplace_back(newType);
+ qDebug(EVENTS).nospace()
+ << newType->matrixId << " -> " << newType->className << "; "
+ << derivedTypes.size() << " derived type(s) registered for "
+ << className;
}
-Event::Event(Type type, const QJsonObject& json) : _type(type), _json(json)
+Event::Event(const QJsonObject& json)
+ : _json(json)
{
if (!json.contains(ContentKeyL)
&& !json.value(UnsignedKeyL).toObject().contains(RedactedCauseKeyL)) {
@@ -51,29 +57,26 @@ Event::Event(Type type, const QJsonObject& json) : _type(type), _json(json)
}
}
-Event::Event(Type type, event_mtype_t matrixType, const QJsonObject& contentJson)
- : Event(type, basicEventJson(matrixType, contentJson))
-{}
-
Event::~Event() = default;
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();
}
+bool Event::isStateEvent() const { return is<StateEvent>(); }
+
+bool Event::isCallEvent() const { return is<CallEvent>(); }
+
void Event::dumpTo(QDebug dbg) const
{
dbg << QJsonDocument(contentJson()).toJson(QJsonDocument::Compact);
diff --git a/lib/events/event.h b/lib/events/event.h
index 6c8961ad..0abef1f0 100644
--- a/lib/events/event.h
+++ b/lib/events/event.h
@@ -1,32 +1,14 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "converters.h"
-#include "logging.h"
-
-#ifdef ENABLE_EVENTTYPE_ALIAS
-# define USE_EVENTTYPE_ALIAS 1
-#endif
+#include "function_traits.h"
+#include "single_key_value.h"
namespace Quotient {
-// === event_ptr_tt<> and type casting facilities ===
+// === event_ptr_tt<> and basic type casting facilities ===
template <typename EventT>
using event_ptr_tt = std::unique_ptr<EventT>;
@@ -45,198 +27,288 @@ inline TargetEventT* weakPtrCast(const event_ptr_tt<EventT>& ptr)
return static_cast<TargetEventT*>(rawPtr(ptr));
}
-/// Re-wrap a smart pointer to base into a smart pointer to derived
-template <typename TargetT, typename SourceT>
-[[deprecated("Consider using eventCast() or visit() instead")]]
-inline event_ptr_tt<TargetT> ptrCast(event_ptr_tt<SourceT>&& ptr)
-{
- return std::unique_ptr<TargetT>(static_cast<TargetT*>(ptr.release()));
-}
-
// === 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 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 UnsignedKeyL = "unsigned"_ls;
-static const auto RedactedCauseKeyL = "redacted_because"_ls;
-static const auto PrevContentKeyL = "prev_content"_ls;
-static const auto StateKeyKeyL = "state_key"_ls;
-
-/// Make a minimal correct Matrix event JSON
-template <typename StrT>
-inline QJsonObject basicEventJson(StrT matrixType, const QJsonObject& content)
-{
- return { { TypeKey, std::forward<StrT>(matrixType) },
- { ContentKey, content } };
-}
-
-// === Event types and event types registry ===
-
-using event_type_t = size_t;
-using event_mtype_t = const char*;
+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 };
+
+using event_type_t = QLatin1String;
+
+// TODO: Remove in 0.8
+struct QUOTIENT_API EventTypeRegistry {
+ [[deprecated("event_type_t is a string since libQuotient 0.7, use it directly instead")]]
+ static QString getMatrixType(event_type_t typeId);
-class EventTypeRegistry {
-public:
+ EventTypeRegistry() = delete;
~EventTypeRegistry() = default;
+ Q_DISABLE_COPY_MOVE(EventTypeRegistry)
+};
- static event_type_t initializeTypeId(event_mtype_t matrixTypeId);
+// === EventMetaType ===
- template <typename EventT>
- static inline event_type_t initializeTypeId()
- {
- return initializeTypeId(EventT::matrixTypeId());
- }
+class Event;
- static QString getMatrixType(event_type_t typeId);
+// TODO: move over to std::derived_from<Event> once it's available everywhere
+template <typename EventT, typename BaseEventT = Event>
+concept EventClass = std::is_base_of_v<BaseEventT, EventT>;
-private:
- EventTypeRegistry() = default;
- Q_DISABLE_COPY(EventTypeRegistry)
- DISABLE_MOVE(EventTypeRegistry)
+template <EventClass EventT>
+bool is(const Event& e);
- static EventTypeRegistry& get()
+//! \brief The base class for event metatypes
+//!
+//! You should not normally have to use this directly, unless you need to devise
+//! a whole new kind of event metatypes.
+class QUOTIENT_API AbstractEventMetaType {
+public:
+ // The public fields here are const and are not to be changeable anyway.
+ // NOLINTBEGIN(misc-non-private-member-variables-in-classes)
+ const char* const className;
+ const event_type_t matrixId;
+ const AbstractEventMetaType* const baseType = nullptr;
+ // NOLINTEND(misc-non-private-member-variables-in-classes)
+
+ explicit AbstractEventMetaType(const char* className)
+ : className(className)
+ {}
+ explicit AbstractEventMetaType(const char* className, event_type_t matrixId,
+ AbstractEventMetaType& nearestBase)
+ : className(className), matrixId(matrixId), baseType(&nearestBase)
{
- static EventTypeRegistry etr;
- return etr;
+ nearestBase.addDerived(this);
}
- std::vector<event_mtype_t> eventTypes;
-};
-
-template <>
-inline event_type_t EventTypeRegistry::initializeTypeId<void>()
-{
- return initializeTypeId("");
-}
+ void addDerived(AbstractEventMetaType *newType);
-template <typename EventT>
-struct EventTypeTraits {
- static event_type_t id()
- {
- static const auto id = EventTypeRegistry::initializeTypeId<EventT>();
- return id;
- }
-};
+ virtual ~AbstractEventMetaType() = default;
-template <typename EventT>
-inline event_type_t typeId()
-{
- return EventTypeTraits<std::decay_t<EventT>>::id();
-}
+protected:
+ // Allow template specialisations to call into one another
+ template <class EventT>
+ friend class EventMetaType;
-inline event_type_t unknownEventTypeId() { return typeId<void>(); }
+ // The returned value indicates whether a generic object has to be created
+ // on the top level when `event` is empty, instead of returning nullptr
+ virtual bool doLoadFrom(const QJsonObject& fullJson, const QString& type,
+ Event*& event) const = 0;
-// === EventFactory ===
+private:
+ std::vector<const AbstractEventMetaType*> derivedTypes{};
+ Q_DISABLE_COPY_MOVE(AbstractEventMetaType)
+};
-/** Create an event of arbitrary type from its arguments */
-template <typename EventT, typename... ArgTs>
-inline event_ptr_tt<EventT> makeEvent(ArgTs&&... args)
+// Any event metatype is unique (note Q_DISABLE_COPY_MOVE above) so can be
+// identified by its address
+inline bool operator==(const AbstractEventMetaType& lhs,
+ const AbstractEventMetaType& rhs)
{
- return std::make_unique<EventT>(std::forward<ArgTs>(args)...);
+ return &lhs == &rhs;
}
-template <typename BaseEventT>
-class EventFactory {
+//! \brief A family of event meta-types to load and match events
+//!
+//! TL;DR for the loadFrom() story:
+//! - for base event types, use QUO_BASE_EVENT and, if you have additional
+//! validation (e.g., JSON has to contain a certain key - see StateEvent
+//! for a real example), define it in the static EventT::isValid() member
+//! function accepting QJsonObject and returning bool.
+//! - for leaf (specific) event types - simply use QUO_EVENT and it will do
+//! everything necessary, including the TypeId definition.
+//! \sa QUO_EVENT, QUO_BASE_EVENT
+template <class EventT>
+class QUOTIENT_API EventMetaType : public AbstractEventMetaType {
+ // Above: can't constrain EventT to be EventClass because it's incomplete
+ // at the point of EventMetaType<EventT> instantiation.
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()
+ using AbstractEventMetaType::AbstractEventMetaType;
+
+ //! \brief Try to load an event from JSON, with dynamic type resolution
+ //!
+ //! The generic logic defined in this class template and invoked applies to
+ //! all event types defined in the library and boils down to the following:
+ //! 1.
+ //! a. If EventT has TypeId defined (which normally is a case of
+ //! all leaf - specific - event types, via QUO_EVENT macro) and
+ //! \p type doesn't exactly match it, nullptr is immediately returned.
+ //! b. In absence of TypeId, an event type is assumed to be a base;
+ //! its derivedTypes are examined, and this algorithm is applied
+ //! recursively on each.
+ //! 2. Optional validation: if EventT (or, due to the way inheritance works,
+ //! any of its base event types) has a static isValid() predicate and
+ //! the event JSON does not satisfy it, nullptr is immediately returned
+ //! to the upper level or to the loadFrom() caller. This is how existence
+ //! of `state_key` is checked in any type derived from StateEvent.
+ //! 3. If step 1b above returned non-nullptr, immediately return it.
+ //! 4.
+ //! a. If EventT::isValid() or EventT::TypeId (either, or both) exist and
+ //! are satisfied (see steps 1a and 2 above), an object of this type
+ //! is created from the passed JSON and returned. In case of a base
+ //! event type, this will be a generic (aka "unknown") event.
+ //! b. If neither exists, a generic event is only created and returned
+ //! when on the top level (i.e., outside of recursion into
+ //! derivedTypes); lower levels return nullptr instead and the type
+ //! lookup continues. The latter is a case of a derived base event
+ //! metatype (e.g. RoomEvent) called from its base event metatype
+ //! (i.e., Event). If no matching type derived from RoomEvent is found,
+ //! the nested lookup returns nullptr rather than a generic RoomEvent,
+ //! so that other types derived from Event could be examined.
+ event_ptr_tt<EventT> loadFrom(const QJsonObject& fullJson,
+ const QString& type) const
{
- 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;
+ Event* event = nullptr;
+ const bool goodEnough = doLoadFrom(fullJson, type, event);
+ if (!event && goodEnough)
+ return event_ptr_tt<EventT>{ new EventT(fullJson) };
+ return event_ptr_tt<EventT>{ static_cast<EventT*>(event) };
}
private:
- static auto& factories()
+ bool doLoadFrom(const QJsonObject& fullJson, const QString& type,
+ Event*& event) const override
{
- using inner_factory_tt = std::function<event_ptr_tt<BaseEventT>(
- const QJsonObject&, const QString&)>;
- static std::vector<inner_factory_tt> _factories {};
- return _factories;
+ if constexpr (requires { EventT::TypeId; }) {
+ if (EventT::TypeId != type)
+ return false;
+ } else {
+ for (const auto& p : derivedTypes) {
+ p->doLoadFrom(fullJson, type, event);
+ if (event) {
+ Q_ASSERT(is<EventT>(*event));
+ return false;
+ }
+ }
+ }
+ if constexpr (requires { EventT::isValid; }) {
+ if (!EventT::isValid(fullJson))
+ return false;
+ } else if constexpr (!requires { EventT::TypeId; })
+ return true; // Create a generic event object if on the top level
+ event = new EventT(fullJson);
+ return false;
}
};
-/** 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()
+// === Event creation facilities ===
+
+//! \brief Create an event of arbitrary type from its arguments
+//!
+//! This should not be used to load events from JSON - use loadEvent() for that.
+//! \sa loadEvent
+template <EventClass EventT, typename... ArgTs>
+inline event_ptr_tt<EventT> makeEvent(ArgTs&&... args)
{
- 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;
- });
+ return std::make_unique<EventT>(std::forward<ArgTs>(args)...);
}
-template <typename EventT>
-inline auto registerEventType()
+template <EventClass EventT>
+constexpr const auto& mostSpecificMetaType()
{
- // 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
+ if constexpr (requires { EventT::MetaType; })
+ return EventT::MetaType;
+ else
+ return EventT::BaseMetaType;
}
+//! \brief Create an event with proper type from a JSON object
+//!
+//! Use this factory template to detect the type from the JSON object
+//! contents (the detected event type should derive from the template
+//! parameter type) and create an event object of that type.
+template <EventClass EventT>
+inline event_ptr_tt<EventT> loadEvent(const QJsonObject& fullJson)
+{
+ return mostSpecificMetaType<EventT>().loadFrom(
+ fullJson, fullJson[TypeKeyL].toString());
+}
+
+//! \brief Create an event from a type string and content JSON
+//!
+//! Use this template to resolve the C++ type from the Matrix type string in
+//! \p matrixType and create an event of that type by passing all parameters
+//! to BaseEventT::basicJson().
+template <EventClass EventT>
+inline event_ptr_tt<EventT> loadEvent(const QString& matrixType,
+ const auto&... otherBasicJsonParams)
+{
+ return mostSpecificMetaType<EventT>().loadFrom(
+ EventT::basicJson(matrixType, otherBasicJsonParams...), matrixType);
+}
+
+template <EventClass EventT>
+struct JsonConverter<event_ptr_tt<EventT>>
+ : JsonObjectUnpacker<event_ptr_tt<EventT>> {
+ // No dump() to avoid any ambiguity on whether a given export to JSON uses
+ // fullJson() or only contentJson()
+ using JsonObjectUnpacker<event_ptr_tt<EventT>>::load;
+ static auto load(const QJsonObject& jo)
+ {
+ return loadEvent<EventT>(jo);
+ }
+};
+
// === Event ===
-class Event {
- Q_GADGET
- Q_PROPERTY(Type type READ type CONSTANT)
- Q_PROPERTY(QJsonObject contentJson READ contentJson CONSTANT)
+class QUOTIENT_API Event {
public:
using Type = event_type_t;
- using factory_t = EventFactory<Event>;
+ static inline EventMetaType<Event> BaseMetaType { "Event" };
+ virtual const AbstractEventMetaType& metaType() const
+ {
+ return BaseMetaType;
+ }
- explicit Event(Type type, const QJsonObject& json);
- explicit Event(Type type, event_mtype_t matrixType,
- const QJsonObject& contentJson = {});
Q_DISABLE_COPY(Event)
- Event(Event&&) = default;
+ Event(Event&&) noexcept = default;
Event& operator=(Event&&) = delete;
virtual ~Event();
- Type type() const { return _type; }
+ /// Make a minimal correct Matrix event JSON
+ static QJsonObject basicJson(const QString& matrixType,
+ const QJsonObject& content)
+ {
+ return { { TypeKey, matrixType }, { ContentKey, content } };
+ }
+
+ //! \brief Event Matrix type, as identified by its metatype object
+ //!
+ //! For generic/unknown events it will contain a descriptive/generic string
+ //! defined by the respective base event type (that can be empty).
+ //! \sa matrixType
+ Type type() const { return metaType().matrixId; }
+
+ //! \brief Exact Matrix type stored in JSON
+ //!
+ //! Coincides with the result of type() (but is slower) for events defined
+ //! in C++ (not necessarily in the library); for generic/unknown events
+ //! the returned value will be different.
QString matrixType() const;
+
+ template <EventClass EventT>
+ bool is() const
+ {
+ return Quotient::is<EventT>(*this);
+ }
+
+ [[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; }
@@ -245,148 +317,320 @@ 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;
+
+ //! \brief Get a part of the content object, assuming a given type
+ //!
+ //! This retrieves the value under `content.<key>` from the event JSON and
+ //! then converts it to \p T using fromJson().
+ //! \sa contentJson, fromJson
+ template <typename T, 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;
+
+ //! \brief Get a part of the unsigned object, assuming a given type
+ //!
+ //! This retrieves the value under `unsigned.<key>` from the event JSON and
+ //! then converts it to \p T using fromJson().
+ //! \sa unsignedJson, fromJson
+ template <typename T, 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)
+ friend QUOTIENT_API QDebug operator<<(QDebug dbg, const Event& e)
{
QDebugStateSaver _dss { dbg };
- dbg.noquote().nospace() << e.matrixType() << '(' << e.type() << "): ";
+ dbg.noquote().nospace()
+ << e.matrixType() << '(' << e.metaType().className << "): ";
e.dumpTo(dbg);
return dbg;
}
- virtual bool isStateEvent() const { return false; }
- virtual bool isCallEvent() const { return false; }
- virtual void dumpTo(QDebug dbg) const;
+ // State events are quite special in Matrix; so isStateEvent() is here,
+ // as an exception. For other base events, Event::is<>() and
+ // Quotient::is<>() should be used; don't add is* methods here
+ bool isStateEvent() const;
+ [[deprecated("Use is<CallEvent>() instead")]] bool isCallEvent() const;
protected:
+ friend class EventMetaType<Event>; // To access the below constructor
+
+ explicit Event(const QJsonObject& json);
+
QJsonObject& editJson() { return _json; }
+ virtual void dumpTo(QDebug dbg) const;
private:
- Type _type;
QJsonObject _json;
};
using EventPtr = event_ptr_tt<Event>;
-template <typename EventT>
+template <EventClass EventT>
using EventsArray = std::vector<event_ptr_tt<EventT>>;
using Events = EventsArray<Event>;
-// === Macros used with event class definitions ===
+// === Facilities for event class definitions ===
+
+//! \brief A template base class to derive your event type from
+//!
+//! This simple class template generates commonly used event constructor
+//! signatures and the content() method with the appropriate return type.
+//! The generic version here is only used with non-trivial \p ContentT (if you
+//! don't need to create an event from its content structure, just go and derive
+//! straight from the respective \p EventBaseT instead of using EventTemplate);
+//! specialisations may override that and provide useful semantics even without
+//! \p ContentT (see EventTemplate<CallEvent>, e.g.).
+//!
+//! The template uses CRTP to pick the event type id from the actual class;
+//! it will fail to compile if \p EventT doesn't provide TypeId. It also uses
+//! the base event type's basicJson(); if you need extra keys to be inserted
+//! you may want to bypass this template as writing the code to that effect in
+//! your class will likely be clearer and more concise.
+//! \sa https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
+//! \sa DEFINE_SIMPLE_EVENT
+template <typename EventT, EventClass BaseEventT, typename ContentT = void>
+class EventTemplate : public BaseEventT {
+ // Above: can't constrain EventT to be EventClass because it's incomplete
+ // by CRTP definition.
+public:
+ static_assert(
+ !std::is_same_v<ContentT, void>,
+ "If you see this, you tried to use EventTemplate with the default"
+ " ContentT type, which is void. This default is only used with explicit"
+ " specialisations (see CallEvent, e.g.). Otherwise, if you don't intend"
+ " to use the content part of EventTemplate then you don't need"
+ " EventTemplate; just use the base event class directly");
+ using content_type = ContentT;
+
+ explicit EventTemplate(const QJsonObject& json)
+ : BaseEventT(json)
+ {}
+ explicit EventTemplate(const ContentT& c)
+ : BaseEventT(EventT::basicJson(EventT::TypeId, toJson(c)))
+ {}
+
+ ContentT content() const { return fromJson<ContentT>(this->contentJson()); }
+};
-// This macro should be used in a public section of an event class to
-// provide matrixTypeId() and typeId().
-#define DEFINE_EVENT_TYPEID(_Id, _Type) \
- static constexpr event_mtype_t matrixTypeId() { return _Id; } \
- static auto typeId() { return Quotient::typeId<_Type>(); } \
+//! \brief Supply event metatype information in base event types
+//!
+//! Use this macro in a public section of your base event class to provide
+//! type identity and enable dynamic loading of generic events of that type.
+//! Do _not_ add this macro if your class is an intermediate wrapper and is not
+//! supposed to be instantiated on its own. Provides BaseMetaType static field
+//! initialised by parameters passed to the macro, and a metaType() override
+//! pointing to that BaseMetaType.
+//! \sa EventMetaType, EventMetaType::SuppressLoadDerived
+#define QUO_BASE_EVENT(CppType_, ...) \
+ friend class EventMetaType<CppType_>; \
+ static inline EventMetaType<CppType_> BaseMetaType{ \
+ #CppType_ __VA_OPT__(,) __VA_ARGS__ }; \
+ const AbstractEventMetaType& metaType() const override \
+ { \
+ return BaseMetaType; \
+ } \
// End of macro
-// This macro should be put after an event class definition (in .h or .cpp)
-// to enable its deserialisation from a /sync and other
-// polymorphic event arrays
-#define REGISTER_EVENT_TYPE(_Type) \
- namespace { \
- [[maybe_unused]] static const auto _factoryAdded##_Type = \
- registerEventType<_Type>(); \
- } \
+//! Supply event metatype information in (specific) event types
+//!
+//! Use this macro in a public section of your event class to provide type
+//! identity and enable dynamic loading of generic events of that type.
+//! Do _not_ use this macro if your class is an intermediate wrapper and is not
+//! supposed to be instantiated on its own. Provides MetaType static field
+//! initialised as described below; a metaType() override pointing to it; and
+//! the TypeId static field that is equal to MetaType.matrixId.
+//!
+//! The first two macro parameters are used as the first two EventMetaType
+//! constructor parameters; the third EventMetaType parameter is always
+//! BaseMetaType; and additional base types can be passed in extra macro
+//! parameters if you need to include the same event type in more than one
+//! event factory hierarchy (e.g., EncryptedEvent).
+//! \sa EventMetaType
+#define QUO_EVENT(CppType_, MatrixType_, ...) \
+ static inline const auto& TypeId = MatrixType_##_ls; \
+ friend class EventMetaType<CppType_>; \
+ static inline const EventMetaType<CppType_> MetaType{ \
+ #CppType_, TypeId, BaseMetaType __VA_OPT__(,) __VA_ARGS__ \
+ }; \
+ const AbstractEventMetaType& metaType() const override \
+ { \
+ return MetaType; \
+ } \
+ [[deprecated("Use " #CppType_ "::TypeId directly instead")]] \
+ static constexpr const char* matrixTypeId() { return MatrixType_; } \
+ [[deprecated("Use " #CppType_ "::TypeId directly instead")]] \
+ static event_type_t typeId() { return TypeId; } \
// End of macro
-// === is<>(), eventCast<>() and visit<>() ===
+//! \deprecated This is the old name for what is now known as QUO_EVENT
+#define DEFINE_EVENT_TYPEID(Type_, Id_) QUO_EVENT(Type_, Id_)
-template <typename EventT>
-inline bool is(const Event& e)
-{
- return e.type() == typeId<EventT>();
-}
+#define QUO_CONTENT_GETTER_X(PartType_, PartName_, JsonKey_) \
+ PartType_ PartName_() const \
+ { \
+ static const auto PartName_##JsonKey = JsonKey_; \
+ return contentPart<PartType_>(PartName_##JsonKey); \
+ }
-inline bool isUnknown(const Event& e)
+//! \brief Define an inline method obtaining a content part
+//!
+//! This macro adds a const method that extracts a JSON value at the key
+//! <tt>toSnakeCase(PartName_)</tt> (sic) and converts it to the type
+//! \p PartType_. Effectively, the generated method is an equivalent of
+//! \code
+//! contentPart<PartType_>(Quotient::toSnakeCase(#PartName_##_ls));
+//! \endcode
+#define QUO_CONTENT_GETTER(PartType_, PartName_) \
+ QUO_CONTENT_GETTER_X(PartType_, PartName_, toSnakeCase(#PartName_##_ls))
+
+//! \deprecated This macro was used after an event class definition
+//! to enable its dynamic loading; it is completely superseded by QUO_EVENT
+#define REGISTER_EVENT_TYPE(Type_)
+
+/// \brief Define a new event class with a single key-value pair in the content
+///
+/// This macro defines a new event class \p Name_ derived from \p Base_,
+/// with Matrix event type \p TypeId_, providing a getter named \p GetterName_
+/// for a single value of type \p ValueType_ inside the event content.
+/// To retrieve the value the getter uses a JSON key name that corresponds to
+/// its own (getter's) name but written in snake_case. \p GetterName_ must be
+/// in camelCase, no quotes (an identifier, not a literal).
+#define DEFINE_SIMPLE_EVENT(Name_, Base_, TypeId_, ValueType_, GetterName_, \
+ JsonKey_) \
+ constexpr auto Name_##ContentKey = JsonKey_##_ls; \
+ class QUOTIENT_API Name_ \
+ : public EventTemplate< \
+ Name_, Base_, \
+ EventContent::SingleKeyValue<ValueType_, Name_##ContentKey>> { \
+ public: \
+ QUO_EVENT(Name_, TypeId_) \
+ using value_type = ValueType_; \
+ using EventTemplate::EventTemplate; \
+ QUO_CONTENT_GETTER_X(ValueType_, GetterName_, Name_##ContentKey) \
+ }; \
+ // End of macro
+
+// === is<>(), eventCast<>() and switchOnType<>() ===
+
+template <EventClass EventT>
+inline bool is(const Event& e)
{
- return e.type() == unknownEventTypeId();
+ if constexpr (requires { EventT::MetaType; }) {
+ return &e.metaType() == &EventT::MetaType;
+ } else {
+ const auto* p = &e.metaType();
+ do {
+ if (p == &EventT::BaseMetaType)
+ return true;
+ } while ((p = p->baseType) != nullptr);
+ return false;
+ }
}
-template <typename EventT, typename BasePtrT>
+//! \brief Cast the event pointer down in a type-safe way
+//!
+//! Checks that the event \p eptr points to actually is of the requested type
+//! and returns a (plain) pointer to the event downcast to that type. \p eptr
+//! can be either "dumb" (BaseEventT*) or "smart" (`event_ptr_tt<>`). This
+//! overload doesn't affect the event ownership - if the original pointer owns
+//! the event it must outlive the downcast pointer to keep it from dangling.
+template <EventClass EventT, typename BasePtrT>
inline auto eventCast(const BasePtrT& eptr)
-> decltype(static_cast<EventT*>(&*eptr))
{
- Q_ASSERT(eptr);
- return is<std::decay_t<EventT>>(*eptr) ? static_cast<EventT*>(&*eptr)
- : nullptr;
+ return eptr && is<std::decay_t<EventT>>(*eptr)
+ ? static_cast<EventT*>(&*eptr)
+ : nullptr;
}
-// A single generic catch-all visitor
-template <typename BaseEventT, typename FnT>
-inline auto visit(const BaseEventT& event, FnT&& visitor)
- -> decltype(visitor(event))
+//! \brief Cast the event pointer down in a type-safe way, with moving
+//!
+//! Checks that the event \p eptr points to actually is of the requested type;
+//! if (and only if) it is, releases the pointer, downcasts it to the requested
+//! event type and returns a new smart pointer wrapping the downcast one.
+//! Unlike the non-moving eventCast() overload, this one only accepts a smart
+//! pointer, and that smart pointer should be an rvalue (either a temporary,
+//! or as a result of std::move()). The ownership, respectively, is transferred
+//! to the new pointer; the original smart pointer is reset to nullptr, as is
+//! normal for `unique_ptr<>::release()`.
+//! \note If \p eptr's event type does not match \p EventT it retains ownership
+//! after calling this overload; if it is a temporary, this normally
+//! leads to the event getting deleted along with the end of
+//! the temporary's lifetime.
+template <EventClass EventT, typename BaseEventT>
+inline auto eventCast(event_ptr_tt<BaseEventT>&& eptr)
{
- return visitor(event);
+ return eptr && is<std::decay_t<EventT>>(*eptr)
+ ? event_ptr_tt<EventT>(static_cast<EventT*>(eptr.release()))
+ : nullptr;
}
namespace _impl {
- template <typename T, typename FnT>
- constexpr auto needs_downcast()
+ template <typename FnT, typename BaseT>
+ concept Invocable_With_Downcast = requires
{
- return !std::is_convertible_v<T, fn_arg_t<FnT>>;
- }
+ requires EventClass<BaseT>;
+ std::is_base_of_v<BaseT, std::remove_cvref_t<fn_arg_t<FnT>>>;
+ };
}
-// A single type-specific void visitor
-template <typename BaseEventT, typename FnT>
-inline std::enable_if_t<_impl::needs_downcast<BaseEventT, FnT>()
- && std::is_void_v<fn_return_t<FnT>>>
-visit(const BaseEventT& event, FnT&& visitor)
+template <EventClass BaseT, typename TailT>
+inline auto switchOnType(const BaseT& event, TailT&& tail)
{
- using event_type = fn_arg_t<FnT>;
- if (is<std::decay_t<event_type>>(event))
- visitor(static_cast<event_type>(event));
+ if constexpr (std::is_invocable_v<TailT, BaseT>) {
+ return tail(event);
+ } else if constexpr (_impl::Invocable_With_Downcast<TailT, BaseT>) {
+ using event_type = fn_arg_t<TailT>;
+ if (is<std::decay_t<event_type>>(event))
+ return tail(static_cast<event_type>(event));
+ return std::invoke_result_t<TailT, event_type>(); // Default-constructed
+ } else { // Treat it as a value to return
+ return std::forward<TailT>(tail);
+ }
}
-// A single type-specific non-void visitor with an optional default value
-// non-voidness is guarded by defaultValue type
-template <typename BaseEventT, typename FnT>
-inline std::enable_if_t<_impl::needs_downcast<BaseEventT, FnT>(), fn_return_t<FnT>>
-visit(const BaseEventT& event, FnT&& visitor,
- fn_return_t<FnT>&& defaultValue = {})
+template <EventClass BaseT, typename FnT1, typename... FnTs>
+inline auto switchOnType(const BaseT& event, FnT1&& fn1, FnTs&&... fns)
{
- 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);
+ using event_type1 = fn_arg_t<FnT1>;
+ if (is<std::decay_t<event_type1>>(event))
+ return fn1(static_cast<event_type1>(event));
+ return switchOnType(event, std::forward<FnTs>(fns)...);
}
-// A chain of 2 or more visitors
-template <typename BaseEventT, typename FnT1, typename FnT2, typename... FnTs>
-inline fn_return_t<FnT1> visit(const BaseEventT& event, FnT1&& visitor1,
- FnT2&& visitor2, FnTs&&... visitors)
+template <EventClass BaseT, typename... FnTs>
+[[deprecated("The new name for visit() is switchOnType()")]] //
+inline auto visit(const BaseT& event, 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 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)
- -> std::enable_if_t<std::is_convertible_v<
- std::decay_t<decltype(**events.begin())>, Event>>
+inline auto visitEach(RangeT&& events, FnTs&&... fns)
+ requires std::is_void_v<
+ 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 802d8176..8db3b7e3 100644
--- a/lib/events/eventcontent.cpp
+++ b/lib/events/eventcontent.cpp
@@ -1,53 +1,55 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "eventcontent.h"
#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
{
QJsonObject o;
- fillJson(&o);
+ fillJson(o);
return o;
}
-FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType,
- const QString& originalFilename)
- : mimeType(mimeType)
- , url(u)
+FileInfo::FileInfo(const QFileInfo& fi)
+ : source(QUrl::fromLocalFile(fi.filePath())),
+ mimeType(QMimeDatabase().mimeTypeForFile(fi)),
+ payloadSize(fi.size()),
+ originalName(fi.fileName())
+{
+ Q_ASSERT(fi.isFile());
+}
+
+FileInfo::FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize,
+ const QMimeType& mimeType, QString originalFilename)
+ : source(move(sourceInfo))
+ , mimeType(mimeType)
, payloadSize(payloadSize)
- , originalName(originalFilename)
-{}
+ , originalName(move(originalFilename))
+{
+ if (!isValid())
+ qCWarning(MESSAGES)
+ << "To client developers: using FileInfo(QUrl, qint64, ...) "
+ "constructor for non-mxc resources is deprecated since Quotient "
+ "0.7; for local resources, use FileInfo(QFileInfo) instead";
+}
-FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson,
- const QString& originalFilename)
- : originalInfoJson(infoJson)
+FileInfo::FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson,
+ QString originalFilename)
+ : source(move(sourceInfo))
+ , originalInfoJson(infoJson)
, mimeType(
QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString()))
- , url(u)
, payloadSize(fromJson<qint64>(infoJson["size"_ls]))
- , originalName(originalFilename)
+ , originalName(move(originalFilename))
{
if (!mimeType.isValid())
mimeType = QMimeDatabase().mimeTypeForData(QByteArray());
@@ -55,49 +57,66 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson,
bool FileInfo::isValid() const
{
- return url.scheme() == "mxc"
- && (url.authority() + url.path()).count('/') == 1;
+ const auto& u = url();
+ return u.scheme() == "mxc" && (u.authority() + u.path()).count('/') == 1;
}
-void FileInfo::fillInfoJson(QJsonObject* infoJson) const
+QUrl FileInfo::url() const
{
- Q_ASSERT(infoJson);
- if (payloadSize != -1)
- infoJson->insert(QStringLiteral("size"), payloadSize);
- if (mimeType.isValid())
- infoJson->insert(QStringLiteral("mimetype"), mimeType.name());
+ return getUrlFromSourceInfo(source);
}
-ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType,
- const QSize& imageSize, const QString& originalFilename)
- : FileInfo(u, fileSize, mimeType, originalFilename), imageSize(imageSize)
+QJsonObject Quotient::EventContent::toInfoJson(const FileInfo& info)
+{
+ QJsonObject infoJson;
+ if (info.payloadSize != -1)
+ infoJson.insert(QStringLiteral("size"), info.payloadSize);
+ if (info.mimeType.isValid())
+ infoJson.insert(QStringLiteral("mimetype"), info.mimeType.name());
+ return infoJson;
+}
+
+ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize)
+ : FileInfo(fi), imageSize(imageSize)
+{}
+
+ImageInfo::ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize,
+ const QMimeType& type, QSize imageSize,
+ const QString& originalFilename)
+ : FileInfo(move(sourceInfo), fileSize, type, originalFilename)
+ , imageSize(imageSize)
{}
-ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson,
+ImageInfo::ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson,
const QString& originalFilename)
- : FileInfo(u, infoJson, originalFilename)
+ : FileInfo(move(sourceInfo), infoJson, originalFilename)
, imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt())
{}
-void ImageInfo::fillInfoJson(QJsonObject* infoJson) const
+QJsonObject Quotient::EventContent::toInfoJson(const ImageInfo& info)
{
- FileInfo::fillInfoJson(infoJson);
- if (imageSize.width() != -1)
- infoJson->insert(QStringLiteral("w"), imageSize.width());
- if (imageSize.height() != -1)
- infoJson->insert(QStringLiteral("h"), imageSize.height());
+ auto infoJson = toInfoJson(static_cast<const FileInfo&>(info));
+ if (info.imageSize.width() != -1)
+ infoJson.insert(QStringLiteral("w"), info.imageSize.width());
+ if (info.imageSize.height() != -1)
+ infoJson.insert(QStringLiteral("h"), info.imageSize.height());
+ return infoJson;
}
-Thumbnail::Thumbnail(const QJsonObject& infoJson)
- : ImageInfo(infoJson["thumbnail_url"_ls].toString(),
+Thumbnail::Thumbnail(const QJsonObject& infoJson,
+ const Omittable<EncryptedFileMetadata>& efm)
+ : ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()),
infoJson["thumbnail_info"_ls].toObject())
-{}
+{
+ if (efm)
+ source = *efm;
+}
-void Thumbnail::fillInfoJson(QJsonObject* infoJson) const
+void Thumbnail::dumpTo(QJsonObject& infoJson) const
{
- if (url.isValid())
- infoJson->insert(QStringLiteral("thumbnail_url"), url.toString());
+ if (url().isValid())
+ fillJson(infoJson, { "thumbnail_url"_ls, "thumbnail_file"_ls }, source);
if (!imageSize.isEmpty())
- infoJson->insert(QStringLiteral("thumbnail_info"),
- toInfoJson<ImageInfo>(*this));
+ infoJson.insert(QStringLiteral("thumbnail_info"),
+ toInfoJson(*this));
}
diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h
index 0d4c047e..af26c0a4 100644
--- a/lib/events/eventcontent.h
+++ b/lib/events/eventcontent.h
@@ -1,282 +1,254 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
// This file contains generic event content definitions, applicable to room
// message events as well as other events (e.g., avatars).
+#include "filesourceinfo.h"
+#include "quotient_export.h"
+
#include <QtCore/QJsonObject>
+#include <QtCore/QMetaType>
#include <QtCore/QMimeType>
#include <QtCore/QSize>
#include <QtCore/QUrl>
-#include <QtCore/QMetaType>
-namespace Quotient {
-namespace EventContent {
- /**
- * A base class for all content types that can be stored
- * in a RoomMessageEvent
- *
- * Each content type class should have a constructor taking
- * a QJsonObject and override fillJson() with an implementation
- * that will fill the target QJsonObject with stored values. It is
- * assumed but not required that a content object can also be created
- * from plain data.
- */
- class Base {
- public:
- explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {}
- virtual ~Base() = default;
-
- // FIXME: make toJson() from converters.* work on base classes
- QJsonObject toJson() const;
-
- public:
- QJsonObject originalJson;
-
- protected:
- Base(const Base&) = default;
- Base(Base&&) = default;
-
- virtual void fillJson(QJsonObject* o) const = 0;
- };
-
- // The below structures fairly follow CS spec 11.2.1.6. The overall
- // set of attributes for each content types is a superset of the spec
- // but specific aggregation structure is altered. See doc comments to
- // each type for the list of available attributes.
-
- // A quick classes inheritance structure follows:
- // FileInfo
- // FileContent : UrlBasedContent<FileInfo, Thumbnail>
- // AudioContent : UrlBasedContent<FileInfo, Duration>
- // ImageInfo : FileInfo + imageSize attribute
- // ImageContent : UrlBasedContent<ImageInfo, Thumbnail>
- // VideoContent : UrlBasedContent<ImageInfo, Thumbnail, Duration>
-
- /**
- * A base/mixin class for structures representing an "info" object for
- * some content types. These include most attachment types currently in
- * the CS API spec.
- *
- * In order to use it in a content class, derive both from TypedBase
- * (or Base) and from FileInfo (or its derivative, such as \p ImageInfo)
- * and call fillInfoJson() to fill the "info" subobject. Make sure
- * to pass an "info" part of JSON to FileInfo constructor, not the whole
- * JSON content, as well as contents of "url" (or a similar key) and
- * optionally "filename" node from the main JSON content. Assuming you
- * don't do unusual things, you should use \p UrlBasedContent<> instead
- * of doing multiple inheritance and overriding Base::fillJson() by hand.
- *
- * This class is not polymorphic.
- */
- class FileInfo {
- public:
- explicit FileInfo(const QUrl& u, qint64 payloadSize = -1,
- const QMimeType& mimeType = {},
- const QString& originalFilename = {});
- FileInfo(const QUrl& u, const QJsonObject& infoJson,
- const QString& originalFilename = {});
-
- bool isValid() const;
-
- void fillInfoJson(QJsonObject* infoJson) const;
-
- /**
- * \brief Extract media id from the URL
- *
- * This can be used, e.g., to construct a QML-facing image://
- * URI as follows:
- * \code "image://provider/" + info.mediaId() \endcode
- */
- QString mediaId() const { return url.authority() + url.path(); }
-
- public:
- QJsonObject originalInfoJson;
- QMimeType mimeType;
- QUrl url;
- qint64 payloadSize;
- QString originalName;
- };
-
- template <typename InfoT>
- QJsonObject toInfoJson(const InfoT& info)
+class QFileInfo;
+
+namespace Quotient::EventContent {
+//! \brief Base for all content types that can be stored in RoomMessageEvent
+//!
+//! Each content type class should have a constructor taking
+//! a QJsonObject and override fillJson() with an implementation
+//! that will fill the target QJsonObject with stored values. It is
+//! assumed but not required that a content object can also be created
+//! from plain data.
+class QUOTIENT_API Base {
+public:
+ explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {}
+ virtual ~Base() = default;
+
+ QJsonObject toJson() const;
+
+public:
+ QJsonObject originalJson;
+
+ // You can't assign those classes
+ Base& operator=(const Base&) = delete;
+ Base& operator=(Base&&) = delete;
+
+protected:
+ Base(const Base&) = default;
+ Base(Base&&) noexcept = default;
+
+ virtual void fillJson(QJsonObject&) const = 0;
+};
+
+// The below structures fairly follow CS spec 11.2.1.6. The overall
+// set of attributes for each content types is a superset of the spec
+// but specific aggregation structure is altered. See doc comments to
+// each type for the list of available attributes.
+
+// A quick classes inheritance structure follows (the definitions are
+// spread across eventcontent.h and roommessageevent.h):
+// UrlBasedContent<InfoT> : InfoT + thumbnail data
+// PlayableContent<InfoT> : + duration attribute
+// FileInfo
+// FileContent = UrlBasedContent<FileInfo>
+// AudioContent = PlayableContent<FileInfo>
+// ImageInfo : FileInfo + imageSize attribute
+// ImageContent = UrlBasedContent<ImageInfo>
+// VideoContent = PlayableContent<ImageInfo>
+
+//! \brief Mix-in class representing `info` subobject in content JSON
+//!
+//! This is one of base classes for content types that deal with files or
+//! URLs. It stores the file metadata attributes, such as size, MIME type
+//! etc. found in the `content/info` subobject of event JSON payloads.
+//! Actual content classes derive from this class _and_ TypedBase that
+//! provides a polymorphic interface to access data in the mix-in. FileInfo
+//! (as well as ImageInfo, that adds image size to the metadata) is NOT
+//! polymorphic and is used in a non-polymorphic way to store thumbnail
+//! metadata (in a separate instance), next to the metadata on the file
+//! itself.
+//!
+//! If you need to make a new _content_ (not info) class based on files/URLs
+//! take UrlBasedContent as the example, i.e.:
+//! 1. Double-inherit from this class (or ImageInfo) and TypedBase.
+//! 2. Provide a constructor from QJsonObject that will pass the `info`
+//! subobject (not the whole content JSON) down to FileInfo/ImageInfo.
+//! 3. Override fillJson() to customise the JSON export logic. Make sure
+//! to call toInfoJson() from it to produce the payload for the `info`
+//! subobject in the JSON payload.
+//!
+//! \sa ImageInfo, FileContent, ImageContent, AudioContent, VideoContent,
+//! UrlBasedContent
+class QUOTIENT_API FileInfo {
+public:
+ FileInfo() = default;
+ //! \brief Construct from a QFileInfo object
+ //!
+ //! \param fi a QFileInfo object referring to an existing file
+ explicit FileInfo(const QFileInfo& fi);
+ explicit FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize = -1,
+ const QMimeType& mimeType = {},
+ QString originalFilename = {});
+ //! \brief Construct from a JSON `info` payload
+ //!
+ //! Make sure to pass the `info` subobject of content JSON, not the
+ //! whole JSON content.
+ FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson,
+ QString originalFilename = {});
+
+ bool isValid() const;
+ QUrl url() const;
+
+ //! \brief Extract media id from the URL
+ //!
+ //! This can be used, e.g., to construct a QML-facing image://
+ //! URI as follows:
+ //! \code "image://provider/" + info.mediaId() \endcode
+ QString mediaId() const { return url().authority() + url().path(); }
+
+public:
+ FileSourceInfo source;
+ QJsonObject originalInfoJson;
+ QMimeType mimeType;
+ qint64 payloadSize = 0;
+ QString originalName;
+};
+
+QUOTIENT_API QJsonObject toInfoJson(const FileInfo& info);
+
+//! \brief A content info class for image/video content types and thumbnails
+class QUOTIENT_API ImageInfo : public FileInfo {
+public:
+ ImageInfo() = default;
+ explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {});
+ explicit ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize = -1,
+ const QMimeType& type = {}, QSize imageSize = {},
+ const QString& originalFilename = {});
+ ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson,
+ const QString& originalFilename = {});
+
+public:
+ QSize imageSize;
+};
+
+QUOTIENT_API QJsonObject toInfoJson(const ImageInfo& info);
+
+//! \brief An auxiliary class for an info type that carries a thumbnail
+//!
+//! This class saves/loads a thumbnail to/from `info` subobject of
+//! the JSON representation of event content; namely, `info/thumbnail_url`
+//! (or, in case of an encrypted thumbnail, `info/thumbnail_file`) and
+//! `info/thumbnail_info` fields are used.
+class QUOTIENT_API Thumbnail : public ImageInfo {
+public:
+ using ImageInfo::ImageInfo;
+ explicit Thumbnail(const QJsonObject& infoJson,
+ const Omittable<EncryptedFileMetadata>& efm = none);
+
+ //! \brief Add thumbnail information to the passed `info` JSON object
+ void dumpTo(QJsonObject& infoJson) const;
+};
+
+class QUOTIENT_API TypedBase : public Base {
+public:
+ virtual QMimeType type() const = 0;
+ virtual const FileInfo* fileInfo() const { return nullptr; }
+ virtual FileInfo* fileInfo() { return nullptr; }
+ virtual const Thumbnail* thumbnailInfo() const { return nullptr; }
+
+protected:
+ explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {}
+ using Base::Base;
+};
+
+//! \brief A template class for content types with a URL and additional info
+//!
+//! Types that derive from this class template take `url` (or, if the file
+//! is encrypted, `file`) and, optionally, `filename` values from
+//! the top-level JSON object and the rest of information from the `info`
+//! subobject, as defined by the parameter type.
+//! \tparam InfoT base info class - FileInfo or ImageInfo
+template <class InfoT>
+class UrlBasedContent : public TypedBase, public InfoT {
+public:
+ using InfoT::InfoT;
+ explicit UrlBasedContent(const QJsonObject& json)
+ : TypedBase(json)
+ , InfoT(QUrl(json["url"].toString()), json["info"].toObject(),
+ json["filename"].toString())
+ , thumbnail(FileInfo::originalInfoJson)
{
- QJsonObject infoJson;
- info.fillInfoJson(&infoJson);
- return infoJson;
+ if (const auto efmJson = json.value("file"_ls).toObject();
+ !efmJson.isEmpty())
+ InfoT::source = fromJson<EncryptedFileMetadata>(efmJson);
+ // Two small hacks on originalJson to expose mediaIds to QML
+ originalJson.insert("mediaId", InfoT::mediaId());
+ originalJson.insert("thumbnailMediaId", thumbnail.mediaId());
}
- /**
- * A content info class for image content types: image, thumbnail, video
- */
- class ImageInfo : public FileInfo {
- public:
- explicit ImageInfo(const QUrl& u, qint64 fileSize = -1,
- QMimeType mimeType = {}, const QSize& imageSize = {},
- const QString& originalFilename = {});
- ImageInfo(const QUrl& u, const QJsonObject& infoJson,
- const QString& originalFilename = {});
-
- void fillInfoJson(QJsonObject* infoJson) const;
-
- public:
- QSize imageSize;
- };
-
- /**
- * An auxiliary class for an info type that carries a thumbnail
- *
- * This class saves/loads a thumbnail to/from "info" subobject of
- * the JSON representation of event content; namely,
- * "info/thumbnail_url" and "info/thumbnail_info" fields are used.
- */
- class Thumbnail : public ImageInfo {
- public:
- Thumbnail() : ImageInfo(QUrl()) {} // To allow empty thumbnails
- Thumbnail(const QJsonObject& infoJson);
- Thumbnail(const ImageInfo& info) : ImageInfo(info) {}
- using ImageInfo::ImageInfo;
-
- /**
- * Writes thumbnail information to "thumbnail_info" subobject
- * and thumbnail URL to "thumbnail_url" node inside "info".
- */
- void fillInfoJson(QJsonObject* infoJson) const;
- };
-
- 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:
- using Base::Base;
- };
-
- /**
- * A base class for content types that have a URL and additional info
- *
- * Types that derive from this class template take "url" and,
- * optionally, "filename" values from the top-level JSON object and
- * the rest of information from the "info" subobject, as defined by
- * the parameter type.
- *
- * \tparam InfoT base info class
- */
- template <class InfoT>
- class UrlBasedContent : public TypedBase, public InfoT {
- public:
- using InfoT::InfoT;
- explicit UrlBasedContent(const QJsonObject& json)
- : TypedBase(json)
- , InfoT(json["url"].toString(), json["info"].toObject(),
- json["filename"].toString())
- {
- // A small hack to facilitate links creation in QML.
- originalJson.insert("mediaId", InfoT::mediaId());
- }
-
- QMimeType type() const override { return InfoT::mimeType; }
- const FileInfo* fileInfo() const override { return this; }
- FileInfo* fileInfo() override { return this; }
-
- protected:
- void fillJson(QJsonObject* json) const override
- {
- Q_ASSERT(json);
- json->insert("url", InfoT::url.toString());
- if (!InfoT::originalName.isEmpty())
- json->insert("filename", InfoT::originalName);
- json->insert("info", toInfoJson<InfoT>(*this));
- }
- };
-
- template <typename InfoT>
- class UrlWithThumbnailContent : public UrlBasedContent<InfoT> {
- public:
- // NB: when using inherited constructors, thumbnail has to be
- // initialised separately
- using UrlBasedContent<InfoT>::UrlBasedContent;
- explicit UrlWithThumbnailContent(const QJsonObject& json)
- : UrlBasedContent<InfoT>(json), thumbnail(InfoT::originalInfoJson)
- {
- // Another small hack, to simplify making a thumbnail link
- UrlBasedContent<InfoT>::originalJson.insert("thumbnailMediaId",
- thumbnail.mediaId());
- }
-
- const Thumbnail* thumbnailInfo() const override { return &thumbnail; }
-
- public:
- Thumbnail thumbnail;
-
- protected:
- void fillJson(QJsonObject* json) const override
- {
- UrlBasedContent<InfoT>::fillJson(json);
- auto infoJson = json->take("info").toObject();
- thumbnail.fillInfoJson(&infoJson);
- json->insert("info", infoJson);
- }
- };
-
- /**
- * Content class for m.image
- *
- * Available fields:
- * - corresponding to the top-level JSON:
- * - url
- * - filename (extension to the spec)
- * - corresponding to the "info" subobject:
- * - payloadSize ("size" in JSON)
- * - mimeType ("mimetype" in JSON)
- * - imageSize (QSize for a combination of "h" and "w" in JSON)
- * - thumbnail.url ("thumbnail_url" in JSON)
- * - corresponding to the "info/thumbnail_info" subobject: contents of
- * thumbnail field, in the same vein as for the main image:
- * - payloadSize
- * - mimeType
- * - imageSize
- */
- using ImageContent = UrlWithThumbnailContent<ImageInfo>;
-
- /**
- * Content class for m.file
- *
- * Available fields:
- * - corresponding to the top-level JSON:
- * - url
- * - filename
- * - corresponding to the "info" subobject:
- * - payloadSize ("size" in JSON)
- * - mimeType ("mimetype" in JSON)
- * - thumbnail.url ("thumbnail_url" in JSON)
- * - corresponding to the "info/thumbnail_info" subobject:
- * - thumbnail.payloadSize
- * - thumbnail.mimeType
- * - thumbnail.imageSize (QSize for "h" and "w" in JSON)
- */
- using FileContent = UrlWithThumbnailContent<FileInfo>;
-} // namespace EventContent
-} // namespace Quotient
+ QMimeType type() const override { return InfoT::mimeType; }
+ const FileInfo* fileInfo() const override { return this; }
+ FileInfo* fileInfo() override { return this; }
+ const Thumbnail* thumbnailInfo() const override { return &thumbnail; }
+
+public:
+ Thumbnail thumbnail;
+
+protected:
+ virtual void fillInfoJson(QJsonObject& infoJson [[maybe_unused]]) const
+ {}
+
+ void fillJson(QJsonObject& json) const override
+ {
+ Quotient::fillJson(json, { "url"_ls, "file"_ls }, InfoT::source);
+ if (!InfoT::originalName.isEmpty())
+ json.insert("filename", InfoT::originalName);
+ auto infoJson = toInfoJson(*this);
+ if (thumbnail.isValid())
+ thumbnail.dumpTo(infoJson);
+ fillInfoJson(infoJson);
+ json.insert("info", infoJson);
+ }
+};
+
+//! \brief Content class for m.image
+//!
+//! Available fields:
+//! - corresponding to the top-level JSON:
+//! - source (corresponding to `url` or `file` in JSON)
+//! - filename (extension to the spec)
+//! - corresponding to the `info` subobject:
+//! - payloadSize (`size` in JSON)
+//! - mimeType (`mimetype` in JSON)
+//! - imageSize (QSize for a combination of `h` and `w` in JSON)
+//! - thumbnail.url (`thumbnail_url` in JSON)
+//! - corresponding to the `info/thumbnail_info` subobject: contents of
+//! thumbnail field, in the same vein as for the main image:
+//! - payloadSize
+//! - mimeType
+//! - imageSize
+using ImageContent = UrlBasedContent<ImageInfo>;
+
+//! \brief Content class for m.file
+//!
+//! Available fields:
+//! - corresponding to the top-level JSON:
+//! - source (corresponding to `url` or `file` in JSON)
+//! - filename
+//! - corresponding to the `info` subobject:
+//! - payloadSize (`size` in JSON)
+//! - mimeType (`mimetype` in JSON)
+//! - thumbnail.source (`thumbnail_url` or `thumbnail_file` in JSON)
+//! - corresponding to the `info/thumbnail_info` subobject:
+//! - thumbnail.payloadSize
+//! - thumbnail.mimeType
+//! - thumbnail.imageSize (QSize for `h` and `w` in JSON)
+using FileContent = UrlBasedContent<FileInfo>;
+} // namespace Quotient::EventContent
Q_DECLARE_METATYPE(const Quotient::EventContent::TypedBase*)
diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h
index ebb96441..b4ac154c 100644
--- a/lib/events/eventloader.h
+++ b/lib/events/eventloader.h
@@ -1,86 +1,13 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#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
- *
- * Use this factory template to detect the type from the JSON object
- * contents (the detected event type should derive from the template
- * parameter type) and create an event object of that type.
- */
-template <typename BaseEventT>
-inline event_ptr_tt<BaseEventT> loadEvent(const QJsonObject& fullJson)
-{
- return _impl::loadEvent<BaseEventT>(fullJson, fullJson[TypeKeyL].toString());
-}
-
-/*! Create an event from a type string and content JSON
- *
- * Use this factory template to resolve the C++ type from the Matrix
- * type string in \p matrixType and create an event of that type that has
- * its content part set to \p content.
- */
-template <typename BaseEventT>
-inline event_ptr_tt<BaseEventT> loadEvent(const QString& matrixType,
- const QJsonObject& content)
-{
- return _impl::loadEvent<BaseEventT>(basicEventJson(matrixType, content),
- matrixType);
+struct [[deprecated(
+ "This header is obsolete since libQuotient 0.7; include a header with"
+ " the respective event type definition instead")]] EventLoaderH;
+StateEventPtr eventLoaderH(EventLoaderH&);
}
-
-/*! Create a state event from a type string, content JSON and state key
- *
- * Use this factory to resolve the C++ type from the Matrix type string
- * in \p matrixType and create a state event of that type with content part
- * set to \p content and state key set to \p stateKey (empty by default).
- */
-inline StateEventPtr loadStateEvent(const QString& matrixType,
- const QJsonObject& content,
- const QString& stateKey = {})
-{
- return _impl::loadEvent<StateEventBase>(
- basicStateEventJson(matrixType, content, stateKey), matrixType);
-}
-
-template <typename EventT>
-struct JsonConverter<event_ptr_tt<EventT>> {
- static auto load(const QJsonValue& jv)
- {
- return loadEvent<EventT>(jv.toObject());
- }
- static auto load(const QJsonDocument& jd)
- {
- return loadEvent<EventT>(jd.object());
- }
-};
-} // namespace Quotient
diff --git a/lib/events/eventrelation.cpp b/lib/events/eventrelation.cpp
new file mode 100644
index 00000000..04972f45
--- /dev/null
+++ b/lib/events/eventrelation.cpp
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: 2022 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "eventrelation.h"
+
+#include "../logging.h"
+#include "event.h"
+
+using namespace Quotient;
+
+void JsonObjectConverter<EventRelation>::dumpTo(QJsonObject& jo,
+ const EventRelation& pod)
+{
+ if (pod.type.isEmpty()) {
+ qCWarning(MAIN) << "Empty relation type; won't dump to JSON";
+ return;
+ }
+ jo.insert(RelTypeKey, pod.type);
+ jo.insert(EventIdKey, pod.eventId);
+ if (pod.type == EventRelation::AnnotationType)
+ jo.insert(QStringLiteral("key"), pod.key);
+}
+
+void JsonObjectConverter<EventRelation>::fillFrom(const QJsonObject& jo,
+ EventRelation& pod)
+{
+ if (const auto replyJson = jo.value(EventRelation::ReplyType).toObject();
+ !replyJson.isEmpty()) {
+ pod.type = EventRelation::ReplyType;
+ fromJson(replyJson[EventIdKeyL], pod.eventId);
+ } else {
+ // The experimental logic for generic relationships (MSC1849)
+ fromJson(jo[RelTypeKey], pod.type);
+ fromJson(jo[EventIdKeyL], pod.eventId);
+ if (pod.type == EventRelation::AnnotationType)
+ fromJson(jo["key"_ls], pod.key);
+ }
+}
diff --git a/lib/events/eventrelation.h b/lib/events/eventrelation.h
new file mode 100644
index 00000000..2a841cf1
--- /dev/null
+++ b/lib/events/eventrelation.h
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: 2022 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "converters.h"
+
+namespace Quotient {
+
+[[maybe_unused]] constexpr auto RelatesToKey = "m.relates_to"_ls;
+constexpr auto RelTypeKey = "rel_type"_ls;
+
+struct QUOTIENT_API EventRelation {
+ using reltypeid_t = QLatin1String;
+
+ QString type;
+ QString eventId;
+ QString key = {}; // Only used for m.annotation for now
+
+ static constexpr auto ReplyType = "m.in_reply_to"_ls;
+ static constexpr auto AnnotationType = "m.annotation"_ls;
+ static constexpr auto ReplacementType = "m.replace"_ls;
+
+ static EventRelation replyTo(QString eventId)
+ {
+ return { ReplyType, std::move(eventId) };
+ }
+ static EventRelation annotate(QString eventId, QString key)
+ {
+ return { AnnotationType, std::move(eventId), std::move(key) };
+ }
+ static EventRelation replace(QString eventId)
+ {
+ return { ReplacementType, std::move(eventId) };
+ }
+
+ [[deprecated("Use ReplyType variable instead")]]
+ static constexpr auto Reply() { return ReplyType; }
+ [[deprecated("Use AnnotationType variable instead")]] //
+ static constexpr auto Annotation() { return AnnotationType; }
+ [[deprecated("Use ReplacementType variable instead")]] //
+ static constexpr auto Replacement() { return ReplacementType; }
+};
+
+template <>
+struct QUOTIENT_API JsonObjectConverter<EventRelation> {
+ static void dumpTo(QJsonObject& jo, const EventRelation& pod);
+ static void fillFrom(const QJsonObject& jo, EventRelation& pod);
+};
+
+}
+
diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp
new file mode 100644
index 00000000..a60d86d2
--- /dev/null
+++ b/lib/events/filesourceinfo.cpp
@@ -0,0 +1,163 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "filesourceinfo.h"
+
+#include "logging.h"
+#include "util.h"
+
+#ifdef Quotient_E2EE_ENABLED
+# include "e2ee/qolmutils.h"
+
+# include <QtCore/QCryptographicHash>
+
+# include <openssl/evp.h>
+#endif
+
+using namespace Quotient;
+
+QByteArray Quotient::decryptFile(const QByteArray& ciphertext,
+ const EncryptedFileMetadata& metadata)
+{
+#ifdef Quotient_E2EE_ENABLED
+ if (QByteArray::fromBase64(metadata.hashes["sha256"_ls].toLatin1())
+ != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) {
+ qCWarning(E2EE) << "Hash verification failed for file";
+ return {};
+ }
+
+ auto _key = metadata.key.k;
+ const auto keyBytes = QByteArray::fromBase64(
+ _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1());
+ int length;
+ auto* ctx = EVP_CIPHER_CTX_new();
+ QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0');
+ EVP_DecryptInit_ex(
+ ctx, EVP_aes_256_ctr(), nullptr,
+ reinterpret_cast<const unsigned char*>(keyBytes.data()),
+ reinterpret_cast<const unsigned char*>(
+ QByteArray::fromBase64(metadata.iv.toLatin1()).data()));
+ EVP_DecryptUpdate(ctx, reinterpret_cast<unsigned char*>(plaintext.data()),
+ &length,
+ reinterpret_cast<const unsigned char*>(ciphertext.data()),
+ ciphertext.size());
+ EVP_DecryptFinal_ex(ctx,
+ reinterpret_cast<unsigned char*>(plaintext.data())
+ + length,
+ &length);
+ EVP_CIPHER_CTX_free(ctx);
+ return plaintext.left(ciphertext.size());
+#else
+ qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, "
+ "cannot decrypt the file";
+ return ciphertext;
+#endif
+}
+
+std::pair<EncryptedFileMetadata, QByteArray> Quotient::encryptFile(
+ const QByteArray& plainText)
+{
+#ifdef Quotient_E2EE_ENABLED
+ auto k = RandomBuffer(32);
+ auto kBase64 = k.toBase64(QByteArray::Base64UrlEncoding
+ | QByteArray::OmitTrailingEquals);
+ auto iv = RandomBuffer(16);
+ JWK key = {
+ "oct"_ls, { "encrypt"_ls, "decrypt"_ls }, "A256CTR"_ls, kBase64, true
+ };
+
+ int length = -1;
+ auto* ctx = EVP_CIPHER_CTX_new();
+ EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, k.bytes(), iv.bytes());
+ const auto blockSize = EVP_CIPHER_CTX_block_size(ctx);
+ QByteArray cipherText(plainText.size() + blockSize - 1, '\0');
+ EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(cipherText.data()),
+ &length,
+ reinterpret_cast<const unsigned char*>(plainText.data()),
+ plainText.size());
+ EVP_EncryptFinal_ex(ctx,
+ reinterpret_cast<unsigned char*>(cipherText.data())
+ + length,
+ &length);
+ EVP_CIPHER_CTX_free(ctx);
+
+ auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256)
+ .toBase64(QByteArray::OmitTrailingEquals);
+ auto ivBase64 = iv.toBase64(QByteArray::OmitTrailingEquals);
+ EncryptedFileMetadata efm = {
+ {}, key, ivBase64, { { QStringLiteral("sha256"), hash } }, "v2"_ls
+ };
+ return { efm, cipherText };
+#else
+ return {};
+#endif
+}
+
+void JsonObjectConverter<EncryptedFileMetadata>::dumpTo(QJsonObject& jo,
+ const EncryptedFileMetadata& pod)
+{
+ addParam<>(jo, QStringLiteral("url"), pod.url);
+ addParam<>(jo, QStringLiteral("key"), pod.key);
+ addParam<>(jo, QStringLiteral("iv"), pod.iv);
+ addParam<>(jo, QStringLiteral("hashes"), pod.hashes);
+ addParam<>(jo, QStringLiteral("v"), pod.v);
+}
+
+void JsonObjectConverter<EncryptedFileMetadata>::fillFrom(const QJsonObject& jo,
+ EncryptedFileMetadata& pod)
+{
+ fromJson(jo.value("url"_ls), pod.url);
+ fromJson(jo.value("key"_ls), pod.key);
+ fromJson(jo.value("iv"_ls), pod.iv);
+ fromJson(jo.value("hashes"_ls), pod.hashes);
+ fromJson(jo.value("v"_ls), pod.v);
+}
+
+void JsonObjectConverter<JWK>::dumpTo(QJsonObject& jo, const JWK& pod)
+{
+ addParam<>(jo, QStringLiteral("kty"), pod.kty);
+ addParam<>(jo, QStringLiteral("key_ops"), pod.keyOps);
+ addParam<>(jo, QStringLiteral("alg"), pod.alg);
+ addParam<>(jo, QStringLiteral("k"), pod.k);
+ addParam<>(jo, QStringLiteral("ext"), pod.ext);
+}
+
+void JsonObjectConverter<JWK>::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);
+}
+
+QUrl Quotient::getUrlFromSourceInfo(const FileSourceInfo& fsi)
+{
+ return std::visit(Overloads { [](const QUrl& url) { return url; },
+ [](const EncryptedFileMetadata& efm) {
+ return efm.url;
+ } },
+ fsi);
+}
+
+void Quotient::setUrlInSourceInfo(FileSourceInfo& fsi, const QUrl& newUrl)
+{
+ std::visit(Overloads { [&newUrl](QUrl& url) { url = newUrl; },
+ [&newUrl](EncryptedFileMetadata& efm) {
+ efm.url = newUrl;
+ } },
+ fsi);
+}
+
+void Quotient::fillJson(QJsonObject& jo,
+ const std::array<QLatin1String, 2>& jsonKeys,
+ const FileSourceInfo& fsi)
+{
+ // NB: Keeping variant_size_v out of the function signature for readability.
+ // NB2: Can't use jsonKeys directly inside static_assert as its value is
+ // unknown so the compiler cannot ensure size() is constexpr (go figure...)
+ static_assert(
+ std::variant_size_v<FileSourceInfo> == decltype(jsonKeys) {}.size());
+ jo.insert(jsonKeys[fsi.index()], toJson(fsi));
+}
diff --git a/lib/events/filesourceinfo.h b/lib/events/filesourceinfo.h
new file mode 100644
index 00000000..8f7e3cbe
--- /dev/null
+++ b/lib/events/filesourceinfo.h
@@ -0,0 +1,90 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "converters.h"
+
+#include <array>
+
+namespace Quotient {
+/**
+ * JSON Web Key object as specified in
+ * https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes
+ * The only currently relevant member is `k`, the rest needs to be set to the defaults specified in the spec.
+ */
+struct JWK
+{
+ Q_GADGET
+ Q_PROPERTY(QString kty MEMBER kty CONSTANT)
+ Q_PROPERTY(QStringList keyOps MEMBER keyOps CONSTANT)
+ Q_PROPERTY(QString alg MEMBER alg CONSTANT)
+ Q_PROPERTY(QString k MEMBER k CONSTANT)
+ Q_PROPERTY(bool ext MEMBER ext CONSTANT)
+
+public:
+ QString kty;
+ QStringList keyOps;
+ QString alg;
+ QString k;
+ bool ext;
+};
+
+struct QUOTIENT_API EncryptedFileMetadata {
+ Q_GADGET
+ Q_PROPERTY(QUrl url MEMBER url CONSTANT)
+ Q_PROPERTY(JWK key MEMBER key CONSTANT)
+ Q_PROPERTY(QString iv MEMBER iv CONSTANT)
+ Q_PROPERTY(QHash<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;
+};
+
+QUOTIENT_API std::pair<EncryptedFileMetadata, QByteArray> encryptFile(
+ const QByteArray& plainText);
+QUOTIENT_API QByteArray decryptFile(const QByteArray& ciphertext,
+ const EncryptedFileMetadata& metadata);
+
+template <>
+struct QUOTIENT_API JsonObjectConverter<EncryptedFileMetadata> {
+ static void dumpTo(QJsonObject& jo, const EncryptedFileMetadata& pod);
+ static void fillFrom(const QJsonObject& jo, EncryptedFileMetadata& pod);
+};
+
+template <>
+struct QUOTIENT_API JsonObjectConverter<JWK> {
+ static void dumpTo(QJsonObject& jo, const JWK& pod);
+ static void fillFrom(const QJsonObject& jo, JWK& pod);
+};
+
+using FileSourceInfo = std::variant<QUrl, EncryptedFileMetadata>;
+
+QUOTIENT_API QUrl getUrlFromSourceInfo(const FileSourceInfo& fsi);
+
+QUOTIENT_API void setUrlInSourceInfo(FileSourceInfo& fsi, const QUrl& newUrl);
+
+// The way FileSourceInfo is stored in JSON requires an extra parameter so
+// the original template is not applicable
+template <>
+void fillJson(QJsonObject&, const FileSourceInfo&) = delete;
+
+//! \brief Export FileSourceInfo to a JSON object
+//!
+//! Depending on what is stored inside FileSourceInfo, this function will insert
+//! - a key-to-string pair where key is taken from jsonKeys[0] and the string
+//! is the URL, if FileSourceInfo stores a QUrl;
+//! - a key-to-object mapping where key is taken from jsonKeys[1] and the object
+//! is the result of converting EncryptedFileMetadata to JSON,
+//! if FileSourceInfo stores EncryptedFileMetadata
+QUOTIENT_API void fillJson(QJsonObject& jo,
+ const std::array<QLatin1String, 2>& jsonKeys,
+ const FileSourceInfo& fsi);
+
+} // namespace Quotient
diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h
new file mode 100644
index 00000000..80aebcf3
--- /dev/null
+++ b/lib/events/keyverificationevent.h
@@ -0,0 +1,258 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "event.h"
+
+namespace Quotient {
+
+static constexpr auto SasV1Method = "m.sas.v1"_ls;
+
+class QUOTIENT_API KeyVerificationEvent : public Event {
+public:
+ QUO_BASE_EVENT(KeyVerificationEvent, "m.key.*"_ls, Event::BaseMetaType)
+
+ using Event::Event;
+
+ /// An opaque identifier for the verification request. Must
+ /// be unique with respect to the devices involved.
+ QUO_CONTENT_GETTER(QString, transactionId)
+};
+
+/// Requests a key verification with another user's devices.
+/// Typically sent as a to-device event.
+class QUOTIENT_API KeyVerificationRequestEvent : public KeyVerificationEvent {
+public:
+ QUO_EVENT(KeyVerificationRequestEvent, "m.key.verification.request")
+
+ using KeyVerificationEvent::KeyVerificationEvent;
+ KeyVerificationRequestEvent(const QString& transactionId,
+ const QString& fromDevice,
+ const QStringList& methods,
+ const QDateTime& timestamp)
+ : KeyVerificationRequestEvent(
+ basicJson(TypeId, { { "transaction_id"_ls, transactionId },
+ { "from_device"_ls, fromDevice },
+ { "methods"_ls, toJson(methods) },
+ { "timestamp"_ls, toJson(timestamp) } }))
+ {}
+
+ /// The device ID which is initiating the request.
+ QUO_CONTENT_GETTER(QString, fromDevice)
+
+ /// The verification methods supported by the sender.
+ QUO_CONTENT_GETTER(QStringList, methods)
+
+ /// The POSIX timestamp in milliseconds for when the request was
+ /// made. If the request is in the future by more than 5 minutes or
+ /// more than 10 minutes in the past, the message should be ignored
+ /// by the receiver.
+ QUO_CONTENT_GETTER(QDateTime, timestamp)
+};
+
+class QUOTIENT_API KeyVerificationReadyEvent : public KeyVerificationEvent {
+public:
+ QUO_EVENT(KeyVerificationReadyEvent, "m.key.verification.ready")
+
+ using KeyVerificationEvent::KeyVerificationEvent;
+ KeyVerificationReadyEvent(const QString& transactionId,
+ const QString& fromDevice,
+ const QStringList& methods)
+ : KeyVerificationReadyEvent(
+ basicJson(TypeId, { { "transaction_id"_ls, transactionId },
+ { "from_device"_ls, fromDevice },
+ { "methods"_ls, toJson(methods) } }))
+ {}
+
+ /// The device ID which is accepting the request.
+ QUO_CONTENT_GETTER(QString, fromDevice)
+
+ /// The verification methods supported by the sender.
+ QUO_CONTENT_GETTER(QStringList, methods)
+};
+
+/// Begins a key verification process.
+class QUOTIENT_API KeyVerificationStartEvent : public KeyVerificationEvent {
+public:
+ QUO_EVENT(KeyVerificationStartEvent, "m.key.verification.start")
+
+ using KeyVerificationEvent::KeyVerificationEvent;
+ KeyVerificationStartEvent(const QString& transactionId,
+ const QString& fromDevice)
+ : KeyVerificationStartEvent(
+ basicJson(TypeId, { { "transaction_id"_ls, transactionId },
+ { "from_device"_ls, fromDevice },
+ { "method"_ls, SasV1Method },
+ { "hashes"_ls, QJsonArray{ "sha256"_ls } },
+ { "key_agreement_protocols"_ls,
+ QJsonArray{ "curve25519-hkdf-sha256"_ls } },
+ { "message_authentication_codes"_ls,
+ QJsonArray{ "hkdf-hmac-sha256"_ls } },
+ { "short_authentication_string"_ls,
+ QJsonArray{ "decimal"_ls, "emoji"_ls } } }))
+ {}
+
+ /// The device ID which is initiating the process.
+ QUO_CONTENT_GETTER(QString, fromDevice)
+
+ /// The verification method to use.
+ QUO_CONTENT_GETTER(QString, method)
+
+ /// Optional method to use to verify the other user's key with.
+ QUO_CONTENT_GETTER(Omittable<QString>, nextMethod)
+
+ // SAS.V1 methods
+
+ /// The key agreement protocols the sending device understands.
+ /// \note Only exist if method is m.sas.v1
+ QStringList keyAgreementProtocols() const
+ {
+ Q_ASSERT(method() == SasV1Method);
+ return contentPart<QStringList>("key_agreement_protocols"_ls);
+ }
+
+ /// The hash methods the sending device understands.
+ /// \note Only exist if method is m.sas.v1
+ QStringList hashes() const
+ {
+ Q_ASSERT(method() == SasV1Method);
+ return contentPart<QStringList>("hashes"_ls);
+ }
+
+ /// The message authentication codes that the sending device understands.
+ /// \note Only exist if method is m.sas.v1
+ QStringList messageAuthenticationCodes() const
+ {
+ Q_ASSERT(method() == SasV1Method);
+ return contentPart<QStringList>("message_authentication_codes"_ls);
+ }
+
+ /// The SAS methods the sending device (and the sending device's
+ /// user) understands.
+ /// \note Only exist if method is m.sas.v1
+ QString shortAuthenticationString() const
+ {
+ Q_ASSERT(method() == SasV1Method);
+ return contentPart<QString>("short_authentification_string"_ls);
+ }
+};
+
+/// Accepts a previously sent m.key.verification.start message.
+/// Typically sent as a to-device event.
+class QUOTIENT_API KeyVerificationAcceptEvent : public KeyVerificationEvent {
+public:
+ QUO_EVENT(KeyVerificationAcceptEvent, "m.key.verification.accept")
+
+ using KeyVerificationEvent::KeyVerificationEvent;
+ KeyVerificationAcceptEvent(const QString& transactionId,
+ const QString& commitment)
+ : KeyVerificationAcceptEvent(basicJson(
+ TypeId, { { "transaction_id"_ls, transactionId },
+ { "method"_ls, SasV1Method },
+ { "key_agreement_protocol"_ls, "curve25519-hkdf-sha256" },
+ { "hash"_ls, "sha256" },
+ { "message_authentication_code"_ls, "hkdf-hmac-sha256" },
+ { "short_authentication_string"_ls,
+ QJsonArray{ "decimal"_ls, "emoji"_ls, } },
+ { "commitment"_ls, commitment } }))
+ {}
+
+ /// The verification method to use. Must be 'm.sas.v1'.
+ QUO_CONTENT_GETTER(QString, method)
+
+ /// The key agreement protocol the device is choosing to use, out of
+ /// the options in the m.key.verification.start message.
+ QUO_CONTENT_GETTER(QString, keyAgreementProtocol)
+
+ /// The hash method the device is choosing to use, out of the
+ /// options in the m.key.verification.start message.
+ QUO_CONTENT_GETTER_X(QString, hashData, "hash"_ls)
+
+ /// The message authentication code the device is choosing to use, out
+ /// of the options in the m.key.verification.start message.
+ QUO_CONTENT_GETTER(QString, messageAuthenticationCode)
+
+ /// The SAS methods both devices involved in the verification process understand.
+ QUO_CONTENT_GETTER(QStringList, shortAuthenticationString)
+
+ /// The hash (encoded as unpadded base64) of the concatenation of the
+ /// device's ephemeral public key (encoded as unpadded base64) and the
+ /// canonical JSON representation of the m.key.verification.start message.
+ QUO_CONTENT_GETTER(QString, commitment)
+};
+
+class QUOTIENT_API KeyVerificationCancelEvent : public KeyVerificationEvent {
+public:
+ QUO_EVENT(KeyVerificationCancelEvent, "m.key.verification.cancel")
+
+ using KeyVerificationEvent::KeyVerificationEvent;
+ KeyVerificationCancelEvent(const QString& transactionId,
+ const QString& reason)
+ : KeyVerificationCancelEvent(
+ basicJson(TypeId, {
+ { "transaction_id"_ls, transactionId },
+ { "reason"_ls, reason },
+ { "code"_ls, reason } // Not a typo
+ }))
+ {}
+
+ /// A human readable description of the code. The client should only
+ /// rely on this string if it does not understand the code.
+ QUO_CONTENT_GETTER(QString, reason)
+
+ /// The error code for why the process/request was cancelled by the user.
+ QUO_CONTENT_GETTER(QString, code)
+};
+
+/// Sends the ephemeral public key for a device to the partner device.
+/// Typically sent as a to-device event.
+class QUOTIENT_API KeyVerificationKeyEvent : public KeyVerificationEvent {
+public:
+ QUO_EVENT(KeyVerificationKeyEvent, "m.key.verification.key")
+
+ using KeyVerificationEvent::KeyVerificationEvent;
+ KeyVerificationKeyEvent(const QString& transactionId, const QString& key)
+ : KeyVerificationKeyEvent(
+ basicJson(TypeId, { { "transaction_id"_ls, transactionId },
+ { "key"_ls, key } }))
+ {}
+
+ /// The device's ephemeral public key, encoded as unpadded base64.
+ QUO_CONTENT_GETTER(QString, key)
+};
+
+/// Sends the MAC of a device's key to the partner device.
+class QUOTIENT_API KeyVerificationMacEvent : public KeyVerificationEvent {
+public:
+ QUO_EVENT(KeyVerificationMacEvent, "m.key.verification.mac")
+
+ using KeyVerificationEvent::KeyVerificationEvent;
+ KeyVerificationMacEvent(const QString& transactionId, const QString& keys,
+ const QJsonObject& mac)
+ : KeyVerificationMacEvent(
+ basicJson(TypeId, { { "transaction_id"_ls, transactionId },
+ { "keys"_ls, keys },
+ { "mac"_ls, mac } }))
+ {}
+
+ /// The device's ephemeral public key, encoded as unpadded base64.
+ QUO_CONTENT_GETTER(QString, keys)
+
+ QHash<QString, QString> mac() const
+ {
+ return contentPart<QHash<QString, QString>>("mac"_ls);
+ }
+};
+
+class QUOTIENT_API KeyVerificationDoneEvent : public KeyVerificationEvent {
+public:
+ QUO_EVENT(KeyVerificationDoneEvent, "m.key.verification.done")
+
+ using KeyVerificationEvent::KeyVerificationEvent;
+ explicit KeyVerificationDoneEvent(const QString& transactionId)
+ : KeyVerificationDoneEvent(
+ basicJson(TypeId, { { "transaction_id"_ls, transactionId } }))
+ {}
+};
+} // namespace Quotient
diff --git a/lib/events/reactionevent.cpp b/lib/events/reactionevent.cpp
deleted file mode 100644
index 003c8ead..00000000
--- a/lib/events/reactionevent.cpp
+++ /dev/null
@@ -1,44 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2019 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#include "reactionevent.h"
-
-using namespace Quotient;
-
-void JsonObjectConverter<EventRelation>::dumpTo(
- QJsonObject& jo, const EventRelation& pod)
-{
- if (pod.type.isEmpty()) {
- qCWarning(MAIN) << "Empty relation type; won't dump to JSON";
- return;
- }
- jo.insert(QStringLiteral("rel_type"), pod.type);
- jo.insert(EventIdKey, pod.eventId);
- if (pod.type == EventRelation::Annotation())
- jo.insert(QStringLiteral("key"), pod.key);
-}
-
-void JsonObjectConverter<EventRelation>::fillFrom(
- const QJsonObject& jo, EventRelation& pod)
-{
- // The experimental logic for generic relationships (MSC1849)
- fromJson(jo["rel_type"_ls], pod.type);
- fromJson(jo[EventIdKeyL], pod.eventId);
- if (pod.type == EventRelation::Annotation())
- fromJson(jo["key"_ls], pod.key);
-}
diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h
index 75c6528c..8d873441 100644
--- a/lib/events/reactionevent.h
+++ b/lib/events/reactionevent.h
@@ -1,73 +1,14 @@
-/******************************************************************************
- * Copyright (C) 2019 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "roomevent.h"
+#include "eventrelation.h"
namespace Quotient {
-struct EventRelation {
- using reltypeid_t = const char*;
- static constexpr reltypeid_t Reply() { return "m.in_reply_to"; }
- static constexpr reltypeid_t Annotation() { return "m.annotation"; }
- static constexpr reltypeid_t Replacement() { return "m.replace"; }
-
- QString type;
- QString eventId;
- QString key = {}; // Only used for m.annotation for now
-
- static EventRelation replyTo(QString eventId)
- {
- return { Reply(), std::move(eventId) };
- }
- static EventRelation annotate(QString eventId, QString key)
- {
- return { Annotation(), std::move(eventId), std::move(key) };
- }
- static EventRelation replace(QString eventId)
- {
- return { Replacement(), std::move(eventId) };
- }
-};
-template <>
-struct JsonObjectConverter<EventRelation> {
- static void dumpTo(QJsonObject& jo, const EventRelation& pod);
- static void fillFrom(const QJsonObject& jo, EventRelation& pod);
-};
-
-class ReactionEvent : public RoomEvent {
-public:
- DEFINE_EVENT_TYPEID("m.reaction", ReactionEvent)
-
- explicit ReactionEvent(const EventRelation& value)
- : RoomEvent(typeId(), matrixTypeId(),
- { { QStringLiteral("m.relates_to"), toJson(value) } })
- {}
- explicit ReactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) {}
- EventRelation relation() const
- {
- return content<EventRelation>(QStringLiteral("m.relates_to"));
- }
-
-private:
- EventRelation _relation;
-};
-REGISTER_EVENT_TYPE(ReactionEvent)
+DEFINE_SIMPLE_EVENT(ReactionEvent, RoomEvent, "m.reaction", EventRelation,
+ relation, "m.relates_to")
} // namespace Quotient
diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp
index bf050cb2..d8f9fa0b 100644
--- a/lib/events/receiptevent.cpp
+++ b/lib/events/receiptevent.cpp
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
/*
Example of a Receipt Event:
@@ -35,31 +20,49 @@ Example of a Receipt Event:
#include "receiptevent.h"
-#include "converters.h"
#include "logging.h"
using namespace Quotient;
-ReceiptEvent::ReceiptEvent(const QJsonObject& obj) : Event(typeId(), obj)
+// 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 Quotient::toJson(const EventsWithReceipts& ewrs)
{
- const auto& contents = contentJson();
- _eventsWithReceipts.reserve(contents.size());
- for (auto eventIt = contents.begin(); eventIt != contents.end(); ++eventIt) {
+ 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;
+}
+
+template<>
+EventsWithReceipts Quotient::fromJson(const QJsonObject& json)
+{
+ EventsWithReceipts result;
+ result.reserve(json.size());
+ for (auto eventIt = json.begin(); eventIt != json.end(); ++eventIt) {
if (eventIt.key().isEmpty()) {
qCWarning(EPHEMERAL)
<< "ReceiptEvent has an empty event id, skipping";
- qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << contents;
+ qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << json;
continue;
}
- const QJsonObject reads =
+ 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 QJsonObject user = userIt.value().toObject();
- receipts.push_back(
+ const auto user = userIt.value().toObject();
+ usersAtEvent.push_back(
{ userIt.key(), fromJson<QDateTime>(user["ts"_ls]) });
}
- _eventsWithReceipts.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 dd54a476..b87e00f6 100644
--- a/lib/events/receiptevent.h
+++ b/lib/events/receiptevent.h
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
@@ -24,28 +9,27 @@
#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 QJsonObject& obj);
+template <>
+QUOTIENT_API EventsWithReceipts fromJson(const QJsonObject& json);
+QUOTIENT_API QJsonObject toJson(const EventsWithReceipts& ewrs);
- const EventsWithReceipts& eventsWithReceipts() const
- {
- return _eventsWithReceipts;
- }
+class QUOTIENT_API ReceiptEvent
+ : public EventTemplate<ReceiptEvent, Event, EventsWithReceipts> {
+public:
+ QUO_EVENT(ReceiptEvent, "m.receipt")
+ using EventTemplate::EventTemplate;
-private:
- EventsWithReceipts _eventsWithReceipts;
+ [[deprecated("Use content() instead")]]
+ EventsWithReceipts eventsWithReceipts() const { return content(); }
};
-REGISTER_EVENT_TYPE(ReceiptEvent)
} // namespace Quotient
diff --git a/lib/events/redactionevent.cpp b/lib/events/redactionevent.cpp
deleted file mode 100644
index bf467718..00000000
--- a/lib/events/redactionevent.cpp
+++ /dev/null
@@ -1 +0,0 @@
-#include "redactionevent.h"
diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h
index 3b3af18e..a2e0b73b 100644
--- a/lib/events/redactionevent.h
+++ b/lib/events/redactionevent.h
@@ -1,38 +1,21 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "roomevent.h"
namespace Quotient {
-class RedactionEvent : public RoomEvent {
+class QUOTIENT_API RedactionEvent : public RoomEvent {
public:
- DEFINE_EVENT_TYPEID("m.room.redaction", RedactionEvent)
+ QUO_EVENT(RedactionEvent, "m.room.redaction")
- explicit RedactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj)
- {}
+ using RoomEvent::RoomEvent;
QString redactedEvent() const
{
return fullJson()["redacts"_ls].toString();
}
- QString reason() const { return contentJson()["reason"_ls].toString(); }
+ QUO_CONTENT_GETTER(QString, reason)
};
-REGISTER_EVENT_TYPE(RedactionEvent)
} // namespace Quotient
diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h
index c2100eaa..1986f852 100644
--- a/lib/events/roomavatarevent.h
+++ b/lib/events/roomavatarevent.h
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
@@ -22,28 +7,17 @@
#include "stateevent.h"
namespace Quotient {
-class RoomAvatarEvent : public StateEvent<EventContent::ImageContent> {
+class QUOTIENT_API RoomAvatarEvent
+ : public KeylessStateEventBase<RoomAvatarEvent,
+ EventContent::ImageContent> {
// It's a bit of an overkill to use a full-fledged ImageContent
// because in reality m.room.avatar usually only has a single URL,
// without a thumbnail. But The Spec says there be thumbnails, and
- // we follow The Spec.
+ // we follow The Spec (and ImageContent is very convenient to reuse here).
public:
- DEFINE_EVENT_TYPEID("m.room.avatar", RoomAvatarEvent)
- explicit RoomAvatarEvent(const QJsonObject& obj) : StateEvent(typeId(), obj)
- {}
- explicit RoomAvatarEvent(const EventContent::ImageContent& avatar)
- : StateEvent(typeId(), matrixTypeId(), QString(), avatar)
- {}
- // A replica of EventContent::ImageInfo constructor
- explicit RoomAvatarEvent(const QUrl& u, qint64 fileSize = -1,
- QMimeType mimeType = {},
- const QSize& imageSize = {},
- const QString& originalFilename = {})
- : RoomAvatarEvent(EventContent::ImageContent {
- u, fileSize, mimeType, imageSize, originalFilename })
- {}
+ QUO_EVENT(RoomAvatarEvent, "m.room.avatar")
+ using KeylessStateEventBase::KeylessStateEventBase;
- QUrl url() const { return content().url; }
+ QUrl url() const { return content().url(); }
};
-REGISTER_EVENT_TYPE(RoomAvatarEvent)
} // namespace Quotient
diff --git a/lib/events/roomcanonicalaliasevent.h b/lib/events/roomcanonicalaliasevent.h
index fadfece0..c73bc92a 100644
--- a/lib/events/roomcanonicalaliasevent.h
+++ b/lib/events/roomcanonicalaliasevent.h
@@ -1,78 +1,44 @@
-/******************************************************************************
- * Copyright (C) 2020 QMatrixClient project
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2020 Ram Nad <ramnad1999@gmail.com>
+// SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "stateevent.h"
namespace Quotient {
-namespace EventContent{
- class AliasesEventContent {
-
- public:
-
- template<typename T1, typename T2>
- AliasesEventContent(T1&& canonicalAlias, T2&& altAliases)
- : canonicalAlias(std::forward<T1>(canonicalAlias))
- , altAliases(std::forward<T2>(altAliases))
- { }
-
- AliasesEventContent(const QJsonObject& json)
- : canonicalAlias(fromJson<QString>(json["alias"]))
- , altAliases(fromJson<QStringList>(json["alt_aliases"]))
- { }
-
- auto toJson() const
- {
- QJsonObject jo;
- addParam<IfNotEmpty>(jo, QStringLiteral("alias"), canonicalAlias);
- addParam<IfNotEmpty>(jo, QStringLiteral("alt_aliases"), altAliases);
- return jo;
- }
-
+namespace EventContent {
+ struct AliasesEventContent {
QString canonicalAlias;
QStringList altAliases;
};
} // namespace EventContent
-class RoomCanonicalAliasEvent
- : public StateEvent<EventContent::AliasesEventContent> {
+template<>
+inline EventContent::AliasesEventContent fromJson(const QJsonObject& jo)
+{
+ return EventContent::AliasesEventContent {
+ fromJson<QString>(jo["alias"_ls]),
+ fromJson<QStringList>(jo["alt_aliases"_ls])
+ };
+}
+template<>
+inline auto toJson(const EventContent::AliasesEventContent& c)
+{
+ QJsonObject jo;
+ addParam<IfNotEmpty>(jo, QStringLiteral("alias"), c.canonicalAlias);
+ addParam<IfNotEmpty>(jo, QStringLiteral("alt_aliases"), c.altAliases);
+ return jo;
+}
+
+class QUOTIENT_API RoomCanonicalAliasEvent
+ : public KeylessStateEventBase<RoomCanonicalAliasEvent,
+ EventContent::AliasesEventContent> {
public:
- DEFINE_EVENT_TYPEID("m.room.canonical_alias", RoomCanonicalAliasEvent)
-
- explicit RoomCanonicalAliasEvent(const QJsonObject& obj)
- : StateEvent(typeId(), obj)
- { }
-
- explicit RoomCanonicalAliasEvent(const QString& canonicalAlias,
- const QStringList& altAliases = {})
- : StateEvent(typeId(), matrixTypeId(), QString(),
- canonicalAlias, altAliases)
- { }
-
- explicit RoomCanonicalAliasEvent(QString&& canonicalAlias,
- QStringList&& altAliases = {})
- : StateEvent(typeId(), matrixTypeId(), QString(),
- std::move(canonicalAlias), std::move(altAliases))
- { }
+ QUO_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias")
+ using KeylessStateEventBase::KeylessStateEventBase;
QString alias() const { return content().canonicalAlias; }
QStringList altAliases() const { return content().altAliases; }
};
-REGISTER_EVENT_TYPE(RoomCanonicalAliasEvent)
} // namespace Quotient
diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp
index c72b5bc2..3b5024d5 100644
--- a/lib/events/roomcreateevent.cpp
+++ b/lib/events/roomcreateevent.cpp
@@ -1,43 +1,40 @@
-/******************************************************************************
- * Copyright (C) 2019 QMatrixClient project
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "roomcreateevent.h"
using namespace Quotient;
+template <>
+RoomType Quotient::fromJson(const QJsonValue& jv)
+{
+ return enumFromJsonString(jv.toString(), RoomTypeStrings,
+ 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();
- return { fromJson<QString>(predJson["room_id"_ls]),
- fromJson<QString>(predJson["event_id"_ls]) };
+ const auto predJson = contentPart<QJsonObject>("predecessor"_ls);
+ return { fromJson<QString>(predJson[RoomIdKeyL]),
+ fromJson<QString>(predJson[EventIdKeyL]) };
}
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 91aefe9e..5968e187 100644
--- a/lib/events/roomcreateevent.h
+++ b/lib/events/roomcreateevent.h
@@ -1,34 +1,17 @@
-/******************************************************************************
- * Copyright (C) 2019 QMatrixClient project
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "stateevent.h"
+#include "quotient_common.h"
namespace Quotient {
-class RoomCreateEvent : public StateEventBase {
+class QUOTIENT_API RoomCreateEvent : public StateEvent {
public:
- DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent)
+ QUO_EVENT(RoomCreateEvent, "m.room.create")
- explicit RoomCreateEvent() : StateEventBase(typeId(), matrixTypeId()) {}
- explicit RoomCreateEvent(const QJsonObject& obj)
- : StateEventBase(typeId(), obj)
- {}
+ using StateEvent::StateEvent;
struct Predecessor {
QString roomId;
@@ -39,6 +22,6 @@ 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 a59cd6e0..e98cb591 100644
--- a/lib/events/roomevent.cpp
+++ b/lib/events/roomevent.cpp
@@ -1,43 +1,18 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "roomevent.h"
-#include "converters.h"
#include "logging.h"
#include "redactionevent.h"
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)
-{}
-
-RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json)
+RoomEvent::RoomEvent(const QJsonObject& json) : Event(json)
{
- const auto unsignedData = json[UnsignedKeyL].toObject();
- const auto redaction = unsignedData[RedactedCauseKeyL];
- if (redaction.isObject())
- _redactedBecause = makeEvent<RedactionEvent>(redaction.toObject());
+ if (const auto redaction = unsignedPart<QJsonObject>(RedactedCauseKeyL);
+ !redaction.isEmpty())
+ _redactedBecause = loadEvent<RedactionEvent>(redaction);
}
RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job
@@ -51,24 +26,24 @@ QDateTime RoomEvent::originTimestamp() const
QString RoomEvent::roomId() const
{
- return fullJson()["room_id"_ls].toString();
+ return fullJson()[RoomIdKeyL].toString();
}
QString RoomEvent::senderId() const
{
- return fullJson()["sender"_ls].toString();
+ return fullJson()[SenderKeyL].toString();
}
bool RoomEvent::isReplaced() const
{
- 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
}
@@ -80,7 +55,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
@@ -90,12 +65,12 @@ QString RoomEvent::stateKey() const
void RoomEvent::setRoomId(const QString& roomId)
{
- editJson().insert(QStringLiteral("room_id"), roomId);
+ editJson().insert(RoomIdKey, roomId);
}
void RoomEvent::setSender(const QString& senderId)
{
- editJson().insert(QStringLiteral("sender"), senderId);
+ editJson().insert(SenderKey, senderId);
}
void RoomEvent::setTransactionId(const QString& txnId)
@@ -115,24 +90,23 @@ void RoomEvent::addId(const QString& newId)
Q_ASSERT(id() == newId);
}
-QJsonObject makeCallContentJson(const QString& callId, int version,
- QJsonObject content)
+void RoomEvent::dumpTo(QDebug dbg) const
{
- content.insert(QStringLiteral("call_id"), callId);
- content.insert(QStringLiteral("version"), version);
- return content;
+ Event::dumpTo(dbg);
+ dbg << " (made at " << originTimestamp().toString(Qt::ISODate) << ')';
}
-CallEventBase::CallEventBase(Type type, event_mtype_t matrixType,
- const QString& callId, int version,
- const QJsonObject& contentJson)
- : RoomEvent(type, matrixType,
- makeCallContentJson(callId, version, contentJson))
-{}
+#ifdef Quotient_E2EE_ENABLED
+void RoomEvent::setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent)
+{
+ _originalEvent = std::move(originalEvent);
+}
-CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json)
- : RoomEvent(type, json)
+const QJsonObject RoomEvent::encryptedJson() const
{
- if (callId().isEmpty())
- qCWarning(EVENTS) << id() << "is a call event with an empty call id";
+ if(!_originalEvent) {
+ return {};
+ }
+ return _originalEvent->fullJson();
}
+#endif
diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h
index 621652cb..203434f6 100644
--- a/lib/events/roomevent.h
+++ b/lib/events/roomevent.h
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
@@ -25,33 +10,22 @@
namespace Quotient {
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)
+// That check could look into Event and find most stuff already deleted...
+// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions)
+class QUOTIENT_API RoomEvent : public Event {
public:
- using factory_t = EventFactory<RoomEvent>;
+ QUO_BASE_EVENT(RoomEvent, {}, Event::BaseMetaType)
- // RedactionEvent is an incomplete type here so we cannot inline
- // constructors and destructors and we cannot use 'using'.
- RoomEvent(Type type, event_mtype_t matrixType,
- const QJsonObject& contentJson = {});
- RoomEvent(Type type, const QJsonObject& json);
- ~RoomEvent() override;
+ ~RoomEvent() override; // Don't inline this - see the private section
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); }
@@ -63,48 +37,48 @@ 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);
+#ifdef Quotient_E2EE_ENABLED
+ void setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent);
+ const RoomEvent* originalEvent() const { return _originalEvent.get(); }
+ const QJsonObject encryptedJson() const;
+#endif
+
+protected:
+ explicit RoomEvent(const QJsonObject& json);
+ void dumpTo(QDebug dbg) const override;
+
private:
+ // RedactionEvent is an incomplete type here so we cannot inline
+ // constructors using it and also destructors (with 'using', in particular).
event_ptr_tt<RedactionEvent> _redactedBecause;
+
+#ifdef Quotient_E2EE_ENABLED
+ event_ptr_tt<RoomEvent> _originalEvent;
+#endif
};
using RoomEventPtr = event_ptr_tt<RoomEvent>;
using RoomEvents = EventsArray<RoomEvent>;
using RoomEventsRange = Range<RoomEvents>;
-class CallEventBase : public RoomEvent {
-public:
- CallEventBase(Type type, event_mtype_t matrixType, const QString& callId,
- int version, const QJsonObject& contentJson = {});
- CallEventBase(Type type, const QJsonObject& json);
- ~CallEventBase() override = default;
- bool isCallEvent() const override { return true; }
-
- QString callId() const { return content<QString>("call_id"_ls); }
- int version() const { return content<int>("version"_ls); }
-};
} // namespace Quotient
Q_DECLARE_METATYPE(Quotient::RoomEvent*)
Q_DECLARE_METATYPE(const Quotient::RoomEvent*)
diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp
deleted file mode 100644
index 66580430..00000000
--- a/lib/events/roomkeyevent.cpp
+++ /dev/null
@@ -1,9 +0,0 @@
-#include "roomkeyevent.h"
-
-using namespace Quotient;
-
-RoomKeyEvent::RoomKeyEvent(const QJsonObject &obj) : Event(typeId(), obj)
-{
- if (roomId().isEmpty())
- qCWarning(E2EE) << "Room key event has empty room id";
-}
diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h
index 679cbf7c..dad5df8b 100644
--- a/lib/events/roomkeyevent.h
+++ b/lib/events/roomkeyevent.h
@@ -1,19 +1,33 @@
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#pragma once
#include "event.h"
namespace Quotient {
-class RoomKeyEvent : public Event
+class QUOTIENT_API RoomKeyEvent : public Event
{
public:
- DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent)
+ QUO_EVENT(RoomKeyEvent, "m.room_key")
- RoomKeyEvent(const QJsonObject& obj);
+ using Event::Event;
+ explicit RoomKeyEvent(const QString& algorithm, const QString& roomId,
+ const QString& sessionId, const QString& sessionKey)
+ : Event(basicJson(TypeId, {
+ { "algorithm", algorithm },
+ { "room_id", roomId },
+ { "session_id", sessionId },
+ { "session_key", sessionKey },
+ }))
+ {}
- QString algorithm() const { return content<QString>("algorithm"_ls); }
- QString roomId() const { return content<QString>("room_id"_ls); }
- QString sessionId() const { return content<QString>("session_id"_ls); }
- QString sessionKey() const { return content<QString>("session_key"_ls); }
+ QUO_CONTENT_GETTER(QString, algorithm)
+ QUO_CONTENT_GETTER(QString, roomId)
+ QUO_CONTENT_GETTER(QString, sessionId)
+ QByteArray sessionKey() const
+ {
+ return contentPart<QString>("session_key"_ls).toLatin1();
+ }
};
-REGISTER_EVENT_TYPE(RoomKeyEvent)
} // namespace Quotient
diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp
index 3193a54d..4e7eae1b 100644
--- a/lib/events/roommemberevent.cpp
+++ b/lib/events/roommemberevent.cpp
@@ -1,47 +1,20 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2019 Karol Kosek <krkkx@protonmail.com>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "roommemberevent.h"
-
-#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") }
-};
-
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 (const auto& ms = jv.toString(); !ms.isEmpty())
+ return flagFromJsonString<Membership>(ms, MembershipStrings);
- if (!membershipString.isEmpty())
- qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString;
- return MembershipType::Undefined;
+ qCWarning(EVENTS) << "Empty membership state";
+ return Membership::Invalid;
}
};
} // namespace Quotient
@@ -49,25 +22,29 @@ 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(sanitized(json["displayname"_ls].toString()))
- , avatarUrl(json["avatar_url"_ls].toString())
+ , displayName(fromJson<Omittable<QString>>(json["displayname"_ls]))
+ , avatarUrl(fromJson<Omittable<QString>>(json["avatar_url"_ls]))
, reason(json["reason"_ls].toString())
-{}
+{
+ if (displayName)
+ displayName = sanitized(*displayName);
+}
-void MemberEventContent::fillJson(QJsonObject* o) const
+QJsonObject MemberEventContent::toJson() 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]);
- o->insert(QStringLiteral("displayname"), displayName);
- if (avatarUrl.isValid())
- o->insert(QStringLiteral("avatar_url"), avatarUrl.toString());
+ QJsonObject o;
+ if (membership != Membership::Invalid)
+ o.insert(QStringLiteral("membership"),
+ flagToJsonString(membership, MembershipStrings));
+ if (displayName)
+ o.insert(QStringLiteral("displayname"), *displayName);
+ if (avatarUrl && avatarUrl->isValid())
+ o.insert(QStringLiteral("avatar_url"), avatarUrl->toString());
if (!reason.isEmpty())
- o->insert(QStringLiteral("reason"), reason);
+ o.insert(QStringLiteral("reason"), reason);
+ return o;
}
bool RoomMemberEvent::changesMembership() const
@@ -77,47 +54,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 : QString();
- return displayName() != prevName;
+ return prevContent() && prevContent()->displayName
+ ? newDisplayName() != *prevContent()->displayName
+ : newDisplayName().has_value();
}
bool RoomMemberEvent::isAvatarUpdate() const
{
- auto prevAvatarUrl = prevContent() ? prevContent()->avatarUrl : QUrl();
- return avatarUrl() != prevAvatarUrl;
+ return prevContent() && prevContent()->avatarUrl
+ ? newAvatarUrl() != *prevContent()->avatarUrl
+ : newAvatarUrl().has_value();
}
diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h
index 783b8207..9f063136 100644
--- a/lib/events/roommemberevent.h
+++ b/lib/events/roommemberevent.h
@@ -1,91 +1,57 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2019 Karol Kosek <krkkx@protonmail.com>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
-#include "eventcontent.h"
#include "stateevent.h"
+#include "quotient_common.h"
namespace Quotient {
-class MemberEventContent : public EventContent::Base {
+class QUOTIENT_API MemberEventContent {
public:
- enum MembershipType : size_t {
- Invite = 0,
- Join,
- Knock,
- Leave,
- Ban,
- Undefined
- };
+ using MembershipType
+ [[deprecated("Use Quotient::Membership instead")]] = Membership;
- explicit MemberEventContent(MembershipType mt = Join) : membership(mt) {}
+ QUO_IMPLICIT MemberEventContent(Membership ms) : membership(ms) {}
explicit MemberEventContent(const QJsonObject& json);
+ QJsonObject toJson() const;
- MembershipType membership;
+ Membership membership;
+ /// (Only for invites) Whether the invite is to a direct chat
bool isDirect = false;
- QString displayName;
- QUrl avatarUrl;
+ Omittable<QString> displayName;
+ Omittable<QUrl> avatarUrl;
QString reason;
-
-protected:
- void fillJson(QJsonObject* o) const override;
};
-using MembershipType = MemberEventContent::MembershipType;
+using MembershipType [[deprecated("Use Membership instead")]] = Membership;
-class RoomMemberEvent : public StateEvent<MemberEventContent> {
+class QUOTIENT_API RoomMemberEvent
+ : public KeyedStateEventBase<RoomMemberEvent, MemberEventContent> {
Q_GADGET
public:
- DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent)
-
- using MembershipType = MemberEventContent::MembershipType;
+ QUO_EVENT(RoomMemberEvent, "m.room.member")
- explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj)
- {}
- [[deprecated("Use RoomMemberEvent(userId, contentArgs) instead")]]
- RoomMemberEvent(MemberEventContent&& c)
- : StateEvent(typeId(), matrixTypeId(), QString(), c)
- {}
- template <typename... ArgTs>
- RoomMemberEvent(const QString& userId, ArgTs&&... contentArgs)
- : StateEvent(typeId(), matrixTypeId(), userId,
- std::forward<ArgTs>(contentArgs)...)
- {}
+ using MembershipType
+ [[deprecated("Use Quotient::Membership instead")]] = Membership;
- /// A special constructor to create unknown RoomMemberEvents
- /**
- * This is needed in order to use RoomMemberEvent as a "base event
- * class" in cases like GetMembersByRoomJob when RoomMemberEvents
- * (rather than RoomEvents or StateEvents) are resolved from JSON.
- * For such cases loadEvent<> requires an underlying class to be
- * constructible with unknownTypeId() instead of its genuine id.
- * Don't use it directly.
- * \sa GetMembersByRoomJob, loadEvent, unknownTypeId
- */
- RoomMemberEvent(Type type, const QJsonObject& fullJson)
- : StateEvent(type, fullJson)
- {}
+ using KeyedStateEventBase::KeyedStateEventBase;
- MembershipType membership() const { return content().membership; }
- QString userId() const { return fullJson()[StateKeyKeyL].toString(); }
+ Membership membership() const { return content().membership; }
+ QString userId() const { return stateKey(); }
bool isDirect() const { return content().isDirect; }
- QString displayName() const { return content().displayName; }
- QUrl avatarUrl() const { return content().avatarUrl; }
+ Omittable<QString> newDisplayName() const { return content().displayName; }
+ Omittable<QUrl> newAvatarUrl() const { return content().avatarUrl; }
+ [[deprecated("Use newDisplayName() instead")]] QString displayName() const
+ {
+ return newDisplayName().value_or(QString());
+ }
+ [[deprecated("Use newAvatarUrl() instead")]] QUrl avatarUrl() const
+ {
+ return newAvatarUrl().value_or(QUrl());
+ }
QString reason() const { return content().reason; }
bool changesMembership() const;
bool isBan() const;
@@ -96,20 +62,5 @@ public:
bool isLeave() const;
bool isRename() const;
bool isAvatarUpdate() const;
-
-private:
- Q_ENUM(MembershipType)
};
-
-template <>
-class EventFactory<RoomMemberEvent> {
-public:
- static event_ptr_tt<RoomMemberEvent> make(const QJsonObject& json,
- const QString&)
- {
- return makeEvent<RoomMemberEvent>(json);
- }
-};
-
-REGISTER_EVENT_TYPE(RoomMemberEvent)
} // namespace Quotient
diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp
index de499e7c..df4840b3 100644
--- a/lib/events/roommessageevent.cpp
+++ b/lib/events/roommessageevent.cpp
@@ -1,44 +1,33 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "roommessageevent.h"
#include "logging.h"
+#include "events/eventrelation.h"
#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;
using MsgType = RoomMessageEvent::MsgType;
-static const auto RelatesToKeyL = "m.relates_to"_ls;
-static const auto MsgTypeKeyL = "msgtype"_ls;
-static const auto FormattedBodyKeyL = "formatted_body"_ls;
-
-static const auto TextTypeKey = "m.text";
-static const auto EmoteTypeKey = "m.emote";
-static const auto NoticeTypeKey = "m.notice";
-
-static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html");
+namespace { // Supporting internal definitions
+constexpr auto RelatesToKey = "m.relates_to"_ls;
+constexpr auto MsgTypeKey = "msgtype"_ls;
+constexpr auto FormattedBodyKey = "formatted_body"_ls;
+constexpr auto TextTypeKey = "m.text"_ls;
+constexpr auto EmoteTypeKey = "m.emote"_ls;
+constexpr auto NoticeTypeKey = "m.notice"_ls;
+constexpr auto HtmlContentTypeId = "org.matrix.custom.html"_ls;
template <typename ContentT>
TypedBase* make(const QJsonObject& json)
@@ -49,13 +38,13 @@ TypedBase* make(const QJsonObject& json)
template <>
TypedBase* make<TextContent>(const QJsonObject& json)
{
- return json.contains(FormattedBodyKeyL) || json.contains(RelatesToKeyL)
+ return json.contains(FormattedBodyKey) || json.contains(RelatesToKey)
? new TextContent(json)
: nullptr;
}
struct MsgTypeDesc {
- QString matrixType;
+ QLatin1String matrixType;
MsgType enumType;
TypedBase* (*maker)(const QJsonObject&);
};
@@ -64,11 +53,11 @@ const std::vector<MsgTypeDesc> msgTypes = {
{ TextTypeKey, MsgType::Text, make<TextContent> },
{ EmoteTypeKey, MsgType::Emote, make<TextContent> },
{ NoticeTypeKey, MsgType::Notice, make<TextContent> },
- { QStringLiteral("m.image"), MsgType::Image, make<ImageContent> },
- { QStringLiteral("m.file"), MsgType::File, make<FileContent> },
- { QStringLiteral("m.location"), MsgType::Location, make<LocationContent> },
- { QStringLiteral("m.video"), MsgType::Video, make<VideoContent> },
- { QStringLiteral("m.audio"), MsgType::Audio, make<AudioContent> }
+ { "m.image"_ls, MsgType::Image, make<ImageContent> },
+ { "m.file"_ls, MsgType::File, make<FileContent> },
+ { "m.location"_ls, MsgType::Location, make<LocationContent> },
+ { "m.video"_ls, MsgType::Video, make<VideoContent> },
+ { "m.audio"_ls, MsgType::Audio, make<AudioContent> }
};
QString msgTypeToJson(MsgType enumType)
@@ -95,49 +84,52 @@ MsgType jsonToMsgType(const QString& matrixType)
return MsgType::Unknown;
}
-inline bool isReplacement(const Omittable<RelatesTo>& rel)
+inline bool isReplacement(const Omittable<EventRelation>& rel)
{
- return rel && rel->type == RelatesTo::ReplacementTypeId();
+ return rel && rel->type == EventRelation::ReplacementType;
}
+} // anonymous namespace
+
QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody,
const QString& jsonMsgType,
TypedBase* content)
{
- auto json = content ? content->toJson() : QJsonObject();
- if (json.contains(RelatesToKeyL)) {
+ QJsonObject json;
+ if (content) {
+ // TODO: replace with content->fillJson(json) when it starts working
+ json = content->toJson();
if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey
&& jsonMsgType != EmoteTypeKey) {
- json.remove(RelatesToKeyL);
- qCWarning(EVENTS)
- << RelatesToKeyL << "cannot be used in" << jsonMsgType
- << "messages; the relation has been stripped off";
- } else {
- // After the above, we know for sure that the content is TextContent
- // and that its RelatesTo structure is not omitted
- auto* textContent = static_cast<const TextContent*>(content);
- Q_ASSERT(textContent && textContent->relatesTo.has_value());
- if (textContent->relatesTo->type == RelatesTo::ReplacementTypeId()) {
- auto newContentJson = json.take("m.new_content"_ls).toObject();
- newContentJson.insert(BodyKey, plainBody);
- newContentJson.insert(MsgTypeKeyL, jsonMsgType);
- json.insert(QStringLiteral("m.new_content"), newContentJson);
- json[MsgTypeKeyL] = jsonMsgType;
- json[BodyKeyL] = "* " + plainBody;
- return json;
+ if (json.contains(RelatesToKey)) {
+ json.remove(RelatesToKey);
+ qCWarning(EVENTS)
+ << RelatesToKey << "cannot be used in" << jsonMsgType
+ << "messages; the relation has been stripped off";
}
+ } else if (auto* textContent = static_cast<const TextContent*>(content);
+ textContent->relatesTo
+ && textContent->relatesTo->type
+ == EventRelation::ReplacementType) {
+ auto newContentJson = json.take("m.new_content"_ls).toObject();
+ newContentJson.insert(BodyKey, plainBody);
+ newContentJson.insert(MsgTypeKey, jsonMsgType);
+ json.insert(QStringLiteral("m.new_content"), newContentJson);
+ json[MsgTypeKey] = jsonMsgType;
+ json[BodyKeyL] = "* " + plainBody;
+ return json;
}
}
- json.insert(QStringLiteral("msgtype"), jsonMsgType);
- json.insert(QStringLiteral("body"), plainBody);
+ json.insert(MsgTypeKey, jsonMsgType);
+ json.insert(BodyKey, plainBody);
return json;
}
RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
const QString& jsonMsgType,
TypedBase* content)
- : RoomEvent(typeId(), matrixTypeId(),
- assembleContentJson(plainBody, jsonMsgType, content))
+ : RoomEvent(
+ basicJson(TypeId, assembleContentJson(plainBody, jsonMsgType, content)))
, _content(content)
{}
@@ -146,6 +138,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();
@@ -179,15 +172,16 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
: rawMsgTypeForFile(file),
contentFromFile(file, asGenericFile))
{}
+#endif
RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj)
- : RoomEvent(typeId(), obj), _content(nullptr)
+ : RoomEvent(obj), _content(nullptr)
{
if (isRedacted())
return;
const QJsonObject content = contentJson();
- if (content.contains(MsgTypeKeyL) && content.contains(BodyKeyL)) {
- auto msgtype = content[MsgTypeKeyL].toString();
+ if (content.contains(MsgTypeKey) && content.contains(BodyKeyL)) {
+ auto msgtype = content[MsgTypeKey].toString();
bool msgTypeFound = false;
for (const auto& mt : msgTypes)
if (mt.matrixType == msgtype) {
@@ -213,12 +207,12 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const
QString RoomMessageEvent::rawMsgtype() const
{
- return contentJson()[MsgTypeKeyL].toString();
+ return contentPart<QString>(MsgTypeKey);
}
QString RoomMessageEvent::plainBody() const
{
- return contentJson()[BodyKeyL].toString();
+ return contentPart<QString>(BodyKeyL);
}
QMimeType RoomMessageEvent::mimeType() const
@@ -276,7 +270,7 @@ QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi)
}
TextContent::TextContent(QString text, const QString& contentType,
- Omittable<RelatesTo> relatesTo)
+ Omittable<EventRelation> relatesTo)
: mimeType(QMimeDatabase().mimeTypeForName(contentType))
, body(std::move(text))
, relatesTo(std::move(relatesTo))
@@ -285,26 +279,8 @@ TextContent::TextContent(QString text, const QString& contentType,
mimeType = QMimeDatabase().mimeTypeForName("text/html");
}
-namespace Quotient {
-// Overload the default fromJson<> logic that defined in converters.h
-// as we want
-template <>
-Omittable<RelatesTo> fromJson(const QJsonValue& jv)
-{
- const auto jo = jv.toObject();
- if (jo.isEmpty())
- return none;
- const auto replyJson = jo.value(RelatesTo::ReplyTypeId()).toObject();
- if (!replyJson.isEmpty())
- return replyTo(fromJson<QString>(replyJson[EventIdKeyL]));
-
- return RelatesTo { jo.value("rel_type"_ls).toString(),
- jo.value(EventIdKeyL).toString() };
-}
-} // namespace Quotient
-
TextContent::TextContent(const QJsonObject& json)
- : relatesTo(fromJson<Omittable<RelatesTo>>(json[RelatesToKeyL]))
+ : relatesTo(fromJson<Omittable<EventRelation>>(json[RelatesToKey]))
{
QMimeDatabase db;
static const auto PlainTextMimeType = db.mimeTypeForName("text/plain");
@@ -317,7 +293,7 @@ TextContent::TextContent(const QJsonObject& json)
// of sending HTML messages.
if (actualJson["format"_ls].toString() == HtmlContentTypeId) {
mimeType = HtmlMimeType;
- body = actualJson[FormattedBodyKeyL].toString();
+ body = actualJson[FormattedBodyKey].toString();
} else {
// Falling back to plain text, as there's no standard way to describe
// rich text in messages.
@@ -326,29 +302,30 @@ TextContent::TextContent(const QJsonObject& json)
}
}
-void TextContent::fillJson(QJsonObject* json) const
+void TextContent::fillJson(QJsonObject &json) const
{
static const auto FormatKey = QStringLiteral("format");
- static const auto FormattedBodyKey = QStringLiteral("formatted_body");
- Q_ASSERT(json);
if (mimeType.inherits("text/html")) {
- json->insert(FormatKey, HtmlContentTypeId);
- json->insert(FormattedBodyKey, body);
+ json.insert(FormatKey, HtmlContentTypeId);
+ json.insert(FormattedBodyKey, body);
}
if (relatesTo) {
- json->insert(QStringLiteral("m.relates_to"),
- relatesTo->type == RelatesTo::ReplyTypeId() ?
- QJsonObject { { relatesTo->type, QJsonObject{ { EventIdKey, relatesTo->eventId } } } } :
- QJsonObject { { "rel_type", relatesTo->type }, { EventIdKey, relatesTo->eventId } }
- );
- if (relatesTo->type == RelatesTo::ReplacementTypeId()) {
+ json.insert(
+ QStringLiteral("m.relates_to"),
+ relatesTo->type == EventRelation::ReplyType
+ ? QJsonObject { { relatesTo->type,
+ QJsonObject {
+ { EventIdKey, relatesTo->eventId } } } }
+ : QJsonObject { { RelTypeKey, relatesTo->type },
+ { EventIdKey, relatesTo->eventId } });
+ if (relatesTo->type == EventRelation::ReplacementType) {
QJsonObject newContentJson;
if (mimeType.inherits("text/html")) {
newContentJson.insert(FormatKey, HtmlContentTypeId);
newContentJson.insert(FormattedBodyKey, body);
}
- json->insert(QStringLiteral("m.new_content"), newContentJson);
+ json.insert(QStringLiteral("m.new_content"), newContentJson);
}
}
}
@@ -369,9 +346,8 @@ QMimeType LocationContent::type() const
return QMimeDatabase().mimeTypeForData(geoUri.toLatin1());
}
-void LocationContent::fillJson(QJsonObject* o) const
+void LocationContent::fillJson(QJsonObject& o) const
{
- Q_ASSERT(o);
- o->insert(QStringLiteral("geo_uri"), geoUri);
- o->insert(QStringLiteral("info"), toInfoJson(thumbnail));
+ o.insert(QStringLiteral("geo_uri"), geoUri);
+ o.insert(QStringLiteral("info"), toInfoJson(thumbnail));
}
diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h
index 2501d097..889fc4dc 100644
--- a/lib/events/roommessageevent.h
+++ b/lib/events/roommessageevent.h
@@ -1,24 +1,12 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "eventcontent.h"
+#include "eventrelation.h"
#include "roomevent.h"
class QFileInfo;
@@ -29,14 +17,10 @@ namespace MessageEventContent = EventContent; // Back-compatibility
/**
* The event class corresponding to m.room.message events
*/
-class RoomMessageEvent : public RoomEvent {
+class QUOTIENT_API RoomMessageEvent : public RoomEvent {
Q_GADGET
- 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)
+ QUO_EVENT(RoomMessageEvent, "m.room.message")
enum class MsgType {
Text,
@@ -55,8 +39,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;
@@ -71,9 +59,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);
@@ -89,47 +94,49 @@ private:
Q_ENUM(MsgType)
};
-REGISTER_EVENT_TYPE(RoomMessageEvent)
+
using MessageEventType = RoomMessageEvent::MsgType;
namespace EventContent {
- // Additional event content types
- struct RelatesTo {
- static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; }
- static constexpr const char* ReplacementTypeId() { return "m.replace"; }
- QString type; // The only supported relation so far
- QString eventId;
+ struct [[deprecated("Use Quotient::EventRelation instead")]] RelatesTo
+ : EventRelation {
+ static constexpr auto ReplyTypeId() { return ReplyType; }
+ static constexpr auto ReplacementTypeId() { return ReplacementType; }
};
- inline RelatesTo replyTo(QString eventId)
+ [[deprecated("Use EventRelation::replyTo() instead")]]
+ inline auto replyTo(QString eventId)
{
- return { RelatesTo::ReplyTypeId(), std::move(eventId) };
+ return EventRelation::replyTo(std::move(eventId));
}
- inline RelatesTo replacementOf(QString eventId)
+ [[deprecated("Use EventRelation::replace() instead")]]
+ inline auto replacementOf(QString eventId)
{
- return { RelatesTo::ReplacementTypeId(), std::move(eventId) };
+ return EventRelation::replace(std::move(eventId));
}
+ // Additional event content types
+
/**
* Rich text content for m.text, m.emote, m.notice
*
* Available fields: mimeType, body. The body can be either rich text
* or plain text, depending on what mimeType specifies.
*/
- class TextContent : public TypedBase {
+ class QUOTIENT_API TextContent : public TypedBase {
public:
TextContent(QString text, const QString& contentType,
- Omittable<RelatesTo> relatesTo = none);
+ Omittable<EventRelation> relatesTo = none);
explicit TextContent(const QJsonObject& json);
QMimeType type() const override { return mimeType; }
QMimeType mimeType;
QString body;
- Omittable<RelatesTo> relatesTo;
+ Omittable<EventRelation> relatesTo;
protected:
- void fillJson(QJsonObject* json) const override;
+ void fillJson(QJsonObject& json) const override;
};
/**
@@ -145,7 +152,7 @@ namespace EventContent {
* - thumbnail.mimeType
* - thumbnail.imageSize
*/
- class LocationContent : public TypedBase {
+ class QUOTIENT_API LocationContent : public TypedBase {
public:
LocationContent(const QString& geoUri, const Thumbnail& thumbnail = {});
explicit LocationContent(const QJsonObject& json);
@@ -157,28 +164,25 @@ namespace EventContent {
Thumbnail thumbnail;
protected:
- void fillJson(QJsonObject* o) const override;
+ void fillJson(QJsonObject& o) const override;
};
/**
* A base class for info types that include duration: audio and video
*/
- template <typename ContentT>
- class PlayableContent : public ContentT {
+ template <typename InfoT>
+ class PlayableContent : public UrlBasedContent<InfoT> {
public:
- using ContentT::ContentT;
+ using UrlBasedContent<InfoT>::UrlBasedContent;
PlayableContent(const QJsonObject& json)
- : ContentT(json)
- , duration(ContentT::originalInfoJson["duration"_ls].toInt())
+ : UrlBasedContent<InfoT>(json)
+ , duration(FileInfo::originalInfoJson["duration"_ls].toInt())
{}
protected:
- void fillJson(QJsonObject* json) const override
+ void fillInfoJson(QJsonObject& infoJson) const override
{
- ContentT::fillJson(json);
- auto infoJson = json->take("info"_ls).toObject();
infoJson.insert(QStringLiteral("duration"), duration);
- json->insert(QStringLiteral("info"), infoJson);
}
public:
@@ -204,7 +208,7 @@ namespace EventContent {
* - mimeType
* - imageSize
*/
- using VideoContent = PlayableContent<UrlWithThumbnailContent<ImageInfo>>;
+ using VideoContent = PlayableContent<ImageInfo>;
/**
* Content class for m.audio
@@ -217,7 +221,13 @@ namespace EventContent {
* - payloadSize ("size" in JSON)
* - mimeType ("mimetype" in JSON)
* - duration
+ * - thumbnail.url ("thumbnail_url" in JSON - extension to the spec)
+ * - corresponding to the "info/thumbnail_info" subobject: contents of
+ * thumbnail field (extension to the spec):
+ * - payloadSize
+ * - mimeType
+ * - imageSize
*/
- using AudioContent = PlayableContent<UrlBasedContent<FileInfo>>;
+ using AudioContent = PlayableContent<FileInfo>;
} // namespace EventContent
} // namespace Quotient
diff --git a/lib/events/roompowerlevelsevent.cpp b/lib/events/roompowerlevelsevent.cpp
index 0a401752..d9bd010b 100644
--- a/lib/events/roompowerlevelsevent.cpp
+++ b/lib/events/roompowerlevelsevent.cpp
@@ -1,9 +1,12 @@
-#include "roompowerlevelsevent.h"
+// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
-#include <QJsonDocument>
+#include "roompowerlevelsevent.h"
using namespace Quotient;
+// The default values used below are defined in
+// https://spec.matrix.org/v1.3/client-server-api/#mroompower_levels
PowerLevelsEventContent::PowerLevelsEventContent(const QJsonObject& json) :
invite(json["invite"_ls].toInt(50)),
kick(json["kick"_ls].toInt(50)),
@@ -15,48 +18,36 @@ PowerLevelsEventContent::PowerLevelsEventContent(const QJsonObject& json) :
users(fromJson<QHash<QString, int>>(json["users"_ls])),
usersDefault(json["users_default"_ls].toInt(0)),
notifications(Notifications{json["notifications"_ls].toObject()["room"_ls].toInt(50)})
-{
-}
+{}
-void PowerLevelsEventContent::fillJson(QJsonObject* o) const {
- o->insert(QStringLiteral("invite"), invite);
- o->insert(QStringLiteral("kick"), kick);
- o->insert(QStringLiteral("ban"), ban);
- o->insert(QStringLiteral("redact"), redact);
- o->insert(QStringLiteral("events"), Quotient::toJson(events));
- o->insert(QStringLiteral("events_default"), eventsDefault);
- o->insert(QStringLiteral("state_default"), stateDefault);
- o->insert(QStringLiteral("users"), Quotient::toJson(users));
- o->insert(QStringLiteral("users_default"), usersDefault);
- o->insert(QStringLiteral("notifications"), QJsonObject{{"room", notifications.room}});
+QJsonObject PowerLevelsEventContent::toJson() const
+{
+ QJsonObject o;
+ o.insert(QStringLiteral("invite"), invite);
+ o.insert(QStringLiteral("kick"), kick);
+ o.insert(QStringLiteral("ban"), ban);
+ o.insert(QStringLiteral("redact"), redact);
+ o.insert(QStringLiteral("events"), Quotient::toJson(events));
+ o.insert(QStringLiteral("events_default"), eventsDefault);
+ o.insert(QStringLiteral("state_default"), stateDefault);
+ o.insert(QStringLiteral("users"), Quotient::toJson(users));
+ o.insert(QStringLiteral("users_default"), usersDefault);
+ o.insert(QStringLiteral("notifications"),
+ QJsonObject { { "room", notifications.room } });
+ return o;
}
-int RoomPowerLevelsEvent::powerLevelForEvent(const QString &eventId) const {
- auto e = events();
-
- if (e.contains(eventId)) {
- return e[eventId];
- }
-
- return eventsDefault();
+int RoomPowerLevelsEvent::powerLevelForEvent(const QString& eventId) const
+{
+ return events().value(eventId, eventsDefault());
}
-int RoomPowerLevelsEvent::powerLevelForState(const QString &eventId) const {
- auto e = events();
-
- if (e.contains(eventId)) {
- return e[eventId];
- }
-
- return stateDefault();
+int RoomPowerLevelsEvent::powerLevelForState(const QString& eventId) const
+{
+ return events().value(eventId, stateDefault());
}
-int RoomPowerLevelsEvent::powerLevelForUser(const QString &userId) const {
- auto u = users();
-
- if (u.contains(userId)) {
- return u[userId];
- }
-
- return usersDefault();
+int RoomPowerLevelsEvent::powerLevelForUser(const QString& userId) const
+{
+ return users().value(userId, usersDefault());
}
diff --git a/lib/events/roompowerlevelsevent.h b/lib/events/roompowerlevelsevent.h
index f0f7207f..6150980a 100644
--- a/lib/events/roompowerlevelsevent.h
+++ b/lib/events/roompowerlevelsevent.h
@@ -1,16 +1,18 @@
+// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#pragma once
-#include "eventcontent.h"
#include "stateevent.h"
namespace Quotient {
-class PowerLevelsEventContent : public EventContent::Base {
-public:
+struct QUOTIENT_API PowerLevelsEventContent {
struct Notifications {
int room;
};
explicit PowerLevelsEventContent(const QJsonObject& json);
+ QJsonObject toJson() const;
int invite;
int kick;
@@ -26,19 +28,14 @@ public:
int usersDefault;
Notifications notifications;
-
-protected:
- void fillJson(QJsonObject* o) const override;
};
-class RoomPowerLevelsEvent : public StateEvent<PowerLevelsEventContent> {
- Q_GADGET
+class QUOTIENT_API RoomPowerLevelsEvent
+ : public KeylessStateEventBase<RoomPowerLevelsEvent, PowerLevelsEventContent> {
public:
- DEFINE_EVENT_TYPEID("m.room.power_levels", RoomPowerLevelsEvent)
+ QUO_EVENT(RoomPowerLevelsEvent, "m.room.power_levels")
- explicit RoomPowerLevelsEvent(const QJsonObject& obj)
- : StateEvent(typeId(), obj)
- {}
+ using KeylessStateEventBase::KeylessStateEventBase;
int invite() const { return content().invite; }
int kick() const { return content().kick; }
@@ -58,19 +55,5 @@ public:
int powerLevelForEvent(const QString& eventId) const;
int powerLevelForState(const QString& eventId) const;
int powerLevelForUser(const QString& userId) const;
-
-private:
};
-
-template <>
-class EventFactory<RoomPowerLevelsEvent> {
-public:
- static event_ptr_tt<RoomPowerLevelsEvent> make(const QJsonObject& json,
- const QString&)
- {
- return makeEvent<RoomPowerLevelsEvent>(json);
- }
-};
-
-REGISTER_EVENT_TYPE(RoomPowerLevelsEvent)
} // namespace Quotient
diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp
index f93eb60d..2c3492d6 100644
--- a/lib/events/roomtombstoneevent.cpp
+++ b/lib/events/roomtombstoneevent.cpp
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2019 QMatrixClient project
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "roomtombstoneevent.h"
@@ -22,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/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h
index 2c2f0663..c85b4dfd 100644
--- a/lib/events/roomtombstoneevent.h
+++ b/lib/events/roomtombstoneevent.h
@@ -1,37 +1,18 @@
-/******************************************************************************
- * Copyright (C) 2019 QMatrixClient project
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "stateevent.h"
namespace Quotient {
-class RoomTombstoneEvent : public StateEventBase {
+class QUOTIENT_API RoomTombstoneEvent : public StateEvent {
public:
- DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent)
+ QUO_EVENT(RoomTombstoneEvent, "m.room.tombstone")
- explicit RoomTombstoneEvent() : StateEventBase(typeId(), matrixTypeId()) {}
- explicit RoomTombstoneEvent(const QJsonObject& obj)
- : StateEventBase(typeId(), obj)
- {}
+ using StateEvent::StateEvent;
QString serverMessage() const;
QString successorRoomId() const;
};
-REGISTER_EVENT_TYPE(RoomTombstoneEvent)
} // namespace Quotient
diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h
index cde5b0fd..2a0d3817 100644
--- a/lib/events/simplestateevents.h
+++ b/lib/events/simplestateevents.h
@@ -1,89 +1,47 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "stateevent.h"
+#include "single_key_value.h"
namespace Quotient {
-namespace EventContent {
- template <typename T>
- class SimpleContent {
- public:
- using value_type = T;
-
- // The constructor is templated to enable perfect forwarding
- template <typename TT>
- SimpleContent(QString keyName, TT&& value)
- : value(std::forward<TT>(value)), key(std::move(keyName))
- {}
- SimpleContent(const QJsonObject& json, QString keyName)
- : value(fromJson<T>(json[keyName])), key(std::move(keyName))
- {}
- QJsonObject toJson() const
- {
- return { { key, Quotient::toJson(value) } };
- }
-
- public:
- T value;
-
- protected:
- QString key;
- };
-} // namespace EventContent
-
-#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \
- class _Name : public StateEvent<EventContent::SimpleContent<_ValueType>> { \
- public: \
- using value_type = content_type::value_type; \
- DEFINE_EVENT_TYPEID(_TypeId, _Name) \
- explicit _Name() : _Name(value_type()) {} \
- template <typename T> \
- explicit _Name(T&& value) \
- : StateEvent(typeId(), matrixTypeId(), QString(), \
- QStringLiteral(#_ContentKey), std::forward<T>(value)) \
- {} \
- explicit _Name(QJsonObject obj) \
- : StateEvent(typeId(), std::move(obj), \
- QStringLiteral(#_ContentKey)) \
- {} \
- auto _ContentKey() const { return content().value; } \
- }; \
- REGISTER_EVENT_TYPE(_Name) \
- // End of macro
+#define DEFINE_SIMPLE_STATE_EVENT(Name_, TypeId_, ValueType_, ContentKey_) \
+ constexpr auto Name_##Key = #ContentKey_##_ls; \
+ class QUOTIENT_API Name_ \
+ : public KeylessStateEventBase< \
+ Name_, EventContent::SingleKeyValue<ValueType_, Name_##Key>> { \
+ public: \
+ using value_type = ValueType_; \
+ QUO_EVENT(Name_, TypeId_) \
+ using KeylessStateEventBase::KeylessStateEventBase; \
+ auto ContentKey_() const { return content().value; } \
+ }; \
+// End of macro
DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name)
DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic)
-
-class RoomAliasesEvent
- : public StateEvent<EventContent::SimpleContent<QStringList>> {
+DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages",
+ QStringList, pinnedEvents)
+
+constexpr auto RoomAliasesEventKey = "aliases"_ls;
+class QUOTIENT_API RoomAliasesEvent
+ : public KeyedStateEventBase<
+ RoomAliasesEvent,
+ EventContent::SingleKeyValue<QStringList, RoomAliasesEventKey>>
+{
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)
- {}
+ QUO_EVENT(RoomAliasesEvent, "m.room.aliases")
+ using KeyedStateEventBase::KeyedStateEventBase;
+
+ Q_DECL_DEPRECATED_X(
+ "m.room.aliases events are deprecated by the Matrix spec; use"
+ " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases")
QString server() const { return stateKey(); }
+ Q_DECL_DEPRECATED_X(
+ "m.room.aliases events are deprecated by the Matrix spec; use"
+ " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases")
QStringList aliases() const { return content().value; }
};
-REGISTER_EVENT_TYPE(RoomAliasesEvent)
} // namespace Quotient
diff --git a/lib/events/single_key_value.h b/lib/events/single_key_value.h
new file mode 100644
index 00000000..ca2bd331
--- /dev/null
+++ b/lib/events/single_key_value.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "converters.h"
+
+namespace Quotient {
+
+namespace EventContent {
+ template <typename T, const QLatin1String& KeyStr>
+ struct SingleKeyValue {
+ // NOLINTBEGIN(google-explicit-constructor): that check should learn
+ // about explicit(false)
+ QUO_IMPLICIT SingleKeyValue(const T& v = {})
+ : value { v }
+ {}
+ QUO_IMPLICIT SingleKeyValue(T&& v)
+ : value { std::move(v) }
+ {}
+ // NOLINTEND(google-explicit-constructor)
+ T value;
+ };
+} // namespace EventContent
+
+template <typename ValueT, const QLatin1String& KeyStr>
+struct JsonConverter<EventContent::SingleKeyValue<ValueT, KeyStr>> {
+ using content_type = EventContent::SingleKeyValue<ValueT, KeyStr>;
+ static content_type load(const QJsonValue& jv)
+ {
+ return { fromJson<ValueT>(jv.toObject().value(JsonKey)) };
+ }
+ static QJsonObject dump(const content_type& c)
+ {
+ return { { JsonKey, toJson(c.value) } };
+ }
+ static inline const auto JsonKey = toSnakeCase(KeyStr);
+};
+} // namespace Quotient
diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp
index 5909e8a6..72ecd5ad 100644
--- a/lib/events/stateevent.cpp
+++ b/lib/events/stateevent.cpp
@@ -1,64 +1,40 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "stateevent.h"
+#include "logging.h"
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);
- });
+StateEvent::StateEvent(const QJsonObject& json)
+ : RoomEvent(json)
+{
+ Q_ASSERT_X(json.contains(StateKeyKeyL), __FUNCTION__,
+ "Attempt to create a state event without state key");
+}
-StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType,
- const QString& stateKey,
+StateEvent::StateEvent(Event::Type type, const QString& stateKey,
const QJsonObject& contentJson)
- : RoomEvent(type, basicStateEventJson(matrixType, contentJson, stateKey))
+ : RoomEvent(basicJson(type, stateKey, contentJson))
{}
-bool StateEventBase::repeatsState() const
+bool StateEvent::repeatsState() const
{
- const auto prevContentJson = unsignedJson().value(PrevContentKeyL);
- return fullJson().value(ContentKeyL) == prevContentJson;
+ return contentJson() == unsignedPart<QJsonObject>(PrevContentKeyL);
}
-QString StateEventBase::replacedState() const
+QString StateEvent::replacedState() const
{
- return unsignedJson().value("replaces_state"_ls).toString();
+ return unsignedPart<QString>("replaces_state"_ls);
}
-void StateEventBase::dumpTo(QDebug dbg) const
+void StateEvent::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 710b4271..992ec2e2 100644
--- a/lib/events/stateevent.h
+++ b/lib/events/stateevent.h
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
@@ -22,41 +7,54 @@
namespace Quotient {
-/// Make a minimal correct Matrix state event JSON
-template <typename StrT>
-inline QJsonObject basicStateEventJson(StrT matrixType,
- const QJsonObject& content,
- const QString& stateKey = {})
-{
- return { { TypeKey, std::forward<StrT>(matrixType) },
- { StateKeyKey, stateKey },
- { ContentKey, content } };
-}
-
-class StateEventBase : public RoomEvent {
+class QUOTIENT_API StateEvent : public RoomEvent {
public:
- using factory_t = EventFactory<StateEventBase>;
+ QUO_BASE_EVENT(StateEvent, "json.contains('state_key')"_ls,
+ RoomEvent::BaseMetaType)
+ static bool isValid(const QJsonObject& fullJson)
+ {
+ return fullJson.contains(StateKeyKeyL);
+ }
- StateEventBase(Type type, const QJsonObject& json) : RoomEvent(type, json)
- {}
- StateEventBase(Type type, event_mtype_t matrixType,
- const QString& stateKey = {},
- const QJsonObject& contentJson = {});
- ~StateEventBase() override = default;
+ //! \brief Static setting of whether a given even type uses state keys
+ //!
+ //! Most event types don't use a state key; overriding this to `true`
+ //! for a given type changes the calls across Quotient to include state key
+ //! in their signatures; otherwise, state key is still accessible but
+ //! constructors and calls in, e.g., RoomStateView don't include it.
+ static constexpr auto needsStateKey = false;
+
+ explicit StateEvent(Type type, const QString& stateKey = {},
+ const QJsonObject& contentJson = {});
+
+ //! Make a minimal correct Matrix state event JSON
+ static QJsonObject basicJson(const QString& matrixTypeId,
+ const QString& stateKey = {},
+ const QJsonObject& contentJson = {})
+ {
+ return { { TypeKey, matrixTypeId },
+ { StateKeyKey, stateKey },
+ { ContentKey, contentJson } };
+ }
- bool isStateEvent() const override { return true; }
QString replacedState() const;
- void dumpTo(QDebug dbg) const override;
-
virtual bool repeatsState() const;
+
+protected:
+ explicit StateEvent(const QJsonObject& json);
+ void dumpTo(QDebug dbg) const override;
};
-using StateEventPtr = event_ptr_tt<StateEventBase>;
-using StateEvents = EventsArray<StateEventBase>;
+using StateEventBase
+ [[deprecated("StateEventBase is StateEvent now")]] = StateEvent;
+using StateEventPtr = event_ptr_tt<StateEvent>;
+using StateEvents = EventsArray<StateEvent>;
-template <>
-inline bool is<StateEventBase>(const Event& e)
+[[deprecated("Use StateEvent::basicJson() instead")]]
+inline QJsonObject basicStateEventJson(const QString& matrixTypeId,
+ const QJsonObject& content,
+ const QString& stateKey = {})
{
- return e.isStateEvent();
+ return StateEvent::basicJson(matrixTypeId, stateKey, content);
}
/**
@@ -65,67 +63,89 @@ inline bool is<StateEventBase>(const Event& e)
* \sa
* https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events
*/
-using StateEventKey = QPair<QString, QString>;
-
-template <typename ContentT>
-struct Prev {
- template <typename... ContentParamTs>
- explicit Prev(const QJsonObject& unsignedJson,
- ContentParamTs&&... contentParams)
- : senderId(unsignedJson.value("prev_sender"_ls).toString())
- , content(unsignedJson.value(PrevContentKeyL).toObject(),
- std::forward<ContentParamTs>(contentParams)...)
- {}
-
- QString senderId;
- ContentT content;
-};
+using StateEventKey = std::pair<QString, QString>;
-template <typename ContentT>
-class StateEvent : public StateEventBase {
+template <typename EventT, typename ContentT>
+class EventTemplate<EventT, StateEvent, ContentT>
+ : public StateEvent {
public:
using content_type = ContentT;
+ struct Prev {
+ explicit Prev() = default;
+ explicit Prev(const QJsonObject& unsignedJson)
+ : senderId(fromJson<QString>(unsignedJson["prev_sender"_ls]))
+ , content(
+ fromJson<Omittable<ContentT>>(unsignedJson[PrevContentKeyL]))
+ {}
+
+ QString senderId;
+ Omittable<ContentT> content;
+ };
+
+ explicit EventTemplate(const QJsonObject& fullJson)
+ : StateEvent(fullJson)
+ , _content(fromJson<ContentT>(Event::contentJson()))
+ , _prev(unsignedJson())
+ {}
template <typename... ContentParamTs>
- explicit StateEvent(Type type, const QJsonObject& fullJson,
- ContentParamTs&&... contentParams)
- : StateEventBase(type, fullJson)
- , _content(contentJson(), std::forward<ContentParamTs>(contentParams)...)
- {
- const auto& unsignedData = unsignedJson();
- if (unsignedData.contains(PrevContentKeyL))
- _prev = std::make_unique<Prev<ContentT>>(
- unsignedData, std::forward<ContentParamTs>(contentParams)...);
- }
- template <typename... ContentParamTs>
- explicit StateEvent(Type type, event_mtype_t matrixType,
- const QString& stateKey,
- ContentParamTs&&... contentParams)
- : StateEventBase(type, matrixType, stateKey)
- , _content(std::forward<ContentParamTs>(contentParams)...)
+ explicit EventTemplate(const QString& stateKey,
+ ContentParamTs&&... contentParams)
+ : StateEvent(EventT::TypeId, stateKey)
+ , _content { std::forward<ContentParamTs>(contentParams)... }
{
- editJson().insert(ContentKey, _content.toJson());
+ editJson().insert(ContentKey, toJson(_content));
}
const ContentT& content() const { return _content; }
+
template <typename VisitorT>
void editContent(VisitorT&& visitor)
{
visitor(_content);
- editJson()[ContentKeyL] = _content.toJson();
- }
- [[deprecated("Use prevContent instead")]] const ContentT* prev_content() const
- {
- return prevContent();
- }
- const ContentT* prevContent() const
- {
- return _prev ? &_prev->content : nullptr;
+ editJson()[ContentKeyL] = toJson(_content);
}
- QString prevSenderId() const { return _prev ? _prev->senderId : QString(); }
+ const Omittable<ContentT>& prevContent() const { return _prev.content; }
+ QString prevSenderId() const { return _prev.senderId; }
private:
ContentT _content;
- std::unique_ptr<Prev<ContentT>> _prev;
+ Prev _prev;
+};
+
+template <typename EventT, typename ContentT>
+class KeyedStateEventBase
+ : public EventTemplate<EventT, StateEvent, ContentT> {
+public:
+ static constexpr auto needsStateKey = true;
+
+ using EventTemplate<EventT, StateEvent, ContentT>::EventTemplate;
};
+
+template <typename EvT>
+concept Keyed_State_Event = EvT::needsStateKey;
+
+template <typename EventT, typename ContentT>
+class KeylessStateEventBase
+ : public EventTemplate<EventT, StateEvent, ContentT> {
+private:
+ using base_type = EventTemplate<EventT, StateEvent, ContentT>;
+
+public:
+ template <typename... ContentParamTs>
+ explicit KeylessStateEventBase(ContentParamTs&&... contentParams)
+ : base_type(QString(), std::forward<ContentParamTs>(contentParams)...)
+ {}
+
+protected:
+ explicit KeylessStateEventBase(const QJsonObject& fullJson)
+ : base_type(fullJson)
+ {}
+};
+
+template <typename EvT>
+concept Keyless_State_Event = !EvT::needsStateKey;
+
} // namespace Quotient
+Q_DECLARE_METATYPE(Quotient::StateEvent*)
+Q_DECLARE_METATYPE(const Quotient::StateEvent*)
diff --git a/lib/events/stickerevent.h b/lib/events/stickerevent.h
new file mode 100644
index 00000000..67905481
--- /dev/null
+++ b/lib/events/stickerevent.h
@@ -0,0 +1,48 @@
+// SDPX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "roomevent.h"
+#include "eventcontent.h"
+
+namespace Quotient {
+
+/// Sticker messages are specialised image messages that are displayed without
+/// controls (e.g. no "download" link, or light-box view on click, as would be
+/// displayed for for m.image events).
+class QUOTIENT_API StickerEvent : public RoomEvent
+{
+public:
+ QUO_EVENT(StickerEvent, "m.sticker")
+
+ explicit StickerEvent(const QJsonObject& obj)
+ : RoomEvent(TypeId, obj)
+ , m_imageContent(
+ EventContent::ImageContent(obj["content"_ls].toObject()))
+ {}
+
+ /// \brief A textual representation or associated description of the
+ /// sticker image.
+ ///
+ /// This could be the alt text of the original image, or a message to
+ /// accompany and further describe the sticker.
+ QUO_CONTENT_GETTER(QString, body)
+
+ /// \brief Metadata about the image referred to in url including a
+ /// thumbnail representation.
+ const EventContent::ImageContent& image() const
+ {
+ return m_imageContent;
+ }
+
+ /// \brief The URL to the sticker image. This must be a valid mxc:// URI.
+ QUrl url() const
+ {
+ return m_imageContent.url();
+ }
+
+private:
+ EventContent::ImageContent m_imageContent;
+};
+} // namespace Quotient
diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp
deleted file mode 100644
index a95d2f0d..00000000
--- a/lib/events/typingevent.cpp
+++ /dev/null
@@ -1,31 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#include "typingevent.h"
-
-#include <QtCore/QJsonArray>
-
-using namespace Quotient;
-
-TypingEvent::TypingEvent(const QJsonObject& obj) : Event(typeId(), obj)
-{
- const auto& array = contentJson()["user_ids"_ls].toArray();
- _users.reserve(array.size());
- for (const auto& user : array)
- _users.push_back(user.toString());
-}
diff --git a/lib/events/typingevent.h b/lib/events/typingevent.h
index 1cf4e69d..b56475af 100644
--- a/lib/events/typingevent.h
+++ b/lib/events/typingevent.h
@@ -1,36 +1,10 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "event.h"
namespace Quotient {
-class TypingEvent : public Event {
-public:
- DEFINE_EVENT_TYPEID("m.typing", TypingEvent)
-
- TypingEvent(const QJsonObject& obj);
-
- const QStringList& users() const { return _users; }
-
-private:
- QStringList _users;
-};
-REGISTER_EVENT_TYPE(TypingEvent)
+DEFINE_SIMPLE_EVENT(TypingEvent, Event, "m.typing", QStringList, users, "user_ids")
} // namespace Quotient
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..a10c81fb
--- /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 QUOTIENT_API 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;
+};
+
+QUOTIENT_API QDebug operator<<(QDebug dbg, const EventStats& es);
+
+}
diff --git a/lib/expected.h b/lib/expected.h
new file mode 100644
index 00000000..81e186ea
--- /dev/null
+++ b/lib/expected.h
@@ -0,0 +1,78 @@
+// SPDX-FileCopyrightText: 2022 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <variant>
+
+namespace Quotient {
+
+//! \brief A minimal subset of std::expected from C++23
+template <typename T, typename E,
+ std::enable_if_t<!std::is_same_v<T, E>, bool> = true>
+class Expected {
+private:
+ template <typename X>
+ using enable_if_constructible_t = std::enable_if_t<
+ std::is_constructible_v<T, X> || std::is_constructible_v<E, X>>;
+
+public:
+ using value_type = T;
+ using error_type = E;
+
+ Expected() = default;
+ Expected(const Expected&) = default;
+ Expected(Expected&&) noexcept = default;
+ ~Expected() = default;
+
+ template <typename X, typename = enable_if_constructible_t<X>>
+ QUO_IMPLICIT Expected(X&& x) // NOLINT(google-explicit-constructor)
+ : data(std::forward<X>(x))
+ {}
+
+ Expected& operator=(const Expected&) = default;
+ Expected& operator=(Expected&&) noexcept = default;
+
+ template <typename X, typename = enable_if_constructible_t<X>>
+ Expected& operator=(X&& x)
+ {
+ data = std::forward<X>(x);
+ return *this;
+ }
+
+ bool has_value() const { return std::holds_alternative<T>(data); }
+ explicit operator bool() const { return has_value(); }
+
+ const value_type& value() const& { return std::get<T>(data); }
+ value_type& value() & { return std::get<T>(data); }
+ value_type value() && { return std::get<T>(std::move(data)); }
+
+ const value_type& operator*() const& { return value(); }
+ value_type& operator*() & { return value(); }
+
+ const value_type* operator->() const& { return std::get_if<T>(&data); }
+ value_type* operator->() & { return std::get_if<T>(&data); }
+
+ template <class U>
+ T value_or(U&& fallback) const&
+ {
+ if (has_value())
+ return value();
+ return std::forward<U>(fallback);
+ }
+ template <class U>
+ T value_or(U&& fallback) &&
+ {
+ if (has_value())
+ return value();
+ return std::forward<U>(fallback);
+ }
+
+ const E& error() const& { return std::get<E>(data); }
+ E& error() & { return std::get<E>(data); }
+
+private:
+ std::variant<T, E> data;
+};
+
+} // namespace Quotient
diff --git a/lib/function_traits.cpp b/lib/function_traits.cpp
new file mode 100644
index 00000000..e3d27122
--- /dev/null
+++ b/lib/function_traits.cpp
@@ -0,0 +1,56 @@
+// 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;
+
+template <typename FnT>
+using fn_return_t = typename function_traits<FnT>::return_type;
+
+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>
+[[maybe_unused]] 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..143ed162
--- /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>
+ 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<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...>;
+};
+
+namespace _impl {
+ template <typename>
+ struct fn_object_traits;
+
+ // Specialisation for a lambda function
+ template <typename ReturnT, typename ClassT, typename... ArgTs>
+ struct fn_object_traits<ReturnT (ClassT::*)(ArgTs...)>
+ : function_traits<ReturnT(ArgTs...)> {};
+
+ // Specialisation for a const lambda function
+ template <typename ReturnT, typename ClassT, typename... ArgTs>
+ struct fn_object_traits<ReturnT (ClassT::*)(ArgTs...) const>
+ : function_traits<ReturnT(ArgTs...)> {};
+
+ // Specialisation for function objects with (non-overloaded) operator()
+ // (this includes non-generic lambdas)
+ template <typename T>
+ requires requires { &T::operator(); }
+ struct fn_traits<T>
+ : public fn_object_traits<decltype(&T::operator())> {};
+
+ // Specialisation for a member function in a non-functor class
+ template <typename ReturnT, typename ClassT, typename... ArgTs>
+ struct fn_traits<ReturnT (ClassT::*)(ArgTs...)>
+ : function_traits<ReturnT(ClassT, ArgTs...)> {};
+
+ // Specialisation for a const member function
+ template <typename ReturnT, typename ClassT, typename... ArgTs>
+ struct fn_traits<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<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<ReturnT (ClassT::*)(ArgTs...) &&>
+ : function_traits<ReturnT(ClassT&&, ArgTs...)> {};
+
+ // Specialisation for a pointer-to-member
+ template <typename ReturnT, typename ClassT>
+ struct fn_traits<ReturnT ClassT::*>
+ : function_traits<ReturnT&(ClassT)> {};
+
+ // Specialisation for a const pointer-to-member
+ template <typename ReturnT, typename ClassT>
+ struct fn_traits<const ReturnT ClassT::*>
+ : function_traits<const ReturnT&(ClassT)> {};
+} // namespace _impl
+
+template <typename FnT, int ArgN = 0>
+using fn_arg_t =
+ std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>;
+
+template <typename FnT>
+constexpr auto fn_arg_count_v =
+ std::tuple_size_v<typename function_traits<FnT>::arg_types>;
+
+} // namespace Quotient
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp
index 5960203d..da645a2d 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -1,20 +1,6 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "basejob.h"
@@ -22,15 +8,12 @@
#include <QtCore/QRegularExpression>
#include <QtCore/QTimer>
-#include <QtCore/QStringBuilder>
#include <QtCore/QMetaEnum>
#include <QtCore/QPointer>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkRequest>
-#include <array>
-
using namespace Quotient;
using std::chrono::seconds, std::chrono::milliseconds;
using namespace std::chrono_literals;
@@ -39,7 +22,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;
@@ -47,19 +30,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;
}
@@ -77,12 +60,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 {
@@ -92,8 +69,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)
@@ -127,10 +104,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;
@@ -161,9 +138,8 @@ public:
QTimer timer;
QTimer retryTimer;
- static constexpr std::array<const JobTimeoutConfig, 3> errorStrategy {
- { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } }
- };
+ static constexpr auto errorStrategy = std::to_array<const JobTimeoutConfig>(
+ { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } });
int maxRetries = int(errorStrategy.size());
int retriesTaken = 0;
@@ -175,10 +151,8 @@ public:
[[nodiscard]] QString dumpRequest() const
{
- // FIXME: use std::array {} when Apple stdlib gets deduction guides for it
- static const auto verbs =
- make_array(QStringLiteral("GET"), QStringLiteral("PUT"),
- QStringLiteral("POST"), QStringLiteral("DELETE"));
+ static const std::array verbs { "GET"_ls, "PUT"_ls, "POST"_ls,
+ "DELETE"_ls };
const auto verbWord = verbs.at(size_t(verb));
return verbWord % ' '
% (reply ? reply->url().toString(QUrl::RemoveQuery)
@@ -187,14 +161,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(makeImpl<Private>(verb, std::move(endpoint), query, std::move(data),
+ needsToken))
{
setObjectName(name);
connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout);
@@ -215,13 +211,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;
@@ -238,16 +227,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
{
@@ -264,7 +256,7 @@ void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes)
d->expectedContentTypes = contentTypes;
}
-const QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; }
+QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; }
void BaseJob::addExpectedKey(const QByteArray& key) { d->expectedKeys << key; }
@@ -277,17 +269,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;
}
@@ -302,19 +294,14 @@ 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(
-#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
- QNetworkRequest::Http2AllowedAttribute
-#else
- QNetworkRequest::HTTP2AllowedAttribute
-#endif
// Qt doesn't combine HTTP2 with SSL quite right, occasionally crashing at
// what seems like an attempt to write to a closed channel. If/when that
// changes, false should be turned to true below.
- , false);
+ req.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
Q_ASSERT(req.url().isValid());
for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it)
req.setRawHeader(it.key(), it.value());
@@ -367,7 +354,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);
@@ -417,42 +404,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)
@@ -517,7 +504,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)
@@ -527,10 +514,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)
@@ -540,16 +527,16 @@ BaseJob::Status BaseJob::prepareError()
d->connection->limitRate(milliseconds(retryAfterMs));
- return { TooManyRequestsError, msg };
+ return { TooManyRequests, msg };
}
if (errCode == "M_CONSENT_NOT_GIVEN") {
- d->errorUrl = errorJson.value("consent_uri"_ls).toString();
- return { UserConsentRequiredError };
+ d->errorUrl = QUrl(errorJson.value("consent_uri"_ls).toString());
+ 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())
@@ -562,9 +549,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)
@@ -731,38 +718,41 @@ 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");
}
}
-int BaseJob::error() const { return d->status.code; }
+int BaseJob::error() const {
+ return d->status.code; }
-QString BaseJob::errorString() const { return d->status.message; }
+QString BaseJob::errorString() const {
+ return d->status.message; }
-QUrl BaseJob::errorUrl() const { return d->errorUrl; }
+QUrl BaseJob::errorUrl() const {
+ return d->errorUrl; }
void BaseJob::setStatus(Status s)
{
@@ -813,7 +803,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 be2926be..555c602b 100644
--- a/lib/jobs/basejob.h
+++ b/lib/jobs/basejob.h
@@ -1,28 +1,16 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "requestdata.h"
-#include "../logging.h"
-#include "../converters.h"
+#include "logging.h"
+#include "converters.h" // Common for csapi/ headers even though not used here
+#include "quotient_common.h" // For DECL_DEPRECATED_ENUMERATOR
#include <QtCore/QObject>
+#include <QtCore/QStringBuilder>
class QNetworkReply;
class QSslError;
@@ -32,12 +20,23 @@ class ConnectionData;
enum class HttpVerb { Get, Put, Post, Delete };
-class BaseJob : public QObject {
+class QUOTIENT_API BaseJob : public QObject {
Q_OBJECT
Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT)
Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries)
Q_PROPERTY(int statusCode READ error NOTIFY statusChanged)
+
+ static QByteArray encodeIfParam(const QString& paramPart);
+ template <int N>
+ static 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()
@@ -48,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,
@@ -57,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,
@@ -86,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,16 +123,25 @@ public:
{
return !operator==(other);
}
+ bool operator==(int otherCode) const
+ {
+ return code == otherCode;
+ }
+ bool operator!=(int otherCode) const
+ {
+ return !operator==(otherCode);
+ }
int code;
QString message;
};
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;
@@ -200,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);
@@ -251,8 +247,8 @@ public:
return dbg << j->objectName();
}
-public slots:
- void initiate(ConnectionData* connData, bool inBackground);
+public Q_SLOTS:
+ void initiate(Quotient::ConnectionData* connData, bool inBackground);
/**
* Abandons the result of this job, arrived or unarrived.
@@ -263,7 +259,7 @@ public slots:
*/
void abandon();
-signals:
+Q_SIGNALS:
/** The job is about to send a network request */
void aboutToSendRequest();
@@ -342,20 +338,22 @@ 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);
- const QByteArrayList expectedKeys() const;
+ QByteArrayList expectedKeys() const;
void addExpectedKey(const QByteArray &key);
void setExpectedKeys(const QByteArrayList &keys);
@@ -367,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
@@ -401,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
*
@@ -433,7 +433,7 @@ protected:
// Job objects should only be deleted via QObject::deleteLater
~BaseJob() override;
-protected slots:
+protected Q_SLOTS:
void timeout();
/*! \brief Check the pending or received reply for upfront issues
@@ -456,7 +456,7 @@ protected slots:
*/
virtual Status checkReply(const QNetworkReply *reply) const;
-private slots:
+private Q_SLOTS:
void sendRequest();
void gotReply();
@@ -467,10 +467,10 @@ private:
void finishJob();
class Private;
- QScopedPointer<Private> d;
+ ImplPtr<Private> d;
};
-inline bool isJobRunning(BaseJob* job)
+inline bool QUOTIENT_API isJobPending(BaseJob* job)
{
return job && job->error() == BaseJob::Pending;
}
diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp
index 0011a97c..759d52c9 100644
--- a/lib/jobs/downloadfilejob.cpp
+++ b/lib/jobs/downloadfilejob.cpp
@@ -1,11 +1,19 @@
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#include "downloadfilejob.h"
#include <QtCore/QFile>
#include <QtCore/QTemporaryFile>
#include <QtNetwork/QNetworkReply>
-using namespace Quotient;
+#ifdef Quotient_E2EE_ENABLED
+# include "events/filesourceinfo.h"
+# include <QtCore/QCryptographicHash>
+#endif
+
+using namespace Quotient;
class DownloadFileJob::Private {
public:
Private() : tempFile(new QTemporaryFile()) {}
@@ -17,6 +25,10 @@ public:
QScopedPointer<QFile> targetFile;
QScopedPointer<QFile> tempFile;
+
+#ifdef Quotient_E2EE_ENABLED
+ Omittable<EncryptedFileMetadata> encryptedFileMetadata;
+#endif
};
QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri)
@@ -29,11 +41,25 @@ DownloadFileJob::DownloadFileJob(const QString& serverName,
const QString& mediaId,
const QString& localFilename)
: GetContentJob(serverName, mediaId)
- , d(localFilename.isEmpty() ? new Private : new Private(localFilename))
+ , d(localFilename.isEmpty() ? makeImpl<Private>()
+ : makeImpl<Private>(localFilename))
{
setObjectName(QStringLiteral("DownloadFileJob"));
}
+#ifdef Quotient_E2EE_ENABLED
+DownloadFileJob::DownloadFileJob(const QString& serverName,
+ const QString& mediaId,
+ const EncryptedFileMetadata& file,
+ const QString& localFilename)
+ : GetContentJob(serverName, mediaId)
+ , d(localFilename.isEmpty() ? makeImpl<Private>()
+ : makeImpl<Private>(localFilename))
+{
+ setObjectName(QStringLiteral("DownloadFileJob"));
+ d->encryptedFileMetadata = file;
+}
+#endif
QString DownloadFileJob::targetFileName() const
{
return (d->targetFile ? d->targetFile : d->tempFile)->fileName();
@@ -48,7 +74,7 @@ void DownloadFileJob::doPrepare()
setStatus(FileError, "Could not open the target file for writing");
return;
}
- if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly)) {
+ if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::ReadWrite)) {
qCWarning(JOBS) << "Couldn't open the temporary file"
<< d->tempFile->fileName() << "for writing";
setStatus(FileError, "Could not open the temporary download file");
@@ -93,21 +119,60 @@ void DownloadFileJob::beforeAbandon()
d->tempFile->remove();
}
+void decryptFile(QFile& sourceFile, const EncryptedFileMetadata& metadata,
+ QFile& targetFile)
+{
+ sourceFile.seek(0);
+ const auto encrypted = sourceFile.readAll(); // TODO: stream decryption
+ const auto decrypted = decryptFile(encrypted, metadata);
+ targetFile.write(decrypted);
+}
+
BaseJob::Status DownloadFileJob::prepareResult()
{
if (d->targetFile) {
- d->targetFile->close();
- if (!d->targetFile->remove()) {
- qCWarning(JOBS) << "Failed to remove the target file placeholder";
- return { FileError, "Couldn't finalise the download" };
+#ifdef Quotient_E2EE_ENABLED
+ if (d->encryptedFileMetadata.has_value()) {
+ decryptFile(*d->tempFile, *d->encryptedFileMetadata, *d->targetFile);
+ d->tempFile->remove();
+ } else {
+#endif
+ d->targetFile->close();
+ if (!d->targetFile->remove()) {
+ qWarning(JOBS) << "Failed to remove the target file placeholder";
+ return { FileError, "Couldn't finalise the download" };
+ }
+ if (!d->tempFile->rename(d->targetFile->fileName())) {
+ qWarning(JOBS) << "Failed to rename" << d->tempFile->fileName()
+ << "to" << d->targetFile->fileName();
+ return { FileError, "Couldn't finalise the download" };
+ }
+#ifdef Quotient_E2EE_ENABLED
}
- if (!d->tempFile->rename(d->targetFile->fileName())) {
- qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName()
- << "to" << d->targetFile->fileName();
- return { FileError, "Couldn't finalise the download" };
+#endif
+ } else {
+#ifdef Quotient_E2EE_ENABLED
+ if (d->encryptedFileMetadata.has_value()) {
+ QTemporaryFile tempTempFile; // Assuming it to be next to tempFile
+ decryptFile(*d->tempFile, *d->encryptedFileMetadata, tempTempFile);
+ d->tempFile->close();
+ if (!d->tempFile->remove()) {
+ qWarning(JOBS)
+ << "Failed to remove the decrypted file placeholder";
+ return { FileError, "Couldn't finalise the download" };
+ }
+ if (!tempTempFile.rename(d->tempFile->fileName())) {
+ qWarning(JOBS) << "Failed to rename" << tempTempFile.fileName()
+ << "to" << d->tempFile->fileName();
+ return { FileError, "Couldn't finalise the download" };
+ }
+ } else {
+#endif
+ d->tempFile->close();
+#ifdef Quotient_E2EE_ENABLED
}
- } else
- d->tempFile->close();
- qCDebug(JOBS) << "Saved a file as" << targetFileName();
+#endif
+ }
+ qDebug(JOBS) << "Saved a file as" << targetFileName();
return Success;
}
diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h
index e00fd9e4..cbbfd244 100644
--- a/lib/jobs/downloadfilejob.h
+++ b/lib/jobs/downloadfilejob.h
@@ -1,9 +1,14 @@
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#pragma once
#include "csapi/content-repo.h"
+#include "events/filesourceinfo.h"
+
namespace Quotient {
-class DownloadFileJob : public GetContentJob {
+class QUOTIENT_API DownloadFileJob : public GetContentJob {
public:
using GetContentJob::makeRequestUrl;
static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri);
@@ -11,11 +16,14 @@ public:
DownloadFileJob(const QString& serverName, const QString& mediaId,
const QString& localFilename = {});
+#ifdef Quotient_E2EE_ENABLED
+ DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFileMetadata& file, const QString& localFilename = {});
+#endif
QString targetFileName() const;
private:
class Private;
- QScopedPointer<Private> d;
+ ImplPtr<Private> d;
void doPrepare() override;
void onSentRequest(QNetworkReply* reply) override;
diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp
index a69f00e9..6fe8ef26 100644
--- a/lib/jobs/mediathumbnailjob.cpp
+++ b/lib/jobs/mediathumbnailjob.cpp
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "mediathumbnailjob.h"
@@ -32,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/mediathumbnailjob.h b/lib/jobs/mediathumbnailjob.h
index e6d39085..c9f6da35 100644
--- a/lib/jobs/mediathumbnailjob.h
+++ b/lib/jobs/mediathumbnailjob.h
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
@@ -23,7 +8,7 @@
#include <QtGui/QPixmap>
namespace Quotient {
-class MediaThumbnailJob : public GetContentThumbnailJob {
+class QUOTIENT_API MediaThumbnailJob : public GetContentThumbnailJob {
public:
using GetContentThumbnailJob::makeRequestUrl;
static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri,
diff --git a/lib/jobs/postreadmarkersjob.h b/lib/jobs/postreadmarkersjob.h
deleted file mode 100644
index 5a4d942c..00000000
--- a/lib/jobs/postreadmarkersjob.h
+++ /dev/null
@@ -1,38 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#pragma once
-
-#include "basejob.h"
-
-#include <QtCore/QJsonObject>
-
-using namespace Quotient;
-
-class PostReadMarkersJob : public BaseJob {
-public:
- explicit PostReadMarkersJob(const QString& roomId,
- const QString& readUpToEventId)
- : BaseJob(
- HttpVerb::Post, "PostReadMarkersJob",
- QStringLiteral("_matrix/client/r0/rooms/%1/read_markers").arg(roomId))
- {
- setRequestData(
- QJsonObject { { QStringLiteral("m.fully_read"), readUpToEventId } });
- }
-};
diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp
index cec15954..ab249f6d 100644
--- a/lib/jobs/requestdata.cpp
+++ b/lib/jobs/requestdata.cpp
@@ -1,5 +1,9 @@
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#include "requestdata.h"
+#include <QtCore/QIODevice>
#include <QtCore/QBuffer>
#include <QtCore/QByteArray>
#include <QtCore/QJsonArray>
@@ -10,7 +14,7 @@ using namespace Quotient;
auto fromData(const QByteArray& data)
{
- auto source = std::make_unique<QBuffer>();
+ auto source = makeImpl<QBuffer, QIODevice>();
source->setData(data);
source->open(QIODevice::ReadOnly);
return source;
@@ -28,4 +32,6 @@ RequestData::RequestData(const QJsonObject& jo) : _source(fromJson(jo)) {}
RequestData::RequestData(const QJsonArray& ja) : _source(fromJson(ja)) {}
-RequestData::~RequestData() = default;
+RequestData::RequestData(QIODevice* source)
+ : _source(acquireImpl(source))
+{}
diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h
index 9cb5ecaf..accc8f71 100644
--- a/lib/jobs/requestdata.h
+++ b/lib/jobs/requestdata.h
@@ -1,26 +1,9 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
-#include <QtCore/QByteArray>
-
-#include <memory>
+#include "util.h"
class QJsonObject;
class QJsonArray;
@@ -34,22 +17,19 @@ namespace Quotient {
* as well as JSON (and possibly other structures in the future) to
* a QByteArray consumed by QNetworkAccessManager request methods.
*/
-class RequestData {
+class QUOTIENT_API RequestData {
public:
- RequestData(const QByteArray& a = {});
- RequestData(const QJsonObject& jo);
- RequestData(const QJsonArray& ja);
- RequestData(QIODevice* source) : _source(std::unique_ptr<QIODevice>(source))
- {}
- RequestData(RequestData&&) = default;
- RequestData& operator=(RequestData&&) = default;
- ~RequestData();
+ // NOLINTBEGIN(google-explicit-constructor): that check should learn about
+ // explicit(false)
+ QUO_IMPLICIT RequestData(const QByteArray& a = {});
+ QUO_IMPLICIT RequestData(const QJsonObject& jo);
+ QUO_IMPLICIT RequestData(const QJsonArray& ja);
+ QUO_IMPLICIT RequestData(QIODevice* source);
+ // NOLINTEND(google-explicit-constructor)
QIODevice* source() const { return _source.get(); }
private:
- std::unique_ptr<QIODevice> _source;
+ ImplPtr<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 9087fe50..f5c632bf 100644
--- a/lib/jobs/syncjob.cpp
+++ b/lib/jobs/syncjob.cpp
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "syncjob.h"
@@ -25,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;
@@ -52,10 +37,12 @@ SyncJob::SyncJob(const QString& since, const Filter& filter, int timeout,
BaseJob::Status SyncJob::prepareResult()
{
d.parseJson(jsonData());
- if (d.unresolvedRooms().isEmpty())
+ if (Q_LIKELY(d.unresolvedRooms().isEmpty()))
return Success;
- qCCritical(MAIN).noquote() << "Incomplete sync response, missing rooms:"
+ Q_ASSERT(d.unresolvedRooms().isEmpty());
+ qCCritical(MAIN).noquote() << "Rooms missing after processing sync "
+ "response, possibly a bug in SyncData: "
<< d.unresolvedRooms().join(',');
return IncorrectResponse;
}
diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h
index bf139a7b..b7bfbbb3 100644
--- a/lib/jobs/syncjob.h
+++ b/lib/jobs/syncjob.h
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
@@ -30,7 +15,7 @@ public:
explicit SyncJob(const QString& since, const Filter& filter,
int timeout = -1, const QString& presence = {});
- SyncData&& takeData() { return std::move(d); }
+ SyncData takeData() { return std::move(d); }
protected:
Status prepareResult() override;
diff --git a/lib/joinstate.h b/lib/joinstate.h
deleted file mode 100644
index 31c2b6a7..00000000
--- a/lib/joinstate.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#pragma once
-
-#include <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/keyverificationsession.cpp b/lib/keyverificationsession.cpp
new file mode 100644
index 00000000..4c61964c
--- /dev/null
+++ b/lib/keyverificationsession.cpp
@@ -0,0 +1,501 @@
+// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "keyverificationsession.h"
+
+#include "connection.h"
+#include "database.h"
+#include "e2ee/qolmaccount.h"
+#include "e2ee/qolmutils.h"
+#include "olm/sas.h"
+
+#include "events/event.h"
+
+#include <QtCore/QCryptographicHash>
+#include <QtCore/QTimer>
+#include <QtCore/QUuid>
+
+#include <chrono>
+
+using namespace Quotient;
+using namespace std::chrono;
+
+const QStringList supportedMethods = { SasV1Method };
+
+QStringList commonSupportedMethods(const QStringList& remoteMethods)
+{
+ QStringList result;
+ for (const auto& method : remoteMethods) {
+ if (supportedMethods.contains(method)) {
+ result += method;
+ }
+ }
+ return result;
+}
+
+KeyVerificationSession::KeyVerificationSession(
+ QString remoteUserId, const KeyVerificationRequestEvent& event,
+ Connection* connection, bool encrypted)
+ : QObject(connection)
+ , m_remoteUserId(std::move(remoteUserId))
+ , m_remoteDeviceId(event.fromDevice())
+ , m_transactionId(event.transactionId())
+ , m_connection(connection)
+ , m_encrypted(encrypted)
+ , m_remoteSupportedMethods(event.methods())
+{
+ const auto& currentTime = QDateTime::currentDateTime();
+ const auto timeoutTime =
+ std::min(event.timestamp().addSecs(600), currentTime.addSecs(120));
+ const milliseconds timeout{ currentTime.msecsTo(timeoutTime) };
+ if (timeout > 5s)
+ init(timeout);
+ // Otherwise don't even bother starting up
+}
+
+KeyVerificationSession::KeyVerificationSession(QString userId, QString deviceId,
+ Connection* connection)
+ : QObject(connection)
+ , m_remoteUserId(std::move(userId))
+ , m_remoteDeviceId(std::move(deviceId))
+ , m_transactionId(QUuid::createUuid().toString())
+ , m_connection(connection)
+ , m_encrypted(false)
+{
+ init(600s);
+ QMetaObject::invokeMethod(this, &KeyVerificationSession::sendRequest);
+}
+
+void KeyVerificationSession::init(milliseconds timeout)
+{
+ QTimer::singleShot(timeout, this, [this] { cancelVerification(TIMEOUT); });
+
+ m_sas = olm_sas(new std::byte[olm_sas_size()]);
+ const auto randomLength = olm_create_sas_random_length(m_sas);
+ olm_create_sas(m_sas, RandomBuffer(randomLength), randomLength);
+}
+
+KeyVerificationSession::~KeyVerificationSession()
+{
+ olm_clear_sas(m_sas);
+ delete[] reinterpret_cast<std::byte*>(m_sas);
+}
+
+void KeyVerificationSession::handleEvent(const KeyVerificationEvent& baseEvent)
+{
+ if (!switchOnType(
+ baseEvent,
+ [this](const KeyVerificationCancelEvent& event) {
+ setError(stringToError(event.code()));
+ setState(CANCELED);
+ return true;
+ },
+ [this](const KeyVerificationStartEvent& event) {
+ if (state() != WAITINGFORREADY && state() != READY)
+ return false;
+ handleStart(event);
+ return true;
+ },
+ [this](const KeyVerificationReadyEvent& event) {
+ if (state() == WAITINGFORREADY)
+ handleReady(event);
+ // ACCEPTED is also fine here because it's possible to receive
+ // ready and start in the same sync, in which case start might
+ // be handled before ready.
+ return state() == WAITINGFORREADY || state() == ACCEPTED;
+ },
+ [this](const KeyVerificationAcceptEvent& event) {
+ if (state() != WAITINGFORACCEPT)
+ return false;
+ m_commitment = event.commitment();
+ sendKey();
+ setState(WAITINGFORKEY);
+ return true;
+ },
+ [this](const KeyVerificationKeyEvent& event) {
+ if (state() != ACCEPTED && state() != WAITINGFORKEY)
+ return false;
+ handleKey(event);
+ return true;
+ },
+ [this](const KeyVerificationMacEvent& event) {
+ if (state() != WAITINGFORMAC && state() != WAITINGFORVERIFICATION)
+ return false;
+ handleMac(event);
+ return true;
+ },
+ [this](const KeyVerificationDoneEvent&) { return state() == DONE; }))
+ cancelVerification(UNEXPECTED_MESSAGE);
+}
+
+struct EmojiStoreEntry : EmojiEntry {
+ QHash<QString, QString> translatedDescriptions;
+
+ explicit EmojiStoreEntry(const QJsonObject& json)
+ : EmojiEntry{ fromJson<QString>(json["emoji"]),
+ fromJson<QString>(json["description"]) }
+ , translatedDescriptions{ fromJson<QHash<QString, QString>>(
+ json["translated_descriptions"]) }
+ {}
+};
+
+using EmojiStore = QVector<EmojiStoreEntry>;
+
+EmojiStore loadEmojiStore()
+{
+ QFile dataFile(":/sas-emoji.json");
+ dataFile.open(QFile::ReadOnly);
+ return fromJson<EmojiStore>(
+ QJsonDocument::fromJson(dataFile.readAll()).array());
+}
+
+EmojiEntry emojiForCode(int code, const QString& language)
+{
+ static const EmojiStore emojiStore = loadEmojiStore();
+ const auto& entry = emojiStore[code];
+ if (!language.isEmpty())
+ if (const auto translatedDescription =
+ emojiStore[code].translatedDescriptions.value(language);
+ !translatedDescription.isNull())
+ return { entry.emoji, translatedDescription };
+
+ return SLICE(entry, EmojiEntry);
+}
+
+void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event)
+{
+ auto eventKey = event.key().toLatin1();
+ olm_sas_set_their_key(m_sas, eventKey.data(), eventKey.size());
+
+ if (startSentByUs) {
+ const auto paddedCommitment =
+ QCryptographicHash::hash((event.key() % m_startEvent).toLatin1(),
+ QCryptographicHash::Sha256)
+ .toBase64();
+ const QLatin1String unpaddedCommitment(paddedCommitment.constData(),
+ paddedCommitment.indexOf('='));
+ if (unpaddedCommitment != m_commitment) {
+ qCWarning(E2EE) << "Commitment mismatch; aborting verification";
+ cancelVerification(MISMATCHED_COMMITMENT);
+ return;
+ }
+ } else {
+ sendKey();
+ }
+
+ std::string key(olm_sas_pubkey_length(m_sas), '\0');
+ olm_sas_get_pubkey(m_sas, key.data(), key.size());
+
+ std::array<std::byte, 6> output{};
+ const auto infoTemplate =
+ startSentByUs ? "MATRIX_KEY_VERIFICATION_SAS|%1|%2|%3|%4|%5|%6|%7"_ls
+ : "MATRIX_KEY_VERIFICATION_SAS|%4|%5|%6|%1|%2|%3|%7"_ls;
+
+ const auto info = infoTemplate
+ .arg(m_connection->userId(), m_connection->deviceId(),
+ key.data(), m_remoteUserId, m_remoteDeviceId,
+ event.key(), m_transactionId)
+ .toLatin1();
+ olm_sas_generate_bytes(m_sas, info.data(), info.size(), output.data(),
+ output.size());
+
+ static constexpr auto x3f = std::byte{ 0x3f };
+ const std::array<std::byte, 7> code{
+ output[0] >> 2,
+ (output[0] << 4 & x3f) | output[1] >> 4,
+ (output[1] << 2 & x3f) | output[2] >> 6,
+ output[2] & x3f,
+ output[3] >> 2,
+ (output[3] << 4 & x3f) | output[4] >> 4,
+ (output[4] << 2 & x3f) | output[5] >> 6
+ };
+
+ const auto uiLanguages = QLocale().uiLanguages();
+ const auto preferredLanguage = uiLanguages.isEmpty()
+ ? QString()
+ : uiLanguages.front().section('-', 0, 0);
+ for (const auto& c : code)
+ m_sasEmojis += emojiForCode(std::to_integer<int>(c), preferredLanguage);
+
+ emit sasEmojisChanged();
+ emit keyReceived();
+ setState(WAITINGFORVERIFICATION);
+}
+
+QString KeyVerificationSession::calculateMac(const QString& input,
+ bool verifying,
+ const QString& keyId)
+{
+ QByteArray inputBytes = input.toLatin1();
+ QByteArray outputBytes(olm_sas_mac_length(m_sas), '\0');
+ const auto macInfo =
+ (verifying ? "MATRIX_KEY_VERIFICATION_MAC%3%4%1%2%5%6"_ls
+ : "MATRIX_KEY_VERIFICATION_MAC%1%2%3%4%5%6"_ls)
+ .arg(m_connection->userId(), m_connection->deviceId(),
+ m_remoteUserId, m_remoteDeviceId, m_transactionId, keyId)
+ .toLatin1();
+ olm_sas_calculate_mac(m_sas, inputBytes.data(), inputBytes.size(),
+ macInfo.data(), macInfo.size(), outputBytes.data(),
+ outputBytes.size());
+ return QString::fromLatin1(outputBytes.data(), outputBytes.indexOf('='));
+}
+
+void KeyVerificationSession::sendMac()
+{
+ QString edKeyId = "ed25519:" % m_connection->deviceId();
+
+ auto keys = calculateMac(edKeyId, false);
+
+ QJsonObject mac;
+ auto key = m_connection->olmAccount()->deviceKeys().keys[edKeyId];
+ mac[edKeyId] = calculateMac(key, false, edKeyId);
+
+ m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId,
+ KeyVerificationMacEvent(m_transactionId, keys,
+ mac),
+ m_encrypted);
+ setState (macReceived ? DONE : WAITINGFORMAC);
+ m_verified = true;
+ if (!m_pendingEdKeyId.isEmpty()) {
+ trustKeys();
+ }
+}
+
+void KeyVerificationSession::sendDone()
+{
+ m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId,
+ KeyVerificationDoneEvent(m_transactionId),
+ m_encrypted);
+}
+
+void KeyVerificationSession::sendKey()
+{
+ QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0');
+ olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size());
+ m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId,
+ KeyVerificationKeyEvent(m_transactionId,
+ keyBytes),
+ m_encrypted);
+}
+
+
+void KeyVerificationSession::cancelVerification(Error error)
+{
+ m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId,
+ KeyVerificationCancelEvent(m_transactionId,
+ errorToString(error)),
+ m_encrypted);
+ setState(CANCELED);
+ setError(error);
+ emit finished();
+ deleteLater();
+}
+
+void KeyVerificationSession::sendReady()
+{
+ auto methods = commonSupportedMethods(m_remoteSupportedMethods);
+
+ if (methods.isEmpty()) {
+ cancelVerification(UNKNOWN_METHOD);
+ return;
+ }
+
+ m_connection->sendToDevice(
+ m_remoteUserId, m_remoteDeviceId,
+ KeyVerificationReadyEvent(m_transactionId, m_connection->deviceId(),
+ methods),
+ m_encrypted);
+ setState(READY);
+
+ if (methods.size() == 1) {
+ sendStartSas();
+ }
+}
+
+void KeyVerificationSession::sendStartSas()
+{
+ startSentByUs = true;
+ KeyVerificationStartEvent event(m_transactionId, m_connection->deviceId());
+ m_startEvent =
+ QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact);
+ m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, event,
+ m_encrypted);
+ setState(WAITINGFORACCEPT);
+}
+
+void KeyVerificationSession::handleReady(const KeyVerificationReadyEvent& event)
+{
+ setState(READY);
+ m_remoteSupportedMethods = event.methods();
+ auto methods = commonSupportedMethods(m_remoteSupportedMethods);
+
+ if (methods.isEmpty())
+ cancelVerification(UNKNOWN_METHOD);
+ else if (methods.size() == 1)
+ sendStartSas(); // -> WAITINGFORACCEPT
+}
+
+void KeyVerificationSession::handleStart(const KeyVerificationStartEvent& event)
+{
+ if (startSentByUs) {
+ if (m_remoteUserId > m_connection->userId() || (m_remoteUserId == m_connection->userId() && m_remoteDeviceId > m_connection->deviceId())) {
+ return;
+ } else {
+ startSentByUs = false;
+ }
+ }
+ QByteArray publicKey(olm_sas_pubkey_length(m_sas), '\0');
+ olm_sas_get_pubkey(m_sas, publicKey.data(), publicKey.size());
+ const auto canonicalEvent = QString(QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact));
+ auto commitment = QString(QCryptographicHash::hash((QString(publicKey) % canonicalEvent).toLatin1(), QCryptographicHash::Sha256).toBase64());
+ commitment = commitment.left(commitment.indexOf('='));
+
+ m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId,
+ KeyVerificationAcceptEvent(m_transactionId,
+ commitment),
+ m_encrypted);
+ setState(ACCEPTED);
+}
+
+void KeyVerificationSession::handleMac(const KeyVerificationMacEvent& event)
+{
+ QStringList keys = event.mac().keys();
+ keys.sort();
+ const auto& key = keys.join(",");
+ const QString edKeyId = "ed25519:"_ls % m_remoteDeviceId;
+
+ if (calculateMac(m_connection->edKeyForUserDevice(m_remoteUserId, m_remoteDeviceId), true, edKeyId) != event.mac()[edKeyId]) {
+ cancelVerification(KEY_MISMATCH);
+ return;
+ }
+
+ if (calculateMac(key, true) != event.keys()) {
+ cancelVerification(KEY_MISMATCH);
+ return;
+ }
+
+ m_pendingEdKeyId = edKeyId;
+
+ if (m_verified) {
+ trustKeys();
+ }
+}
+
+void KeyVerificationSession::trustKeys()
+{
+ m_connection->database()->setSessionVerified(m_pendingEdKeyId);
+ emit m_connection->sessionVerified(m_remoteUserId, m_remoteDeviceId);
+ macReceived = true;
+
+ if (state() == WAITINGFORMAC) {
+ setState(DONE);
+ sendDone();
+ emit finished();
+ deleteLater();
+ }
+}
+
+QVector<EmojiEntry> KeyVerificationSession::sasEmojis() const
+{
+ return m_sasEmojis;
+}
+
+void KeyVerificationSession::sendRequest()
+{
+ m_connection->sendToDevice(
+ m_remoteUserId, m_remoteDeviceId,
+ KeyVerificationRequestEvent(m_transactionId, m_connection->deviceId(),
+ supportedMethods,
+ QDateTime::currentDateTime()),
+ m_encrypted);
+ setState(WAITINGFORREADY);
+}
+
+KeyVerificationSession::State KeyVerificationSession::state() const
+{
+ return m_state;
+}
+
+void KeyVerificationSession::setState(KeyVerificationSession::State state)
+{
+ m_state = state;
+ emit stateChanged();
+}
+
+KeyVerificationSession::Error KeyVerificationSession::error() const
+{
+ return m_error;
+}
+
+void KeyVerificationSession::setError(Error error)
+{
+ m_error = error;
+ emit errorChanged();
+}
+
+QString KeyVerificationSession::errorToString(Error error)
+{
+ switch(error) {
+ case NONE:
+ return "none"_ls;
+ case TIMEOUT:
+ return "m.timeout"_ls;
+ case USER:
+ return "m.user"_ls;
+ case UNEXPECTED_MESSAGE:
+ return "m.unexpected_message"_ls;
+ case UNKNOWN_TRANSACTION:
+ return "m.unknown_transaction"_ls;
+ case UNKNOWN_METHOD:
+ return "m.unknown_method"_ls;
+ case KEY_MISMATCH:
+ return "m.key_mismatch"_ls;
+ case USER_MISMATCH:
+ return "m.user_mismatch"_ls;
+ case INVALID_MESSAGE:
+ return "m.invalid_message"_ls;
+ case SESSION_ACCEPTED:
+ return "m.accepted"_ls;
+ case MISMATCHED_COMMITMENT:
+ return "m.mismatched_commitment"_ls;
+ case MISMATCHED_SAS:
+ return "m.mismatched_sas"_ls;
+ default:
+ return "m.user"_ls;
+ }
+}
+
+KeyVerificationSession::Error KeyVerificationSession::stringToError(const QString& error)
+{
+ if (error == "m.timeout"_ls) {
+ return REMOTE_TIMEOUT;
+ } else if (error == "m.user"_ls) {
+ return REMOTE_USER;
+ } else if (error == "m.unexpected_message"_ls) {
+ return REMOTE_UNEXPECTED_MESSAGE;
+ } else if (error == "m.unknown_message"_ls) {
+ return REMOTE_UNEXPECTED_MESSAGE;
+ } else if (error == "m.unknown_transaction"_ls) {
+ return REMOTE_UNKNOWN_TRANSACTION;
+ } else if (error == "m.unknown_method"_ls) {
+ return REMOTE_UNKNOWN_METHOD;
+ } else if (error == "m.key_mismatch"_ls) {
+ return REMOTE_KEY_MISMATCH;
+ } else if (error == "m.user_mismatch"_ls) {
+ return REMOTE_USER_MISMATCH;
+ } else if (error == "m.invalid_message"_ls) {
+ return REMOTE_INVALID_MESSAGE;
+ } else if (error == "m.accepted"_ls) {
+ return REMOTE_SESSION_ACCEPTED;
+ } else if (error == "m.mismatched_commitment"_ls) {
+ return REMOTE_MISMATCHED_COMMITMENT;
+ } else if (error == "m.mismatched_sas"_ls) {
+ return REMOTE_MISMATCHED_SAS;
+ }
+ return NONE;
+}
+
+QString KeyVerificationSession::remoteDeviceId() const
+{
+ return m_remoteDeviceId;
+}
diff --git a/lib/keyverificationsession.h b/lib/keyverificationsession.h
new file mode 100644
index 00000000..32a91cfc
--- /dev/null
+++ b/lib/keyverificationsession.h
@@ -0,0 +1,153 @@
+// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "events/keyverificationevent.h"
+
+#include <QtCore/QObject>
+
+struct OlmSAS;
+
+namespace Quotient {
+class Connection;
+
+struct QUOTIENT_API EmojiEntry {
+ QString emoji;
+ QString description;
+
+ Q_GADGET
+ Q_PROPERTY(QString emoji MEMBER emoji CONSTANT)
+ Q_PROPERTY(QString description MEMBER description CONSTANT)
+
+public:
+ bool operator==(const EmojiEntry& rhs) const = default;
+};
+
+/** A key verification session. Listen for incoming sessions by connecting to Connection::newKeyVerificationSession.
+ Start a new session using Connection::startKeyVerificationSession.
+ The object is delete after finished is emitted.
+*/
+class QUOTIENT_API KeyVerificationSession : public QObject
+{
+ Q_OBJECT
+
+public:
+ enum State {
+ INCOMING, ///< There is a request for verification incoming
+ //! We sent a request for verification and are waiting for ready
+ WAITINGFORREADY,
+ //! Either party sent a ready as a response to a request; the user
+ //! selects a method
+ READY,
+ WAITINGFORACCEPT, ///< We sent a start and are waiting for an accept
+ ACCEPTED, ///< The other party sent an accept and is waiting for a key
+ WAITINGFORKEY, ///< We're waiting for a key
+ //! We're waiting for the *user* to verify the emojis
+ WAITINGFORVERIFICATION,
+ WAITINGFORMAC, ///< We're waiting for the mac
+ CANCELED, ///< The session has been canceled
+ DONE, ///< The verification is done
+ };
+ Q_ENUM(State)
+
+ enum Error {
+ NONE,
+ TIMEOUT,
+ REMOTE_TIMEOUT,
+ USER,
+ REMOTE_USER,
+ UNEXPECTED_MESSAGE,
+ REMOTE_UNEXPECTED_MESSAGE,
+ UNKNOWN_TRANSACTION,
+ REMOTE_UNKNOWN_TRANSACTION,
+ UNKNOWN_METHOD,
+ REMOTE_UNKNOWN_METHOD,
+ KEY_MISMATCH,
+ REMOTE_KEY_MISMATCH,
+ USER_MISMATCH,
+ REMOTE_USER_MISMATCH,
+ INVALID_MESSAGE,
+ REMOTE_INVALID_MESSAGE,
+ SESSION_ACCEPTED,
+ REMOTE_SESSION_ACCEPTED,
+ MISMATCHED_COMMITMENT,
+ REMOTE_MISMATCHED_COMMITMENT,
+ MISMATCHED_SAS,
+ REMOTE_MISMATCHED_SAS,
+ };
+ Q_ENUM(Error)
+
+ Q_PROPERTY(QString remoteDeviceId MEMBER m_remoteDeviceId CONSTANT)
+ Q_PROPERTY(QVector<EmojiEntry> sasEmojis READ sasEmojis NOTIFY sasEmojisChanged)
+ Q_PROPERTY(State state READ state NOTIFY stateChanged)
+ Q_PROPERTY(Error error READ error NOTIFY errorChanged)
+
+ KeyVerificationSession(QString remoteUserId,
+ const KeyVerificationRequestEvent& event,
+ Connection* connection, bool encrypted);
+ KeyVerificationSession(QString userId, QString deviceId,
+ Connection* connection);
+ ~KeyVerificationSession() override;
+ Q_DISABLE_COPY_MOVE(KeyVerificationSession)
+
+ void handleEvent(const KeyVerificationEvent& baseEvent);
+
+ QVector<EmojiEntry> sasEmojis() const;
+ State state() const;
+
+ Error error() const;
+
+ QString remoteDeviceId() const;
+
+public Q_SLOTS:
+ void sendRequest();
+ void sendReady();
+ void sendMac();
+ void sendStartSas();
+ void sendKey();
+ void sendDone();
+ void cancelVerification(Error error);
+
+Q_SIGNALS:
+ void keyReceived();
+ void sasEmojisChanged();
+ void stateChanged();
+ void errorChanged();
+ void finished();
+
+private:
+ const QString m_remoteUserId;
+ const QString m_remoteDeviceId;
+ const QString m_transactionId;
+ Connection* m_connection;
+ OlmSAS* m_sas = nullptr;
+ QVector<EmojiEntry> m_sasEmojis;
+ bool startSentByUs = false;
+ State m_state = INCOMING;
+ Error m_error = NONE;
+ QString m_startEvent;
+ QString m_commitment;
+ bool macReceived = false;
+ bool m_encrypted;
+ QStringList m_remoteSupportedMethods;
+ bool m_verified = false;
+ QString m_pendingEdKeyId{};
+
+ void handleReady(const KeyVerificationReadyEvent& event);
+ void handleStart(const KeyVerificationStartEvent& event);
+ void handleKey(const KeyVerificationKeyEvent& event);
+ void handleMac(const KeyVerificationMacEvent& event);
+ void init(std::chrono::milliseconds timeout);
+ void setState(State state);
+ void setError(Error error);
+ static QString errorToString(Error error);
+ static Error stringToError(const QString& error);
+ void trustKeys();
+
+ QByteArray macInfo(bool verifying, const QString& key = "KEY_IDS"_ls);
+ QString calculateMac(const QString& input, bool verifying, const QString& keyId= "KEY_IDS"_ls);
+};
+
+} // namespace Quotient
+Q_DECLARE_METATYPE(Quotient::EmojiEntry)
diff --git a/lib/logging.cpp b/lib/logging.cpp
index c346fbf1..460caced 100644
--- a/lib/logging.cpp
+++ b/lib/logging.cpp
@@ -1,20 +1,6 @@
-/******************************************************************************
- * Copyright (C) 2017 Elvis Angelaccio <elvid.angelaccio@kde.org>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Elvis Angelaccio <elvid.angelaccio@kde.org>
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "logging.h"
@@ -24,9 +10,13 @@
LOGGING_CATEGORY(MAIN, "quotient.main")
LOGGING_CATEGORY(EVENTS, "quotient.events")
LOGGING_CATEGORY(STATE, "quotient.events.state")
+LOGGING_CATEGORY(MEMBERS, "quotient.events.members")
LOGGING_CATEGORY(MESSAGES, "quotient.events.messages")
LOGGING_CATEGORY(EPHEMERAL, "quotient.events.ephemeral")
LOGGING_CATEGORY(E2EE, "quotient.e2ee")
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")
+LOGGING_CATEGORY(DATABASE, "quotient.database")
diff --git a/lib/logging.h b/lib/logging.h
index ce4131bb..1fafa04b 100644
--- a/lib/logging.h
+++ b/lib/logging.h
@@ -1,20 +1,6 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Elvis Angelaccio <elvid.angelaccio@kde.org>
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
@@ -23,13 +9,17 @@
Q_DECLARE_LOGGING_CATEGORY(MAIN)
Q_DECLARE_LOGGING_CATEGORY(STATE)
+Q_DECLARE_LOGGING_CATEGORY(MEMBERS)
Q_DECLARE_LOGGING_CATEGORY(MESSAGES)
Q_DECLARE_LOGGING_CATEGORY(EVENTS)
Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL)
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)
+Q_DECLARE_LOGGING_CATEGORY(DATABASE)
namespace Quotient {
// QDebug manipulators
@@ -48,24 +38,13 @@ 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 dbg.verbosity(QDebug::MinimumVerbosity);
}
inline qint64 profilerMinNsecs()
@@ -79,15 +58,24 @@ inline qint64 profilerMinNsecs()
* 1000;
}
} // namespace Quotient
-/// \deprecated Use namespace Quotient instead
-namespace QMatrixClient = Quotient;
-inline QDebug operator<<(QDebug debug_object, const QElapsedTimer& et)
+/**
+ * @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); // NOLINT(performance-unnecessary-value-param)
+}
+
+inline QDebug operator<<(QDebug debug_object, QElapsedTimer et)
{
- auto val = et.nsecsElapsed() / 1000;
- if (val < 1000)
- debug_object << val << "µs";
- else
- debug_object << val / 1000 << "ms";
+ // NOLINTNEXTLINE(bugprone-integer-division)
+ debug_object << static_cast<double>(et.nsecsElapsed() / 1000) / 1000
+ << "ms"; // Show in ms with 3 decimal digits precision
return debug_object;
}
diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp
new file mode 100644
index 00000000..ce833b98
--- /dev/null
+++ b/lib/mxcreply.cpp
@@ -0,0 +1,99 @@
+// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "mxcreply.h"
+
+#include <QtCore/QBuffer>
+#include "accountregistry.h"
+#include "room.h"
+
+#ifdef Quotient_E2EE_ENABLED
+#include "events/filesourceinfo.h"
+#endif
+
+using namespace Quotient;
+
+class MxcReply::Private
+{
+public:
+ explicit Private(QNetworkReply* r = nullptr)
+ : m_reply(r)
+ {}
+ QNetworkReply* m_reply;
+ Omittable<EncryptedFileMetadata> m_encryptedFile;
+ QIODevice* m_device = nullptr;
+};
+
+MxcReply::MxcReply(QNetworkReply* reply)
+ : d(makeImpl<Private>(reply))
+{
+ d->m_device = d->m_reply;
+ reply->setParent(this);
+ connect(d->m_reply, &QNetworkReply::finished, this, [this]() {
+ setError(d->m_reply->error(), d->m_reply->errorString());
+ setOpenMode(ReadOnly);
+ Q_EMIT finished();
+ });
+}
+
+MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId)
+ : d(makeImpl<Private>(reply))
+{
+ reply->setParent(this);
+ connect(d->m_reply, &QNetworkReply::finished, this, [this]() {
+ setError(d->m_reply->error(), d->m_reply->errorString());
+
+#ifdef Quotient_E2EE_ENABLED
+ if(!d->m_encryptedFile.has_value()) {
+ d->m_device = d->m_reply;
+ } else {
+ auto buffer = new QBuffer(this);
+ buffer->setData(
+ decryptFile(d->m_reply->readAll(), *d->m_encryptedFile));
+ buffer->open(ReadOnly);
+ d->m_device = buffer;
+ }
+#else
+ d->m_device = d->m_reply;
+#endif
+ setOpenMode(ReadOnly);
+ emit finished();
+ });
+
+#ifdef Quotient_E2EE_ENABLED
+ auto eventIt = room->findInTimeline(eventId);
+ if(eventIt != room->historyEdge()) {
+ if (auto event = eventIt->viewAs<RoomMessageEvent>()) {
+ if (auto* efm = std::get_if<EncryptedFileMetadata>(
+ &event->content()->fileInfo()->source))
+ d->m_encryptedFile = *efm;
+ }
+ }
+#endif
+}
+
+MxcReply::MxcReply()
+ : d(ZeroImpl<Private>())
+{
+ 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 errorOccurred(QNetworkReply::ProtocolInvalidOperationError);
+ emit finished();
+ }, Qt::QueuedConnection);
+}
+
+qint64 MxcReply::readData(char *data, qint64 maxSize)
+{
+ return d->m_device->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..f6c4a34d
--- /dev/null
+++ b/lib/mxcreply.h
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "util.h"
+
+#include <QtNetwork/QNetworkReply>
+
+namespace Quotient {
+class Room;
+
+class QUOTIENT_API MxcReply : public QNetworkReply
+{
+ Q_OBJECT
+public:
+ explicit MxcReply();
+ explicit MxcReply(QNetworkReply *reply);
+ MxcReply(QNetworkReply* reply, Room* room, const QString& eventId);
+
+public Q_SLOTS:
+ void abort() override;
+
+protected:
+ qint64 readData(char *data, qint64 maxSize) override;
+
+private:
+ class Private;
+ ImplPtr<Private> d;
+};
+}
diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp
index e8aa85df..44a306d1 100644
--- a/lib/networkaccessmanager.cpp
+++ b/lib/networkaccessmanager.cpp
@@ -1,35 +1,45 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "networkaccessmanager.h"
+#include "connection.h"
+#include "room.h"
+#include "accountregistry.h"
+#include "mxcreply.h"
+
#include <QtCore/QCoreApplication>
+#include <QtCore/QThread>
+#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(makeImpl<Private>(this))
{}
QList<QSslError> NetworkAccessManager::ignoredSslErrors() const
@@ -37,6 +47,16 @@ QList<QSslError> NetworkAccessManager::ignoredSslErrors() const
return d->ignoredSslErrors;
}
+void NetworkAccessManager::ignoreSslErrors(bool ignore) const
+{
+ if (ignore) {
+ connect(this, &QNetworkAccessManager::sslErrors, this,
+ [](QNetworkReply* reply) { reply->ignoreSslErrors(); });
+ } else {
+ disconnect(this, &QNetworkAccessManager::sslErrors, this, nullptr);
+ }
+}
+
void NetworkAccessManager::addIgnoredSslError(const QSslError& error)
{
d->ignoredSslErrors << error;
@@ -47,31 +67,63 @@ void NetworkAccessManager::clearIgnoredSslErrors()
d->ignoredSslErrors.clear();
}
-static NetworkAccessManager* createNam()
-{
- auto nam = new NetworkAccessManager(QCoreApplication::instance());
-#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
- // See #109; in newer Qt, bearer management is deprecated altogether
- NetworkAccessManager::connect(nam,
- &QNetworkAccessManager::networkAccessibleChanged, [nam] {
- nam->setNetworkAccessible(QNetworkAccessManager::Accessible);
- });
-#endif
- return nam;
-}
-
NetworkAccessManager* NetworkAccessManager::instance()
{
- static auto* nam = createNam();
+ thread_local auto* nam = [] {
+ auto* namInit = new NetworkAccessManager();
+ connect(QThread::currentThread(), &QThread::finished, namInit,
+ &QObject::deleteLater);
+ return namInit;
+ }();
return nam;
}
-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 = Accounts.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 a678b80f..01b0599d 100644
--- a/lib/networkaccessmanager.h
+++ b/lib/networkaccessmanager.h
@@ -1,46 +1,35 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
-#include <QtNetwork/QNetworkAccessManager>
+#include "util.h"
-#include <memory>
+#include <QtNetwork/QNetworkAccessManager>
namespace Quotient {
-class NetworkAccessManager : public QNetworkAccessManager {
+
+class QUOTIENT_API NetworkAccessManager : public QNetworkAccessManager {
Q_OBJECT
public:
NetworkAccessManager(QObject* parent = nullptr);
- ~NetworkAccessManager() override;
QList<QSslError> ignoredSslErrors() const;
void addIgnoredSslError(const QSslError& error);
void clearIgnoredSslErrors();
+ void ignoreSslErrors(bool ignore = true) const;
/** 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;
class Private;
- std::unique_ptr<Private> d;
+ ImplPtr<Private> d;
};
} // namespace Quotient
diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp
index 40ecba11..06b1fdf9 100644
--- a/lib/networksettings.cpp
+++ b/lib/networksettings.cpp
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "networksettings.h"
@@ -26,9 +11,9 @@ void NetworkSettings::setupApplicationProxy() const
{ proxyType(), proxyHostName(), proxyPort() });
}
-QTNT_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType,
+QUO_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType,
"proxy_type", QNetworkProxy::DefaultProxy, setProxyType)
-QTNT_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname",
+QUO_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname",
{}, setProxyHostName)
-QTNT_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1,
+QUO_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1,
setProxyPort)
diff --git a/lib/networksettings.h b/lib/networksettings.h
index 2399cf5f..44247e59 100644
--- a/lib/networksettings.h
+++ b/lib/networksettings.h
@@ -1,20 +1,5 @@
-/******************************************************************************
- * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
@@ -25,11 +10,11 @@
Q_DECLARE_METATYPE(QNetworkProxy::ProxyType)
namespace Quotient {
-class NetworkSettings : public SettingsGroup {
+class QUOTIENT_API NetworkSettings : public SettingsGroup {
Q_OBJECT
- QTNT_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType)
- QTNT_DECLARE_SETTING(QString, proxyHostName, setProxyHostName)
- QTNT_DECLARE_SETTING(quint16, proxyPort, setProxyPort)
+ QUO_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType)
+ QUO_DECLARE_SETTING(QString, proxyHostName, setProxyHostName)
+ QUO_DECLARE_SETTING(quint16, proxyPort, setProxyPort)
Q_PROPERTY(QString proxyHost READ proxyHostName WRITE setProxyHostName)
public:
template <typename... ArgTs>
diff --git a/lib/omittable.h b/lib/omittable.h
new file mode 100644
index 00000000..0718aaff
--- /dev/null
+++ b/lib/omittable.h
@@ -0,0 +1,217 @@
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <optional>
+#include <functional>
+
+namespace Quotient {
+
+template <typename T>
+class Omittable;
+
+constexpr auto none = std::nullopt;
+
+//! \brief Lift an operation into dereferenceable types (Omittables or pointers)
+//!
+//! This is a more generic version of Omittable::then() that extends to
+//! an arbitrary number of arguments of any type that is dereferenceable (unary
+//! operator*() can be applied to it) and (explicitly or implicitly) convertible
+//! to bool. This allows to streamline checking for nullptr/none before applying
+//! the operation on the underlying types. \p fn is only invoked if all \p args
+//! are "truthy" (i.e. <tt>(... && bool(args)) == true</tt>).
+//! \param fn A callable that should accept the types stored inside
+//! Omittables/pointers passed in \p args
+//! \return Always an Omittable: if \p fn returns another type, lift() wraps
+//! it in an Omittable; if \p fn returns an Omittable, that return value
+//! (or none) is returned as is.
+template <typename FnT, typename... ArgTs>
+inline auto lift(FnT&& fn, ArgTs&&... args)
+{
+ if constexpr (std::is_void_v<decltype(std::invoke(std::forward<FnT>(fn),
+ *args...))>) {
+ if ((... && bool(args)))
+ std::invoke(std::forward<FnT>(fn), *args...);
+ } else
+ return (... && bool(args))
+ ? Omittable(std::invoke(std::forward<FnT>(fn), *args...))
+ : none;
+}
+
+/** `std::optional` with tweaks
+ *
+ * The tweaks are:
+ * - streamlined assignment (operator=)/emplace()ment of values that can be
+ * used to implicitly construct the underlying type, including
+ * direct-list-initialisation, e.g.:
+ * \code
+ * struct S { int a; char b; }
+ * Omittable<S> o;
+ * o = { 1, 'a' }; // std::optional would require o = S { 1, 'a' }
+ * \endcode
+ * - entirely deleted value(). The technical reason is that Xcode 10 doesn't
+ * have it; but besides that, value_or() or (after explicit checking)
+ * `operator*()`/`operator->()` are better alternatives within Quotient
+ * that doesn't practice throwing exceptions (as doesn't most of Qt).
+ * - ensure() to provide a safer lvalue accessor instead of operator* or
+ * operator->. Allows chained initialisation of nested Omittables:
+ * \code
+ * struct Inner { int member = 10; Omittable<int> innermost; };
+ * struct Outer { int anotherMember = 10; Omittable<Inner> inner; };
+ * Omittable<Outer> o; // = { 10, std::nullopt };
+ * o.ensure().inner.ensure().innermost.emplace(42);
+ * \endcode
+ * - merge() - a soft version of operator= that only overwrites its first
+ * operand with the second one if the second one is not empty.
+ * - then() and then_or() to streamline read-only interrogation in a "monadic"
+ * interface.
+ */
+template <typename T>
+class Omittable : public std::optional<T> {
+public:
+ using base_type = std::optional<T>;
+ using value_type = std::decay_t<T>;
+
+ using std::optional<T>::optional;
+
+ // Overload emplace() and operator=() to allow passing braced-init-lists
+ // (the standard emplace() does direct-initialisation but
+ // not direct-list-initialisation).
+ using base_type::operator=;
+ Omittable& operator=(const value_type& v)
+ {
+ base_type::operator=(v);
+ return *this;
+ }
+ Omittable& operator=(value_type&& v)
+ {
+ base_type::operator=(std::move(v));
+ return *this;
+ }
+
+ using base_type::emplace;
+ T& emplace(const T& val) { return base_type::emplace(val); }
+ T& emplace(T&& val) { return base_type::emplace(std::move(val)); }
+
+ // Use value_or() or check (with operator! or has_value) before accessing
+ // with operator-> or operator*
+ // The technical reason is that Xcode 10 has incomplete std::optional
+ // that has no value(); but using value() may also mean that you rely
+ // on the optional throwing an exception (which is not an assumed practice
+ // throughout Quotient) or that you spend unnecessary CPU cycles on
+ // an extraneous has_value() check.
+ auto& value() = delete;
+ const auto& value() const = delete;
+
+ template <typename U>
+ value_type& ensure(U&& defaultValue = value_type {})
+ {
+ return this->has_value() ? this->operator*()
+ : this->emplace(std::forward<U>(defaultValue));
+ }
+ value_type& ensure(const value_type& defaultValue)
+ {
+ return ensure<>(defaultValue);
+ }
+ value_type& ensure(value_type&& defaultValue)
+ {
+ return ensure<>(std::move(defaultValue));
+ }
+
+ //! Merge the value from another Omittable
+ //! \return true if \p other is not omitted and the value of
+ //! the current Omittable was different (or omitted),
+ //! in other words, if the current Omittable has changed;
+ //! false otherwise
+ template <typename T1>
+ auto merge(const std::optional<T1>& other)
+ -> std::enable_if_t<std::is_convertible_v<T1, T>, bool>
+ {
+ if (!other || (this->has_value() && **this == *other))
+ return false;
+ this->emplace(*other);
+ return true;
+ }
+
+ // The below is inspired by the proposed std::optional monadic operations
+ // (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0798r6.html).
+
+ //! \brief Lift a callable into the Omittable
+ //!
+ //! 'Lifting', as used in functional programming, means here invoking
+ //! a callable (e.g., a function) on the contents of the Omittable if it has
+ //! any and wrapping the returned value (that may be of a different type T2)
+ //! into a new Omittable\<T2>. If the current Omittable is empty,
+ //! the invocation is skipped altogether and Omittable\<T2>{none} is
+ //! returned instead.
+ //! \note if \p fn already returns an Omittable (i.e., it is a 'functor',
+ //! in functional programming terms), then() will not wrap another
+ //! Omittable around but will just return what \p fn returns. The
+ //! same doesn't hold for the parameter: if \p fn accepts an Omittable
+ //! you have to wrap it in another Omittable before calling then().
+ //! \return `none` if the current Omittable has `none`;
+ //! otherwise, the Omittable returned from a call to \p fn
+ //! \tparam FnT a callable with \p T (or <tt>const T&</tt>)
+ //! returning Omittable<T2>, T2 is any supported type
+ //! \sa then_or, transform
+ template <typename FnT>
+ auto then(FnT&& fn) const
+ {
+ return lift(std::forward<FnT>(fn), *this);
+ }
+
+ //! \brief Lift a callable into the rvalue Omittable
+ //!
+ //! This is an rvalue overload for then().
+ template <typename FnT>
+ auto then(FnT&& fn)
+ {
+ return lift(std::forward<FnT>(fn), *this);
+ }
+
+ //! \brief Lift a callable into the const lvalue Omittable, with a fallback
+ //!
+ //! This effectively does the same what then() does, except that it returns
+ //! a value of type returned by the callable, or the provided fallback value
+ //! if the current Omittable is empty. This is a typesafe version to apply
+ //! an operation on an Omittable without having to deal with another
+ //! Omittable afterwards.
+ template <typename FnT, typename FallbackT>
+ auto then_or(FnT&& fn, FallbackT&& fallback) const
+ {
+ return then(std::forward<FnT>(fn))
+ .value_or(std::forward<FallbackT>(fallback));
+ }
+
+ //! \brief Lift a callable into the rvalue Omittable, with a fallback
+ //!
+ //! This is an overload for functions that accept rvalue
+ template <typename FnT, typename FallbackT>
+ auto then_or(FnT&& fn, FallbackT&& fallback)
+ {
+ return then(std::forward<FnT>(fn))
+ .value_or(std::forward<FallbackT>(fallback));
+ }
+};
+
+template <typename T>
+Omittable(T&&) -> Omittable<T>;
+
+//! \brief Merge the value from an optional
+//! This is an adaptation of Omittable::merge() to the case when the value
+//! on the left hand side is not an Omittable.
+//! \return true if \p rhs is not omitted and the \p lhs value was different,
+//! in other words, if \p lhs has changed;
+//! false otherwise
+template <typename T1, typename T2>
+inline auto merge(T1& lhs, const std::optional<T2>& rhs)
+ -> std::enable_if_t<std::is_assignable_v<T1&, const T2&>, bool>
+{
+ if (!rhs || lhs == *rhs)
+ return false;
+ lhs = *rhs;
+ return true;
+}
+
+} // namespace Quotient
diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h
index 699735d4..ef7f6f80 100644
--- a/lib/qt_connection_util.h
+++ b/lib/qt_connection_util.h
@@ -1,139 +1,103 @@
-/******************************************************************************
- * Copyright (C) 2019 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
-#include "util.h"
+#include "function_traits.h"
#include <QtCore/QPointer>
namespace Quotient {
namespace _impl {
- template <typename... ArgTs>
- using decorated_slot_tt =
- std::function<void(QMetaObject::Connection&, const ArgTs&...)>;
+ enum ConnectionType { SingleShot, Until };
- template <typename SenderT, typename SignalT, typename ContextT, typename... ArgTs>
- inline QMetaObject::Connection
- connectDecorated(SenderT* sender, SignalT signal, ContextT* context,
- decorated_slot_tt<ArgTs...> decoratedSlot,
- Qt::ConnectionType connType)
+ template <ConnectionType CType>
+ inline auto connect(auto* sender, auto signal, auto* context, auto slotLike,
+ 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 -
- // arguments are always copied (at best - COWed) to the context of
- // the slot. Therefore the slot decorator receives const ArgTs&...
- // rather than ArgTs&&...
- // TODO (C++20): std::bind_front() instead of lambda.
- c = QObject::connect(sender, signal, context,
- [pc = std::move(pc),
- decoratedSlot = std::move(decoratedSlot)](const ArgTs&... args) {
- Q_ASSERT(*pc); // If it's been triggered, it should exist
- decoratedSlot(*pc, args...);
+ auto pConn = std::make_unique<QMetaObject::Connection>();
+ auto& c = *pConn; // Save the reference before pConn is moved from
+ c = QObject::connect(
+ sender, signal, context,
+ [slotLike, pConn = std::move(pConn)](const auto&... args)
+ // The requires-expression below is necessary to prevent Qt
+ // from eagerly trying to fill the lambda with more arguments
+ // than slotLike() (i.e., the original slot) can handle
+ requires requires { slotLike(args...); } {
+ static_assert(CType == Until || CType == SingleShot,
+ "Unsupported disconnection type");
+ if constexpr (CType == SingleShot) {
+ // Disconnect early to avoid re-triggers during slotLike()
+ QObject::disconnect(*pConn);
+ // Qt kindly keeps slot objects until they do their job,
+ // even if they disconnect themselves in the process (see
+ // how doActivate() in qobject.cpp handles c->slotObj).
+ slotLike(args...);
+ } else if constexpr (CType == Until) {
+ if (slotLike(args...))
+ QObject::disconnect(*pConn);
+ }
},
connType);
return c;
}
- template <typename SenderT, typename SignalT, typename ContextT,
- typename... ArgTs>
- inline QMetaObject::Connection
- connectUntil(SenderT* sender, SignalT signal, ContextT* context,
- std::function<bool(ArgTs...)> functor,
- Qt::ConnectionType connType)
- {
- return connectDecorated(sender, signal, context,
- decorated_slot_tt<ArgTs...>(
- [functor = std::move(functor)](QMetaObject::Connection& c,
- const ArgTs&... args) {
- if (functor(args...))
- QObject::disconnect(c);
- }),
- connType);
- }
- template <typename SenderT, typename SignalT, typename ContextT,
- typename... ArgTs>
- inline QMetaObject::Connection
- connectSingleShot(SenderT* sender, SignalT signal, ContextT* context,
- std::function<void(ArgTs...)> slot,
- Qt::ConnectionType connType)
- {
- return connectDecorated(sender, signal, context,
- decorated_slot_tt<ArgTs...>(
- [slot = std::move(slot)](QMetaObject::Connection& c,
- const ArgTs&... args) {
- QObject::disconnect(c);
- slot(args...);
- }),
- connType);
- }
+
+ template <typename SlotT, typename ReceiverT>
+ concept PmfSlot =
+ (fn_arg_count_v<SlotT> > 0
+ && std::is_base_of_v<std::decay_t<fn_arg_t<SlotT, 0>>, ReceiverT>);
} // namespace _impl
-/*! \brief Create a connection that self-disconnects when its "slot" returns true
- *
- * A slot accepted by connectUntil() is different from classic Qt slots
- * in that its return value must be bool, not void. The slot's return value
- * controls whether the connection should be kept; if the slot returns false,
- * the connection remains; upon returning true, the slot is disconnected from
- * the signal. Because of a different slot signature connectUntil() doesn't
- * accept member functions as QObject::connect or Quotient::connectSingleShot
- * do; you should pass a lambda or a pre-bound member function to it.
- */
-template <typename SenderT, typename SignalT, typename ContextT, typename FunctorT>
-inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context,
- const FunctorT& slot,
+//! \brief Create a connection that self-disconnects when its slot returns true
+//!
+//! A slot accepted by connectUntil() is different from classic Qt slots
+//! in that its return value must be bool, not void. Because of that different
+//! signature connectUntil() doesn't accept member functions in the way
+//! QObject::connect or Quotient::connectSingleShot do; you should pass a lambda
+//! or a pre-bound member function to it.
+//! \return whether the connection should be dropped; false means that the
+//! connection remains; upon returning true, the slot is disconnected
+//! from the signal.
+inline auto connectUntil(auto* sender, auto signal, auto* context,
+ auto smartSlot,
Qt::ConnectionType connType = Qt::AutoConnection)
{
- return _impl::connectUntil(sender, signal, context, wrap_in_function(slot),
- connType);
+ return _impl::connect<_impl::Until>(sender, signal, context, smartSlot,
+ connType);
}
-/// Create a connection that self-disconnects after triggering on the signal
-template <typename SenderT, typename SignalT, typename ContextT, typename FunctorT>
-inline auto connectSingleShot(SenderT* sender, SignalT signal,
- ContextT* context, const FunctorT& slot,
+//! Create a connection that self-disconnects after triggering on the signal
+template <typename ContextT, typename SlotT>
+inline auto connectSingleShot(auto* sender, auto signal, ContextT* context,
+ SlotT slot,
Qt::ConnectionType connType = Qt::AutoConnection)
{
- return _impl::connectSingleShot(
- sender, signal, context, wrap_in_function(slot), connType);
-}
-
-// Specialisation for usual Qt slots passed as pointers-to-members.
-template <typename SenderT, typename SignalT, typename ReceiverT,
- typename SlotObjectT, typename... ArgTs>
-inline auto connectSingleShot(SenderT* sender, SignalT signal,
- ReceiverT* receiver,
- void (SlotObjectT::*slot)(ArgTs...),
- Qt::ConnectionType connType = Qt::AutoConnection)
-{
- // TODO: when switching to C++20, use std::bind_front() instead
- return _impl::connectSingleShot(sender, signal, receiver,
- wrap_in_function(
- [receiver, slot](const ArgTs&... args) {
- (receiver->*slot)(args...);
- }),
- connType);
+#if QT_VERSION_MAJOR >= 6
+ return QObject::connect(sender, signal, context, slot,
+ Qt::ConnectionType(connType
+ | Qt::SingleShotConnection));
+#else
+ // In case of classic Qt pointer-to-member-function slots the receiver
+ // object has to be pre-bound to the slot to make it self-contained
+ if constexpr (_impl::PmfSlot<SlotT, ContextT>) {
+ auto&& boundSlot =
+# if __cpp_lib_bind_front // Needs Apple Clang 13 (other platforms are fine)
+ std::bind_front(slot, context);
+# else
+ [context, slot](const auto&... args)
+ requires requires { (context->*slot)(args...); }
+ {
+ (context->*slot)(args...);
+ };
+# endif
+ return _impl::connect<_impl::SingleShot>(
+ sender, signal, context,
+ std::forward<decltype(boundSlot)>(boundSlot), connType);
+ } else {
+ return _impl::connect<_impl::SingleShot>(sender, signal, context, slot,
+ 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 bb05af05..7fec9274 100644
--- a/lib/quotient_common.h
+++ b/lib/quotient_common.h
@@ -1,20 +1,103 @@
+// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#pragma once
+#include "quotient_export.h"
+
#include <qobjectdefs.h>
+#include <array>
+
+
+//! \brief Quotient replacement for the Q_FLAG/Q_DECLARE_FLAGS combination
+//!
+//! Although the comment in QTBUG-82295 says that Q_FLAG[_NS] "should" be
+//! applied to the enum type only, Qt then doesn't allow to wrap the
+//! corresponding flag type (defined with Q_DECLARE_FLAGS) into a QVariant.
+//! This macro defines Q_FLAG and on top of that adds Q_ENUM_IMPL which is
+//! a part of Q_ENUM() macro that enables the metatype data but goes under
+//! the moc radar to avoid double registration of the same data in the map
+//! defined in moc_*.cpp.
+//!
+//! Simply put, instead of using Q_FLAG/Q_DECLARE_FLAGS combo (and struggling
+//! to figure out what you should pass to Q_FLAG if you want to make it
+//! wrappable in a QVariant) use the macro below, and things will just work.
+//!
+//! \sa https://bugreports.qt.io/browse/QTBUG-82295
+#define QUO_DECLARE_FLAGS(Flags, Enum) \
+ Q_DECLARE_FLAGS(Flags, Enum) \
+ Q_ENUM_IMPL(Enum) \
+ Q_FLAG(Flags)
+
+//! \brief Quotient replacement for the Q_FLAG_NS/Q_DECLARE_FLAGS combination
+//!
+//! This is the equivalent of QUO_DECLARE_FLAGS for enums declared at the
+//! namespace level (be sure to provide Q_NAMESPACE _in the same file_
+//! as the enum definition and this macro).
+//! \sa QUO_DECLARE_FLAGS
+#define QUO_DECLARE_FLAGS_NS(Flags, Enum) \
+ Q_DECLARE_FLAGS(Flags, Enum) \
+ Q_ENUM_NS_IMPL(Enum) \
+ Q_FLAG_NS(Flags)
+
namespace Quotient {
-Q_NAMESPACE
+Q_NAMESPACE_EXPORT(QUOTIENT_API)
-/** 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 };
+// 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 : uint16_t {
+ // Specific power-of-2 values (1,2,4,...) are important here as syncdata.cpp
+ // depends on that, as well as Join being the first in line
+ Invalid = 0x0,
+ Join = 0x1,
+ Leave = 0x2,
+ Invite = 0x4,
+ Knock = 0x8,
+ Ban = 0x10,
+ Undefined = Invalid
+};
+QUO_DECLARE_FLAGS_NS(MembershipMask, Membership)
+
+constexpr std::array MembershipStrings {
+ // The order MUST be the same as the order in the Membership enum
+ "join", "leave", "invite", "knock", "ban"
+};
+//! \brief Local user join-state names
+//!
+//! 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)
+
+[[maybe_unused]] constexpr std::array JoinStateStrings {
+ MembershipStrings[0], MembershipStrings[1], MembershipStrings[2],
+ MembershipStrings[3] /* same as MembershipStrings, sans "ban" */
+};
+
+//! \brief Network job running policy flags
+//!
+//! So far only background/foreground flags are available.
+//! \sa Connection::callApi, Connection::run
+enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 };
Q_ENUM_NS(RunningPolicy)
-enum UriResolveResult : short {
+//! \brief The result of URI resolution using UriResolver
+//! \sa UriResolver
+enum UriResolveResult : int8_t {
StillResolving = -1,
UriResolved = 0,
CouldNotResolve,
@@ -24,6 +107,20 @@ enum UriResolveResult : short {
};
Q_ENUM_NS(UriResolveResult)
+enum class RoomType : uint8_t {
+ Space = 0,
+ Undefined = 0xFF,
+};
+Q_ENUM_NS(RoomType)
+
+[[maybe_unused]] constexpr std::array RoomTypeStrings { "m.space" };
+
+enum class EncryptionType : uint8_t {
+ MegolmV1AesSha2 = 0,
+ Undefined = 0xFF,
+};
+Q_ENUM_NS(EncryptionType)
+
} // namespace Quotient
-/// \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/quotient_export.h b/lib/quotient_export.h
new file mode 100644
index 00000000..56767443
--- /dev/null
+++ b/lib/quotient_export.h
@@ -0,0 +1,25 @@
+// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QtCore/qglobal.h>
+
+#ifdef QUOTIENT_STATIC
+# define QUOTIENT_API
+# define QUOTIENT_HIDDEN
+#else
+# ifndef QUOTIENT_API
+# ifdef BUILDING_SHARED_QUOTIENT
+ /* We are building this library */
+# define QUOTIENT_API Q_DECL_EXPORT
+# else
+ /* We are using this library */
+# define QUOTIENT_API Q_DECL_IMPORT
+# endif
+# endif
+
+# ifndef QUOTIENT_HIDDEN
+# define QUOTIENT_HIDDEN Q_DECL_HIDDEN
+# endif
+#endif
diff --git a/lib/room.cpp b/lib/room.cpp
index 7631abe1..0cf818ce 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -1,37 +1,33 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name>
+// SPDX-FileCopyrightText: 2017 Marius Gripsgard <marius@ubports.com>
+// SPDX-FileCopyrightText: 2018 Josip Delic <delijati@googlemail.com>
+// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org>
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-FileCopyrightText: 2020 Ram Nad <ramnad1999@gmail.com>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "room.h"
#include "avatar.h"
#include "connection.h"
#include "converters.h"
-#include "e2ee.h"
#include "syncdata.h"
#include "user.h"
+#include "eventstats.h"
+#include "roomstateview.h"
+#include "qt_connection_util.h"
+
+// NB: since Qt 6, moc_room.cpp needs User fully defined
+#include "moc_room.cpp"
#include "csapi/account-data.h"
#include "csapi/banning.h"
#include "csapi/inviting.h"
#include "csapi/kicking.h"
#include "csapi/leaving.h"
-#include "csapi/receipts.h"
#include "csapi/read_markers.h"
+#include "csapi/receipts.h"
#include "csapi/redaction.h"
#include "csapi/room_send.h"
#include "csapi/room_state.h"
@@ -39,28 +35,24 @@
#include "csapi/rooms.h"
#include "csapi/tags.h"
-#include "events/callanswerevent.h"
-#include "events/callcandidatesevent.h"
-#include "events/callhangupevent.h"
-#include "events/callinviteevent.h"
+#include "events/callevents.h"
#include "events/encryptionevent.h"
#include "events/reactionevent.h"
#include "events/receiptevent.h"
#include "events/redactionevent.h"
#include "events/roomavatarevent.h"
+#include "events/roomcanonicalaliasevent.h"
#include "events/roomcreateevent.h"
#include "events/roommemberevent.h"
+#include "events/roompowerlevelsevent.h"
#include "events/roomtombstoneevent.h"
#include "events/simplestateevents.h"
#include "events/typingevent.h"
-#include "events/roompowerlevelsevent.h"
#include "jobs/downloadfilejob.h"
#include "jobs/mediathumbnailjob.h"
-#include "events/roomcanonicalaliasevent.h"
#include <QtCore/QDir>
#include <QtCore/QHash>
-#include <QtCore/QMimeDatabase>
#include <QtCore/QPointer>
#include <QtCore/QRegularExpression>
#include <QtCore/QStringBuilder> // for efficient string concats (operator%)
@@ -71,13 +63,15 @@
#include <functional>
#ifdef Quotient_E2EE_ENABLED
-#include <account.h> // QtOlm
-#include <errors.h> // QtOlm
-#include <groupsession.h> // QtOlm
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmaccount.h"
+#include "e2ee/qolminboundsession.h"
+#include "e2ee/qolmutility.h"
+#include "database.h"
#endif // Quotient_E2EE_ENABLED
+
using namespace Quotient;
-using namespace QtOlm;
using namespace std::placeholders;
using std::move;
#if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123)
@@ -109,7 +103,7 @@ public:
static decltype(baseState) stubbedState;
/// The state of the room at syncEdge()
/// \sa syncEdge
- QHash<StateEventKey, const StateEventBase*> currentState;
+ RoomStateView currentState;
/// Servers with aliases for this room except the one of the local user
/// \sa Room::remoteAliases
QSet<QString> aliasServers;
@@ -120,27 +114,31 @@ public:
// A map from evtId to a map of relation type to a vector of event
// pointers. Not using QMultiHash, because we want to quickly return
// a number of relations for a given event without enumerating them.
- QHash<QPair<QString, QString>, RelatedEvents> relations;
+ QHash<std::pair<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;
+ QHash<QString, ReadReceipt> lastReadReceipts;
QString fullyReadUntilEventId;
TagsMap tags;
UnorderedMap<QString, EventPtr> accountData;
QString prevBatch;
QPointer<GetRoomEventsJob> eventsHistoryJob;
QPointer<GetMembersByRoomJob> allMembersJob;
+ // Map from megolm sessionId to set of eventIds
+ UnorderedMap<QString, QSet<QString>> undecryptedEvents;
struct FileTransferPrivateInfo {
FileTransferPrivateInfo() = default;
@@ -207,9 +205,9 @@ public:
rev_iter_t historyEdge() const { return timeline.crend(); }
Timeline::const_iterator syncEdge() const { return timeline.cend(); }
- void getPreviousContent(int limit = 10);
+ void getPreviousContent(int limit = 10, const QString &filter = {});
- const StateEventBase* getCurrentState(const StateEventKey& evtKey) const
+ const StateEvent* getCurrentState(const StateEventKey& evtKey) const
{
const auto* evt = currentState.value(evtKey, nullptr);
if (!evt) {
@@ -217,10 +215,11 @@ public:
// In the absence of a real event, make a stub as-if an event
// with empty content has been received. Event classes should be
// prepared for empty/invalid/malicious content anyway.
- stubbedState.emplace(evtKey, loadStateEvent(evtKey.first, {},
- evtKey.second));
+ stubbedState.emplace(
+ evtKey, loadEvent<StateEvent>(evtKey.first, 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);
@@ -230,61 +229,20 @@ public:
return evt;
}
- template <typename EventT>
- const EventT* getCurrentState(const QString& stateKey = {}) const
- {
- const StateEventKey evtKey { EventT::matrixTypeId(), stateKey };
- const auto* evt = currentState.value(evtKey, nullptr);
- if (!evt) {
- if (stubbedState.find(evtKey) == stubbedState.end()) {
- // In the absence of a real event, make a stub as-if an event
- // with empty content has been received. Event classes should be
- // prepared for empty/invalid/malicious content anyway.
- stubbedState.emplace(
- evtKey, makeEvent<EventT>(basicStateEventJson(
- EventT::matrixTypeId(), {}, evtKey.second)));
- qCDebug(STATE) << "A new stub event created for key {"
- << evtKey.first << evtKey.second << "}";
- }
- evt = stubbedState[evtKey].get();
- Q_ASSERT(evt);
- }
- Q_ASSERT(evt->type() == EventT::typeId()
- && evt->matrixType() == EventT::matrixTypeId()
- && evt->stateKey() == stateKey);
- return static_cast<const EventT*>(evt);
- }
-
-// template <typename EventT>
-// const auto& getCurrentStateContent(const QString& stateKey = {}) const
-// {
-// if (const auto* evt =
-// currentState.value({ EventT::matrixTypeId(), stateKey }, nullptr))
-// return evt->content();
-// return EventT::content_type()
-// }
-
- bool isEventNotable(const TimelineItem& ti) const
- {
- return !ti->isRedacted() && ti->senderId() != connection->userId()
- && 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());
- // Update baseState afterwards to make sure that the old state
- // is valid and usable inside processStateEvent
- changes |= q->processStateEvent(evt);
- baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr);
+ if (auto change = q->processStateEvent(evt); change) {
+ changes |= change;
+ baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr);
+ }
}
if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
qCDebug(PROFILER)
@@ -296,6 +254,9 @@ public:
Changes addNewMessageEvents(RoomEvents&& events);
void addHistoricalMessageEvents(RoomEvents&& events);
+ Changes updateStatsFromSyncData(const SyncRoomData &data, bool fromCache);
+ void postprocessChanges(Changes changes, bool saveState = true);
+
/** Move events into the timeline
*
* Insert events into the timeline, either new or historical.
@@ -312,13 +273,20 @@ public:
* Remove events from the passed container that are already in the timeline
*/
void dropDuplicateEvents(RoomEvents& events) const;
-
- void setLastReadReceipt(User* u, rev_iter_t newMarker,
- QString newEvtId = {});
+ void decryptIncomingEvents(RoomEvents& events);
+
+ //! \brief update last receipt record for a given user
+ //!
+ //! \return previous event id of the receipt if the new receipt changed
+ //! it, or `none` if no change took place
+ Omittable<QString> setLastReadReceipt(const QString& userId, rev_iter_t newMarker,
+ ReadReceipt newReceipt = {});
+ Changes setLocalLastReadReceipt(const rev_iter_t& newMarker,
+ ReadReceipt newReceipt = {},
+ bool deferStatsUpdate = false);
Changes setFullyReadMarker(const QString &eventId);
- Changes updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to);
- Changes recalculateUnreadCount(bool force = false);
- void markMessagesAsRead(const rev_iter_t &upToMarker);
+ Changes updateStats(const rev_iter_t& from, const rev_iter_t& to);
+ bool markMessagesAsRead(const rev_iter_t& upToMarker);
void getAllMembers();
@@ -330,12 +298,16 @@ 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);
void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr);
- SetRoomStateWithKeyJob* requestSetState(const StateEventBase& event)
+ SetRoomStateWithKeyJob* requestSetState(const QString& evtType,
+ const QString& stateKey,
+ const QJsonObject& contentJson)
{
// if (event.roomId().isEmpty())
// event.setRoomId(id);
@@ -343,14 +315,8 @@ public:
// event.setSender(connection->userId());
// TODO: Queue up state events sending (see #133).
// TODO: Maybe addAsPending() as well, despite having no txnId
- return connection->callApi<SetRoomStateWithKeyJob>(
- id, event.matrixType(), event.stateKey(), event.contentJson());
- }
-
- template <typename EvT, typename... ArgTs>
- auto requestSetState(ArgTs&&... args)
- {
- return requestSetState(EvT(std::forward<ArgTs>(args)...));
+ return connection->callApi<SetRoomStateWithKeyJob>(id, evtType, stateKey,
+ contentJson);
}
/*! Apply redaction to the timeline
@@ -376,87 +342,122 @@ public:
bool isLocalUser(const User* u) const { return u == q->localUser(); }
#ifdef Quotient_E2EE_ENABLED
- // A map from <sessionId, messageIndex> to <event_id, origin_server_ts>
- QHash<QPair<QString, uint32_t>, QPair<QString, QDateTime>>
- groupSessionIndexRecord; // TODO: cache
- // A map from senderKey to a map of sessionId to InboundGroupSession
- // Not using QMultiHash, because we want to quickly return
- // a number of relations for a given event without enumerating them.
- QHash<QPair<QString, QString>, InboundGroupSession*> groupSessions; // TODO:
- // cache
- bool addInboundGroupSession(QString senderKey, QString sessionId,
- QString sessionKey)
+ UnorderedMap<QString, QOlmInboundGroupSessionPtr> groupSessions;
+ int currentMegolmSessionMessageCount = 0;
+ //TODO save this to database
+ unsigned long long currentMegolmSessionCreationTimestamp = 0;
+ QOlmOutboundGroupSessionPtr currentOutboundMegolmSession = nullptr;
+
+ bool addInboundGroupSession(QString sessionId, QByteArray sessionKey,
+ const QString& senderId,
+ const QString& olmSessionId)
{
- if (groupSessions.contains({ senderKey, sessionId })) {
- qCDebug(E2EE) << "Inbound Megolm session" << sessionId
- << "with senderKey" << senderKey << "already exists";
+ if (groupSessions.contains(sessionId)) {
+ qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "already exists";
return false;
}
- InboundGroupSession* megolmSession;
- try {
- megolmSession = new InboundGroupSession(sessionKey.toLatin1(),
- InboundGroupSession::Init,
- q);
- } catch (OlmError* e) {
- qCDebug(E2EE) << "Unable to create new InboundGroupSession"
- << e->what();
- return false;
- }
- if (megolmSession->id() != sessionId) {
- qCDebug(E2EE) << "Session ID mismatch in m.room_key event sent "
- "from sender with key"
- << senderKey;
+ auto expectedMegolmSession = QOlmInboundGroupSession::create(sessionKey);
+ Q_ASSERT(expectedMegolmSession.has_value());
+ auto&& megolmSession = *expectedMegolmSession;
+ if (megolmSession->sessionId() != sessionId) {
+ qCWarning(E2EE) << "Session ID mismatch in m.room_key event";
return false;
}
- groupSessions.insert({ senderKey, sessionId }, megolmSession);
+ megolmSession->setSenderId(senderId);
+ megolmSession->setOlmSessionId(olmSessionId);
+ qCWarning(E2EE) << "Adding inbound session";
+ connection->saveMegolmSession(q, *megolmSession);
+ groupSessions[sessionId] = std::move(megolmSession);
return true;
}
QString groupSessionDecryptMessage(QByteArray cipher,
- const QString& senderKey,
const QString& sessionId,
const QString& eventId,
- QDateTime timestamp)
+ QDateTime timestamp,
+ const QString& senderId)
{
- std::pair<QString, uint32_t> decrypted;
- QPair<QString, QString> senderSessionPairKey =
- qMakePair(senderKey, sessionId);
- if (!groupSessions.contains(senderSessionPairKey)) {
- qCDebug(E2EE) << "Unable to decrypt event" << eventId
- << "The sender's device has not sent us the keys for "
- "this message";
- return QString();
+ auto groupSessionIt = groupSessions.find(sessionId);
+ if (groupSessionIt == groupSessions.end()) {
+ // qCWarning(E2EE) << "Unable to decrypt event" << eventId
+ // << "The sender's device has not sent us the keys for "
+ // "this message";
+ return {};
}
- InboundGroupSession* senderSession =
- groupSessions.value(senderSessionPairKey);
- if (!senderSession) {
- qCDebug(E2EE) << "Unable to decrypt event" << eventId
- << "senderSessionPairKey:" << senderSessionPairKey;
- return QString();
+ auto& senderSession = groupSessionIt->second;
+ if (senderSession->senderId() != senderId) {
+ qCWarning(E2EE) << "Sender from event does not match sender from session";
+ return {};
}
- try {
- decrypted = senderSession->decrypt(cipher);
- } catch (OlmError* e) {
- qCDebug(E2EE) << "Unable to decrypt event" << eventId
- << "with matching megolm session:" << e->what();
- return QString();
+ auto decryptResult = senderSession->decrypt(cipher);
+ if(!decryptResult) {
+ qCWarning(E2EE) << "Unable to decrypt event" << eventId
+ << "with matching megolm session:" << decryptResult.error();
+ return {};
}
- QPair<QString, QDateTime> properties = groupSessionIndexRecord.value(
- qMakePair(senderSession->id(), decrypted.second));
- if (properties.first.isEmpty()) {
- groupSessionIndexRecord.insert(qMakePair(senderSession->id(),
- decrypted.second),
- qMakePair(eventId, timestamp));
+ const auto& [content, index] = *decryptResult;
+ const auto& [recordEventId, ts] =
+ q->connection()->database()->groupSessionIndexRecord(
+ q->id(), senderSession->sessionId(), index);
+ if (recordEventId.isEmpty()) {
+ q->connection()->database()->addGroupSessionIndexRecord(
+ q->id(), senderSession->sessionId(), index, eventId,
+ timestamp.toMSecsSinceEpoch());
} else {
- if ((properties.first != eventId)
- || (properties.second != timestamp)) {
- qCDebug(E2EE) << "Detected a replay attack on event" << eventId;
- return QString();
+ if ((eventId != recordEventId)
+ || (ts != timestamp.toMSecsSinceEpoch())) {
+ qCWarning(E2EE) << "Detected a replay attack on event" << eventId;
+ return {};
}
}
+ return content;
+ }
+
+ bool shouldRotateMegolmSession() const
+ {
+ const auto* encryptionConfig = currentState.get<EncryptionEvent>();
+ if (!encryptionConfig || !encryptionConfig->useEncryption())
+ return false;
+
+ const auto rotationInterval = encryptionConfig->rotationPeriodMs();
+ const auto rotationMessageCount = encryptionConfig->rotationPeriodMsgs();
+ return currentOutboundMegolmSession->messageCount()
+ >= rotationMessageCount
+ || currentOutboundMegolmSession->creationTime().addMSecs(
+ rotationInterval)
+ < QDateTime::currentDateTime();
+ }
+
+ bool hasValidMegolmSession() const
+ {
+ if (!q->usesEncryption()) {
+ return false;
+ }
+ return currentOutboundMegolmSession != nullptr;
+ }
+
+ void createMegolmSession() {
+ qCDebug(E2EE) << "Creating new outbound megolm session for room "
+ << q->objectName();
+ currentOutboundMegolmSession = QOlmOutboundGroupSession::create();
+ connection->saveCurrentOutboundMegolmSession(
+ id, *currentOutboundMegolmSession);
- return decrypted.first;
+ addInboundGroupSession(currentOutboundMegolmSession->sessionId(),
+ currentOutboundMegolmSession->sessionKey(),
+ q->localUser()->id(), "SELF"_ls);
+ }
+
+ QMultiHash<QString, QString> getDevicesWithoutKey() const
+ {
+ QMultiHash<QString, QString> devices;
+ for (const auto& user : q->users())
+ for (const auto& deviceId : connection->devicesForUser(user->id()))
+ devices.insert(user->id(), deviceId);
+
+ return connection->database()->devicesWithoutKey(
+ id, devices, currentOutboundMegolmSession->sessionId());
}
#endif // Quotient_E2EE_ENABLED
@@ -477,12 +478,37 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
// https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/
d->q = this;
d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name
- connectUntil(connection, &Connection::loadedRoomState, this, [this](Room* r) {
- if (this == r)
- emit baseStateLoaded();
- return this == r; // loadedRoomState fires only once per room
+#ifdef Quotient_E2EE_ENABLED
+ connectSingleShot(this, &Room::encryption, this, [this, connection](){
+ connection->encryptionUpdate(this);
+ });
+ connect(this, &Room::userAdded, this, [this, connection](){
+ if(usesEncryption()) {
+ connection->encryptionUpdate(this);
+ }
+ });
+ d->groupSessions = connection->loadRoomMegolmSessions(this);
+ d->currentOutboundMegolmSession =
+ connection->loadCurrentOutboundMegolmSession(this->id());
+ if (d->shouldRotateMegolmSession()) {
+ d->currentOutboundMegolmSession = nullptr;
+ }
+ connect(this, &Room::userRemoved, this, [this](){
+ if (!usesEncryption()) {
+ return;
+ }
+ if (d->hasValidMegolmSession()) {
+ d->createMegolmSession();
+ }
+ qCDebug(E2EE) << "Invalidating current megolm session because user left";
+
});
- qCDebug(STATE) << "New" << toCString(initialJoinState) << "Room:" << id;
+
+ connect(this, &Room::beforeDestruction, this, [=](){
+ connection->database()->clearRoomData(id);
+ });
+#endif
+ qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id;
}
Room::~Room() { delete d; }
@@ -491,8 +517,8 @@ const QString& Room::id() const { return d->id; }
QString Room::version() const
{
- const auto v = d->getCurrentState<RoomCreateEvent>()->version();
- return v.isEmpty() ? QStringLiteral("1") : v;
+ const auto v = currentState().query(&RoomCreateEvent::version);
+ return v && !v->isEmpty() ? *v : QStringLiteral("1");
}
bool Room::isUnstable() const
@@ -503,7 +529,10 @@ bool Room::isUnstable() const
QString Room::predecessorId() const
{
- return d->getCurrentState<RoomCreateEvent>()->predecessor().roomId;
+ if (const auto* evt = currentState().get<RoomCreateEvent>())
+ return evt->predecessor().roomId;
+
+ return {};
}
Room* Room::predecessor(JoinStates statesFilter) const
@@ -518,7 +547,8 @@ Room* Room::predecessor(JoinStates statesFilter) const
QString Room::successorId() const
{
- return d->getCurrentState<RoomTombstoneEvent>()->successorRoomId();
+ return currentState().queryOr(&RoomTombstoneEvent::successorRoomId,
+ QString());
}
Room* Room::successor(JoinStates statesFilter) const
@@ -545,50 +575,56 @@ bool Room::allHistoryLoaded() const
QString Room::name() const
{
- return d->getCurrentState<RoomNameEvent>()->name();
+ return currentState().content<RoomNameEvent>().value;
}
QStringList Room::aliases() const
{
- const auto* evt = d->getCurrentState<RoomCanonicalAliasEvent>();
- auto result = evt->altAliases();
- if (!evt->alias().isEmpty())
- result << evt->alias();
- return result;
+ if (const auto* evt = currentState().get<RoomCanonicalAliasEvent>()) {
+ auto result = evt->altAliases();
+ if (!evt->alias().isEmpty())
+ result << evt->alias();
+ return result;
+ }
+ return {};
}
QStringList Room::altAliases() const
{
- return d->getCurrentState<RoomCanonicalAliasEvent>()->altAliases();
+ return currentState().content<RoomCanonicalAliasEvent>().altAliases;
}
-QStringList Room::localAliases() const
+QString Room::canonicalAlias() const
{
- return d->getCurrentState<RoomAliasesEvent>(
- connection()->domain())
- ->aliases();
+ return currentState().queryOr(&RoomCanonicalAliasEvent::alias, QString());
}
-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::displayName() const { return d->displayname; }
+
+QStringList Room::pinnedEventIds() const {
+ return currentState().queryOr(&RoomPinnedEvent::pinnedEvents, QStringList());
}
-QString Room::canonicalAlias() const
+QVector<const Quotient::RoomEvent*> Quotient::Room::pinnedEvents() const
{
- return d->getCurrentState<RoomCanonicalAliasEvent>()->alias();
+ QVector<const RoomEvent*> pinnedEvents;
+ for (const auto& evtId : pinnedEventIds())
+ if (const auto& it = findInTimeline(evtId); it != historyEdge())
+ pinnedEvents.append(it->event());
+
+ return pinnedEvents;
}
-QString Room::displayName() const { return d->displayname; }
+QString Room::displayNameForHtml() const
+{
+ return displayName().toHtmlEscaped();
+}
void Room::refreshDisplayName() { d->updateDisplayname(); }
QString Room::topic() const
{
- return d->getCurrentState<RoomTopicEvent>()->topic();
+ return currentState().queryOr(&RoomTopicEvent::topic, QString());
}
QString Room::avatarMediaId() const { return d->avatar.mediaId(); }
@@ -603,13 +639,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 {};
}
@@ -621,9 +657,19 @@ User* Room::user(const QString& userId) const
JoinState Room::memberJoinState(User* user) const
{
- return user != nullptr && d->membersMap.contains(user->name(this), user)
- ? JoinState::Join
- : JoinState::Leave;
+ return d->membersMap.contains(user->name(this), user) ? JoinState::Join
+ : JoinState::Leave;
+}
+
+Membership Room::memberState(const QString& userId) const
+{
+ return currentState().queryOr(userId, &RoomMemberEvent::membership,
+ Membership::Leave);
+}
+
+bool Room::isMember(const QString& userId) const
+{
+ return memberState(userId) == Membership::Join;
}
JoinState Room::joinState() const { return d->joinState; }
@@ -634,195 +680,277 @@ 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);
}
-void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker,
- QString newEvtId)
+Omittable<QString> Room::Private::setLastReadReceipt(const QString& userId,
+ rev_iter_t newMarker,
+ ReadReceipt newReceipt)
{
- if (!u) {
- Q_ASSERT(u != nullptr); // For Debug builds
- qCCritical(MAIN) << "Empty user, skipping read receipt registration";
- return; // For Release builds
- }
- if (q->memberJoinState(u) != JoinState::Join) {
- qCWarning(EPHEMERAL)
- << "Won't record read receipt for non-member" << u->id();
- return;
- }
-
- if (newMarker == historyEdge() && !newEvtId.isEmpty())
- newMarker = q->findInTimeline(newEvtId);
+ if (newMarker == historyEdge() && !newReceipt.eventId.isEmpty())
+ newMarker = q->findInTimeline(newReceipt.eventId);
if (newMarker != historyEdge()) {
- // NB: with reverse iterators, timeline history >= sync edge
- if (newMarker >= q->readMarker(u)) {
- qCDebug(EPHEMERAL) << "The new read receipt for" << u->id()
- << "is at or behind the old one, skipping";
- return;
- }
-
// Try to auto-promote the read marker over the user's own messages
// (switch to direct iterators for that).
const auto eagerMarker = find_if(newMarker.base(), syncEdge(),
[=](const TimelineItem& ti) {
- return ti->senderId() != u->id();
- })
- - 1;
- newEvtId = (*eagerMarker)->id();
- if (eagerMarker != newMarker.base() - 1) // &*(rIt.base() - 1) === &*rIt
- qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << u->id()
- << "to" << newEvtId;
- }
+ return ti->senderId() != userId;
+ });
+ // eagerMarker is now just after the desired event for newMarker
+ if (eagerMarker != newMarker.base()) {
+ newMarker = rev_iter_t(eagerMarker);
+ qDebug(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.
+ // This logic tackles, in particular, the case when the new event is not
+ // found (most likely, because it's too old and hasn't been fetched from
+ // the server yet) but there is a previous marker for a user; in that case,
+ // the previous marker is kept because read receipts are not supposed
+ // to move backwards. If neither new nor old event is found, the new receipt
+ // is blindly stored, in a hope it's also "newer" in the timeline.
+ // NB: with reverse iterators, timeline history edge >= sync edge
+ if (prevEventId == newReceipt.eventId
+ || newMarker > q->findInTimeline(prevEventId))
+ return {};
- auto& storedId = lastReadEventIds[u];
- if (storedId == newEvtId)
- return;
// Finally make the change
- eventIdReadUsers.remove(storedId, u);
- eventIdReadUsers.insert(newEvtId, u);
- swap(storedId, newEvtId); // Now newEvtId actually stores the old eventId
- qCDebug(EPHEMERAL) << "The new read receipt for" << u->id() << "is at"
- << storedId;
- emit q->lastReadEventChanged(u);
- if (!isLocalUser(u))
- emit q->readMarkerForUserMoved(u, newEvtId, storedId);
+
+ 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); // NB: qCDebug can't be used like that
+ dbg << "The new read receipt for" << userId << "is now at";
+ if (newMarker == historyEdge())
+ dbg << storedReceipt.eventId;
+ else
+ dbg << *newMarker;
+ }
+
+ // NB: This method, unlike setLocalLastReadReceipt, doesn't emit
+ // lastReadEventChanged() to avoid numerous emissions when many read
+ // receipts arrive. It can be called thousands of times during an initial
+ // sync, e.g.
+ // TODO: remove in 0.8
+ if (const auto member = q->user(userId); !isLocalUser(member))
+ QT_IGNORE_DEPRECATIONS(emit q->readMarkerForUserMoved(
+ member, prevEventId, storedReceipt.eventId);)
+ return prevEventId;
+}
+
+Room::Changes Room::Private::setLocalLastReadReceipt(const rev_iter_t& newMarker,
+ ReadReceipt newReceipt,
+ bool deferStatsUpdate)
+{
+ auto prevEventId =
+ setLastReadReceipt(connection->userId(), newMarker, move(newReceipt));
+ if (!prevEventId)
+ return Change::None;
+ Changes changes = Change::Other;
+ if (!deferStatsUpdate) {
+ if (unreadStats.updateOnMarkerMove(q, q->findInTimeline(*prevEventId),
+ newMarker)) {
+ qDebug(MESSAGES)
+ << "Updated unread event statistics in" << q->objectName()
+ << "after moving the local read receipt:" << unreadStats;
+ changes |= Change::UnreadStats;
+ }
+ Q_ASSERT(unreadStats.isValidFor(q, newMarker)); // post-check
+ }
+ emit q->lastReadEventChanged({ connection->userId() });
+ return changes;
}
-Room::Changes Room::Private::updateUnreadCount(const rev_iter_t& from,
- const rev_iter_t& to)
+Room::Changes Room::Private::updateStats(const rev_iter_t& from,
+ const rev_iter_t& to)
{
Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend());
Q_ASSERT(to >= from && to <= timeline.crend());
- auto fullyReadMarker = q->readMarker();
+ 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
+ && setLocalLastReadReceipt(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 NoChange; // What's arrived is already fully read
+ return Change::None; // What's arrived is already fully read
// If there's no read marker in the whole room, initialise it
if (fullyReadMarker == historyEdge() && q->allHistoryLoaded())
return setFullyReadMarker(timeline.front()->id());
- // Catch a special case when the last fully read event id refers to an
- // event that has just arrived. In this case we should recalculate
- // unreadMessages to get an exact number instead of an estimation
- // (see https://github.com/quotient-im/libQuotient/wiki/unread_count).
- // For the same reason (switching from the estimation to the exact
- // number) this branch always emits unreadMessagesChanged() and returns
- // UnreadNotifsChange, even if the estimation luckily matched the exact
- // result.
- if (fullyReadMarker < to)
- return recalculateUnreadCount(true);
-
- // At this point the fully read marker is somewhere beyond the "oldest"
- // message from the arrived batch - add up newly arrived messages to
- // the current counter, instead of a complete recalculation.
- Q_ASSERT(to <= fullyReadMarker);
+ // Catch a case when the id in the last fully read marker or the local read
+ // receipt refers to an event that has just arrived. In this case either
+ // one (unreadStats) or both statistics should be recalculated to get
+ // an exact number instead of an estimation (see documentation on
+ // EventStats::isEstimate). For the same reason (switching from the
+ // estimate to the exact number) this branch forces returning
+ // Change::UnreadStats and also possibly Change::PartiallyReadStats, even if
+ // the estimation luckily matched the exact result.
+ if (readReceiptMarker < to || changes /*i.e. read receipt was corrected*/) {
+ unreadStats = EventStats::fromMarker(q, readReceiptMarker);
+ Q_ASSERT(!unreadStats.isEstimate);
+ qCDebug(MESSAGES).nospace() << "Recalculated unread event statistics in"
+ << q->objectName() << ": " << unreadStats;
+ changes |= Change::UnreadStats;
+ if (fullyReadMarker < to) {
+ // Add up to unreadStats instead of counting same events again
+ partiallyReadStats = EventStats::fromRange(q, readReceiptMarker,
+ q->fullyReadMarker(),
+ unreadStats);
+ Q_ASSERT(!partiallyReadStats.isEstimate);
+
+ qCDebug(MESSAGES).nospace()
+ << "Recalculated partially read event statistics in "
+ << q->objectName() << ": " << partiallyReadStats;
+ return changes | Change::PartiallyReadStats;
+ }
+ }
- 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 in"
- << q->objectName() << "took" << et;
-
- if (newUnreadMessages == 0)
- return NoChange;
-
- // 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() == historyEdge()
- ? "in total at least"
- : "in total")
- << unreadMessages << "unread message(s)";
- emit q->unreadMessagesChanged(q);
- return UnreadNotifsChange;
-}
-
-Room::Changes Room::Private::recalculateUnreadCount(bool force)
-{
- // The recalculation logic assumes that the fully read marker points at
- // a specific position in the timeline
- Q_ASSERT(q->readMarker() != historyEdge());
- const auto oldUnreadCount = unreadMessages;
- QElapsedTimer et;
- et.start();
- unreadMessages =
- int(count_if(timeline.crbegin(), q->readMarker(),
- [this](const auto& ti) { return isEventNotable(ti); }));
- if (et.nsecsElapsed() > profilerMinNsecs() / 10)
- qCDebug(PROFILER) << "Recounting unread messages in" << q->objectName()
- << "took" << et;
+ // As of here, at least the fully read marker (but maybe also read receipt)
+ // points to somewhere beyond the "oldest" message from the arrived batch -
+ // add up newly arrived messages to the current stats, instead of a complete
+ // recalculation.
+ Q_ASSERT(fullyReadMarker >= to);
- // See https://github.com/quotient-im/libQuotient/wiki/unread_count
- if (unreadMessages == 0)
- unreadMessages = -1;
+ const auto newStats = EventStats::fromRange(q, from, to);
+ Q_ASSERT(!newStats.isEstimate);
+ if (newStats.empty())
+ return changes;
- if (!force && unreadMessages == oldUnreadCount)
- return NoChange;
+ 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 (unreadMessages == -1)
- qCDebug(MESSAGES)
- << "Room" << displayname << "has no more unread messages";
- else
- qCDebug(MESSAGES) << "Room" << displayname << "still has"
- << unreadMessages << "unread message(s)";
- emit q->unreadMessagesChanged(q);
- return UnreadNotifsChange;
+ 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::setFullyReadMarker(const QString& eventId)
{
if (fullyReadUntilEventId == eventId)
- return NoChange;
+ return Change::None;
+
+ const auto prevReadMarker = q->fullyReadMarker();
+ const auto newReadMarker = q->findInTimeline(eventId);
+ if (newReadMarker > prevReadMarker)
+ return Change::None;
const auto prevFullyReadId = std::exchange(fullyReadUntilEventId, eventId);
qCDebug(MESSAGES) << "Fully read marker in" << q->objectName() //
<< "set to" << fullyReadUntilEventId;
- emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId);
-
- Changes changes = ReadMarkerChange;
- if (const auto rm = q->readMarker(); rm != historyEdge()) {
- // Pull read receipt if it's behind
- if (auto rr = q->readMarker(q->localUser()); rr > rm)
- setLastReadReceipt(q->localUser(), rm);
- changes |= recalculateUnreadCount();
+ 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 |= setLocalLastReadReceipt(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
+ QT_IGNORE_DEPRECATIONS(
+ emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId);)
return changes;
}
-void Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker)
+void Room::setReadReceipt(const QString& atEventId)
{
- if (upToMarker < q->readMarker()) {
- setFullyReadMarker((*upToMarker)->id());
- // Assuming that if a read receipt was sent on a newer event, it will
- // stay there instead of "un-reading" notifications/mentions from
- // m.fully_read to m.read
+ if (const auto changes =
+ d->setLocalLastReadReceipt(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(QString uptoEventId)
+void Room::markMessagesAsRead(const QString& uptoEventId)
{
d->markMessagesAsRead(findInTimeline(uptoEventId));
}
void Room::markAllMessagesAsRead()
{
- if (!d->timeline.empty())
- d->markMessagesAsRead(d->timeline.crbegin());
+ d->markMessagesAsRead(d->timeline.crbegin());
}
bool Room::canSwitchVersions() const
@@ -830,8 +958,9 @@ bool Room::canSwitchVersions() const
if (!successorId().isEmpty())
return false; // No one can upgrade a room that's already upgraded
- if (const auto* plEvt = d->getCurrentState<RoomPowerLevelsEvent>()) {
- const auto currentUserLevel = plEvt->powerLevelForUser(localUser()->id());
+ if (const auto* plEvt = currentState().get<RoomPowerLevelsEvent>()) {
+ const auto currentUserLevel =
+ plEvt->powerLevelForUser(localUser()->id());
const auto tombstonePowerLevel =
plEvt->powerLevelForState("m.room.tombstone"_ls);
return currentUserLevel >= tombstonePowerLevel;
@@ -839,16 +968,45 @@ 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());
+}
+
+Notification Room::checkForNotifications(const TimelineItem &ti)
+{
+ return { Notification::None };
+}
+
+bool Room::hasUnreadMessages() const { return !d->partiallyReadStats.empty(); }
+
+int countFromStats(const EventStats& s)
+{
+ return s.empty() ? -1 : int(s.notableCount);
+}
+
+int Room::unreadCount() const { return countFromStats(partiallyReadStats()); }
+
+EventStats Room::partiallyReadStats() const { return d->partiallyReadStats; }
-int Room::unreadCount() const { return d->unreadMessages; }
+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(); }
-Room::rev_iter_t Room::timelineEdge() const { return d->historyEdge(); }
-
TimelineItem::index_t Room::minTimelineIndex() const
{
return d->timeline.empty() ? 0 : d->timeline.front().index();
@@ -867,7 +1025,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);
}
@@ -898,28 +1056,38 @@ Room::findPendingEvent(const QString& txnId) const
});
}
-const Room::RelatedEvents Room::relatedEvents(const QString& evtId,
- const char* relType) const
+const Room::RelatedEvents Room::relatedEvents(
+ const QString& evtId, EventRelation::reltypeid_t relType) const
{
return d->relations.value({ evtId, relType });
}
-const Room::RelatedEvents Room::relatedEvents(const RoomEvent& evt,
- const char* relType) const
+const Room::RelatedEvents Room::relatedEvents(
+ const RoomEvent& evt, EventRelation::reltypeid_t relType) const
{
return relatedEvents(evt.id(), relType);
}
+const RoomCreateEvent* Room::creation() const
+{
+ return currentState().get<RoomCreateEvent>();
+}
+
+const RoomTombstoneEvent *Room::tombstone() const
+{
+ return currentState().get<RoomTombstoneEvent>();
+}
+
void Room::Private::getAllMembers()
{
// If already loaded or already loading, there's nothing to do here.
- if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob))
+ if (q->joinedCount() <= membersMap.size() || isJobPending(allMembersJob))
return;
allMembersJob = connection->callApi<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
@@ -929,8 +1097,7 @@ void Room::Private::getAllMembers()
it != syncEdge(); ++it)
if (is<RoomMemberEvent>(**it))
roomChanges |= q->processStateEvent(**it);
- if (roomChanges & MembersChange)
- emit q->memberListChanged();
+ postprocessChanges(roomChanges);
emit q->allMembersLoaded();
});
}
@@ -995,12 +1162,6 @@ void Room::setLastDisplayedEventId(const QString& eventId)
d->lastDisplayedEventId = eventId;
emit lastDisplayedEventChanged();
- if (d->displayed && marker < readMarker(localUser())) {
- d->setLastReadReceipt(localUser(), marker);
- connection()->callApi<PostReceiptJob>(BackgroundRequest, id(),
- QStringLiteral("m.read"),
- QUrl::toPercentEncoding(eventId));
- }
}
void Room::setLastDisplayedEvent(TimelineItem::index_t index)
@@ -1012,41 +1173,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 fullyReadMarker(); }
+
+QString Room::readMarkerEventId() const { return lastFullyReadEventId(); }
+
+ReadReceipt Room::lastReadReceipt(const QString& userId) const
+{
+ return d->lastReadReceipts.value(userId);
+}
+
+ReadReceipt Room::lastLocalReadReceipt() const
+{
+ return d->lastReadReceipts.value(localUser()->id());
+}
+
+Room::rev_iter_t Room::localReadReceiptMarker() const
+{
+ return findInTimeline(lastLocalReadReceipt().eventId);
}
-Room::rev_iter_t Room::readMarker() const
+QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; }
+
+Room::rev_iter_t Room::fullyReadMarker() const
{
return findInTimeline(d->fullyReadUntilEventId);
}
-QString Room::readMarkerEventId() const
+QSet<QString> Room::userIdsAtEvent(const QString& eventId)
{
- return d->fullyReadUntilEventId;
+ return d->eventIdReadUsers.value(eventId);
}
-QList<User*> Room::usersAtEventId(const QString& eventId)
+QSet<User*> Room::usersAtEventId(const QString& eventId)
{
- return d->eventIdReadUsers.values(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;
}
-int Room::notificationCount() const { return d->notificationCount; }
+qsizetype Room::notificationCount() const
+{
+ return d->unreadStats.notableCount;
+}
void Room::resetNotificationCount()
{
- if (d->notificationCount == 0)
+ if (d->unreadStats.notableCount == 0)
return;
- d->notificationCount = 0;
+ d->unreadStats.notableCount = 0;
emit notificationCountChanged();
}
-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();
}
@@ -1141,8 +1331,8 @@ void Room::setTags(TagsMap newTags, ActionScope applyOn)
d->setTags(move(newTags));
connection()->callApi<SetAccountDataPerRoomJob>(
- localUser()->id(), id(), TagEvent::matrixTypeId(),
- TagEvent(d->tags).contentJson());
+ localUser()->id(), id(), TagEvent::TypeId,
+ Quotient::toJson(TagEvent::content_type { d->tags }));
if (propagate) {
for (auto* r = this; (r = r->successor(joinStates));)
@@ -1184,6 +1374,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("[/\\<>|\"*?:]"), "_");
@@ -1237,9 +1438,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 {};
@@ -1250,8 +1450,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 {};
}
@@ -1316,29 +1515,49 @@ QList<User*> Room::users() const { return d->membersMap.values(); }
QStringList Room::memberNames() const
{
+ return safeMemberNames();
+}
+
+QStringList Room::safeMemberNames() const
+{
QStringList res;
res.reserve(d->membersMap.size());
- for (auto u : qAsConst(d->membersMap))
- res.append(roomMembername(u));
+ for (const auto* u: std::as_const(d->membersMap))
+ res.append(safeMemberName(u->id()));
return res;
}
-int Room::memberCount() const { return d->membersMap.size(); }
+QStringList Room::htmlSafeMemberNames() const
+{
+ QStringList res;
+ res.reserve(d->membersMap.size());
+ for (const auto* u: std::as_const(d->membersMap))
+ res.append(htmlSafeMemberName(u->id()));
+
+ return res;
+}
int Room::timelineSize() const { return int(d->timeline.size()); }
bool Room::usesEncryption() const
{
- return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty();
+ return !currentState()
+ .queryOr(&EncryptionEvent::algorithm, QString())
+ .isEmpty();
}
-const StateEventBase* Room::getCurrentState(const QString& evtType,
- const QString& stateKey) const
+const StateEvent* Room::getCurrentState(const QString& evtType,
+ const QString& stateKey) const
{
return d->getCurrentState({ evtType, stateKey });
}
+RoomStateView Room::currentState() const
+{
+ return d->currentState;
+}
+
RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent)
{
#ifndef Quotient_E2EE_ENABLED
@@ -1346,39 +1565,64 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent)
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
return {};
#else // Quotient_E2EE_ENABLED
- if (encryptedEvent.algorithm() == MegolmV1AesSha2AlgoKey) {
- QString decrypted = d->groupSessionDecryptMessage(
- encryptedEvent.ciphertext(), encryptedEvent.senderKey(),
- encryptedEvent.sessionId(), encryptedEvent.id(),
- encryptedEvent.originTimestamp());
- if (decrypted.isEmpty()) {
- return {};
- }
- return makeEvent<RoomMessageEvent>(
- QJsonDocument::fromJson(decrypted.toUtf8()).object());
+ if (encryptedEvent.algorithm() != MegolmV1AesSha2AlgoKey) {
+ qWarning(E2EE) << "Algorithm of the encrypted event with id"
+ << encryptedEvent.id() << "is not decryptable by the current device";
+ return {};
+ }
+ QString decrypted = d->groupSessionDecryptMessage(
+ encryptedEvent.ciphertext(), encryptedEvent.sessionId(),
+ encryptedEvent.id(), encryptedEvent.originTimestamp(),
+ encryptedEvent.senderId());
+ if (decrypted.isEmpty()) {
+ // qCWarning(E2EE) << "Encrypted message is empty";
+ return {};
}
- qCDebug(E2EE) << "Algorithm of the encrypted event with id"
- << encryptedEvent.id() << "is not for the current device";
+ auto decryptedEvent = encryptedEvent.createDecrypted(decrypted);
+ if (decryptedEvent->roomId() == id()) {
+ return decryptedEvent;
+ }
+ qCWarning(E2EE) << "Decrypted event" << encryptedEvent.id() << "not for this room; discarding.";
return {};
#endif // Quotient_E2EE_ENABLED
}
void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent,
- const QString& senderKey)
+ const QString& senderId,
+ const QString& olmSessionId)
{
#ifndef Quotient_E2EE_ENABLED
Q_UNUSED(roomKeyEvent)
- Q_UNUSED(senderKey)
+ Q_UNUSED(senderId)
+ Q_UNUSED(olmSessionId)
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
#else // Quotient_E2EE_ENABLED
if (roomKeyEvent.algorithm() != MegolmV1AesSha2AlgoKey) {
qCWarning(E2EE) << "Ignoring unsupported algorithm"
<< roomKeyEvent.algorithm() << "in m.room_key event";
}
- if (d->addInboundGroupSession(senderKey, roomKeyEvent.sessionId(),
- roomKeyEvent.sessionKey())) {
- qCDebug(E2EE) << "added new inboundGroupSession:"
- << d->groupSessions.count();
+ if (d->addInboundGroupSession(roomKeyEvent.sessionId(),
+ roomKeyEvent.sessionKey(), senderId,
+ olmSessionId)) {
+ qCWarning(E2EE) << "added new inboundGroupSession:"
+ << d->groupSessions.size();
+ auto undecryptedEvents = d->undecryptedEvents[roomKeyEvent.sessionId()];
+ for (const auto& eventId : undecryptedEvents) {
+ const auto pIdx = d->eventsIndex.constFind(eventId);
+ if (pIdx == d->eventsIndex.cend())
+ continue;
+ auto& ti = d->timeline[Timeline::size_type(*pIdx - minTimelineIndex())];
+ if (auto encryptedEvent = ti.viewAs<EncryptedEvent>()) {
+ if (auto decrypted = decryptMessage(*encryptedEvent)) {
+ // The reference will survive the pointer being moved
+ auto& decryptedEvent = *decrypted;
+ auto oldEvent = ti.replaceEvent(std::move(decrypted));
+ decryptedEvent.setOriginalEvent(std::move(oldEvent));
+ emit replacedEvent(ti.event(), decryptedEvent.originalEvent());
+ d->undecryptedEvents[roomKeyEvent.sessionId()] -= eventId;
+ }
+ }
+ }
}
#endif // Quotient_E2EE_ENABLED
}
@@ -1402,29 +1646,35 @@ 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)
{
- const auto userName =
- getCurrentState<RoomMemberEvent>(u->id())->displayName();
- // If there is exactly one namesake of the added user, signal member
- // renaming for that other one because the two should be disambiguated now.
+ const auto maybeUserName =
+ currentState.query(u->id(), &RoomMemberEvent::newDisplayName);
+ if (!maybeUserName)
+ qCWarning(MEMBERS) << "insertMemberIntoMap():" << u->id()
+ << "has no name (even empty)";
+ const auto userName = maybeUserName.value_or(QString());
const auto namesakes = membersMap.values(userName);
+ qCDebug(MEMBERS) << "insertMemberIntoMap(), user" << u->id()
+ << "with name" << userName << '-'
+ << namesakes.size() << "namesake(s) found";
- // Callers should check they are not adding an existing user once more.
+ // Callers should make sure they are not adding an existing user once more
Q_ASSERT(!namesakes.contains(u));
if (namesakes.contains(u)) { // Release version whines but continues
- qCCritical(STATE) << "Trying to add a user" << u->id() << "to room"
- << q->objectName() << "but that's already in it";
+ qCCritical(MEMBERS) << "Trying to add a user" << u->id() << "to room"
+ << q->objectName() << "but that's already in it";
return;
}
+ // If there is exactly one namesake of the added user, signal member
+ // renaming for that other one because the two should be disambiguated now
if (namesakes.size() == 1)
emit q->memberAboutToRename(namesakes.front(),
namesakes.front()->fullName(q));
@@ -1435,26 +1685,50 @@ void Room::Private::insertMemberIntoMap(User* u)
void Room::Private::removeMemberFromMap(User* u)
{
- const auto userName =
- getCurrentState<RoomMemberEvent>(u->id())->displayName();
+ const auto userName = currentState.queryOr(u->id(),
+ &RoomMemberEvent::newDisplayName,
+ QString());
+ qCDebug(MEMBERS) << "removeMemberFromMap(), username" << userName
+ << "for user" << u->id();
User* namesake = nullptr;
auto namesakes = membersMap.values(userName);
+ // If there was one namesake besides the removed user, signal member
+ // renaming for it because it doesn't need to be disambiguated any more.
if (namesakes.size() == 2) {
- namesake = namesakes.front() == u ? namesakes.back() : namesakes.front();
+ namesake =
+ namesakes.front() == u ? namesakes.back() : namesakes.front();
Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken");
emit q->memberAboutToRename(namesake, userName);
}
- membersMap.remove(userName, u);
- // If there was one namesake besides the removed user, signal member
- // renaming for it because it doesn't need to be disambiguated any more.
+ if (membersMap.remove(userName, u) == 0) {
+ qCDebug(MEMBERS) << "No entries removed; checking the whole list";
+ // Unless at the stage of initial filling, this no removed entries
+ // is suspicious; double-check that this user is not found in
+ // the whole map, and stop (for debug builds) or shout in the logs
+ // (for release builds) if there's one. That search is O(n), which
+ // may come rather expensive for larger rooms.
+ QElapsedTimer et;
+ auto it = std::find(membersMap.cbegin(), membersMap.cend(), u);
+ if (et.nsecsElapsed() > profilerMinNsecs() / 10)
+ qCDebug(MEMBERS) << "...done in" << et;
+ if (it != membersMap.cend()) {
+ // The assert (still) does more harm than good, it seems
+// Q_ASSERT_X(false, __FUNCTION__,
+// "Mismatched name in the room members list");
+ qCCritical(MEMBERS) << "Mismatched name in the room members list;"
+ " avoiding the list corruption";
+ membersMap.remove(it.key(), u);
+ }
+ }
if (namesake)
emit q->memberRenamed(namesake);
}
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
@@ -1480,11 +1754,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;
@@ -1492,103 +1767,209 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events,
return Timeline::size_type(insertedSize);
}
+QString Room::memberName(const QString& mxId) const
+{
+ // See https://github.com/matrix-org/matrix-doc/issues/1375
+ if (const auto rme = currentState().get<RoomMemberEvent>(mxId)) {
+ if (rme->newDisplayName())
+ return *rme->newDisplayName();
+ if (rme->prevContent() && rme->prevContent()->displayName)
+ return *rme->prevContent()->displayName;
+ }
+ return {};
+}
+
QString Room::roomMembername(const User* u) const
{
+ Q_ASSERT(u != nullptr);
+ return disambiguatedMemberName(u->id());
+}
+
+QString Room::roomMembername(const QString& userId) const
+{
+ return disambiguatedMemberName(userId);
+}
+
+inline QString makeFullUserName(const QString& displayName, const QString& mxId)
+{
+ return displayName % " (" % mxId % ')';
+}
+
+QString Room::disambiguatedMemberName(const QString& mxId) const
+{
// See the CS spec, section 11.2.2.3
- const auto username = u->name(this);
+ const auto username = memberName(mxId);
if (username.isEmpty())
- return u->id();
+ return mxId;
auto namesakesIt = qAsConst(d->membersMap).find(username);
// We expect a user to be a member of the room - but technically it is
- // possible to invoke roomMemberName() even for non-members. In such case
+ // possible to invoke this function even for non-members. In such case
// we return the full name, just in case.
if (namesakesIt == d->membersMap.cend())
- return u->fullName(this);
+ return makeFullUserName(username, mxId);
auto nextUserIt = namesakesIt;
if (++nextUserIt == d->membersMap.cend() || nextUserIt.key() != username)
return username; // No disambiguation necessary
- return u->fullName(this); // Disambiguate fully
+ return makeFullUserName(username, mxId); // Disambiguate fully
}
-QString Room::roomMembername(const QString& userId) const
+QString Room::safeMemberName(const QString& userId) const
{
- if (auto* const u = user(userId))
- return roomMembername(u);
- return {};
+ return sanitized(disambiguatedMemberName(userId));
}
-QString Room::safeMemberName(const QString& userId) const
+QString Room::htmlSafeMemberName(const QString& userId) const
{
- return sanitized(roomMembername(userId));
+ return safeMemberName(userId).toHtmlEscaped();
+}
+
+QUrl Room::memberAvatarUrl(const QString &mxId) const
+{
+ // See https://github.com/matrix-org/matrix-doc/issues/1375
+ if (const auto rme = currentState().get<RoomMemberEvent>(mxId)) {
+ if (rme->newAvatarUrl())
+ return *rme->newAvatarUrl();
+ if (rme->prevContent() && rme->prevContent()->avatarUrl)
+ return *rme->prevContent()->avatarUrl;
+ }
+ return {};
+}
+
+Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data,
+ 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)
{
+ qCDebug(MAIN) << "--- Updating room" << id() << "/" << objectName();
+ bool firstUpdate = d->baseState.empty();
+
if (d->prevBatch.isEmpty())
d->prevBatch = data.timelinePrevBatch;
setJoinState(data.joinState);
- Changes roomChanges = Change::NoChange;
+ 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);
- // The order of calculation is important - don't merge these lines!
- roomChanges |= d->addNewMessageEvents(move(data.timeline));
+ roomChanges |= d->updateStatsFromSyncData(data, fromCache);
- 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);
+ if (firstUpdate)
+ emit baseStateLoaded();
+ qCDebug(MAIN) << "--- Finished updating room" << id() << "/" << objectName();
+}
- 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
- // -2 is a special value to which SyncRoomData::SyncRoomData sets
- // unreadCount when it's missing in the payload (to distinguish from
- // explicit 0 in the payload).
- if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) {
- qCDebug(MESSAGES) << "Setting unread_count to" << data.unreadCount;
- d->unreadMessages = data.unreadCount;
- emit unreadMessagesChanged(this);
- }
-
- // Similar to unreadCount, SyncRoomData constructor assigns -1 to
- // highlightCount/notificationCount when those are missing in the payload
- if (data.highlightCount != -1 && data.highlightCount != d->highlightCount) {
- qCDebug(MESSAGES).nospace()
- << "Highlights in " << objectName() //
- << ": " << d->highlightCount << " -> " << data.highlightCount;
- d->highlightCount = data.highlightCount;
- emit highlightCountChanged();
- }
- if (data.notificationCount != -1
- && data.notificationCount != d->notificationCount) //
- {
- qCDebug(MESSAGES).nospace()
- << "Notifications in " << objectName() //
- << ": " << d->notificationCount << " -> " << data.notificationCount;
- d->notificationCount = data.notificationCount;
- emit notificationCountChanged();
- }
- if (roomChanges != Change::NoChange) {
- d->updateDisplayname();
- emit changed(roomChanges);
- if (!fromCache)
- connection()->saveRoomState(this);
+ if (changes
+ & (Change::Name | Change::Aliases | Change::Members | Change::Summary))
+ updateDisplayname();
+
+ if (changes & Change::PartiallyReadStats) {
+ QT_IGNORE_DEPRECATIONS(
+ 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" << Qt::hex << uint(changes)
+ << "in" << q->objectName();
+ emit q->changed(changes);
+ if (saveState)
+ connection->saveRoomState(q);
}
RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event)
@@ -1608,41 +1989,73 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event)
QString Room::Private::sendEvent(RoomEventPtr&& event)
{
- if (q->usesEncryption()) {
- qCCritical(MAIN) << "Room" << q->objectName()
- << "enforces encryption; sending encrypted messages "
- "is not supported yet";
+ if (!q->successorId().isEmpty()) {
+ qCWarning(MAIN) << q << "has been upgraded, event won't be sent";
+ return {};
}
- if (q->successorId().isEmpty())
- return doSendEvent(addAsPending(std::move(event)));
- qCWarning(MAIN) << q << "has been upgraded, event won't be sent";
- return {};
+ return doSendEvent(addAsPending(std::move(event)));
}
QString Room::Private::doSendEvent(const RoomEvent* pEvent)
{
const auto txnId = pEvent->transactionId();
// TODO, #133: Enqueue the job rather than immediately trigger it.
+ const RoomEvent* _event = pEvent;
+ std::unique_ptr<EncryptedEvent> encryptedEvent;
+
+ if (q->usesEncryption()) {
+#ifndef Quotient_E2EE_ENABLED
+ qWarning() << "This build of libQuotient does not support E2EE.";
+ return {};
+#else
+ if (!hasValidMegolmSession() || shouldRotateMegolmSession()) {
+ createMegolmSession();
+ }
+ // Send the session to other people
+ connection->sendSessionKeyToDevices(
+ id, currentOutboundMegolmSession->sessionId(),
+ currentOutboundMegolmSession->sessionKey(), getDevicesWithoutKey(),
+ currentOutboundMegolmSession->sessionMessageIndex());
+
+ const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson());
+ currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1);
+ connection->saveCurrentOutboundMegolmSession(
+ id, *currentOutboundMegolmSession);
+ encryptedEvent = makeEvent<EncryptedEvent>(
+ encrypted, q->connection()->olmAccount()->identityKeys().curve25519,
+ q->connection()->deviceId(),
+ currentOutboundMegolmSession->sessionId());
+ encryptedEvent->setTransactionId(connection->generateTxnId());
+ encryptedEvent->setRoomId(id);
+ encryptedEvent->setSender(connection->userId());
+ if(pEvent->contentJson().contains("m.relates_to"_ls)) {
+ encryptedEvent->setRelation(pEvent->contentJson()["m.relates_to"_ls].toObject());
+ }
+ // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out
+ _event = encryptedEvent.get();
+#endif
+ }
+
if (auto call =
connection->callApi<SendMessageJob>(BackgroundRequest, id,
- pEvent->matrixType(), txnId,
- pEvent->contentJson())) {
+ _event->matrixType(), txnId,
+ _event->contentJson())) {
Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] {
auto it = q->findPendingEvent(txnId);
if (it == unsyncedEvents.end()) {
- qCWarning(EVENTS) << "Pending event for transaction" << txnId
+ qWarning(EVENTS) << "Pending event for transaction" << txnId
<< "not found - got synced so soon?";
return;
}
it->setDeparted();
- qCDebug(EVENTS) << "Event txn" << txnId << "has departed";
emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
});
- Room::connect(call, &BaseJob::failure, q,
- std::bind(&Room::Private::onEventSendingFailure, this,
- txnId, call));
- Room::connect(call, &BaseJob::success, q, [this, call, txnId] {
+ Room::connect(call, &BaseJob::result, q, [this, txnId, call] {
+ if (!call->status().good()) {
+ onEventSendingFailure(txnId, call);
+ return;
+ }
auto it = q->findPendingEvent(txnId);
if (it != unsyncedEvents.end()) {
if (it->deliveryStatus() != EventStatus::ReachedServer) {
@@ -1650,7 +2063,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
}
} else
- qCDebug(EVENTS) << "Pending event for transaction" << txnId
+ qDebug(EVENTS) << "Pending event for transaction" << txnId
<< "already merged";
emit q->messageSent(txnId, call->eventId());
@@ -1686,7 +2099,7 @@ QString Room::retryMessage(const QString& txnId)
<< "File for transaction" << txnId
<< "has already been uploaded, bypassing re-upload";
} else {
- if (isJobRunning(transferIt->job)) {
+ if (isJobPending(transferIt->job)) {
qCDebug(MESSAGES) << "Abandoning the upload job for transaction"
<< txnId << "and starting again";
transferIt->job->abandon();
@@ -1708,6 +2121,10 @@ QString Room::retryMessage(const QString& txnId)
return d->doSendEvent(it->event());
}
+// Using a function defers actual tr() invocation to the moment when
+// translations are initialised
+auto FileTransferCancelledMsg() { return Room::tr("File transfer cancelled"); }
+
void Room::discardMessage(const QString& txnId)
{
auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(),
@@ -1719,10 +2136,10 @@ void Room::discardMessage(const QString& txnId)
const auto& transferIt = d->fileTransfers.find(txnId);
if (transferIt != d->fileTransfers.end()) {
Q_ASSERT(transferIt->isUpload);
- if (isJobRunning(transferIt->job)) {
+ if (isJobPending(transferIt->job)) {
transferIt->status = FileTransferInfo::Cancelled;
transferIt->job->abandon();
- emit fileTransferFailed(txnId, tr("File upload cancelled"));
+ emit fileTransferFailed(txnId, FileTransferCancelledMsg());
} else if (transferIt->status == FileTransferInfo::Completed) {
qCWarning(MAIN)
<< "File for transaction" << txnId
@@ -1762,57 +2179,81 @@ 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";
- }
- }
- });
- 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();
- }
- }
+ const auto& transferJob = fileTransfers.value(txnId).job;
+ connect(q, &Room::fileTransferCompleted, transferJob,
+ [this, txnId](const QString& tId, const QUrl&,
+ const FileSourceInfo& fileMetadata) {
+ if (tId != txnId)
+ return;
+
+ const auto it = q->findPendingEvent(txnId);
+ if (it != unsyncedEvents.end()) {
+ it->setFileUploaded(fileMetadata);
+ emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
+ doSendEvent(it->get());
+ } else {
+ // Normally in this situation we should instruct
+ // the media server to delete the file; alas, there's no
+ // API specced for that.
+ qCWarning(MAIN)
+ << "File uploaded to" << getUrlFromSourceInfo(fileMetadata)
+ << "but the event referring to it was "
+ "cancelled";
+ }
+ });
+ connect(q, &Room::fileTransferFailed, transferJob,
+ [this, txnId](const QString& tId) {
+ if (tId != txnId)
+ 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));
@@ -1824,34 +2265,45 @@ QString Room::postJson(const QString& matrixType,
return d->sendEvent(loadEvent<RoomEvent>(matrixType, eventContent));
}
-SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) const
+SetRoomStateWithKeyJob* Room::setState(const StateEvent& evt)
+{
+ return setState(evt.matrixType(), evt.stateKey(), evt.contentJson());
+}
+
+SetRoomStateWithKeyJob* Room::setState(const QString& evtType,
+ const QString& stateKey,
+ const QJsonObject& contentJson)
{
- return d->requestSetState(evt);
+ return d->requestSetState(evtType, stateKey, contentJson);
}
void Room::setName(const QString& newName)
{
- d->requestSetState<RoomNameEvent>(newName);
+ setState<RoomNameEvent>(newName);
}
void Room::setCanonicalAlias(const QString& newAlias)
{
- d->requestSetState<RoomCanonicalAliasEvent>(newAlias, altAliases());
+ setState<RoomCanonicalAliasEvent>(newAlias, altAliases());
}
+void Room::setPinnedEvents(const QStringList& events)
+{
+ setState<RoomPinnedEvent>(events);
+}
void Room::setLocalAliases(const QStringList& aliases)
{
- d->requestSetState<RoomCanonicalAliasEvent>(canonicalAlias(), aliases);
+ setState<RoomCanonicalAliasEvent>(canonicalAlias(), aliases);
}
void Room::setTopic(const QString& newTopic)
{
- d->requestSetState<RoomTopicEvent>(newTopic);
+ setState<RoomTopicEvent>(newTopic);
}
bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re)
{
- if (le->type() != re->type())
+ if (le->metaType() != re->metaType())
return false;
if (!re->id().isEmpty())
@@ -1902,11 +2354,12 @@ void Room::sendCallCandidates(const QString& callId,
d->sendEvent<CallCandidatesEvent>(callId, candidates);
}
-void Room::answerCall(const QString& callId, const int lifetime,
+void Room::answerCall(const QString& callId, [[maybe_unused]] int lifetime,
const QString& sdp)
{
- Q_ASSERT(supportsCalls());
- d->sendEvent<CallAnswerEvent>(callId, lifetime, sdp);
+ qCWarning(MAIN) << "To client developer: drop lifetime parameter from "
+ "Room::answerCall(), it is no more accepted";
+ answerCall(callId, sdp);
}
void Room::answerCall(const QString& callId, const QString& sdp)
@@ -1921,17 +2374,20 @@ void Room::hangupCall(const QString& callId)
d->sendEvent<CallHangupEvent>(callId);
}
-void Room::getPreviousContent(int limit) { d->getPreviousContent(limit); }
+void Room::getPreviousContent(int limit, const QString& filter)
+{
+ d->getPreviousContent(limit, filter);
+}
-void Room::Private::getPreviousContent(int limit)
+void Room::Private::getPreviousContent(int limit, const QString &filter)
{
- if (isJobRunning(eventsHistoryJob))
+ if (isJobPending(eventsHistoryJob))
return;
- eventsHistoryJob =
- connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit);
+ eventsHistoryJob = connection->callApi<GetRoomEventsJob>(id, "b", prevBatch,
+ "", limit, filter);
emit q->eventsHistoryJobChanged();
- connect(eventsHistoryJob, &BaseJob::success, q, [=] {
+ connect(eventsHistoryJob, &BaseJob::success, q, [this] {
prevBatch = eventsHistoryJob->end();
addHistoricalMessageEvents(eventsHistoryJob->chunk());
});
@@ -1950,12 +2406,6 @@ LeaveRoomJob* Room::leaveRoom()
return connection()->leaveRoom(this);
}
-SetRoomStateWithKeyJob* Room::setMemberState(const QString& memberId,
- const RoomMemberEvent& event) const
-{
- return d->requestSetState<RoomMemberEvent>(memberId, event.content());
-}
-
void Room::kickMember(const QString& memberId, const QString& reason)
{
connection()->callApi<KickJob>(id(), memberId, reason);
@@ -1983,18 +2433,35 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename,
Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__,
"localFilename should point at a local file");
auto fileName = localFilename.toLocalFile();
+ FileSourceInfo fileMetadata;
+#ifdef Quotient_E2EE_ENABLED
+ QTemporaryFile tempFile;
+ if (usesEncryption()) {
+ tempFile.open();
+ QFile file(localFilename.toLocalFile());
+ file.open(QFile::ReadOnly);
+ QByteArray data;
+ std::tie(fileMetadata, data) = encryptFile(file.readAll());
+ tempFile.write(data);
+ tempFile.close();
+ fileName = QFileInfo(tempFile).absoluteFilePath();
+ }
+#endif
auto job = connection()->uploadFile(fileName, overrideContentType);
- if (isJobRunning(job)) {
+ if (isJobPending(job)) {
d->fileTransfers[id] = { job, fileName, true };
connect(job, &BaseJob::uploadProgress, this,
[this, id](qint64 sent, qint64 total) {
d->fileTransfers[id].update(sent, total);
emit fileTransferProgress(id, sent, total);
});
- connect(job, &BaseJob::success, this, [this, id, localFilename, job] {
- d->fileTransfers[id].status = FileTransferInfo::Completed;
- emit fileTransferCompleted(id, localFilename, job->contentUri());
- });
+ connect(job, &BaseJob::success, this,
+ [this, id, localFilename, job, fileMetadata]() mutable {
+ // The lambda is mutable to change encryptedFileMetadata
+ d->fileTransfers[id].status = FileTransferInfo::Completed;
+ setUrlInSourceInfo(fileMetadata, QUrl(job->contentUri()));
+ emit fileTransferCompleted(id, localFilename, fileMetadata);
+ });
connect(job, &BaseJob::failure, this,
std::bind(&Private::failedTransfer, d, id, job->errorString()));
emit newFileTransfer(id, localFilename);
@@ -2027,11 +2494,11 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
<< "has an empty or malformed mxc URL; won't download";
return;
}
- const auto fileUrl = fileInfo->url;
+ const auto fileUrl = fileInfo->url();
auto filePath = localFilename.toLocalFile();
if (filePath.isEmpty()) { // Setup default file path
filePath =
- fileInfo->url.path().mid(1) % '_' % d->fileNameToDownload(event);
+ fileInfo->url().path().mid(1) % '_' % d->fileNameToDownload(event);
if (filePath.size() > 200) // If too long, elide in the middle
filePath.replace(128, filePath.size() - 192, "---");
@@ -2039,8 +2506,18 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
filePath = QDir::tempPath() % '/' % filePath;
qDebug(MAIN) << "File path:" << filePath;
}
- auto job = connection()->downloadFile(fileUrl, filePath);
- if (isJobRunning(job)) {
+ DownloadFileJob *job = nullptr;
+#ifdef Quotient_E2EE_ENABLED
+ if (auto* fileMetadata =
+ std::get_if<EncryptedFileMetadata>(&fileInfo->source)) {
+ job = connection()->downloadFile(fileUrl, *fileMetadata, filePath);
+ } else {
+#endif
+ job = connection()->downloadFile(fileUrl, filePath);
+#ifdef Quotient_E2EE_ENABLED
+ }
+#endif
+ if (isJobPending(job)) {
// If there was a previous transfer (completed or failed), overwrite it.
d->fileTransfers[eventId] = { job, job->targetFileName() };
connect(job, &BaseJob::downloadProgress, this,
@@ -2056,22 +2533,23 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
connect(job, &BaseJob::failure, this,
std::bind(&Private::failedTransfer, d, eventId,
job->errorString()));
+ emit newFileTransfer(eventId, localFilename);
} else
d->failedTransfer(eventId);
}
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 (isJobRunning(it->job))
+ 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
@@ -2099,6 +2577,26 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const
events.erase(dupsBegin, events.end());
}
+void Room::Private::decryptIncomingEvents(RoomEvents& events)
+{
+#ifdef Quotient_E2EE_ENABLED
+ QElapsedTimer et;
+ et.start();
+ size_t totalDecrypted = 0;
+ for (auto& eptr : events)
+ if (const auto& eeptr = eventCast<EncryptedEvent>(eptr)) {
+ if (auto decrypted = q->decryptMessage(*eeptr)) {
+ ++totalDecrypted;
+ auto&& oldEvent = exchange(eptr, move(decrypted));
+ eptr->setOriginalEvent(::move(oldEvent));
+ } else
+ undecryptedEvents[eeptr->sessionId()] += eeptr->id();
+ }
+ if (totalDecrypted > 5 || et.nsecsElapsed() >= profilerMinNsecs())
+ qDebug(PROFILER) << "Decrypted" << totalDecrypted << "events in" << et;
+#endif
+}
+
/** Make a redacted event
*
* This applies the redaction procedure as defined by the CS API specification
@@ -2108,10 +2606,10 @@ 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,
- QStringLiteral("room_id"), QStringLiteral("sender"), StateKeyKey,
+ static const QStringList keepKeys {
+ EventIdKey, TypeKey, RoomIdKey, SenderKey, StateKeyKey,
QStringLiteral("hashes"), QStringLiteral("signatures"),
QStringLiteral("depth"), QStringLiteral("prev_events"),
QStringLiteral("prev_state"), QStringLiteral("auth_events"),
@@ -2119,18 +2617,18 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
QStringLiteral("membership") };
// clang-format on
- std::vector<std::pair<Event::Type, QStringList>> keepContentKeysMap {
- { RoomMemberEvent::typeId(), { QStringLiteral("membership") } },
- { RoomCreateEvent::typeId(), { QStringLiteral("creator") } },
- { RoomPowerLevelsEvent::typeId(),
+ 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") } }
- // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } }
- // , { RoomHistoryVisibility::typeId(),
- // { QStringLiteral("history_visibility") } }
+ // TODO: Replace with RoomJoinRules::TypeId etc. once available
+ { "m.room.join_rules"_ls, { QStringLiteral("join_rule") } },
+ { "m.room.history_visibility"_ls,
+ { QStringLiteral("history_visibility") } }
};
for (auto it = originalJson.begin(); it != originalJson.end();) {
if (!keepKeys.contains(it.key()))
@@ -2139,9 +2637,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 {
@@ -2155,7 +2653,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);
@@ -2183,12 +2681,14 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction));
qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id();
if (oldEvent->isStateEvent()) {
- const StateEventKey evtKey { oldEvent->matrixType(),
- oldEvent->stateKey() };
- Q_ASSERT(currentState.contains(evtKey));
- if (currentState.value(evtKey) == oldEvent.get()) {
- Q_ASSERT(ti.index() >= 0); // Historical states can't be in
- // currentState
+ // Check whether the old event was a part of current state; if it was,
+ // update the current state to the redacted event object.
+ const auto currentStateEvt =
+ currentState.get(oldEvent->matrixType(), oldEvent->stateKey());
+ Q_ASSERT(currentStateEvt);
+ if (currentStateEvt == oldEvent.get()) {
+ // Historical states can't be in currentState
+ Q_ASSERT(ti.index() >= 0);
qCDebug(STATE).nospace()
<< "Redacting state " << oldEvent->matrixType() << "/"
<< oldEvent->stateKey();
@@ -2200,8 +2700,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
}
if (const auto* reaction = eventCast<ReactionEvent>(oldEvent)) {
const auto& targetEvtId = reaction->relation().eventId;
- const auto lookupKey =
- qMakePair(targetEvtId, EventRelation::Annotation());
+ const std::pair lookupKey { targetEvtId, EventRelation::AnnotationType };
if (relations.contains(lookupKey)) {
relations[lookupKey].removeOne(reaction);
emit q->updatedEvent(targetEvtId);
@@ -2209,6 +2708,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
}
q->onRedaction(*oldEvent, *ti);
emit q->replacedEvent(ti.event(), rawPtr(oldEvent));
+ // By now, all references to oldEvent must have been updated to ti.event()
return true;
}
@@ -2220,8 +2720,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();
@@ -2281,10 +2786,13 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
{
dropDuplicateEvents(events);
if (events.empty())
- return Change::NoChange;
+ return Change::None;
+
+ decryptIncomingEvents(events);
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
@@ -2334,7 +2842,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);
@@ -2391,7 +2899,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
if (q->supportsCalls())
for (auto it = from; it != syncEdge(); ++it)
- if (const auto* evt = it->viewAs<CallEventBase>())
+ if (const auto* evt = it->viewAs<CallEvent>())
emit q->callEvent(q, evt);
if (totalInserted > 0) {
@@ -2407,23 +2915,16 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
<< totalInserted << "new events; the last event is now"
<< timeline.back();
- // The first event in the just-added batch (referred to by `from`)
- // defines whose read receipt can possibly be promoted any further over
- // the same author's events newly arrived. Others will need explicit
- // read receipts from the server - or, for the local user, calling
- // setLastDisplayedEventId() - to promote their read receipts over
- // the new message events.
- if (auto* const firstWriter = q->user((*from)->senderId())) {
- setLastReadReceipt(firstWriter, rev_iter_t(from + 1));
- if (firstWriter == q->localUser() && q->readMarker().base() == from) {
- // If the local user's message(s) is/are first in the batch
- // and the fully read marker was right before it, promote
- // the fully read marker to the same event as the read receipt.
- roomChanges |=
- setFullyReadMarker(lastReadEventIds.value(firstWriter));
- }
- }
- roomChanges |= updateUnreadCount(timeline.crbegin(), rev_iter_t(from));
+ roomChanges |= updateStats(timeline.crbegin(), rev_iter_t(from));
+
+ // If the local user's message(s) is/are first in the batch
+ // and the fully read marker was right before it, promote
+ // the fully read marker to the same event as the read receipt.
+ const auto& firstWriterId = (*from)->senderId();
+ if (firstWriterId == connection->userId()
+ && q->fullyReadMarker().base() == from)
+ roomChanges |=
+ setFullyReadMarker(q->lastReadReceipt(firstWriterId).eventId);
}
Q_ASSERT(timeline.size() == timelineSize + totalInserted);
@@ -2435,14 +2936,17 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
{
- QElapsedTimer et;
- et.start();
const auto timelineSize = timeline.size();
dropDuplicateEvents(events);
if (events.empty())
return;
+ decryptIncomingEvents(events);
+
+ QElapsedTimer et;
+ et.start();
+ Changes changes {};
// In case of lazy-loading new members may be loaded with historical
// messages. Also, the cache doesn't store events with empty content;
// so when such events show up in the timeline they should be properly
@@ -2450,8 +2954,8 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
for (const auto& eptr : events) {
const auto& e = *eptr;
if (e.isStateEvent()
- && !currentState.contains({ e.matrixType(), e.stateKey() })) {
- q->processStateEvent(e);
+ && !currentState.contains(e.matrixType(), e.stateKey())) {
+ changes |= q->processStateEvent(e);
}
}
@@ -2471,108 +2975,133 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
emit q->updatedEvent(relation.eventId);
}
}
- if (updateUnreadCount(from, historyEdge()) != NoChange)
- connection->saveRoomState(q);
-
- // When there are no unread messages and the read marker is within the
- // known timeline, unreadMessages == -1
- // (see https://github.com/quotient-im/libQuotient/wiki/unread_count).
- Q_ASSERT(unreadMessages != 0 || q->readMarker() == historyEdge());
-
Q_ASSERT(timeline.size() == timelineSize + insertedSize);
if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs())
qCDebug(PROFILER) << "Added" << insertedSize << "historical event(s) to"
<< q->objectName() << "in" << et;
+
+ changes |= updateStats(from, historyEdge());
+ if (changes)
+ postprocessChanges(changes);
}
Room::Changes Room::processStateEvent(const RoomEvent& e)
{
if (!e.isStateEvent())
- return Change::NoChange;
-
- auto* const sender = user(e.senderId());
- if (!sender) {
- qCWarning(MAIN) << "State event" << e.id()
- << "is invalid and won't be processed";
- return Change::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
- // a stub if a value is not found, and what's needed here is a "real" event
- // or nullptr.
+ // to it, anticipating a change further in the function.
auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }];
// Prepare for the state change
- const auto oldRme = static_cast<const RoomMemberEvent*>(curStateEvent);
- visit(e, [this, &oldRme](const RoomMemberEvent& rme) {
- auto* const u = user(rme.userId());
- if (!u) { // Invalid user id?
- qCWarning(MAIN)
- << "Could not get a user object for" << rme.userId();
- return;
- }
- // TODO: remove along with User::processEvent() in 0.7
- const auto prevMembership = oldRme ? oldRme->membership()
- : MembershipType::Leave;
- u->processEvent(rme, this, oldRme == nullptr);
-
- switch (prevMembership) {
- case MembershipType::Invite:
- if (rme.membership() != prevMembership) {
- d->usersInvited.removeOne(u);
- Q_ASSERT(!d->usersInvited.contains(u));
+ // clang-format off
+ const bool proceed = switchOnType(e
+ , [this, curStateEvent](const RoomMemberEvent& rme) {
+ // clang-format on
+ auto* oldRme = static_cast<const RoomMemberEvent*>(curStateEvent);
+ auto* u = user(rme.userId());
+ if (!u) { // Some terribly malformed user id?
+ qCCritical(MAIN) << "Could not get a user object for"
+ << rme.userId();
+ return false; // Stay low and hope for the best...
}
- break;
- case MembershipType::Join:
- switch (rme.membership()) {
- case MembershipType::Join: // rename/avatar change or no-op
- if (rme.displayName() != oldRme->displayName()) {
- emit memberAboutToRename(u, rme.displayName());
+ const auto prevMembership = oldRme ? oldRme->membership()
+ : Membership::Leave;
+ switch (prevMembership) {
+ case Membership::Invite:
+ if (rme.membership() != prevMembership) {
+ d->usersInvited.removeOne(u);
+ Q_ASSERT(!d->usersInvited.contains(u));
+ }
+ break;
+ case Membership::Join:
+ if (rme.membership() == Membership::Join) {
+ // rename/avatar change or no-op
+ if (rme.newDisplayName()) {
+ emit memberAboutToRename(u, *rme.newDisplayName());
+ d->removeMemberFromMap(u);
+ }
+ if (!rme.newDisplayName() && !rme.newAvatarUrl()) {
+ qCWarning(MEMBERS)
+ << "No-op membership event for" << rme.userId()
+ << "- retaining the state";
+ qCWarning(MEMBERS) << "The event dump:" << rme;
+ return false;
+ }
+ } else {
+ if (rme.membership() == Membership::Invite)
+ qCWarning(MAIN)
+ << "Membership change from Join to Invite:" << rme;
+ // whatever the new membership, it's no more Join
d->removeMemberFromMap(u);
+ emit userRemoved(u);
}
break;
- case MembershipType::Invite:
- qCWarning(MAIN) << "Membership change from Join to Invite:"
- << rme;
- [[fallthrough]];
- default: // whatever the new membership, it's no more Join
- d->removeMemberFromMap(u);
- emit userRemoved(u);
+ case 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 Membership::Undefined:
+ ; // A warning will be dropped in the post-processing block below
}
- break;
- default:
- if (rme.membership() == MembershipType::Invite
- || rme.membership() == MembershipType::Join) {
- d->membersLeft.removeOne(u);
- Q_ASSERT(!d->membersLeft.contains(u));
+ return true;
+ // clang-format off
+ }
+ , [this, curStateEvent]( const EncryptionEvent& ee) {
+ // clang-format on
+ auto* oldEncEvt =
+ static_cast<const EncryptionEvent*>(curStateEvent);
+ if (ee.algorithm().isEmpty()) {
+ qWarning(STATE)
+ << "The encryption event for room" << objectName()
+ << "doesn't have 'algorithm' specified - ignoring";
+ return false;
}
+ if (oldEncEvt
+ && oldEncEvt->encryption() != EncryptionType::Undefined) {
+ qCWarning(STATE) << "The room is already encrypted but a new"
+ " room encryption event arrived - ignoring";
+ return false;
+ }
+ return true;
+ // clang-format off
}
- });
+ , true); // By default, go forward with the state change
+ // clang-format on
+ if (!proceed) {
+ 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 =
- std::exchange(curStateEvent, static_cast<const StateEventBase*>(&e));
+ std::exchange(curStateEvent, static_cast<const StateEvent*>(&e));
Q_ASSERT(!oldStateEvent
|| (oldStateEvent->matrixType() == e.matrixType()
&& oldStateEvent->stateKey() == e.stateKey()));
- if (!is<RoomMemberEvent>(e)) // Room member events are too numerous
+ if (is<RoomMemberEvent>(e))
+ qCDebug(MEMBERS) << "Updated room member state:" << e;
+ else
qCDebug(STATE) << "Updated room state:" << e;
// Update internal structures as per the change and work out the return value
// clang-format off
- return visit(e
+ const auto result = switchOnType(e
, [] (const RoomNameEvent&) {
- return NameChange;
- }
- , [] (const RoomAliasesEvent&) {
- return NoChange; // This event has been removed by MSC2432
+ 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();
@@ -2584,73 +3113,68 @@ 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&) {
+ emit pinnedEventsChanged();
+ return Change::Other;
+ }
, [] (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,oldRme,sender] (const RoomMemberEvent& evt) {
+ , [this,oldStateEvent] (const RoomMemberEvent& evt) {
// clang-format on
auto* u = user(evt.userId());
- if (!u)
- return NoChange; // Already warned earlier
- // TODO: remove in 0.7
- u->processEvent(evt, this, oldRme == nullptr);
-
- const auto prevMembership = oldRme ? oldRme->membership()
- : MembershipType::Leave;
+ const auto* oldMemberEvent =
+ static_cast<const RoomMemberEvent*>(oldStateEvent);
+ const auto prevMembership = oldMemberEvent
+ ? oldMemberEvent->membership()
+ : 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 if (oldRme->displayName() != evt.displayName()) {
- d->insertMemberIntoMap(u);
- emit memberRenamed(u);
+ } else {
+ if (evt.newDisplayName()) {
+ d->insertMemberIntoMap(u);
+ emit memberRenamed(u);
+ }
+ if (evt.newAvatarUrl())
+ 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, sender);
+ 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 Membership::Undefined:
+ qCWarning(MEMBERS) << "Ignored undefined membership type";
}
- return MembersChange;
+ return Change::Members;
// clang-format off
}
- , [this, oldEncEvt = static_cast<const EncryptionEvent*>(oldStateEvent)](
- const EncryptionEvent& ee) {
- // clang-format on
- if (ee.algorithm().isEmpty()) {
- qWarning(STATE)
- << "The encryption event for room" << objectName()
- << "doesn't have 'algorithm' specified - ignoring";
- return NoChange;
- }
- if (oldEncEvt
- && oldEncEvt->encryption() != EncryptionEventContent::Undefined) {
- qCWarning(STATE) << "The room is already encrypted but a new"
- " room encryption event arrived - ignoring";
- return NoChange;
- }
+ , [this] (const EncryptionEvent&) {
// As encryption can only be switched on once, emit the signal here
// instead of aggregating and emitting in updateData()
emit encryption();
- return OtherChange;
- // clang-format off
+ return Change::Other;
}
, [this] (const RoomTombstoneEvent& evt) {
const auto successorId = evt.successorRoomId();
@@ -2666,80 +3190,93 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return true;
});
- return OtherChange;
+ return Change::Other;
+ // clang-format off
}
- );
+ , Change::Other);
// clang-format on
+ 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 QString& userId : qAsConst(evt->users())) {
- auto* const u = user(userId);
- if (memberJoinState(u) == JoinState::Join)
- d->usersTyping.append(u);
- }
- if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER)
- << "Processing typing events from" << evt->users().size()
- << "user(s) in" << objectName() << "took" << et;
- emit typingChanged();
- }
- if (auto* evt = eventCast<ReceiptEvent>(event)) {
- int totalReceipts = 0;
- for (const auto& p : qAsConst(evt->eventsWithReceipts())) {
- totalReceipts += p.receipts.size();
- {
- if (p.receipts.size() == 1)
- qCDebug(EPHEMERAL)
- << objectName() << "received a read receipt for"
- << p.evtId << "from" << p.receipts[0].userId;
- else
- qCDebug(EPHEMERAL)
- << objectName() << "received read receipts for"
- << p.evtId << "from" << p.receipts.size() << "users";
- }
- const auto newMarker = findInTimeline(p.evtId);
- if (newMarker == historyEdge())
- qCDebug(EPHEMERAL) << "Event of the read receipt(s) is not "
- "found; saving them anyway";
- for (const Receipt& r : p.receipts)
- if (auto* const u = user(r.userId);
- memberJoinState(u) == JoinState::Join) {
- // If the event is not found (most likely, because it's
- // too old and hasn't been fetched from the server yet)
- // but there is a previous marker for a user, keep
- // the previous marker because read receipts are not
- // supposed to move backwards. Otherwise, blindly
- // store the event id for this user and update the read
- // marker when/if the event is fetched later on.
- d->setLastReadReceipt(u, newMarker, p.evtId);
+ switchOnType(*event,
+ [this, &et](const TypingEvent& evt) {
+ const auto& users = evt.users();
+ d->usersTyping.clear();
+ d->usersTyping.reserve(users.size()); // Assume all are members
+ for (const auto& userId : users)
+ if (isMember(userId))
+ d->usersTyping.append(user(userId));
+
+ if (d->usersTyping.size() > 3
+ || et.nsecsElapsed() >= profilerMinNsecs())
+ qDebug(PROFILER)
+ << "Processing typing events from" << users.size()
+ << "user(s) in" << objectName() << "took" << et;
+ emit typingChanged();
+ },
+ [this, &changes, &et](const ReceiptEvent& evt) {
+ const auto& receiptsJson = evt.contentJson();
+ QVector<QString> updatedUserIds;
+ // Most often (especially for bigger batches), receipts are
+ // scattered across events (an anecdotal evidence showed 1.2-1.3
+ // receipts per event on average).
+ updatedUserIds.reserve(receiptsJson.size() * 2);
+ for (auto eventIt = receiptsJson.begin();
+ eventIt != receiptsJson.end(); ++eventIt) {
+ const auto evtId = eventIt.key();
+ const auto newMarker = findInTimeline(evtId);
+ if (newMarker == historyEdge())
+ qDebug(EPHEMERAL)
+ << "Event" << evtId
+ << "is not found; saving read receipt(s) anyway";
+ const auto reads =
+ eventIt.value().toObject().value("m.read"_ls).toObject();
+ for (auto userIt = reads.begin(); userIt != reads.end();
+ ++userIt) {
+ ReadReceipt rr{ evtId,
+ fromJson<QDateTime>(
+ userIt->toObject().value("ts"_ls)) };
+ const auto userId = userIt.key();
+ if (userId == connection()->userId()) {
+ // Local user is special, and will get a signal about
+ // its read receipt separately from (and before) a
+ // signal on everybody else. No particular reason, just
+ // less cumbersome code.
+ changes |= d->setLocalLastReadReceipt(newMarker, rr);
+ } else if (d->setLastReadReceipt(userId, newMarker, rr)) {
+ changes |= Change::Other;
+ updatedUserIds.push_back(userId);
+ }
}
- }
- if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10
- || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "Processing" << totalReceipts << "receipt(s) on"
- << evt->eventsWithReceipts().size()
- << "event(s) in" << objectName() << "took" << et;
- }
+ }
+ if (updatedUserIds.size() > 10
+ || et.nsecsElapsed() >= profilerMinNsecs())
+ qDebug(PROFILER)
+ << "Processing" << updatedUserIds.size()
+ << "non-local receipt(s) on" << receiptsJson.size()
+ << "event(s) in" << objectName() << "took" << et;
+ if (!updatedUserIds.empty())
+ emit lastReadEventChanged(updatedUserIds);
+ });
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<const ReadMarkerEvent>(event))
- changes |= d->setFullyReadMarker(evt->event_id());
+ changes |= d->setFullyReadMarker(evt->eventId());
// For all account data events
auto& currentData = d->accountData[event->matrixType()];
@@ -2751,7 +3288,10 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event)
qCDebug(STATE) << "Updated account data of type"
<< currentData->matrixType();
emit accountDataChanged(currentData->matrixType());
- changes |= Change::AccountDataChange;
+ // TODO: Drop AccountDataChange in 0.8
+ // 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 changes;
}
@@ -2833,7 +3373,7 @@ QString Room::Private::calculateDisplayname() const
shortlist = buildShortlist(membersLeft);
QStringList names;
- for (auto u : shortlist) {
+ for (const auto* u : shortlist) {
if (u == nullptr || isLocalUser(u))
break;
// Only disambiguate if the room is not empty
@@ -2921,41 +3461,24 @@ QJsonObject Room::Private::toJson() const
{ QStringLiteral("events"), accountDataEvents } });
}
- if (const auto& readReceiptEventId = lastReadEventIds.value(q->localUser());
- !readReceiptEventId.isEmpty()) //
+ if (const auto& readReceipt = q->lastReadReceipt(connection->userId());
+ !readReceipt.eventId.isEmpty()) //
{
- // Okay, that's a mouthful; but basically, it's simply placing an m.read
- // event in the 'ephemeral' section of the cached sync payload.
- // See also receiptevent.* and m.read example in the spec.
- // Only the local user's read receipt is saved - others' are really
- // considered ephemeral but this one is useful in understanding where
- // the user is in the timeline before any history is loaded.
result.insert(
QStringLiteral("ephemeral"),
QJsonObject {
{ QStringLiteral("events"),
- QJsonArray { QJsonObject {
- { TypeKey, ReceiptEvent::matrixTypeId() },
- { ContentKey,
- QJsonObject {
- { readReceiptEventId,
- QJsonObject {
- { QStringLiteral("m.read"),
- QJsonObject {
- { connection->userId(),
- QJsonObject {} } } } } } } } } } } });
+ QJsonArray { ReceiptEvent({ { readReceipt.eventId,
+ { { connection->userId(),
+ readReceipt.timestamp } } } })
+ .fullJson() } } });
}
- QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey,
- 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);
+ result.insert(UnreadNotificationsKey,
+ QJsonObject { { PartiallyReadCountKey,
+ countFromStats(partiallyReadStats) },
+ { HighlightCountKey, serverHighlightCount } });
+ result.insert(NewUnreadCountKey, countFromStats(unreadStats));
if (et.elapsed() > 30)
qCDebug(PROFILER) << "Room::toJson() for" << q->objectName() << "took"
@@ -2970,15 +3493,28 @@ MemberSorter Room::memberSorter() const { return MemberSorter(this); }
bool MemberSorter::operator()(User* u1, User* u2) const
{
- return operator()(u1, room->roomMembername(u2));
+ return operator()(u1, room->disambiguatedMemberName(u2->id()));
}
-bool MemberSorter::operator()(User* u1, const QString& u2name) const
+bool MemberSorter::operator()(User* u1, QStringView u2name) const
{
- auto n1 = room->roomMembername(u1);
+ auto n1 = room->disambiguatedMemberName(u1->id());
if (n1.startsWith('@'))
n1.remove(0, 1);
- auto n2 = u2name.midRef(u2name.startsWith('@') ? 1 : 0);
+ 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>(EncryptionType::MegolmV1AesSha2);
+}
diff --git a/lib/room.h b/lib/room.h
index 6270a5a5..e2d6b869 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -1,26 +1,18 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name>
+// SPDX-FileCopyrightText: 2017 Marius Gripsgard <marius@ubports.com>
+// SPDX-FileCopyrightText: 2018 Josip Delic <delijati@googlemail.com>
+// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org>
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-FileCopyrightText: 2020 Ram Nad <ramnad1999@gmail.com>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "connection.h"
+#include "roomstateview.h"
#include "eventitem.h"
-#include "joinstate.h"
+#include "quotient_common.h"
#include "csapi/message_pagination.h"
@@ -30,6 +22,7 @@
#include "events/roommessageevent.h"
#include "events/roomcreateevent.h"
#include "events/roomtombstoneevent.h"
+#include "events/eventrelation.h"
#include <QtCore/QJsonObject>
#include <QtGui/QImage>
@@ -54,7 +47,7 @@ class RedactEventJob;
* This is specifically tuned to work with QML exposing all traits as
* Q_PROPERTY values.
*/
-class FileTransferInfo {
+class QUOTIENT_API FileTransferInfo {
Q_GADGET
Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT)
Q_PROPERTY(bool active READ active CONSTANT)
@@ -80,7 +73,46 @@ public:
bool failed() const { return status == Failed; }
};
-class Room : public QObject {
+//! \brief Data structure for a room member's read receipt
+//! \sa Room::lastReadReceipt
+class QUOTIENT_API 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(Type)
+
+ Type type = None;
+
+private:
+ Q_GADGET
+ Q_PROPERTY(Type type MEMBER type CONSTANT)
+};
+
+class QUOTIENT_API Room : public QObject {
Q_OBJECT
Q_PROPERTY(Connection* connection READ connection CONSTANT)
Q_PROPERTY(User* localUser READ localUser CONSTANT)
@@ -94,6 +126,9 @@ class Room : public QObject {
Q_PROPERTY(QStringList altAliases READ altAliases NOTIFY namesChanged)
Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged)
Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged)
+ Q_PROPERTY(QStringList pinnedEventIds READ pinnedEventIds WRITE setPinnedEvents
+ NOTIFY pinnedEventsChanged)
+ Q_PROPERTY(QString displayNameForHtml READ displayNameForHtml NOTIFY displaynameChanged)
Q_PROPERTY(QString topic READ topic NOTIFY topicChanged)
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged
STORED false)
@@ -101,8 +136,7 @@ class Room : public QObject {
Q_PROPERTY(bool usesEncryption READ usesEncryption NOTIFY encryption)
Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages)
- Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged)
- Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged)
+ Q_PROPERTY(QStringList memberNames READ safeMemberNames 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)
@@ -113,21 +147,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)
@@ -139,26 +180,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;
@@ -188,18 +252,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;
@@ -209,13 +270,14 @@ public:
QList<User*> membersLeft() const;
Q_INVOKABLE QList<Quotient::User*> users() const;
+ Q_DECL_DEPRECATED_X("Use safeMemberNames() or htmlSafeMemberNames() instead") //
QStringList memberNames() const;
- [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]]
- int memberCount() const;
+ QStringList safeMemberNames() const;
+ QStringList htmlSafeMemberNames() const;
int timelineSize() const;
bool usesEncryption() const;
RoomEventPtr decryptMessage(const EncryptedEvent& encryptedEvent);
- void handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, const QString& senderKey);
+ void handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, const QString& senderId, const QString& olmSessionId);
int joinedCount() const;
int invitedCount() const;
int totalMemberCount() const;
@@ -251,31 +313,58 @@ public:
/**
* \brief Check the join state of a given user in this room
*
- * \note Banned and invited users are not tracked for now (Leave
+ * \note Banned and invited users are not tracked separately for now (Leave
* will be returned for them).
*
* \return Join if the user is a room member; Leave otherwise
*/
+ Q_DECL_DEPRECATED_X("Use isMember() instead")
Q_INVOKABLE Quotient::JoinState memberJoinState(Quotient::User* user) const;
- /**
- * Get a disambiguated name for a given user in
- * the context of the room
- */
+ //! \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
+ Q_DECL_DEPRECATED_X("Use safeMemberName() instead")
Q_INVOKABLE QString roomMembername(const Quotient::User* u) const;
- /**
- * Get a disambiguated name for a user with this id in
- * the context of the room
- */
+ //! \brief Get a disambiguated name for a user with this id in the room
+ Q_DECL_DEPRECATED_X("Use safeMemberName() instead")
Q_INVOKABLE QString roomMembername(const QString& userId) const;
- /** Get a display-safe member name in the context of this room
+ /*!
+ * \brief Get a disambiguated name for the member with the given MXID
+ *
+ * This function should only be used for non-UI code; consider using
+ * safeMemberName() or htmlSafeMemberName() for displayed strings.
+ */
+ Q_INVOKABLE QString disambiguatedMemberName(const QString& mxId) const;
+
+ /*! Get a display-safe member name in the context of this room
*
- * Display-safe means HTML-safe + without RLO/LRO markers
+ * Display-safe means disambiguated and without RLO/LRO markers
* (see https://github.com/quotient-im/Quaternion/issues/545).
*/
Q_INVOKABLE QString safeMemberName(const QString& userId) const;
+ /*! Get an HTML-safe member name in the context of this room
+ *
+ * This function adds HTML escaping on top of safeMemberName() safeguards.
+ */
+ Q_INVOKABLE QString htmlSafeMemberName(const QString& userId) const;
+
+ //! \brief Get an avatar for the member with the given MXID
+ QUrl memberAvatarUrl(const QString& mxId) const;
+
const Timeline& messageEvents() const;
const PendingEvents& pendingEvents() const;
@@ -296,8 +385,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
@@ -309,14 +396,12 @@ public:
PendingEvents::const_iterator findPendingEvent(const QString& txnId) const;
const RelatedEvents relatedEvents(const QString& evtId,
- const char* relType) const;
+ EventRelation::reltypeid_t relType) const;
const RelatedEvents relatedEvents(const RoomEvent& evt,
- const char* relType) const;
+ EventRelation::reltypeid_t relType) const;
- const RoomCreateEvent* creation() const
- { return getCurrentState<RoomCreateEvent>(); }
- const RoomTombstoneEvent* tombstone() const
- { return getCurrentState<RoomTombstoneEvent>(); }
+ const RoomCreateEvent* creation() const;
+ const RoomTombstoneEvent* tombstone() const;
bool displayed() const;
/// Mark the room as currently displayed to the user
@@ -336,62 +421,223 @@ public:
void setLastDisplayedEventId(const QString& eventId);
void setLastDisplayedEvent(TimelineItem::index_t index);
- /*! \brief Obtain a read receipt of any user
- *
- * Since 0.6.8, there's an important difference between the single-argument
- * and the zero-argument overloads of this function: a call with an argument
- * returns the last _read receipt_ position (for any room member) while
- * a call without arguments returns the last _fully read_ position.
- * This is due to API stability guarantees; 0.7 will have distinctly named
- * methods to return read receipts and the fully read marker.
- */
+ //! \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
- *
- * \sa the description for the single-argument overload of this function
- */
+ //! \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
+ //! \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. If the fully read marker is within
- * the displayed viewport (between firstDisplayedMarker() and
- * lastDisplayedMarker()) then it is advanced as well.
- *
- * 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
@@ -427,12 +673,12 @@ public:
* actions on the room to its predecessors and successors.
*/
enum ActionScope {
- ThisRoomOnly, //< Do not apply to predecessors and successors
- WithinSameState, //< Apply to predecessors and successors in the same
- //< state as the current one
- OmitLeftState, //< Apply to all reachable predecessors and successors
- //< except those in Leave state
- WholeSequence //< Apply to all reachable predecessors and successors
+ ThisRoomOnly, ///< Do not apply to predecessors and successors
+ WithinSameState, ///< Apply to predecessors and successors in the same
+ ///< state as the current one
+ OmitLeftState, ///< Apply to all reachable predecessors and successors
+ ///< except those in Leave state
+ WholeSequence ///< Apply to all reachable predecessors and successors
};
/** Overwrite the room's tags
@@ -461,6 +707,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;
@@ -506,39 +755,54 @@ public:
/*! This method returns a (potentially empty) state event corresponding
* to the pair of event type \p evtType and state key \p stateKey.
*/
- Q_INVOKABLE const Quotient::StateEventBase*
- getCurrentState(const QString& evtType, const QString& stateKey = {}) const;
+ [[deprecated("Use currentState().get() instead; "
+ "make sure to check its result for nullptrs")]] //
+ const StateEvent* getCurrentState(const QString& evtType,
+ const QString& stateKey = {}) const;
/// Get a state event with the given event type and state key
/*! This is a typesafe overload that accepts a C++ event type instead of
* its Matrix name.
*/
template <typename EvT>
+ [[deprecated("Use currentState().get() instead; "
+ "make sure to check its result for nullptrs")]] //
const EvT* getCurrentState(const QString& stateKey = {}) const
{
- const auto* evt =
- eventCast<const EvT>(getCurrentState(EvT::matrixTypeId(), stateKey));
+ QT_IGNORE_DEPRECATIONS(const auto* evt = eventCast<const EvT>(
+ getCurrentState(EvT::TypeId, stateKey));)
Q_ASSERT(evt);
- Q_ASSERT(evt->matrixTypeId() == EvT::matrixTypeId()
+ Q_ASSERT(evt->matrixType() == EvT::TypeId
&& evt->stateKey() == stateKey);
return evt;
}
- /// Set a state event of the given type with the given arguments
- /*! This typesafe overload attempts to send a state event with the type
- * \p EvT and the content defined by \p args. Specifically, the function
- * creates a temporary object of type \p EvT passing \p args to
- * the constructor, and sends a request to the homeserver using
- * the Matrix event type defined by \p EvT and the event content produced
- * via EvT::contentJson().
- */
+ /// \brief Get the current room state
+ RoomStateView currentState() const;
+
+ //! Send a request to update the room state with the given event
+ SetRoomStateWithKeyJob* setState(const StateEvent& evt);
+
+ //! \brief Set a state event of the given type with the given arguments
+ //!
+ //! This typesafe overload attempts to send a state event with the type
+ //! \p EvT and the content defined by \p args. Specifically, the function
+ //! constructs a temporary object of type \p EvT with its content
+ //! list-initialised from \p args, and sends a request to the homeserver
+ //! using the Matrix event type defined by \p EvT and the event content
+ //! produced via EvT::contentJson().
+ //!
+ //! \note This call is not suitable for events that assume non-empty
+ //! stateKey, such as member events; for those you have to create
+ //! a temporary event object yourself and use the setState() overload
+ //! that accepts StateEvent const-ref.
template <typename EvT, typename... ArgTs>
- auto setState(ArgTs&&... args) const
+ auto setState(ArgTs&&... args)
{
return setState(EvT(std::forward<ArgTs>(args)...));
}
-public slots:
+public Q_SLOTS:
/** Check whether the room should be upgraded */
void checkVersion();
@@ -549,8 +813,13 @@ public 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
@@ -562,10 +831,13 @@ public slots:
QString retryMessage(const QString& txnId);
void discardMessage(const QString& txnId);
- /// Send a request to update the room state with the given event
- SetRoomStateWithKeyJob* setState(const StateEventBase& evt) const;
+ //! Send a request to update the room state based on freeform inputs
+ SetRoomStateWithKeyJob* setState(const QString& evtType,
+ const QString& stateKey,
+ const QJsonObject& contentJson);
void setName(const QString& newName);
void setCanonicalAlias(const QString& newAlias);
+ void setPinnedEvents(const QStringList& events);
/// Set room aliases on the user's current server
void setLocalAliases(const QStringList& aliases);
void setTopic(const QString& newTopic);
@@ -573,13 +845,10 @@ public slots:
/// You shouldn't normally call this method; it's here for debugging
void refreshDisplayName();
- void getPreviousContent(int limit = 10);
+ void getPreviousContent(int limit = 10, const QString &filter = {});
void inviteToRoom(const QString& memberId);
LeaveRoomJob* leaveRoom();
- /// \deprecated - use setState() instead")
- SetRoomStateWithKeyJob* setMemberState(const QString& memberId,
- const RoomMemberEvent& event) const;
void kickMember(const QString& memberId, const QString& reason = {});
void ban(const QString& userId, const QString& reason = {});
void unban(const QString& userId);
@@ -591,7 +860,12 @@ public slots:
void downloadFile(const QString& eventId, const QUrl& localFilename = {});
void cancelFileTransfer(const QString& id);
- /// Mark the bottommost message in the room as fully 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)
@@ -600,12 +874,19 @@ public slots:
void inviteCall(const QString& callId, const int lifetime,
const QString& sdp);
void sendCallCandidates(const QString& callId, const QJsonArray& candidates);
- void answerCall(const QString& callId, const int lifetime,
- const QString& sdp);
+ [[deprecated("Lifetime argument is no more passed; "
+ "use 2-arg Room::answerCall() instead")]]
+ void answerCall(const QString& callId, int lifetime, const QString& sdp);
void answerCall(const QString& callId, const QString& sdp);
void hangupCall(const QString& callId);
-signals:
+ /**
+ * Activates encryption for this room.
+ * Warning: Cannot be undone
+ */
+ void activateEncryption();
+
+Q_SIGNALS:
/// Initial set of state events has been loaded
/**
* The initial set is what comes from the initial sync for the room.
@@ -616,11 +897,11 @@ 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
@@ -664,12 +945,14 @@ signals:
void namesChanged(Quotient::Room* room);
void displaynameAboutToChange(Quotient::Room* room);
void displaynameChanged(Quotient::Room* room, QString oldName);
+ void pinnedEventsChanged();
void topicChanged();
void avatarChanged();
void userAdded(Quotient::User* user);
void userRemoved(Quotient::User* user);
void memberAboutToRename(Quotient::User* user, QString newName);
void memberRenamed(Quotient::User* user);
+ void memberAvatarChanged(Quotient::User* user);
/// The list of members has changed
/** Emitted no more than once per sync, this is a good signal to
* for cases when some action should be done upon any change in
@@ -687,17 +970,26 @@ 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();
- void lastReadEventChanged(Quotient::User* user);
+ //! The event the m.read receipt points to has changed for the listed users
+ //! \sa lastReadReceipt
+ void lastReadEventChanged(QVector<QString> userIds);
+ 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);
@@ -710,9 +1002,11 @@ signals:
void newFileTransfer(QString id, QUrl localFile);
void fileTransferProgress(QString id, qint64 progress, qint64 total);
- void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl);
+ void fileTransferCompleted(QString id, QUrl localFile,
+ FileSourceInfo fileMetadata);
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);
@@ -738,6 +1032,7 @@ protected:
{}
virtual QJsonObject toJson() const;
virtual void updateData(SyncRoomData&& data, bool fromCache = false);
+ virtual Notification checkForNotifications(const TimelineItem& ti);
private:
friend class Connection;
@@ -751,12 +1046,12 @@ private:
void setJoinState(JoinState state);
};
-class MemberSorter {
+class QUOTIENT_API MemberSorter {
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
@@ -769,4 +1064,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/roomstateview.cpp b/lib/roomstateview.cpp
new file mode 100644
index 00000000..be0f7c6c
--- /dev/null
+++ b/lib/roomstateview.cpp
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "roomstateview.h"
+
+using namespace Quotient;
+
+const StateEvent* RoomStateView::get(const QString& evtType,
+ const QString& stateKey) const
+{
+ return value({ evtType, stateKey });
+}
+
+bool RoomStateView::contains(const QString& evtType,
+ const QString& stateKey) const
+{
+ return contains({ evtType, stateKey });
+}
+
+QJsonObject RoomStateView::contentJson(const QString& evtType,
+ const QString& stateKey) const
+{
+ return queryOr(evtType, stateKey, &Event::contentJson, QJsonObject());
+}
+
+const QVector<const StateEvent*> RoomStateView::eventsOfType(
+ const QString& evtType) const
+{
+ auto vals = QVector<const StateEvent*>();
+ for (auto it = cbegin(); it != cend(); ++it)
+ if (it.key().first == evtType)
+ vals.append(it.value());
+
+ return vals;
+}
diff --git a/lib/roomstateview.h b/lib/roomstateview.h
new file mode 100644
index 00000000..c5261a1e
--- /dev/null
+++ b/lib/roomstateview.h
@@ -0,0 +1,211 @@
+// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "events/stateevent.h"
+
+#include <QtCore/QHash>
+
+namespace Quotient {
+
+class Room;
+
+// NB: Both concepts below expect EvT::needsStateKey to exist so you can't
+// express one via negation of the other (there's still an invalid case of
+// a non-state event where needsStateKey is not even defined).
+
+template <typename FnT, class EvT = std::decay_t<fn_arg_t<FnT>>>
+concept Keyed_State_Fn = EvT::needsStateKey;
+
+template <typename FnT, class EvT = std::decay_t<fn_arg_t<FnT>>>
+concept Keyless_State_Fn = !EvT::needsStateKey;
+
+class QUOTIENT_API RoomStateView
+ : private QHash<StateEventKey, const StateEvent*> {
+ Q_GADGET
+public:
+ const QHash<StateEventKey, const StateEvent*>& events() const
+ {
+ return *this;
+ }
+
+ //! \brief Get a state event with the given event type and state key
+ //! \return A state event corresponding to the pair of event type
+ //! \p evtType and state key \p stateKey, or nullptr if there's
+ //! no such \p evtType / \p stateKey combination in the current
+ //! state.
+ //! \warning In libQuotient 0.7 the return type changed to an OmittableCref
+ //! which is effectively a nullable const reference wrapper. You
+ //! have to check that it has_value() before using. Alternatively
+ //! you can now use queryCurrentState() to access state safely.
+ //! \sa getCurrentStateContentJson
+ const StateEvent* get(const QString& evtType,
+ const QString& stateKey = {}) const;
+
+ //! \brief Get a state event with the given event type and state key
+ //!
+ //! This is a typesafe overload that accepts a C++ event type instead of
+ //! its Matrix name. It is only defined for events with state key (i.e.,
+ //! derived from KeyedStateEvent).
+ template <Keyed_State_Event EvT>
+ const EvT* get(const QString& stateKey = {}) const
+ {
+ if (const auto* evt = get(EvT::TypeId, stateKey)) {
+ Q_ASSERT(evt->matrixType() == EvT::TypeId
+ && evt->stateKey() == stateKey);
+ return eventCast<const EvT>(evt);
+ }
+ return nullptr;
+ }
+
+ //! \brief Get a state event with the given event type
+ //!
+ //! This is a typesafe overload that accepts a C++ event type instead of
+ //! its Matrix name. This overload only defined for events that do not use
+ //! state key (i.e., derived from KeylessStateEvent).
+ template <Keyless_State_Event EvT>
+ const EvT* get() const
+ {
+ if (const auto* evt = get(EvT::TypeId)) {
+ Q_ASSERT(evt->matrixType() == EvT::TypeId);
+ return eventCast<const EvT>(evt);
+ }
+ return nullptr;
+ }
+
+ using QHash::contains;
+
+ bool contains(const QString& evtType, const QString& stateKey = {}) const;
+
+ template <Keyed_State_Event EvT>
+ bool contains(const QString& stateKey = {}) const
+ {
+ return contains(EvT::TypeId, stateKey);
+ }
+
+ template <Keyless_State_Event EvT>
+ bool contains() const
+ {
+ return contains(EvT::TypeId);
+ }
+
+ template <Keyed_State_Event EvT>
+ auto content(const QString& stateKey,
+ typename EvT::content_type defaultValue = {}) const
+ {
+ // EventBase<>::content is special in that it returns a const-ref,
+ // and lift() inside queryOr() can't wrap that in a temporary Omittable.
+ if (const auto evt = get<EvT>(stateKey))
+ return evt->content();
+ return std::move(defaultValue);
+ }
+
+ template <Keyless_State_Event EvT>
+ auto content(typename EvT::content_type defaultValue = {}) const
+ {
+ // Same as above
+ if (const auto evt = get<EvT>())
+ return evt->content();
+ return defaultValue;
+ }
+
+ //! \brief Get the content of the current state event with the given
+ //! event type and state key
+ //! \return An empty object if there's no event in the current state with
+ //! this event type and state key; the contents of the event
+ //! <tt>'content'</tt> object otherwise
+ Q_INVOKABLE QJsonObject contentJson(const QString& evtType,
+ const QString& stateKey = {}) const;
+
+ //! \brief Get all state events in the room of a certain type.
+ //!
+ //! This method returns all known state events that have occured in
+ //! the room of the given type.
+ const QVector<const StateEvent*> eventsOfType(const QString& evtType) const;
+
+ //! \brief Run a function on a state event with the given type and key
+ //!
+ //! Use this overload when there's no predefined event type or the event
+ //! type is unknown at compile time.
+ //! \return an Omittable with either the result of the function call, or
+ //! with `none` if the event is not found or the function fails
+ template <typename FnT>
+ auto query(const QString& evtType, const QString& stateKey, FnT&& fn) const
+ {
+ return lift(std::forward<FnT>(fn), get(evtType, stateKey));
+ }
+
+ //! \brief Run a function on a state event with the given type and key
+ //!
+ //! This is an overload for keyed state events (those that have
+ //! `needsStateKey == true`) with type defined at compile time.
+ //! \return an Omittable with either the result of the function call, or
+ //! with `none` if the event is not found or the function fails
+ template <Keyed_State_Fn FnT>
+ auto query(const QString& stateKey, FnT&& fn) const
+ {
+ using EventT = std::decay_t<fn_arg_t<FnT>>;
+ return lift(std::forward<FnT>(fn), get<EventT>(stateKey));
+ }
+
+ //! \brief Run a function on a keyless state event with the given type
+ //!
+ //! This is an overload for keyless state events (those having
+ //! `needsStateKey == false`) with type defined at compile time.
+ //! \return an Omittable with either the result of the function call, or
+ //! with `none` if the event is not found or the function fails
+ template <Keyless_State_Fn FnT>
+ auto query(FnT&& fn) const
+ {
+ using EventT = std::decay_t<fn_arg_t<FnT>>;
+ return lift(std::forward<FnT>(fn), get<EventT>());
+ }
+
+ //! \brief Same as query() but with a fallback value
+ //!
+ //! This is a shortcut for `query().value_or()`, passing respective
+ //! arguments to the respective functions. This is an overload for the case
+ //! when the event type cannot be fixed at compile time.
+ //! \return the result of \p fn execution, or \p fallback if the requested
+ //! event doesn't exist or the function fails
+ template <typename FnT, typename FallbackT>
+ auto queryOr(const QString& evtType, const QString& stateKey, FnT&& fn,
+ FallbackT&& fallback) const
+ {
+ return query(evtType, stateKey, std::forward<FnT>(fn))
+ .value_or(std::forward<FallbackT>(fallback));
+ }
+
+ //! \brief Same as query() but with a fallback value
+ //!
+ //! This is a shortcut for `query().value_or()`, passing respective
+ //! arguments to the respective functions. This is an overload for the case
+ //! when the event type cannot be fixed at compile time.
+ //! \return the result of \p fn execution, or \p fallback if the requested
+ //! event doesn't exist or the function fails
+ template <typename FnT, typename FallbackT>
+ auto queryOr(const QString& stateKey, FnT&& fn, FallbackT&& fallback) const
+ {
+ return query(stateKey, std::forward<FnT>(fn))
+ .value_or(std::forward<FallbackT>(fallback));
+ }
+
+ //! \brief Same as query() but with a fallback value
+ //!
+ //! This is a shortcut for `query().value_or()`, passing respective
+ //! arguments to the respective functions. This is an overload for the case
+ //! when the event type cannot be fixed at compile time.
+ //! \return the result of \p fn execution, or \p fallback if the requested
+ //! event doesn't exist or the function fails
+ template <typename FnT, typename FallbackT>
+ auto queryOr(FnT&& fn, FallbackT&& fallback) const
+ {
+ return query(std::forward<FnT>(fn))
+ .value_or(std::forward<FallbackT>(fallback));
+ }
+
+private:
+ friend class Room;
+};
+} // namespace Quotient
diff --git a/lib/settings.cpp b/lib/settings.cpp
index dd086d9c..510d253c 100644
--- a/lib/settings.cpp
+++ b/lib/settings.cpp
@@ -1,5 +1,9 @@
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#include "settings.h"
+#include "util.h"
#include "logging.h"
#include <QtCore/QUrl>
@@ -18,7 +22,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)
@@ -97,17 +103,18 @@ void SettingsGroup::remove(const QString& key)
Settings::remove(fullKey);
}
-QTNT_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {},
+QUO_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {},
setDeviceId)
-QTNT_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {},
+QUO_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {},
setDeviceName)
-QTNT_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false,
+QUO_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false,
setKeepLoggedIn)
-static const auto HomeserverKey = QStringLiteral("homeserver");
-static const auto AccessTokenKey = QStringLiteral("access_token");
-static const auto EncryptionAccountPickleKey =
- QStringLiteral("encryption_account_pickle");
+namespace {
+constexpr auto HomeserverKey = "homeserver"_ls;
+constexpr auto AccessTokenKey = "access_token"_ls;
+constexpr auto EncryptionAccountPickleKey = "encryption_account_pickle"_ls;
+}
QUrl AccountSettings::homeserver() const
{
@@ -121,19 +128,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);
@@ -144,18 +138,12 @@ void AccountSettings::clearAccessToken()
QByteArray AccountSettings::encryptionAccountPickle()
{
- QString passphrase = ""; // FIXME: add QtKeychain
return value("encryption_account_pickle", "").toByteArray();
}
void AccountSettings::setEncryptionAccountPickle(
const QByteArray& encryptionAccountPickle)
{
- qCWarning(MAIN)
- << "Saving encryption_account_pickle to QSettings is insecure."
- " Developers, do it manually or contribute to share QtKeychain "
- "logic to libQuotient.";
- QString passphrase = ""; // FIXME: add QtKeychain
setValue("encryption_account_pickle", encryptionAccountPickle);
}
diff --git a/lib/settings.h b/lib/settings.h
index c45764a6..ff99d488 100644
--- a/lib/settings.h
+++ b/lib/settings.h
@@ -1,23 +1,10 @@
-/******************************************************************************
- * Copyright (C) 2016 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
+#include "quotient_export.h"
+
#include <QtCore/QSettings>
#include <QtCore/QUrl>
#include <QtCore/QVector>
@@ -26,7 +13,7 @@ class QVariant;
namespace Quotient {
-class Settings : public QSettings {
+class QUOTIENT_API Settings : public QSettings {
Q_OBJECT
public:
/// Add a legacy organisation/application name to migrate settings from
@@ -91,11 +78,10 @@ protected:
QSettings legacySettings { legacyOrganizationName, legacyApplicationName };
};
-class SettingsGroup : public Settings {
+class QUOTIENT_API 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))
{}
@@ -120,7 +106,7 @@ private:
QString groupPath;
};
-#define QTNT_DECLARE_SETTING(type, propname, setter) \
+#define QUO_DECLARE_SETTING(type, propname, setter) \
Q_PROPERTY(type propname READ propname WRITE setter) \
public: \
type propname() const; \
@@ -128,7 +114,7 @@ public: \
\
private:
-#define QTNT_DEFINE_SETTING(classname, type, propname, qsettingname, \
+#define QUO_DEFINE_SETTING(classname, type, propname, qsettingname, \
defaultValue, setter) \
type classname::propname() const \
{ \
@@ -140,21 +126,17 @@ private:
setValue(QStringLiteral(qsettingname), std::move(newValue)); \
}
-class AccountSettings : public SettingsGroup {
+class QUOTIENT_API AccountSettings : public SettingsGroup {
Q_OBJECT
Q_PROPERTY(QString userId READ userId CONSTANT)
- QTNT_DECLARE_SETTING(QString, deviceId, setDeviceId)
- QTNT_DECLARE_SETTING(QString, deviceName, setDeviceName)
- QTNT_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn)
- /** \deprecated \sa setAccessToken */
- Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken)
+ QUO_DECLARE_SETTING(QString, deviceId, setDeviceId)
+ QUO_DECLARE_SETTING(QString, deviceName, setDeviceName)
+ QUO_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn)
Q_PROPERTY(QByteArray encryptionAccountPickle READ encryptionAccountPickle
WRITE setEncryptionAccountPickle)
public:
- 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;
@@ -162,11 +144,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/ssosession.cpp b/lib/ssosession.cpp
index be701204..93e252cc 100644
--- a/lib/ssosession.cpp
+++ b/lib/ssosession.cpp
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#include "ssosession.h"
#include "connection.h"
@@ -12,10 +15,10 @@ using namespace Quotient;
class SsoSession::Private {
public:
- Private(SsoSession* q, const QString& initialDeviceName = {},
- const QString& deviceId = {}, Connection* connection = nullptr)
- : initialDeviceName(initialDeviceName)
- , deviceId(deviceId)
+ Private(SsoSession* q, QString initialDeviceName = {},
+ QString deviceId = {}, Connection* connection = nullptr)
+ : initialDeviceName(std::move(initialDeviceName))
+ , deviceId(std::move(deviceId))
, connection(connection)
{
auto* server = new QTcpServer(q);
@@ -26,7 +29,7 @@ public:
.arg(server->serverPort());
ssoUrl = connection->getUrlForApi<RedirectToSSOJob>(callbackUrl);
- QObject::connect(server, &QTcpServer::newConnection, q, [this, server] {
+ QObject::connect(server, &QTcpServer::newConnection, q, [this, q, server] {
qCDebug(MAIN) << "SSO callback initiated";
socket = server->nextPendingConnection();
server->close();
@@ -40,8 +43,14 @@ public:
});
QObject::connect(socket, &QTcpSocket::disconnected, socket,
&QTcpSocket::deleteLater);
+ QObject::connect(socket, &QObject::destroyed, q,
+ &QObject::deleteLater);
});
+ qCDebug(MAIN) << "SSO session constructed";
}
+ ~Private() { qCDebug(MAIN) << "SSO session deconstructed"; }
+ Q_DISABLE_COPY_MOVE(Private)
+
void processCallback();
void sendHttpResponse(const QByteArray& code, const QByteArray& msg);
void onError(const QByteArray& code, const QString& errorMsg);
@@ -58,19 +67,12 @@ public:
SsoSession::SsoSession(Connection* connection, const QString& initialDeviceName,
const QString& deviceId)
: QObject(connection)
- , d(std::make_unique<Private>(this, initialDeviceName, deviceId, connection))
-{
- qCDebug(MAIN) << "SSO session constructed";
-}
-
-SsoSession::~SsoSession()
-{
- qCDebug(MAIN) << "SSO session deconstructed";
-}
+ , d(makeImpl<Private>(this, initialDeviceName, deviceId, connection))
+{}
QUrl SsoSession::ssoUrl() const { return d->ssoUrl; }
-QUrl SsoSession::callbackUrl() const { return d->callbackUrl; }
+QUrl SsoSession::callbackUrl() const { return QUrl(d->callbackUrl); }
void SsoSession::Private::processCallback()
{
@@ -79,29 +81,29 @@ void SsoSession::Private::processCallback()
// (see at https://github.com/clementine-player/Clementine/)
const auto& requestParts = requestData.split(' ');
if (requestParts.size() < 2 || requestParts[1].isEmpty()) {
- onError("400 Bad Request", tr("No login token in SSO callback"));
+ onError("400 Bad Request", tr("Malformed single sign-on callback"));
return;
}
const auto& QueryItemName = QStringLiteral("loginToken");
QUrlQuery query { QUrl(requestParts[1]).query() };
if (!query.hasQueryItem(QueryItemName)) {
- onError("400 Bad Request", tr("Malformed single sign-on callback"));
+ onError("400 Bad Request", tr("No login token in SSO callback"));
+ return;
}
qCDebug(MAIN) << "Found the token in SSO callback, logging in";
connection->loginWithToken(query.queryItemValue(QueryItemName).toLatin1(),
initialDeviceName, deviceId);
connect(connection, &Connection::connected, socket, [this] {
- const QString msg =
- "The application '" % QCoreApplication::applicationName()
- % "' has successfully logged in as a user " % connection->userId()
- % " with device id " % connection->deviceId()
- % ". This window can be closed. Thank you.\r\n";
+ const auto msg =
+ tr("The application '%1' has successfully logged in as a user %2 "
+ "with device id %3. This window can be closed. Thank you.\r\n")
+ .arg(QCoreApplication::applicationName(), connection->userId(),
+ connection->deviceId());
sendHttpResponse("200 OK", msg.toHtmlEscaped().toUtf8());
socket->disconnectFromHost();
});
connect(connection, &Connection::loginError, socket, [this] {
onError("401 Unauthorised", tr("Login failed"));
- socket->disconnectFromHost();
});
}
@@ -125,4 +127,5 @@ void SsoSession::Private::onError(const QByteArray& code,
// [kitsune] Yeah, I know, dirty. Maybe the "right" way would be to have
// an intermediate signal but that seems just a fight for purity.
emit connection->loginError(errorMsg, requestData);
+ socket->disconnectFromHost();
}
diff --git a/lib/ssosession.h b/lib/ssosession.h
index 5845cd4d..e6a3f8fb 100644
--- a/lib/ssosession.h
+++ b/lib/ssosession.h
@@ -1,13 +1,13 @@
+// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#pragma once
+#include "util.h"
+
#include <QtCore/QUrl>
#include <QtCore/QObject>
-#include <memory>
-
-class QTcpServer;
-class QTcpSocket;
-
namespace Quotient {
class Connection;
@@ -26,19 +26,20 @@ class Connection;
* connection->prepareForSso(initialDeviceName)->ssoUrl());
* \endcode
*/
-class SsoSession : public QObject {
+class QUOTIENT_API SsoSession : public QObject {
Q_OBJECT
Q_PROPERTY(QUrl ssoUrl READ ssoUrl CONSTANT)
Q_PROPERTY(QUrl callbackUrl READ callbackUrl CONSTANT)
public:
SsoSession(Connection* connection, const QString& initialDeviceName,
const QString& deviceId = {});
- ~SsoSession() override;
+ ~SsoSession() override = default;
+
QUrl ssoUrl() const;
QUrl callbackUrl() const;
private:
class Private;
- std::unique_ptr<Private> d;
+ ImplPtr<Private> d;
};
} // namespace Quotient
diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp
index a3809469..ec7203af 100644
--- a/lib/syncdata.cpp
+++ b/lib/syncdata.cpp
@@ -1,33 +1,15 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "syncdata.h"
-#include "events/eventloader.h"
+#include "logging.h"
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
using namespace Quotient;
-const QString SyncRoomData::UnreadCountKey =
- QStringLiteral("x-quotient.unread_count");
-
bool RoomSummary::isEmpty() const
{
return !joinedMemberCount && !invitedMemberCount && !heroes;
@@ -36,9 +18,10 @@ bool RoomSummary::isEmpty() const
bool RoomSummary::merge(const RoomSummary& other)
{
// Using bitwise OR to prevent computation shortcut.
- return joinedMemberCount.merge(other.joinedMemberCount)
- | invitedMemberCount.merge(other.invitedMemberCount)
- | heroes.merge(other.heroes);
+ return static_cast<bool>(
+ static_cast<int>(joinedMemberCount.merge(other.joinedMemberCount))
+ | static_cast<int>(invitedMemberCount.merge(other.invitedMemberCount))
+ | static_cast<int>(heroes.merge(other.heroes)));
}
QDebug Quotient::operator<<(QDebug dbg, const RoomSummary& rs)
@@ -79,23 +62,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();
@@ -104,17 +87,52 @@ 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(-1);
- notificationCount = unreadJson.value("notification_count"_ls).toInt(-1);
+ 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);
+}
+
+QDebug Quotient::operator<<(QDebug dbg, const DevicesList& devicesList)
+{
+ QDebugStateSaver _(dbg);
+ QStringList sl;
+ if (!devicesList.changed.isEmpty())
+ sl << QStringLiteral("changed: %1").arg(devicesList.changed.join(", "));
+ if (!devicesList.left.isEmpty())
+ sl << QStringLiteral("left %1").arg(devicesList.left.join(", "));
+ dbg.nospace().noquote() << sl.join(QStringLiteral("; "));
+ return dbg;
+}
+
+void JsonObjectConverter<DevicesList>::dumpTo(QJsonObject& jo,
+ const DevicesList& rs)
+{
+ addParam<IfNotEmpty>(jo, QStringLiteral("changed"),
+ rs.changed);
+ addParam<IfNotEmpty>(jo, QStringLiteral("left"),
+ rs.left);
+}
+
+void JsonObjectConverter<DevicesList>::fillFrom(const QJsonObject& jo,
+ DevicesList& rs)
+{
+ fromJson(jo["changed"_ls], rs.changed);
+ fromJson(jo["left"_ls], rs.left);
}
SyncData::SyncData(const QString& cacheFileName)
{
QFileInfo cacheFileInfo { cacheFileName };
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)
@@ -125,7 +143,7 @@ SyncData::SyncData(const QString& cacheFileName)
<< "is required; discarding the cache";
}
-SyncDataList&& SyncData::takeRoomData() { return move(roomData); }
+SyncDataList SyncData::takeRoomData() { return move(roomData); }
QString SyncData::fileNameForRoom(QString roomId)
{
@@ -133,11 +151,18 @@ QString SyncData::fileNameForRoom(QString roomId)
return roomId + ".json";
}
-Events&& SyncData::takePresenceData() { return std::move(presenceData); }
+Events SyncData::takePresenceData() { return std::move(presenceData); }
-Events&& SyncData::takeAccountData() { return std::move(accountData); }
+Events SyncData::takeAccountData() { return std::move(accountData); }
-Events&& SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); }
+Events SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); }
+
+std::pair<int, int> SyncData::cacheVersion()
+{
+ return { MajorCacheVersion, 2 };
+}
+
+DevicesList SyncData::takeDevicesList() { return std::move(devicesList); }
QJsonObject SyncData::loadJson(const QString& fileName)
{
@@ -155,12 +180,7 @@ QJsonObject SyncData::loadJson(const QString& fileName)
const auto json = data.startsWith('{')
? QJsonDocument::fromJson(data).object()
-#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
- : QCborValue::fromCbor(data).toJsonValue().toObject()
-#else
- : QJsonDocument::fromBinaryData(data).object()
-#endif
- ;
+ : QCborValue::fromCbor(data).toJsonValue().toObject();
if (json.isEmpty()) {
qCWarning(MAIN) << "State cache in" << fileName
<< "is broken or empty, discarding";
@@ -181,24 +201,32 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir)
fromJson(json.value("device_one_time_keys_count"_ls),
deviceOneTimeKeysCount_);
+ if(json.contains("device_lists")) {
+ fromJson(json.value("device_lists"), devicesList);
+ }
+
auto rooms = json.value("rooms"_ls).toObject();
auto totalRooms = 0;
auto totalEvents = 0;
for (size_t i = 0; i < JoinStateStrings.size(); ++i) {
- // This assumes that JoinState values go over powers of 2: 1,2,4,...
+ // 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(roomData.size() + static_cast<size_t>(rs.size()));
for (auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) {
- auto roomJson =
- roomIt->isObject()
- ? roomIt->toObject()
- : loadJson(baseDir + fileNameForRoom(roomIt.key()));
- if (roomJson.isEmpty()) {
- unresolvedRoomIds.push_back(roomIt.key());
- continue;
- }
+ QJsonObject roomJson;
+ if (!baseDir.isEmpty()) {
+ // Loading data from the local cache, with room objects saved in
+ // individual files rather than inline
+ roomJson = loadJson(baseDir + fileNameForRoom(roomIt.key()));
+ if (roomJson.isEmpty()) {
+ unresolvedRoomIds.push_back(roomIt.key());
+ continue;
+ }
+ } else // When loading from /sync response, everything is inline
+ roomJson = roomIt->toObject();
+
roomData.emplace_back(roomIt.key(), joinState, roomJson);
const auto& r = roomData.back();
totalEvents += r.state.size() + r.ephemeral.size()
diff --git a/lib/syncdata.h b/lib/syncdata.h
index 67d04557..9358ec8f 100644
--- a/lib/syncdata.h
+++ b/lib/syncdata.h
@@ -1,28 +1,19 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#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
@@ -31,7 +22,7 @@ namespace Quotient {
* means that nothing has come from the server; heroes.value().isEmpty()
* means a peculiar case of a room with the only member - the current user.
*/
-struct RoomSummary {
+struct QUOTIENT_API RoomSummary {
Omittable<int> joinedMemberCount;
Omittable<int> invitedMemberCount;
Omittable<QStringList> heroes; //< mxids of users to take part in the room
@@ -44,13 +35,33 @@ struct RoomSummary {
};
QDebug operator<<(QDebug dbg, const RoomSummary& rs);
-
template <>
struct JsonObjectConverter<RoomSummary> {
static void dumpTo(QJsonObject& jo, const RoomSummary& rs);
static void fillFrom(const QJsonObject& jo, RoomSummary& rs);
};
+/// Information on e2e device updates. Note: only present on an
+/// incremental sync.
+struct DevicesList {
+ /// List of users who have updated their device identity keys, or who
+ /// now share an encrypted room with the client since the previous
+ /// sync response.
+ QStringList changed;
+
+ /// List of users with whom we do not share any encrypted rooms
+ /// anymore since the previous sync response.
+ QStringList left;
+};
+
+QDebug operator<<(QDebug dhg, const DevicesList& devicesList);
+
+template <>
+struct JsonObjectConverter<DevicesList> {
+ static void dumpTo(QJsonObject &jo, const DevicesList &dev);
+ static void fillFrom(const QJsonObject& jo, DevicesList& rs);
+};
+
class SyncRoomData {
public:
QString roomId;
@@ -63,16 +74,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.
@@ -89,20 +98,22 @@ public:
*/
void parseJson(const QJsonObject& json, const QString& baseDir = {});
- Events&& takePresenceData();
- Events&& takeAccountData();
- Events&& takeToDeviceEvents();
+ Events takePresenceData();
+ Events takeAccountData();
+ Events takeToDeviceEvents();
const QHash<QString, int>& deviceOneTimeKeysCount() const
{
return deviceOneTimeKeysCount_;
}
- SyncDataList&& takeRoomData();
+ SyncDataList takeRoomData();
+ DevicesList takeDevicesList();
QString nextBatch() const { return nextBatch_; }
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:
@@ -113,6 +124,7 @@ private:
SyncDataList roomData;
QStringList unresolvedRoomIds;
QHash<QString, int> deviceOneTimeKeysCount_;
+ DevicesList devicesList;
static QJsonObject loadJson(const QString& fileName);
};
diff --git a/lib/uri.cpp b/lib/uri.cpp
index 9eefdc83..91751df0 100644
--- a/lib/uri.cpp
+++ b/lib/uri.cpp
@@ -1,28 +1,36 @@
+// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#include "uri.h"
+#include "util.h"
#include "logging.h"
#include <QtCore/QRegularExpression>
using namespace Quotient;
-struct ReplacePair { QByteArray uriString; char sigil; };
+namespace {
+
+struct ReplacePair { QLatin1String uriString; char sigil; };
/// \brief Defines bi-directional mapping of path prefixes and sigils
///
/// When there are two prefixes for the same sigil, the first matching
/// entry for a given sigil is used.
-static const auto replacePairs = {
- ReplacePair { "u/", '@' },
- { "user/", '@' },
- { "roomid/", '!' },
- { "r/", '#' },
- { "room/", '#' },
+const ReplacePair replacePairs[] = {
+ { "u/"_ls, '@' },
+ { "user/"_ls, '@' },
+ { "roomid/"_ls, '!' },
+ { "r/"_ls, '#' },
+ { "room/"_ls, '#' },
// The notation for bare event ids is not proposed in MSC2312 but there's
// https://github.com/matrix-org/matrix-doc/pull/2644
- { "e/", '$' },
- { "event/", '$' }
+ { "e/"_ls, '$' },
+ { "event/"_ls, '$' }
};
+}
+
Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query)
{
if (primaryId.isEmpty())
@@ -67,12 +75,12 @@ 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();
}
-static auto matrixToUrlRegexInit()
+static inline auto matrixToUrlRegexInit()
{
// See https://matrix.org/docs/spec/appendices#matrix-to-navigation
const QRegularExpression MatrixToUrlRE {
@@ -95,7 +103,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;
@@ -125,9 +133,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")) };
}
}
@@ -163,7 +171,7 @@ QUrl Uri::toUrl(UriForm form) const
return {};
if (form == CanonicalUri || type() == NonMatrix)
- return *this; // NOLINT(cppcoreguidelines-slicing): It's intentional
+ return SLICE(*this, QUrl);
QUrl url;
url.setScheme("https");
@@ -183,14 +191,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/uri.h b/lib/uri.h
index 270766dd..78cd27c8 100644
--- a/lib/uri.h
+++ b/lib/uri.h
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#pragma once
#include "quotient_common.h"
@@ -20,7 +23,7 @@ namespace Quotient {
* its type, and obtain components, also in either unencoded (for displaying)
* or encoded (for APIs) form.
*/
-class Uri : private QUrl {
+class QUOTIENT_API Uri : private QUrl {
Q_GADGET
public:
enum Type : char {
diff --git a/lib/uriresolver.cpp b/lib/uriresolver.cpp
index e5f19a96..681e3842 100644
--- a/lib/uriresolver.cpp
+++ b/lib/uriresolver.cpp
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#include "uriresolver.h"
#include "connection.h"
@@ -5,6 +8,8 @@
using namespace Quotient;
+UriResolverBase::~UriResolverBase() = default;
+
UriResolveResult UriResolverBase::visitResource(Connection* account,
const Uri& uri)
{
@@ -24,9 +29,9 @@ UriResolveResult UriResolverBase::visitResource(Connection* account,
case Uri::UserId: {
if (uri.action() == "join")
return IncorrectAction;
- if (auto* const user = account->user(uri.primaryId()))
- return visitUser(user, uri.action());
- return InvalidUri;
+ auto* user = account->user(uri.primaryId());
+ Q_ASSERT(user != nullptr);
+ return visitUser(user, uri.action());
}
case Uri::RoomId:
case Uri::RoomAlias: {
diff --git a/lib/uriresolver.h b/lib/uriresolver.h
index 9b2ced9d..9140046c 100644
--- a/lib/uriresolver.h
+++ b/lib/uriresolver.h
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
#pragma once
#include "uri.h"
@@ -22,7 +25,7 @@ class User;
* gradual implementation. Derived classes are encouraged to override as many
* of them as possible.
*/
-class UriResolverBase {
+class QUOTIENT_API UriResolverBase {
public:
/*! \brief Resolve the resource and dispatch an action depending on its type
*
@@ -39,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`
/*!
@@ -64,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
@@ -93,7 +105,7 @@ protected:
*
* \sa UriResolverBase, UriDispatcher
*/
-UriResolveResult
+QUOTIENT_API UriResolveResult
visitResource(Connection* account, const Uri& uri,
std::function<UriResolveResult(User*, QString)> userHandler,
std::function<void(Room*, QString)> roomEventHandler,
@@ -129,7 +141,7 @@ inline UriResolveResult checkResource(Connection* account, const Uri& uri)
* synchronously - the returned value is the result of resolving the URI,
* not acting on it.
*/
-class UriDispatcher : public QObject, public UriResolverBase {
+class QUOTIENT_API UriDispatcher : public QObject, public UriResolverBase {
Q_OBJECT
public:
explicit UriDispatcher(QObject* parent = nullptr) : QObject(parent) {}
@@ -141,7 +153,7 @@ public:
return UriResolverBase::visitResource(account, uri);
}
-signals:
+Q_SIGNALS:
/// An action on a user has been requested
void userAction(Quotient::User* user, QString action);
diff --git a/lib/user.cpp b/lib/user.cpp
index 4e369a4f..4c3fc9e2 100644
--- a/lib/user.cpp
+++ b/lib/user.cpp
@@ -1,20 +1,6 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "user.h"
@@ -47,47 +33,26 @@ public:
QString id;
qreal hueF;
- // In the following two, isNull/nullopt mean they are uninitialised;
- // isEmpty/Avatar::url().isEmpty() mean they are initialised but empty.
QString defaultName;
- std::optional<Avatar> defaultAvatar;
-
+ Avatar defaultAvatar;
// NB: This container is ever-growing. Even if the user no more scrolls
// the timeline that far back, historical avatars are still kept around.
// This is consistent with the rest of Quotient, as room timelines
- // are never rotated either. This will probably change in the future.
+ // are never vacuumed either. This will probably change in the future.
/// Map of mediaId to Avatar objects
static UnorderedMap<QString, Avatar> otherAvatars;
-
- void fetchProfile(const User* q);
-
- template <typename SourceT>
- bool doSetAvatar(SourceT&& source, User* q);
};
decltype(User::Private::otherAvatars) User::Private::otherAvatars {};
-void User::Private::fetchProfile(const User* q)
-{
- defaultAvatar.emplace(Avatar {});
- defaultName = "";
- auto* j =
- q->connection()->callApi<GetUserProfileJob>(BackgroundRequest,
- QUrl::toPercentEncoding(id));
- // FIXME: accepting const User* and const_cast'ing it here is only
- // until we get a better User API in 0.7
- QObject::connect(j, &BaseJob::success, q,
- [this, q = const_cast<User*>(q), j] {
- q->updateName(j->displayname());
- defaultAvatar->updateUrl(j->avatarUrl());
- emit q->avatarChanged(q, nullptr);
- });
-}
-
User::User(QString userId, Connection* connection)
- : QObject(connection), d(new Private(move(userId)))
+ : QObject(connection), d(makeImpl<Private>(move(userId)))
{
setObjectName(id());
+ if (connection->userId() == id()) {
+ // Load profile information for local user.
+ load();
+ }
}
Connection* User::connection() const
@@ -96,7 +61,17 @@ Connection* User::connection() const
return static_cast<Connection*>(parent());
}
-User::~User() = default;
+void User::load()
+{
+ auto* profileJob =
+ connection()->callApi<GetUserProfileJob>(id());
+ connect(profileJob, &BaseJob::result, this, [this, profileJob] {
+ d->defaultName = profileJob->displayname();
+ d->defaultAvatar = Avatar(QUrl(profileJob->avatarUrl()));
+ emit defaultNameChanged();
+ emit defaultAvatarChanged();
+ });
+}
QString User::id() const { return d->id; }
@@ -105,52 +80,16 @@ 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 == ':';
}
int User::hue() const { return int(hueF() * 359); }
-/// \sa https://github.com/matrix-org/matrix-doc/issues/1375
-///
-/// Relies on untrusted prevContent so can't be put to RoomMemberEvent and
-/// in general should rather be remade in terms of the room's eventual "state
-/// time machine"
-QString getBestKnownName(const RoomMemberEvent* event)
-{
- const auto& jv = event->contentJson().value("displayname"_ls);
- return !jv.isUndefined()
- ? jv.toString()
- : event->prevContent() ? event->prevContent()->displayName
- : QString();
-}
-
QString User::name(const Room* room) const
{
- if (room)
- return getBestKnownName(room->getCurrentState<RoomMemberEvent>(id()));
-
- if (d->defaultName.isNull())
- d->fetchProfile(this);
-
- return d->defaultName;
-}
-
-QString User::rawName(const Room* room) const { return name(room); }
-
-void User::updateName(const QString& newName, const Room* r)
-{
- Q_ASSERT(r == nullptr);
- if (newName == d->defaultName)
- return;
-
- emit nameAboutToChange(newName, d->defaultName, nullptr);
- const auto& oldName =
- std::exchange(d->defaultName, newName);
- emit nameChanged(d->defaultName, oldName, nullptr);
+ return room ? room->memberName(id()) : d->defaultName;
}
-void User::updateName(const QString&, const QString&, const Room*) {}
-void User::updateAvatarUrl(const QUrl&, const QUrl&, const Room*) {}
void User::rename(const QString& newName)
{
@@ -160,12 +99,18 @@ void User::rename(const QString& newName)
connect(connection()->callApi<SetDisplayNameJob>(id(), actualNewName),
&BaseJob::success, this, [this, actualNewName] {
- d->fetchProfile(this);
- updateName(actualNewName);
+ // Check again, it could have changed meanwhile
+ if (actualNewName != d->defaultName) {
+ d->defaultName = actualNewName;
+ emit defaultNameChanged();
+ } else
+ qCWarning(MAIN)
+ << "User" << id() << "already has profile name set to"
+ << actualNewName;
});
}
-void User::rename(const QString& newName, const Room* r)
+void User::rename(const QString& newName, Room* r)
{
if (!r) {
qCWarning(MAIN) << "Passing a null room to two-argument User::rename()"
@@ -174,51 +119,51 @@ void User::rename(const QString& newName, const Room* r)
return;
}
// #481: take the current state and update it with the new name
- auto evtC = r->getCurrentState<RoomMemberEvent>(id())->content();
- Q_ASSERT_X(evtC.membership == MembershipType::Join, __FUNCTION__,
- "Attempt to rename a user that's not a room member");
- evtC.displayName = sanitized(newName);
- r->setState<RoomMemberEvent>(id(), move(evtC));
- // The state will be updated locally after it arrives with sync
+ if (const auto& maybeEvt = r->currentState().get<RoomMemberEvent>(id())) {
+ auto content = maybeEvt->content();
+ if (content.membership == Membership::Join) {
+ content.displayName = sanitized(newName);
+ r->setState<RoomMemberEvent>(id(), move(content));
+ // The state will be updated locally after it arrives with sync
+ return;
+ }
+ }
+ qCCritical(MEMBERS)
+ << "Attempt to rename a non-member in a room context - ignored";
}
template <typename SourceT>
-bool User::Private::doSetAvatar(SourceT&& source, User* q)
-{
- if (!defaultAvatar) {
- defaultName = "";
- defaultAvatar.emplace(Avatar {});
- }
- return defaultAvatar->upload(
- q->connection(), source, [this, q](const QString& contentUri) {
- auto* j =
- q->connection()->callApi<SetAvatarUrlJob>(id, contentUri);
- QObject::connect(j, &BaseJob::success, q,
- [this, q, newUrl = QUrl(contentUri)] {
- // Fetch displayname to complete the profile
- fetchProfile(q);
- if (newUrl == defaultAvatar->url()) {
- qCWarning(MAIN)
- << "User" << id
- << "already has avatar URL set to"
- << newUrl.toDisplayString();
- return;
- }
-
- defaultAvatar->updateUrl(newUrl);
- emit q->avatarChanged(q, nullptr);
- });
+inline bool User::doSetAvatar(SourceT&& source)
+{
+ return d->defaultAvatar.upload(
+ connection(), source, [this](const QUrl& contentUri) {
+ auto* j = connection()->callApi<SetAvatarUrlJob>(id(), contentUri);
+ connect(j, &BaseJob::success, this,
+ [this, contentUri] {
+ if (contentUri == d->defaultAvatar.url()) {
+ d->defaultAvatar.updateUrl(contentUri);
+ emit defaultAvatarChanged();
+ } else
+ qCWarning(MAIN) << "User" << id()
+ << "already has avatar URL set to"
+ << contentUri.toDisplayString();
+ });
});
}
bool User::setAvatar(const QString& fileName)
{
- return d->doSetAvatar(fileName, this);
+ return doSetAvatar(fileName);
}
bool User::setAvatar(QIODevice* source)
{
- return d->doSetAvatar(source, this);
+ return doSetAvatar(source);
+}
+
+void User::removeAvatar()
+{
+ connection()->callApi<SetAvatarUrlJob>(id(), QUrl());
}
void User::requestDirectChat() { connection()->requestDirectChat(this); }
@@ -231,13 +176,8 @@ bool User::isIgnored() const { return connection()->isIgnored(this); }
QString User::displayname(const Room* room) const
{
- if (room)
- return room->roomMembername(this);
-
- if (auto n = name(); !n.isEmpty())
- return n;
-
- return d->id;
+ return room ? room->safeMemberName(id())
+ : d->defaultName.isEmpty() ? d->id : d->defaultName;
}
QString User::fullName(const Room* room) const
@@ -246,50 +186,30 @@ QString User::fullName(const Room* room) const
return displayName.isEmpty() ? id() : (displayName % " (" % id() % ')');
}
-QString User::bridged() const { return {}; }
-
-/// \sa getBestKnownName, https://github.com/matrix-org/matrix-doc/issues/1375
-QUrl getBestKnownAvatarUrl(const RoomMemberEvent* event)
-{
- const auto& jv = event->contentJson().value("avatar_url"_ls);
- return !jv.isUndefined()
- ? jv.toString()
- : event->prevContent() ? event->prevContent()->avatarUrl
- : QUrl();
-}
-
const Avatar& User::avatarObject(const Room* room) const
{
- if (!room) {
- if (!d->defaultAvatar) {
- d->fetchProfile(this);
- }
- return *d->defaultAvatar;
- }
+ if (!room)
+ return d->defaultAvatar;
- const auto& url =
- getBestKnownAvatarUrl(room->getCurrentState<RoomMemberEvent>(id()));
+ const auto& url = room->memberAvatarUrl(id());
const auto& mediaId = url.authority() + url.path();
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, [=] {
- emit avatarChanged(this, room);
- callback();
- });
+ return avatarObject(room).get(connection(), width, height, callback);
}
QString User::avatarMediaId(const Room* room) const
@@ -302,32 +222,4 @@ QUrl User::avatarUrl(const Room* room) const
return avatarObject(room).url();
}
-void User::processEvent(const RoomMemberEvent& event, const Room* room,
- bool firstMention)
-{
- Q_ASSERT(room);
-
- // This is prone to abuse if prevContent is forged; only here until 0.7
- // (and the whole method, actually).
- const auto& oldName = event.prevContent() ? event.prevContent()->displayName
- : QString();
- const auto& newName = getBestKnownName(&event);
- // A hacky way to find out if it's about to change or already changed;
- // making it a lambda allows to omit stub event creation when unneeded
- const auto& isAboutToChange = [&event, room, this] {
- return room->getCurrentState<RoomMemberEvent>(id()) != &event;
- };
- if (firstMention || newName != oldName) {
- if (isAboutToChange())
- emit nameAboutToChange(newName, oldName, room);
- else
- emit nameChanged(newName, oldName, room);
- }
- const auto& oldAvatarUrl =
- event.prevContent() ? event.prevContent()->avatarUrl : QUrl();
- const auto& newAvatarUrl = getBestKnownAvatarUrl(&event);
- if ((firstMention || newAvatarUrl != oldAvatarUrl) && !isAboutToChange())
- emit avatarChanged(this, room);
-}
-
qreal User::hueF() const { return d->hueF; }
diff --git a/lib/user.h b/lib/user.h
index a4985877..dfbff4a0 100644
--- a/lib/user.h
+++ b/lib/user.h
@@ -1,24 +1,11 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "avatar.h"
+#include "util.h"
#include <QtCore/QObject>
@@ -27,22 +14,19 @@ class Connection;
class Room;
class RoomMemberEvent;
-class User : public QObject {
+class QUOTIENT_API User : public QObject {
Q_OBJECT
Q_PROPERTY(QString id READ id CONSTANT)
Q_PROPERTY(bool isGuest READ isGuest CONSTANT)
Q_PROPERTY(int hue READ hue CONSTANT)
Q_PROPERTY(qreal hueF READ hueF CONSTANT)
- Q_PROPERTY(QString name READ name NOTIFY nameChanged)
- Q_PROPERTY(QString displayName READ displayname NOTIFY nameChanged STORED false)
- Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged STORED false)
- Q_PROPERTY(QString bridgeName READ bridged NOTIFY nameChanged STORED false)
- Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged
- STORED false)
- Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged)
+ Q_PROPERTY(QString name READ name NOTIFY defaultNameChanged)
+ Q_PROPERTY(QString displayName READ displayname NOTIFY defaultNameChanged STORED false)
+ Q_PROPERTY(QString fullName READ fullName NOTIFY defaultNameChanged STORED false)
+ Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY defaultAvatarChanged STORED false)
+ Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY defaultAvatarChanged)
public:
User(QString userId, Connection* connection);
- ~User() override;
Connection* connection() const;
@@ -55,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.
@@ -85,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
@@ -114,31 +83,26 @@ 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;
- // TODO: This method is only there to emit obsolete signals:
- // nameAboutToChange(), nameChanged() and avatarChanged() - all of these
- // to be removed in 0.7
- /// \deprecated
- void processEvent(const RoomMemberEvent& event, const Room* r,
- bool firstMention);
-
-public slots:
+public Q_SLOTS:
/// Set a new name in the global user profile
void rename(const QString& newName);
/// Set a new name for the user in one room
- void rename(const QString& newName, const Room* r);
+ void rename(const QString& newName, Room* r);
/// Upload the file and use it as an avatar
bool setAvatar(const QString& fileName);
/// Upload contents of the QIODevice and set that as an avatar
bool setAvatar(QIODevice* source);
+ /// Removes the avatar from the profile
+ void removeAvatar();
/// Create or find a direct chat with this user
/*! The resulting chat is returned asynchronously via
* Connection::directChatAvailable()
@@ -150,27 +114,20 @@ public slots:
void unmarkIgnore();
/// Check whether the user is in ignore list
bool isIgnored() const;
+ /// Force loading displayName and avartar url. This is required in
+ /// some cases where the you need to use an user independent of the
+ /// room.
+ void load();
-signals:
- /// \deprecated Use Room::memberListChanged() for member changes
- void nameAboutToChange(QString newName, QString oldName,
- const Quotient::Room* roomContext);
- /// \deprecated Use Room::memberListChanged() for member changes
- void nameChanged(QString newName, QString oldName,
- const Quotient::Room* roomContext);
- /// \deprecated Use Room::memberListChanged() for member changes
- void avatarChanged(Quotient::User* user, const Quotient::Room* roomContext);
-
-private slots: // TODO: remove in 0.7
- /// \deprecated
- void updateName(const QString& newName, const Room* r = nullptr);
- /// \deprecated
- void updateName(const QString&, const QString&, const Room* = nullptr);
- /// \deprecated
- void updateAvatarUrl(const QUrl&, const QUrl&, const Room* = nullptr);
+Q_SIGNALS:
+ void defaultNameChanged();
+ void defaultAvatarChanged();
private:
class Private;
- QScopedPointer<Private> d;
+ ImplPtr<Private> d;
+
+ template <typename SourceT>
+ bool doSetAvatar(SourceT&& source);
};
} // namespace Quotient
diff --git a/lib/util.cpp b/lib/util.cpp
index cf5e81a3..359b2959 100644
--- a/lib/util.cpp
+++ b/lib/util.cpp
@@ -1,20 +1,6 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "util.h"
@@ -28,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
@@ -118,7 +101,7 @@ qreal Quotient::stringToHueF(const QString& s)
}
static const auto ServerPartRegEx = QStringLiteral(
- "(\\[[^][:blank:]]+\\]|[-[:alnum:].]+)" // Either IPv6 address or hostname/IPv4 address
+ "(\\[[^][:space:]]+]|[-[:alnum:].]+)" // IPv6 address or hostname/IPv4 address
"(?::(\\d{1,5}))?" // Optional port
);
@@ -133,34 +116,31 @@ 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;
+}
+
+bool Quotient::encryptionSupported()
+{
+#ifdef Quotient_E2EE_ENABLED
+ return true;
+#else
+ return false;
+#endif
+}
diff --git a/lib/util.h b/lib/util.h
index 8c92df74..ab219488 100644
--- a/lib/util.h
+++ b/lib/util.h
@@ -1,35 +1,66 @@
-/******************************************************************************
- * Copyright (C) 2016 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <kitsune-ral@users.sf.net>
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
+#include "quotient_export.h"
+
#include <QtCore/QLatin1String>
#include <QtCore/QHashFunctions>
-#include <functional>
#include <memory>
#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
+
+#if __cpp_conditional_explicit >= 201806L
+#define QUO_IMPLICIT explicit(false)
+#else
+#define QUO_IMPLICIT
+#endif
+
+#define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \
+ Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended
+
+/// \brief Copy an object with slicing
+///
+/// Unintended slicing is bad, which why there's a C++ Core Guideline that
+/// basically says "don't slice, or if you do, make it explicit". Sonar and
+/// clang-tidy have warnings matching this guideline; unfortunately, those
+/// warnings trigger even when you have a dedicated method (as the guideline
+/// recommends) that makes a slicing copy.
+///
+/// This macro is meant for cases when slicing is intended: the static cast
+/// silences the static analysis warning, and the macro appearance itself makes
+/// it very clear that slicing is wanted here. It is made as a macro
+/// (not as a function template) to support the case of private inheritance
+/// in which a function template would not be able to cast to the private base
+/// (see Uri::toUrl() for an example of just that situation).
+#define SLICE(Object, ToType) ToType{static_cast<const ToType&>(Object)}
namespace Quotient {
/// An equivalent of std::hash for QTypes to enable std::unordered_map<QType, ...>
@@ -44,173 +75,7 @@ struct HashQ {
template <typename KeyT, typename ValT>
using UnorderedMap = std::unordered_map<KeyT, ValT, HashQ<KeyT>>;
-constexpr auto none = std::nullopt;
-
-/** `std::optional` with tweaks
- *
- * The tweaks are:
- * - streamlined assignment (operator=)/emplace()ment of values that can be
- * used to implicitly construct the underlying type, including
- * direct-list-initialisation, e.g.:
- * \code
- * struct S { int a; char b; }
- * Omittable<S> o;
- * o = { 1, 'a' }; // std::optional would require o = S { 1, 'a' }
- * \endcode
- * - entirely deleted value(). The technical reason is that Xcode 10 doesn't
- * have it; but besides that, value_or() or (after explicit checking)
- * `operator*()`/`operator->()` are better alternatives within Quotient
- * that doesn't practice throwing exceptions (as doesn't most of Qt).
- * - disabled non-const lvalue operator*() and operator->(), as it's too easy
- * to inadvertently cause a value change through them.
- * - edit() to provide a safe and explicit lvalue accessor instead of those
- * above. Requires the underlying type to be default-constructible.
- * Allows chained initialisation of nested Omittables:
- * \code
- * struct Inner { int member = 10; Omittable<int> innermost; };
- * struct Outer { int anotherMember = 10; Omittable<Inner> inner; };
- * Omittable<Outer> o; // = { 10, std::nullopt };
- * o.edit().inner.edit().innermost.emplace(42);
- * \endcode
- * - merge() - a soft version of operator= that only overwrites its first
- * operand with the second one if the second one is not empty.
- */
-template <typename T>
-class Omittable : public std::optional<T> {
-public:
- using base_type = std::optional<T>;
- using value_type = std::decay_t<T>;
-
- using std::optional<T>::optional;
-
- // Overload emplace() and operator=() to allow passing braced-init-lists
- // (the standard emplace() does direct-initialisation but
- // not direct-list-initialisation).
- using base_type::operator=;
- Omittable& operator=(const value_type& v)
- {
- base_type::operator=(v);
- return *this;
- }
- Omittable& operator=(value_type&& v)
- {
- base_type::operator=(v);
- return *this;
- }
- using base_type::emplace;
- T& emplace(const T& val) { return base_type::emplace(val); }
- T& emplace(T&& val) { return base_type::emplace(std::move(val)); }
-
- // use value_or() or check (with operator! or has_value) before accessing
- // with operator-> or operator*
- // The technical reason is that Xcode 10 has incomplete std::optional
- // that has no value(); but using value() may also mean that you rely
- // on the optional throwing an exception (which is not assumed practice
- // throughout Quotient) or that you spend unnecessary CPU cycles on
- // an extraneous has_value() check.
- value_type& value() = delete;
- const value_type& value() const = delete;
- value_type& edit()
- {
- return this->has_value() ? base_type::operator*() : this->emplace();
- }
-
- [[deprecated("Use '!o' or '!o.has_value()' instead of 'o.omitted()'")]]
- bool omitted() const
- {
- return !this->has_value();
- }
-
- /// Merge the value from another Omittable
- /// \return true if \p other is not omitted and the value of
- /// the current Omittable was different (or omitted);
- /// in other words, if the current Omittable has changed;
- /// false otherwise
- template <typename T1>
- auto merge(const Omittable<T1>& other)
- -> std::enable_if_t<std::is_convertible<T1, T>::value, bool>
- {
- if (!other || (this->has_value() && **this == *other))
- return false;
- *this = other;
- return true;
- }
-
- // Hide non-const lvalue operator-> and operator* as these are
- // a bit too surprising: value() & doesn't lazy-create an object;
- // and it's too easy to inadvertently change the underlying value.
-
- const value_type* operator->() const& { return base_type::operator->(); }
- value_type* operator->() && { return base_type::operator->(); }
- const value_type& operator*() const& { return base_type::operator*(); }
- value_type& operator*() && { return base_type::operator*(); }
-};
-
-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...>;
- // 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...)>;
-};
-
-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)
-{
- return typename function_traits<FnT>::function_type(std::forward<FnT>(f));
-}
-
-inline auto operator"" _ls(const char* s, std::size_t size)
+constexpr auto operator"" _ls(const char* s, std::size_t size)
{
return QLatin1String(s, int(size));
}
@@ -246,6 +111,23 @@ private:
iterator to;
};
+template <typename T>
+class asKeyValueRange
+{
+public:
+ asKeyValueRange(T& data)
+ : m_data { data }
+ {}
+
+ auto begin() { return m_data.keyValueBegin(); }
+ auto end() { return m_data.keyValueEnd(); }
+
+private:
+ T &m_data;
+};
+template <typename T>
+asKeyValueRange(T&) -> asKeyValueRange<T>;
+
/** A replica of std::find_first_of that returns a pair of iterators
*
* Convenient for cases when you need to know which particular "first of"
@@ -253,8 +135,8 @@ private:
*/
template <typename InputIt, typename ForwardIt, typename Pred>
inline std::pair<InputIt, ForwardIt> findFirstOf(InputIt first, InputIt last,
- ForwardIt sFirst,
- ForwardIt sLast, Pred pred)
+ ForwardIt sFirst,
+ ForwardIt sLast, Pred pred)
{
for (; first != last; ++first)
for (auto it = sFirst; it != sLast; ++it)
@@ -264,27 +146,90 @@ inline std::pair<InputIt, ForwardIt> findFirstOf(InputIt first, InputIt last,
return std::make_pair(last, sLast);
}
+//! \brief An owning implementation pointer
+//!
+//! This is basically std::unique_ptr<> to hold your pimpl's but without having
+//! to define default constructors/operator=() out of line.
+//! Thanks to https://oliora.github.io/2015/12/29/pimpl-and-rule-of-zero.html
+//! for inspiration
+template <typename ImplType, typename TypeToDelete = ImplType>
+using ImplPtr = std::unique_ptr<ImplType, void (*)(TypeToDelete*)>;
+
+// Why this works (see also the link above): because this defers the moment
+// of requiring sizeof of ImplType to the place where makeImpl is invoked
+// (which is located, necessarily, in the .cpp file after ImplType definition).
+// The stock unique_ptr deleter (std::default_delete) normally needs sizeof
+// at the same spot - as long as you defer definition of the owning type
+// constructors and operator='s to the .cpp file as well. Which means you
+// have to explicitly declare and define them (even if with = default),
+// formally breaking the rule of zero; informally, just adding boilerplate code.
+// The custom deleter itself is instantiated at makeImpl invocation - there's
+// no way earlier to even know how ImplType will be deleted and whether that
+// will need sizeof(ImplType) earlier. In theory it's a tad slower because
+// the deleter is called by the pointer; however, the difference will not
+// be noticeable (if exist at all) for any class with non-trivial contents.
+
+//! \brief make_unique for ImplPtr
+//!
+//! Since std::make_unique is not compatible with ImplPtr, this should be used
+//! in constructors of frontend classes to create implementation instances.
+template <typename ImplType, typename TypeToDelete = ImplType, typename... ArgTs>
+inline ImplPtr<ImplType, TypeToDelete> makeImpl(ArgTs&&... args)
+{
+ return ImplPtr<ImplType, TypeToDelete> {
+ new ImplType{std::forward<ArgTs>(args)...},
+ [](TypeToDelete* impl) { delete impl; }
+ };
+}
+
+template <typename ImplType, typename TypeToDelete = ImplType>
+inline ImplPtr<ImplType, TypeToDelete> acquireImpl(ImplType* from)
+{
+ return ImplPtr<ImplType, TypeToDelete> { from, [](TypeToDelete* impl) {
+ delete impl;
+ } };
+}
+
+template <typename ImplType, typename TypeToDelete = ImplType>
+constexpr ImplPtr<ImplType, TypeToDelete> ZeroImpl()
+{
+ return { nullptr, [](TypeToDelete*) { /* nullptr doesn't need deletion */ } };
+}
+
+//! \brief Multiplex several functors in one
+//!
+//! This is a well-known trick to wrap several lambdas into a single functor
+//! class that can be passed to std::visit.
+//! \sa https://en.cppreference.com/w/cpp/utility/variant/visit
+template <typename... FunctorTs>
+struct Overloads : FunctorTs... {
+ using FunctorTs::operator()...;
+};
+
+template <typename... FunctorTs>
+Overloads(FunctorTs&&...) -> Overloads<FunctorTs...>;
+
/** Convert what looks like a URL or a Matrix ID to an HTML hyperlink */
-void linkifyUrls(QString& htmlEscapedText);
+QUOTIENT_API void linkifyUrls(QString& htmlEscapedText);
/** Sanitize the text before showing in HTML
*
* This does toHtmlEscaped() and removes Unicode BiDi marks.
*/
-QString sanitized(const QString& plainText);
+QUOTIENT_API QString sanitized(const QString& plainText);
/** Pretty-print plain text into HTML
*
* This includes HTML escaping of <,>,",& and calling linkifyUrls()
*/
-QString prettyPrint(const QString& plainText);
+QUOTIENT_API QString prettyPrint(const QString& plainText);
/** Return a path to cache directory after making sure that it exists
*
* The returned path has a trailing slash, clients don't need to append it.
- * \param dir path to cache directory relative to the standard cache path
+ * \param dirName path to cache directory relative to the standard cache path
*/
-QString cacheLocation(const QString& dirName);
+QUOTIENT_API QString cacheLocation(const QString& dirName);
/** Hue color component of based of the hash of the string.
*
@@ -293,8 +238,14 @@ QString cacheLocation(const QString& dirName);
* Naming and range are the same as QColor's hueF method:
* https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision
*/
-qreal stringToHueF(const QString& s);
+QUOTIENT_API qreal stringToHueF(const QString& s);
/** Extract the serverpart from MXID */
-QString serverPart(const QString& mxId);
+QUOTIENT_API QString serverPart(const QString& mxId);
+
+QUOTIENT_API QString versionString();
+QUOTIENT_API int majorVersion();
+QUOTIENT_API int minorVersion();
+QUOTIENT_API int patchVersion();
+QUOTIENT_API bool encryptionSupported();
} // namespace Quotient