// SPDX-FileCopyrightText: 2016 Kitsune Ral // SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "converters.h" #include "logging.h" #include "function_traits.h" namespace Quotient { // === event_ptr_tt<> and type casting facilities === template using event_ptr_tt = std::unique_ptr; /// Unwrap a plain pointer from a smart pointer template inline EventT* rawPtr(const event_ptr_tt& ptr) { return ptr.get(); } /// Unwrap a plain pointer and downcast it to the specified type template inline TargetEventT* weakPtrCast(const event_ptr_tt& ptr) { return static_cast(rawPtr(ptr)); } // === Standard Matrix key names and basicEventJson() === static const auto TypeKey = QStringLiteral("type"); static const auto BodyKey = QStringLiteral("body"); static const auto ContentKey = QStringLiteral("content"); static const auto EventIdKey = QStringLiteral("event_id"); static const auto SenderKey = QStringLiteral("sender"); static const auto RoomIdKey = QStringLiteral("room_id"); static const auto UnsignedKey = QStringLiteral("unsigned"); static const auto StateKeyKey = QStringLiteral("state_key"); static const auto TypeKeyL = "type"_ls; static const auto BodyKeyL = "body"_ls; static const auto ContentKeyL = "content"_ls; static const auto EventIdKeyL = "event_id"_ls; static const auto SenderKeyL = "sender"_ls; static const auto RoomIdKeyL = "room_id"_ls; static const auto UnsignedKeyL = "unsigned"_ls; static const auto RedactedCauseKeyL = "redacted_because"_ls; static const auto PrevContentKeyL = "prev_content"_ls; static const auto StateKeyKeyL = "state_key"_ls; /// Make a minimal correct Matrix event JSON inline QJsonObject basicEventJson(const QString& matrixType, const QJsonObject& content) { return { { TypeKey, matrixType }, { ContentKey, content } }; } // === Event types and event types registry === using event_type_t = size_t; using event_mtype_t = const char*; class EventTypeRegistry { public: ~EventTypeRegistry() = default; static event_type_t initializeTypeId(event_mtype_t matrixTypeId); template static inline event_type_t initializeTypeId() { return initializeTypeId(EventT::matrixTypeId()); } static QString getMatrixType(event_type_t typeId); private: EventTypeRegistry() = default; Q_DISABLE_COPY_MOVE(EventTypeRegistry) static EventTypeRegistry& get() { static EventTypeRegistry etr; return etr; } std::vector eventTypes; }; template <> inline event_type_t EventTypeRegistry::initializeTypeId() { return initializeTypeId(""); } template struct EventTypeTraits { static event_type_t id() { static const auto id = EventTypeRegistry::initializeTypeId(); return id; } }; template inline event_type_t typeId() { return EventTypeTraits>::id(); } inline event_type_t unknownEventTypeId() { return typeId(); } // === EventFactory === /** Create an event of arbitrary type from its arguments */ template inline event_ptr_tt makeEvent(ArgTs&&... args) { return std::make_unique(std::forward(args)...); } template class EventFactory { public: template static auto addMethod(FnT&& method) { factories().emplace_back(std::forward(method)); return 0; } /** Chain two type factories * Adds the factory class of EventT2 (EventT2::factory_t) to * the list in factory class of EventT1 (EventT1::factory_t) so * that when EventT1::factory_t::make() is invoked, types of * EventT2 factory are looked through as well. This is used * to include RoomEvent types into the more general Event factory, * and state event types into the RoomEvent factory. */ template static auto chainFactory() { return addMethod(&EventT::factory_t::make); } static event_ptr_tt make(const QJsonObject& json, const QString& matrixType) { for (const auto& f : factories()) if (auto e = f(json, matrixType)) return e; return nullptr; } private: static auto& factories() { using inner_factory_tt = std::function( const QJsonObject&, const QString&)>; static std::vector _factories {}; return _factories; } }; /** Add a type to its default factory * Adds a standard factory method (via makeEvent<>) for a given * type to EventT::factory_t factory class so that it can be * created dynamically from loadEvent<>(). * * \tparam EventT the type to enable dynamic creation of * \return the registered type id * \sa loadEvent, Event::type */ template inline auto setupFactory() { qDebug(EVENTS) << "Adding factory method for" << EventT::matrixTypeId(); return EventT::factory_t::addMethod([](const QJsonObject& json, const QString& jsonMatrixType) { return EventT::matrixTypeId() == jsonMatrixType ? makeEvent(json) : nullptr; }); } template 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(); return _; // Only to facilitate usage in static initialisation } // === Event === class Event { public: using Type = event_type_t; using factory_t = EventFactory; explicit Event(Type type, const QJsonObject& json); explicit Event(Type type, event_mtype_t matrixType, const QJsonObject& contentJson = {}); Q_DISABLE_COPY(Event) Event(Event&&) = default; Event& operator=(Event&&) = delete; virtual ~Event(); Type type() const { return _type; } QString matrixType() const; [[deprecated("Use fullJson() and stringify it with QJsonDocument::toJson() " "or by other means")]] QByteArray originalJson() const; [[deprecated("Use fullJson() instead")]] // QJsonObject originalJsonObject() const { return fullJson(); } const QJsonObject& fullJson() const { return _json; } // According to the CS API spec, every event also has // a "content" object; but since its structure is different for // different types, we're implementing it per-event type. // NB: const return types below are meant to catch accidental attempts // to change event JSON (e.g., consider contentJson()["inexistentKey"]). const QJsonObject contentJson() const; template const T contentPart(KeyT&& key) const { return fromJson(contentJson()[std::forward(key)]); } template [[deprecated("Use contentPart() to get a part of the event content")]] // T content(const QString& key) const { return contentPart(key); } const QJsonObject unsignedJson() const; template const T unsignedPart(KeyT&& key) const { return fromJson(unsignedJson()[std::forward(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; } protected: QJsonObject& editJson() { return _json; } virtual void dumpTo(QDebug dbg) const; private: Type _type; QJsonObject _json; }; using EventPtr = event_ptr_tt; template using EventsArray = std::vector>; using Events = EventsArray; // === Macros used with event class definitions === // This macro should be used in a public section of an event class to // provide matrixTypeId() and typeId(). #define DEFINE_EVENT_TYPEID(_Id, _Type) \ static constexpr event_mtype_t matrixTypeId() { return _Id; } \ static auto typeId() { return Quotient::typeId<_Type>(); } \ // End of macro // This macro should be put after an event class definition (in .h or .cpp) // to enable its deserialisation from a /sync and other // polymorphi RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json) { if (const auto redaction = unsignedJson()[RedactedCauseKeyL]; redaction.isObject()) _redactedBecause = makeEvent<RedactionEvent>(redaction.toObject()); } RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job QString RoomEvent::id() const { return fullJson()[EventIdKeyL].toString(); } QDateTime RoomEvent::originTimestamp() const { return Quotient::fromJson<QDateTime>(fullJson()["origin_server_ts"_ls]); } QString RoomEvent::roomId() const { return fullJson()[RoomIdKeyL].toString(); } QString RoomEvent::senderId() const { return fullJson()[SenderKeyL].toString(); } bool RoomEvent::isReplaced() const { return unsignedJson()["m.relations"_ls].toObject().contains("m.replace"); } QString RoomEvent::replacedBy() const { // clang-format off return unsignedJson()["m.relations"_ls].toObject() .value("m.replace").toObject() .value(EventIdKeyL).toString(); // clang-format on } QString RoomEvent::redactionReason() const { return isRedacted() ? _redactedBecause->reason() : QString {}; } QString RoomEvent::transactionId() const { return unsignedJson()["transaction_id"_ls].toString(); } QString RoomEvent::stateKey() const { return fullJson()[StateKeyKeyL].toString(); } void RoomEvent::setRoomId(const QString& roomId) { editJson().insert(RoomIdKey, roomId); } void RoomEvent::setSender(const QString& senderId) { editJson().insert(SenderKey, senderId); } void RoomEvent::setTransactionId(const QString& txnId) { auto unsignedData = fullJson()[UnsignedKeyL].toObject(); unsignedData.insert(QStringLiteral("transaction_id"), txnId); editJson().insert(UnsignedKey, unsignedData); Q_ASSERT(transactionId() == txnId); } void RoomEvent::addId(const QString& newId) { Q_ASSERT(id().isEmpty()); Q_ASSERT(!newId.isEmpty()); editJson().insert(EventIdKey, newId); qCDebug(EVENTS) << "Event txnId -> id:" << transactionId() << "->" << id(); Q_ASSERT(id() == newId); } void RoomEvent::dumpTo(QDebug dbg) const { Event::dumpTo(dbg); dbg << " (made at " << originTimestamp().toString(Qt::ISODate) << ')'; } QJsonObject makeCallContentJson(const QString& callId, int version, QJsonObject content) { content.insert(QStringLiteral("call_id"), callId); content.insert(QStringLiteral("version"), version); return content; } CallEventBase::CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, int version, const QJsonObject& contentJson) : RoomEvent(type, matrixType, makeCallContentJson(callId, version, contentJson)) {} CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json) : RoomEvent(type, json) { if (callId().isEmpty()) qCWarning(EVENTS) << id() << "is a call event with an empty call id"; }