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