1 /******************************************************************************
2 * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 */
18
19 #include "roommessageevent.h"
20
21 #include "logging.h"
22
23 #include <QtCore/QFileInfo>
24 #include <QtCore/QMimeDatabase>
25 #include <QtGui/QImageReader>
26 #include <QtMultimedia/QMediaResource>
27
28 using namespace Quotient;
29 using namespace EventContent;
30
31 using MsgType = RoomMessageEvent::MsgType;
32
33 static const auto RelatesToKeyL = "m.relates_to"_ls;
34 static const auto MsgTypeKeyL = "msgtype"_ls;
35 static const auto FormattedBodyKeyL = "formatted_body"_ls;
36
37 static const auto TextTypeKey = "m.text";
38 static const auto EmoteTypeKey = "m.emote";
39 static const auto NoticeTypeKey = "m.notice";
40
41 static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html");
42
43 template <typename ContentT>
make(const QJsonObject & json)44 TypedBase* make(const QJsonObject& json)
45 {
46 return new ContentT(json);
47 }
48
49 template <>
make(const QJsonObject & json)50 TypedBase* make<TextContent>(const QJsonObject& json)
51 {
52 return json.contains(FormattedBodyKeyL) || json.contains(RelatesToKeyL)
53 ? new TextContent(json)
54 : nullptr;
55 }
56
57 struct MsgTypeDesc {
58 QString matrixType;
59 MsgType enumType;
60 TypedBase* (*maker)(const QJsonObject&);
61 };
62
63 const std::vector<MsgTypeDesc> msgTypes = {
64 { TextTypeKey, MsgType::Text, make<TextContent> },
65 { EmoteTypeKey, MsgType::Emote, make<TextContent> },
66 { NoticeTypeKey, MsgType::Notice, make<TextContent> },
67 { QStringLiteral("m.image"), MsgType::Image, make<ImageContent> },
68 { QStringLiteral("m.file"), MsgType::File, make<FileContent> },
69 { QStringLiteral("m.location"), MsgType::Location, make<LocationContent> },
70 { QStringLiteral("m.video"), MsgType::Video, make<VideoContent> },
71 { QStringLiteral("m.audio"), MsgType::Audio, make<AudioContent> }
72 };
73
msgTypeToJson(MsgType enumType)74 QString msgTypeToJson(MsgType enumType)
75 {
76 auto it = std::find_if(msgTypes.begin(), msgTypes.end(),
77 [=](const MsgTypeDesc& mtd) {
78 return mtd.enumType == enumType;
79 });
80 if (it != msgTypes.end())
81 return it->matrixType;
82
83 return {};
84 }
85
jsonToMsgType(const QString & matrixType)86 MsgType jsonToMsgType(const QString& matrixType)
87 {
88 auto it = std::find_if(msgTypes.begin(), msgTypes.end(),
89 [=](const MsgTypeDesc& mtd) {
90 return mtd.matrixType == matrixType;
91 });
92 if (it != msgTypes.end())
93 return it->enumType;
94
95 return MsgType::Unknown;
96 }
97
isReplacement(const Omittable<RelatesTo> & rel)98 inline bool isReplacement(const Omittable<RelatesTo>& rel)
99 {
100 return rel && rel->type == RelatesTo::ReplacementTypeId();
101 }
102
assembleContentJson(const QString & plainBody,const QString & jsonMsgType,TypedBase * content)103 QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody,
104 const QString& jsonMsgType,
105 TypedBase* content)
106 {
107 auto json = content ? content->toJson() : QJsonObject();
108 if (json.contains(RelatesToKeyL)) {
109 if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey
110 && jsonMsgType != EmoteTypeKey) {
111 json.remove(RelatesToKeyL);
112 qCWarning(EVENTS)
113 << RelatesToKeyL << "cannot be used in" << jsonMsgType
114 << "messages; the relation has been stripped off";
115 } else {
116 // After the above, we know for sure that the content is TextContent
117 // and that its RelatesTo structure is not omitted
118 auto* textContent = static_cast<const TextContent*>(content);
119 Q_ASSERT(textContent && textContent->relatesTo.has_value());
120 if (textContent->relatesTo->type == RelatesTo::ReplacementTypeId()) {
121 auto newContentJson = json.take("m.new_content"_ls).toObject();
122 newContentJson.insert(BodyKey, plainBody);
123 newContentJson.insert(MsgTypeKeyL, jsonMsgType);
124 json.insert(QStringLiteral("m.new_content"), newContentJson);
125 json[MsgTypeKeyL] = jsonMsgType;
126 json[BodyKeyL] = "* " + plainBody;
127 return json;
128 }
129 }
130 }
131 json.insert(QStringLiteral("msgtype"), jsonMsgType);
132 json.insert(QStringLiteral("body"), plainBody);
133 return json;
134 }
135
RoomMessageEvent(const QString & plainBody,const QString & jsonMsgType,TypedBase * content)136 RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
137 const QString& jsonMsgType,
138 TypedBase* content)
139 : RoomEvent(typeId(), matrixTypeId(),
140 assembleContentJson(plainBody, jsonMsgType, content))
141 , _content(content)
142 {}
143
RoomMessageEvent(const QString & plainBody,MsgType msgType,TypedBase * content)144 RoomMessageEvent::RoomMessageEvent(const QString& plainBody, MsgType msgType,
145 TypedBase* content)
146 : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content)
147 {}
148
contentFromFile(const QFileInfo & file,bool asGenericFile)149 TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile)
150 {
151 auto filePath = file.absoluteFilePath();
152 auto localUrl = QUrl::fromLocalFile(filePath);
153 auto mimeType = QMimeDatabase().mimeTypeForFile(file);
154 if (!asGenericFile) {
155 auto mimeTypeName = mimeType.name();
156 if (mimeTypeName.startsWith("image/"))
157 return new ImageContent(localUrl, file.size(), mimeType,
158 QImageReader(filePath).size(),
159 file.fileName());
160
161 // duration can only be obtained asynchronously and can only be reliably
162 // done by starting to play the file. Left for a future implementation.
163 if (mimeTypeName.startsWith("video/"))
164 return new VideoContent(localUrl, file.size(), mimeType,
165 QMediaResource(localUrl).resolution(),
166 file.fileName());
167
168 if (mimeTypeName.startsWith("audio/"))
169 return new AudioContent(localUrl, file.size(), mimeType,
170 file.fileName());
171 }
172 return new FileContent(localUrl, file.size(), mimeType, file.fileName());
173 }
174
RoomMessageEvent(const QString & plainBody,const QFileInfo & file,bool asGenericFile)175 RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
176 const QFileInfo& file, bool asGenericFile)
177 : RoomMessageEvent(plainBody,
178 asGenericFile ? QStringLiteral("m.file")
179 : rawMsgTypeForFile(file),
180 contentFromFile(file, asGenericFile))
181 {}
182
RoomMessageEvent(const QJsonObject & obj)183 RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj)
184 : RoomEvent(typeId(), obj), _content(nullptr)
185 {
186 if (isRedacted())
187 return;
188 const QJsonObject content = contentJson();
189 if (content.contains(MsgTypeKeyL) && content.contains(BodyKeyL)) {
190 auto msgtype = content[MsgTypeKeyL].toString();
191 bool msgTypeFound = false;
192 for (const auto& mt : msgTypes)
193 if (mt.matrixType == msgtype) {
194 _content.reset(mt.maker(content));
195 msgTypeFound = true;
196 }
197
198 if (!msgTypeFound) {
199 qCWarning(EVENTS) << "RoomMessageEvent: unknown msg_type,"
200 << " full content dump follows";
201 qCWarning(EVENTS) << formatJson << content;
202 }
203 } else {
204 qCWarning(EVENTS) << "No body or msgtype in room message event";
205 qCWarning(EVENTS) << formatJson << obj;
206 }
207 }
208
msgtype() const209 RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const
210 {
211 return jsonToMsgType(rawMsgtype());
212 }
213
rawMsgtype() const214 QString RoomMessageEvent::rawMsgtype() const
215 {
216 return contentJson()[MsgTypeKeyL].toString();
217 }
218
plainBody() const219 QString RoomMessageEvent::plainBody() const
220 {
221 return contentJson()[BodyKeyL].toString();
222 }
223
mimeType() const224 QMimeType RoomMessageEvent::mimeType() const
225 {
226 static const auto PlainTextMimeType =
227 QMimeDatabase().mimeTypeForName("text/plain");
228 return _content ? _content->type() : PlainTextMimeType;
229 }
230
hasTextContent() const231 bool RoomMessageEvent::hasTextContent() const
232 {
233 return !content()
234 || (msgtype() == MsgType::Text || msgtype() == MsgType::Emote
235 || msgtype() == MsgType::Notice);
236 }
237
hasFileContent() const238 bool RoomMessageEvent::hasFileContent() const
239 {
240 return content() && content()->fileInfo();
241 }
242
hasThumbnail() const243 bool RoomMessageEvent::hasThumbnail() const
244 {
245 return content() && content()->thumbnailInfo();
246 }
247
replacedEvent() const248 QString RoomMessageEvent::replacedEvent() const
249 {
250 if (!content() || !hasTextContent())
251 return {};
252
253 const auto& rel = static_cast<const TextContent*>(content())->relatesTo;
254 return isReplacement(rel) ? rel->eventId : QString();
255 }
256
rawMsgTypeForMimeType(const QMimeType & mimeType)257 QString rawMsgTypeForMimeType(const QMimeType& mimeType)
258 {
259 auto name = mimeType.name();
260 return name.startsWith("image/")
261 ? QStringLiteral("m.image")
262 : name.startsWith("video/")
263 ? QStringLiteral("m.video")
264 : name.startsWith("audio/") ? QStringLiteral("m.audio")
265 : QStringLiteral("m.file");
266 }
267
rawMsgTypeForUrl(const QUrl & url)268 QString RoomMessageEvent::rawMsgTypeForUrl(const QUrl& url)
269 {
270 return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForUrl(url));
271 }
272
rawMsgTypeForFile(const QFileInfo & fi)273 QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi)
274 {
275 return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi));
276 }
277
TextContent(QString text,const QString & contentType,Omittable<RelatesTo> relatesTo)278 TextContent::TextContent(QString text, const QString& contentType,
279 Omittable<RelatesTo> relatesTo)
280 : mimeType(QMimeDatabase().mimeTypeForName(contentType))
281 , body(std::move(text))
282 , relatesTo(std::move(relatesTo))
283 {
284 if (contentType == HtmlContentTypeId)
285 mimeType = QMimeDatabase().mimeTypeForName("text/html");
286 }
287
288 namespace Quotient {
289 // Overload the default fromJson<> logic that defined in converters.h
290 // as we want
291 template <>
fromJson(const QJsonValue & jv)292 Omittable<RelatesTo> fromJson(const QJsonValue& jv)
293 {
294 const auto jo = jv.toObject();
295 if (jo.isEmpty())
296 return none;
297 const auto replyJson = jo.value(RelatesTo::ReplyTypeId()).toObject();
298 if (!replyJson.isEmpty())
299 return replyTo(fromJson<QString>(replyJson[EventIdKeyL]));
300
301 return RelatesTo { jo.value("rel_type"_ls).toString(),
302 jo.value(EventIdKeyL).toString() };
303 }
304 } // namespace Quotient
305
TextContent(const QJsonObject & json)306 TextContent::TextContent(const QJsonObject& json)
307 : relatesTo(fromJson<Omittable<RelatesTo>>(json[RelatesToKeyL]))
308 {
309 QMimeDatabase db;
310 static const auto PlainTextMimeType = db.mimeTypeForName("text/plain");
311 static const auto HtmlMimeType = db.mimeTypeForName("text/html");
312
313 const auto actualJson = isReplacement(relatesTo)
314 ? json.value("m.new_content"_ls).toObject()
315 : json;
316 // Special-casing the custom matrix.org's (actually, Riot's) way
317 // of sending HTML messages.
318 if (actualJson["format"_ls].toString() == HtmlContentTypeId) {
319 mimeType = HtmlMimeType;
320 body = actualJson[FormattedBodyKeyL].toString();
321 } else {
322 // Falling back to plain text, as there's no standard way to describe
323 // rich text in messages.
324 mimeType = PlainTextMimeType;
325 body = actualJson[BodyKeyL].toString();
326 }
327 }
328
fillJson(QJsonObject * json) const329 void TextContent::fillJson(QJsonObject* json) const
330 {
331 static const auto FormatKey = QStringLiteral("format");
332 static const auto FormattedBodyKey = QStringLiteral("formatted_body");
333
334 Q_ASSERT(json);
335 if (mimeType.inherits("text/html")) {
336 json->insert(FormatKey, HtmlContentTypeId);
337 json->insert(FormattedBodyKey, body);
338 }
339 if (relatesTo) {
340 json->insert(QStringLiteral("m.relates_to"),
341 QJsonObject { { "rel_type", relatesTo->type }, { EventIdKey, relatesTo->eventId } });
342 if (relatesTo->type == RelatesTo::ReplacementTypeId()) {
343 QJsonObject newContentJson;
344 if (mimeType.inherits("text/html")) {
345 json->insert(FormatKey, HtmlContentTypeId);
346 json->insert(FormattedBodyKey, body);
347 }
348 json->insert(QStringLiteral("m.new_content"), newContentJson);
349 }
350 }
351 }
352
LocationContent(const QString & geoUri,const Thumbnail & thumbnail)353 LocationContent::LocationContent(const QString& geoUri,
354 const Thumbnail& thumbnail)
355 : geoUri(geoUri), thumbnail(thumbnail)
356 {}
357
LocationContent(const QJsonObject & json)358 LocationContent::LocationContent(const QJsonObject& json)
359 : TypedBase(json)
360 , geoUri(json["geo_uri"_ls].toString())
361 , thumbnail(json["info"_ls].toObject())
362 {}
363
type() const364 QMimeType LocationContent::type() const
365 {
366 return QMimeDatabase().mimeTypeForData(geoUri.toLatin1());
367 }
368
fillJson(QJsonObject * o) const369 void LocationContent::fillJson(QJsonObject* o) const
370 {
371 Q_ASSERT(o);
372 o->insert(QStringLiteral("geo_uri"), geoUri);
373 o->insert(QStringLiteral("info"), toInfoJson(thumbnail));
374 }
375