diff options
Diffstat (limited to 'lib/events')
-rw-r--r-- | lib/events/accountdataevents.h | 35 | ||||
-rw-r--r-- | lib/events/event.cpp | 8 | ||||
-rw-r--r-- | lib/events/event.h | 28 | ||||
-rw-r--r-- | lib/events/eventcontent.cpp | 38 | ||||
-rw-r--r-- | lib/events/eventcontent.h | 28 | ||||
-rw-r--r-- | lib/events/eventloader.h | 11 | ||||
-rw-r--r-- | lib/events/roomavatarevent.h | 3 | ||||
-rw-r--r-- | lib/events/roomcreateevent.cpp | 45 | ||||
-rw-r--r-- | lib/events/roomcreateevent.h | 49 | ||||
-rw-r--r-- | lib/events/roomevent.cpp | 5 | ||||
-rw-r--r-- | lib/events/roommemberevent.cpp | 14 | ||||
-rw-r--r-- | lib/events/roommemberevent.h | 31 | ||||
-rw-r--r-- | lib/events/roommessageevent.cpp | 129 | ||||
-rw-r--r-- | lib/events/roommessageevent.h | 36 | ||||
-rw-r--r-- | lib/events/roomtombstoneevent.cpp | 31 | ||||
-rw-r--r-- | lib/events/roomtombstoneevent.h | 41 | ||||
-rw-r--r-- | lib/events/simplestateevents.h | 31 | ||||
-rw-r--r-- | lib/events/stateevent.cpp | 22 | ||||
-rw-r--r-- | lib/events/stateevent.h | 31 |
19 files changed, 501 insertions, 115 deletions
diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index d1c1abc8..a99d85ac 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -36,37 +36,38 @@ namespace QMatrixClient order_type order; TagRecord (order_type order = none) : order(order) { } - explicit TagRecord(const QJsonObject& jo) + + bool operator<(const TagRecord& other) const + { + // Per The Spec, rooms with no order should be after those with order + return !order.omitted() && + (other.order.omitted() || order.value() < other.order.value()); + } + }; + + template <> struct JsonObjectConverter<TagRecord> + { + static void fillFrom(const QJsonObject& jo, TagRecord& rec) { // Parse a float both from JSON double and JSON string because // libqmatrixclient previously used to use strings to store order. const auto orderJv = jo.value("order"_ls); if (orderJv.isDouble()) - order = fromJson<float>(orderJv); - else if (orderJv.isString()) + rec.order = fromJson<float>(orderJv); + if (orderJv.isString()) { bool ok; - order = orderJv.toString().toFloat(&ok); + rec.order = orderJv.toString().toFloat(&ok); if (!ok) - order = none; + rec.order = none; } } - - bool operator<(const TagRecord& other) const + static void dumpTo(QJsonObject& jo, const TagRecord& rec) { - // Per The Spec, rooms with no order should be after those with order - return !order.omitted() && - (other.order.omitted() || order.value() < other.order.value()); + addParam<IfNotEmpty>(jo, QStringLiteral("order"), rec.order); } }; - inline QJsonValue toJson(const TagRecord& rec) - { - QJsonObject o; - addParam<IfNotEmpty>(o, QStringLiteral("order"), rec.order); - return o; - } - using TagsMap = QHash<QString, TagRecord>; #define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ diff --git a/lib/events/event.cpp b/lib/events/event.cpp index fd6e3939..6505d89a 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -38,7 +38,8 @@ event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId) QString EventTypeRegistry::getMatrixType(event_type_t typeId) { - return typeId < get().eventTypes.size() ? get().eventTypes[typeId] : ""; + return typeId < get().eventTypes.size() + ? get().eventTypes[typeId] : QString(); } Event::Event(Type type, const QJsonObject& json) @@ -77,3 +78,8 @@ const QJsonObject Event::unsignedJson() const { return fullJson()[UnsignedKeyL].toObject(); } + +void Event::dumpTo(QDebug dbg) const +{ + dbg << QJsonDocument(contentJson()).toJson(QJsonDocument::Compact); +} diff --git a/lib/events/event.h b/lib/events/event.h index e0d83976..b7bbd83e 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -32,19 +32,23 @@ namespace QMatrixClient template <typename EventT> using event_ptr_tt = std::unique_ptr<EventT>; + /// Unwrap a plain pointer from a smart pointer template <typename EventT> - inline EventT* rawPtr(const event_ptr_tt<EventT>& ptr) // unwrap + inline EventT* rawPtr(const event_ptr_tt<EventT>& ptr) { return ptr.get(); } + /// Unwrap a plain pointer and downcast it to the specified type template <typename TargetEventT, typename EventT> inline TargetEventT* weakPtrCast(const event_ptr_tt<EventT>& ptr) { return static_cast<TargetEventT*>(rawPtr(ptr)); } + /// Re-wrap a smart pointer to base into a smart pointer to derived template <typename TargetT, typename SourceT> + [[deprecated("Consider using eventCast() or visit() instead")]] inline event_ptr_tt<TargetT> ptrCast(event_ptr_tt<SourceT>&& ptr) { return unique_ptr_cast<TargetT>(ptr); @@ -208,6 +212,9 @@ namespace QMatrixClient template <typename EventT> inline auto registerEventType() { + // Initialise exactly once, even if this function is called twice for + // the same type (for whatever reason - you never know the ways of + // static initialisation is done). static const auto _ = setupFactory<EventT>(); return _; // Only to facilitate usage in static initialisation } @@ -257,8 +264,18 @@ namespace QMatrixClient return fromJson<T>(contentJson()[key]); } + friend QDebug operator<<(QDebug dbg, const Event& e) + { + QDebugStateSaver _dss { dbg }; + dbg.noquote().nospace() + << e.matrixType() << '(' << e.type() << "): "; + e.dumpTo(dbg); + return dbg; + } + virtual bool isStateEvent() const { return false; } virtual bool isCallEvent() const { return false; } + virtual void dumpTo(QDebug dbg) const; protected: QJsonObject& editJson() { return _json; } @@ -327,7 +344,8 @@ namespace QMatrixClient -> decltype(static_cast<EventT*>(&*eptr)) { Q_ASSERT(eptr); - return is<EventT>(*eptr) ? static_cast<EventT*>(&*eptr) : nullptr; + return is<std::decay_t<EventT>>(*eptr) + ? static_cast<EventT*>(&*eptr) : nullptr; } // A single generic catch-all visitor @@ -359,7 +377,7 @@ namespace QMatrixClient visit(const BaseEventT& event, FnT&& visitor) { using event_type = fn_arg_t<FnT>; - if (is<event_type>(event)) + if (is<std::decay_t<event_type>>(event)) visitor(static_cast<event_type>(event)); } @@ -373,7 +391,7 @@ namespace QMatrixClient fn_return_t<FnT>&& defaultValue = {}) { using event_type = fn_arg_t<FnT>; - if (is<event_type>(event)) + if (is<std::decay_t<event_type>>(event)) return visitor(static_cast<event_type>(event)); return std::forward<fn_return_t<FnT>>(defaultValue); } @@ -386,7 +404,7 @@ namespace QMatrixClient FnTs&&... visitors) { using event_type1 = fn_arg_t<FnT1>; - if (is<event_type1>(event)) + if (is<std::decay_t<event_type1>>(event)) return visitor1(static_cast<event_type1&>(event)); return visit(event, std::forward<FnT2>(visitor2), std::forward<FnTs>(visitors)...); diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index a6b1c763..77f756cd 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -17,6 +17,8 @@ */ #include "eventcontent.h" + +#include "converters.h" #include "util.h" #include <QtCore/QMimeDatabase> @@ -30,7 +32,7 @@ QJsonObject Base::toJson() const return o; } -FileInfo::FileInfo(const QUrl& u, int payloadSize, const QMimeType& mimeType, +FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType, const QString& originalFilename) : mimeType(mimeType), url(u), payloadSize(payloadSize) , originalName(originalFilename) @@ -41,23 +43,31 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, : originalInfoJson(infoJson) , mimeType(QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) , url(u) - , payloadSize(infoJson["size"_ls].toInt()) + , payloadSize(fromJson<qint64>(infoJson["size"_ls])) , originalName(originalFilename) { if (!mimeType.isValid()) mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); } +bool FileInfo::isValid() const +{ + return url.scheme() == "mxc" + && (url.authority() + url.path()).count('/') == 1; +} + void FileInfo::fillInfoJson(QJsonObject* infoJson) const { Q_ASSERT(infoJson); - infoJson->insert(QStringLiteral("size"), payloadSize); - infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); + if (payloadSize != -1) + infoJson->insert(QStringLiteral("size"), payloadSize); + if (mimeType.isValid()) + infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); } -ImageInfo::ImageInfo(const QUrl& u, int fileSize, QMimeType mimeType, - const QSize& imageSize) - : FileInfo(u, fileSize, mimeType), imageSize(imageSize) +ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType, + const QSize& imageSize, const QString& originalFilename) + : FileInfo(u, fileSize, mimeType, originalFilename), imageSize(imageSize) { } ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, @@ -69,8 +79,10 @@ ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, void ImageInfo::fillInfoJson(QJsonObject* infoJson) const { FileInfo::fillInfoJson(infoJson); - infoJson->insert(QStringLiteral("w"), imageSize.width()); - infoJson->insert(QStringLiteral("h"), imageSize.height()); + if (imageSize.width() != -1) + infoJson->insert(QStringLiteral("w"), imageSize.width()); + if (imageSize.height() != -1) + infoJson->insert(QStringLiteral("h"), imageSize.height()); } Thumbnail::Thumbnail(const QJsonObject& infoJson) @@ -80,7 +92,9 @@ Thumbnail::Thumbnail(const QJsonObject& infoJson) void Thumbnail::fillInfoJson(QJsonObject* infoJson) const { - infoJson->insert(QStringLiteral("thumbnail_url"), url.toString()); - infoJson->insert(QStringLiteral("thumbnail_info"), - toInfoJson<ImageInfo>(*this)); + if (url.isValid()) + infoJson->insert(QStringLiteral("thumbnail_url"), url.toString()); + if (!imageSize.isEmpty()) + infoJson->insert(QStringLiteral("thumbnail_info"), + toInfoJson<ImageInfo>(*this)); } diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 91d7a8c8..ab31a75d 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -43,9 +43,10 @@ namespace QMatrixClient class Base { public: - explicit Base (const QJsonObject& o = {}) : originalJson(o) { } + explicit Base (QJsonObject o = {}) : originalJson(std::move(o)) { } virtual ~Base() = default; + // FIXME: make toJson() from converters.* work on base classes QJsonObject toJson() const; public: @@ -87,12 +88,14 @@ namespace QMatrixClient class FileInfo { public: - explicit FileInfo(const QUrl& u, int payloadSize = -1, + explicit FileInfo(const QUrl& u, qint64 payloadSize = -1, const QMimeType& mimeType = {}, const QString& originalFilename = {}); FileInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename = {}); + bool isValid() const; + void fillInfoJson(QJsonObject* infoJson) const; /** @@ -108,7 +111,7 @@ namespace QMatrixClient QJsonObject originalInfoJson; QMimeType mimeType; QUrl url; - int payloadSize; + qint64 payloadSize; QString originalName; }; @@ -126,9 +129,10 @@ namespace QMatrixClient class ImageInfo : public FileInfo { public: - explicit ImageInfo(const QUrl& u, int fileSize = -1, + explicit ImageInfo(const QUrl& u, qint64 fileSize = -1, QMimeType mimeType = {}, - const QSize& imageSize = {}); + const QSize& imageSize = {}, + const QString& originalFilename = {}); ImageInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename = {}); @@ -148,10 +152,10 @@ namespace QMatrixClient class Thumbnail : public ImageInfo { public: + Thumbnail() : ImageInfo(QUrl()) { } // To allow empty thumbnails Thumbnail(const QJsonObject& infoJson); - Thumbnail(const ImageInfo& info) - : ImageInfo(info) - { } + Thumbnail(const ImageInfo& info) : ImageInfo(info) { } + using ImageInfo::ImageInfo; /** * Writes thumbnail information to "thumbnail_info" subobject @@ -166,6 +170,7 @@ namespace QMatrixClient explicit TypedBase(const QJsonObject& o = {}) : Base(o) { } virtual QMimeType type() const = 0; virtual const FileInfo* fileInfo() const { return nullptr; } + virtual FileInfo* fileInfo() { return nullptr; } virtual const Thumbnail* thumbnailInfo() const { return nullptr; } }; @@ -183,9 +188,7 @@ namespace QMatrixClient class UrlBasedContent : public TypedBase, public InfoT { public: - UrlBasedContent(QUrl url, InfoT&& info, QString filename = {}) - : InfoT(url, std::forward<InfoT>(info), filename) - { } + using InfoT::InfoT; explicit UrlBasedContent(const QJsonObject& json) : TypedBase(json) , InfoT(json["url"].toString(), json["info"].toObject(), @@ -197,6 +200,7 @@ namespace QMatrixClient 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 @@ -213,7 +217,7 @@ namespace QMatrixClient class UrlWithThumbnailContent : public UrlBasedContent<InfoT> { public: - // TODO: POD constructor + using UrlBasedContent<InfoT>::UrlBasedContent; explicit UrlWithThumbnailContent(const QJsonObject& json) : UrlBasedContent<InfoT>(json) , thumbnail(InfoT::originalInfoJson) diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 3ee9a181..da663392 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -19,7 +19,6 @@ #pragma once #include "stateevent.h" -#include "converters.h" namespace QMatrixClient { namespace _impl { @@ -58,11 +57,15 @@ namespace QMatrixClient { matrixType); } - template <typename EventT> struct FromJsonObject<event_ptr_tt<EventT>> + template <typename EventT> struct JsonConverter<event_ptr_tt<EventT>> { - auto operator()(const QJsonObject& jo) const + static auto load(const QJsonValue& jv) { - return loadEvent<EventT>(jo); + return loadEvent<EventT>(jv.toObject()); + } + static auto load(const QJsonDocument& jd) + { + return loadEvent<EventT>(jd.object()); } }; } // namespace QMatrixClient diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index 491861b1..a43d3a85 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -18,8 +18,7 @@ #pragma once -#include "event.h" - +#include "stateevent.h" #include "eventcontent.h" namespace QMatrixClient diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp new file mode 100644 index 00000000..8fd0f1de --- /dev/null +++ b/lib/events/roomcreateevent.cpp @@ -0,0 +1,45 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "roomcreateevent.h" + +using namespace QMatrixClient; + +bool RoomCreateEvent::isFederated() const +{ + return fromJson<bool>(contentJson()["m.federate"_ls]); +} + +QString RoomCreateEvent::version() const +{ + return fromJson<QString>(contentJson()["room_version"_ls]); +} + +RoomCreateEvent::Predecessor RoomCreateEvent::predecessor() const +{ + const auto predJson = contentJson()["predecessor"_ls].toObject(); + return { + fromJson<QString>(predJson["room_id"_ls]), + fromJson<QString>(predJson["event_id"_ls]) + }; +} + +bool RoomCreateEvent::isUpgrade() const +{ + return contentJson().contains("predecessor"_ls); +} diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h new file mode 100644 index 00000000..0a8f27cc --- /dev/null +++ b/lib/events/roomcreateevent.h @@ -0,0 +1,49 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#include "stateevent.h" + +namespace QMatrixClient +{ + class RoomCreateEvent : public StateEventBase + { + public: + DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent) + + explicit RoomCreateEvent() + : StateEventBase(typeId(), matrixTypeId()) + { } + explicit RoomCreateEvent(const QJsonObject& obj) + : StateEventBase(typeId(), obj) + { } + + struct Predecessor + { + QString roomId; + QString eventId; + }; + + bool isFederated() const; + QString version() const; + Predecessor predecessor() const; + bool isUpgrade() const; + }; + REGISTER_EVENT_TYPE(RoomCreateEvent) +} diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 80d121de..3d03509f 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -42,10 +42,6 @@ RoomEvent::RoomEvent(Type type, const QJsonObject& json) _redactedBecause = makeEvent<RedactionEvent>(redaction.toObject()); return; } - - const auto& txnId = transactionId(); - if (!txnId.isEmpty()) - qCDebug(EVENTS) << "Event transactionId:" << txnId; } RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job @@ -90,7 +86,6 @@ void RoomEvent::setTransactionId(const QString& txnId) auto unsignedData = fullJson()[UnsignedKeyL].toObject(); unsignedData.insert(QStringLiteral("transaction_id"), txnId); editJson().insert(UnsignedKey, unsignedData); - qCDebug(EVENTS) << "New event transactionId:" << txnId; Q_ASSERT(transactionId() == txnId); } diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index eaa3302c..6da76526 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -23,20 +23,17 @@ #include <array> -using namespace QMatrixClient; - static const std::array<QString, 5> membershipStrings = { { QStringLiteral("invite"), QStringLiteral("join"), QStringLiteral("knock"), QStringLiteral("leave"), QStringLiteral("ban") } }; -namespace QMatrixClient -{ +namespace QMatrixClient { template <> - struct FromJson<MembershipType> + struct JsonConverter<MembershipType> { - MembershipType operator()(const QJsonValue& jv) const + static MembershipType load(const QJsonValue& jv) { const auto& membershipString = jv.toString(); for (auto it = membershipStrings.begin(); @@ -48,13 +45,14 @@ namespace QMatrixClient return MembershipType::Undefined; } }; - } +using namespace QMatrixClient; + MemberEventContent::MemberEventContent(const QJsonObject& json) : membership(fromJson<MembershipType>(json["membership"_ls])) , isDirect(json["is_direct"_ls].toBool()) - , displayName(json["displayname"_ls].toString()) + , displayName(sanitized(json["displayname"_ls].toString())) , avatarUrl(json["avatar_url"_ls].toString()) { } diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index db25d026..b8224033 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -29,13 +29,10 @@ namespace QMatrixClient enum MembershipType : size_t { Invite = 0, Join, Knock, Leave, Ban, Undefined }; - explicit MemberEventContent(MembershipType mt = MembershipType::Join) + explicit MemberEventContent(MembershipType mt = Join) : membership(mt) { } explicit MemberEventContent(const QJsonObject& json); - explicit MemberEventContent(const QJsonValue& jv) - : MemberEventContent(jv.toObject()) - { } MembershipType membership; bool isDirect = false; @@ -60,11 +57,19 @@ namespace QMatrixClient : StateEvent(typeId(), obj) { } RoomMemberEvent(MemberEventContent&& c) - : StateEvent(typeId(), matrixTypeId(), c.toJson()) + : StateEvent(typeId(), matrixTypeId(), c) { } - // This is a special constructor enabling RoomMemberEvent to be - // a base class for more specific member events. + /// A special constructor to create unknown RoomMemberEvents + /** + * This is needed in order to use RoomMemberEvent as a "base event + * class" in cases like GetMembersByRoomJob when RoomMemberEvents + * (rather than RoomEvents or StateEvents) are resolved from JSON. + * For such cases loadEvent<> requires an underlying class to be + * constructible with unknownTypeId() instead of its genuine id. + * Don't use it directly. + * \sa GetMembersByRoomJob, loadEvent, unknownTypeId + */ RoomMemberEvent(Type type, const QJsonObject& fullJson) : StateEvent(type, fullJson) { } @@ -84,6 +89,18 @@ namespace QMatrixClient private: REGISTER_ENUM(MembershipType) }; + + template <> + class EventFactory<RoomMemberEvent> + { + public: + static event_ptr_tt<RoomMemberEvent> make(const QJsonObject& json, + const QString&) + { + return makeEvent<RoomMemberEvent>(json); + } + }; + REGISTER_EVENT_TYPE(RoomMemberEvent) DEFINE_EVENTTYPE_ALIAS(RoomMember, RoomMemberEvent) } // namespace QMatrixClient diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 1c5cf058..8f4e0ebc 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -21,18 +21,38 @@ #include "logging.h" #include <QtCore/QMimeDatabase> +#include <QtCore/QFileInfo> +#include <QtGui/QImageReader> +#include <QtMultimedia/QMediaResource> using namespace QMatrixClient; using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; +static const auto RelatesToKey = "m.relates_to"_ls; +static const auto MsgTypeKey = "msgtype"_ls; +static const auto BodyKey = "body"_ls; +static const auto FormattedBodyKey = "formatted_body"_ls; + +static const auto TextTypeKey = "m.text"; +static const auto NoticeTypeKey = "m.notice"; + +static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html"); + template <typename ContentT> TypedBase* make(const QJsonObject& json) { return new ContentT(json); } +template <> +TypedBase* make<TextContent>(const QJsonObject& json) +{ + return json.contains(FormattedBodyKey) || json.contains(RelatesToKey) + ? new TextContent(json) : nullptr; +} + struct MsgTypeDesc { QString matrixType; @@ -41,9 +61,9 @@ struct MsgTypeDesc }; const std::vector<MsgTypeDesc> msgTypes = - { { QStringLiteral("m.text"), MsgType::Text, make<TextContent> } + { { TextTypeKey, MsgType::Text, make<TextContent> } , { QStringLiteral("m.emote"), MsgType::Emote, make<TextContent> } - , { QStringLiteral("m.notice"), MsgType::Notice, make<TextContent> } + , { NoticeTypeKey, MsgType::Notice, make<TextContent> } , { QStringLiteral("m.image"), MsgType::Image, make<ImageContent> } , { QStringLiteral("m.file"), MsgType::File, make<FileContent> } , { QStringLiteral("m.location"), MsgType::Location, make<LocationContent> } @@ -71,22 +91,26 @@ MsgType jsonToMsgType(const QString& matrixType) return MsgType::Unknown; } -inline QJsonObject toMsgJson(const QString& plainBody, const QString& jsonMsgType, - TypedBase* content) +QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, + const QString& jsonMsgType, TypedBase* content) { auto json = content ? content->toJson() : QJsonObject(); + if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey && + json.contains(RelatesToKey)) + { + json.remove(RelatesToKey); + qCWarning(EVENTS) << RelatesToKey << "cannot be used in" << jsonMsgType + << "messages; the relation has been stripped off"; + } json.insert(QStringLiteral("msgtype"), jsonMsgType); json.insert(QStringLiteral("body"), plainBody); return json; } -static const auto MsgTypeKey = "msgtype"_ls; -static const auto BodyKey = "body"_ls; - RoomMessageEvent::RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) : RoomEvent(typeId(), matrixTypeId(), - toMsgJson(plainBody, jsonMsgType, content)) + assembleContentJson(plainBody, jsonMsgType, content)) , _content(content) { } @@ -95,6 +119,40 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody, : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content) { } +TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) +{ + auto filePath = file.absoluteFilePath(); + auto localUrl = QUrl::fromLocalFile(filePath); + auto mimeType = QMimeDatabase().mimeTypeForFile(file); + if (!asGenericFile) + { + auto mimeTypeName = mimeType.name(); + if (mimeTypeName.startsWith("image/")) + return new ImageContent(localUrl, file.size(), mimeType, + 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(), + file.fileName()); + + if (mimeTypeName.startsWith("audio/")) + return new AudioContent(localUrl, file.size(), mimeType, + file.fileName()); + } + return new FileContent(localUrl, file.size(), mimeType, file.fileName()); +} + +RoomMessageEvent::RoomMessageEvent(const QString& plainBody, + const QFileInfo& file, bool asGenericFile) + : RoomMessageEvent(plainBody, + asGenericFile ? QStringLiteral("m.file") : rawMsgTypeForFile(file), + contentFromFile(file, asGenericFile)) +{ } + RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj), _content(nullptr) { @@ -104,13 +162,17 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) if ( content.contains(MsgTypeKey) && content.contains(BodyKey) ) { auto msgtype = content[MsgTypeKey].toString(); + bool msgTypeFound = false; for (const auto& mt: msgTypes) if (mt.matrixType == msgtype) + { _content.reset(mt.maker(content)); + msgTypeFound = true; + } - if (!_content) + if (!msgTypeFound) { - qCWarning(EVENTS) << "RoomMessageEvent: couldn't load content," + qCWarning(EVENTS) << "RoomMessageEvent: unknown msg_type," << " full content dump follows"; qCWarning(EVENTS) << formatJson << content; } @@ -142,14 +204,13 @@ QMimeType RoomMessageEvent::mimeType() const static const auto PlainTextMimeType = QMimeDatabase().mimeTypeForName("text/plain"); return _content ? _content->type() : PlainTextMimeType; - ; } bool RoomMessageEvent::hasTextContent() const { - return content() && + return !content() || (msgtype() == MsgType::Text || msgtype() == MsgType::Emote || - msgtype() == MsgType::Notice); // FIXME: Unbind from specific msgtypes + msgtype() == MsgType::Notice); } bool RoomMessageEvent::hasFileContent() const @@ -162,10 +223,31 @@ bool RoomMessageEvent::hasThumbnail() const return content() && content()->thumbnailInfo(); } -TextContent::TextContent(const QString& text, const QString& contentType) +QString rawMsgTypeForMimeType(const QMimeType& mimeType) +{ + auto name = mimeType.name(); + return name.startsWith("image/") ? QStringLiteral("m.image") : + name.startsWith("video/") ? QStringLiteral("m.video") : + name.startsWith("audio/") ? QStringLiteral("m.audio") : + QStringLiteral("m.file"); +} + +QString RoomMessageEvent::rawMsgTypeForUrl(const QUrl& url) +{ + return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForUrl(url)); +} + +QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi) +{ + return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi)); +} + +TextContent::TextContent(const QString& text, const QString& contentType, + Omittable<RelatesTo> relatesTo) : mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text) + , relatesTo(std::move(relatesTo)) { - if (contentType == "org.matrix.custom.html") + if (contentType == HtmlContentTypeId) mimeType = QMimeDatabase().mimeTypeForName("text/html"); } @@ -177,16 +259,20 @@ TextContent::TextContent(const QJsonObject& json) // Special-casing the custom matrix.org's (actually, Riot's) way // of sending HTML messages. - if (json["format"_ls].toString() == "org.matrix.custom.html") + if (json["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; - body = json["formatted_body"_ls].toString(); + body = json[FormattedBodyKey].toString(); } else { // Falling back to plain text, as there's no standard way to describe // rich text in messages. mimeType = PlainTextMimeType; body = json[BodyKey].toString(); } + const auto replyJson = json[RelatesToKey].toObject() + .value(RelatesTo::ReplyTypeId()).toObject(); + if (!replyJson.isEmpty()) + relatesTo = replyTo(fromJson<QString>(replyJson[EventIdKeyL])); } void TextContent::fillJson(QJsonObject* json) const @@ -194,13 +280,16 @@ void TextContent::fillJson(QJsonObject* json) const Q_ASSERT(json); if (mimeType.inherits("text/html")) { - json->insert(QStringLiteral("format"), - QStringLiteral("org.matrix.custom.html")); + json->insert(QStringLiteral("format"), HtmlContentTypeId); json->insert(QStringLiteral("formatted_body"), body); } + if (!relatesTo.omitted()) + json->insert(QStringLiteral("m.relates_to"), + QJsonObject { { relatesTo->type, relatesTo->eventId } }); } -LocationContent::LocationContent(const QString& geoUri, const ImageInfo& thumbnail) +LocationContent::LocationContent(const QString& geoUri, + const Thumbnail& thumbnail) : geoUri(geoUri), thumbnail(thumbnail) { } diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 4c29a93e..c2e075eb 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -21,6 +21,8 @@ #include "roomevent.h" #include "eventcontent.h" +class QFileInfo; + namespace QMatrixClient { namespace MessageEventContent = EventContent; // Back-compatibility @@ -49,6 +51,9 @@ namespace QMatrixClient explicit RoomMessageEvent(const QString& plainBody, MsgType msgType = MsgType::Text, EventContent::TypedBase* content = nullptr); + explicit RoomMessageEvent(const QString& plainBody, + const QFileInfo& file, + bool asGenericFile = false); explicit RoomMessageEvent(const QJsonObject& obj); MsgType msgtype() const; @@ -56,14 +61,27 @@ namespace QMatrixClient QString plainBody() const; EventContent::TypedBase* content() const { return _content.data(); } + template <typename VisitorT> + void editContent(VisitorT visitor) + { + visitor(*_content); + editJson()[ContentKeyL] = + assembleContentJson(plainBody(), rawMsgtype(), content()); + } QMimeType mimeType() const; bool hasTextContent() const; bool hasFileContent() const; bool hasThumbnail() const; + static QString rawMsgTypeForUrl(const QUrl& url); + static QString rawMsgTypeForFile(const QFileInfo& fi); + private: QScopedPointer<EventContent::TypedBase> _content; + static QJsonObject assembleContentJson(const QString& plainBody, + const QString& jsonMsgType, EventContent::TypedBase* content); + REGISTER_ENUM(MsgType) }; REGISTER_EVENT_TYPE(RoomMessageEvent) @@ -74,6 +92,17 @@ namespace QMatrixClient { // Additional event content types + struct RelatesTo + { + static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; } + QString type; // The only supported relation so far + QString eventId; + }; + inline RelatesTo replyTo(QString eventId) + { + return { RelatesTo::ReplyTypeId(), std::move(eventId) }; + } + /** * Rich text content for m.text, m.emote, m.notice * @@ -83,13 +112,15 @@ namespace QMatrixClient class TextContent: public TypedBase { public: - TextContent(const QString& text, const QString& contentType); + TextContent(const QString& text, const QString& contentType, + Omittable<RelatesTo> relatesTo = none); explicit TextContent(const QJsonObject& json); QMimeType type() const override { return mimeType; } QMimeType mimeType; QString body; + Omittable<RelatesTo> relatesTo; protected: void fillJson(QJsonObject* json) const override; @@ -112,7 +143,7 @@ namespace QMatrixClient { public: LocationContent(const QString& geoUri, - const ImageInfo& thumbnail); + const Thumbnail& thumbnail = {}); explicit LocationContent(const QJsonObject& json); QMimeType type() const override; @@ -132,6 +163,7 @@ namespace QMatrixClient class PlayableContent : public ContentT { public: + using ContentT::ContentT; PlayableContent(const QJsonObject& json) : ContentT(json) , duration(ContentT::originalInfoJson["duration"_ls].toInt()) diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp new file mode 100644 index 00000000..9c3bafd4 --- /dev/null +++ b/lib/events/roomtombstoneevent.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "roomtombstoneevent.h" + +using namespace QMatrixClient; + +QString RoomTombstoneEvent::serverMessage() const +{ + return fromJson<QString>(contentJson()["body"_ls]); +} + +QString RoomTombstoneEvent::successorRoomId() const +{ + return fromJson<QString>(contentJson()["replacement_room"_ls]); +} diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h new file mode 100644 index 00000000..c7008ec4 --- /dev/null +++ b/lib/events/roomtombstoneevent.h @@ -0,0 +1,41 @@ +/****************************************************************************** +* Copyright (C) 2019 QMatrixClient project +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#include "stateevent.h" + +namespace QMatrixClient +{ + class RoomTombstoneEvent : public StateEventBase + { + public: + DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent) + + explicit RoomTombstoneEvent() + : StateEventBase(typeId(), matrixTypeId()) + { } + explicit RoomTombstoneEvent(const QJsonObject& obj) + : StateEventBase(typeId(), obj) + { } + + QString serverMessage() const; + QString successorRoomId() const; + }; + REGISTER_EVENT_TYPE(RoomTombstoneEvent) +} diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 56be947c..2c23d9ca 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -19,7 +19,6 @@ #pragma once #include "stateevent.h" -#include "eventcontent.h" #include "converters.h" @@ -28,7 +27,7 @@ namespace QMatrixClient namespace EventContent { template <typename T> - class SimpleContent: public Base + class SimpleContent { public: using value_type = T; @@ -39,41 +38,39 @@ namespace QMatrixClient : value(std::forward<TT>(value)), key(std::move(keyName)) { } SimpleContent(const QJsonObject& json, QString keyName) - : Base(json) - , value(QMatrixClient::fromJson<T>(json[keyName])) + : value(fromJson<T>(json[keyName])) , key(std::move(keyName)) { } + QJsonObject toJson() const + { + return { { key, QMatrixClient::toJson(value) } }; + } public: T value; protected: QString key; - - private: - void fillJson(QJsonObject* json) const override - { - Q_ASSERT(json); - json->insert(key, QMatrixClient::toJson(value)); - } }; } // namespace EventContent -#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ - class _Name : public StateEvent<EventContent::SimpleContent<_ContentType>> \ +#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ + class _Name : public StateEvent<EventContent::SimpleContent<_ValueType>> \ { \ public: \ - using content_type = _ContentType; \ + using value_type = content_type::value_type; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(const QJsonObject& obj) \ - : StateEvent(typeId(), obj, QStringLiteral(#_ContentKey)) \ - { } \ + explicit _Name() : _Name(value_type()) { } \ template <typename T> \ explicit _Name(T&& value) \ : StateEvent(typeId(), matrixTypeId(), \ 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) \ diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 877d0fae..a84f302b 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -20,17 +20,20 @@ using namespace QMatrixClient; +// Aside from the normal factory to instantiate StateEventBase inheritors +// StateEventBase itself can be instantiated if there's a state_key JSON key +// but the event type is unknown. [[gnu::unused]] static auto stateEventTypeInitialised = RoomEvent::factory_t::addMethod( [] (const QJsonObject& json, const QString& matrixType) -> StateEventPtr { - if (!json.contains("state_key")) + if (!json.contains("state_key"_ls)) return nullptr; if (auto e = StateEventBase::factory_t::make(json, matrixType)) return e; - return nullptr; + return makeEvent<StateEventBase>(unknownEventTypeId(), json); }); bool StateEventBase::repeatsState() const @@ -38,3 +41,18 @@ bool StateEventBase::repeatsState() const const auto prevContentJson = unsignedJson().value(PrevContentKeyL); return fullJson().value(ContentKeyL) == prevContentJson; } + +QString StateEventBase::replacedState() const +{ + return unsignedJson().value("replaces_state"_ls).toString(); +} + +void StateEventBase::dumpTo(QDebug dbg) const +{ + if (!stateKey().isEmpty()) + dbg << '<' << stateKey() << "> "; + if (unsignedJson().contains(PrevContentKeyL)) + dbg << QJsonDocument(unsignedJson()[PrevContentKeyL].toObject()) + .toJson(QJsonDocument::Compact) << " -> "; + RoomEvent::dumpTo(dbg); +} diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 6032132e..3f54f7bf 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -30,11 +30,24 @@ namespace QMatrixClient { ~StateEventBase() override = default; bool isStateEvent() const override { return true; } + QString replacedState() const; + void dumpTo(QDebug dbg) const override; + virtual bool repeatsState() const; }; using StateEventPtr = event_ptr_tt<StateEventBase>; using StateEvents = EventsArray<StateEventBase>; + template <> + inline bool is<StateEventBase>(const Event& e) { return e.isStateEvent(); } + + /** + * A combination of event type and state key uniquely identifies a piece + * of state in Matrix. + * \sa https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events + */ + using StateEventKey = QPair<QString, QString>; + template <typename ContentT> struct Prev { @@ -78,6 +91,12 @@ namespace QMatrixClient { } const ContentT& content() const { return _content; } + template <typename VisitorT> + void editContent(VisitorT&& visitor) + { + visitor(_content); + editJson()[ContentKeyL] = _content.toJson(); + } [[deprecated("Use prevContent instead")]] const ContentT* prev_content() const { return prevContent(); } const ContentT* prevContent() const @@ -85,8 +104,18 @@ namespace QMatrixClient { QString prevSenderId() const { return _prev ? _prev->senderId : QString(); } - protected: + private: ContentT _content; std::unique_ptr<Prev<ContentT>> _prev; }; } // namespace QMatrixClient + +namespace std { + template <> struct hash<QMatrixClient::StateEventKey> + { + size_t operator()(const QMatrixClient::StateEventKey& k) const Q_DECL_NOEXCEPT + { + return qHash(k); + } + }; +} |