| 1 | // SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> |
| 2 | // SPDX-License-Identifier: LGPL-2.1-or-later |
| 3 | |
| 4 | #pragma once |
| 5 | |
| 6 | // This file contains generic event content definitions, applicable to room |
| 7 | // message events as well as other events (e.g., avatars). |
| 8 | |
| 9 | #include "filesourceinfo.h" |
| 10 | #include <Quotient/quotient_export.h> |
| 11 | |
| 12 | #include <QtCore/QJsonObject> |
| 13 | #include <QtCore/QMetaType> |
| 14 | #include <QtCore/QMimeType> |
| 15 | #include <QtCore/QSize> |
| 16 | #include <QtCore/QUrl> |
| 17 | |
| 18 | class QFileInfo; |
| 19 | |
| 20 | namespace Quotient::EventContent { |
| 21 | //! \brief Base for all content types that can be stored in RoomMessageEvent |
| 22 | //! |
| 23 | //! Each content type class should have a constructor taking |
| 24 | //! a QJsonObject and override fillJson() with an implementation |
| 25 | //! that will fill the target QJsonObject with stored values. It is |
| 26 | //! assumed but not required that a content object can also be created |
| 27 | //! from plain data. |
| 28 | class QUOTIENT_API Base { |
| 29 | public: |
| 30 | explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {} |
| 31 | virtual ~Base() = default; |
| 32 | |
| 33 | QJsonObject toJson() const; |
| 34 | |
| 35 | public: |
| 36 | QJsonObject originalJson; |
| 37 | |
| 38 | // You can't assign those classes |
| 39 | Base& operator=(const Base&) = delete; |
| 40 | Base& operator=(Base&&) = delete; |
| 41 | |
| 42 | protected: |
| 43 | Base(const Base&) = default; |
| 44 | Base(Base&&) noexcept = default; |
| 45 | |
| 46 | virtual void fillJson(QJsonObject&) const = 0; |
| 47 | }; |
| 48 | |
| 49 | // The below structures fairly follow CS spec 11.2.1.6. The overall |
| 50 | // set of attributes for each content types is a superset of the spec |
| 51 | // but specific aggregation structure is altered. See doc comments to |
| 52 | // each type for the list of available attributes. |
| 53 | |
| 54 | // A quick classes inheritance structure follows (the definitions are |
| 55 | // spread across eventcontent.h and roommessageevent.h): |
| 56 | // UrlBasedContent<InfoT> : InfoT + thumbnail data |
| 57 | // PlayableContent<InfoT> : + duration attribute |
| 58 | // FileInfo |
| 59 | // FileContent = UrlBasedContent<FileInfo> |
| 60 | // AudioContent = PlayableContent<FileInfo> |
| 61 | // ImageInfo : FileInfo + imageSize attribute |
| 62 | // ImageContent = UrlBasedContent<ImageInfo> |
| 63 | // VideoContent = PlayableContent<ImageInfo> |
| 64 | |
| 65 | //! \brief Mix-in class representing `info` subobject in content JSON |
| 66 | //! |
| 67 | //! This is one of base classes for content types that deal with files or |
| 68 | //! URLs. It stores the file metadata attributes, such as size, MIME type |
| 69 | //! etc. found in the `content/info` subobject of event JSON payloads. |
| 70 | //! Actual content classes derive from this class _and_ TypedBase that |
| 71 | //! provides a polymorphic interface to access data in the mix-in. FileInfo |
| 72 | //! (as well as ImageInfo, that adds image size to the metadata) is NOT |
| 73 | //! polymorphic and is used in a non-polymorphic way to store thumbnail |
| 74 | //! metadata (in a separate instance), next to the metadata on the file |
| 75 | //! itself. |
| 76 | //! |
| 77 | //! If you need to make a new _content_ (not info) class based on files/URLs |
| 78 | //! take UrlBasedContent as the example, i.e.: |
| 79 | //! 1. Double-inherit from this class (or ImageInfo) and TypedBase. |
| 80 | //! 2. Provide a constructor from QJsonObject that will pass the `info` |
| 81 | //! subobject (not the whole content JSON) down to FileInfo/ImageInfo. |
| 82 | //! 3. Override fillJson() to customise the JSON export logic. Make sure |
| 83 | //! to call toInfoJson() from it to produce the payload for the `info` |
| 84 | //! subobject in the JSON payload. |
| 85 | //! |
| 86 | //! \sa ImageInfo, FileContent, ImageContent, AudioContent, VideoContent, |
| 87 | //! UrlBasedContent |
| 88 | struct QUOTIENT_API FileInfo { |
| 89 | FileInfo() = default; |
| 90 | //! \brief Construct from a QFileInfo object |
| 91 | //! |
| 92 | //! \param fi a QFileInfo object referring to an existing file |
| 93 | explicit FileInfo(const QFileInfo& fi); |
| 94 | explicit FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize = -1, |
| 95 | const QMimeType& mimeType = {}, |
| 96 | QString originalFilename = {}); |
| 97 | //! \brief Construct from a JSON `info` payload |
| 98 | //! |
| 99 | //! Make sure to pass the `info` subobject of content JSON, not the |
| 100 | //! whole JSON content. |
| 101 | FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, |
| 102 | QString originalFilename = {}); |
| 103 | |
| 104 | bool isValid() const; |
| 105 | QUrl url() const; |
| 106 | |
| 107 | //! \brief Extract media id from the URL |
| 108 | //! |
| 109 | //! This can be used, e.g., to construct a QML-facing image:// |
| 110 | //! URI as follows: |
| 111 | //! \code "image://provider/" + info.mediaId() \endcode |
| 112 | QString mediaId() const { return url().authority() + url().path(); } |
| 113 | |
| 114 | FileSourceInfo source; |
| 115 | QJsonObject originalInfoJson; |
| 116 | QMimeType mimeType; |
| 117 | qint64 payloadSize = 0; |
| 118 | QString originalName; |
| 119 | }; |
| 120 | |
| 121 | QUOTIENT_API QJsonObject toInfoJson(const FileInfo& info); |
| 122 | |
| 123 | //! \brief A content info class for image/video content types and thumbnails |
| 124 | struct QUOTIENT_API ImageInfo : public FileInfo { |
| 125 | ImageInfo() = default; |
| 126 | explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); |
| 127 | explicit ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize = -1, |
| 128 | const QMimeType& type = {}, QSize imageSize = {}, |
| 129 | const QString& originalFilename = {}); |
| 130 | ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, |
| 131 | const QString& originalFilename = {}); |
| 132 | |
| 133 | QSize imageSize; |
| 134 | }; |
| 135 | |
| 136 | QUOTIENT_API QJsonObject toInfoJson(const ImageInfo& info); |
| 137 | |
| 138 | //! \brief An auxiliary class for an info type that carries a thumbnail |
| 139 | //! |
| 140 | //! This class saves/loads a thumbnail to/from `info` subobject of |
| 141 | //! the JSON representation of event content; namely, `info/thumbnail_url` |
| 142 | //! (or, in case of an encrypted thumbnail, `info/thumbnail_file`) and |
| 143 | //! `info/thumbnail_info` fields are used. |
| 144 | struct QUOTIENT_API Thumbnail : public ImageInfo { |
| 145 | using ImageInfo::ImageInfo; |
| 146 | explicit Thumbnail(const QJsonObject& infoJson, |
| 147 | const Omittable<EncryptedFileMetadata>& efm = none); |
| 148 | |
| 149 | //! \brief Add thumbnail information to the passed `info` JSON object |
| 150 | void dumpTo(QJsonObject& infoJson) const; |
| 151 | }; |
| 152 | |
| 153 | class QUOTIENT_API TypedBase : public Base { |
| 154 | public: |
| 155 | virtual QMimeType type() const = 0; |
| 156 | virtual const FileInfo* fileInfo() const { return nullptr; } |
| 157 | virtual FileInfo* fileInfo() { return nullptr; } |
| 158 | virtual const Thumbnail* thumbnailInfo() const { return nullptr; } |
| 159 | |
| 160 | protected: |
| 161 | explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {} |
| 162 | using Base::Base; |
| 163 | }; |
| 164 | |
| 165 | //! \brief A template class for content types with a URL and additional info |
| 166 | //! |
| 167 | //! Types that derive from this class template take `url` (or, if the file |
| 168 | //! is encrypted, `file`) and, optionally, `filename` values from |
| 169 | //! the top-level JSON object and the rest of information from the `info` |
| 170 | //! subobject, as defined by the parameter type. |
| 171 | //! \tparam InfoT base info class - FileInfo or ImageInfo |
| 172 | template <class InfoT> |
| 173 | class UrlBasedContent : public TypedBase, public InfoT { |
| 174 | public: |
| 175 | using InfoT::InfoT; |
| 176 | explicit UrlBasedContent(const QJsonObject& json) |
| 177 | : TypedBase(json) |
| 178 | , InfoT(QUrl(json["url"_ls ].toString()), json["info"_ls ].toObject(), |
| 179 | json["filename"_ls ].toString()) |
| 180 | , thumbnail(FileInfo::originalInfoJson) |
| 181 | { |
| 182 | if (const auto efmJson = json.value(key: "file"_ls ).toObject(); |
| 183 | !efmJson.isEmpty()) |
| 184 | InfoT::source = fromJson<EncryptedFileMetadata>(json: efmJson); |
| 185 | // Two small hacks on originalJson to expose mediaIds to QML |
| 186 | originalJson.insert("mediaId"_ls , InfoT::mediaId()); |
| 187 | originalJson.insert("thumbnailMediaId"_ls , thumbnail.mediaId()); |
| 188 | } |
| 189 | |
| 190 | QMimeType type() const override { return InfoT::mimeType; } |
| 191 | const FileInfo* fileInfo() const override { return this; } |
| 192 | FileInfo* fileInfo() override { return this; } |
| 193 | const Thumbnail* thumbnailInfo() const override { return &thumbnail; } |
| 194 | |
| 195 | public: |
| 196 | Thumbnail thumbnail; |
| 197 | |
| 198 | protected: |
| 199 | virtual void fillInfoJson(QJsonObject& infoJson [[maybe_unused]]) const |
| 200 | {} |
| 201 | |
| 202 | void fillJson(QJsonObject& json) const override |
| 203 | { |
| 204 | Quotient::fillJson(json, { "url"_ls , "file"_ls }, InfoT::source); |
| 205 | if (!InfoT::originalName.isEmpty()) |
| 206 | json.insert("filename"_ls , InfoT::originalName); |
| 207 | auto infoJson = toInfoJson(*this); |
| 208 | if (thumbnail.isValid()) |
| 209 | thumbnail.dumpTo(infoJson); |
| 210 | fillInfoJson(infoJson); |
| 211 | json.insert("info"_ls , infoJson); |
| 212 | } |
| 213 | }; |
| 214 | |
| 215 | //! \brief Content class for m.image |
| 216 | //! |
| 217 | //! Available fields: |
| 218 | //! - corresponding to the top-level JSON: |
| 219 | //! - source (corresponding to `url` or `file` in JSON) |
| 220 | //! - filename (extension to the spec) |
| 221 | //! - corresponding to the `info` subobject: |
| 222 | //! - payloadSize (`size` in JSON) |
| 223 | //! - mimeType (`mimetype` in JSON) |
| 224 | //! - imageSize (QSize for a combination of `h` and `w` in JSON) |
| 225 | //! - thumbnail.url (`thumbnail_url` in JSON) |
| 226 | //! - corresponding to the `info/thumbnail_info` subobject: contents of |
| 227 | //! thumbnail field, in the same vein as for the main image: |
| 228 | //! - payloadSize |
| 229 | //! - mimeType |
| 230 | //! - imageSize |
| 231 | using ImageContent = UrlBasedContent<ImageInfo>; |
| 232 | |
| 233 | //! \brief Content class for m.file |
| 234 | //! |
| 235 | //! Available fields: |
| 236 | //! - corresponding to the top-level JSON: |
| 237 | //! - source (corresponding to `url` or `file` in JSON) |
| 238 | //! - filename |
| 239 | //! - corresponding to the `info` subobject: |
| 240 | //! - payloadSize (`size` in JSON) |
| 241 | //! - mimeType (`mimetype` in JSON) |
| 242 | //! - thumbnail.source (`thumbnail_url` or `thumbnail_file` in JSON) |
| 243 | //! - corresponding to the `info/thumbnail_info` subobject: |
| 244 | //! - thumbnail.payloadSize |
| 245 | //! - thumbnail.mimeType |
| 246 | //! - thumbnail.imageSize (QSize for `h` and `w` in JSON) |
| 247 | using FileContent = UrlBasedContent<FileInfo>; |
| 248 | } // namespace Quotient::EventContent |
| 249 | Q_DECLARE_METATYPE(const Quotient::EventContent::TypedBase*) |
| 250 | |