diff options
Diffstat (limited to 'lib/events')
42 files changed, 1051 insertions, 1203 deletions
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; }; |