aboutsummaryrefslogtreecommitdiff
path: root/lib/events/roommessageevent.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/events/roommessageevent.cpp')
-rw-r--r--lib/events/roommessageevent.cpp304
1 files changed, 217 insertions, 87 deletions
diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp
index 1c5cf058..df4840b3 100644
--- a/lib/events/roommessageevent.cpp
+++ b/lib/events/roommessageevent.cpp
@@ -1,60 +1,71 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "roommessageevent.h"
#include "logging.h"
+#include "events/eventrelation.h"
+#include <QtCore/QFileInfo>
#include <QtCore/QMimeDatabase>
+#include <QtGui/QImageReader>
+#if QT_VERSION_MAJOR < 6
+# include <QtMultimedia/QMediaResource>
+#endif
-using namespace QMatrixClient;
+using namespace Quotient;
using namespace EventContent;
using MsgType = RoomMessageEvent::MsgType;
+namespace { // Supporting internal definitions
+constexpr auto RelatesToKey = "m.relates_to"_ls;
+constexpr auto MsgTypeKey = "msgtype"_ls;
+constexpr auto FormattedBodyKey = "formatted_body"_ls;
+constexpr auto TextTypeKey = "m.text"_ls;
+constexpr auto EmoteTypeKey = "m.emote"_ls;
+constexpr auto NoticeTypeKey = "m.notice"_ls;
+constexpr auto HtmlContentTypeId = "org.matrix.custom.html"_ls;
+
template <typename ContentT>
TypedBase* make(const QJsonObject& json)
{
return new ContentT(json);
}
-struct MsgTypeDesc
+template <>
+TypedBase* make<TextContent>(const QJsonObject& json)
{
- QString matrixType;
+ return json.contains(FormattedBodyKey) || json.contains(RelatesToKey)
+ ? new TextContent(json)
+ : nullptr;
+}
+
+struct MsgTypeDesc {
+ QLatin1String matrixType;
MsgType enumType;
TypedBase* (*maker)(const QJsonObject&);
};
-const std::vector<MsgTypeDesc> msgTypes =
- { { QStringLiteral("m.text"), MsgType::Text, make<TextContent> }
- , { QStringLiteral("m.emote"), MsgType::Emote, make<TextContent> }
- , { QStringLiteral("m.notice"), MsgType::Notice, make<TextContent> }
- , { QStringLiteral("m.image"), MsgType::Image, make<ImageContent> }
- , { QStringLiteral("m.file"), MsgType::File, make<FileContent> }
- , { QStringLiteral("m.location"), MsgType::Location, make<LocationContent> }
- , { QStringLiteral("m.video"), MsgType::Video, make<VideoContent> }
- , { QStringLiteral("m.audio"), MsgType::Audio, make<AudioContent> }
- };
+const std::vector<MsgTypeDesc> msgTypes = {
+ { TextTypeKey, MsgType::Text, make<TextContent> },
+ { EmoteTypeKey, MsgType::Emote, make<TextContent> },
+ { NoticeTypeKey, MsgType::Notice, make<TextContent> },
+ { "m.image"_ls, MsgType::Image, make<ImageContent> },
+ { "m.file"_ls, MsgType::File, make<FileContent> },
+ { "m.location"_ls, MsgType::Location, make<LocationContent> },
+ { "m.video"_ls, MsgType::Video, make<VideoContent> },
+ { "m.audio"_ls, MsgType::Audio, make<AudioContent> }
+};
QString msgTypeToJson(MsgType enumType)
{
auto it = std::find_if(msgTypes.begin(), msgTypes.end(),
- [=](const MsgTypeDesc& mtd) { return mtd.enumType == enumType; });
+ [=](const MsgTypeDesc& mtd) {
+ return mtd.enumType == enumType;
+ });
if (it != msgTypes.end())
return it->matrixType;
@@ -64,59 +75,126 @@ QString msgTypeToJson(MsgType enumType)
MsgType jsonToMsgType(const QString& matrixType)
{
auto it = std::find_if(msgTypes.begin(), msgTypes.end(),
- [=](const MsgTypeDesc& mtd) { return mtd.matrixType == matrixType; });
+ [=](const MsgTypeDesc& mtd) {
+ return mtd.matrixType == matrixType;
+ });
if (it != msgTypes.end())
return it->enumType;
return MsgType::Unknown;
}
-inline QJsonObject toMsgJson(const QString& plainBody, const QString& jsonMsgType,
- TypedBase* content)
+inline bool isReplacement(const Omittable<EventRelation>& rel)
{
- auto json = content ? content->toJson() : QJsonObject();
- json.insert(QStringLiteral("msgtype"), jsonMsgType);
- json.insert(QStringLiteral("body"), plainBody);
- return json;
+ return rel && rel->type == EventRelation::ReplacementType;
}
-static const auto MsgTypeKey = "msgtype"_ls;
-static const auto BodyKey = "body"_ls;
+} // anonymous namespace
+
+QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody,
+ const QString& jsonMsgType,
+ TypedBase* content)
+{
+ QJsonObject json;
+ if (content) {
+ // TODO: replace with content->fillJson(json) when it starts working
+ json = content->toJson();
+ if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey
+ && jsonMsgType != EmoteTypeKey) {
+ if (json.contains(RelatesToKey)) {
+ json.remove(RelatesToKey);
+ qCWarning(EVENTS)
+ << RelatesToKey << "cannot be used in" << jsonMsgType
+ << "messages; the relation has been stripped off";
+ }
+ } else if (auto* textContent = static_cast<const TextContent*>(content);
+ textContent->relatesTo
+ && textContent->relatesTo->type
+ == EventRelation::ReplacementType) {
+ auto newContentJson = json.take("m.new_content"_ls).toObject();
+ newContentJson.insert(BodyKey, plainBody);
+ newContentJson.insert(MsgTypeKey, jsonMsgType);
+ json.insert(QStringLiteral("m.new_content"), newContentJson);
+ json[MsgTypeKey] = jsonMsgType;
+ json[BodyKeyL] = "* " + plainBody;
+ return json;
+ }
+ }
+ json.insert(MsgTypeKey, jsonMsgType);
+ json.insert(BodyKey, plainBody);
+ return json;
+}
RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
- const QString& jsonMsgType, TypedBase* content)
- : RoomEvent(typeId(), matrixTypeId(),
- toMsgJson(plainBody, jsonMsgType, content))
+ const QString& jsonMsgType,
+ TypedBase* content)
+ : RoomEvent(
+ basicJson(TypeId, assembleContentJson(plainBody, jsonMsgType, content)))
, _content(content)
-{ }
+{}
-RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
- MsgType msgType, TypedBase* content)
+RoomMessageEvent::RoomMessageEvent(const QString& plainBody, MsgType msgType,
+ TypedBase* content)
: RoomMessageEvent(plainBody, msgTypeToJson(msgType), content)
-{ }
+{}
+
+#if QT_VERSION_MAJOR < 6
+TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile)
+{
+ auto filePath = file.absoluteFilePath();
+ auto localUrl = QUrl::fromLocalFile(filePath);
+ auto mimeType = QMimeDatabase().mimeTypeForFile(file);
+ if (!asGenericFile) {
+ auto mimeTypeName = mimeType.name();
+ if (mimeTypeName.startsWith("image/"))
+ return new ImageContent(localUrl, file.size(), mimeType,
+ QImageReader(filePath).size(),
+ file.fileName());
+
+ // duration can only be obtained asynchronously and can only be reliably
+ // done by starting to play the file. Left for a future implementation.
+ if (mimeTypeName.startsWith("video/"))
+ return new VideoContent(localUrl, file.size(), mimeType,
+ QMediaResource(localUrl).resolution(),
+ file.fileName());
+
+ if (mimeTypeName.startsWith("audio/"))
+ return new AudioContent(localUrl, file.size(), mimeType,
+ file.fileName());
+ }
+ return new FileContent(localUrl, file.size(), mimeType, file.fileName());
+}
+
+RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
+ const QFileInfo& file, bool asGenericFile)
+ : RoomMessageEvent(plainBody,
+ asGenericFile ? QStringLiteral("m.file")
+ : rawMsgTypeForFile(file),
+ contentFromFile(file, asGenericFile))
+{}
+#endif
RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj)
- : RoomEvent(typeId(), obj), _content(nullptr)
+ : RoomEvent(obj), _content(nullptr)
{
if (isRedacted())
return;
const QJsonObject content = contentJson();
- if ( content.contains(MsgTypeKey) && content.contains(BodyKey) )
- {
+ if (content.contains(MsgTypeKey) && content.contains(BodyKeyL)) {
auto msgtype = content[MsgTypeKey].toString();
- for (const auto& mt: msgTypes)
- if (mt.matrixType == msgtype)
+ bool msgTypeFound = false;
+ for (const auto& mt : msgTypes)
+ if (mt.matrixType == msgtype) {
_content.reset(mt.maker(content));
+ msgTypeFound = true;
+ }
- if (!_content)
- {
- qCWarning(EVENTS) << "RoomMessageEvent: couldn't load content,"
+ if (!msgTypeFound) {
+ qCWarning(EVENTS) << "RoomMessageEvent: unknown msg_type,"
<< " full content dump follows";
qCWarning(EVENTS) << formatJson << content;
}
- }
- else
- {
+ } else {
qCWarning(EVENTS) << "No body or msgtype in room message event";
qCWarning(EVENTS) << formatJson << obj;
}
@@ -129,27 +207,26 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const
QString RoomMessageEvent::rawMsgtype() const
{
- return contentJson()[MsgTypeKey].toString();
+ return contentPart<QString>(MsgTypeKey);
}
QString RoomMessageEvent::plainBody() const
{
- return contentJson()[BodyKey].toString();
+ return contentPart<QString>(BodyKeyL);
}
QMimeType RoomMessageEvent::mimeType() const
{
static const auto PlainTextMimeType =
- QMimeDatabase().mimeTypeForName("text/plain");
+ QMimeDatabase().mimeTypeForName("text/plain");
return _content ? _content->type() : PlainTextMimeType;
- ;
}
bool RoomMessageEvent::hasTextContent() const
{
- return content() &&
- (msgtype() == MsgType::Text || msgtype() == MsgType::Emote ||
- msgtype() == MsgType::Notice); // FIXME: Unbind from specific msgtypes
+ return !content()
+ || (msgtype() == MsgType::Text || msgtype() == MsgType::Emote
+ || msgtype() == MsgType::Notice);
}
bool RoomMessageEvent::hasFileContent() const
@@ -162,62 +239,115 @@ bool RoomMessageEvent::hasThumbnail() const
return content() && content()->thumbnailInfo();
}
-TextContent::TextContent(const QString& text, const QString& contentType)
- : mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text)
+QString RoomMessageEvent::replacedEvent() const
+{
+ if (!content() || !hasTextContent())
+ return {};
+
+ const auto& rel = static_cast<const TextContent*>(content())->relatesTo;
+ return isReplacement(rel) ? rel->eventId : QString();
+}
+
+QString rawMsgTypeForMimeType(const QMimeType& mimeType)
+{
+ auto name = mimeType.name();
+ return name.startsWith("image/")
+ ? QStringLiteral("m.image")
+ : name.startsWith("video/")
+ ? QStringLiteral("m.video")
+ : name.startsWith("audio/") ? QStringLiteral("m.audio")
+ : QStringLiteral("m.file");
+}
+
+QString RoomMessageEvent::rawMsgTypeForUrl(const QUrl& url)
{
- if (contentType == "org.matrix.custom.html")
+ return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForUrl(url));
+}
+
+QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi)
+{
+ return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi));
+}
+
+TextContent::TextContent(QString text, const QString& contentType,
+ Omittable<EventRelation> relatesTo)
+ : mimeType(QMimeDatabase().mimeTypeForName(contentType))
+ , body(std::move(text))
+ , relatesTo(std::move(relatesTo))
+{
+ if (contentType == HtmlContentTypeId)
mimeType = QMimeDatabase().mimeTypeForName("text/html");
}
TextContent::TextContent(const QJsonObject& json)
+ : relatesTo(fromJson<Omittable<EventRelation>>(json[RelatesToKey]))
{
QMimeDatabase db;
static const auto PlainTextMimeType = db.mimeTypeForName("text/plain");
static const auto HtmlMimeType = db.mimeTypeForName("text/html");
- // Special-casing the custom matrix.org's (actually, Riot's) way
+ const auto actualJson = isReplacement(relatesTo)
+ ? json.value("m.new_content"_ls).toObject()
+ : json;
+ // Special-casing the custom matrix.org's (actually, Element's) way
// of sending HTML messages.
- if (json["format"_ls].toString() == "org.matrix.custom.html")
- {
+ if (actualJson["format"_ls].toString() == HtmlContentTypeId) {
mimeType = HtmlMimeType;
- body = json["formatted_body"_ls].toString();
+ body = actualJson[FormattedBodyKey].toString();
} else {
// Falling back to plain text, as there's no standard way to describe
// rich text in messages.
mimeType = PlainTextMimeType;
- body = json[BodyKey].toString();
+ body = actualJson[BodyKeyL].toString();
}
}
-void TextContent::fillJson(QJsonObject* json) const
+void TextContent::fillJson(QJsonObject &json) const
{
- Q_ASSERT(json);
- if (mimeType.inherits("text/html"))
- {
- json->insert(QStringLiteral("format"),
- QStringLiteral("org.matrix.custom.html"));
- json->insert(QStringLiteral("formatted_body"), body);
+ static const auto FormatKey = QStringLiteral("format");
+
+ if (mimeType.inherits("text/html")) {
+ json.insert(FormatKey, HtmlContentTypeId);
+ json.insert(FormattedBodyKey, body);
+ }
+ if (relatesTo) {
+ json.insert(
+ QStringLiteral("m.relates_to"),
+ relatesTo->type == EventRelation::ReplyType
+ ? QJsonObject { { relatesTo->type,
+ QJsonObject {
+ { EventIdKey, relatesTo->eventId } } } }
+ : QJsonObject { { RelTypeKey, relatesTo->type },
+ { EventIdKey, relatesTo->eventId } });
+ if (relatesTo->type == EventRelation::ReplacementType) {
+ QJsonObject newContentJson;
+ if (mimeType.inherits("text/html")) {
+ newContentJson.insert(FormatKey, HtmlContentTypeId);
+ newContentJson.insert(FormattedBodyKey, body);
+ }
+ json.insert(QStringLiteral("m.new_content"), newContentJson);
+ }
}
}
-LocationContent::LocationContent(const QString& geoUri, const ImageInfo& thumbnail)
+LocationContent::LocationContent(const QString& geoUri,
+ const Thumbnail& thumbnail)
: geoUri(geoUri), thumbnail(thumbnail)
-{ }
+{}
LocationContent::LocationContent(const QJsonObject& json)
: TypedBase(json)
, geoUri(json["geo_uri"_ls].toString())
, thumbnail(json["info"_ls].toObject())
-{ }
+{}
QMimeType LocationContent::type() const
{
return QMimeDatabase().mimeTypeForData(geoUri.toLatin1());
}
-void LocationContent::fillJson(QJsonObject* o) const
+void LocationContent::fillJson(QJsonObject& o) const
{
- Q_ASSERT(o);
- o->insert(QStringLiteral("geo_uri"), geoUri);
- o->insert(QStringLiteral("info"), toInfoJson(thumbnail));
+ o.insert(QStringLiteral("geo_uri"), geoUri);
+ o.insert(QStringLiteral("info"), toInfoJson(thumbnail));
}