aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorAlexey Rusakov <Kitsune-Ral@users.sf.net>2022-08-24 09:41:51 +0200
committerAlexey Rusakov <Kitsune-Ral@users.sf.net>2022-08-24 09:41:51 +0200
commit82f4efb0227e7e22e831733fae3952818b063ac2 (patch)
tree3b154a16f9d355996a59c611230d0e010edab57f /lib
parent16d4f4e48304543a0ab59b235edba07f5f2c2204 (diff)
parent6308bff3336ca7680eee54d9bd125f780fa9f033 (diff)
downloadlibquotient-82f4efb0227e7e22e831733fae3952818b063ac2.tar.gz
libquotient-82f4efb0227e7e22e831733fae3952818b063ac2.zip
Merge branch 'dev' into device-verification
# Conflicts: # autotests/testfilecrypto.cpp # lib/connection.cpp # lib/connection.h # lib/database.cpp # lib/database.h # lib/e2ee/qolmoutboundsession.cpp # lib/e2ee/qolmoutboundsession.h # lib/eventitem.h # lib/events/encryptedevent.cpp # lib/events/encryptedevent.h # lib/events/encryptedfile.cpp # lib/events/encryptedfile.h # lib/events/keyverificationevent.cpp # lib/events/keyverificationevent.h # lib/events/roomkeyevent.h # lib/room.cpp # lib/room.h
Diffstat (limited to 'lib')
-rw-r--r--lib/accountregistry.cpp83
-rw-r--r--lib/accountregistry.h56
-rw-r--r--lib/avatar.cpp8
-rw-r--r--lib/connection.cpp671
-rw-r--r--lib/connection.h76
-rw-r--r--lib/converters.cpp14
-rw-r--r--lib/converters.h235
-rw-r--r--lib/csapi/account-data.cpp16
-rw-r--r--lib/csapi/admin.cpp4
-rw-r--r--lib/csapi/administrative_contact.cpp70
-rw-r--r--lib/csapi/administrative_contact.h26
-rw-r--r--lib/csapi/appservice_room_directory.cpp14
-rw-r--r--lib/csapi/appservice_room_directory.h3
-rw-r--r--lib/csapi/banning.cpp20
-rw-r--r--lib/csapi/capabilities.cpp4
-rw-r--r--lib/csapi/content-repo.cpp29
-rw-r--r--lib/csapi/content-repo.h3
-rw-r--r--lib/csapi/create_room.cpp32
-rw-r--r--lib/csapi/create_room.h46
-rw-r--r--lib/csapi/cross_signing.cpp20
-rw-r--r--lib/csapi/cross_signing.h10
-rw-r--r--lib/csapi/definitions/auth_data.h7
-rw-r--r--lib/csapi/definitions/openid_token.h8
-rw-r--r--lib/csapi/definitions/public_rooms_response.h45
-rw-r--r--lib/csapi/definitions/push_condition.h4
-rw-r--r--lib/csapi/device_management.cpp34
-rw-r--r--lib/csapi/device_management.h3
-rw-r--r--lib/csapi/directory.cpp20
-rw-r--r--lib/csapi/event_context.cpp4
-rw-r--r--lib/csapi/filter.cpp8
-rw-r--r--lib/csapi/inviting.cpp10
-rw-r--r--lib/csapi/inviting.h2
-rw-r--r--lib/csapi/joining.cpp20
-rw-r--r--lib/csapi/joining.h8
-rw-r--r--lib/csapi/keys.cpp42
-rw-r--r--lib/csapi/keys.h33
-rw-r--r--lib/csapi/kicking.cpp10
-rw-r--r--lib/csapi/knocking.cpp8
-rw-r--r--lib/csapi/knocking.h2
-rw-r--r--lib/csapi/leaving.cpp12
-rw-r--r--lib/csapi/list_joined_rooms.cpp4
-rw-r--r--lib/csapi/list_public_rooms.cpp32
-rw-r--r--lib/csapi/login.cpp28
-rw-r--r--lib/csapi/login.h30
-rw-r--r--lib/csapi/logout.cpp8
-rw-r--r--lib/csapi/message_pagination.cpp17
-rw-r--r--lib/csapi/message_pagination.h55
-rw-r--r--lib/csapi/notifications.cpp4
-rw-r--r--lib/csapi/notifications.h3
-rw-r--r--lib/csapi/openid.cpp4
-rw-r--r--lib/csapi/openid.h5
-rw-r--r--lib/csapi/peeking_events.cpp4
-rw-r--r--lib/csapi/peeking_events.h4
-rw-r--r--lib/csapi/presence.cpp14
-rw-r--r--lib/csapi/profile.cpp30
-rw-r--r--lib/csapi/pusher.cpp29
-rw-r--r--lib/csapi/pushrules.cpp48
-rw-r--r--lib/csapi/read_markers.cpp10
-rw-r--r--lib/csapi/receipts.cpp4
-rw-r--r--lib/csapi/redaction.cpp8
-rw-r--r--lib/csapi/refresh.cpp18
-rw-r--r--lib/csapi/refresh.h65
-rw-r--r--lib/csapi/registration.cpp71
-rw-r--r--lib/csapi/registration.h53
-rw-r--r--lib/csapi/registration_tokens.cpp33
-rw-r--r--lib/csapi/registration_tokens.h44
-rw-r--r--lib/csapi/relations.cpp111
-rw-r--r--lib/csapi/relations.h277
-rw-r--r--lib/csapi/report_content.cpp10
-rw-r--r--lib/csapi/room_send.cpp4
-rw-r--r--lib/csapi/room_send.h3
-rw-r--r--lib/csapi/room_state.cpp4
-rw-r--r--lib/csapi/room_upgrades.cpp8
-rw-r--r--lib/csapi/rooms.cpp20
-rw-r--r--lib/csapi/rooms.h13
-rw-r--r--lib/csapi/search.cpp8
-rw-r--r--lib/csapi/space_hierarchy.cpp43
-rw-r--r--lib/csapi/space_hierarchy.h152
-rw-r--r--lib/csapi/sso_login_redirect.cpp8
-rw-r--r--lib/csapi/tags.cpp18
-rw-r--r--lib/csapi/third_party_lookup.cpp24
-rw-r--r--lib/csapi/third_party_membership.cpp14
-rw-r--r--lib/csapi/third_party_membership.h2
-rw-r--r--lib/csapi/to_device.cpp8
-rw-r--r--lib/csapi/typing.cpp10
-rw-r--r--lib/csapi/users.cpp10
-rw-r--r--lib/csapi/versions.h8
-rw-r--r--lib/csapi/voip.cpp4
-rw-r--r--lib/csapi/whoami.cpp4
-rw-r--r--lib/csapi/whoami.h8
-rw-r--r--lib/database.cpp108
-rw-r--r--lib/database.h52
-rw-r--r--lib/e2ee/e2ee.h82
-rw-r--r--lib/e2ee/qolmaccount.cpp158
-rw-r--r--lib/e2ee/qolmaccount.h44
-rw-r--r--lib/e2ee/qolminboundsession.cpp14
-rw-r--r--lib/e2ee/qolminboundsession.h16
-rw-r--r--lib/e2ee/qolmmessage.cpp4
-rw-r--r--lib/e2ee/qolmoutboundsession.cpp18
-rw-r--r--lib/e2ee/qolmoutboundsession.h20
-rw-r--r--lib/e2ee/qolmsession.cpp73
-rw-r--r--lib/e2ee/qolmsession.h42
-rw-r--r--lib/e2ee/qolmutility.cpp14
-rw-r--r--lib/e2ee/qolmutility.h7
-rw-r--r--lib/eventitem.cpp21
-rw-r--r--lib/eventitem.h31
-rw-r--r--lib/events/accountdataevents.h33
-rw-r--r--lib/events/callanswerevent.cpp11
-rw-r--r--lib/events/callanswerevent.h6
-rw-r--r--lib/events/callcandidatesevent.h17
-rw-r--r--lib/events/callinviteevent.cpp2
-rw-r--r--lib/events/callinviteevent.h7
-rw-r--r--lib/events/directchatevent.cpp2
-rw-r--r--lib/events/encryptedevent.cpp12
-rw-r--r--lib/events/encryptedevent.h2
-rw-r--r--lib/events/encryptedfile.cpp118
-rw-r--r--lib/events/encryptedfile.h63
-rw-r--r--lib/events/encryptionevent.cpp65
-rw-r--r--lib/events/encryptionevent.h44
-rw-r--r--lib/events/event.cpp2
-rw-r--r--lib/events/event.h152
-rw-r--r--lib/events/eventcontent.cpp101
-rw-r--r--lib/events/eventcontent.h496
-rw-r--r--lib/events/eventloader.h48
-rw-r--r--lib/events/eventrelation.h6
-rw-r--r--lib/events/filesourceinfo.cpp172
-rw-r--r--lib/events/filesourceinfo.h90
-rw-r--r--lib/events/keyverificationevent.cpp194
-rw-r--r--lib/events/keyverificationevent.h122
-rw-r--r--lib/events/redactionevent.h4
-rw-r--r--lib/events/roomavatarevent.h4
-rw-r--r--lib/events/roomcanonicalaliasevent.h45
-rw-r--r--lib/events/roomcreateevent.cpp19
-rw-r--r--lib/events/roomevent.cpp22
-rw-r--r--lib/events/roomevent.h9
-rw-r--r--lib/events/roomkeyevent.h15
-rw-r--r--lib/events/roommemberevent.cpp34
-rw-r--r--lib/events/roommemberevent.h7
-rw-r--r--lib/events/roommessageevent.cpp26
-rw-r--r--lib/events/roommessageevent.h29
-rw-r--r--lib/events/roompowerlevelsevent.cpp66
-rw-r--r--lib/events/roompowerlevelsevent.h8
-rw-r--r--lib/events/simplestateevents.h80
-rw-r--r--lib/events/single_key_value.h27
-rw-r--r--lib/events/stateevent.cpp8
-rw-r--r--lib/events/stateevent.h41
-rw-r--r--lib/events/stickerevent.cpp26
-rw-r--r--lib/events/stickerevent.h19
-rw-r--r--lib/expected.h77
-rw-r--r--lib/function_traits.cpp3
-rw-r--r--lib/function_traits.h36
-rw-r--r--lib/jobs/basejob.cpp28
-rw-r--r--lib/jobs/downloadfilejob.cpp56
-rw-r--r--lib/jobs/downloadfilejob.h5
-rw-r--r--lib/jobs/requestdata.cpp6
-rw-r--r--lib/jobs/requestdata.h22
-rw-r--r--lib/jobs/syncjob.h2
-rw-r--r--lib/keyverificationsession.cpp6
-rw-r--r--lib/logging.h19
-rw-r--r--lib/mxcreply.cpp21
-rw-r--r--lib/networkaccessmanager.cpp15
-rw-r--r--lib/qt_connection_util.h169
-rw-r--r--lib/quotient_common.h67
-rw-r--r--lib/room.cpp636
-rw-r--r--lib/room.h15
-rw-r--r--lib/roomstateview.h3
-rw-r--r--lib/syncdata.cpp24
-rw-r--r--lib/syncdata.h10
-rw-r--r--lib/uri.cpp2
-rw-r--r--lib/util.cpp9
-rw-r--r--lib/util.h72
171 files changed, 4213 insertions, 3164 deletions
diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp
index 616b54b4..ad7c5f99 100644
--- a/lib/accountregistry.cpp
+++ b/lib/accountregistry.cpp
@@ -5,6 +5,7 @@
#include "accountregistry.h"
#include "connection.h"
+#include <QtCore/QCoreApplication>
using namespace Quotient;
@@ -15,14 +16,16 @@ void AccountRegistry::add(Connection* a)
beginInsertRows(QModelIndex(), size(), size());
push_back(a);
endInsertRows();
+ emit accountCountChanged();
}
void AccountRegistry::drop(Connection* a)
{
- const auto idx = indexOf(a);
- beginRemoveRows(QModelIndex(), idx, idx);
- remove(idx);
- endRemoveRows();
+ if (const auto idx = indexOf(a); idx != -1) {
+ beginRemoveRows(QModelIndex(), idx, idx);
+ remove(idx);
+ endRemoveRows();
+ }
Q_ASSERT(!contains(a));
}
@@ -54,8 +57,6 @@ QHash<int, QByteArray> AccountRegistry::roleNames() const
return { { AccountRole, "connection" } };
}
-
-
Connection* AccountRegistry::get(const QString& userId)
{
for (const auto &connection : *this) {
@@ -64,3 +65,73 @@ Connection* AccountRegistry::get(const QString& userId)
}
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
index 2f6dffdf..9560688e 100644
--- a/lib/accountregistry.h
+++ b/lib/accountregistry.h
@@ -5,18 +5,35 @@
#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 const_iterator = QVector::const_iterator;
- using const_reference = QVector::const_reference;
+ 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,
@@ -26,24 +43,24 @@ public:
[[deprecated("Use Accounts variable instead")]] //
static AccountRegistry& instance();
- // Expose most of QVector's const-API but only provide add() and drop()
+ // 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 QVector<Connection*>& accounts() const { return *this; }
+ const vector_t& accounts() const { return *this; }
void add(Connection* a);
void drop(Connection* a);
- const_iterator begin() const { return QVector::begin(); }
- const_iterator end() const { return QVector::end(); }
- const_reference front() const { return QVector::front(); }
- const_reference back() const { return QVector::back(); }
+ 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 QVector::isEmpty, QVector::empty;
- using QVector::size, QVector::count, QVector::capacity;
- using QVector::cbegin, QVector::cend, QVector::contains;
+ 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
@@ -52,9 +69,24 @@ public:
[[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 9304a3de..13de99bf 100644
--- a/lib/avatar.cpp
+++ b/lib/avatar.cpp
@@ -39,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;
@@ -124,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,
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 68aed4e4..3e1e556f 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -36,23 +36,21 @@
#include <variant>
#ifdef Quotient_E2EE_ENABLED
-# include "e2ee/qolmaccount.h"
-# include "e2ee/qolmutils.h"
# include "database.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"
# include "keyverificationsession.h"
+#endif // Quotient_E2EE_ENABLED
#if QT_VERSION_MAJOR >= 6
# include <qt6keychain/keychain.h>
#else
# include <qt5keychain/keychain.h>
#endif
-#endif // Quotient_E2EE_ENABLED
-
-#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
-# include <QtCore/QCborValue>
-#endif
#include <QtCore/QCoreApplication>
#include <QtCore/QDir>
@@ -64,7 +62,6 @@
#include <QtCore/QStringBuilder>
#include <QtNetwork/QDnsLookup>
-
using namespace Quotient;
// This is very much Qt-specific; STL iterators don't have key() and value()
@@ -94,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;
@@ -182,15 +178,6 @@ public:
void consumeToDeviceEvents(Events&& toDeviceEvents);
void consumeDevicesList(DevicesList&& devicesList);
- template <typename EventT>
- EventT* unpackAccountData() const
- {
- const auto& eventIt = accountData.find(EventT::matrixTypeId());
- return eventIt == accountData.end()
- ? nullptr
- : weakPtrCast<EventT>(eventIt->second);
- }
-
void packAndSendAccountData(EventPtr&& event)
{
const auto eventType = event->matrixType();
@@ -213,108 +200,85 @@ public:
#ifdef Quotient_E2EE_ENABLED
void loadSessions() {
- olmSessions = q->database()->loadOlmSessions(q->picklingMode());
- }
- void saveSession(QOlmSessionPtr& session, const QString &senderKey) {
- auto pickleResult = session->pickle(q->picklingMode());
- if (std::holds_alternative<QOlmError>(pickleResult)) {
- qCWarning(E2EE) << "Failed to pickle olm session. Error" << std::get<QOlmError>(pickleResult);
- return;
- }
- q->database()->saveOlmSession(senderKey, session->sessionId(), std::get<QByteArray>(pickleResult), QDateTime::currentDateTime());
+ olmSessions = q->database()->loadOlmSessions(picklingMode);
}
- std::pair<QString, QString> sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr<QOlmAccount>& olmAccount)
+ void saveSession(const QOlmSession& session, const QString& senderKey) const
{
- Q_ASSERT(message.type() == QOlmMessage::PreKey);
- for (size_t i = 0; i < olmSessions[senderKey].size(); i++) {
- auto& session = olmSessions[senderKey][i];
- const auto matches = session->matchesInboundSessionFrom(senderKey, message);
- if(std::holds_alternative<bool>(matches) && std::get<bool>(matches)) {
- qCDebug(E2EE) << "Found inbound session";
- const auto result = session->decrypt(message);
- if(std::holds_alternative<QString>(result)) {
- q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime());
- auto pickle = session->pickle(q->picklingMode());
- if (std::holds_alternative<QByteArray>(pickle)) {
- q->database()->updateOlmSession(senderKey, session->sessionId(), std::get<QByteArray>(pickle));
- } else {
- qCWarning(E2EE) << "Failed to pickle olm session.";
- }
- auto s = std::move(session);
- olmSessions[senderKey].erase(olmSessions[senderKey].begin() + i);
- olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(s));
- return { std::get<QString>(result), olmSessions[senderKey][0]->sessionId() };
- } else {
- qCDebug(E2EE) << "Failed to decrypt prekey message";
- return {};
- }
- }
- }
- qCDebug(E2EE) << "Creating new inbound session";
- auto newSessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), message);
- if(std::holds_alternative<QOlmError>(newSessionResult)) {
- qCWarning(E2EE) << "Failed to create inbound session for" << senderKey << std::get<QOlmError>(newSessionResult);
- return {};
- }
- auto newSession = std::move(std::get<QOlmSessionPtr>(newSessionResult));
- auto error = olmAccount->removeOneTimeKeys(newSession);
- if (error) {
- qWarning(E2EE) << "Failed to remove one time key for session" << newSession->sessionId();
- }
- const auto result = newSession->decrypt(message);
- QString sessionId = newSession->sessionId();
- saveSession(newSession, senderKey);
- olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(newSession));
- if(std::holds_alternative<QString>(result)) {
- return { std::get<QString>(result), sessionId };
- } else {
- qCDebug(E2EE) << "Failed to decrypt prekey message with new session";
- return {};
- }
+ if (auto pickleResult = session.pickle(picklingMode))
+ q->database()->saveOlmSession(senderKey, session.sessionId(),
+ *pickleResult,
+ QDateTime::currentDateTime());
+ else
+ qCWarning(E2EE) << "Failed to pickle olm session. Error"
+ << pickleResult.error();
}
- std::pair<QString, QString> sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey)
+
+ template <typename FnT>
+ std::pair<QString, QString> doDecryptMessage(const QOlmSession& session,
+ const QOlmMessage& message,
+ FnT&& andThen) const
{
- Q_ASSERT(message.type() == QOlmMessage::General);
- for (size_t i = 0; i < olmSessions[senderKey].size(); i++) {
- auto& session = olmSessions[senderKey][i];
- const auto result = session->decrypt(message);
- if(std::holds_alternative<QString>(result)) {
- q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime());
- auto pickle = session->pickle(q->picklingMode());
- if (std::holds_alternative<QByteArray>(pickle)) {
- q->database()->updateOlmSession(senderKey, session->sessionId(), std::get<QByteArray>(pickle));
- } else {
- qCWarning(E2EE) << "Failed to pickle olm session.";
- }
- auto s = std::move(session);
- olmSessions[senderKey].erase(olmSessions[senderKey].begin() + i);
- olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(s));
- return { std::get<QString>(result), olmSessions[senderKey][0]->sessionId() };
- }
+ const auto expectedMessage = session.decrypt(message);
+ if (expectedMessage) {
+ const auto result =
+ std::make_pair(*expectedMessage, session.sessionId());
+ andThen();
+ return result;
}
- qCWarning(E2EE) << "Failed to decrypt message";
+ 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, std::unique_ptr<QOlmAccount>& account)
+ const QJsonObject& personalCipherObject, const QByteArray& senderKey)
{
- QString decrypted;
- QString olmSessionId;
- int type = personalCipherObject.value(TypeKeyL).toInt(-1);
- QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1();
- if (type == QOlmMessage::PreKey) {
- QOlmMessage preKeyMessage(body, QOlmMessage::PreKey);
- auto result = sessionDecryptPrekey(preKeyMessage, senderKey, account);
- decrypted = result.first;
- olmSessionId = result.second;
- } else if (type == QOlmMessage::General) {
- QOlmMessage message(body, QOlmMessage::General);
- auto result = sessionDecryptGeneral(message, senderKey);
- decrypted = result.first;
- olmSessionId = result.second;
+ 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 {};
}
- return { decrypted, olmSessionId };
+ 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);
+ auto error = olmAccount->removeOneTimeKeys(*newSession);
+ if (error) {
+ 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
@@ -334,8 +298,9 @@ public:
qCDebug(E2EE) << "Encrypted event is not for the current device";
return {};
}
- const auto [decrypted, olmSessionId] = sessionDecryptMessage(
- personalCipherObject, encryptedEvent.senderKey().toLatin1(), olmAccount);
+ const auto [decrypted, olmSessionId] =
+ sessionDecryptMessage(personalCipherObject,
+ encryptedEvent.senderKey().toLatin1());
if (decrypted.isEmpty()) {
qCDebug(E2EE) << "Problem with new session from senderKey:"
<< encryptedEvent.senderKey()
@@ -389,10 +354,53 @@ public:
#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 encryptSessionKeyEvent(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(false);
+ 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)
@@ -571,11 +579,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
+ saveAccessTokenToKeychain();
+#ifdef Quotient_E2EE_ENABLED
database->clear();
-#endif // Quotient_E2EE_ENABLED
+#endif
});
connect(loginJob, &BaseJob::failure, q, [this, loginJob] {
emit q->loginError(loginJob->errorString(), loginJob->rawDataSample());
@@ -591,6 +598,7 @@ void Connection::Private::completeSetup(const QString& mxId)
<< "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
@@ -632,9 +640,7 @@ void Connection::Private::completeSetup(const QString& mxId)
olmAccount = std::make_unique<QOlmAccount>(data->userId(), data->deviceId(), q);
connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount);
-#ifdef Quotient_E2EE_ENABLED
loadSessions();
-#endif
if (database->accountPickle().isEmpty()) {
// create new account and save unpickle data
@@ -707,7 +713,8 @@ 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
@@ -856,16 +863,14 @@ void Connection::Private::consumeRoomData(SyncDataList&& roomDataList,
}
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();
}
}
@@ -962,38 +967,45 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents)
{
#ifdef Quotient_E2EE_ENABLED
if (!toDeviceEvents.empty()) {
- qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() << "to-device events";
- visitEach(toDeviceEvents, [this](const EncryptedEvent& event) {
- if (event.algorithm() != OlmV1Curve25519AesSha2AlgoKey) {
- qCDebug(E2EE) << "Unsupported algorithm" << event.id() << "for event" << event.algorithm();
- return;
- }
- if (q->isKnownCurveKey(event.senderId(), event.senderKey())) {
- handleEncryptedToDeviceEvent(event);
- return;
+ qCDebug(E2EE) << "Consuming" << toDeviceEvents.size()
+ << "to-device events";
+ for (auto&& tdEvt : toDeviceEvents) {
+ 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));
+ continue;
}
- trackedUsers += event.senderId();
- outdatedUsers += event.senderId();
- encryptionUpdateRequired = true;
- pendingEncryptedEvents.push_back(std::make_unique<EncryptedEvent>(event.fullJson()));
- }, [this](const KeyVerificationRequestEvent& event) {
- auto session = new KeyVerificationSession(q->userId(), event, q, false, q);
- emit q->newKeyVerificationSession(session);
- }, [this](const KeyVerificationReadyEvent& event) {
- emit q->incomingKeyVerificationReady(event);
- }, [this](const KeyVerificationStartEvent& event) {
- emit q->incomingKeyVerificationStart(event);
- }, [this](const KeyVerificationAcceptEvent& event) {
- emit q->incomingKeyVerificationAccept(event);
- }, [this](const KeyVerificationKeyEvent& event) {
- emit q->incomingKeyVerificationKey(event);
- }, [this](const KeyVerificationMacEvent& event) {
- emit q->incomingKeyVerificationMac(event);
- }, [this](const KeyVerificationDoneEvent& event) {
- emit q->incomingKeyVerificationDone(event);
- }, [this](const KeyVerificationCancelEvent& event) {
- emit q->incomingKeyVerificationCancel(event);
- });
+ switchOnType(*tdEvt,
+ [this](const KeyVerificationRequestEvent& event) {
+ auto session = new KeyVerificationSession(q->userId(), event, q, false, q);
+ emit q->newKeyVerificationSession(session);
+ }, [this](const KeyVerificationReadyEvent& event) {
+ emit q->incomingKeyVerificationReady(event);
+ }, [this](const KeyVerificationStartEvent& event) {
+ emit q->incomingKeyVerificationStart(event);
+ }, [this](const KeyVerificationAcceptEvent& event) {
+ emit q->incomingKeyVerificationAccept(event);
+ }, [this](const KeyVerificationKeyEvent& event) {
+ emit q->incomingKeyVerificationKey(event);
+ }, [this](const KeyVerificationMacEvent& event) {
+ emit q->incomingKeyVerificationMac(event);
+ }, [this](const KeyVerificationDoneEvent& event) {
+ emit q->incomingKeyVerificationDone(event);
+ }, [this](const KeyVerificationCancelEvent& event) {
+ emit q->incomingKeyVerificationCancel(event);
+ });
+ }
}
#endif
}
@@ -1008,7 +1020,7 @@ void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& eve
}
switchOnType(*decryptedEvent,
- [this, senderKey = event.senderKey(), &event, olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) {
+ [this, &event, olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) {
if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) {
detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), olmSessionId);
} else {
@@ -1197,15 +1209,14 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url,
}
#ifdef Quotient_E2EE_ENABLED
-DownloadFileJob* Connection::downloadFile(const QUrl& url,
- const EncryptedFile& file,
- const QString& localFilename)
+DownloadFileJob* Connection::downloadFile(
+ const QUrl& url, const EncryptedFileMetadata& fileMetadata,
+ const QString& localFilename)
{
auto mediaId = url.authority() + url.path();
auto idParts = splitMediaId(mediaId);
- auto* job =
- callApi<DownloadFileJob>(idParts.front(), idParts.back(), file, localFilename);
- return job;
+ return callApi<DownloadFileJob>(idParts.front(), idParts.back(),
+ fileMetadata, localFilename);
}
#endif
@@ -1377,26 +1388,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,
@@ -1489,12 +1485,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);
@@ -1696,7 +1694,7 @@ bool Connection::isIgnored(const User* user) const
IgnoredUsersList Connection::ignoredUsers() const
{
- const auto* event = d->unpackAccountData<IgnoredUsersEvent>();
+ const auto* event = accountData<IgnoredUsersEvent>();
return event ? event->ignored_users() : IgnoredUsersList();
}
@@ -1736,7 +1734,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)
@@ -1761,9 +1759,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)
@@ -1850,16 +1853,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 {
@@ -1912,11 +1909,11 @@ 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 {
@@ -1929,15 +1926,9 @@ void Connection::saveState() const
}
#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());
@@ -2119,19 +2110,20 @@ void Connection::Private::loadOutdatedUserDevices()
continue;
}
}
- deviceKeys[user][device.deviceId] = device;
+ deviceKeys[user][device.deviceId] = SLICE(device, DeviceKeys);
}
outdatedUsers -= user;
}
saveDevicesList();
for(size_t i = 0; i < pendingEncryptedEvents.size();) {
- if (q->isKnownCurveKey(pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(), pendingEncryptedEvents[i]->contentJson()["sender_key"].toString())) {
- handleEncryptedToDeviceEvent(*(pendingEncryptedEvents[i].get()));
+ if (isKnownCurveKey(
+ pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(),
+ pendingEncryptedEvents[i]->contentPart<QString>("sender_key"_ls))) {
+ handleEncryptedToDeviceEvent(*pendingEncryptedEvents[i]);
pendingEncryptedEvents.erase(pendingEncryptedEvents.begin() + i);
- } else {
- i++;
- }
+ } else
+ ++i;
}
});
}
@@ -2234,101 +2226,236 @@ void Connection::saveOlmAccount()
{
qCDebug(E2EE) << "Saving olm account";
#ifdef Quotient_E2EE_ENABLED
- auto pickle = d->olmAccount->pickle(d->picklingMode);
- d->database->setAccountPickle(std::get<QByteArray>(pickle));
+ if (const auto expectedPickle = d->olmAccount->pickle(d->picklingMode))
+ d->database->setAccountPickle(*expectedPickle);
+ else
+ qCWarning(E2EE) << "Couldn't save Olm account pickle:"
+ << expectedPickle.error();
#endif
}
#ifdef Quotient_E2EE_ENABLED
QJsonObject Connection::decryptNotification(const QJsonObject &notification)
{
- auto room = this->room(notification["room_id"].toString());
+ auto r = room(notification["room_id"].toString());
auto event = makeEvent<EncryptedEvent>(notification["event"].toObject());
- auto decrypted = room->decryptMessage(*event);
- if(!decrypted) {
- return QJsonObject();
- }
- return decrypted->fullJson();
+ const auto decrypted = r->decryptMessage(*event);
+ return decrypted ? decrypted->fullJson() : QJsonObject();
}
-Database* Connection::database()
+Database* Connection::database() const
{
return d->database;
}
-UnorderedMap<QString, QOlmInboundGroupSessionPtr> Connection::loadRoomMegolmSessions(Room* room)
+UnorderedMap<QString, QOlmInboundGroupSessionPtr>
+Connection::loadRoomMegolmSessions(const Room* room) const
{
return database()->loadMegolmSessions(room->id(), picklingMode());
}
-void Connection::saveMegolmSession(Room* room, QOlmInboundGroupSession* session)
+void Connection::saveMegolmSession(const Room* room,
+ const QOlmInboundGroupSession& session) const
{
- database()->saveMegolmSession(room->id(), session->sessionId(), session->pickle(picklingMode()), session->senderId(), session->olmSessionId());
+ database()->saveMegolmSession(room->id(), session.sessionId(),
+ session.pickle(picklingMode()),
+ session.senderId(), session.olmSessionId());
}
-QStringList Connection::devicesForUser(User* user) const
+QStringList Connection::devicesForUser(const QString& userId) const
{
- return d->deviceKeys[user->id()].keys();
+ return d->deviceKeys[userId].keys();
}
-QString Connection::curveKeyForUserDevice(const QString& user, const QString& device) const
+QString Connection::Private::curveKeyForUserDevice(const QString& userId,
+ const QString& device) const
{
- return d->deviceKeys[user][device].keys["curve25519:" % device];
+ return deviceKeys[userId][device].keys["curve25519:" % device];
}
-QString Connection::edKeyForUserDevice(const QString& user, const QString& device) const
+QString Connection::edKeyForUserDevice(const QString& userId,
+ const QString& device) const
{
- return d->deviceKeys[user][device].keys["ed25519:" % device];
+ return d->deviceKeys[userId][device].keys["ed25519:" % device];
}
-bool Connection::hasOlmSession(User* user, const QString& deviceId) const
+bool Connection::Private::isKnownCurveKey(const QString& userId,
+ const QString& curveKey) const
{
- const auto& curveKey = curveKeyForUserDevice(user->id(), deviceId);
- return d->olmSessions.contains(curveKey) && d->olmSessions[curveKey].size() > 0;
+ 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();
}
-QPair<QOlmMessage::Type, QByteArray> Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message)
+bool Connection::hasOlmSession(const QString& user,
+ const QString& deviceId) const
{
- const auto& curveKey = curveKeyForUserDevice(user->id(), device);
- QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType();
- auto result = d->olmSessions[curveKey][0]->encrypt(message);
- auto pickle = d->olmSessions[curveKey][0]->pickle(picklingMode());
- if (std::holds_alternative<QByteArray>(pickle)) {
- database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), std::get<QByteArray>(pickle));
+ 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();
+ QOlmMessage::Type type = olmSession->encryptMessageType();
+ const auto result = olmSession->encrypt(message);
+ if (const auto pickle = olmSession->pickle(picklingMode)) {
+ database->updateOlmSession(curveKey, olmSession->sessionId(), *pickle);
} else {
- qCWarning(E2EE) << "Failed to pickle olm session.";
+ qWarning(E2EE) << "Failed to pickle olm session: " << pickle.error();
}
- return qMakePair(type, result.toCiphertext());
+ return { type, result.toCiphertext() };
}
-void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey)
+bool Connection::Private::createOlmSession(const QString& targetUserId,
+ const QString& targetDeviceId,
+ const OneTimeKeys& oneTimeKeyObject)
{
- auto session = QOlmSession::createOutboundSession(olmAccount(), theirIdentityKey, theirOneTimeKey);
- if (std::holds_alternative<QOlmError>(session)) {
- qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << std::get<QOlmError>(session);
- return;
+ 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;
}
- d->saveSession(std::get<std::unique_ptr<QOlmSession>>(session), theirIdentityKey);
- d->olmSessions[theirIdentityKey].push_back(std::move(std::get<std::unique_ptr<QOlmSession>>(session)));
+ 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);
+ 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::encryptSessionKeyEvent(
+ QJsonObject payloadJson, const QString& targetUserId,
+ const QString& targetDeviceId) const
+{
+ 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();
}
-QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(Room* room)
+void Connection::sendSessionKeyToDevices(
+ const QString& roomId, const QByteArray& sessionId,
+ const QByteArray& sessionKey, const QMultiHash<QString, QString>& devices,
+ int index)
{
- return d->database->loadCurrentOutboundMegolmSession(room->id(), d->picklingMode);
+ 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 keyEventJson = RoomKeyEvent(MegolmV1AesSha2AlgoKey, roomId, sessionId,
+ sessionKey, userId())
+ .fullJson();
+ keyEventJson.insert(SenderKeyL, userId());
+ keyEventJson.insert("sender_device"_ls, deviceId());
+ keyEventJson.insert(
+ "keys"_ls,
+ QJsonObject {
+ { Ed25519Key, QString(olmAccount()->identityKeys().ed25519) } });
+
+ auto job = callApi<ClaimKeysJob>(hash);
+ connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, keyEventJson, 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();
+ usersToDevicesToContent[targetUserId][targetDeviceId] =
+ d->encryptSessionKeyEvent(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);
+ }
+ });
}
-void Connection::saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data)
+QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(
+ const QString& roomId) const
{
- d->database->saveCurrentOutboundMegolmSession(room->id(), d->picklingMode, data);
+ return d->database->loadCurrentOutboundMegolmSession(roomId,
+ d->picklingMode);
}
-bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey)
+void Connection::saveCurrentOutboundMegolmSession(
+ const QString& roomId, const QOlmOutboundGroupSession& session) const
{
- auto query = database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId AND curveKey=:curveKey"));
- query.bindValue(":matrixId", user);
- query.bindValue(":curveKey", curveKey);
- database()->execute(query);
- return query.next();
+ d->database->saveCurrentOutboundMegolmSession(roomId, d->picklingMode,
+ session);
}
#endif
@@ -2339,10 +2466,9 @@ void Connection::startKeyVerificationSession(const QString& deviceId)
Q_EMIT newKeyVerificationSession(session);
}
-void Connection::sendToDevice(const QString& userId, const QString& deviceId, event_ptr_tt<Event> event, bool encrypted)
+void Connection::sendToDevice(const QString& userId, const QString& deviceId,
+ event_ptr_tt<Event> event, bool encrypted)
{
-
- UsersToDevicesToEvents payload;
if (encrypted) {
QJsonObject payloadJson = event->fullJson();
payloadJson["recipient"] = userId;
@@ -2354,16 +2480,23 @@ void Connection::sendToDevice(const QString& userId, const QString& deviceId, ev
senderObject["ed25519"] = QString(olmAccount()->identityKeys().ed25519);
payloadJson["keys"] = senderObject;
- const auto& u = user(userId);
- auto cipherText = olmEncryptMessage(u, deviceId, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact));
+ auto cipherText = d->olmEncryptMessage(
+ userId, deviceId,
+ QJsonDocument(payloadJson).toJson(QJsonDocument::Compact));
QJsonObject encryptedJson;
- encryptedJson[curveKeyForUserDevice(userId, deviceId)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}, {"sender", this->userId()}};
- auto encryptedEvent = makeEvent<EncryptedEvent>(encryptedJson, olmAccount()->identityKeys().curve25519);
- payload[userId][deviceId] = std::move(encryptedEvent);
- } else {
- payload[userId][deviceId] = std::move(event);
- }
- sendToDevices(payload[userId][deviceId]->matrixType(), payload);
+ encryptedJson[d->curveKeyForUserDevice(userId, deviceId)] =
+ QJsonObject{ { "type", cipherText.first },
+ { "body", QString(cipherText.second) },
+ { "sender", this->userId() } };
+ const auto& contentJson =
+ EncryptedEvent(encryptedJson,
+ olmAccount()->identityKeys().curve25519)
+ .contentJson();
+ sendToDevices(EncryptedEvent::TypeId,
+ { { userId, { { deviceId, contentJson } } } });
+ } else
+ sendToDevices(event->matrixType(),
+ { { userId, { { deviceId, event->contentJson() } } } });
}
bool Connection::isVerifiedSession(const QString& megolmSessionId)
diff --git a/lib/connection.h b/lib/connection.h
index fc189ac4..b684d16b 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -53,7 +53,7 @@ class SendToDeviceJob;
class SendMessageJob;
class LeaveRoomJob;
class Database;
-struct EncryptedFile;
+struct EncryptedFileMetadata;
class QOlmAccount;
class QOlmInboundGroupSession;
@@ -135,8 +135,7 @@ class QUOTIENT_API Connection : public QObject {
Q_PROPERTY(bool canChangePassword READ canChangePassword NOTIFY capabilitiesLoaded)
public:
- using UsersToDevicesToEvents =
- UnorderedMap<QString, UnorderedMap<QString, std::unique_ptr<Event>>>;
+ using UsersToDevicesToContent = QHash<QString, QHash<QString, QJsonObject>>;
enum RoomVisibility {
PublishRoom,
@@ -177,24 +176,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.
- */
+ //! \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 <typename EventT>
- const typename EventT::content_type accountData() const
+ 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
@@ -320,21 +320,33 @@ public:
bool isLoggedIn() const;
#ifdef Quotient_E2EE_ENABLED
QOlmAccount* olmAccount() const;
- Database* database();
- bool hasOlmSession(User* user, const QString& deviceId) 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;
+
- QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(Room* room);
- void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data);
+ QString edKeyForUserDevice(const QString& user, const QString& device) const;
+ bool hasOlmSession(const QString& user, const QString& deviceId) const;
/// Returns true if this megolm session comes from a verified device
bool isVerifiedSession(const QString& megolmSessionId);
- //This assumes that an olm session with (user, device) exists
- QPair<QOlmMessage::Type, QByteArray> olmEncryptMessage(User* user, const QString& device, const QByteArray& message);
- void createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey);
+ void sendSessionKeyToDevices(const QString& roomId,
+ const QByteArray& sessionId,
+ const QByteArray& sessionKey,
+ const QMultiHash<QString, QString>& devices,
+ int index);
- UnorderedMap<QString, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(Room* room);
- void saveMegolmSession(Room* room, QOlmInboundGroupSession* session);
+ 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;
@@ -607,7 +619,8 @@ public Q_SLOTS:
const QString& localFilename = {});
#ifdef Quotient_E2EE_ENABLED
- DownloadFileJob* downloadFile(const QUrl& url, const EncryptedFile& file,
+ DownloadFileJob* downloadFile(const QUrl& url,
+ const EncryptedFileMetadata& fileMetadata,
const QString& localFilename = {});
#endif
/**
@@ -687,7 +700,7 @@ public Q_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);
@@ -699,13 +712,6 @@ public Q_SLOTS:
#ifdef Quotient_E2EE_ENABLED
void encryptionUpdate(Room *room);
- PicklingMode picklingMode() const;
- QJsonObject decryptNotification(const QJsonObject &notification);
-
- QStringList devicesForUser(User* user) const;
- QString curveKeyForUserDevice(const QString &user, const QString& device) const;
- QString edKeyForUserDevice(const QString& user, const QString& device) const;
- bool isKnownCurveKey(const QString& user, const QString& curveKey);
#endif
Q_SIGNALS:
diff --git a/lib/converters.cpp b/lib/converters.cpp
index 444ca4f6..b0e3a4b6 100644
--- a/lib/converters.cpp
+++ b/lib/converters.cpp
@@ -2,9 +2,23 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "converters.h"
+#include "logging.h"
#include <QtCore/QVariant>
+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 Quotient::JsonConverter<QVariant>::dump(const QVariant& v)
{
return QJsonValue::fromVariant(v);
diff --git a/lib/converters.h b/lib/converters.h
index 515c96fd..688f7bbd 100644
--- a/lib/converters.h
+++ b/lib/converters.h
@@ -16,6 +16,7 @@
#include <type_traits>
#include <vector>
+#include <variant>
class QVariant;
@@ -27,23 +28,19 @@ struct JsonObjectConverter {
static void fillFrom(const QJsonObject&, T&) = delete;
};
-namespace _impl {
- template <typename T, typename = void>
- struct JsonExporter {
- static QJsonObject dump(const T& data)
- {
- QJsonObject jo;
- JsonObjectConverter<T>::dumpTo(jo, data);
- return jo;
- }
- };
+template <typename PodT, typename JsonT>
+PodT fromJson(const JsonT&);
- template <typename T>
- struct JsonExporter<
- T, std::enable_if_t<std::is_invocable_v<decltype(&T::toJson), T>>> {
- static auto dump(const T& data) { return data.toJson(); }
- };
-}
+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(QJsonValueRef jvr) { return fromJson<T>(QJsonValue(jvr)); }
+ 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
//!
@@ -61,13 +58,24 @@ namespace _impl {
//! 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 : _impl::JsonExporter<T> {
+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)
+ {
+ 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)
{
// 'else' below are required to suppress code generation for unused
// branches - 'return' is not enough
@@ -81,66 +89,143 @@ struct JsonConverter : _impl::JsonExporter<T> {
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,
- typename = std::enable_if_t<!std::is_constructible_v<QJsonValue, T>>>
+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; }
-inline auto toJson(const QJsonValue& jv) { return jv; }
-
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)
{
- return JsonConverter<T>::load(jv);
+ // JsonT here can be whatever the respective JsonConverter specialisation
+ // accepts but by default it's QJsonValue, QJsonDocument, or QJsonObject
+ return JsonConverter<PodT>::load(json);
}
-template<>
-inline QJsonValue fromJson(const QJsonValue& jv) { return jv; }
+// 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)
+{
+ 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;
+}
+
+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];
+ }
+
+ _impl::reportEnumOutOfBounds(static_cast<uint32_t>(v),
+ qt_getEnumName(FlagT()));
+ Q_ASSERT(false);
+ return {};
}
// JsonConverter<> specialisations
@@ -163,6 +248,14 @@ inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); }
template <>
inline QString fromJson(const QJsonValue& jv) { return jv.toString(); }
+//! Use fromJson<QString> and use 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 <>
+inline QByteArray fromJson(const QJsonValue& jv) = delete;
+
template <>
inline QJsonArray fromJson(const QJsonValue& jv) { return jv.toArray(); }
@@ -179,15 +272,7 @@ inline QDateTime fromJson(const QJsonValue& jv)
return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC);
}
-inline QJsonValue toJson(const QDate& val) {
- return toJson(
-#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
- QDateTime(val)
-#else
- val.startOfDay()
-#endif
- );
-}
+inline QJsonValue toJson(const QDate& val) { return toJson(val.startOfDay()); }
template <>
inline QDate fromJson(const QJsonValue& jv)
{
@@ -216,6 +301,26 @@ struct QUOTIENT_API JsonConverter<QVariant> {
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)
@@ -414,4 +519,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 09fc8d40..8c71f6c5 100644
--- a/lib/csapi/account-data.cpp
+++ b/lib/csapi/account-data.cpp
@@ -9,23 +9,23 @@ using namespace Quotient;
SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type,
const QJsonObject& content)
: BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataJob"),
- makePath("/_matrix/client/r0", "/user/", userId, "/account_data/",
+ makePath("/_matrix/client/v3", "/user/", userId, "/account_data/",
type))
{
- setRequestData(RequestData(toJson(content)));
+ setRequestData({ toJson(content) });
}
QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId,
const QString& type)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/user/",
+ makePath("/_matrix/client/v3", "/user/",
userId, "/account_data/", type));
}
GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type)
: BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataJob"),
- makePath("/_matrix/client/r0", "/user/", userId, "/account_data/",
+ makePath("/_matrix/client/v3", "/user/", userId, "/account_data/",
type))
{}
@@ -34,10 +34,10 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId,
const QString& type,
const QJsonObject& content)
: BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataPerRoomJob"),
- makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ makePath("/_matrix/client/v3", "/user/", userId, "/rooms/",
roomId, "/account_data/", type))
{
- setRequestData(RequestData(toJson(content)));
+ setRequestData({ toJson(content) });
}
QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl,
@@ -46,7 +46,7 @@ QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl,
const QString& type)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/user/",
+ makePath("/_matrix/client/v3", "/user/",
userId, "/rooms/", roomId,
"/account_data/", type));
}
@@ -55,6 +55,6 @@ GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId,
const QString& roomId,
const QString& type)
: BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataPerRoomJob"),
- makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ makePath("/_matrix/client/v3", "/user/", userId, "/rooms/",
roomId, "/account_data/", type))
{}
diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp
index 81dd0624..322212db 100644
--- a/lib/csapi/admin.cpp
+++ b/lib/csapi/admin.cpp
@@ -9,11 +9,11 @@ using namespace Quotient;
QUrl GetWhoIsJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/admin/whois/", userId));
}
GetWhoIsJob::GetWhoIsJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetWhoIsJob"),
- makePath("/_matrix/client/r0", "/admin/whois/", userId))
+ makePath("/_matrix/client/v3", "/admin/whois/", userId))
{}
diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp
index 589c9fc1..aa55d934 100644
--- a/lib/csapi/administrative_contact.cpp
+++ b/lib/csapi/administrative_contact.cpp
@@ -9,59 +9,59 @@ using namespace Quotient;
QUrl GetAccount3PIDsJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(
- std::move(baseUrl), makePath("/_matrix/client/r0", "/account/3pid"));
+ std::move(baseUrl), makePath("/_matrix/client/v3", "/account/3pid"));
}
GetAccount3PIDsJob::GetAccount3PIDsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetAccount3PIDsJob"),
- makePath("/_matrix/client/r0", "/account/3pid"))
+ makePath("/_matrix/client/v3", "/account/3pid"))
{}
Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds)
: BaseJob(HttpVerb::Post, QStringLiteral("Post3PIDsJob"),
- makePath("/_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"),
- makePath("/_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"),
- makePath("/_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"),
- makePath("/_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");
}
@@ -69,32 +69,32 @@ Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium,
const QString& address,
const QString& idServer)
: BaseJob(HttpVerb::Post, QStringLiteral("Unbind3pidFromAccountJob"),
- makePath("/_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"),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/account/3pid/email/requestToken"),
false)
{
- setRequestData(RequestData(toJson(body)));
+ setRequestData({ toJson(body) });
}
RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob(
const MsisdnValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDMSISDNJob"),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/account/3pid/msisdn/requestToken"),
false)
{
- setRequestData(RequestData(toJson(body)));
+ setRequestData({ toJson(body) });
}
diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h
index e636b12a..27334850 100644
--- a/lib/csapi/administrative_contact.h
+++ b/lib/csapi/administrative_contact.h
@@ -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 <>
@@ -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.
@@ -295,7 +311,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.
@@ -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,7 +353,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.
@@ -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 40d784c6..dff7e032 100644
--- a/lib/csapi/appservice_room_directory.cpp
+++ b/lib/csapi/appservice_room_directory.cpp
@@ -6,14 +6,16 @@
using namespace Quotient;
-UpdateAppserviceRoomDirectoryVisibilityJob::UpdateAppserviceRoomDirectoryVisibilityJob(
- const QString& networkId, const QString& roomId, const QString& visibility)
+UpdateAppserviceRoomDirectoryVisibilityJob::
+ UpdateAppserviceRoomDirectoryVisibilityJob(const QString& networkId,
+ const QString& roomId,
+ const QString& visibility)
: BaseJob(HttpVerb::Put,
QStringLiteral("UpdateAppserviceRoomDirectoryVisibilityJob"),
- makePath("/_matrix/client/r0", "/directory/list/appservice/",
+ 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 6b2801ca..d6268979 100644
--- a/lib/csapi/appservice_room_directory.h
+++ b/lib/csapi/appservice_room_directory.h
@@ -21,8 +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 QUOTIENT_API UpdateAppserviceRoomDirectoryVisibilityJob
- : public BaseJob {
+class QUOTIENT_API UpdateAppserviceRoomDirectoryVisibilityJob : public BaseJob {
public:
/*! \brief Updates a room's visibility in the application service's room
* directory.
diff --git a/lib/csapi/banning.cpp b/lib/csapi/banning.cpp
index 472128bb..e04075b7 100644
--- a/lib/csapi/banning.cpp
+++ b/lib/csapi/banning.cpp
@@ -9,21 +9,21 @@ using namespace Quotient;
BanJob::BanJob(const QString& roomId, const QString& userId,
const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("BanJob"),
- makePath("/_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,
const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("UnbanJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/unban"))
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/unban"))
{
- 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/capabilities.cpp b/lib/csapi/capabilities.cpp
index bc21e462..ca2a543f 100644
--- a/lib/csapi/capabilities.cpp
+++ b/lib/csapi/capabilities.cpp
@@ -9,12 +9,12 @@ using namespace Quotient;
QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(
- std::move(baseUrl), makePath("/_matrix/client/r0", "/capabilities"));
+ std::move(baseUrl), makePath("/_matrix/client/v3", "/capabilities"));
}
GetCapabilitiesJob::GetCapabilitiesJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetCapabilitiesJob"),
- makePath("/_matrix/client/r0", "/capabilities"))
+ makePath("/_matrix/client/v3", "/capabilities"))
{
addExpectedKey("capabilities");
}
diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp
index 6d1e38b6..6f6738af 100644
--- a/lib/csapi/content-repo.cpp
+++ b/lib/csapi/content-repo.cpp
@@ -16,11 +16,11 @@ auto queryToUploadContent(const QString& filename)
UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename,
const QString& contentType)
: BaseJob(HttpVerb::Post, QStringLiteral("UploadContentJob"),
- makePath("/_matrix/media/r0", "/upload"),
+ makePath("/_matrix/media/v3", "/upload"),
queryToUploadContent(filename))
{
setRequestHeader("Content-Type", contentType.toLatin1());
- setRequestData(RequestData(content));
+ setRequestData({ content });
addExpectedKey("content_uri");
}
@@ -35,7 +35,7 @@ QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName,
const QString& mediaId, bool allowRemote)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/media/r0", "/download/",
+ makePath("/_matrix/media/v3", "/download/",
serverName, "/", mediaId),
queryToGetContent(allowRemote));
}
@@ -43,7 +43,7 @@ QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName,
GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId,
bool allowRemote)
: BaseJob(HttpVerb::Get, QStringLiteral("GetContentJob"),
- makePath("/_matrix/media/r0", "/download/", serverName, "/",
+ makePath("/_matrix/media/v3", "/download/", serverName, "/",
mediaId),
queryToGetContent(allowRemote), {}, false)
{
@@ -64,7 +64,7 @@ QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl,
bool allowRemote)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/media/r0", "/download/",
+ makePath("/_matrix/media/v3", "/download/",
serverName, "/", mediaId, "/",
fileName),
queryToGetContentOverrideName(allowRemote));
@@ -75,7 +75,7 @@ GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName,
const QString& fileName,
bool allowRemote)
: BaseJob(HttpVerb::Get, QStringLiteral("GetContentOverrideNameJob"),
- makePath("/_matrix/media/r0", "/download/", serverName, "/",
+ makePath("/_matrix/media/v3", "/download/", serverName, "/",
mediaId, "/", fileName),
queryToGetContentOverrideName(allowRemote), {}, false)
{
@@ -101,16 +101,17 @@ QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl,
{
return BaseJob::makeRequestUrl(
std::move(baseUrl),
- makePath("/_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"),
- makePath("/_matrix/media/r0", "/thumbnail/", serverName, "/",
+ makePath("/_matrix/media/v3", "/thumbnail/", serverName, "/",
mediaId),
queryToGetContentThumbnail(width, height, method, allowRemote),
{}, false)
@@ -130,24 +131,24 @@ QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QUrl& url,
Omittable<qint64> ts)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/media/r0",
+ makePath("/_matrix/media/v3",
"/preview_url"),
queryToGetUrlPreview(url, ts));
}
GetUrlPreviewJob::GetUrlPreviewJob(const QUrl& url, Omittable<qint64> ts)
: BaseJob(HttpVerb::Get, QStringLiteral("GetUrlPreviewJob"),
- makePath("/_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),
- makePath("/_matrix/media/r0", "/config"));
+ makePath("/_matrix/media/v3", "/config"));
}
GetConfigJob::GetConfigJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetConfigJob"),
- makePath("/_matrix/media/r0", "/config"))
+ makePath("/_matrix/media/v3", "/config"))
{}
diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h
index 511db985..2ba66a35 100644
--- a/lib/csapi/content-repo.h
+++ b/lib/csapi/content-repo.h
@@ -162,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
diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp
index 9aaef87f..afae80af 100644
--- a/lib/csapi/create_room.cpp
+++ b/lib/csapi/create_room.cpp
@@ -16,24 +16,26 @@ CreateRoomJob::CreateRoomJob(const QString& visibility,
const QString& preset, Omittable<bool> isDirect,
const QJsonObject& powerLevelContentOverride)
: BaseJob(HttpVerb::Post, QStringLiteral("CreateRoomJob"),
- makePath("/_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 7d566057..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:
@@ -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
index 1fa0e949..83136d71 100644
--- a/lib/csapi/cross_signing.cpp
+++ b/lib/csapi/cross_signing.cpp
@@ -9,23 +9,25 @@ using namespace Quotient;
UploadCrossSigningKeysJob::UploadCrossSigningKeysJob(
const Omittable<CrossSigningKey>& masterKey,
const Omittable<CrossSigningKey>& selfSigningKey,
- const Omittable<CrossSigningKey>& userSigningKey)
+ const Omittable<CrossSigningKey>& userSigningKey,
+ const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningKeysJob"),
- makePath("/_matrix/client/r0", "/keys/device_signing/upload"))
+ makePath("/_matrix/client/v3", "/keys/device_signing/upload"))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("master_key"), masterKey);
- addParam<IfNotEmpty>(_data, QStringLiteral("self_signing_key"),
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("master_key"), masterKey);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("self_signing_key"),
selfSigningKey);
- addParam<IfNotEmpty>(_data, QStringLiteral("user_signing_key"),
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("user_signing_key"),
userSigningKey);
- setRequestData(std::move(_data));
+ 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/r0", "/keys/signatures/upload"))
+ makePath("/_matrix/client/v3", "/keys/signatures/upload"))
{
- setRequestData(RequestData(toJson(signatures)));
+ setRequestData({ toJson(signatures) });
}
diff --git a/lib/csapi/cross_signing.h b/lib/csapi/cross_signing.h
index 617b61d1..6cea73e6 100644
--- a/lib/csapi/cross_signing.h
+++ b/lib/csapi/cross_signing.h
@@ -4,6 +4,7 @@
#pragma once
+#include "csapi/definitions/auth_data.h"
#include "csapi/definitions/cross_signing_key.h"
#include "jobs/basejob.h"
@@ -35,11 +36,16 @@ public:
* 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<CrossSigningKey>& userSigningKey = none,
+ const Omittable<AuthenticationData>& auth = none);
};
/*! \brief Upload cross-signing signatures.
@@ -55,7 +61,7 @@ public:
* The signatures to be published.
*/
explicit UploadCrossSigningSignaturesJob(
- const QHash<QString, QHash<QString, QJsonObject>>& signatures = {});
+ const QHash<QString, QHash<QString, QJsonObject>>& signatures);
// Result properties
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/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 2938b4ec..d0a2595c 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;
@@ -49,7 +46,6 @@ 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);
@@ -64,7 +60,6 @@ struct JsonObjectConverter<PublicRoomsChunk> {
}
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);
@@ -77,44 +72,4 @@ struct JsonObjectConverter<PublicRoomsChunk> {
}
};
-/// 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);
- }
-};
-
} // namespace Quotient
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/device_management.cpp b/lib/csapi/device_management.cpp
index da6dbc76..6f2badee 100644
--- a/lib/csapi/device_management.cpp
+++ b/lib/csapi/device_management.cpp
@@ -9,53 +9,53 @@ using namespace Quotient;
QUrl GetDevicesJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/devices"));
+ makePath("/_matrix/client/v3", "/devices"));
}
GetDevicesJob::GetDevicesJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetDevicesJob"),
- makePath("/_matrix/client/r0", "/devices"))
+ makePath("/_matrix/client/v3", "/devices"))
{}
QUrl GetDeviceJob::makeRequestUrl(QUrl baseUrl, const QString& deviceId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/devices/",
+ makePath("/_matrix/client/v3", "/devices/",
deviceId));
}
GetDeviceJob::GetDeviceJob(const QString& deviceId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetDeviceJob"),
- makePath("/_matrix/client/r0", "/devices/", deviceId))
+ makePath("/_matrix/client/v3", "/devices/", deviceId))
{}
UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId,
const QString& displayName)
: BaseJob(HttpVerb::Put, QStringLiteral("UpdateDeviceJob"),
- makePath("/_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"),
- makePath("/_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"),
- makePath("/_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 430d2132..c10389b3 100644
--- a/lib/csapi/device_management.h
+++ b/lib/csapi/device_management.h
@@ -86,7 +86,8 @@ 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 QUOTIENT_API DeleteDeviceJob : public BaseJob {
public:
diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp
index b351b4ef..c1255bb1 100644
--- a/lib/csapi/directory.cpp
+++ b/lib/csapi/directory.cpp
@@ -8,48 +8,48 @@ using namespace Quotient;
SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId)
: BaseJob(HttpVerb::Put, QStringLiteral("SetRoomAliasJob"),
- makePath("/_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),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/directory/room/", roomAlias));
}
GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomIdByAliasJob"),
- makePath("/_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),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/directory/room/", roomAlias));
}
DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomAliasJob"),
- makePath("/_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),
- makePath("/_matrix/client/r0", "/rooms/",
+ makePath("/_matrix/client/v3", "/rooms/",
roomId, "/aliases"));
}
GetLocalAliasesJob::GetLocalAliasesJob(const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetLocalAliasesJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/aliases"))
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/aliases"))
{
addExpectedKey("aliases");
}
diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp
index 877838e2..4ebbbf98 100644
--- a/lib/csapi/event_context.cpp
+++ b/lib/csapi/event_context.cpp
@@ -20,7 +20,7 @@ QUrl GetEventContextJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
const QString& filter)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/rooms/",
+ makePath("/_matrix/client/v3", "/rooms/",
roomId, "/context/", eventId),
queryToGetEventContext(limit, filter));
}
@@ -30,7 +30,7 @@ GetEventContextJob::GetEventContextJob(const QString& roomId,
Omittable<int> limit,
const QString& filter)
: BaseJob(HttpVerb::Get, QStringLiteral("GetEventContextJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/context/",
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/context/",
eventId),
queryToGetEventContext(limit, filter))
{}
diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp
index 38c68be7..2469fbd1 100644
--- a/lib/csapi/filter.cpp
+++ b/lib/csapi/filter.cpp
@@ -8,9 +8,9 @@ using namespace Quotient;
DefineFilterJob::DefineFilterJob(const QString& userId, const Filter& filter)
: BaseJob(HttpVerb::Post, QStringLiteral("DefineFilterJob"),
- makePath("/_matrix/client/r0", "/user/", userId, "/filter"))
+ makePath("/_matrix/client/v3", "/user/", userId, "/filter"))
{
- setRequestData(RequestData(toJson(filter)));
+ setRequestData({ toJson(filter) });
addExpectedKey("filter_id");
}
@@ -18,12 +18,12 @@ QUrl GetFilterJob::makeRequestUrl(QUrl baseUrl, const QString& userId,
const QString& filterId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/user/",
+ makePath("/_matrix/client/v3", "/user/",
userId, "/filter/", filterId));
}
GetFilterJob::GetFilterJob(const QString& userId, const QString& filterId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetFilterJob"),
- makePath("/_matrix/client/r0", "/user/", userId, "/filter/",
+ makePath("/_matrix/client/v3", "/user/", userId, "/filter/",
filterId))
{}
diff --git a/lib/csapi/inviting.cpp b/lib/csapi/inviting.cpp
index 39d24611..41a8b5be 100644
--- a/lib/csapi/inviting.cpp
+++ b/lib/csapi/inviting.cpp
@@ -9,10 +9,10 @@ using namespace Quotient;
InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId,
const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("InviteUserJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/invite"))
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/invite"))
{
- 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/inviting.h b/lib/csapi/inviting.h
index 21e6cb74..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
diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp
index 373c1c6a..cdba95e9 100644
--- a/lib/csapi/joining.cpp
+++ b/lib/csapi/joining.cpp
@@ -10,13 +10,13 @@ JoinRoomByIdJob::JoinRoomByIdJob(
const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned,
const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomByIdJob"),
- makePath("/_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);
- addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
- setRequestData(std::move(_data));
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason);
+ setRequestData({ _dataJson });
addExpectedKey("room_id");
}
@@ -32,13 +32,13 @@ JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias,
const Omittable<ThirdPartySigned>& thirdPartySigned,
const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomJob"),
- makePath("/_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);
- addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
- 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 f64152f7..233537bb 100644
--- a/lib/csapi/joining.h
+++ b/lib/csapi/joining.h
@@ -22,8 +22,8 @@ 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 QUOTIENT_API JoinRoomByIdJob : public BaseJob {
public:
@@ -64,8 +64,8 @@ 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 QUOTIENT_API JoinRoomJob : public BaseJob {
public:
diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp
index d6bd2fab..2e4978f2 100644
--- a/lib/csapi/keys.cpp
+++ b/lib/csapi/keys.cpp
@@ -7,39 +7,43 @@
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"),
- makePath("/_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"),
- makePath("/_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"),
- makePath("/_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");
}
@@ -55,13 +59,13 @@ QUrl GetKeysChangesJob::makeRequestUrl(QUrl baseUrl, const QString& from,
const QString& to)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/keys/changes"),
queryToGetKeysChanges(from, to));
}
GetKeysChangesJob::GetKeysChangesJob(const QString& from, const QString& to)
: BaseJob(HttpVerb::Get, QStringLiteral("GetKeysChangesJob"),
- makePath("/_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 ce1ca9ed..2f2ebc6d 100644
--- a/lib/csapi/keys.h
+++ b/lib/csapi/keys.h
@@ -4,6 +4,8 @@
#pragma once
+#include "e2ee/e2ee.h"
+
#include "csapi/definitions/cross_signing_key.h"
#include "csapi/definitions/device_keys.h"
@@ -30,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);
@@ -207,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);
}
};
@@ -233,7 +256,7 @@ public:
* \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.
@@ -241,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 433e592c..4ca39c4c 100644
--- a/lib/csapi/kicking.cpp
+++ b/lib/csapi/kicking.cpp
@@ -9,10 +9,10 @@ using namespace Quotient;
KickJob::KickJob(const QString& roomId, const QString& userId,
const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("KickJob"),
- makePath("/_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/knocking.cpp b/lib/csapi/knocking.cpp
index 73e13e6e..b9da4b9b 100644
--- a/lib/csapi/knocking.cpp
+++ b/lib/csapi/knocking.cpp
@@ -16,11 +16,11 @@ auto queryToKnockRoom(const QStringList& serverName)
KnockRoomJob::KnockRoomJob(const QString& roomIdOrAlias,
const QStringList& serverName, const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("KnockRoomJob"),
- makePath("/_matrix/client/r0", "/knock/", roomIdOrAlias),
+ makePath("/_matrix/client/v3", "/knock/", roomIdOrAlias),
queryToKnockRoom(serverName))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
- setRequestData(std::move(_data));
+ 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
index e3645b59..f43033a8 100644
--- a/lib/csapi/knocking.h
+++ b/lib/csapi/knocking.h
@@ -25,7 +25,7 @@ namespace Quotient {
* history visibility to the user.
*
* The knock will appear as an entry in the response of the
- * [`/sync`](/client-server-api/#get_matrixclientr0sync) API.
+ * [`/sync`](/client-server-api/#get_matrixclientv3sync) API.
*/
class QUOTIENT_API KnockRoomJob : public BaseJob {
public:
diff --git a/lib/csapi/leaving.cpp b/lib/csapi/leaving.cpp
index 0e5386be..ba91f26a 100644
--- a/lib/csapi/leaving.cpp
+++ b/lib/csapi/leaving.cpp
@@ -8,21 +8,21 @@ using namespace Quotient;
LeaveRoomJob::LeaveRoomJob(const QString& roomId, const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("LeaveRoomJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/leave"))
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/leave"))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason);
+ setRequestData({ _dataJson });
}
QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/rooms/",
+ makePath("/_matrix/client/v3", "/rooms/",
roomId, "/forget"));
}
ForgetRoomJob::ForgetRoomJob(const QString& roomId)
: BaseJob(HttpVerb::Post, QStringLiteral("ForgetRoomJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/forget"))
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/forget"))
{}
diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp
index 22ba04da..cdcf3eb2 100644
--- a/lib/csapi/list_joined_rooms.cpp
+++ b/lib/csapi/list_joined_rooms.cpp
@@ -9,12 +9,12 @@ using namespace Quotient;
QUrl GetJoinedRoomsJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(
- std::move(baseUrl), makePath("/_matrix/client/r0", "/joined_rooms"));
+ std::move(baseUrl), makePath("/_matrix/client/v3", "/joined_rooms"));
}
GetJoinedRoomsJob::GetJoinedRoomsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedRoomsJob"),
- makePath("/_matrix/client/r0", "/joined_rooms"))
+ makePath("/_matrix/client/v3", "/joined_rooms"))
{
addExpectedKey("joined_rooms");
}
diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp
index 25f8da5c..4deecfc2 100644
--- a/lib/csapi/list_public_rooms.cpp
+++ b/lib/csapi/list_public_rooms.cpp
@@ -10,25 +10,25 @@ QUrl GetRoomVisibilityOnDirectoryJob::makeRequestUrl(QUrl baseUrl,
const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/directory/list/room/", roomId));
}
GetRoomVisibilityOnDirectoryJob::GetRoomVisibilityOnDirectoryJob(
const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomVisibilityOnDirectoryJob"),
- makePath("/_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"),
- makePath("/_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,
@@ -46,7 +46,7 @@ QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable<int> limit,
const QString& server)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/publicRooms"),
queryToGetPublicRooms(limit, since, server));
}
@@ -54,7 +54,7 @@ QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable<int> limit,
GetPublicRoomsJob::GetPublicRoomsJob(Omittable<int> limit, const QString& since,
const QString& server)
: BaseJob(HttpVerb::Get, QStringLiteral("GetPublicRoomsJob"),
- makePath("/_matrix/client/r0", "/publicRooms"),
+ makePath("/_matrix/client/v3", "/publicRooms"),
queryToGetPublicRooms(limit, since, server), {}, false)
{
addExpectedKey("chunk");
@@ -74,17 +74,17 @@ QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server,
Omittable<bool> includeAllNetworks,
const QString& thirdPartyInstanceId)
: BaseJob(HttpVerb::Post, QStringLiteral("QueryPublicRoomsJob"),
- makePath("/_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/login.cpp b/lib/csapi/login.cpp
index 71fd93c5..81e603b5 100644
--- a/lib/csapi/login.cpp
+++ b/lib/csapi/login.cpp
@@ -9,29 +9,33 @@ using namespace Quotient;
QUrl GetLoginFlowsJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/login"));
+ makePath("/_matrix/client/v3", "/login"));
}
GetLoginFlowsJob::GetLoginFlowsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetLoginFlowsJob"),
- makePath("/_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"),
- makePath("/_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 });
}
diff --git a/lib/csapi/login.h b/lib/csapi/login.h
index ce6951eb..b9f14266 100644
--- a/lib/csapi/login.h
+++ b/lib/csapi/login.h
@@ -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 e8083e31..9ec54c71 100644
--- a/lib/csapi/logout.cpp
+++ b/lib/csapi/logout.cpp
@@ -9,21 +9,21 @@ using namespace Quotient;
QUrl LogoutJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/logout"));
+ makePath("/_matrix/client/v3", "/logout"));
}
LogoutJob::LogoutJob()
: BaseJob(HttpVerb::Post, QStringLiteral("LogoutJob"),
- makePath("/_matrix/client/r0", "/logout"))
+ makePath("/_matrix/client/v3", "/logout"))
{}
QUrl LogoutAllJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(
- std::move(baseUrl), makePath("/_matrix/client/r0", "/logout/all"));
+ std::move(baseUrl), makePath("/_matrix/client/v3", "/logout/all"));
}
LogoutAllJob::LogoutAllJob()
: BaseJob(HttpVerb::Post, QStringLiteral("LogoutAllJob"),
- makePath("/_matrix/client/r0", "/logout/all"))
+ makePath("/_matrix/client/v3", "/logout/all"))
{}
diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp
index 1a93b75b..0b2c99ce 100644
--- a/lib/csapi/message_pagination.cpp
+++ b/lib/csapi/message_pagination.cpp
@@ -11,7 +11,7 @@ auto queryToGetRoomEvents(const QString& from, const QString& to,
const QString& filter)
{
QUrlQuery _q;
- addParam<>(_q, QStringLiteral("from"), from);
+ addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
addParam<IfNotEmpty>(_q, QStringLiteral("to"), to);
addParam<>(_q, QStringLiteral("dir"), dir);
addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
@@ -20,20 +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),
- makePath("/_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"),
- makePath("/_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 8c18f104..9831ae2d 100644
--- a/lib/csapi/message_pagination.h
+++ b/lib/csapi/message_pagination.h
@@ -25,20 +25,30 @@ public:
* \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 1e523c6f..38aed174 100644
--- a/lib/csapi/notifications.cpp
+++ b/lib/csapi/notifications.cpp
@@ -21,7 +21,7 @@ QUrl GetNotificationsJob::makeRequestUrl(QUrl baseUrl, const QString& from,
const QString& only)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/notifications"),
queryToGetNotifications(from, limit, only));
}
@@ -30,7 +30,7 @@ GetNotificationsJob::GetNotificationsJob(const QString& from,
Omittable<int> limit,
const QString& only)
: BaseJob(HttpVerb::Get, QStringLiteral("GetNotificationsJob"),
- makePath("/_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 23211758..48167877 100644
--- a/lib/csapi/notifications.h
+++ b/lib/csapi/notifications.h
@@ -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 5c93a2d7..7e89b8a6 100644
--- a/lib/csapi/openid.cpp
+++ b/lib/csapi/openid.cpp
@@ -9,8 +9,8 @@ using namespace Quotient;
RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId,
const QJsonObject& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestOpenIdTokenJob"),
- makePath("/_matrix/client/r0", "/user/", userId,
+ makePath("/_matrix/client/v3", "/user/", userId,
"/openid/request_token"))
{
- setRequestData(RequestData(toJson(body)));
+ setRequestData({ toJson(body) });
}
diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h
index 773b6011..b3f72a25 100644
--- a/lib/csapi/openid.h
+++ b/lib/csapi/openid.h
@@ -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 eb5d22fa..9dd1445e 100644
--- a/lib/csapi/peeking_events.cpp
+++ b/lib/csapi/peeking_events.cpp
@@ -20,13 +20,13 @@ QUrl PeekEventsJob::makeRequestUrl(QUrl baseUrl, const QString& from,
Omittable<int> timeout, const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_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"),
- makePath("/_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 14cb6f0b..ff688c49 100644
--- a/lib/csapi/peeking_events.h
+++ b/lib/csapi/peeking_events.h
@@ -9,7 +9,7 @@
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
@@ -24,7 +24,7 @@ namespace Quotient {
*/
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 4f77c466..828ccfb7 100644
--- a/lib/csapi/presence.cpp
+++ b/lib/csapi/presence.cpp
@@ -9,24 +9,24 @@ using namespace Quotient;
SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence,
const QString& statusMsg)
: BaseJob(HttpVerb::Put, QStringLiteral("SetPresenceJob"),
- makePath("/_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),
- makePath("/_matrix/client/r0", "/presence/",
+ makePath("/_matrix/client/v3", "/presence/",
userId, "/status"));
}
GetPresenceJob::GetPresenceJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetPresenceJob"),
- makePath("/_matrix/client/r0", "/presence/", userId, "/status"))
+ makePath("/_matrix/client/v3", "/presence/", userId, "/status"))
{
addExpectedKey("presence");
}
diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp
index 64ac84ca..f024ed82 100644
--- a/lib/csapi/profile.cpp
+++ b/lib/csapi/profile.cpp
@@ -9,56 +9,58 @@ using namespace Quotient;
SetDisplayNameJob::SetDisplayNameJob(const QString& userId,
const QString& displayname)
: BaseJob(HttpVerb::Put, QStringLiteral("SetDisplayNameJob"),
- makePath("/_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),
- makePath("/_matrix/client/r0", "/profile/",
+ makePath("/_matrix/client/v3", "/profile/",
userId, "/displayname"));
}
GetDisplayNameJob::GetDisplayNameJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetDisplayNameJob"),
- makePath("/_matrix/client/r0", "/profile/", userId, "/displayname"),
+ makePath("/_matrix/client/v3", "/profile/", userId,
+ "/displayname"),
false)
{}
SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl)
: BaseJob(HttpVerb::Put, QStringLiteral("SetAvatarUrlJob"),
- makePath("/_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),
- makePath("/_matrix/client/r0", "/profile/",
+ makePath("/_matrix/client/v3", "/profile/",
userId, "/avatar_url"));
}
GetAvatarUrlJob::GetAvatarUrlJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetAvatarUrlJob"),
- makePath("/_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),
- makePath("/_matrix/client/r0", "/profile/",
+ makePath("/_matrix/client/v3", "/profile/",
userId));
}
GetUserProfileJob::GetUserProfileJob(const QString& userId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetUserProfileJob"),
- makePath("/_matrix/client/r0", "/profile/", userId), false)
+ makePath("/_matrix/client/v3", "/profile/", userId), false)
{}
diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp
index ef4b3767..fb6595fc 100644
--- a/lib/csapi/pusher.cpp
+++ b/lib/csapi/pusher.cpp
@@ -9,12 +9,12 @@ using namespace Quotient;
QUrl GetPushersJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/pushers"));
+ makePath("/_matrix/client/v3", "/pushers"));
}
GetPushersJob::GetPushersJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushersJob"),
- makePath("/_matrix/client/r0", "/pushers"))
+ makePath("/_matrix/client/v3", "/pushers"))
{}
PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind,
@@ -23,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"),
- makePath("/_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/pushrules.cpp b/lib/csapi/pushrules.cpp
index 0d840788..2376654a 100644
--- a/lib/csapi/pushrules.cpp
+++ b/lib/csapi/pushrules.cpp
@@ -9,12 +9,12 @@ using namespace Quotient;
QUrl GetPushRulesJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(
- std::move(baseUrl), makePath("/_matrix/client/r0", "/pushrules"));
+ std::move(baseUrl), makePath("/_matrix/client/v3", "/pushrules"));
}
GetPushRulesJob::GetPushRulesJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushRulesJob"),
- makePath("/_matrix/client/r0", "/pushrules"))
+ makePath("/_matrix/client/v3", "/pushrules"))
{
addExpectedKey("global");
}
@@ -23,14 +23,14 @@ QUrl GetPushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
const QString& kind, const QString& ruleId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/pushrules/",
+ makePath("/_matrix/client/v3", "/pushrules/",
scope, "/", kind, "/", ruleId));
}
GetPushRuleJob::GetPushRuleJob(const QString& scope, const QString& kind,
const QString& ruleId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleJob"),
- makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind,
"/", ruleId))
{}
@@ -39,14 +39,14 @@ QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
const QString& ruleId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/pushrules/",
+ makePath("/_matrix/client/v3", "/pushrules/",
scope, "/", kind, "/", ruleId));
}
DeletePushRuleJob::DeletePushRuleJob(const QString& scope, const QString& kind,
const QString& ruleId)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeletePushRuleJob"),
- makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind,
"/", ruleId))
{}
@@ -65,15 +65,15 @@ SetPushRuleJob::SetPushRuleJob(const QString& scope, const QString& kind,
const QVector<PushCondition>& conditions,
const QString& pattern)
: BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleJob"),
- makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ 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,
@@ -81,7 +81,7 @@ QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
const QString& ruleId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/pushrules/",
+ makePath("/_matrix/client/v3", "/pushrules/",
scope, "/", kind, "/", ruleId,
"/enabled"));
}
@@ -90,7 +90,7 @@ IsPushRuleEnabledJob::IsPushRuleEnabledJob(const QString& scope,
const QString& kind,
const QString& ruleId)
: BaseJob(HttpVerb::Get, QStringLiteral("IsPushRuleEnabledJob"),
- makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind,
"/", ruleId, "/enabled"))
{
addExpectedKey("enabled");
@@ -100,12 +100,12 @@ SetPushRuleEnabledJob::SetPushRuleEnabledJob(const QString& scope,
const QString& kind,
const QString& ruleId, bool enabled)
: BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleEnabledJob"),
- makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ 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,
@@ -113,7 +113,7 @@ QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope,
const QString& ruleId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/pushrules/",
+ makePath("/_matrix/client/v3", "/pushrules/",
scope, "/", kind, "/", ruleId,
"/actions"));
}
@@ -122,7 +122,7 @@ GetPushRuleActionsJob::GetPushRuleActionsJob(const QString& scope,
const QString& kind,
const QString& ruleId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleActionsJob"),
- makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind,
"/", ruleId, "/actions"))
{
addExpectedKey("actions");
@@ -133,10 +133,10 @@ SetPushRuleActionsJob::SetPushRuleActionsJob(const QString& scope,
const QString& ruleId,
const QVector<QVariant>& actions)
: BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleActionsJob"),
- makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind,
+ 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/read_markers.cpp b/lib/csapi/read_markers.cpp
index f2edb71e..de5f4a9a 100644
--- a/lib/csapi/read_markers.cpp
+++ b/lib/csapi/read_markers.cpp
@@ -10,10 +10,10 @@ SetReadMarkerJob::SetReadMarkerJob(const QString& roomId,
const QString& mFullyRead,
const QString& mRead)
: BaseJob(HttpVerb::Post, QStringLiteral("SetReadMarkerJob"),
- makePath("/_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<>(_dataJson, QStringLiteral("m.fully_read"), mFullyRead);
+ addParam<IfNotEmpty>(_dataJson, QStringLiteral("m.read"), mRead);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/receipts.cpp b/lib/csapi/receipts.cpp
index 401c3bfe..0194603d 100644
--- a/lib/csapi/receipts.cpp
+++ b/lib/csapi/receipts.cpp
@@ -10,8 +10,8 @@ PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType
const QString& eventId,
const QJsonObject& receipt)
: BaseJob(HttpVerb::Post, QStringLiteral("PostReceiptJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/receipt/",
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/receipt/",
receiptType, "/", eventId))
{
- setRequestData(RequestData(toJson(receipt)));
+ setRequestData({ toJson(receipt) });
}
diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp
index acf1b0e4..154abd9b 100644
--- a/lib/csapi/redaction.cpp
+++ b/lib/csapi/redaction.cpp
@@ -9,10 +9,10 @@ using namespace Quotient;
RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId,
const QString& txnId, const QString& reason)
: BaseJob(HttpVerb::Put, QStringLiteral("RedactEventJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/redact/",
+ 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/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 153abcee..04c0fe12 100644
--- a/lib/csapi/registration.cpp
+++ b/lib/csapi/registration.cpp
@@ -18,85 +18,91 @@ 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"),
- makePath("/_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"),
- makePath("/_matrix/client/r0", "/register/email/requestToken"),
+ makePath("/_matrix/client/v3", "/register/email/requestToken"),
false)
{
- setRequestData(RequestData(toJson(body)));
+ setRequestData({ toJson(body) });
}
RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob(
const MsisdnValidationData& body)
: BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterMSISDNJob"),
- makePath("/_matrix/client/r0", "/register/msisdn/requestToken"),
+ makePath("/_matrix/client/v3", "/register/msisdn/requestToken"),
false)
{
- setRequestData(RequestData(toJson(body)));
+ setRequestData({ toJson(body) });
}
ChangePasswordJob::ChangePasswordJob(const QString& newPassword,
bool logoutDevices,
const Omittable<AuthenticationData>& auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"),
- makePath("/_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"),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/account/password/email/requestToken"),
false)
{
- setRequestData(RequestData(toJson(body)));
+ setRequestData({ toJson(body) });
}
RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob(
const MsisdnValidationData& body)
: BaseJob(HttpVerb::Post,
QStringLiteral("RequestTokenToResetPasswordMSISDNJob"),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/account/password/msisdn/requestToken"),
false)
{
- setRequestData(RequestData(toJson(body)));
+ setRequestData({ toJson(body) });
}
DeactivateAccountJob::DeactivateAccountJob(
const Omittable<AuthenticationData>& auth, const QString& idServer)
: BaseJob(HttpVerb::Post, QStringLiteral("DeactivateAccountJob"),
- makePath("/_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");
}
@@ -111,13 +117,14 @@ QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl,
const QString& username)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/register/available"),
queryToCheckUsernameAvailability(username));
}
-CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& username)
+CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(
+ const QString& username)
: BaseJob(HttpVerb::Get, QStringLiteral("CheckUsernameAvailabilityJob"),
- makePath("/_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 10375971..21d7f9d7 100644
--- a/lib/csapi/registration.h
+++ b/lib/csapi/registration.h
@@ -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
@@ -227,7 +243,8 @@ 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
@@ -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
@@ -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
@@ -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);
@@ -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
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..8bcecee4
--- /dev/null
+++ b/lib/csapi/relations.cpp
@@ -0,0 +1,111 @@
+/******************************************************************************
+ * 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)
+{
+ QUrlQuery _q;
+ addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
+ addParam<IfNotEmpty>(_q, QStringLiteral("to"), to);
+ addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
+ return _q;
+}
+
+QUrl GetRelatingEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
+ const QString& eventId,
+ const QString& from, const QString& to,
+ Omittable<int> limit)
+{
+ return BaseJob::makeRequestUrl(std::move(baseUrl),
+ makePath("/_matrix/client/v1", "/rooms/",
+ roomId, "/relations/", eventId),
+ queryToGetRelatingEvents(from, to, limit));
+}
+
+GetRelatingEventsJob::GetRelatingEventsJob(const QString& roomId,
+ const QString& eventId,
+ const QString& from,
+ const QString& to,
+ Omittable<int> limit)
+ : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsJob"),
+ makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/",
+ eventId),
+ queryToGetRelatingEvents(from, to, limit))
+{
+ addExpectedKey("chunk");
+}
+
+auto queryToGetRelatingEventsWithRelType(const QString& from, const QString& to,
+ Omittable<int> limit)
+{
+ QUrlQuery _q;
+ addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
+ addParam<IfNotEmpty>(_q, QStringLiteral("to"), to);
+ addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
+ 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)
+{
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl),
+ makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/",
+ eventId, "/", relType),
+ queryToGetRelatingEventsWithRelType(from, to, limit));
+}
+
+GetRelatingEventsWithRelTypeJob::GetRelatingEventsWithRelTypeJob(
+ const QString& roomId, const QString& eventId, const QString& relType,
+ const QString& from, const QString& to, Omittable<int> limit)
+ : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsWithRelTypeJob"),
+ makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/",
+ eventId, "/", relType),
+ queryToGetRelatingEventsWithRelType(from, to, limit))
+{
+ addExpectedKey("chunk");
+}
+
+auto queryToGetRelatingEventsWithRelTypeAndEventType(const QString& from,
+ const QString& to,
+ Omittable<int> limit)
+{
+ QUrlQuery _q;
+ addParam<IfNotEmpty>(_q, QStringLiteral("from"), from);
+ addParam<IfNotEmpty>(_q, QStringLiteral("to"), to);
+ addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit);
+ 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)
+{
+ return BaseJob::makeRequestUrl(
+ std::move(baseUrl),
+ makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/",
+ eventId, "/", relType, "/", eventType),
+ queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit));
+}
+
+GetRelatingEventsWithRelTypeAndEventTypeJob::
+ GetRelatingEventsWithRelTypeAndEventTypeJob(
+ const QString& roomId, const QString& eventId, const QString& relType,
+ const QString& eventType, const QString& from, const QString& to,
+ Omittable<int> limit)
+ : BaseJob(HttpVerb::Get,
+ QStringLiteral("GetRelatingEventsWithRelTypeAndEventTypeJob"),
+ makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/",
+ eventId, "/", relType, "/", eventType),
+ queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit))
+{
+ addExpectedKey("chunk");
+}
diff --git a/lib/csapi/relations.h b/lib/csapi/relations.h
new file mode 100644
index 00000000..985a43b5
--- /dev/null
+++ b/lib/csapi/relations.h
@@ -0,0 +1,277 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "events/eventloader.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` 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.
+ */
+ explicit GetRelatingEventsJob(const QString& roomId, const QString& eventId,
+ const QString& from = {},
+ const QString& to = {},
+ Omittable<int> limit = none);
+
+ /*! \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);
+
+ // 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` 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.
+ */
+ explicit GetRelatingEventsWithRelTypeJob(const QString& roomId,
+ const QString& eventId,
+ const QString& relType,
+ const QString& from = {},
+ const QString& to = {},
+ Omittable<int> limit = none);
+
+ /*! \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);
+
+ // 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` 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.
+ */
+ explicit GetRelatingEventsWithRelTypeAndEventTypeJob(
+ const QString& roomId, const QString& eventId, const QString& relType,
+ const QString& eventType, const QString& from = {},
+ const QString& to = {}, Omittable<int> limit = none);
+
+ /*! \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);
+
+ // 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 0a76d5b8..bc52208f 100644
--- a/lib/csapi/report_content.cpp
+++ b/lib/csapi/report_content.cpp
@@ -9,11 +9,11 @@ using namespace Quotient;
ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId,
Omittable<int> score, const QString& reason)
: BaseJob(HttpVerb::Post, QStringLiteral("ReportContentJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/report/",
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/report/",
eventId))
{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("score"), score);
- addParam<IfNotEmpty>(_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/room_send.cpp b/lib/csapi/room_send.cpp
index f80f9300..2319496f 100644
--- a/lib/csapi/room_send.cpp
+++ b/lib/csapi/room_send.cpp
@@ -9,9 +9,9 @@ using namespace Quotient;
SendMessageJob::SendMessageJob(const QString& roomId, const QString& eventType,
const QString& txnId, const QJsonObject& body)
: BaseJob(HttpVerb::Put, QStringLiteral("SendMessageJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/send/",
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/send/",
eventType, "/", txnId))
{
- setRequestData(RequestData(toJson(body)));
+ setRequestData({ toJson(body) });
addExpectedKey("event_id");
}
diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h
index fea3d59d..fcb6b24f 100644
--- a/lib/csapi/room_send.h
+++ b/lib/csapi/room_send.h
@@ -16,7 +16,8 @@ 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 QUOTIENT_API SendMessageJob : public BaseJob {
public:
diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp
index f6d2e6ec..b4adb739 100644
--- a/lib/csapi/room_state.cpp
+++ b/lib/csapi/room_state.cpp
@@ -11,9 +11,9 @@ SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId,
const QString& stateKey,
const QJsonObject& body)
: BaseJob(HttpVerb::Put, QStringLiteral("SetRoomStateWithKeyJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/",
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/state/",
eventType, "/", stateKey))
{
- setRequestData(RequestData(toJson(body)));
+ setRequestData({ toJson(body) });
addExpectedKey("event_id");
}
diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp
index d4129cfb..b03fb6e8 100644
--- a/lib/csapi/room_upgrades.cpp
+++ b/lib/csapi/room_upgrades.cpp
@@ -8,10 +8,10 @@ using namespace Quotient;
UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion)
: BaseJob(HttpVerb::Post, QStringLiteral("UpgradeRoomJob"),
- makePath("/_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/rooms.cpp b/lib/csapi/rooms.cpp
index 5310aa32..563f4fa5 100644
--- a/lib/csapi/rooms.cpp
+++ b/lib/csapi/rooms.cpp
@@ -10,14 +10,14 @@ QUrl GetOneRoomEventJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
const QString& eventId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/rooms/",
+ makePath("/_matrix/client/v3", "/rooms/",
roomId, "/event/", eventId));
}
GetOneRoomEventJob::GetOneRoomEventJob(const QString& roomId,
const QString& eventId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetOneRoomEventJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/event/",
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/event/",
eventId))
{}
@@ -26,7 +26,7 @@ QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
const QString& stateKey)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/rooms/",
+ makePath("/_matrix/client/v3", "/rooms/",
roomId, "/state/", eventType, "/",
stateKey));
}
@@ -35,20 +35,20 @@ GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId,
const QString& eventType,
const QString& stateKey)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateWithKeyJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/",
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/state/",
eventType, "/", stateKey))
{}
QUrl GetRoomStateJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/rooms/",
+ makePath("/_matrix/client/v3", "/rooms/",
roomId, "/state"));
}
GetRoomStateJob::GetRoomStateJob(const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/state"))
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/state"))
{}
auto queryToGetMembersByRoom(const QString& at, const QString& membership,
@@ -68,7 +68,7 @@ QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId,
{
return BaseJob::makeRequestUrl(
std::move(baseUrl),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"),
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/members"),
queryToGetMembersByRoom(at, membership, notMembership));
}
@@ -77,7 +77,7 @@ GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId,
const QString& membership,
const QString& notMembership)
: BaseJob(HttpVerb::Get, QStringLiteral("GetMembersByRoomJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"),
+ makePath("/_matrix/client/v3", "/rooms/", roomId, "/members"),
queryToGetMembersByRoom(at, membership, notMembership))
{}
@@ -85,12 +85,12 @@ QUrl GetJoinedMembersByRoomJob::makeRequestUrl(QUrl baseUrl,
const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/rooms/",
+ makePath("/_matrix/client/v3", "/rooms/",
roomId, "/joined_members"));
}
GetJoinedMembersByRoomJob::GetJoinedMembersByRoomJob(const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedMembersByRoomJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId,
+ makePath("/_matrix/client/v3", "/rooms/", roomId,
"/joined_members"))
{}
diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h
index f0815109..247fb13f 100644
--- a/lib/csapi/rooms.h
+++ b/lib/csapi/rooms.h
@@ -5,7 +5,6 @@
#pragma once
#include "events/eventloader.h"
-#include "events/roommemberevent.h"
#include "jobs/basejob.h"
namespace Quotient {
@@ -38,7 +37,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.
@@ -146,10 +145,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,9 +153,8 @@ 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 QUOTIENT_API GetJoinedMembersByRoomJob : public BaseJob {
public:
diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp
index 295dd1cc..4e2c9e92 100644
--- a/lib/csapi/search.cpp
+++ b/lib/csapi/search.cpp
@@ -16,11 +16,11 @@ auto queryToSearch(const QString& nextBatch)
SearchJob::SearchJob(const Categories& searchCategories,
const QString& nextBatch)
: BaseJob(HttpVerb::Post, QStringLiteral("SearchJob"),
- makePath("/_matrix/client/r0", "/search"),
+ makePath("/_matrix/client/v3", "/search"),
queryToSearch(nextBatch))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("search_categories"), searchCategories);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("search_categories"), searchCategories);
+ setRequestData({ _dataJson });
addExpectedKey("search_categories");
}
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..7a421be8
--- /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/eventloader.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 871d6ff6..71f8147c 100644
--- a/lib/csapi/sso_login_redirect.cpp
+++ b/lib/csapi/sso_login_redirect.cpp
@@ -16,14 +16,14 @@ auto queryToRedirectToSSO(const QString& redirectUrl)
QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/login/sso/redirect"),
queryToRedirectToSSO(redirectUrl));
}
RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl)
: BaseJob(HttpVerb::Get, QStringLiteral("RedirectToSSOJob"),
- makePath("/_matrix/client/r0", "/login/sso/redirect"),
+ makePath("/_matrix/client/v3", "/login/sso/redirect"),
queryToRedirectToSSO(redirectUrl), {}, false)
{}
@@ -38,7 +38,7 @@ QUrl RedirectToIdPJob::makeRequestUrl(QUrl baseUrl, const QString& idpId,
const QString& redirectUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/login/sso/redirect/", idpId),
queryToRedirectToIdP(redirectUrl));
}
@@ -46,6 +46,6 @@ QUrl RedirectToIdPJob::makeRequestUrl(QUrl baseUrl, const QString& idpId,
RedirectToIdPJob::RedirectToIdPJob(const QString& idpId,
const QString& redirectUrl)
: BaseJob(HttpVerb::Get, QStringLiteral("RedirectToIdPJob"),
- makePath("/_matrix/client/r0", "/login/sso/redirect/", idpId),
+ makePath("/_matrix/client/v3", "/login/sso/redirect/", idpId),
queryToRedirectToIdP(redirectUrl), {}, false)
{}
diff --git a/lib/csapi/tags.cpp b/lib/csapi/tags.cpp
index f717de6e..2c85842d 100644
--- a/lib/csapi/tags.cpp
+++ b/lib/csapi/tags.cpp
@@ -10,13 +10,13 @@ QUrl GetRoomTagsJob::makeRequestUrl(QUrl baseUrl, const QString& userId,
const QString& roomId)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0", "/user/",
+ makePath("/_matrix/client/v3", "/user/",
userId, "/rooms/", roomId, "/tags"));
}
GetRoomTagsJob::GetRoomTagsJob(const QString& userId, const QString& roomId)
: BaseJob(HttpVerb::Get, QStringLiteral("GetRoomTagsJob"),
- makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ makePath("/_matrix/client/v3", "/user/", userId, "/rooms/",
roomId, "/tags"))
{}
@@ -24,20 +24,20 @@ SetRoomTagJob::SetRoomTagJob(const QString& userId, const QString& roomId,
const QString& tag, Omittable<float> order,
const QVariantHash& additionalProperties)
: BaseJob(HttpVerb::Put, QStringLiteral("SetRoomTagJob"),
- makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ 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),
- makePath("/_matrix/client/r0", "/user/",
+ makePath("/_matrix/client/v3", "/user/",
userId, "/rooms/", roomId, "/tags/",
tag));
}
@@ -45,6 +45,6 @@ QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId,
DeleteRoomTagJob::DeleteRoomTagJob(const QString& userId, const QString& roomId,
const QString& tag)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomTagJob"),
- makePath("/_matrix/client/r0", "/user/", userId, "/rooms/",
+ makePath("/_matrix/client/v3", "/user/", userId, "/rooms/",
roomId, "/tags/", tag))
{}
diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp
index 4c930668..1e5870ce 100644
--- a/lib/csapi/third_party_lookup.cpp
+++ b/lib/csapi/third_party_lookup.cpp
@@ -9,26 +9,26 @@ using namespace Quotient;
QUrl GetProtocolsJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/thirdparty/protocols"));
}
GetProtocolsJob::GetProtocolsJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolsJob"),
- makePath("/_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),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/thirdparty/protocol/", protocol));
}
GetProtocolMetadataJob::GetProtocolMetadataJob(const QString& protocol)
: BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolMetadataJob"),
- makePath("/_matrix/client/r0", "/thirdparty/protocol/", protocol))
+ makePath("/_matrix/client/v3", "/thirdparty/protocol/", protocol))
{}
auto queryToQueryLocationByProtocol(const QString& searchFields)
@@ -43,7 +43,7 @@ QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl,
const QString& searchFields)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/thirdparty/location/", protocol),
queryToQueryLocationByProtocol(searchFields));
}
@@ -51,7 +51,7 @@ QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl,
QueryLocationByProtocolJob::QueryLocationByProtocolJob(
const QString& protocol, const QString& searchFields)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByProtocolJob"),
- makePath("/_matrix/client/r0", "/thirdparty/location/", protocol),
+ makePath("/_matrix/client/v3", "/thirdparty/location/", protocol),
queryToQueryLocationByProtocol(searchFields))
{}
@@ -67,7 +67,7 @@ QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl,
const QString& fields)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/thirdparty/user/", protocol),
queryToQueryUserByProtocol(fields));
}
@@ -75,7 +75,7 @@ QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl,
QueryUserByProtocolJob::QueryUserByProtocolJob(const QString& protocol,
const QString& fields)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByProtocolJob"),
- makePath("/_matrix/client/r0", "/thirdparty/user/", protocol),
+ makePath("/_matrix/client/v3", "/thirdparty/user/", protocol),
queryToQueryUserByProtocol(fields))
{}
@@ -89,14 +89,14 @@ auto queryToQueryLocationByAlias(const QString& alias)
QUrl QueryLocationByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& alias)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/thirdparty/location"),
queryToQueryLocationByAlias(alias));
}
QueryLocationByAliasJob::QueryLocationByAliasJob(const QString& alias)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByAliasJob"),
- makePath("/_matrix/client/r0", "/thirdparty/location"),
+ makePath("/_matrix/client/v3", "/thirdparty/location"),
queryToQueryLocationByAlias(alias))
{}
@@ -110,13 +110,13 @@ auto queryToQueryUserByID(const QString& userid)
QUrl QueryUserByIDJob::makeRequestUrl(QUrl baseUrl, const QString& userid)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- makePath("/_matrix/client/r0",
+ makePath("/_matrix/client/v3",
"/thirdparty/user"),
queryToQueryUserByID(userid));
}
QueryUserByIDJob::QueryUserByIDJob(const QString& userid)
: BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByIDJob"),
- makePath("/_matrix/client/r0", "/thirdparty/user"),
+ makePath("/_matrix/client/v3", "/thirdparty/user"),
queryToQueryUserByID(userid))
{}
diff --git a/lib/csapi/third_party_membership.cpp b/lib/csapi/third_party_membership.cpp
index 59275e41..3ca986c7 100644
--- a/lib/csapi/third_party_membership.cpp
+++ b/lib/csapi/third_party_membership.cpp
@@ -10,12 +10,12 @@ InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer,
const QString& idAccessToken,
const QString& medium, const QString& address)
: BaseJob(HttpVerb::Post, QStringLiteral("InviteBy3PIDJob"),
- makePath("/_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 1edb969e..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
diff --git a/lib/csapi/to_device.cpp b/lib/csapi/to_device.cpp
index 628e8314..e10fac69 100644
--- a/lib/csapi/to_device.cpp
+++ b/lib/csapi/to_device.cpp
@@ -10,10 +10,10 @@ SendToDeviceJob::SendToDeviceJob(
const QString& eventType, const QString& txnId,
const QHash<QString, QHash<QString, QJsonObject>>& messages)
: BaseJob(HttpVerb::Put, QStringLiteral("SendToDeviceJob"),
- makePath("/_matrix/client/r0", "/sendToDevice/", eventType, "/",
+ makePath("/_matrix/client/v3", "/sendToDevice/", eventType, "/",
txnId))
{
- QJsonObject _data;
- addParam<>(_data, QStringLiteral("messages"), messages);
- setRequestData(std::move(_data));
+ QJsonObject _dataJson;
+ addParam<>(_dataJson, QStringLiteral("messages"), messages);
+ setRequestData({ _dataJson });
}
diff --git a/lib/csapi/typing.cpp b/lib/csapi/typing.cpp
index c9673118..21bd45ae 100644
--- a/lib/csapi/typing.cpp
+++ b/lib/csapi/typing.cpp
@@ -9,11 +9,11 @@ using namespace Quotient;
SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId,
bool typing, Omittable<int> timeout)
: BaseJob(HttpVerb::Put, QStringLiteral("SetTypingJob"),
- makePath("/_matrix/client/r0", "/rooms/", roomId, "/typing/",
+ 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/users.cpp b/lib/csapi/users.cpp
index 48b727f0..c65280ee 100644
--- a/lib/csapi/users.cpp
+++ b/lib/csapi/users.cpp
@@ -9,12 +9,12 @@ using namespace Quotient;
SearchUserDirectoryJob::SearchUserDirectoryJob(const QString& searchTerm,
Omittable<int> limit)
: BaseJob(HttpVerb::Post, QStringLiteral("SearchUserDirectoryJob"),
- makePath("/_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/versions.h b/lib/csapi/versions.h
index 4445dbd2..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
diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp
index c748ad94..1e1f2441 100644
--- a/lib/csapi/voip.cpp
+++ b/lib/csapi/voip.cpp
@@ -9,10 +9,10 @@ using namespace Quotient;
QUrl GetTurnServerJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(
- std::move(baseUrl), makePath("/_matrix/client/r0", "/voip/turnServer"));
+ std::move(baseUrl), makePath("/_matrix/client/v3", "/voip/turnServer"));
}
GetTurnServerJob::GetTurnServerJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetTurnServerJob"),
- makePath("/_matrix/client/r0", "/voip/turnServer"))
+ makePath("/_matrix/client/v3", "/voip/turnServer"))
{}
diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp
index ed8a9817..af0c5d31 100644
--- a/lib/csapi/whoami.cpp
+++ b/lib/csapi/whoami.cpp
@@ -9,12 +9,12 @@ using namespace Quotient;
QUrl GetTokenOwnerJob::makeRequestUrl(QUrl baseUrl)
{
return BaseJob::makeRequestUrl(
- std::move(baseUrl), makePath("/_matrix/client/r0", "/account/whoami"));
+ std::move(baseUrl), makePath("/_matrix/client/v3", "/account/whoami"));
}
GetTokenOwnerJob::GetTokenOwnerJob()
: BaseJob(HttpVerb::Get, QStringLiteral("GetTokenOwnerJob"),
- makePath("/_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 fba099f6..3451dbc3 100644
--- a/lib/csapi/whoami.h
+++ b/lib/csapi/whoami.h
@@ -41,6 +41,14 @@ public:
/// 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
index a85d96bb..79793b9d 100644
--- a/lib/database.cpp
+++ b/lib/database.cpp
@@ -14,9 +14,7 @@
#include "e2ee/e2ee.h"
#include "e2ee/qolmsession.h"
#include "e2ee/qolminboundsession.h"
-#include "connection.h"
-#include "user.h"
-#include "room.h"
+#include "e2ee/qolmoutboundsession.h"
using namespace Quotient;
Database::Database(const QString& matrixId, const QString& deviceId, QObject* parent)
@@ -31,11 +29,11 @@ Database::Database(const QString& matrixId, const QString& deviceId, QObject* pa
database().open();
switch(version()) {
- case 0: migrateTo1();
- case 1: migrateTo2();
- case 2: migrateTo3();
- case 3: migrateTo4();
- case 4: migrateTo5();
+ case 0: migrateTo1(); [[fallthrough]];
+ case 1: migrateTo2(); [[fallthrough]];
+ case 2: migrateTo3(); [[fallthrough]];
+ case 3: migrateTo4(); [[fallthrough]];
+ case 4: migrateTo5();
}
}
@@ -43,7 +41,7 @@ int Database::version()
{
auto query = execute(QStringLiteral("PRAGMA user_version;"));
if (query.next()) {
- bool ok;
+ bool ok = false;
int value = query.value(0).toInt(&ok);
qCDebug(DATABASE) << "Database version" << value;
if (ok)
@@ -212,12 +210,14 @@ UnorderedMap<QString, std::vector<QOlmSessionPtr>> Database::loadOlmSessions(con
commit();
UnorderedMap<QString, std::vector<QOlmSessionPtr>> sessions;
while (query.next()) {
- auto session = QOlmSession::unpickle(query.value("pickle").toByteArray(), picklingMode);
- if (std::holds_alternative<QOlmError>(session)) {
- qCWarning(E2EE) << "Failed to unpickle olm session";
- continue;
- }
- sessions[query.value("senderKey").toString()].push_back(std::move(std::get<QOlmSessionPtr>(session)));
+ 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;
}
@@ -231,15 +231,15 @@ UnorderedMap<QString, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(c
commit();
UnorderedMap<QString, QOlmInboundGroupSessionPtr> sessions;
while (query.next()) {
- auto session = QOlmInboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode);
- if (std::holds_alternative<QOlmError>(session)) {
- qCWarning(E2EE) << "Failed to unpickle megolm session";
- continue;
- }
-
- sessions[query.value("sessionId").toString()] = std::move(std::get<QOlmInboundGroupSessionPtr>(session));
- sessions[query.value("sessionId").toString()]->setOlmSessionId(query.value("olmSessionId").toString());
- sessions[query.value("sessionId").toString()]->setSenderId(query.value("senderId").toString());
+ 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;
}
@@ -319,20 +319,22 @@ void Database::setOlmSessionLastReceived(const QString& sessionId, const QDateTi
commit();
}
-void Database::saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& session)
+void Database::saveCurrentOutboundMegolmSession(
+ const QString& roomId, const PicklingMode& picklingMode,
+ const QOlmOutboundGroupSession& session)
{
- const auto pickle = session->pickle(picklingMode);
- if (std::holds_alternative<QByteArray>(pickle)) {
+ const auto pickle = session.pickle(picklingMode);
+ if (pickle) {
auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;"));
deleteQuery.bindValue(":roomId", roomId);
- deleteQuery.bindValue(":sessionId", session->sessionId());
+ 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", std::get<QByteArray>(pickle));
- insertQuery.bindValue(":creationTime", session->creationTime());
- insertQuery.bindValue(":messageCount", session->messageCount());
+ insertQuery.bindValue(":sessionId", session.sessionId());
+ insertQuery.bindValue(":pickle", pickle.value());
+ insertQuery.bindValue(":creationTime", session.creationTime());
+ insertQuery.bindValue(":messageCount", session.messageCount());
transaction();
execute(deleteQuery);
@@ -348,8 +350,8 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt
execute(query);
if (query.next()) {
auto sessionResult = QOlmOutboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode);
- if (std::holds_alternative<QOlmOutboundGroupSessionPtr>(sessionResult)) {
- auto session = std::move(std::get<QOlmOutboundGroupSessionPtr>(sessionResult));
+ if (sessionResult) {
+ auto session = std::move(*sessionResult);
session->setCreationTime(query.value("creationTime").toDateTime());
session->setMessageCount(query.value("messageCount").toInt());
return session;
@@ -358,41 +360,35 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt
return nullptr;
}
-void Database::setDevicesReceivedKey(const QString& roomId, QHash<User *, QStringList> devices, const QString& sessionId, int index)
+void Database::setDevicesReceivedKey(const QString& roomId, const QVector<std::tuple<QString, QString, QString>>& devices, const QString& sessionId, int index)
{
- auto connection = dynamic_cast<Connection *>(parent());
transaction();
- for (const auto& user : devices.keys()) {
- for (const auto& device : devices[user]) {
- 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->id());
- query.bindValue(":deviceId", device);
- query.bindValue(":identityKey", connection->curveKeyForUserDevice(user->id(), device));
- query.bindValue(":sessionId", sessionId);
- query.bindValue(":i", index);
- execute(query);
- }
+ 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();
}
-QHash<QString, QStringList> Database::devicesWithoutKey(Room* room, const QString &sessionId)
+QMultiHash<QString, QString> Database::devicesWithoutKey(
+ const QString& roomId, QMultiHash<QString, QString> devices,
+ const QString& sessionId)
{
- auto connection = dynamic_cast<Connection *>(parent());
- QHash<QString, QStringList> devices;
- for (const auto& user : room->users()) {
- devices[user->id()] = connection->devicesForUser(user);
- }
-
auto query = prepareQuery(QStringLiteral("SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId"));
- query.bindValue(":roomId", room->id());
+ query.bindValue(":roomId", roomId);
query.bindValue(":sessionId", sessionId);
transaction();
execute(query);
commit();
while (query.next()) {
- devices[query.value("userId").toString()].removeAll(query.value("deviceId").toString());
+ devices.remove(query.value("userId").toString(),
+ query.value("deviceId").toString());
}
return devices;
}
diff --git a/lib/database.h b/lib/database.h
index afc41e42..8a133f8e 100644
--- a/lib/database.h
+++ b/lib/database.h
@@ -11,11 +11,7 @@
#include "e2ee/e2ee.h"
-#include "e2ee/qolmoutboundsession.h"
-
namespace Quotient {
-class User;
-class Room;
class QUOTIENT_API Database : public QObject
{
@@ -34,21 +30,41 @@ public:
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 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 QOlmOutboundGroupSessionPtr& data);
- void updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle);
+ 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 User -> [Device] that have not received key yet
- QHash<QString, QStringList> devicesWithoutKey(Room* room, const QString &sessionId);
- void setDevicesReceivedKey(const QString& roomId, QHash<User *, QStringList> devices, const QString& sessionId, int index);
+ // 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);
@@ -62,4 +78,4 @@ private:
QString m_matrixId;
};
-}
+} // namespace Quotient
diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h
index 268cb525..0772b70a 100644
--- a/lib/e2ee/e2ee.h
+++ b/lib/e2ee/e2ee.h
@@ -6,9 +6,13 @@
#pragma once
#include "converters.h"
-#include "quotient_common.h"
+#include "expected.h"
+#include "qolmerrors.h"
#include <QtCore/QMetaType>
+#include <QtCore/QStringBuilder>
+
+#include <array>
#include <variant>
namespace Quotient {
@@ -33,10 +37,11 @@ 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)
{
- static constexpr auto SupportedAlgorithms =
- make_array(OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey);
return std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(),
algorithm)
!= SupportedAlgorithms.cend();
@@ -55,6 +60,12 @@ 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, QOlmError>;
+
struct IdentityKeys
{
QByteArray curve25519;
@@ -62,45 +73,66 @@ struct IdentityKeys
};
//! Struct representing the one-time keys.
-struct QUOTIENT_API OneTimeKeys
+struct UnsignedOneTimeKeys
{
QHash<QString, QHash<QString, QString>> keys;
//! Get the HashMap containing the curve25519 one-time keys.
- QHash<QString, QString> curve25519() const;
-
- //! Get a reference to the hashmap corresponding to given key type.
-// std::optional<QHash<QString, QString>> get(QString keyType) const;
+ QHash<QString, QString> curve25519() const { return keys[Curve25519Key]; }
};
-//! Struct representing the signed one-time keys.
-class SignedOneTimeKey
-{
+class SignedOneTimeKey {
public:
- //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key.
- QString key;
+ 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)
+ {}
- //! Required. Signatures of the key object.
- //! The signature is calculated using the process described at Signing JSON.
- QHash<QString, QHash<QString, QString>> signatures;
-};
+ //! Unpadded Base64-encoded 32-byte Curve25519 public key
+ QString key() const { return payload["key"_ls].toString(); }
+ //! \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]);
+ }
-template <>
-struct JsonObjectConverter<SignedOneTimeKey> {
- static void fillFrom(const QJsonObject& jo, SignedOneTimeKey& result)
+ QByteArray signature(QStringView userId, QStringView deviceId) const
{
- fromJson(jo.value("key"_ls), result.key);
- fromJson(jo.value("signatures"_ls), result.signatures);
+ return payload["signatures"_ls][userId]["ed25519:"_ls % deviceId]
+ .toString()
+ .toLatin1();
}
- static void dumpTo(QJsonObject &jo, const SignedOneTimeKey &result)
+ //! Whether the key is a fallback key
+ bool isFallback() const { return payload["fallback"_ls].toBool(); }
+ auto toJson() const { return payload; }
+ auto toJsonForVerification() const
{
- addParam<>(jo, QStringLiteral("key"), result.key);
- addParam<>(jo, QStringLiteral("signatures"), result.signatures);
+ 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>>;
+
template <typename T>
class asKeyValueRange
{
diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp
index 476a60bd..ccb191f4 100644
--- a/lib/e2ee/qolmaccount.cpp
+++ b/lib/e2ee/qolmaccount.cpp
@@ -5,6 +5,7 @@
#include "qolmaccount.h"
#include "connection.h"
+#include "e2ee/qolmsession.h"
#include "e2ee/qolmutility.h"
#include "e2ee/qolmutils.h"
@@ -12,20 +13,9 @@
#include <QtCore/QRandomGenerator>
-using namespace Quotient;
-
-QHash<QString, QString> OneTimeKeys::curve25519() const
-{
- return keys[Curve25519Key];
-}
+#include <olm/olm.h>
-//std::optional<QHash<QString, QString>> OneTimeKeys::get(QString keyType) const
-//{
-// if (!keys.contains(keyType)) {
-// return std::nullopt;
-// }
-// return keys[keyType];
-//}
+using namespace Quotient;
// Convert olm error to enum
QOlmError lastError(OlmAccount *account) {
@@ -70,7 +60,7 @@ void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode)
}
}
-std::variant<QByteArray, QOlmError> QOlmAccount::pickle(const PicklingMode &mode)
+QOlmExpected<QByteArray> QOlmAccount::pickle(const PicklingMode &mode)
{
const QByteArray key = toKey(mode);
const size_t pickleLength = olm_pickle_account_length(m_account);
@@ -119,20 +109,15 @@ QByteArray QOlmAccount::sign(const QJsonObject &message) const
QByteArray QOlmAccount::signIdentityKeys() const
{
const auto keys = identityKeys();
- QJsonObject body
- {
- {"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)}
- }
- }
- };
- return sign(QJsonDocument(body).toJson(QJsonDocument::Compact));
-
+ 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
@@ -140,11 +125,15 @@ size_t QOlmAccount::maxNumberOfOneTimeKeys() const
return olm_account_max_number_of_one_time_keys(m_account);
}
-size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const
+size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys)
{
- const size_t randomLength = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys);
+ const size_t randomLength =
+ olm_account_generate_one_time_keys_random_length(m_account,
+ numberOfKeys);
QByteArray randomBuffer = getRandom(randomLength);
- const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLength);
+ const auto error =
+ olm_account_generate_one_time_keys(m_account, numberOfKeys,
+ randomBuffer.data(), randomLength);
if (error == olm_error()) {
throw lastError(m_account);
@@ -153,49 +142,39 @@ size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const
return error;
}
-OneTimeKeys QOlmAccount::oneTimeKeys() const
+UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const
{
const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account);
- QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0');
+ QByteArray oneTimeKeysBuffer(static_cast<int>(oneTimeKeyLength), '0');
- const auto error = olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength);
+ const auto error = olm_account_one_time_keys(m_account,
+ oneTimeKeysBuffer.data(),
+ oneTimeKeyLength);
if (error == olm_error()) {
throw lastError(m_account);
}
const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object();
- OneTimeKeys oneTimeKeys;
+ UnsignedOneTimeKeys oneTimeKeys;
fromJson(json, oneTimeKeys.keys);
return oneTimeKeys;
}
-QHash<QString, SignedOneTimeKey> QOlmAccount::signOneTimeKeys(const OneTimeKeys &keys) const
+OneTimeKeys QOlmAccount::signOneTimeKeys(const UnsignedOneTimeKeys &keys) const
{
- QHash<QString, SignedOneTimeKey> signedOneTimeKeys;
- for (const auto &keyid : keys.curve25519().keys()) {
- const auto oneTimeKey = keys.curve25519()[keyid];
- QByteArray sign = signOneTimeKey(oneTimeKey);
- signedOneTimeKeys["signed_curve25519:" + keyid] = signedOneTimeKey(oneTimeKey.toUtf8(), sign);
- }
+ 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;
}
-SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray &key, const QString &signature) const
-{
- SignedOneTimeKey sign{};
- sign.key = key;
- sign.signatures = {{m_userId, {{"ed25519:" + m_deviceId, signature}}}};
- return sign;
-}
-
-QByteArray QOlmAccount::signOneTimeKey(const QString &key) const
+std::optional<QOlmError> QOlmAccount::removeOneTimeKeys(
+ const QOlmSession& session)
{
- QJsonDocument j(QJsonObject{{"key", key}});
- return sign(j.toJson(QJsonDocument::Compact));
-}
-
-std::optional<QOlmError> QOlmAccount::removeOneTimeKeys(const QOlmSessionPtr &session) const
-{
- const auto error = olm_remove_one_time_keys(m_account, session->raw());
+ const auto error = olm_remove_one_time_keys(m_account, session.raw());
if (error == olm_error()) {
return lastError(m_account);
@@ -208,54 +187,47 @@ OlmAccount* QOlmAccount::data() { return m_account; }
DeviceKeys QOlmAccount::deviceKeys() const
{
- DeviceKeys deviceKeys;
- deviceKeys.userId = m_userId;
- deviceKeys.deviceId = m_deviceId;
- deviceKeys.algorithms = QStringList {"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"};
+ static QStringList Algorithms(SupportedAlgorithms.cbegin(),
+ SupportedAlgorithms.cend());
const auto idKeys = identityKeys();
- deviceKeys.keys["curve25519:" + m_deviceId] = idKeys.curve25519;
- deviceKeys.keys["ed25519:" + m_deviceId] = idKeys.ed25519;
-
- const auto sign = signIdentityKeys();
- deviceKeys.signatures[m_userId]["ed25519:" + m_deviceId] = sign;
-
- return deviceKeys;
+ 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 OneTimeKeys &oneTimeKeys)
+UploadKeysJob* QOlmAccount::createUploadKeyRequest(
+ const UnsignedOneTimeKeys& oneTimeKeys) const
{
- auto keys = deviceKeys();
-
- if (oneTimeKeys.curve25519().isEmpty()) {
- return new UploadKeysJob(keys);
- }
-
- // Sign & append the one time keys.
- auto temp = signOneTimeKeys(oneTimeKeys);
- QHash<QString, QVariant> oneTimeKeysSigned;
- for (const auto &[keyId, key] : asKeyValueRange(temp)) {
- oneTimeKeysSigned[keyId] = QVariant::fromValue(toJson(key));
- }
-
- return new UploadKeysJob(keys, oneTimeKeysSigned);
+ return new UploadKeysJob(deviceKeys(), signOneTimeKeys(oneTimeKeys));
}
-std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage)
+QOlmExpected<QOlmSessionPtr> QOlmAccount::createInboundSession(
+ const QOlmMessage& preKeyMessage)
{
Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey);
return QOlmSession::createInboundSession(this, preKeyMessage);
}
-std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage)
+QOlmExpected<QOlmSessionPtr> QOlmAccount::createInboundSessionFrom(
+ const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage)
{
Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey);
- return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage);
+ return QOlmSession::createInboundSessionFrom(this, theirIdentityKey,
+ preKeyMessage);
}
-std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey)
+QOlmExpected<QOlmSessionPtr> QOlmAccount::createOutboundSession(
+ const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey)
{
- return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey);
+ return QOlmSession::createOutboundSession(this, theirIdentityKey,
+ theirOneTimeKey);
}
void QOlmAccount::markKeysAsPublished()
@@ -292,10 +264,6 @@ bool Quotient::ed25519VerifySignature(const QString& signingKey,
QByteArray signingKeyBuf = signingKey.toUtf8();
QOlmUtility utility;
auto signatureBuf = signature.toUtf8();
- auto result = utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf);
- if (std::holds_alternative<QOlmError>(result)) {
- return false;
- }
-
- return std::get<bool>(result);
+ return utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf)
+ .value_or(false);
}
diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h
index 17f43f1a..f2a31314 100644
--- a/lib/e2ee/qolmaccount.h
+++ b/lib/e2ee/qolmaccount.h
@@ -5,21 +5,16 @@
#pragma once
-#include "csapi/keys.h"
#include "e2ee/e2ee.h"
-#include "e2ee/qolmerrors.h"
#include "e2ee/qolmmessage.h"
-#include "e2ee/qolmsession.h"
-#include <QObject>
-struct OlmAccount;
+#include "csapi/keys.h"
-namespace Quotient {
+#include <QtCore/QObject>
-class QOlmSession;
-class Connection;
+struct OlmAccount;
-using QOlmSessionPtr = std::unique_ptr<QOlmSession>;
+namespace Quotient {
//! An olm account manages all cryptographic keys used on a device.
//! \code{.cpp}
@@ -30,7 +25,7 @@ class QUOTIENT_API QOlmAccount : public QObject
Q_OBJECT
public:
QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent = nullptr);
- ~QOlmAccount();
+ ~QOlmAccount() override;
//! Creates a new instance of OlmAccount. During the instantiation
//! the Ed25519 fingerprint key pair and the Curve25519 identity key
@@ -44,7 +39,7 @@ public:
void unpickle(QByteArray &pickled, const PicklingMode &mode);
//! Serialises an OlmAccount to encrypted Base64.
- std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode);
+ QOlmExpected<QByteArray> pickle(const PicklingMode &mode);
//! Returns the account's public identity keys already formatted as JSON
IdentityKeys identityKeys() const;
@@ -61,40 +56,39 @@ public:
size_t maxNumberOfOneTimeKeys() const;
//! Generates the supplied number of one time keys.
- size_t generateOneTimeKeys(size_t numberOfKeys) const;
+ size_t generateOneTimeKeys(size_t numberOfKeys);
//! Gets the OlmAccount's one time keys formatted as JSON.
- OneTimeKeys oneTimeKeys() const;
+ UnsignedOneTimeKeys oneTimeKeys() const;
//! Sign all one time keys.
- QHash<QString, SignedOneTimeKey> signOneTimeKeys(const OneTimeKeys &keys) const;
-
- //! Sign one time key.
- QByteArray signOneTimeKey(const QString &key) const;
-
- SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const;
+ OneTimeKeys signOneTimeKeys(const UnsignedOneTimeKeys &keys) const;
- UploadKeysJob *createUploadKeyRequest(const OneTimeKeys &oneTimeKeys);
+ UploadKeysJob* createUploadKeyRequest(const UnsignedOneTimeKeys& oneTimeKeys) const;
DeviceKeys deviceKeys() const;
//! Remove the one time key used to create the supplied session.
- [[nodiscard]] std::optional<QOlmError> removeOneTimeKeys(const QOlmSessionPtr &session) const;
+ [[nodiscard]] std::optional<QOlmError> removeOneTimeKeys(
+ const QOlmSession& session);
//! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
//!
//! \param message An Olm pre-key message that was encrypted for this account.
- std::variant<QOlmSessionPtr, QOlmError> createInboundSession(const QOlmMessage &preKeyMessage);
+ 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.
- std::variant<QOlmSessionPtr, QOlmError> createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage);
+ QOlmExpected<QOlmSessionPtr> createInboundSessionFrom(
+ const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage);
//! Creates an outbound session for sending messages to a specific
/// identity and one time key.
- std::variant<QOlmSessionPtr, QOlmError> createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey);
+ QOlmExpected<QOlmSessionPtr> createOutboundSession(
+ const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey);
void markKeysAsPublished();
@@ -103,7 +97,7 @@ public:
OlmAccount *data();
Q_SIGNALS:
- void needsSave() const;
+ void needsSave();
private:
OlmAccount *m_account = nullptr; // owning
diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp
index 60d871ef..17f06205 100644
--- a/lib/e2ee/qolminboundsession.cpp
+++ b/lib/e2ee/qolminboundsession.cpp
@@ -70,7 +70,8 @@ QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const
return pickledBuf;
}
-std::variant<std::unique_ptr<QOlmInboundGroupSession>, QOlmError> QOlmInboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode)
+QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::unpickle(
+ const QByteArray& pickled, const PicklingMode& mode)
{
QByteArray pickledBuf = pickled;
const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]);
@@ -85,7 +86,8 @@ std::variant<std::unique_ptr<QOlmInboundGroupSession>, QOlmError> QOlmInboundGro
return std::make_unique<QOlmInboundGroupSession>(groupSession);
}
-std::variant<std::pair<QString, uint32_t>, QOlmError> QOlmInboundGroupSession::decrypt(const QByteArray &message)
+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;
@@ -114,10 +116,10 @@ std::variant<std::pair<QString, uint32_t>, QOlmError> QOlmInboundGroupSession::d
QByteArray output(plaintextLen, '0');
std::memcpy(output.data(), plaintextBuf.data(), plaintextLen);
- return std::make_pair<QString, qint32>(QString(output), messageIndex);
+ return std::make_pair(output, messageIndex);
}
-std::variant<QByteArray, QOlmError> QOlmInboundGroupSession::exportSession(uint32_t messageIndex)
+QOlmExpected<QByteArray> QOlmInboundGroupSession::exportSession(uint32_t messageIndex)
{
const auto keyLength = olm_export_inbound_group_session_length(m_groupSession);
QByteArray keyBuf(keyLength, '0');
@@ -154,9 +156,9 @@ QString QOlmInboundGroupSession::olmSessionId() const
{
return m_olmSessionId;
}
-void QOlmInboundGroupSession::setOlmSessionId(const QString& olmSessionId)
+void QOlmInboundGroupSession::setOlmSessionId(const QString& newOlmSessionId)
{
- m_olmSessionId = olmSessionId;
+ m_olmSessionId = newOlmSessionId;
}
QString QOlmInboundGroupSession::senderId() const
diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h
index 32112b97..1a9b4415 100644
--- a/lib/e2ee/qolminboundsession.h
+++ b/lib/e2ee/qolminboundsession.h
@@ -5,11 +5,8 @@
#pragma once
#include "e2ee/e2ee.h"
-#include "e2ee/qolmerrors.h"
-#include "olm/olm.h"
-#include <memory>
-#include <variant>
+#include <olm/olm.h>
namespace Quotient {
@@ -27,14 +24,13 @@ public:
QByteArray pickle(const PicklingMode &mode) const;
//! Deserialises from encrypted Base64 that was previously obtained by pickling
//! an `OlmInboundGroupSession`.
- static std::variant<std::unique_ptr<QOlmInboundGroupSession>, QOlmError>
- unpickle(const QByteArray& picked, const PicklingMode& mode);
+ static QOlmExpected<QOlmInboundGroupSessionPtr> unpickle(
+ const QByteArray& pickled, const PicklingMode& mode);
//! Decrypts ciphertext received for this group session.
- std::variant<std::pair<QString, uint32_t>, QOlmError> decrypt(
- const QByteArray& message);
+ 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.
- std::variant<QByteArray, QOlmError> exportSession(uint32_t messageIndex);
+ 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.
@@ -44,7 +40,7 @@ public:
//! 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& setOlmSessionId);
+ void setOlmSessionId(const QString& newOlmSessionId);
//! The sender of this session.
QString senderId() const;
diff --git a/lib/e2ee/qolmmessage.cpp b/lib/e2ee/qolmmessage.cpp
index 81b166b0..f9b4a5c2 100644
--- a/lib/e2ee/qolmmessage.cpp
+++ b/lib/e2ee/qolmmessage.cpp
@@ -4,6 +4,8 @@
#include "qolmmessage.h"
+#include "util.h"
+
using namespace Quotient;
QOlmMessage::QOlmMessage(QByteArray ciphertext, QOlmMessage::Type type)
@@ -26,7 +28,7 @@ QOlmMessage::Type QOlmMessage::type() const
QByteArray QOlmMessage::toCiphertext() const
{
- return QByteArray(*this);
+ return SLICE(*this, QByteArray);
}
QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext)
diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp
index 8852bcf3..a2eff2c8 100644
--- a/lib/e2ee/qolmoutboundsession.cpp
+++ b/lib/e2ee/qolmoutboundsession.cpp
@@ -13,8 +13,7 @@ QOlmError lastError(OlmOutboundGroupSession *session) {
QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session)
: m_groupSession(session)
-{
-}
+{}
QOlmOutboundGroupSession::~QOlmOutboundGroupSession()
{
@@ -22,7 +21,7 @@ QOlmOutboundGroupSession::~QOlmOutboundGroupSession()
delete[](reinterpret_cast<uint8_t *>(m_groupSession));
}
-std::unique_ptr<QOlmOutboundGroupSession> QOlmOutboundGroupSession::create()
+QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create()
{
auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]);
const auto randomLength = olm_init_outbound_group_session_random_length(olmOutboundGroupSession);
@@ -45,7 +44,7 @@ std::unique_ptr<QOlmOutboundGroupSession> QOlmOutboundGroupSession::create()
return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession);
}
-std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::pickle(const PicklingMode &mode)
+QOlmExpected<QByteArray> QOlmOutboundGroupSession::pickle(const PicklingMode &mode) const
{
QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0');
QByteArray key = toKey(mode);
@@ -61,7 +60,7 @@ std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::pickle(const Pickl
return pickledBuf;
}
-std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError> QOlmOutboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode)
+QOlmExpected<QOlmOutboundGroupSessionPtr> QOlmOutboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode)
{
QByteArray pickledBuf = pickled;
auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]);
@@ -80,7 +79,7 @@ std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError> QOlmOutboundG
return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession);
}
-std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::encrypt(const QString &plaintext)
+QOlmExpected<QByteArray> QOlmOutboundGroupSession::encrypt(const QString &plaintext) const
{
QByteArray plaintextBuf = plaintext.toUtf8();
const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length());
@@ -112,12 +111,13 @@ QByteArray QOlmOutboundGroupSession::sessionId() const
return idBuffer;
}
-std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::sessionKey() const
+QOlmExpected<QByteArray> QOlmOutboundGroupSession::sessionKey() const
{
const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession);
QByteArray keyBuffer(keyMaxLength, '0');
- const auto error = olm_outbound_group_session_key(m_groupSession, reinterpret_cast<uint8_t *>(keyBuffer.data()),
- keyMaxLength);
+ const auto error = olm_outbound_group_session_key(
+ m_groupSession, reinterpret_cast<uint8_t*>(keyBuffer.data()),
+ keyMaxLength);
if (error == olm_error()) {
return lastError(m_groupSession);
}
diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h
index 10ca35c0..9a82d22a 100644
--- a/lib/e2ee/qolmoutboundsession.h
+++ b/lib/e2ee/qolmoutboundsession.h
@@ -4,10 +4,10 @@
#pragma once
-#include "olm/olm.h"
-#include "e2ee/qolmerrors.h"
#include "e2ee/e2ee.h"
+
#include <memory>
+#include <olm/olm.h>
namespace Quotient {
@@ -19,15 +19,16 @@ public:
~QOlmOutboundGroupSession();
//! Creates a new instance of `QOlmOutboundGroupSession`.
//! Throw OlmError on errors
- static std::unique_ptr<QOlmOutboundGroupSession> create();
+ static QOlmOutboundGroupSessionPtr create();
//! Serialises a `QOlmOutboundGroupSession` to encrypted Base64.
- std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode);
+ QOlmExpected<QByteArray> pickle(const PicklingMode &mode) const;
//! Deserialises from encrypted Base64 that was previously obtained by
//! pickling a `QOlmOutboundGroupSession`.
- static std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError>
- unpickle(const QByteArray& pickled, const PicklingMode& mode);
+ static QOlmExpected<QOlmOutboundGroupSessionPtr> unpickle(
+ const QByteArray& pickled, const PicklingMode& mode);
+
//! Encrypts a plaintext message using the session.
- std::variant<QByteArray, QOlmError> encrypt(const QString &plaintext);
+ QOlmExpected<QByteArray> encrypt(const QString& plaintext) const;
//! Get the current message index for this session.
//!
@@ -42,7 +43,7 @@ public:
//!
//! Each message is sent with a different ratchet key. This function returns the
//! ratchet key that will be used for the next message.
- std::variant<QByteArray, QOlmError> sessionKey() const;
+ QOlmExpected<QByteArray> sessionKey() const;
QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession);
int messageCount() const;
@@ -56,5 +57,4 @@ private:
QDateTime m_creationTime = QDateTime::currentDateTime();
};
-using QOlmOutboundGroupSessionPtr = std::unique_ptr<QOlmOutboundGroupSession>;
-}
+} // namespace Quotient
diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp
index e575ff39..2a98d5d8 100644
--- a/lib/e2ee/qolmsession.cpp
+++ b/lib/e2ee/qolmsession.cpp
@@ -3,10 +3,12 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "qolmsession.h"
+
#include "e2ee/qolmutils.h"
#include "logging.h"
+
#include <cstring>
-#include <QDebug>
+#include <olm/olm.h>
using namespace Quotient;
@@ -25,7 +27,9 @@ OlmSession* QOlmSession::create()
return olm_session(new uint8_t[olm_session_size()]);
}
-std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInbound(QOlmAccount *account, const QOlmMessage &preKeyMessage, bool from, const QString &theirIdentityKey)
+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 in when creating inbound session" << BadMessageFormat;
@@ -51,17 +55,22 @@ std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInbound(QOlmAccount *
return std::make_unique<QOlmSession>(olmSession);
}
-std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage)
+QOlmExpected<QOlmSessionPtr> QOlmSession::createInboundSession(
+ QOlmAccount* account, const QOlmMessage& preKeyMessage)
{
return createInbound(account, preKeyMessage);
}
-std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage)
+QOlmExpected<QOlmSessionPtr> QOlmSession::createInboundSessionFrom(
+ QOlmAccount* account, const QString& theirIdentityKey,
+ const QOlmMessage& preKeyMessage)
{
return createInbound(account, preKeyMessage, true, theirIdentityKey);
}
-std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey)
+QOlmExpected<QOlmSessionPtr> QOlmSession::createOutboundSession(
+ QOlmAccount* account, const QString& theirIdentityKey,
+ const QString& theirOneTimeKey)
{
auto *olmOutboundSession = create();
const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession);
@@ -87,12 +96,13 @@ std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createOutboundSession(QOlmA
return std::make_unique<QOlmSession>(olmOutboundSession);
}
-std::variant<QByteArray, QOlmError> QOlmSession::pickle(const PicklingMode &mode)
+QOlmExpected<QByteArray> QOlmSession::pickle(const PicklingMode &mode) const
{
QByteArray pickledBuf(olm_pickle_session_length(m_session), '0');
QByteArray key = toKey(mode);
const auto error = olm_pickle_session(m_session, key.data(), key.length(),
- pickledBuf.data(), pickledBuf.length());
+ pickledBuf.data(),
+ pickledBuf.length());
if (error == olm_error()) {
return lastError(m_session);
@@ -103,7 +113,8 @@ std::variant<QByteArray, QOlmError> QOlmSession::pickle(const PicklingMode &mode
return pickledBuf;
}
-std::variant<QOlmSessionPtr, QOlmError> QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode)
+QOlmExpected<QOlmSessionPtr> QOlmSession::unpickle(const QByteArray& pickled,
+ const PicklingMode& mode)
{
QByteArray pickledBuf = pickled;
auto *olmSession = create();
@@ -138,7 +149,7 @@ QOlmMessage QOlmSession::encrypt(const QString &plaintext)
return QOlmMessage(messageBuf, messageType);
}
-std::variant<QString, QOlmError> QOlmSession::decrypt(const QOlmMessage &message) const
+QOlmExpected<QByteArray> QOlmSession::decrypt(const QOlmMessage &message) const
{
const auto messageType = message.type();
const auto ciphertext = message.toCiphertext();
@@ -207,45 +218,35 @@ bool QOlmSession::hasReceivedMessage() const
return olm_session_has_received_message(m_session);
}
-std::variant<bool, QOlmError> QOlmSession::matchesInboundSession(const QOlmMessage &preKeyMessage) const
+bool QOlmSession::matchesInboundSession(const QOlmMessage& preKeyMessage) const
{
Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey);
QByteArray oneTimeKeyBuf(preKeyMessage.data());
- const auto matchesResult = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length());
+ const auto maybeMatches =
+ olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(),
+ oneTimeKeyBuf.length());
- if (matchesResult == olm_error()) {
+ if (maybeMatches == olm_error()) {
return lastError(m_session);
}
- switch (matchesResult) {
- case 0:
- return false;
- case 1:
- return true;
- default:
- return QOlmError::Unknown;
- }
+ return maybeMatches == 1;
}
-std::variant<bool, QOlmError> QOlmSession::matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const
+
+bool QOlmSession::matchesInboundSessionFrom(
+ const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const
{
const auto theirIdentityKeyBuf = theirIdentityKey.toUtf8();
auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext();
- const auto error = olm_matches_inbound_session_from(m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(),
- oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
+ const auto maybeMatches = olm_matches_inbound_session_from(
+ m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(),
+ oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
- if (error == olm_error()) {
- return lastError(m_session);
- }
- switch (error) {
- case 0:
- return false;
- case 1:
- return true;
- default:
- return QOlmError::Unknown;
- }
+ if (maybeMatches == olm_error())
+ qCWarning(E2EE) << "Error matching an inbound session:"
+ << olm_session_last_error(m_session);
+ return maybeMatches == 1;
}
QOlmSession::QOlmSession(OlmSession *session)
: m_session(session)
-{
-}
+{}
diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h
index f20c9837..021092c7 100644
--- a/lib/e2ee/qolmsession.h
+++ b/lib/e2ee/qolmsession.h
@@ -4,17 +4,14 @@
#pragma once
-#include <QDebug>
-#include <olm/olm.h> // FIXME: OlmSession
#include "e2ee/e2ee.h"
#include "e2ee/qolmmessage.h"
#include "e2ee/qolmerrors.h"
#include "e2ee/qolmaccount.h"
-namespace Quotient {
+struct OlmSession;
-class QOlmAccount;
-class QOlmSession;
+namespace Quotient {
//! Either an outbound or inbound session for secure communication.
class QUOTIENT_API QOlmSession
@@ -22,32 +19,31 @@ class QUOTIENT_API QOlmSession
public:
~QOlmSession();
//! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
- static std::variant<std::unique_ptr<QOlmSession>, QOlmError>
- createInboundSession(QOlmAccount* account, const QOlmMessage& preKeyMessage);
+ static QOlmExpected<QOlmSessionPtr> createInboundSession(
+ QOlmAccount* account, const QOlmMessage& preKeyMessage);
- static std::variant<std::unique_ptr<QOlmSession>, QOlmError>
- createInboundSessionFrom(QOlmAccount* account,
- const QString& theirIdentityKey,
- const QOlmMessage& preKeyMessage);
+ static QOlmExpected<QOlmSessionPtr> createInboundSessionFrom(
+ QOlmAccount* account, const QString& theirIdentityKey,
+ const QOlmMessage& preKeyMessage);
- static std::variant<std::unique_ptr<QOlmSession>, QOlmError>
- createOutboundSession(QOlmAccount* account, const QString& theirIdentityKey,
- const QString& theirOneTimeKey);
+ static QOlmExpected<QOlmSessionPtr> createOutboundSession(
+ QOlmAccount* account, const QString& theirIdentityKey,
+ const QString& theirOneTimeKey);
//! Serialises an `QOlmSession` to encrypted Base64.
- std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode);
+ QOlmExpected<QByteArray> pickle(const PicklingMode &mode) const;
//! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`.
- static std::variant<std::unique_ptr<QOlmSession>, QOlmError> unpickle(
+ static QOlmExpected<QOlmSessionPtr> unpickle(
const QByteArray& pickled, const PicklingMode& mode);
//! Encrypts a plaintext message using the session.
QOlmMessage encrypt(const QString &plaintext);
- //! Decrypts a message using this session. Decoding is lossy, meaing if
+ //! 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` (�).
- std::variant<QString, QOlmError> decrypt(const QOlmMessage &message) const;
+ QOlmExpected<QByteArray> decrypt(const QOlmMessage &message) const;
//! Get a base64-encoded identifier for this session.
QByteArray sessionId() const;
@@ -59,11 +55,10 @@ public:
bool hasReceivedMessage() const;
//! Checks if the 'prekey' message is for this in-bound session.
- std::variant<bool, QOlmError> matchesInboundSession(
- const QOlmMessage& preKeyMessage) const;
+ bool matchesInboundSession(const QOlmMessage& preKeyMessage) const;
//! Checks if the 'prekey' message is for this in-bound session.
- std::variant<bool, QOlmError> matchesInboundSessionFrom(
+ bool matchesInboundSessionFrom(
const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const;
friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs)
@@ -71,8 +66,7 @@ public:
return lhs.sessionId() < rhs.sessionId();
}
- friend bool operator<(const std::unique_ptr<QOlmSession>& lhs,
- const std::unique_ptr<QOlmSession>& rhs)
+ friend bool operator<(const QOlmSessionPtr& lhs, const QOlmSessionPtr& rhs)
{
return *lhs < *rhs;
}
@@ -83,7 +77,7 @@ public:
private:
//! Helper function for creating new sessions and handling errors.
static OlmSession* create();
- static std::variant<std::unique_ptr<QOlmSession>, QOlmError> createInbound(
+ static QOlmExpected<QOlmSessionPtr> createInbound(
QOlmAccount* account, const QOlmMessage& preKeyMessage,
bool from = false, const QString& theirIdentityKey = "");
OlmSession* m_session;
diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp
index 9f09a37f..84559085 100644
--- a/lib/e2ee/qolmutility.cpp
+++ b/lib/e2ee/qolmutility.cpp
@@ -3,8 +3,8 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "e2ee/qolmutility.h"
-#include "olm/olm.h"
-#include <QDebug>
+
+#include <olm/olm.h>
using namespace Quotient;
@@ -40,8 +40,9 @@ QString QOlmUtility::sha256Utf8Msg(const QString &message) const
return sha256Bytes(message.toUtf8());
}
-std::variant<bool, QOlmError> QOlmUtility::ed25519Verify(const QByteArray &key,
- const QByteArray &message, const QByteArray &signature)
+QOlmExpected<bool> QOlmUtility::ed25519Verify(const QByteArray& key,
+ const QByteArray& message,
+ const QByteArray& signature)
{
QByteArray signatureBuf(signature.length(), '0');
std::copy(signature.begin(), signature.end(), signatureBuf.begin());
@@ -57,8 +58,5 @@ std::variant<bool, QOlmError> QOlmUtility::ed25519Verify(const QByteArray &key,
return error;
}
- if (ret != 0) {
- return false;
- }
- return true;
+ return !ret; // ret == 0 means success
}
diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h
index a12af49a..5f6bcdc5 100644
--- a/lib/e2ee/qolmutility.h
+++ b/lib/e2ee/qolmutility.h
@@ -4,15 +4,12 @@
#pragma once
-#include <variant>
-#include "e2ee/qolmerrors.h"
+#include "e2ee/e2ee.h"
struct OlmUtility;
namespace Quotient {
-class QOlmSession;
-
//! Allows you to make use of crytographic hashing via SHA-2 and
//! verifying ed25519 signatures.
class QUOTIENT_API QOlmUtility
@@ -32,7 +29,7 @@ public:
//! \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.
- std::variant<bool, QOlmError> ed25519Verify(const QByteArray &key,
+ QOlmExpected<bool> ed25519Verify(const QByteArray &key,
const QByteArray &message, const QByteArray &signature);
private:
diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp
index 302ae053..a2e2a156 100644
--- a/lib/eventitem.cpp
+++ b/lib/eventitem.cpp
@@ -8,32 +8,23 @@
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; });
- }
- setStatus(EventStatus::FileUploaded);
-}
-
-void PendingEventItem::setEncryptedFile(const EncryptedFile& encryptedFile)
-{
- if (auto* rme = getAs<RoomMessageEvent>()) {
- Q_ASSERT(rme->hasFileContent());
- rme->editContent([encryptedFile](EventContent::TypedBase& ec) {
- ec.fileInfo()->file = encryptedFile;
+ rae->editContent([&uploadedFileData](EventContent::FileInfo& fi) {
+ fi.source = uploadedFileData;
});
}
+ setStatus(EventStatus::FileUploaded);
}
// Not exactly sure why but this helps with the linker not finding
diff --git a/lib/eventitem.h b/lib/eventitem.h
index d8313736..90d9f458 100644
--- a/lib/eventitem.h
+++ b/lib/eventitem.h
@@ -3,18 +3,18 @@
#pragma once
-#include "events/stateevent.h"
#include "quotient_common.h"
+#include "events/filesourceinfo.h"
+#include "events/stateevent.h"
+
#include <any>
#include <utility>
-#include "events/encryptedfile.h"
-
namespace Quotient {
namespace EventStatus {
- QUO_NAMESPACE
+ Q_NAMESPACE_EXPORT(QUOTIENT_API)
/** Special marks an event can assume
*
@@ -22,16 +22,16 @@ namespace EventStatus {
* 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_ENUM_NS(Code)
} // namespace EventStatus
@@ -115,8 +115,7 @@ public:
QString annotation() const { return _annotation; }
void setDeparted() { setStatus(EventStatus::Departed); }
- void setFileUploaded(const QUrl& remoteUrl);
- void setEncryptedFile(const EncryptedFile& encryptedFile);
+ void setFileUploaded(const FileSourceInfo &uploadedFileData);
void setReachedServer(const QString& eventId)
{
setStatus(EventStatus::ReachedServer);
diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h
index 12f1f00b..24c3353c 100644
--- a/lib/events/accountdataevents.h
+++ b/lib/events/accountdataevents.h
@@ -32,7 +32,7 @@ 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;
@@ -46,27 +46,14 @@ struct JsonObjectConverter<TagRecord> {
using TagsMap = QHash<QString, TagRecord>;
-#define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \
- class QUOTIENT_API _Name : public Event { \
- public: \
- using content_type = _ContentType; \
- DEFINE_EVENT_TYPEID(_TypeId, _Name) \
- explicit _Name(const QJsonObject& obj) : Event(typeId(), obj) {} \
- explicit _Name(const content_type& content) \
- : Event(typeId(), matrixTypeId(), \
- QJsonObject { \
- { QStringLiteral(#_ContentKey), toJson(content) } }) \
- {} \
- auto _ContentKey() const \
- { \
- return contentPart<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>,
+DEFINE_SIMPLE_EVENT(TagEvent, Event, "m.tag", TagsMap, tags)
+DEFINE_SIMPLE_EVENT(ReadMarkerEventImpl, Event, "m.fully_read", QString, eventId)
+class ReadMarkerEvent : public ReadMarkerEventImpl {
+public:
+ using ReadMarkerEventImpl::ReadMarkerEventImpl;
+ [[deprecated("Use ReadMarkerEvent::eventId() instead")]]
+ QString event_id() const { return eventId(); }
+};
+DEFINE_SIMPLE_EVENT(IgnoredUsersEvent, Event, "m.ignored_user_list", QSet<QString>,
ignored_users)
} // namespace Quotient
diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp
index be83d9d0..f75f8ad3 100644
--- a/lib/events/callanswerevent.cpp
+++ b/lib/events/callanswerevent.cpp
@@ -14,7 +14,6 @@ m.call.answer
"type": "answer"
},
"call_id": "12345",
- "lifetime": 60000,
"version": 0
},
"event_id": "$WLGTSEFSEF:localhost",
@@ -33,16 +32,6 @@ CallAnswerEvent::CallAnswerEvent(const QJsonObject& 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,
diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h
index 8ffe60f2..4d539b85 100644
--- a/lib/events/callanswerevent.h
+++ b/lib/events/callanswerevent.h
@@ -13,14 +13,8 @@ public:
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 contentPart<int>("lifetime"_ls);
- } // FIXME: Omittable<>?
QString sdp() const
{
return contentPart<QJsonObject>("answer"_ls).value("sdp"_ls).toString();
diff --git a/lib/events/callcandidatesevent.h b/lib/events/callcandidatesevent.h
index 74c38f2c..e949f722 100644
--- a/lib/events/callcandidatesevent.h
+++ b/lib/events/callcandidatesevent.h
@@ -23,20 +23,9 @@ public:
{ { QStringLiteral("candidates"), candidates } })
{}
- QJsonArray candidates() const
- {
- return contentPart<QJsonArray>("candidates"_ls);
- }
-
- QString callId() const
- {
- return contentPart<QString>("call_id");
- }
-
- int version() const
- {
- return contentPart<int>("version");
- }
+ QUO_CONTENT_GETTER(QJsonArray, candidates)
+ QUO_CONTENT_GETTER(QString, callId)
+ QUO_CONTENT_GETTER(int, version)
};
REGISTER_EVENT_TYPE(CallCandidatesEvent)
diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp
index 11d50768..2f26a1cb 100644
--- a/lib/events/callinviteevent.cpp
+++ b/lib/events/callinviteevent.cpp
@@ -33,7 +33,7 @@ CallInviteEvent::CallInviteEvent(const QJsonObject& obj)
qCDebug(EVENTS) << "Call Invite event";
}
-CallInviteEvent::CallInviteEvent(const QString& callId, const int lifetime,
+CallInviteEvent::CallInviteEvent(const QString& callId, int lifetime,
const QString& sdp)
: CallEventBase(
typeId(), matrixTypeId(), callId, 0,
diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h
index 47362b5c..5b4ca0df 100644
--- a/lib/events/callinviteevent.h
+++ b/lib/events/callinviteevent.h
@@ -13,13 +13,10 @@ public:
explicit CallInviteEvent(const QJsonObject& obj);
- explicit CallInviteEvent(const QString& callId, const int lifetime,
+ explicit CallInviteEvent(const QString& callId, int lifetime,
const QString& sdp);
- int lifetime() const
- {
- return contentPart<int>("lifetime"_ls);
- } // FIXME: Omittable<>?
+ QUO_CONTENT_GETTER(int, lifetime)
QString sdp() const
{
return contentPart<QJsonObject>("offer"_ls).value("sdp"_ls).toString();
diff --git a/lib/events/directchatevent.cpp b/lib/events/directchatevent.cpp
index 0ee1f7b0..83bb1e32 100644
--- a/lib/events/directchatevent.cpp
+++ b/lib/events/directchatevent.cpp
@@ -3,8 +3,6 @@
#include "directchatevent.h"
-#include <QtCore/QJsonArray>
-
using namespace Quotient;
QMultiHash<QString, QString> DirectChatEvent::usersToDirectChats() const
diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp
index 3af3d6ff..ec00ad4c 100644
--- a/lib/events/encryptedevent.cpp
+++ b/lib/events/encryptedevent.cpp
@@ -49,14 +49,16 @@ RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const
eventObject["event_id"] = id();
eventObject["sender"] = senderId();
eventObject["origin_server_ts"] = originTimestamp().toMSecsSinceEpoch();
- if (const auto relatesToJson = contentPart("m.relates_to"_ls); !relatesToJson.isUndefined()) {
+ if (const auto relatesToJson = contentPart<QJsonObject>("m.relates_to"_ls);
+ !relatesToJson.isEmpty()) {
auto content = eventObject["content"].toObject();
- content["m.relates_to"] = relatesToJson.toObject();
+ content["m.relates_to"] = relatesToJson;
eventObject["content"] = content;
}
- if (const auto redactsJson = unsignedPart("redacts"_ls); !redactsJson.isUndefined()) {
+ if (const auto redactsJson = unsignedPart<QString>("redacts"_ls);
+ !redactsJson.isEmpty()) {
auto unsign = eventObject["unsigned"].toObject();
- unsign["redacts"] = redactsJson.toString();
+ unsign["redacts"] = redactsJson;
eventObject["unsigned"] = unsign;
}
return loadEvent<RoomEvent>(eventObject);
@@ -64,7 +66,7 @@ RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const
void EncryptedEvent::setRelation(const QJsonObject& relation)
{
- auto content = editJson()["content"_ls].toObject();
+ auto content = contentJson();
content["m.relates_to"] = relation;
editJson()["content"] = content;
}
diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h
index bfacdec9..ddd5e415 100644
--- a/lib/events/encryptedevent.h
+++ b/lib/events/encryptedevent.h
@@ -58,8 +58,6 @@ public:
RoomEventPtr createDecrypted(const QString &decrypted) const;
void setRelation(const QJsonObject& relation);
-
- bool isVerified();
};
REGISTER_EVENT_TYPE(EncryptedEvent)
diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp
deleted file mode 100644
index bb4e26c7..00000000
--- a/lib/events/encryptedfile.cpp
+++ /dev/null
@@ -1,118 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
-//
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#include "encryptedfile.h"
-#include "logging.h"
-
-#ifdef Quotient_E2EE_ENABLED
-#include <openssl/evp.h>
-#include <QtCore/QCryptographicHash>
-#include "e2ee/qolmutils.h"
-#endif
-
-using namespace Quotient;
-
-QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const
-{
-#ifdef Quotient_E2EE_ENABLED
- auto _key = key.k;
- const auto keyBytes = QByteArray::fromBase64(
- _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1());
- const auto sha256 = QByteArray::fromBase64(hashes["sha256"].toLatin1());
- if (sha256
- != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) {
- qCWarning(E2EE) << "Hash verification failed for file";
- return {};
- }
- {
- 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(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<EncryptedFile, QByteArray> EncryptedFile::encryptFile(const QByteArray &plainText)
-{
-#ifdef Quotient_E2EE_ENABLED
- QByteArray k = getRandom(32);
- auto kBase64 = k.toBase64();
- QByteArray iv = getRandom(16);
- JWK key = {"oct"_ls, {"encrypt"_ls, "decrypt"_ls}, "A256CTR"_ls, QString(k.toBase64()).replace(u'/', u'_').replace(u'+', u'-').left(kBase64.indexOf('=')), true};
-
- int length;
- auto* ctx = EVP_CIPHER_CTX_new();
- QByteArray cipherText(plainText.size(), plainText.size() + EVP_MAX_BLOCK_LENGTH - 1);
- EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast<const unsigned char*>(k.data()),reinterpret_cast<const unsigned char*>(iv.data()));
- 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();
- auto ivBase64 = iv.toBase64();
- EncryptedFile file = {{}, key, ivBase64.left(ivBase64.indexOf('=')), {{QStringLiteral("sha256"), hash.left(hash.indexOf('='))}}, "v2"_ls};
- return {file, cipherText};
-#else
- return {{}, {}};
-#endif
-}
-
-void JsonObjectConverter<EncryptedFile>::dumpTo(QJsonObject& jo,
- const EncryptedFile& pod)
-{
- addParam<>(jo, QStringLiteral("url"), pod.url);
- addParam<>(jo, QStringLiteral("key"), pod.key);
- addParam<>(jo, QStringLiteral("iv"), pod.iv);
- addParam<>(jo, QStringLiteral("hashes"), pod.hashes);
- addParam<>(jo, QStringLiteral("v"), pod.v);
-}
-
-void JsonObjectConverter<EncryptedFile>::fillFrom(const QJsonObject& jo,
- EncryptedFile& pod)
-{
- fromJson(jo.value("url"_ls), pod.url);
- fromJson(jo.value("key"_ls), pod.key);
- fromJson(jo.value("iv"_ls), pod.iv);
- fromJson(jo.value("hashes"_ls), pod.hashes);
- fromJson(jo.value("v"_ls), pod.v);
-}
-
-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);
-}
diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h
deleted file mode 100644
index b2808395..00000000
--- a/lib/events/encryptedfile.h
+++ /dev/null
@@ -1,63 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
-//
-// SPDX-License-Identifier: LGPl-2.1-or-later
-
-#pragma once
-
-#include "converters.h"
-
-namespace Quotient {
-/**
- * JSON Web Key object as specified in
- * https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes
- * The only currently relevant member is `k`, the rest needs to be set to the defaults specified in the spec.
- */
-struct JWK
-{
- Q_GADGET
- Q_PROPERTY(QString kty MEMBER kty CONSTANT)
- Q_PROPERTY(QStringList keyOps MEMBER keyOps CONSTANT)
- Q_PROPERTY(QString alg MEMBER alg CONSTANT)
- Q_PROPERTY(QString k MEMBER k CONSTANT)
- Q_PROPERTY(bool ext MEMBER ext CONSTANT)
-
-public:
- QString kty;
- QStringList keyOps;
- QString alg;
- QString k;
- bool ext;
-};
-
-struct QUOTIENT_API EncryptedFile
-{
- Q_GADGET
- Q_PROPERTY(QUrl url MEMBER url CONSTANT)
- Q_PROPERTY(JWK key MEMBER key CONSTANT)
- Q_PROPERTY(QString iv MEMBER iv CONSTANT)
- Q_PROPERTY(QHash<QString, QString> hashes MEMBER hashes CONSTANT)
- Q_PROPERTY(QString v MEMBER v CONSTANT)
-
-public:
- QUrl url;
- JWK key;
- QString iv;
- QHash<QString, QString> hashes;
- QString v;
-
- QByteArray decryptFile(const QByteArray &ciphertext) const;
- static std::pair<EncryptedFile, QByteArray> encryptFile(const QByteArray& plainText);
-};
-
-template <>
-struct QUOTIENT_API JsonObjectConverter<EncryptedFile> {
- static void dumpTo(QJsonObject& jo, const EncryptedFile& pod);
- static void fillFrom(const QJsonObject& jo, EncryptedFile& pod);
-};
-
-template <>
-struct QUOTIENT_API JsonObjectConverter<JWK> {
- static void dumpTo(QJsonObject& jo, const JWK& pod);
- static void fillFrom(const QJsonObject& jo, JWK& pod);
-};
-} // namespace Quotient
diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp
index 6272c668..8872447b 100644
--- a/lib/events/encryptionevent.cpp
+++ b/lib/events/encryptionevent.cpp
@@ -6,52 +6,47 @@
#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(EncryptionType et)
+EncryptionEventContent::EncryptionEventContent(Quotient::EncryptionType et)
: encryption(et)
{
- if(encryption != Undefined) {
- algorithm = encryptionStrings[encryption];
+ 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 124ced33..91452c3f 100644
--- a/lib/events/encryptionevent.h
+++ b/lib/events/encryptionevent.h
@@ -4,57 +4,49 @@
#pragma once
-#include "eventcontent.h"
-#include "stateevent.h"
#include "quotient_common.h"
+#include "stateevent.h"
namespace Quotient {
-class QUOTIENT_API 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;
- QUO_IMPLICIT EncryptionEventContent(EncryptionType et);
- [[deprecated("This constructor will require explicit EncryptionType soon")]] //
- explicit EncryptionEventContent()
- : EncryptionEventContent(Undefined)
- {}
+ // 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 QUOTIENT_API EncryptionEvent : public StateEvent<EncryptionEventContent> {
- Q_GADGET
public:
DEFINE_EVENT_TYPEID("m.room.encryption", EncryptionEvent)
- using EncryptionType = EncryptionEventContent::EncryptionType;
- Q_ENUM(EncryptionType)
+ using EncryptionType
+ [[deprecated("Use Quotient::EncryptionType instead")]] =
+ Quotient::EncryptionType;
explicit EncryptionEvent(const QJsonObject& obj)
: StateEvent(typeId(), obj)
{}
- [[deprecated("This constructor will require an explicit parameter soon")]] //
-// explicit EncryptionEvent()
-// : EncryptionEvent(QJsonObject())
-// {}
explicit EncryptionEvent(EncryptionEventContent&& content)
: StateEvent(typeId(), matrixTypeId(), QString(), std::move(content))
{}
- EncryptionType encryption() const { return content().encryption; }
-
+ 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; }
+
+ 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 4c304a3c..1f1eebaa 100644
--- a/lib/events/event.cpp
+++ b/lib/events/event.cpp
@@ -29,7 +29,7 @@ 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(type, basicJson(matrixType, contentJson))
{}
Event::~Event() = default;
diff --git a/lib/events/event.h b/lib/events/event.h
index 113fa3fa..b7454337 100644
--- a/lib/events/event.h
+++ b/lib/events/event.h
@@ -48,13 +48,6 @@ const QString RoomIdKey { RoomIdKeyL };
const QString UnsignedKey { UnsignedKeyL };
const QString StateKeyKey { StateKeyKeyL };
-/// Make a minimal correct Matrix event JSON
-inline QJsonObject basicEventJson(const QString& matrixType,
- const QJsonObject& content)
-{
- return { { TypeKey, matrixType }, { ContentKey, content } };
-}
-
// === Event types ===
using event_type_t = QLatin1String;
@@ -193,6 +186,13 @@ public:
Event& operator=(Event&&) = delete;
virtual ~Event();
+ /// Make a minimal correct Matrix event JSON
+ static QJsonObject basicJson(const QString& matrixType,
+ const QJsonObject& content)
+ {
+ return { { TypeKey, matrixType }, { ContentKey, content } };
+ }
+
Type type() const { return _type; }
QString matrixType() const;
[[deprecated("Use fullJson() and stringify it with QJsonDocument::toJson() "
@@ -212,7 +212,7 @@ public:
const QJsonObject contentJson() const;
- template <typename T = QJsonValue, typename KeyT>
+ template <typename T, typename KeyT>
const T contentPart(KeyT&& key) const
{
return fromJson<T>(contentJson()[std::forward<KeyT>(key)]);
@@ -227,7 +227,7 @@ public:
const QJsonObject unsignedJson() const;
- template <typename T = QJsonValue, typename KeyT>
+ template <typename T, typename KeyT>
const T unsignedPart(KeyT&& key) const
{
return fromJson<T>(unsignedJson()[std::forward<KeyT>(key)]);
@@ -258,6 +258,21 @@ template <typename EventT>
using EventsArray = std::vector<event_ptr_tt<EventT>>;
using Events = EventsArray<Event>;
+//! \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_) \
+ PartType_ PartName_() const \
+ { \
+ static const auto JsonKey = toSnakeCase(#PartName_##_ls); \
+ return contentPart<PartType_>(JsonKey); \
+ }
+
// === Facilities for event class definitions ===
// This macro should be used in a public section of an event class to
@@ -278,6 +293,32 @@ using Events = EventsArray<Event>;
Type_::factory.addMethod<Type_>(); \
// End of macro
+/// \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_) \
+ class QUOTIENT_API Name_ : public Base_ { \
+ public: \
+ using content_type = ValueType_; \
+ DEFINE_EVENT_TYPEID(TypeId_, Name_) \
+ explicit Name_(const QJsonObject& obj) : Base_(TypeId, obj) {} \
+ explicit Name_(const content_type& content) \
+ : Name_(Base_::basicJson(TypeId, { { JsonKey, toJson(content) } })) \
+ {} \
+ auto GetterName_() const \
+ { \
+ return contentPart<content_type>(JsonKey); \
+ } \
+ static inline const auto JsonKey = toSnakeCase(#GetterName_##_ls); \
+ }; \
+ REGISTER_EVENT_TYPE(Name_) \
+ // End of macro
+
// === is<>(), eventCast<>() and switchOnType<>() ===
template <class EventT>
@@ -291,65 +332,72 @@ inline bool isUnknown(const Event& e)
return e.type() == UnknownEventTypeId;
}
+//! \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 <class 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 trivial generic catch-all "switch"
-template <class BaseEventT, typename FnT>
-inline auto switchOnType(const BaseEventT& event, FnT&& fn)
- -> decltype(fn(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 <class EventT, typename BaseEventT>
+inline auto eventCast(event_ptr_tt<BaseEventT>&& eptr)
{
- return fn(event);
+ return eptr && is<std::decay_t<EventT>>(*eptr)
+ ? event_ptr_tt<EventT>(static_cast<EventT*>(eptr.release()))
+ : nullptr;
}
namespace _impl {
- // Using bool instead of auto below because auto apparently upsets MSVC
- template <class BaseT, typename FnT>
- constexpr bool needs_downcast =
- std::is_base_of_v<BaseT, std::decay_t<fn_arg_t<FnT>>>
- && !std::is_same_v<BaseT, std::decay_t<fn_arg_t<FnT>>>;
+ template <typename FnT, class BaseT>
+ concept Invocable_With_Downcast =
+ std::is_base_of_v<BaseT, std::remove_cvref_t<fn_arg_t<FnT>>>;
}
-// A trivial type-specific "switch" for a void function
-template <class BaseT, typename FnT>
-inline auto switchOnType(const BaseT& event, FnT&& fn)
- -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT>
- && std::is_void_v<fn_return_t<FnT>>>
+template <class 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))
- fn(static_cast<event_type>(event));
-}
-
-// A trivial type-specific "switch" for non-void functions with an optional
-// default value; non-voidness is guarded by defaultValue type
-template <class BaseT, typename FnT>
-inline auto switchOnType(const BaseT& event, FnT&& fn,
- fn_return_t<FnT>&& defaultValue = {})
- -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT>, fn_return_t<FnT>>
-{
- using event_type = fn_arg_t<FnT>;
- if (is<std::decay_t<event_type>>(event))
- return fn(static_cast<event_type>(event));
- return std::move(defaultValue);
+ 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 switch for a chain of 2 or more functions
-template <class BaseT, typename FnT1, typename FnT2, typename... FnTs>
-inline std::common_type_t<fn_return_t<FnT1>, fn_return_t<FnT2>>
-switchOnType(const BaseT& event, FnT1&& fn1, FnT2&& fn2, FnTs&&... fns)
+template <class BaseT, typename FnT1, typename... FnTs>
+inline auto switchOnType(const BaseT& event, FnT1&& fn1, FnTs&&... fns)
{
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<FnT2>(fn2),
- std::forward<FnTs>(fns)...);
+ return fn1(static_cast<event_type1>(event));
+ return switchOnType(event, std::forward<FnTs>(fns)...);
}
template <class BaseT, typename... FnTs>
@@ -364,8 +412,8 @@ inline auto visit(const BaseT& event, FnTs&&... fns)
// TODO: replace with ranges::for_each once all standard libraries have it
template <typename RangeT, typename... FnTs>
inline auto visitEach(RangeT&& events, FnTs&&... fns)
- -> std::enable_if_t<std::is_void_v<
- decltype(switchOnType(**begin(events), std::forward<FnTs>(fns)...))>>
+ requires std::is_void_v<
+ decltype(switchOnType(**begin(events), std::forward<FnTs>(fns)...))>
{
for (auto&& evtPtr: events)
switchOnType(*evtPtr, std::forward<FnTs>(fns)...);
diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp
index 9d7edf20..8db3b7e3 100644
--- a/lib/events/eventcontent.cpp
+++ b/lib/events/eventcontent.cpp
@@ -15,26 +15,25 @@ using std::move;
QJsonObject Base::toJson() const
{
QJsonObject o;
- fillJson(&o);
+ fillJson(o);
return o;
}
-FileInfo::FileInfo(const QFileInfo &fi)
- : mimeType(QMimeDatabase().mimeTypeForFile(fi))
- , url(QUrl::fromLocalFile(fi.filePath()))
- , payloadSize(fi.size())
- , originalName(fi.fileName())
+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(QUrl u, qint64 payloadSize, const QMimeType& mimeType,
- Omittable<EncryptedFile> file, QString originalFilename)
- : mimeType(mimeType)
- , url(move(u))
+FileInfo::FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize,
+ const QMimeType& mimeType, QString originalFilename)
+ : source(move(sourceInfo))
+ , mimeType(mimeType)
, payloadSize(payloadSize)
, originalName(move(originalFilename))
- , file(file)
{
if (!isValid())
qCWarning(MESSAGES)
@@ -43,77 +42,81 @@ FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType,
"0.7; for local resources, use FileInfo(QFileInfo) instead";
}
-FileInfo::FileInfo(QUrl mxcUrl, const QJsonObject& infoJson,
- const Omittable<EncryptedFile> &file,
+FileInfo::FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson,
QString originalFilename)
- : originalInfoJson(infoJson)
+ : source(move(sourceInfo))
+ , originalInfoJson(infoJson)
, mimeType(
QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString()))
- , url(move(mxcUrl))
, payloadSize(fromJson<qint64>(infoJson["size"_ls]))
, originalName(move(originalFilename))
- , file(file)
{
- if(url.isEmpty() && file.has_value()) {
- url = file->url;
- }
if (!mimeType.isValid())
mimeType = QMimeDatabase().mimeTypeForData(QByteArray());
}
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());
- //TODO add encryptedfile
+ return getUrlFromSourceInfo(source);
+}
+
+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(const QUrl& mxcUrl, qint64 fileSize, const QMimeType& type,
- QSize imageSize, const Omittable<EncryptedFile> &file, const QString& originalFilename)
- : FileInfo(mxcUrl, fileSize, type, file, originalFilename)
+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& mxcUrl, const QJsonObject& infoJson,
- const Omittable<EncryptedFile> &file,
+ImageInfo::ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson,
const QString& originalFilename)
- : FileInfo(mxcUrl, infoJson, file, 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, const Omittable<EncryptedFile> &file)
+Thumbnail::Thumbnail(const QJsonObject& infoJson,
+ const Omittable<EncryptedFileMetadata>& efm)
: ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()),
- infoJson["thumbnail_info"_ls].toObject(),
- file)
-{}
+ 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 de9a792b..af26c0a4 100644
--- a/lib/events/eventcontent.h
+++ b/lib/events/eventcontent.h
@@ -6,279 +6,249 @@
// This file contains generic event content definitions, applicable to room
// message events as well as other events (e.g., avatars).
-#include "encryptedfile.h"
+#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>
class QFileInfo;
-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 QUOTIENT_API 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 (the definitions are
- // spread across eventcontent.h and roommessageevent.h):
- // FileInfo
- // FileContent : UrlWithThumbnailContent<FileInfo>
- // AudioContent : PlayableContent<UrlBasedContent<FileInfo>>
- // ImageInfo : FileInfo + imageSize attribute
- // ImageContent : UrlWithThumbnailContent<ImageInfo>
- // VideoContent : PlayableContent<UrlWithThumbnailContent<ImageInfo>>
-
- /**
- * 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 QUOTIENT_API FileInfo {
- public:
- FileInfo() = default;
- explicit FileInfo(const QFileInfo& fi);
- explicit FileInfo(QUrl mxcUrl, qint64 payloadSize = -1,
- const QMimeType& mimeType = {},
- Omittable<EncryptedFile> file = none,
- QString originalFilename = {});
- FileInfo(QUrl mxcUrl, const QJsonObject& infoJson,
- const Omittable<EncryptedFile> &file,
- 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 = 0;
- QString originalName;
- Omittable<EncryptedFile> file = none;
- };
-
- template <typename InfoT>
- QJsonObject toInfoJson(const InfoT& info)
+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 QUOTIENT_API ImageInfo : public FileInfo {
- public:
- ImageInfo() = default;
- explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {});
- explicit ImageInfo(const QUrl& mxcUrl, qint64 fileSize = -1,
- const QMimeType& type = {}, QSize imageSize = {},
- const Omittable<EncryptedFile> &file = none,
- const QString& originalFilename = {});
- ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson,
- const Omittable<EncryptedFile> &encryptedFile,
- 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 QUOTIENT_API Thumbnail : public ImageInfo {
- public:
- using ImageInfo::ImageInfo;
- Thumbnail(const QJsonObject& infoJson, const Omittable<EncryptedFile> &file = none);
-
- /**
- * Writes thumbnail information to "thumbnail_info" subobject
- * and thumbnail URL to "thumbnail_url" node inside "info".
- */
- void fillInfoJson(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;
- };
-
- /**
- * 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 QUOTIENT_API UrlBasedContent : public TypedBase, public InfoT {
- public:
- using InfoT::InfoT;
- explicit UrlBasedContent(const QJsonObject& json)
- : TypedBase(json)
- , InfoT(QUrl(json["url"].toString()), json["info"].toObject(),
- fromJson<Omittable<EncryptedFile>>(json["file"]), 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);
- if (!InfoT::file.has_value()) {
- json->insert("url", InfoT::url.toString());
- } else {
- json->insert("file", Quotient::toJson(*InfoT::file));
- }
- if (!InfoT::originalName.isEmpty())
- json->insert("filename", InfoT::originalName);
- json->insert("info", toInfoJson<InfoT>(*this));
- }
- };
-
- template <typename InfoT>
- class QUOTIENT_API 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 fe624d70..4c639efa 100644
--- a/lib/events/eventloader.h
+++ b/lib/events/eventloader.h
@@ -19,43 +19,27 @@ inline event_ptr_tt<BaseEventT> loadEvent(const QJsonObject& fullJson)
return doLoadEvent<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)
+//! \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 <typename BaseEventT, typename... BasicJsonParamTs>
+inline event_ptr_tt<BaseEventT> loadEvent(
+ const QString& matrixType, const BasicJsonParamTs&... basicJsonParams)
{
- return doLoadEvent<BaseEventT>(basicEventJson(matrixType, content),
- matrixType);
-}
-
-/*! 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 doLoadEvent<StateEventBase>(
- basicStateEventJson(matrixType, content, stateKey), matrixType);
+ return doLoadEvent<BaseEventT>(
+ BaseEventT::basicJson(matrixType, basicJsonParams...), matrixType);
}
template <typename EventT>
-struct JsonConverter<event_ptr_tt<EventT>> {
- static auto load(const QJsonValue& jv)
+struct JsonConverter<event_ptr_tt<EventT>>
+ : JsonObjectUnpacker<event_ptr_tt<EventT>> {
+ using JsonObjectUnpacker<event_ptr_tt<EventT>>::load;
+ static auto load(const QJsonObject& jo)
{
- return loadEvent<EventT>(jv.toObject());
- }
- static auto load(const QJsonDocument& jd)
- {
- return loadEvent<EventT>(jd.object());
+ return loadEvent<EventT>(jo);
}
};
+
} // namespace Quotient
diff --git a/lib/events/eventrelation.h b/lib/events/eventrelation.h
index e445ee42..2a841cf1 100644
--- a/lib/events/eventrelation.h
+++ b/lib/events/eventrelation.h
@@ -34,11 +34,11 @@ struct QUOTIENT_API EventRelation {
return { ReplacementType, std::move(eventId) };
}
- [[deprecated("Use ReplyRelation variable instead")]]
+ [[deprecated("Use ReplyType variable instead")]]
static constexpr auto Reply() { return ReplyType; }
- [[deprecated("Use AnnotationRelation variable instead")]] //
+ [[deprecated("Use AnnotationType variable instead")]] //
static constexpr auto Annotation() { return AnnotationType; }
- [[deprecated("Use ReplacementRelation variable instead")]] //
+ [[deprecated("Use ReplacementType variable instead")]] //
static constexpr auto Replacement() { return ReplacementType; }
};
diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp
new file mode 100644
index 00000000..e8b6794b
--- /dev/null
+++ b/lib/events/filesourceinfo.cpp
@@ -0,0 +1,172 @@
+// 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
+ QByteArray k = getRandom(32);
+ auto kBase64 = k.toBase64();
+ QByteArray iv = getRandom(16);
+ JWK key = { "oct"_ls,
+ { "encrypt"_ls, "decrypt"_ls },
+ "A256CTR"_ls,
+ QString(k.toBase64())
+ .replace(u'/', u'_')
+ .replace(u'+', u'-')
+ .left(kBase64.indexOf('=')),
+ true };
+
+ int length;
+ auto* ctx = EVP_CIPHER_CTX_new();
+ EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr,
+ reinterpret_cast<const unsigned char*>(k.data()),
+ reinterpret_cast<const unsigned char*>(iv.data()));
+ 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();
+ auto ivBase64 = iv.toBase64();
+ EncryptedFileMetadata efm = { {},
+ key,
+ ivBase64.left(ivBase64.indexOf('=')),
+ { { QStringLiteral("sha256"),
+ hash.left(hash.indexOf('=')) } },
+ "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.cpp b/lib/events/keyverificationevent.cpp
deleted file mode 100644
index e7f5b019..00000000
--- a/lib/events/keyverificationevent.cpp
+++ /dev/null
@@ -1,194 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#include "keyverificationevent.h"
-
-using namespace Quotient;
-
-KeyVerificationRequestEvent::KeyVerificationRequestEvent(const QJsonObject &obj)
- : Event(typeId(), obj)
-{}
-
-QString KeyVerificationRequestEvent::fromDevice() const
-{
- return contentPart<QString>("from_device"_ls);
-}
-
-QString KeyVerificationRequestEvent::transactionId() const
-{
- return contentPart<QString>("transaction_id"_ls);
-}
-
-QStringList KeyVerificationRequestEvent::methods() const
-{
- return contentPart<QStringList>("methods"_ls);
-}
-
-uint64_t KeyVerificationRequestEvent::timestamp() const
-{
- return contentPart<double>("timestamp"_ls);
-}
-
-KeyVerificationStartEvent::KeyVerificationStartEvent(const QJsonObject &obj)
- : Event(typeId(), obj)
-{}
-
-QString KeyVerificationStartEvent::fromDevice() const
-{
- return contentPart<QString>("from_device"_ls);
-}
-
-QString KeyVerificationStartEvent::transactionId() const
-{
- return contentPart<QString>("transaction_id"_ls);
-}
-
-QString KeyVerificationStartEvent::method() const
-{
- return contentPart<QString>("method"_ls);
-}
-
-Omittable<QString> KeyVerificationStartEvent::nextMethod() const
-{
- return contentPart<Omittable<QString>>("method_ls");
-}
-
-QStringList KeyVerificationStartEvent::keyAgreementProtocols() const
-{
- Q_ASSERT(method() == QStringLiteral("m.sas.v1"));
- return contentPart<QStringList>("key_agreement_protocols"_ls);
-}
-
-QStringList KeyVerificationStartEvent::hashes() const
-{
- Q_ASSERT(method() == QStringLiteral("m.sas.v1"));
- return contentPart<QStringList>("hashes"_ls);
-
-}
-
-QStringList KeyVerificationStartEvent::messageAuthenticationCodes() const
-{
- Q_ASSERT(method() == QStringLiteral("m.sas.v1"));
- return contentPart<QStringList>("message_authentication_codes"_ls);
-}
-
-QString KeyVerificationStartEvent::shortAuthenticationString() const
-{
- return contentPart<QString>("short_authentification_string"_ls);
-}
-
-KeyVerificationAcceptEvent::KeyVerificationAcceptEvent(const QJsonObject &obj)
- : Event(typeId(), obj)
-{}
-
-QString KeyVerificationAcceptEvent::transactionId() const
-{
- return contentPart<QString>("transaction_id"_ls);
-}
-
-QString KeyVerificationAcceptEvent::method() const
-{
- return contentPart<QString>("method"_ls);
-}
-
-QString KeyVerificationAcceptEvent::keyAgreementProtocol() const
-{
- return contentPart<QString>("key_agreement_protocol"_ls);
-}
-
-QString KeyVerificationAcceptEvent::hashData() const
-{
- return contentPart<QString>("hash"_ls);
-}
-
-QStringList KeyVerificationAcceptEvent::shortAuthenticationString() const
-{
- return contentPart<QStringList>("short_authentification_string"_ls);
-}
-
-QString KeyVerificationAcceptEvent::commitment() const
-{
- return contentPart<QString>("commitment"_ls);
-}
-
-KeyVerificationCancelEvent::KeyVerificationCancelEvent(const QJsonObject &obj)
- : Event(typeId(), obj)
-{}
-
-QString KeyVerificationCancelEvent::transactionId() const
-{
- return contentPart<QString>("transaction_id"_ls);
-}
-
-QString KeyVerificationCancelEvent::reason() const
-{
- return contentPart<QString>("reason"_ls);
-}
-
-QString KeyVerificationCancelEvent::code() const
-{
- return contentPart<QString>("code"_ls);
-}
-
-KeyVerificationKeyEvent::KeyVerificationKeyEvent(const QJsonObject &obj)
- : Event(typeId(), obj)
-{}
-
-QString KeyVerificationKeyEvent::transactionId() const
-{
- return contentPart<QString>("transaction_id"_ls);
-}
-
-QString KeyVerificationKeyEvent::key() const
-{
- return contentPart<QString>("key"_ls);
-}
-
-KeyVerificationMacEvent::KeyVerificationMacEvent(const QJsonObject &obj)
- : Event(typeId(), obj)
-{}
-
-QString KeyVerificationMacEvent::transactionId() const
-{
- return contentPart<QString>("transaction_id"_ls);
-}
-
-QString KeyVerificationMacEvent::keys() const
-{
- return contentPart<QString>("keys"_ls);
-}
-
-QHash<QString, QString> KeyVerificationMacEvent::mac() const
-{
- return contentPart<QHash<QString, QString>>("mac"_ls);
-}
-
-KeyVerificationDoneEvent::KeyVerificationDoneEvent(const QJsonObject &obj)
- : Event(typeId(), obj)
-{
-}
-
-QString KeyVerificationDoneEvent::transactionId() const
-{
- return contentPart<QString>("transaction_id"_ls);
-}
-
-
-KeyVerificationReadyEvent::KeyVerificationReadyEvent(const QJsonObject &obj)
- : Event(typeId(), obj)
-{}
-
-QString KeyVerificationReadyEvent::fromDevice() const
-{
- return contentPart<QString>("from_device"_ls);
-}
-
-QString KeyVerificationReadyEvent::transactionId() const
-{
- return contentPart<QString>("transaction_id"_ls);
-}
-
-QStringList KeyVerificationReadyEvent::methods() const
-{
- return contentPart<QStringList>("methods"_ls);
-}
diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h
index a9f63968..cdbd5d74 100644
--- a/lib/events/keyverificationevent.h
+++ b/lib/events/keyverificationevent.h
@@ -7,29 +7,33 @@
namespace Quotient {
+static constexpr auto SasV1Method = "m.sas.v1"_ls;
+
/// Requests a key verification with another user's devices.
/// Typically sent as a to-device event.
class QUOTIENT_API KeyVerificationRequestEvent : public Event {
public:
DEFINE_EVENT_TYPEID("m.key.verification.request", KeyVerificationRequestEvent)
- explicit KeyVerificationRequestEvent(const QJsonObject& obj);
+ explicit KeyVerificationRequestEvent(const QJsonObject& obj)
+ : Event(TypeId, obj)
+ {}
/// The device ID which is initiating the request.
- QString fromDevice() const;
+ QUO_CONTENT_GETTER(QString, fromDevice)
/// An opaque identifier for the verification request. Must
/// be unique with respect to the devices involved.
- QString transactionId() const;
+ QUO_CONTENT_GETTER(QString, transactionId)
/// The verification methods supported by the sender.
- QStringList methods() const;
+ 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.
- uint64_t timestamp() const;
+ QUO_CONTENT_GETTER(QDateTime, timestamp)
};
REGISTER_EVENT_TYPE(KeyVerificationRequestEvent)
@@ -37,16 +41,18 @@ class QUOTIENT_API KeyVerificationReadyEvent : public Event {
public:
DEFINE_EVENT_TYPEID("m.key.verification.ready", KeyVerificationReadyEvent)
- explicit KeyVerificationReadyEvent(const QJsonObject& obj);
+ explicit KeyVerificationReadyEvent(const QJsonObject& obj)
+ : Event(TypeId, obj)
+ {}
/// The device ID which is accepting the request.
- QString fromDevice() const;
+ QUO_CONTENT_GETTER(QString, fromDevice)
/// The transaction id of the verification request
- QString transactionId() const;
+ QUO_CONTENT_GETTER(QString, transactionId)
/// The verification methods supported by the sender.
- QStringList methods() const;
+ QUO_CONTENT_GETTER(QStringList, methods)
};
REGISTER_EVENT_TYPE(KeyVerificationReadyEvent)
@@ -56,39 +62,57 @@ class QUOTIENT_API KeyVerificationStartEvent : public Event {
public:
DEFINE_EVENT_TYPEID("m.key.verification.start", KeyVerificationStartEvent)
- explicit KeyVerificationStartEvent(const QJsonObject &obj);
+ explicit KeyVerificationStartEvent(const QJsonObject &obj)
+ : Event(TypeId, obj)
+ {}
/// The device ID which is initiating the process.
- QString fromDevice() const;
+ QUO_CONTENT_GETTER(QString, fromDevice)
/// An opaque identifier for the verification request. Must
/// be unique with respect to the devices involved.
- QString transactionId() const;
+ QUO_CONTENT_GETTER(QString, transactionId)
/// The verification method to use.
- QString method() const;
+ QUO_CONTENT_GETTER(QString, method)
/// Optional method to use to verify the other user's key with.
- Omittable<QString> nextMethod() const;
+ 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;
+ 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;
+ 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;
+ 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;
+ QString shortAuthenticationString() const
+ {
+ Q_ASSERT(method() == SasV1Method);
+ return contentPart<QString>("short_authentification_string"_ls);
+ }
};
REGISTER_EVENT_TYPE(KeyVerificationStartEvent)
@@ -98,33 +122,38 @@ class QUOTIENT_API KeyVerificationAcceptEvent : public Event {
public:
DEFINE_EVENT_TYPEID("m.key.verification.accept", KeyVerificationAcceptEvent)
- explicit KeyVerificationAcceptEvent(const QJsonObject& obj);
+ explicit KeyVerificationAcceptEvent(const QJsonObject& obj)
+ : Event(TypeId, obj)
+ {}
/// An opaque identifier for the verification process.
- QString transactionId() const;
+ QUO_CONTENT_GETTER(QString, transactionId)
/// The verification method to use. Must be 'm.sas.v1'.
- QString method() const;
+ 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.
- QString keyAgreementProtocol() const;
+ 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.
- QString hashData() const;
+ QString hashData() const
+ {
+ return contentPart<QString>("hash"_ls);
+ }
/// The message authentication code the device is choosing to use, out
/// of the options in the m.key.verification.start message.
- QString messageAuthenticationCode() const;
+ QUO_CONTENT_GETTER(QString, messageAuthenticationCode)
/// The SAS methods both devices involved in the verification process understand.
- QStringList shortAuthenticationString() const;
+ 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.
- QString commitment() const;
+ QUO_CONTENT_GETTER(QString, commitment)
};
REGISTER_EVENT_TYPE(KeyVerificationAcceptEvent)
@@ -132,17 +161,19 @@ class QUOTIENT_API KeyVerificationCancelEvent : public Event {
public:
DEFINE_EVENT_TYPEID("m.key.verification.cancel", KeyVerificationCancelEvent)
- explicit KeyVerificationCancelEvent(const QJsonObject &obj);
+ explicit KeyVerificationCancelEvent(const QJsonObject &obj)
+ : Event(TypeId, obj)
+ {}
/// An opaque identifier for the verification process.
- QString transactionId() const;
+ QUO_CONTENT_GETTER(QString, transactionId)
/// A human readable description of the code. The client should only
/// rely on this string if it does not understand the code.
- QString reason() const;
+ QUO_CONTENT_GETTER(QString, reason)
/// The error code for why the process/request was cancelled by the user.
- QString code() const;
+ QUO_CONTENT_GETTER(QString, code)
};
REGISTER_EVENT_TYPE(KeyVerificationCancelEvent)
@@ -152,13 +183,15 @@ class QUOTIENT_API KeyVerificationKeyEvent : public Event {
public:
DEFINE_EVENT_TYPEID("m.key.verification.key", KeyVerificationKeyEvent)
- explicit KeyVerificationKeyEvent(const QJsonObject &obj);
+ explicit KeyVerificationKeyEvent(const QJsonObject &obj)
+ : Event(TypeId, obj)
+ {}
- /// An opaque identifier for the verification process.
- QString transactionId() const;
+ /// An opaque identifier for the verification process.
+ QUO_CONTENT_GETTER(QString, transactionId)
/// The device's ephemeral public key, encoded as unpadded base64.
- QString key() const;
+ QUO_CONTENT_GETTER(QString, key)
};
REGISTER_EVENT_TYPE(KeyVerificationKeyEvent)
@@ -167,15 +200,20 @@ class QUOTIENT_API KeyVerificationMacEvent : public Event {
public:
DEFINE_EVENT_TYPEID("m.key.verification.mac", KeyVerificationMacEvent)
- explicit KeyVerificationMacEvent(const QJsonObject &obj);
+ explicit KeyVerificationMacEvent(const QJsonObject &obj)
+ : Event(TypeId, obj)
+ {}
- /// An opaque identifier for the verification process.
- QString transactionId() const;
+ /// An opaque identifier for the verification process.
+ QUO_CONTENT_GETTER(QString, transactionId)
/// The device's ephemeral public key, encoded as unpadded base64.
- QString keys() const;
+ QUO_CONTENT_GETTER(QString, keys)
- QHash<QString, QString> mac() const;
+ QHash<QString, QString> mac() const
+ {
+ return contentPart<QHash<QString, QString>>("mac"_ls);
+ }
};
REGISTER_EVENT_TYPE(KeyVerificationMacEvent)
@@ -183,10 +221,12 @@ class QUOTIENT_API KeyVerificationDoneEvent : public Event {
public:
DEFINE_EVENT_TYPEID("m.key.verification.done", KeyVerificationRequestEvent)
- explicit KeyVerificationDoneEvent(const QJsonObject& obj);
+ explicit KeyVerificationDoneEvent(const QJsonObject& obj)
+ : Event(TypeId, obj)
+ {}
/// The same transactionId as before
- QString transactionId() const;
+ QUO_CONTENT_GETTER(QString, transactionId)
};
REGISTER_EVENT_TYPE(KeyVerificationDoneEvent)
diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h
index be20bf52..63617e54 100644
--- a/lib/events/redactionevent.h
+++ b/lib/events/redactionevent.h
@@ -6,7 +6,7 @@
#include "roomevent.h"
namespace Quotient {
-class RedactionEvent : public RoomEvent {
+class QUOTIENT_API RedactionEvent : public RoomEvent {
public:
DEFINE_EVENT_TYPEID("m.room.redaction", RedactionEvent)
@@ -17,7 +17,7 @@ public:
{
return fullJson()["redacts"_ls].toString();
}
- QString reason() const { return contentPart<QString>("reason"_ls); }
+ QUO_CONTENT_GETTER(QString, reason)
};
REGISTER_EVENT_TYPE(RedactionEvent)
} // namespace Quotient
diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h
index c54b5801..af291696 100644
--- a/lib/events/roomavatarevent.h
+++ b/lib/events/roomavatarevent.h
@@ -26,10 +26,10 @@ public:
const QSize& imageSize = {},
const QString& originalFilename = {})
: RoomAvatarEvent(EventContent::ImageContent {
- mxcUrl, fileSize, mimeType, imageSize, none, originalFilename })
+ mxcUrl, fileSize, mimeType, imageSize, originalFilename })
{}
- 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 bb8654e5..60ca68ac 100644
--- a/lib/events/roomcanonicalaliasevent.h
+++ b/lib/events/roomcanonicalaliasevent.h
@@ -7,36 +7,31 @@
#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
+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 StateEvent<EventContent::AliasesEventContent> {
public:
DEFINE_EVENT_TYPEID("m.room.canonical_alias", RoomCanonicalAliasEvent)
diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp
index bb6de648..3b5024d5 100644
--- a/lib/events/roomcreateevent.cpp
+++ b/lib/events/roomcreateevent.cpp
@@ -6,20 +6,11 @@
using namespace Quotient;
template <>
-struct Quotient::JsonConverter<RoomType> {
- static RoomType load(const QJsonValue& jv)
- {
- const auto& roomTypeString = jv.toString();
- for (auto it = RoomTypeStrings.begin(); it != RoomTypeStrings.end();
- ++it)
- if (roomTypeString == *it)
- return RoomType(it - RoomTypeStrings.begin());
-
- if (!roomTypeString.isEmpty())
- qCWarning(EVENTS) << "Unknown Room Type: " << roomTypeString;
- return RoomType::Undefined;
- }
-};
+RoomType Quotient::fromJson(const QJsonValue& jv)
+{
+ return enumFromJsonString(jv.toString(), RoomTypeStrings,
+ RoomType::Undefined);
+}
bool RoomCreateEvent::isFederated() const
{
diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp
index 2f482871..e695e0ec 100644
--- a/lib/events/roomevent.cpp
+++ b/lib/events/roomevent.cpp
@@ -15,9 +15,9 @@ RoomEvent::RoomEvent(Type type, event_mtype_t matrixType,
RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json)
{
- if (const auto redaction = unsignedPart(RedactedCauseKeyL);
- redaction.isObject())
- _redactedBecause = makeEvent<RedactionEvent>(redaction.toObject());
+ if (const auto redaction = unsignedPart<QJsonObject>(RedactedCauseKeyL);
+ !redaction.isEmpty())
+ _redactedBecause = makeEvent<RedactionEvent>(redaction);
}
RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job
@@ -101,22 +101,22 @@ void RoomEvent::dumpTo(QDebug dbg) const
dbg << " (made at " << originTimestamp().toString(Qt::ISODate) << ')';
}
-QJsonObject makeCallContentJson(const QString& callId, int version,
- QJsonObject content)
+QJsonObject CallEventBase::basicJson(const QString& matrixType,
+ const QString& callId, int version,
+ QJsonObject contentJson)
{
- content.insert(QStringLiteral("call_id"), callId);
- content.insert(QStringLiteral("version"), version);
- return content;
+ contentJson.insert(QStringLiteral("call_id"), callId);
+ contentJson.insert(QStringLiteral("version"), version);
+ return RoomEvent::basicJson(matrixType, contentJson);
}
CallEventBase::CallEventBase(Type type, event_mtype_t matrixType,
const QString& callId, int version,
const QJsonObject& contentJson)
- : RoomEvent(type, matrixType,
- makeCallContentJson(callId, version, contentJson))
+ : RoomEvent(type, basicJson(matrixType, callId, version, contentJson))
{}
-CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json)
+CallEventBase::CallEventBase(Type type, const QJsonObject& json)
: RoomEvent(type, json)
{
if (callId().isEmpty())
diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h
index 5670f55f..9461340b 100644
--- a/lib/events/roomevent.h
+++ b/lib/events/roomevent.h
@@ -96,8 +96,13 @@ public:
~CallEventBase() override = default;
bool isCallEvent() const override { return true; }
- QString callId() const { return contentPart<QString>("call_id"_ls); }
- int version() const { return contentPart<int>("version"_ls); }
+ QUO_CONTENT_GETTER(QString, callId)
+ QUO_CONTENT_GETTER(int, version)
+
+protected:
+ static QJsonObject basicJson(const QString& matrixType,
+ const QString& callId, int version,
+ QJsonObject contentJson = {});
};
} // namespace Quotient
Q_DECLARE_METATYPE(Quotient::RoomEvent*)
diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h
index cb3fe7e7..9eb2854b 100644
--- a/lib/events/roomkeyevent.h
+++ b/lib/events/roomkeyevent.h
@@ -12,12 +12,17 @@ public:
DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent)
explicit RoomKeyEvent(const QJsonObject& obj);
- explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, const QString &sessionId, const QString& sessionKey, const QString& senderId);
+ explicit RoomKeyEvent(const QString& algorithm, const QString& roomId,
+ const QString& sessionId, const QString& sessionKey,
+ const QString& senderId);
- QString algorithm() const { return contentPart<QString>("algorithm"_ls); }
- QString roomId() const { return contentPart<QString>(RoomIdKeyL); }
- QString sessionId() const { return contentPart<QString>("session_id"_ls); }
- QString sessionKey() const { return contentPart<QString>("session_key"_ls); }
+ 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 b4770224..953ff8ae 100644
--- a/lib/events/roommemberevent.cpp
+++ b/lib/events/roommemberevent.cpp
@@ -4,8 +4,6 @@
#include "roommemberevent.h"
-#include "logging.h"
-
#include <QtCore/QtAlgorithms>
namespace Quotient {
@@ -13,18 +11,10 @@ template <>
struct JsonConverter<Membership> {
static Membership load(const QJsonValue& jv)
{
- const auto& ms = jv.toString();
- if (ms.isEmpty())
- {
- qCWarning(EVENTS) << "Empty membership state";
- return Membership::Invalid;
- }
- const auto it =
- std::find(MembershipStrings.begin(), MembershipStrings.end(), ms);
- if (it != MembershipStrings.end())
- return Membership(1U << (it - MembershipStrings.begin()));
-
- qCWarning(EVENTS) << "Unknown Membership value: " << ms;
+ if (const auto& ms = jv.toString(); !ms.isEmpty())
+ return flagFromJsonString<Membership>(ms, MembershipStrings);
+
+ qCWarning(EVENTS) << "Empty membership state";
return Membership::Invalid;
}
};
@@ -43,19 +33,19 @@ MemberEventContent::MemberEventContent(const QJsonObject& json)
displayName = sanitized(*displayName);
}
-void MemberEventContent::fillJson(QJsonObject* o) const
+QJsonObject MemberEventContent::toJson() const
{
- Q_ASSERT(o);
+ QJsonObject o;
if (membership != Membership::Invalid)
- o->insert(QStringLiteral("membership"),
- MembershipStrings[qCountTrailingZeroBits(
- std::underlying_type_t<Membership>(membership))]);
+ o.insert(QStringLiteral("membership"),
+ flagToJsonString(membership, MembershipStrings));
if (displayName)
- o->insert(QStringLiteral("displayname"), *displayName);
+ o.insert(QStringLiteral("displayname"), *displayName);
if (avatarUrl && avatarUrl->isValid())
- o->insert(QStringLiteral("avatar_url"), avatarUrl->toString());
+ 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
diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h
index ceb7826b..dd33ea6b 100644
--- a/lib/events/roommemberevent.h
+++ b/lib/events/roommemberevent.h
@@ -5,18 +5,18 @@
#pragma once
-#include "eventcontent.h"
#include "stateevent.h"
#include "quotient_common.h"
namespace Quotient {
-class QUOTIENT_API MemberEventContent : public EventContent::Base {
+class QUOTIENT_API MemberEventContent {
public:
using MembershipType
[[deprecated("Use Quotient::Membership instead")]] = Membership;
QUO_IMPLICIT MemberEventContent(Membership ms) : membership(ms) {}
explicit MemberEventContent(const QJsonObject& json);
+ QJsonObject toJson() const;
Membership membership;
/// (Only for invites) Whether the invite is to a direct chat
@@ -24,9 +24,6 @@ public:
Omittable<QString> displayName;
Omittable<QUrl> avatarUrl;
QString reason;
-
-protected:
- void fillJson(QJsonObject* o) const override;
};
using MembershipType [[deprecated("Use Membership instead")]] = Membership;
diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp
index d63352cb..2a6ae93c 100644
--- a/lib/events/roommessageevent.cpp
+++ b/lib/events/roommessageevent.cpp
@@ -148,21 +148,21 @@ TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile)
auto mimeTypeName = mimeType.name();
if (mimeTypeName.startsWith("image/"))
return new ImageContent(localUrl, file.size(), mimeType,
- QImageReader(filePath).size(), none,
+ QImageReader(filePath).size(),
file.fileName());
// duration can only be obtained asynchronously and can only be reliably
// done by starting to play the file. Left for a future implementation.
if (mimeTypeName.startsWith("video/"))
return new VideoContent(localUrl, file.size(), mimeType,
- QMediaResource(localUrl).resolution(), none,
+ QMediaResource(localUrl).resolution(),
file.fileName());
if (mimeTypeName.startsWith("audio/"))
- return new AudioContent(localUrl, file.size(), mimeType, none,
+ return new AudioContent(localUrl, file.size(), mimeType,
file.fileName());
}
- return new FileContent(localUrl, file.size(), mimeType, none, file.fileName());
+ return new FileContent(localUrl, file.size(), mimeType, file.fileName());
}
RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
@@ -302,17 +302,16 @@ TextContent::TextContent(const QJsonObject& json)
}
}
-void TextContent::fillJson(QJsonObject* json) const
+void TextContent::fillJson(QJsonObject &json) const
{
static const auto FormatKey = QStringLiteral("format");
- 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(
+ json.insert(
QStringLiteral("m.relates_to"),
relatesTo->type == EventRelation::ReplyType
? QJsonObject { { relatesTo->type,
@@ -326,7 +325,7 @@ void TextContent::fillJson(QJsonObject* json) const
newContentJson.insert(FormatKey, HtmlContentTypeId);
newContentJson.insert(FormattedBodyKey, body);
}
- json->insert(QStringLiteral("m.new_content"), newContentJson);
+ json.insert(QStringLiteral("m.new_content"), newContentJson);
}
}
}
@@ -347,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 03a51328..6968ad70 100644
--- a/lib/events/roommessageevent.h
+++ b/lib/events/roommessageevent.h
@@ -136,7 +136,7 @@ namespace EventContent {
Omittable<EventRelation> relatesTo;
protected:
- void fillJson(QJsonObject* json) const override;
+ void fillJson(QJsonObject& json) const override;
};
/**
@@ -164,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 QUOTIENT_API 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:
@@ -211,7 +208,7 @@ namespace EventContent {
* - mimeType
* - imageSize
*/
- using VideoContent = PlayableContent<UrlWithThumbnailContent<ImageInfo>>;
+ using VideoContent = PlayableContent<ImageInfo>;
/**
* Content class for m.audio
@@ -224,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 8d262ddf..d9bd010b 100644
--- a/lib/events/roompowerlevelsevent.cpp
+++ b/lib/events/roompowerlevelsevent.cpp
@@ -3,10 +3,10 @@
#include "roompowerlevelsevent.h"
-#include <QJsonDocument>
-
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)),
@@ -18,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 415cc814..a1638a27 100644
--- a/lib/events/roompowerlevelsevent.h
+++ b/lib/events/roompowerlevelsevent.h
@@ -3,17 +3,16 @@
#pragma once
-#include "eventcontent.h"
#include "stateevent.h"
namespace Quotient {
-class QUOTIENT_API 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;
@@ -29,9 +28,6 @@ public:
int usersDefault;
Notifications notifications;
-
-protected:
- void fillJson(QJsonObject* o) const override;
};
class QUOTIENT_API RoomPowerLevelsEvent
diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h
index 9610574b..a8eaab56 100644
--- a/lib/events/simplestateevents.h
+++ b/lib/events/simplestateevents.h
@@ -4,65 +4,51 @@
#pragma once
#include "stateevent.h"
+#include "single_key_value.h"
namespace Quotient {
-namespace EventContent {
- template <typename T>
- struct SimpleContent {
- 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) } };
- }
-
- T value;
- const QString key;
- };
-} // namespace EventContent
-
-#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \
- class QUOTIENT_API _Name \
- : public StateEvent<EventContent::SimpleContent<_ValueType>> { \
- public: \
- using value_type = content_type::value_type; \
- DEFINE_EVENT_TYPEID(_TypeId, _Name) \
- 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) \
+#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \
+ constexpr auto _Name##Key = #_ContentKey##_ls; \
+ class QUOTIENT_API _Name \
+ : public StateEvent< \
+ EventContent::SingleKeyValue<_ValueType, &_Name##Key>> { \
+ public: \
+ using value_type = _ValueType; \
+ DEFINE_EVENT_TYPEID(_TypeId, _Name) \
+ template <typename T> \
+ explicit _Name(T&& value) \
+ : StateEvent(TypeId, matrixTypeId(), QString(), \
+ std::forward<T>(value)) \
+ {} \
+ explicit _Name(QJsonObject obj) \
+ : StateEvent(TypeId, std::move(obj)) \
+ {} \
+ auto _ContentKey() const { return content().value; } \
+ }; \
+ REGISTER_EVENT_TYPE(_Name) \
// End of macro
DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name)
DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic)
-DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", QStringList, pinnedEvents)
+DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages",
+ QStringList, pinnedEvents)
-class [[deprecated(
- "m.room.aliases events are deprecated by the Matrix spec; use"
- " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases")]] //
-RoomAliasesEvent : public StateEvent<EventContent::SimpleContent<QStringList>> {
+constexpr auto RoomAliasesEventKey = "aliases"_ls;
+class QUOTIENT_API RoomAliasesEvent
+ : public StateEvent<
+ EventContent::SingleKeyValue<QStringList, &RoomAliasesEventKey>> {
public:
DEFINE_EVENT_TYPEID("m.room.aliases", RoomAliasesEvent)
explicit RoomAliasesEvent(const QJsonObject& obj)
- : StateEvent(typeId(), obj, QStringLiteral("aliases"))
+ : StateEvent(typeId(), obj)
{}
+ 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; }
};
} // namespace Quotient
diff --git a/lib/events/single_key_value.h b/lib/events/single_key_value.h
new file mode 100644
index 00000000..75ca8cd2
--- /dev/null
+++ b/lib/events/single_key_value.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "converters.h"
+
+namespace Quotient {
+
+namespace EventContent {
+ template <typename T, const QLatin1String* KeyStr>
+ struct SingleKeyValue {
+ 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 e53d47d4..1df24df0 100644
--- a/lib/events/stateevent.cpp
+++ b/lib/events/stateevent.cpp
@@ -6,9 +6,9 @@
using namespace Quotient;
StateEventBase::StateEventBase(Type type, const QJsonObject& json)
- : RoomEvent(json.contains(StateKeyKeyL) ? type : unknownEventTypeId(), json)
+ : RoomEvent(json.contains(StateKeyKeyL) ? type : UnknownEventTypeId, json)
{
- if (Event::type() == unknownEventTypeId() && !json.contains(StateKeyKeyL))
+ if (Event::type() == UnknownEventTypeId && !json.contains(StateKeyKeyL))
qWarning(EVENTS) << "Attempt to create a state event with no stateKey -"
"forcing the event type to unknown to avoid damage";
}
@@ -16,12 +16,12 @@ StateEventBase::StateEventBase(Type type, const QJsonObject& json)
StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType,
const QString& stateKey,
const QJsonObject& contentJson)
- : RoomEvent(type, basicStateEventJson(matrixType, contentJson, stateKey))
+ : RoomEvent(type, basicJson(type, stateKey, contentJson))
{}
bool StateEventBase::repeatsState() const
{
- const auto prevContentJson = unsignedPart(PrevContentKeyL);
+ const auto prevContentJson = unsignedPart<QJsonObject>(PrevContentKeyL);
return fullJson().value(ContentKeyL) == prevContentJson;
}
diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h
index 88da68f8..9f1d7118 100644
--- a/lib/events/stateevent.h
+++ b/lib/events/stateevent.h
@@ -7,16 +7,6 @@
namespace Quotient {
-/// Make a minimal correct Matrix state event JSON
-inline QJsonObject basicStateEventJson(const QString& matrixTypeId,
- const QJsonObject& content,
- const QString& stateKey = {})
-{
- return { { TypeKey, matrixTypeId },
- { StateKeyKey, stateKey },
- { ContentKey, content } };
-}
-
class QUOTIENT_API StateEventBase : public RoomEvent {
public:
static inline EventFactory<StateEventBase> factory { "StateEvent" };
@@ -27,6 +17,16 @@ public:
const QJsonObject& contentJson = {});
~StateEventBase() override = default;
+ //! 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;
@@ -36,6 +36,14 @@ public:
using StateEventPtr = event_ptr_tt<StateEventBase>;
using StateEvents = EventsArray<StateEventBase>;
+[[deprecated("Use StateEventBase::basicJson() instead")]]
+inline QJsonObject basicStateEventJson(const QString& matrixTypeId,
+ const QJsonObject& content,
+ const QString& stateKey = {})
+{
+ return StateEventBase::basicJson(matrixTypeId, stateKey, content);
+}
+
//! \brief Override RoomEvent factory with that from StateEventBase if JSON has
//! stateKey
//!
@@ -64,7 +72,7 @@ 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>;
+using StateEventKey = std::pair<QString, QString>;
template <typename ContentT>
struct Prev {
@@ -72,7 +80,7 @@ struct Prev {
explicit Prev(const QJsonObject& unsignedJson,
ContentParamTs&&... contentParams)
: senderId(unsignedJson.value("prev_sender"_ls).toString())
- , content(unsignedJson.value(PrevContentKeyL).toObject(),
+ , content(fromJson<ContentT>(unsignedJson.value(PrevContentKeyL)),
std::forward<ContentParamTs>(contentParams)...)
{}
@@ -89,7 +97,8 @@ public:
explicit StateEvent(Type type, const QJsonObject& fullJson,
ContentParamTs&&... contentParams)
: StateEventBase(type, fullJson)
- , _content(contentJson(), std::forward<ContentParamTs>(contentParams)...)
+ , _content(fromJson<ContentT>(contentJson()),
+ std::forward<ContentParamTs>(contentParams)...)
{
const auto& unsignedData = unsignedJson();
if (unsignedData.contains(PrevContentKeyL))
@@ -101,9 +110,9 @@ public:
const QString& stateKey,
ContentParamTs&&... contentParams)
: StateEventBase(type, matrixType, stateKey)
- , _content(std::forward<ContentParamTs>(contentParams)...)
+ , _content{std::forward<ContentParamTs>(contentParams)...}
{
- editJson().insert(ContentKey, _content.toJson());
+ editJson().insert(ContentKey, toJson(_content));
}
const ContentT& content() const { return _content; }
@@ -111,7 +120,7 @@ public:
void editContent(VisitorT&& visitor)
{
visitor(_content);
- editJson()[ContentKeyL] = _content.toJson();
+ editJson()[ContentKeyL] = toJson(_content);
}
const ContentT* prevContent() const
{
diff --git a/lib/events/stickerevent.cpp b/lib/events/stickerevent.cpp
deleted file mode 100644
index 628fd154..00000000
--- a/lib/events/stickerevent.cpp
+++ /dev/null
@@ -1,26 +0,0 @@
-// SDPX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#include "stickerevent.h"
-
-using namespace Quotient;
-
-StickerEvent::StickerEvent(const QJsonObject &obj)
- : RoomEvent(typeId(), obj)
- , m_imageContent(EventContent::ImageContent(obj["content"_ls].toObject()))
-{}
-
-QString StickerEvent::body() const
-{
- return contentPart<QString>("body"_ls);
-}
-
-const EventContent::ImageContent &StickerEvent::image() const
-{
- return m_imageContent;
-}
-
-QUrl StickerEvent::url() const
-{
- return m_imageContent.url;
-}
diff --git a/lib/events/stickerevent.h b/lib/events/stickerevent.h
index 0957dca3..e378422d 100644
--- a/lib/events/stickerevent.h
+++ b/lib/events/stickerevent.h
@@ -16,21 +16,32 @@ class QUOTIENT_API StickerEvent : public RoomEvent
public:
DEFINE_EVENT_TYPEID("m.sticker", StickerEvent)
- explicit StickerEvent(const QJsonObject &obj);
+ 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.
- QString body() const;
+ QUO_CONTENT_GETTER(QString, body)
/// \brief Metadata about the image referred to in url including a
/// thumbnail representation.
- const EventContent::ImageContent &image() const;
+ 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;
+ QUrl url() const
+ {
+ return m_imageContent.url();
+ }
+
private:
EventContent::ImageContent m_imageContent;
};
diff --git a/lib/expected.h b/lib/expected.h
new file mode 100644
index 00000000..7b9e7f1d
--- /dev/null
+++ b/lib/expected.h
@@ -0,0 +1,77 @@
+// 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;
+ explicit Expected(const Expected&) = default;
+ explicit Expected(Expected&&) noexcept = default;
+
+ template <typename X, typename = enable_if_constructible_t<X>>
+ Expected(X&& x)
+ : 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
index 6542101a..e3d27122 100644
--- a/lib/function_traits.cpp
+++ b/lib/function_traits.cpp
@@ -7,6 +7,9 @@
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<>");
diff --git a/lib/function_traits.h b/lib/function_traits.h
index 83b8e425..143ed162 100644
--- a/lib/function_traits.h
+++ b/lib/function_traits.h
@@ -8,7 +8,7 @@
namespace Quotient {
namespace _impl {
- template <typename AlwaysVoid, typename>
+ template <typename>
struct fn_traits {};
}
@@ -21,73 +21,73 @@ namespace _impl {
*/
template <typename T>
struct function_traits
- : public _impl::fn_traits<void, std::remove_reference_t<T>> {};
+ : 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...>;
- // See also the comment for wrap_in_function() in qt_connection_util.h
- using function_type = std::function<ReturnT(ArgTs...)>;
};
namespace _impl {
- template <typename AlwaysVoid, typename>
+ template <typename>
struct fn_object_traits;
// Specialisation for a lambda function
template <typename ReturnT, typename ClassT, typename... ArgTs>
- struct fn_object_traits<void, ReturnT (ClassT::*)(ArgTs...)>
+ 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<void, ReturnT (ClassT::*)(ArgTs...) const>
+ 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>
- struct fn_traits<decltype(void(&T::operator())), T>
- : public fn_object_traits<void, decltype(&T::operator())> {};
+ 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<void, ReturnT (ClassT::*)(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<void, ReturnT (ClassT::*)(ArgTs...) const>
+ 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<void, ReturnT (ClassT::*)(ArgTs...) const&>
+ 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<void, ReturnT (ClassT::*)(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<void, ReturnT 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<void, const ReturnT ClassT::*>
+ struct fn_traits<const ReturnT ClassT::*>
: function_traits<const ReturnT&(ClassT)> {};
} // namespace _impl
-template <typename FnT>
-using fn_return_t = typename function_traits<FnT>::return_type;
-
template <typename FnT, int ArgN = 0>
using fn_arg_t =
std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>;
+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 b6858b5a..da645a2d 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -138,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;
@@ -152,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)
@@ -301,16 +298,10 @@ void BaseJob::Private::sendRequest()
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());
@@ -754,11 +745,14 @@ QString BaseJob::statusCaption() const
}
}
-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)
{
diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp
index d00fc5f4..759d52c9 100644
--- a/lib/jobs/downloadfilejob.cpp
+++ b/lib/jobs/downloadfilejob.cpp
@@ -8,8 +8,9 @@
#include <QtNetwork/QNetworkReply>
#ifdef Quotient_E2EE_ENABLED
-# include <QtCore/QCryptographicHash>
-# include "events/encryptedfile.h"
+# include "events/filesourceinfo.h"
+
+# include <QtCore/QCryptographicHash>
#endif
using namespace Quotient;
@@ -26,7 +27,7 @@ public:
QScopedPointer<QFile> tempFile;
#ifdef Quotient_E2EE_ENABLED
- Omittable<EncryptedFile> encryptedFile;
+ Omittable<EncryptedFileMetadata> encryptedFileMetadata;
#endif
};
@@ -49,14 +50,14 @@ DownloadFileJob::DownloadFileJob(const QString& serverName,
#ifdef Quotient_E2EE_ENABLED
DownloadFileJob::DownloadFileJob(const QString& serverName,
const QString& mediaId,
- const EncryptedFile& file,
+ const EncryptedFileMetadata& file,
const QString& localFilename)
: GetContentJob(serverName, mediaId)
, d(localFilename.isEmpty() ? makeImpl<Private>()
: makeImpl<Private>(localFilename))
{
setObjectName(QStringLiteral("DownloadFileJob"));
- d->encryptedFile = file;
+ d->encryptedFileMetadata = file;
}
#endif
QString DownloadFileJob::targetFileName() const
@@ -118,27 +119,31 @@ 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) {
#ifdef Quotient_E2EE_ENABLED
- if (d->encryptedFile.has_value()) {
- d->tempFile->seek(0);
- QByteArray encrypted = d->tempFile->readAll();
-
- EncryptedFile file = *d->encryptedFile;
- const auto decrypted = file.decryptFile(encrypted);
- d->targetFile->write(decrypted);
+ if (d->encryptedFileMetadata.has_value()) {
+ decryptFile(*d->tempFile, *d->encryptedFileMetadata, *d->targetFile);
d->tempFile->remove();
} else {
#endif
d->targetFile->close();
if (!d->targetFile->remove()) {
- qCWarning(JOBS) << "Failed to remove the target file placeholder";
+ qWarning(JOBS) << "Failed to remove the target file placeholder";
return { FileError, "Couldn't finalise the download" };
}
if (!d->tempFile->rename(d->targetFile->fileName())) {
- qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName()
+ qWarning(JOBS) << "Failed to rename" << d->tempFile->fileName()
<< "to" << d->targetFile->fileName();
return { FileError, "Couldn't finalise the download" };
}
@@ -147,13 +152,20 @@ BaseJob::Status DownloadFileJob::prepareResult()
#endif
} else {
#ifdef Quotient_E2EE_ENABLED
- if (d->encryptedFile.has_value()) {
- d->tempFile->seek(0);
- const auto encrypted = d->tempFile->readAll();
-
- EncryptedFile file = *d->encryptedFile;
- const auto decrypted = file.decryptFile(encrypted);
- d->tempFile->write(decrypted);
+ 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();
@@ -161,6 +173,6 @@ BaseJob::Status DownloadFileJob::prepareResult()
}
#endif
}
- qCDebug(JOBS) << "Saved a file as" << targetFileName();
+ qDebug(JOBS) << "Saved a file as" << targetFileName();
return Success;
}
diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h
index ffa3d055..cbbfd244 100644
--- a/lib/jobs/downloadfilejob.h
+++ b/lib/jobs/downloadfilejob.h
@@ -4,7 +4,8 @@
#pragma once
#include "csapi/content-repo.h"
-#include "events/encryptedfile.h"
+
+#include "events/filesourceinfo.h"
namespace Quotient {
class QUOTIENT_API DownloadFileJob : public GetContentJob {
@@ -16,7 +17,7 @@ public:
const QString& localFilename = {});
#ifdef Quotient_E2EE_ENABLED
- DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFile& file, const QString& localFilename = {});
+ DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFileMetadata& file, const QString& localFilename = {});
#endif
QString targetFileName() const;
diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp
index 2c001ccc..ab249f6d 100644
--- a/lib/jobs/requestdata.cpp
+++ b/lib/jobs/requestdata.cpp
@@ -14,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;
@@ -33,7 +33,5 @@ RequestData::RequestData(const QJsonObject& jo) : _source(fromJson(jo)) {}
RequestData::RequestData(const QJsonArray& ja) : _source(fromJson(ja)) {}
RequestData::RequestData(QIODevice* source)
- : _source(std::unique_ptr<QIODevice>(source))
+ : _source(acquireImpl(source))
{}
-
-RequestData::~RequestData() = default;
diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h
index 41ad833a..accc8f71 100644
--- a/lib/jobs/requestdata.h
+++ b/lib/jobs/requestdata.h
@@ -3,11 +3,7 @@
#pragma once
-#include "quotient_export.h"
-
-#include <QtCore/QByteArray>
-
-#include <memory>
+#include "util.h"
class QJsonObject;
class QJsonArray;
@@ -23,17 +19,17 @@ namespace Quotient {
*/
class QUOTIENT_API RequestData {
public:
- RequestData(const QByteArray& a = {});
- RequestData(const QJsonObject& jo);
- RequestData(const QJsonArray& ja);
- RequestData(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
diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h
index 830a7c71..b7bfbbb3 100644
--- a/lib/jobs/syncjob.h
+++ b/lib/jobs/syncjob.h
@@ -15,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/keyverificationsession.cpp b/lib/keyverificationsession.cpp
index 3b3b7627..d889b465 100644
--- a/lib/keyverificationsession.cpp
+++ b/lib/keyverificationsession.cpp
@@ -23,8 +23,10 @@ KeyVerificationSession::KeyVerificationSession(const QString& remoteUserId, cons
, m_encrypted(encrypted)
, m_remoteSupportedMethods(event.methods())
{
- auto timeoutTime = std::min<long int>(event.timestamp() + 600000, QDateTime::currentDateTime().addSecs(120).toMSecsSinceEpoch());
- m_timeout = timeoutTime - QDateTime::currentMSecsSinceEpoch();
+ auto timeoutTime = std::min(event.timestamp().addSecs(600),
+ QDateTime::currentDateTime().addSecs(120));
+ m_timeout =
+ timeoutTime.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch();
if (m_timeout <= 5000) {
return;
}
diff --git a/lib/logging.h b/lib/logging.h
index fc0a4c99..1fafa04b 100644
--- a/lib/logging.h
+++ b/lib/logging.h
@@ -44,12 +44,7 @@ inline QDebug formatJson(QDebug debug_object)
//! Suppress full qualification of enums/QFlags when logging
inline QDebug terse(QDebug dbg)
{
- return
-#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
- dbg.setVerbosity(0), dbg;
-#else
- dbg.verbosity(QDebug::MinimumVerbosity);
-#endif
+ return dbg.verbosity(QDebug::MinimumVerbosity);
}
inline qint64 profilerMinNsecs()
@@ -74,15 +69,13 @@ inline qint64 profilerMinNsecs()
*/
inline QDebug operator<<(QDebug debug_object, Quotient::QDebugManip qdm)
{
- return qdm(debug_object);
+ return qdm(debug_object); // NOLINT(performance-unnecessary-value-param)
}
-inline QDebug operator<<(QDebug debug_object, const QElapsedTimer& et)
+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
index b757bb93..c7547be8 100644
--- a/lib/mxcreply.cpp
+++ b/lib/mxcreply.cpp
@@ -5,11 +5,10 @@
#include <QtCore/QBuffer>
#include "accountregistry.h"
-#include "connection.h"
#include "room.h"
#ifdef Quotient_E2EE_ENABLED
-#include "events/encryptedfile.h"
+#include "events/filesourceinfo.h"
#endif
using namespace Quotient;
@@ -21,7 +20,7 @@ public:
: m_reply(r)
{}
QNetworkReply* m_reply;
- Omittable<EncryptedFile> m_encryptedFile;
+ Omittable<EncryptedFileMetadata> m_encryptedFile;
QIODevice* m_device = nullptr;
};
@@ -48,9 +47,9 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId)
if(!d->m_encryptedFile.has_value()) {
d->m_device = d->m_reply;
} else {
- EncryptedFile file = *d->m_encryptedFile;
auto buffer = new QBuffer(this);
- buffer->setData(file.decryptFile(d->m_reply->readAll()));
+ buffer->setData(
+ decryptFile(d->m_reply->readAll(), *d->m_encryptedFile));
buffer->open(ReadOnly);
d->m_device = buffer;
}
@@ -65,17 +64,13 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId)
auto eventIt = room->findInTimeline(eventId);
if(eventIt != room->historyEdge()) {
auto event = eventIt->viewAs<RoomMessageEvent>();
- d->m_encryptedFile = event->content()->fileInfo()->file;
+ if (auto* efm = std::get_if<EncryptedFileMetadata>(
+ &event->content()->fileInfo()->source))
+ d->m_encryptedFile = *efm;
}
#endif
}
-#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
-#define ERROR_SIGNAL errorOccurred
-#else
-#define ERROR_SIGNAL error
-#endif
-
MxcReply::MxcReply()
: d(ZeroImpl<Private>())
{
@@ -87,7 +82,7 @@ MxcReply::MxcReply()
setError(QNetworkReply::ProtocolInvalidOperationError,
BadRequestPhrase);
setFinished(true);
- emit ERROR_SIGNAL(QNetworkReply::ProtocolInvalidOperationError);
+ emit errorOccurred(QNetworkReply::ProtocolInvalidOperationError);
emit finished();
}, Qt::QueuedConnection);
}
diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp
index f4e7b1af..38ab07cc 100644
--- a/lib/networkaccessmanager.cpp
+++ b/lib/networkaccessmanager.cpp
@@ -68,24 +68,11 @@ void NetworkAccessManager::clearIgnoredSslErrors()
d->ignoredSslErrors.clear();
}
-static NetworkAccessManager* createNam()
-{
- auto nam = new NetworkAccessManager();
-#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
- // See #109; in newer Qt, bearer management is deprecated altogether
- NetworkAccessManager::connect(nam,
- &QNetworkAccessManager::networkAccessibleChanged, [nam] {
- nam->setNetworkAccessible(QNetworkAccessManager::Accessible);
- });
-#endif
- return nam;
-}
-
NetworkAccessManager* NetworkAccessManager::instance()
{
static QThreadStorage<NetworkAccessManager*> storage;
if(!storage.hasLocalData()) {
- storage.setLocalData(createNam());
+ storage.setLocalData(new NetworkAccessManager());
}
return storage.localData();
}
diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h
index 86593cc8..ef7f6f80 100644
--- a/lib/qt_connection_util.h
+++ b/lib/qt_connection_util.h
@@ -9,101 +9,67 @@
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)
{
- auto pc = std::make_unique<QMetaObject::Connection>();
- 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);
- }
- // TODO: get rid of it as soon as Apple Clang gets proper deduction guides
- // for std::function<>
- // ...or consider using QtPrivate magic used by QObject::connect()
- // ...for inspiration, also check a possible std::not_fn implementation
- // at https://en.cppreference.com/w/cpp/utility/functional/not_fn
- template <typename FnT>
- inline auto wrap_in_function(FnT&& f)
- {
- return typename function_traits<FnT>::function_type(std::forward<FnT>(f));
- }
+ 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, _impl::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)
{
#if QT_VERSION_MAJOR >= 6
@@ -111,25 +77,26 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal,
Qt::ConnectionType(connType
| Qt::SingleShotConnection));
#else
- return _impl::connectSingleShot(
- sender, signal, context, _impl::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,
- _impl::wrap_in_function(
- [receiver, slot](const ArgTs&... args) {
- (receiver->*slot)(args...);
- }),
- connType);
+ // 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
}
diff --git a/lib/quotient_common.h b/lib/quotient_common.h
index 2b785a39..7fec9274 100644
--- a/lib/quotient_common.h
+++ b/lib/quotient_common.h
@@ -41,44 +41,8 @@
Q_ENUM_NS_IMPL(Enum) \
Q_FLAG_NS(Flags)
-// Apple Clang hasn't caught up with explicit(bool) yet
-#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
-
-#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
-// The first line forward-declares the namespace static metaobject with
-// QUOTIENT_API so that dynamically linked clients could serialise flag/enum
-// values from the namespace; Qt before 5.14 doesn't help with that. The second
-// line is needed for moc to do its job on the namespace.
-#define QUO_NAMESPACE \
- extern QUOTIENT_API const QMetaObject staticMetaObject; \
- Q_NAMESPACE
-#else
-// Since Qt 5.14.0, it's all packed in a single macro
-#define QUO_NAMESPACE Q_NAMESPACE_EXPORT(QUOTIENT_API)
-#endif
-
namespace Quotient {
-QUO_NAMESPACE
-
-// std::array {} needs explicit template parameters on macOS because
-// Apple stdlib doesn't have deduction guides for std::array. C++20 has
-// to_array() but that can't be borrowed, this time because of MSVC:
-// https://developercommunity.visualstudio.com/t/vc-ice-p1-initc-line-3652-from-stdto-array/1464038
-// Therefore a simpler (but also slightly more wobbly - it resolves the element
-// type using std::common_type<>) make_array facility is implemented here.
-template <typename... Ts>
-constexpr auto make_array(Ts&&... items)
-{
- return std::array<std::common_type_t<Ts...>, sizeof...(items)>(
- { std::forward<Ts>(items)... });
-}
+Q_NAMESPACE_EXPORT(QUOTIENT_API)
// TODO: code like this should be generated from the CS API definition
@@ -87,7 +51,7 @@ constexpr auto make_array(Ts&&... items)
//! These are used for member events. The names here are case-insensitively
//! equal to state names used on the wire.
//! \sa MemberEventContent, RoomMemberEvent
-enum class Membership : unsigned int {
+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,
@@ -100,9 +64,10 @@ enum class Membership : unsigned int {
};
QUO_DECLARE_FLAGS_NS(MembershipMask, Membership)
-constexpr auto MembershipStrings = make_array(
- // The order MUST be the same as the order in the original enum
- "join", "leave", "invite", "knock", "ban");
+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
//!
@@ -118,10 +83,10 @@ enum class JoinState : std::underlying_type_t<Membership> {
};
QUO_DECLARE_FLAGS_NS(JoinStates, JoinState)
-[[maybe_unused]] constexpr auto JoinStateStrings = make_array(
+[[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
//!
@@ -132,7 +97,7 @@ Q_ENUM_NS(RunningPolicy)
//! \brief The result of URI resolution using UriResolver
//! \sa UriResolver
-enum UriResolveResult : short {
+enum UriResolveResult : int8_t {
StillResolving = -1,
UriResolved = 0,
CouldNotResolve,
@@ -142,13 +107,19 @@ enum UriResolveResult : short {
};
Q_ENUM_NS(UriResolveResult)
-enum RoomType {
- Space,
- Undefined,
+enum class RoomType : uint8_t {
+ Space = 0,
+ Undefined = 0xFF,
};
Q_ENUM_NS(RoomType)
-[[maybe_unused]] constexpr auto RoomTypeStrings = make_array("m.space");
+[[maybe_unused]] constexpr std::array RoomTypeStrings { "m.space" };
+
+enum class EncryptionType : uint8_t {
+ MegolmV1AesSha2 = 0,
+ Undefined = 0xFF,
+};
+Q_ENUM_NS(EncryptionType)
} // namespace Quotient
Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask)
diff --git a/lib/room.cpp b/lib/room.cpp
index a423e04f..3ee81dcc 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -12,7 +12,6 @@
#include "avatar.h"
#include "connection.h"
#include "converters.h"
-#include "e2ee/qolmoutboundsession.h"
#include "syncdata.h"
#include "user.h"
#include "eventstats.h"
@@ -118,7 +117,7 @@ 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;
QHash<QString, Notification> notifications;
@@ -219,8 +218,9 @@ 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<StateEventBase>(evtKey.first,
+ evtKey.second));
qCDebug(STATE) << "A new stub event created for key {"
<< evtKey.first << evtKey.second << "}";
qCDebug(STATE) << "Stubbed state size:" << stubbedState.size();
@@ -277,10 +277,17 @@ public:
* Remove events from the passed container that are already in the timeline
*/
void dropDuplicateEvents(RoomEvents& events) const;
-
- Changes setLastReadReceipt(const QString& userId, rev_iter_t newMarker,
- ReadReceipt newReceipt = {},
- bool deferStatsUpdate = false);
+ 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 updateStats(const rev_iter_t& from, const rev_iter_t& to);
bool markMessagesAsRead(const rev_iter_t& upToMarker);
@@ -340,17 +347,21 @@ public:
#ifdef Quotient_E2EE_ENABLED
UnorderedMap<QString, QOlmInboundGroupSessionPtr> groupSessions;
-
+ int currentMegolmSessionMessageCount = 0;
+ //TODO save this to database
+ unsigned long long currentMegolmSessionCreationTimestamp = 0;
QOlmOutboundGroupSessionPtr currentOutboundMegolmSession = nullptr;
- bool addInboundGroupSession(QString sessionId, QString sessionKey, const QString& senderId, const QString& olmSessionId)
+ bool addInboundGroupSession(QString sessionId, QByteArray sessionKey,
+ const QString& senderId,
+ const QString& olmSessionId)
{
- if (groupSessions.find(sessionId) != groupSessions.end()) {
+ if (groupSessions.contains(sessionId)) {
qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "already exists";
return false;
}
- auto megolmSession = QOlmInboundGroupSession::create(sessionKey.toLatin1());
+ auto megolmSession = QOlmInboundGroupSession::create(sessionKey);
if (megolmSession->sessionId() != sessionId) {
qCWarning(E2EE) << "Session ID mismatch in m.room_key event";
return false;
@@ -358,13 +369,12 @@ public:
megolmSession->setSenderId(senderId);
megolmSession->setOlmSessionId(olmSessionId);
qCWarning(E2EE) << "Adding inbound session";
- connection->saveMegolmSession(q, megolmSession.get());
+ 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,
@@ -375,7 +385,7 @@ public:
// qCWarning(E2EE) << "Unable to decrypt event" << eventId
// << "The sender's device has not sent us the keys for "
// "this message";
- return QString();
+ return {};
}
auto& senderSession = groupSessionIt->second;
if (senderSession->senderId() != senderId) {
@@ -383,19 +393,24 @@ public:
return {};
}
auto decryptResult = senderSession->decrypt(cipher);
- if(std::holds_alternative<QOlmError>(decryptResult)) {
+ if(!decryptResult) {
qCWarning(E2EE) << "Unable to decrypt event" << eventId
- << "with matching megolm session:" << std::get<QOlmError>(decryptResult);
- return QString();
+ << "with matching megolm session:" << decryptResult.error();
+ return {};
}
- const auto& [content, index] = std::get<std::pair<QString, uint32_t>>(decryptResult);
- const auto& [recordEventId, ts] = q->connection()->database()->groupSessionIndexRecord(q->id(), senderSession->sessionId(), index);
+ 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());
+ q->connection()->database()->addGroupSessionIndexRecord(
+ q->id(), senderSession->sessionId(), index, eventId,
+ timestamp.toMSecsSinceEpoch());
} else {
- if ((eventId != recordEventId) || (ts != timestamp.toMSecsSinceEpoch())) {
+ if ((eventId != recordEventId)
+ || (ts != timestamp.toMSecsSinceEpoch())) {
qCWarning(E2EE) << "Detected a replay attack on event" << eventId;
- return QString();
+ return {};
}
}
return content;
@@ -403,10 +418,17 @@ public:
bool shouldRotateMegolmSession() const
{
- if (!q->usesEncryption()) {
+ const auto* encryptionConfig = currentState.get<EncryptionEvent>();
+ if (!encryptionConfig || !encryptionConfig->useEncryption())
return false;
- }
- return currentOutboundMegolmSession->messageCount() >= rotationMessageCount() || currentOutboundMegolmSession->creationTime().addMSecs(rotationInterval()) < QDateTime::currentDateTime();
+
+ const auto rotationInterval = encryptionConfig->rotationPeriodMs();
+ const auto rotationMessageCount = encryptionConfig->rotationPeriodMsgs();
+ return currentOutboundMegolmSession->messageCount()
+ >= rotationMessageCount
+ || currentOutboundMegolmSession->creationTime().addMSecs(
+ rotationInterval)
+ < QDateTime::currentDateTime();
}
bool hasValidMegolmSession() const
@@ -417,137 +439,45 @@ public:
return currentOutboundMegolmSession != nullptr;
}
- /// Time in milliseconds after which the outgoing megolmsession should be replaced
- unsigned int rotationInterval() const
- {
- if (!q->usesEncryption()) {
- return 0;
- }
- return q->getCurrentState<EncryptionEvent>()->rotationPeriodMs();
- }
-
- // Number of messages sent by this user after which the outgoing megolm session should be replaced
- int rotationMessageCount() const
- {
- if (!q->usesEncryption()) {
- return 0;
- }
- return q->getCurrentState<EncryptionEvent>()->rotationPeriodMsgs();
- }
void createMegolmSession() {
- qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->id();
+ qCDebug(E2EE) << "Creating new outbound megolm session for room "
+ << q->objectName();
currentOutboundMegolmSession = QOlmOutboundGroupSession::create();
- connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession);
+ connection->saveCurrentOutboundMegolmSession(
+ id, *currentOutboundMegolmSession);
const auto sessionKey = currentOutboundMegolmSession->sessionKey();
- if(std::holds_alternative<QOlmError>(sessionKey)) {
+ if(!sessionKey) {
qCWarning(E2EE) << "Failed to load key for new megolm session";
return;
}
- addInboundGroupSession(currentOutboundMegolmSession->sessionId(), std::get<QByteArray>(sessionKey), q->localUser()->id(), "SELF"_ls);
+ addInboundGroupSession(currentOutboundMegolmSession->sessionId(), *sessionKey, q->localUser()->id(), "SELF"_ls);
}
- std::unique_ptr<EncryptedEvent> payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey)
+ QMultiHash<QString, QString> getDevicesWithoutKey() const
{
- // Noisy but nice for debugging
- //qCDebug(E2EE) << "Creating the payload for" << user->id() << device << sessionId << sessionKey.toHex();
- const auto event = makeEvent<RoomKeyEvent>("m.megolm.v1.aes-sha2", q->id(), sessionId, sessionKey, q->localUser()->id());
- QJsonObject payloadJson = event->fullJson();
- payloadJson["recipient"] = user->id();
- payloadJson["sender"] = connection->user()->id();
- QJsonObject recipientObject;
- recipientObject["ed25519"] = connection->edKeyForUserDevice(user->id(), device);
- payloadJson["recipient_keys"] = recipientObject;
- QJsonObject senderObject;
- senderObject["ed25519"] = QString(connection->olmAccount()->identityKeys().ed25519);
- payloadJson["keys"] = senderObject;
- payloadJson["sender_device"] = connection->deviceId();
- auto cipherText = connection->olmEncryptMessage(user, device, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact));
- QJsonObject encrypted;
- encrypted[connection->curveKeyForUserDevice(user->id(), device)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}};
-
- return makeEvent<EncryptedEvent>(encrypted, connection->olmAccount()->identityKeys().curve25519);
- }
-
- QHash<User*, QStringList> getDevicesWithoutKey() const
- {
- QHash<User*, QStringList> devices;
- auto rawDevices = q->connection()->database()->devicesWithoutKey(q, QString(currentOutboundMegolmSession->sessionId()));
- for (const auto& user : rawDevices.keys()) {
- devices[q->connection()->user(user)] = rawDevices[user];
- }
- return devices;
- }
+ QMultiHash<QString, QString> devices;
+ for (const auto& user : q->users())
+ for (const auto& deviceId : connection->devicesForUser(user->id()))
+ devices.insert(user->id(), deviceId);
- void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey, const QHash<User*, QStringList> devices, int index)
- {
- qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex();
- QHash<QString, QHash<QString, QString>> hash;
- for (const auto& user : devices.keys()) {
- QHash<QString, QString> u;
- for(const auto &device : devices[user]) {
- if (!connection->hasOlmSession(user, device)) {
- u[device] = "signed_curve25519"_ls;
- qCDebug(E2EE) << "Adding" << user << device << "to keys to claim";
- }
- }
- if (!u.isEmpty()) {
- hash[user->id()] = u;
- }
- }
- if (hash.isEmpty()) {
- return;
- }
- auto job = connection->callApi<ClaimKeysJob>(hash);
- connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey, devices, index](){
- Connection::UsersToDevicesToEvents usersToDevicesToEvents;
- const auto data = job->jsonData();
- for(const auto &user : devices.keys()) {
- for(const auto &device : devices[user]) {
- const auto recipientCurveKey = connection->curveKeyForUserDevice(user->id(), device);
- if (!connection->hasOlmSession(user, device)) {
- qCDebug(E2EE) << "Creating a new session for" << user << device;
- if(data["one_time_keys"][user->id()][device].toObject().isEmpty()) {
- qWarning() << "No one time key for" << user << device;
- continue;
- }
- const auto keyId = data["one_time_keys"][user->id()][device].toObject().keys()[0];
- const auto oneTimeKey = data["one_time_keys"][user->id()][device][keyId]["key"].toString();
- const auto signature = data["one_time_keys"][user->id()][device][keyId]["signatures"][user->id()][QStringLiteral("ed25519:") + device].toString().toLatin1();
- auto signedData = data["one_time_keys"][user->id()][device][keyId].toObject();
- signedData.remove("unsigned");
- signedData.remove("signatures");
- auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature);
- if (std::holds_alternative<QOlmError>(signatureMatch)) {
- qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device.";
- continue;
- } else {
- }
- connection->createOlmSession(recipientCurveKey, oneTimeKey);
- }
- usersToDevicesToEvents[user->id()][device] = payloadForUserDevice(user, device, sessionId, sessionKey);
- }
- }
- if (!usersToDevicesToEvents.empty()) {
- connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents);
- connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index);
- }
- });
+ return connection->database()->devicesWithoutKey(
+ id, devices, currentOutboundMegolmSession->sessionId());
}
- void sendMegolmSession(const QHash<User *, QStringList>& devices) {
+ void sendMegolmSession(const QMultiHash<QString, QString>& devices) const {
// Save the session to this device
const auto sessionId = currentOutboundMegolmSession->sessionId();
- const auto _sessionKey = currentOutboundMegolmSession->sessionKey();
- if(std::holds_alternative<QOlmError>(_sessionKey)) {
+ const auto sessionKey = currentOutboundMegolmSession->sessionKey();
+ if(!sessionKey) {
qCWarning(E2EE) << "Error loading session key";
return;
}
- const auto sessionKey = std::get<QByteArray>(_sessionKey);
- const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519;
// Send the session to other people
- sendRoomKeyToDevices(sessionId, sessionKey, devices, currentOutboundMegolmSession->sessionMessageIndex());
+ connection->sendSessionKeyToDevices(
+ id, sessionId, *sessionKey, devices,
+ currentOutboundMegolmSession->sessionMessageIndex());
}
#endif // Quotient_E2EE_ENABLED
@@ -569,11 +499,6 @@ 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);
@@ -584,7 +509,8 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
}
});
d->groupSessions = connection->loadRoomMegolmSessions(this);
- d->currentOutboundMegolmSession = connection->loadCurrentOutboundMegolmSession(this);
+ d->currentOutboundMegolmSession =
+ connection->loadCurrentOutboundMegolmSession(this->id());
if (d->shouldRotateMegolmSession()) {
d->currentOutboundMegolmSession = nullptr;
}
@@ -592,7 +518,9 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
if (!usesEncryption()) {
return;
}
- d->currentOutboundMegolmSession = nullptr;
+ if (d->hasValidMegolmSession()) {
+ d->createMegolmSession();
+ }
qCDebug(E2EE) << "Invalidating current megolm session because user left";
});
@@ -779,10 +707,9 @@ void Room::setJoinState(JoinState state)
emit joinStateChanged(oldState, state);
}
-Room::Changes Room::Private::setLastReadReceipt(const QString& userId,
- rev_iter_t newMarker,
- ReadReceipt newReceipt,
- bool deferStatsUpdate)
+Omittable<QString> Room::Private::setLastReadReceipt(const QString& userId,
+ rev_iter_t newMarker,
+ ReadReceipt newReceipt)
{
if (newMarker == historyEdge() && !newReceipt.eventId.isEmpty())
newMarker = q->findInTimeline(newReceipt.eventId);
@@ -796,7 +723,7 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId,
// eagerMarker is now just after the desired event for newMarker
if (eagerMarker != newMarker.base()) {
newMarker = rev_iter_t(eagerMarker);
- qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId
+ qDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId
<< "to" << *newMarker;
}
// Fill newReceipt with the event (and, if needed, timestamp) from
@@ -810,14 +737,19 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId,
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 Change::None;
+ return {};
// Finally make the change
- Changes changes = Change::Other;
auto oldEventReadUsersIt =
eventIdReadUsers.find(prevEventId); // clazy:exclude=detaching-member
if (oldEventReadUsersIt != eventIdReadUsers.end()) {
@@ -829,7 +761,7 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId,
storedReceipt = move(newReceipt);
{
- auto dbg = qDebug(EPHEMERAL); // This trick needs qDebug, not qCDebug
+ 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;
@@ -837,25 +769,37 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId,
dbg << *newMarker;
}
- // TODO: use Room::member() when it becomes a thing and only emit signals
- // for actual members, not just any user
- const auto member = q->user(userId);
- Q_ASSERT(member != nullptr);
- if (isLocalUser(member) && !deferStatsUpdate) {
- if (unreadStats.updateOnMarkerMove(q, q->findInTimeline(prevEventId),
+ // 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))
+ 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)) {
- qCDebug(MESSAGES)
+ 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(member);
- // TODO: remove in 0.8
- if (!isLocalUser(member))
- emit q->readMarkerForUserMoved(member, prevEventId,
- storedReceipt.eventId);
+ emit q->lastReadEventChanged({ connection->userId() });
return changes;
}
@@ -870,7 +814,7 @@ Room::Changes Room::Private::updateStats(const rev_iter_t& from,
Changes changes = Change::None;
// Correct the read receipt to never be behind the fully read marker
if (readReceiptMarker > fullyReadMarker
- && setLastReadReceipt(connection->userId(), fullyReadMarker, {}, true)) {
+ && setLocalLastReadReceipt(fullyReadMarker, {}, true)) {
changes |= Change::Other;
readReceiptMarker = q->localReadReceiptMarker();
qCInfo(MESSAGES) << "The local m.read receipt was behind m.fully_read "
@@ -968,7 +912,7 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId)
QT_IGNORE_DEPRECATIONS(Changes changes = Change::ReadMarker|Change::Other;)
if (const auto rm = q->fullyReadMarker(); rm != historyEdge()) {
// Pull read receipt if it's behind, and update statistics
- changes |= setLastReadReceipt(connection->userId(), rm);
+ changes |= setLocalLastReadReceipt(rm);
if (partiallyReadStats.updateOnMarkerMove(q, prevReadMarker, rm)) {
changes |= Change::PartiallyReadStats;
qCDebug(MESSAGES)
@@ -986,9 +930,8 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId)
void Room::setReadReceipt(const QString& atEventId)
{
- if (const auto changes = d->setLastReadReceipt(localUser()->id(),
- historyEdge(),
- { atEventId })) {
+ if (const auto changes =
+ d->setLocalLastReadReceipt(historyEdge(), { atEventId })) {
connection()->callApi<PostReceiptJob>(BackgroundRequest, id(),
QStringLiteral("m.read"),
QUrl::toPercentEncoding(atEventId));
@@ -1517,7 +1460,7 @@ QUrl Room::urlToThumbnail(const QString& eventId) const
auto* thumbnail = event->content()->thumbnailInfo();
Q_ASSERT(thumbnail != nullptr);
return connection()->getUrlForApi<MediaThumbnailJob>(
- thumbnail->url, thumbnail->imageSize);
+ thumbnail->url(), thumbnail->imageSize);
}
qCDebug(MAIN) << "Event" << eventId << "has no thumbnail";
return {};
@@ -1528,7 +1471,7 @@ QUrl Room::urlToDownload(const QString& eventId) const
if (auto* event = d->getEventWithFile(eventId)) {
auto* fileInfo = event->content()->fileInfo();
Q_ASSERT(fileInfo != nullptr);
- return connection()->getUrlForApi<DownloadFileJob>(fileInfo->url);
+ return connection()->getUrlForApi<DownloadFileJob>(fileInfo->url());
}
return {};
}
@@ -1600,7 +1543,7 @@ QStringList Room::safeMemberNames() const
{
QStringList res;
res.reserve(d->membersMap.size());
- for (auto u: std::as_const(d->membersMap))
+ for (const auto* u: std::as_const(d->membersMap))
res.append(safeMemberName(u->id()));
return res;
@@ -1610,7 +1553,7 @@ QStringList Room::htmlSafeMemberNames() const
{
QStringList res;
res.reserve(d->membersMap.size());
- for (auto u: std::as_const(d->membersMap))
+ for (const auto* u: std::as_const(d->membersMap))
res.append(htmlSafeMemberName(u->id()));
return res;
@@ -1649,9 +1592,9 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent)
return {};
}
QString decrypted = d->groupSessionDecryptMessage(
- encryptedEvent.ciphertext(), encryptedEvent.senderKey(),
- encryptedEvent.sessionId(), encryptedEvent.id(),
- encryptedEvent.originTimestamp(), encryptedEvent.senderId());
+ encryptedEvent.ciphertext(), encryptedEvent.sessionId(),
+ encryptedEvent.id(), encryptedEvent.originTimestamp(),
+ encryptedEvent.senderId());
if (decrypted.isEmpty()) {
// qCWarning(E2EE) << "Encrypted message is empty";
return {};
@@ -1680,7 +1623,8 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent,
<< roomKeyEvent.algorithm() << "in m.room_key event";
}
if (d->addInboundGroupSession(roomKeyEvent.sessionId(),
- roomKeyEvent.sessionKey(), senderId, olmSessionId)) {
+ roomKeyEvent.sessionKey(), senderId,
+ olmSessionId)) {
qCWarning(E2EE) << "added new inboundGroupSession:"
<< d->groupSessions.size();
auto undecryptedEvents = d->undecryptedEvents[roomKeyEvent.sessionId()];
@@ -1690,8 +1634,7 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent,
continue;
auto& ti = d->timeline[Timeline::size_type(*pIdx - minTimelineIndex())];
if (auto encryptedEvent = ti.viewAs<EncryptedEvent>()) {
- auto decrypted = decryptMessage(*encryptedEvent);
- if(decrypted) {
+ if (auto decrypted = decryptMessage(*encryptedEvent)) {
// The reference will survive the pointer being moved
auto& decryptedEvent = *decrypted;
auto oldEvent = ti.replaceEvent(std::move(decrypted));
@@ -1986,6 +1929,9 @@ Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data,
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);
@@ -2011,6 +1957,9 @@ void Room::updateData(SyncRoomData&& data, bool fromCache)
emit namesChanged(this);
d->postprocessChanges(roomChanges, !fromCache);
+ if (firstUpdate)
+ emit baseStateLoaded();
+ qCDebug(MAIN) << "--- Finished updating room" << id() << "/" << objectName();
}
void Room::Private::postprocessChanges(Changes changes, bool saveState)
@@ -2036,11 +1985,8 @@ void Room::Private::postprocessChanges(Changes changes, bool saveState)
if (changes & Change::Highlights)
emit q->highlightCountChanged();
- qCDebug(MAIN) << terse << changes << "= hex" <<
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- Qt::
-#endif
- hex << uint(changes) << "in" << q->objectName();
+ qCDebug(MAIN) << terse << changes << "= hex" << Qt::hex << uint(changes)
+ << "in" << q->objectName();
emit q->changed(changes);
if (saveState)
connection->saveRoomState(q);
@@ -2076,6 +2022,7 @@ 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
@@ -2085,17 +2032,17 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
if (!hasValidMegolmSession() || shouldRotateMegolmSession()) {
createMegolmSession();
}
- const auto devicesWithoutKey = getDevicesWithoutKey();
- sendMegolmSession(devicesWithoutKey);
+ sendMegolmSession(getDevicesWithoutKey());
const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson());
currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1);
- connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession);
- if(std::holds_alternative<QOlmError>(encrypted)) {
- qWarning(E2EE) << "Error encrypting message" << std::get<QOlmError>(encrypted);
+ connection->saveCurrentOutboundMegolmSession(
+ id, *currentOutboundMegolmSession);
+ if(!encrypted) {
+ qWarning(E2EE) << "Error encrypting message" << encrypted.error();
return {};
}
- auto encryptedEvent = new EncryptedEvent(std::get<QByteArray>(encrypted), q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId());
+ 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());
@@ -2103,7 +2050,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
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;
+ _event = encryptedEvent.get();
#endif
}
@@ -2114,17 +2061,18 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
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();
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, _event] {
+ 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) {
@@ -2132,13 +2080,10 @@ 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());
- if (q->usesEncryption()){
- delete _event;
- }
});
} else
onEventSendingFailure(txnId);
@@ -2193,11 +2138,9 @@ QString Room::retryMessage(const QString& txnId)
return d->doSendEvent(it->event());
}
-// Lambda defers actual tr() invocation to the moment when translations are
-// initialised
-const auto FileTransferCancelledMsg = [] {
- return Room::tr("File transfer cancelled");
-};
+// 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)
{
@@ -2262,28 +2205,26 @@ QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl)
// Below, the upload job is used as a context object to clean up connections
const auto& transferJob = fileTransfers.value(txnId).job;
connect(q, &Room::fileTransferCompleted, transferJob,
- [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri, Omittable<EncryptedFile> encryptedFile) {
- if (tId != txnId)
- return;
+ [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(mxcUri);
- if (encryptedFile) {
- it->setEncryptedFile(*encryptedFile);
- }
- emit q->pendingEventChanged(
- int(it - unsyncedEvents.begin()));
- doSendEvent(it->get());
- } else {
- // Normally in this situation we should instruct
- // the media server to delete the file; alas, there's no
- // API specced for that.
- qCWarning(MAIN) << "File uploaded to" << mxcUri
- << "but the event referring to it was "
- "cancelled";
- }
- });
+ 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)
@@ -2309,13 +2250,13 @@ QString Room::postFile(const QString& plainText,
Q_ASSERT(content != nullptr && content->fileInfo() != nullptr);
const auto* const fileInfo = content->fileInfo();
Q_ASSERT(fileInfo != nullptr);
- QFileInfo localFile { fileInfo->url.toLocalFile() };
+ QFileInfo localFile { fileInfo->url().toLocalFile() };
Q_ASSERT(localFile.isFile());
return d->doPostFile(
makeEvent<RoomMessageEvent>(
plainText, RoomMessageEvent::rawMsgTypeForFile(localFile), content),
- fileInfo->url);
+ fileInfo->url());
}
#if QT_VERSION_MAJOR < 6
@@ -2343,8 +2284,7 @@ QString Room::postJson(const QString& matrixType,
SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt)
{
- return d->requestSetState(evt.matrixType(), evt.stateKey(),
- evt.contentJson());
+ return setState(evt.matrixType(), evt.stateKey(), evt.contentJson());
}
SetRoomStateWithKeyJob* Room::setState(const QString& evtType,
@@ -2431,11 +2371,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)
@@ -2450,15 +2391,18 @@ void Room::hangupCall(const QString& callId)
d->sendEvent<CallHangupEvent>(callId);
}
-void Room::getPreviousContent(int limit, const QString &filter) { d->getPreviousContent(limit, filter); }
+void Room::getPreviousContent(int limit, const QString& filter)
+{
+ d->getPreviousContent(limit, filter);
+}
void Room::Private::getPreviousContent(int limit, const QString &filter)
{
if (isJobPending(eventsHistoryJob))
return;
- eventsHistoryJob =
- connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit, filter);
+ eventsHistoryJob = connection->callApi<GetRoomEventsJob>(id, "b", prevBatch,
+ "", limit, filter);
emit q->eventsHistoryJobChanged();
connect(eventsHistoryJob, &BaseJob::success, q, [this] {
prevBatch = eventsHistoryJob->end();
@@ -2506,18 +2450,18 @@ 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();
- Omittable<EncryptedFile> encryptedFile = std::nullopt;
+ FileSourceInfo fileMetadata;
#ifdef Quotient_E2EE_ENABLED
QTemporaryFile tempFile;
if (usesEncryption()) {
tempFile.open();
QFile file(localFilename.toLocalFile());
file.open(QFile::ReadOnly);
- auto [e, data] = EncryptedFile::encryptFile(file.readAll());
+ QByteArray data;
+ std::tie(fileMetadata, data) = encryptFile(file.readAll());
tempFile.write(data);
tempFile.close();
fileName = QFileInfo(tempFile).absoluteFilePath();
- encryptedFile = e;
}
#endif
auto job = connection()->uploadFile(fileName, overrideContentType);
@@ -2528,16 +2472,13 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename,
d->fileTransfers[id].update(sent, total);
emit fileTransferProgress(id, sent, total);
});
- connect(job, &BaseJob::success, this, [this, id, localFilename, job, encryptedFile] {
- d->fileTransfers[id].status = FileTransferInfo::Completed;
- if (encryptedFile) {
- auto file = *encryptedFile;
- file.url = QUrl(job->contentUri());
- emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), file);
- } else {
- emit fileTransferCompleted(id, localFilename, QUrl(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);
@@ -2570,11 +2511,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, "---");
@@ -2584,9 +2525,9 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
}
DownloadFileJob *job = nullptr;
#ifdef Quotient_E2EE_ENABLED
- if(fileInfo->file.has_value()) {
- auto file = *fileInfo->file;
- job = connection()->downloadFile(fileUrl, file, filePath);
+ if (auto* fileMetadata =
+ std::get_if<EncryptedFileMetadata>(&fileInfo->source)) {
+ job = connection()->downloadFile(fileUrl, *fileMetadata, filePath);
} else {
#endif
job = connection()->downloadFile(fileUrl, filePath);
@@ -2609,6 +2550,7 @@ 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);
}
@@ -2652,6 +2594,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
@@ -2679,10 +2641,11 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
{ QStringLiteral("ban"), QStringLiteral("events"),
QStringLiteral("events_default"), QStringLiteral("kick"),
QStringLiteral("redact"), QStringLiteral("state_default"),
- QStringLiteral("users"), QStringLiteral("users_default") } }
- // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } }
- // , { RoomHistoryVisibility::typeId(),
- // { QStringLiteral("history_visibility") } }
+ QStringLiteral("users"), QStringLiteral("users_default") } },
+ // 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()))
@@ -2754,7 +2717,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
}
if (const auto* reaction = eventCast<ReactionEvent>(oldEvent)) {
const auto& targetEvtId = reaction->relation().eventId;
- const QPair lookupKey { targetEvtId, EventRelation::AnnotationType };
+ const std::pair lookupKey { targetEvtId, EventRelation::AnnotationType };
if (relations.contains(lookupKey)) {
relations[lookupKey].removeOne(reaction);
emit q->updatedEvent(targetEvtId);
@@ -2842,23 +2805,11 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
if (events.empty())
return Change::None;
+ decryptIncomingEvents(events);
+
QElapsedTimer et;
et.start();
-#ifdef Quotient_E2EE_ENABLED
- for(long unsigned int i = 0; i < events.size(); i++) {
- if(auto* encrypted = eventCast<EncryptedEvent>(events[i])) {
- auto decrypted = q->decryptMessage(*encrypted);
- if(decrypted) {
- auto oldEvent = std::exchange(events[i], std::move(decrypted));
- events[i]->setOriginalEvent(std::move(oldEvent));
- } else {
- undecryptedEvents[encrypted->sessionId()] += encrypted->id();
- }
- }
- }
-#endif
-
{
// Pre-process redactions and edits so that events that get
// redacted/replaced in the same batch landed in the timeline already
@@ -3002,30 +2953,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;
- Changes changes {};
-
-#ifdef Quotient_E2EE_ENABLED
- for(long unsigned int i = 0; i < events.size(); i++) {
- if(auto* encrypted = eventCast<EncryptedEvent>(events[i])) {
- auto decrypted = q->decryptMessage(*encrypted);
- if(decrypted) {
- auto oldEvent = std::exchange(events[i], std::move(decrypted));
- events[i]->setOriginalEvent(std::move(oldEvent));
- } else {
- undecryptedEvents[encrypted->sessionId()] += encrypted->id();
- }
- }
- }
-#endif
+ 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
@@ -3142,7 +3080,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return false;
}
if (oldEncEvt
- && oldEncEvt->encryption() != EncryptionEventContent::Undefined) {
+ && oldEncEvt->encryption() != EncryptionType::Undefined) {
qCWarning(STATE) << "The room is already encrypted but a new"
" room encryption event arrived - ignoring";
return false;
@@ -3283,58 +3221,66 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event)
Changes changes {};
QElapsedTimer et;
et.start();
- if (auto* evt = eventCast<TypingEvent>(event)) {
- d->usersTyping.clear();
- d->usersTyping.reserve(evt->users().size()); // Assume all are members
- for (const auto& userId : evt->users())
- if (isMember(userId))
- d->usersTyping.append(user(userId));
-
- if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER)
- << "Processing typing events from" << evt->users().size()
- << "user(s) in" << objectName() << "took" << et;
- emit typingChanged();
- }
- if (auto* evt = eventCast<ReceiptEvent>(event)) {
- int totalReceipts = 0;
- const auto& eventsWithReceipts = evt->eventsWithReceipts();
- for (const auto& p : eventsWithReceipts) {
- totalReceipts += p.receipts.size();
- const auto newMarker = findInTimeline(p.evtId);
- if (newMarker == historyEdge())
- qCDebug(EPHEMERAL)
- << "Event" << p.evtId
- << "is not found; saving read receipt(s) anyway";
- // If the event is not found (most likely, because it's too old and
- // hasn't been fetched from the server yet) but there is a previous
- // marker for a user, keep the previous marker because read receipts
- // are not supposed to move backwards. Otherwise, blindly store
- // the event id for this user and update the read marker when/if
- // the event is fetched later on.
- const auto updatedCount = std::count_if(
- p.receipts.cbegin(), p.receipts.cend(),
- [this, &changes, &newMarker, &evtId = p.evtId](const auto& r) {
- const auto change =
- d->setLastReadReceipt(r.userId, newMarker,
- { evtId, r.timestamp });
- changes |= change;
- return change & Change::Any;
- });
-
- if (p.receipts.size() > 1)
- qCDebug(EPHEMERAL) << p.evtId << "marked as read for"
- << updatedCount << "user(s)";
- if (updatedCount < p.receipts.size())
- qCDebug(EPHEMERAL) << p.receipts.size() - updatedCount
- << "receipts were skipped";
- }
- if (eventsWithReceipts.size() > 3 || totalReceipts > 10
- || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "Processing" << totalReceipts
- << "receipt(s) on" << eventsWithReceipts.size()
- << "event(s) in" << objectName() << "took" << et;
- }
+ 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 (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;
}
@@ -3444,7 +3390,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
@@ -3587,5 +3533,5 @@ void Room::activateEncryption()
qCWarning(E2EE) << "Room" << objectName() << "is already encrypted";
return;
}
- setState<EncryptionEvent>(EncryptionEventContent::MegolmV1AesSha2);
+ setState<EncryptionEvent>(EncryptionType::MegolmV1AesSha2);
}
diff --git a/lib/room.h b/lib/room.h
index f5199eff..44504691 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -758,7 +758,8 @@ public:
[[deprecated("Use currentState().get() instead; "
"make sure to check its result for nullptrs")]] //
const Quotient::StateEventBase*
- getCurrentState(const QString& evtType, const QString& stateKey = {}) const;
+ 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
@@ -870,8 +871,9 @@ public Q_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);
@@ -971,9 +973,9 @@ Q_SIGNALS:
void displayedChanged(bool displayed);
void firstDisplayedEventChanged();
void lastDisplayedEventChanged();
- //! The event that m.read receipt points to has changed
+ //! The event the m.read receipt points to has changed for the listed users
//! \sa lastReadReceipt
- void lastReadEventChanged(Quotient::User* user);
+ void lastReadEventChanged(QVector<QString> userIds);
void fullyReadMarkerMoved(QString fromEventId, QString toEventId);
//! \deprecated since 0.7 - use fullyReadMarkerMoved
void readMarkerMoved(QString fromEventId, QString toEventId);
@@ -997,7 +999,8 @@ Q_SIGNALS:
void newFileTransfer(QString id, QUrl localFile);
void fileTransferProgress(QString id, qint64 progress, qint64 total);
- void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable<EncryptedFile> encryptedFile = std::nullopt);
+ void fileTransferCompleted(QString id, QUrl localFile,
+ FileSourceInfo fileMetadata);
void fileTransferFailed(QString id, QString errorMessage = {});
// fileTransferCancelled() is no more here; use fileTransferFailed() and
// check the transfer status instead
diff --git a/lib/roomstateview.h b/lib/roomstateview.h
index cab69ae3..29cce00e 100644
--- a/lib/roomstateview.h
+++ b/lib/roomstateview.h
@@ -11,7 +11,8 @@ namespace Quotient {
class Room;
-class RoomStateView : private QHash<StateEventKey, const StateEventBase*> {
+class QUOTIENT_API RoomStateView
+ : private QHash<StateEventKey, const StateEventBase*> {
Q_GADGET
public:
const QHash<StateEventKey, const StateEventBase*>& events() const
diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp
index 78957cbe..93416bc4 100644
--- a/lib/syncdata.cpp
+++ b/lib/syncdata.cpp
@@ -18,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)
@@ -142,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)
{
@@ -150,18 +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); }
+DevicesList SyncData::takeDevicesList() { return std::move(devicesList); }
QJsonObject SyncData::loadJson(const QString& fileName)
{
@@ -179,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";
diff --git a/lib/syncdata.h b/lib/syncdata.h
index 6b70140d..9358ec8f 100644
--- a/lib/syncdata.h
+++ b/lib/syncdata.h
@@ -98,15 +98,15 @@ 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();
- DevicesList&& takeDevicesList();
+ SyncDataList takeRoomData();
+ DevicesList takeDevicesList();
QString nextBatch() const { return nextBatch_; }
diff --git a/lib/uri.cpp b/lib/uri.cpp
index 6b7d1d20..91751df0 100644
--- a/lib/uri.cpp
+++ b/lib/uri.cpp
@@ -171,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");
diff --git a/lib/util.cpp b/lib/util.cpp
index 03ebf325..359b2959 100644
--- a/lib/util.cpp
+++ b/lib/util.cpp
@@ -135,3 +135,12 @@ 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 753eb1ea..46b1767e 100644
--- a/lib/util.h
+++ b/lib/util.h
@@ -37,6 +37,31 @@ static_assert(false, "Use Q_DISABLE_MOVE instead; Quotient enables it across all
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, ...>
template <typename T>
@@ -93,8 +118,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)
@@ -110,8 +135,8 @@ inline std::pair<InputIt, ForwardIt> findFirstOf(InputIt first, InputIt last,
//! 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>
-using ImplPtr = std::unique_ptr<ImplType, void (*)(ImplType*)>;
+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
@@ -131,20 +156,42 @@ using ImplPtr = std::unique_ptr<ImplType, void (*)(ImplType*)>;
//!
//! 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 DeleterType = void (*)(ImplType*),
- typename... ArgTs>
-inline ImplPtr<ImplType> makeImpl(ArgTs&&... args)
+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> { new ImplType(std::forward<ArgTs>(args)...),
- [](ImplType* impl) { delete impl; } };
+ return ImplPtr<ImplType, TypeToDelete> { from, [](TypeToDelete* impl) {
+ delete impl;
+ } };
}
-template <typename ImplType>
-const inline ImplPtr<ImplType> ZeroImpl()
+template <typename ImplType, typename TypeToDelete = ImplType>
+constexpr ImplPtr<ImplType, TypeToDelete> ZeroImpl()
{
- return { nullptr, [](ImplType*) { /* nullptr doesn't need deletion */ } };
+ 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 */
QUOTIENT_API void linkifyUrls(QString& htmlEscapedText);
@@ -183,4 +230,5 @@ QUOTIENT_API QString versionString();
QUOTIENT_API int majorVersion();
QUOTIENT_API int minorVersion();
QUOTIENT_API int patchVersion();
+QUOTIENT_API bool encryptionSupported();
} // namespace Quotient