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 | |