aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitsune Ral <Kitsune-Ral@users.sf.net>2017-07-15 13:07:53 +0900
committerGitHub <noreply@github.com>2017-07-15 13:07:53 +0900
commit3dfb74af71fe0cceaeb14d1ef0ade846f3a659f9 (patch)
tree40482bd167f1e086541ad71cdf634324b40383c6
parent47c2015beb75693c7181fb7359a4aa166e926fe7 (diff)
parent749def2b983d2338272c0891d15de20df22e2eea (diff)
downloadlibquotient-3dfb74af71fe0cceaeb14d1ef0ade846f3a659f9.tar.gz
libquotient-3dfb74af71fe0cceaeb14d1ef0ade846f3a659f9.zip
Merge pull request #72 from QMatrixClient/kitsune-send-events
Events creation and sending
-rw-r--r--CMakeLists.txt2
-rw-r--r--connection.cpp10
-rw-r--r--connection.h5
-rw-r--r--connectiondata.cpp10
-rw-r--r--connectiondata.h8
-rw-r--r--events/event.cpp13
-rw-r--r--events/event.h50
-rw-r--r--events/roommessageevent.cpp207
-rw-r--r--events/roommessageevent.h335
-rw-r--r--jobs/basejob.h9
-rw-r--r--jobs/postmessagejob.cpp64
-rw-r--r--jobs/sendeventjob.cpp41
-rw-r--r--jobs/sendeventjob.h (renamed from jobs/postmessagejob.h)35
-rw-r--r--libqmatrixclient.pri4
-rw-r--r--room.cpp17
-rw-r--r--room.h8
16 files changed, 610 insertions, 208 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ad7c5a34..1d118d82 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -63,7 +63,7 @@ set(libqmatrixclient_SRCS
jobs/basejob.cpp
jobs/checkauthmethods.cpp
jobs/passwordlogin.cpp
- jobs/postmessagejob.cpp
+ jobs/sendeventjob.cpp
jobs/postreceiptjob.cpp
jobs/joinroomjob.cpp
jobs/leaveroomjob.cpp
diff --git a/connection.cpp b/connection.cpp
index 56628a07..f9f1490c 100644
--- a/connection.cpp
+++ b/connection.cpp
@@ -23,7 +23,7 @@
#include "room.h"
#include "jobs/passwordlogin.h"
#include "jobs/logoutjob.h"
-#include "jobs/postmessagejob.h"
+#include "jobs/sendeventjob.h"
#include "jobs/postreceiptjob.h"
#include "jobs/joinroomjob.h"
#include "jobs/leaveroomjob.h"
@@ -32,7 +32,6 @@
#include "jobs/mediathumbnailjob.h"
#include <QtNetwork/QDnsLookup>
-#include <QtCore/QDebug>
using namespace QMatrixClient;
@@ -188,7 +187,7 @@ void Connection::stopSync()
void Connection::postMessage(Room* room, const QString& type, const QString& message) const
{
- callApi<PostMessageJob>(room->id(), type, message);
+ callApi<SendEventJob>(room->id(), type, message);
}
PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const
@@ -316,3 +315,8 @@ Room* Connection::createRoom(const QString& roomId)
{
return new Room(this, roomId);
}
+
+QByteArray Connection::generateTxnId()
+{
+ return d->data->generateTxnId();
+}
diff --git a/connection.h b/connection.h
index f0b097fd..e3f33155 100644
--- a/connection.h
+++ b/connection.h
@@ -91,6 +91,11 @@ namespace QMatrixClient
return job;
}
+ /** Generates a new transaction id. Transaction id's are unique within
+ * a single Connection object
+ */
+ Q_INVOKABLE QByteArray generateTxnId();
+
signals:
void resolved();
void connected();
diff --git a/connectiondata.cpp b/connectiondata.cpp
index 6c7eff8c..cd91ef27 100644
--- a/connectiondata.cpp
+++ b/connectiondata.cpp
@@ -21,6 +21,7 @@
#include "logging.h"
#include <QtNetwork/QNetworkAccessManager>
+#include <cstdlib>
using namespace QMatrixClient;
@@ -35,6 +36,9 @@ struct ConnectionData::Private
QUrl baseUrl;
QString accessToken;
QString lastEvent;
+
+ mutable unsigned int txnCounter = 0;
+ const int id = std::rand(); // We don't really care about pure randomness
};
ConnectionData::ConnectionData(QUrl baseUrl)
@@ -89,3 +93,9 @@ void ConnectionData::setLastEvent(QString identifier)
{
d->lastEvent = identifier;
}
+
+QByteArray ConnectionData::generateTxnId() const
+{
+ return QByteArray::number(d->id) + 'q' +
+ QByteArray::number(++d->txnCounter);
+}
diff --git a/connectiondata.h b/connectiondata.h
index 0eadab80..7b0097d6 100644
--- a/connectiondata.h
+++ b/connectiondata.h
@@ -27,7 +27,7 @@ namespace QMatrixClient
class ConnectionData
{
public:
- ConnectionData(QUrl baseUrl);
+ explicit ConnectionData(QUrl baseUrl);
virtual ~ConnectionData();
QString accessToken() const;
@@ -41,8 +41,10 @@ namespace QMatrixClient
QString lastEvent() const;
void setLastEvent( QString identifier );
+ QByteArray generateTxnId() const;
+
private:
- class Private;
+ struct Private;
Private* d;
};
-}
+} // namespace QMatrixClient
diff --git a/events/event.cpp b/events/event.cpp
index bd7e1b03..653396bd 100644
--- a/events/event.cpp
+++ b/events/event.cpp
@@ -95,6 +95,7 @@ RoomEvent::RoomEvent(Type type, const QJsonObject& rep)
, _serverTimestamp(toTimestamp(rep["origin_server_ts"]))
, _roomId(rep["room_id"].toString())
, _senderId(rep["sender"].toString())
+ , _txnId(rep["unsigned"].toObject().value("transactionId").toString())
{
if (_id.isEmpty())
{
@@ -103,14 +104,22 @@ RoomEvent::RoomEvent(Type type, const QJsonObject& rep)
}
if (!rep.contains("origin_server_ts"))
{
- qCWarning(EVENTS) << "Event: can't find server timestamp in a room event";
+ qCWarning(EVENTS) << "Can't find server timestamp in a room event";
qCWarning(EVENTS) << formatJson << rep;
}
if (_senderId.isEmpty())
{
- qCWarning(EVENTS) << "user_id not found in a room event";
+ qCWarning(EVENTS) << "Can't find sender in a room event";
qCWarning(EVENTS) << formatJson << rep;
}
+ if (!_txnId.isEmpty())
+ qCDebug(EVENTS) << "Event transactionId:" << _txnId;
+}
+
+void RoomEvent::addId(const QString& id)
+{
+ Q_ASSERT(_id.isEmpty()); Q_ASSERT(!id.isEmpty());
+ _id = id;
}
RoomEvent* RoomEvent::fromJson(const QJsonObject& obj)
diff --git a/events/event.h b/events/event.h
index fd2f6feb..8760aa28 100644
--- a/events/event.h
+++ b/events/event.h
@@ -37,14 +37,16 @@ namespace QMatrixClient
RoomMember, RoomTopic, Typing, Receipt, Unknown
};
- explicit Event(Type type, const QJsonObject& rep);
+ explicit Event(Type type) : _type(type) { }
+ Event(Type type, const QJsonObject& rep);
Event(const Event&) = delete;
Type type() const { return _type; }
QByteArray originalJson() const;
- // Every event also has a "content" object but since its structure is
- // different for different types, we're implementing it per-event type
+ // According to the CS API spec, every event also has
+ // a "content" object; but since its structure is different for
+ // different types, we're implementing it per-event type
// (and in most cases it will be a combination of other fields
// instead of "content" field).
@@ -61,6 +63,8 @@ namespace QMatrixClient
QJsonObject _originalJson;
REGISTER_ENUM(Type)
+ Q_PROPERTY(Type type READ type CONSTANT)
+ Q_PROPERTY(QJsonObject contentJson READ contentJson CONSTANT)
};
using EventType = Event::Type;
template <typename EventT>
@@ -91,13 +95,42 @@ namespace QMatrixClient
class RoomEvent : public Event
{
+ Q_GADGET
+ Q_PROPERTY(QString id READ id)
+ Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT)
+ Q_PROPERTY(QString roomId READ roomId CONSTANT)
+ Q_PROPERTY(QString senderId READ senderId CONSTANT)
+ Q_PROPERTY(QString transactionId READ transactionId CONSTANT)
public:
+ explicit RoomEvent(Type type) : Event(type) { }
RoomEvent(Type type, const QJsonObject& rep);
- const QString& id() const { return _id; }
+ const QString& id() const { return _id; }
const QDateTime& timestamp() const { return _serverTimestamp; }
- const QString& roomId() const { return _roomId; }
- const QString& senderId() const { return _senderId; }
+ const QString& roomId() const { return _roomId; }
+ const QString& senderId() const { return _senderId; }
+ const QString& transactionId() const { return _txnId; }
+
+ /**
+ * Sets the transaction id for locally created events. This should be
+ * done before the event is exposed to any code using the respective
+ * Q_PROPERTY.
+ *
+ * \param txnId - transaction id, normally obtained from
+ * Connection::generateTxnId()
+ */
+ void setTransactionId(const QString& txnId) { _txnId = txnId; }
+
+ /**
+ * Sets event id for locally created events
+ *
+ * When a new event is created locally, it has no server id yet.
+ * This function allows to add the id once the confirmation from
+ * the server is received. There should be no id set previously
+ * in the event. It's the responsibility of the code calling addId()
+ * to notify clients that use Q_PROPERTY(id) about its change
+ */
+ void addId(const QString& id);
// "Static override" of the one in Event
static RoomEvent* fromJson(const QJsonObject& obj);
@@ -107,6 +140,11 @@ namespace QMatrixClient
QDateTime _serverTimestamp;
QString _roomId;
QString _senderId;
+ QString _txnId;
};
using RoomEvents = EventsBatch<RoomEvent>;
} // namespace QMatrixClient
+Q_DECLARE_OPAQUE_POINTER(QMatrixClient::Event*)
+Q_DECLARE_METATYPE(QMatrixClient::Event*)
+Q_DECLARE_OPAQUE_POINTER(QMatrixClient::RoomEvent*)
+Q_DECLARE_METATYPE(QMatrixClient::RoomEvent*)
diff --git a/events/roommessageevent.cpp b/events/roommessageevent.cpp
index 19da8827..179eac77 100644
--- a/events/roommessageevent.cpp
+++ b/events/roommessageevent.cpp
@@ -25,57 +25,78 @@
using namespace QMatrixClient;
using namespace MessageEventContent;
-using ContentPair = std::pair<CType, Base*>;
+using MsgType = RoomMessageEvent::MsgType;
-template <CType EnumType, typename ContentT>
-ContentPair make(const QJsonObject& json)
+template <typename ContentT>
+Base* make(const QJsonObject& json)
{
- return { EnumType, new ContentT(json) };
+ return new ContentT(json);
}
-ContentPair makeVideo(const QJsonObject& json)
+struct MsgTypeDesc
{
- auto c = new VideoContent(json);
- // Only for m.video, the spec puts a thumbnail inside "info" JSON key. Once
- // this is fixed, VideoContent creation will switch to make<>().
- const QJsonObject infoJson = json["info"].toObject();
- if (infoJson.contains("thumbnail_url"))
- {
- c->thumbnail = ImageInfo(infoJson["thumbnail_url"].toString(),
- infoJson["thumbnail_info"].toObject());
- }
- return { CType::Video, c };
+ QString jsonType;
+ MsgType enumType;
+ Base* (*maker)(const QJsonObject&);
};
-ContentPair makeUnknown(const QJsonObject& json)
+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> }
+ };
+
+QString msgTypeToJson(MsgType enumType)
{
- qCDebug(EVENTS) << "RoomMessageEvent: couldn't resolve msgtype, JSON follows:";
- qCDebug(EVENTS) << json;
- return { CType::Unknown, new Base() };
+ auto it = std::find_if(msgTypes.begin(), msgTypes.end(),
+ [=](const MsgTypeDesc& mtd) { return mtd.enumType == enumType; });
+ if (it != msgTypes.end())
+ return it->jsonType;
+
+ qCCritical(EVENTS) << "Unknown msgtype:" << enumType;
+ return {};
}
+MsgType jsonToMsgType(const QString& jsonType)
+{
+ auto it = std::find_if(msgTypes.begin(), msgTypes.end(),
+ [=](const MsgTypeDesc& mtd) { return mtd.jsonType == jsonType; });
+ if (it != msgTypes.end())
+ return it->enumType;
+
+ qCCritical(EVENTS) << "Unknown msgtype:" << jsonType;
+ return {};
+}
+
+RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
+ MsgType msgType, Base* content)
+ : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content)
+{ }
+
RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj)
- : RoomEvent(Type::RoomMessage, obj), _msgtype(CType::Unknown)
- , _content(nullptr)
+ : RoomEvent(Type::RoomMessage, obj), _content(nullptr)
{
const QJsonObject content = contentJson();
if ( content.contains("msgtype") && content.contains("body") )
{
_plainBody = content["body"].toString();
- auto factory = lookup(content["msgtype"].toString(),
- "m.text", &make<CType::Text, TextContent>,
- "m.emote", &make<CType::Emote, TextContent>,
- "m.notice", &make<CType::Notice, TextContent>,
- "m.image", &make<CType::Image, ImageContent>,
- "m.file", &make<CType::File, FileContent>,
- "m.location", &make<CType::Location, LocationContent>,
- "m.video", &makeVideo,
- "m.audio", &make<CType::Audio, AudioContent>,
- // Insert new message types before this line
- &makeUnknown
- );
- std::tie(_msgtype, _content) = factory(content);
+ _msgtype = content["msgtype"].toString();
+ for (auto mt: msgTypes)
+ if (mt.jsonType == _msgtype)
+ _content.reset(mt.maker(content));
+
+ if (!_content)
+ {
+ qCWarning(EVENTS) << "RoomMessageEvent: couldn't load content,"
+ << " full content dump follows";
+ qCWarning(EVENTS) << formatJson << content;
+ }
}
else
{
@@ -84,12 +105,43 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj)
}
}
-RoomMessageEvent::~RoomMessageEvent()
+RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const
+{
+ return jsonToMsgType(_msgtype);
+}
+
+QMimeType RoomMessageEvent::mimeType() const
+{
+ return _content ? _content->mimeType :
+ QMimeDatabase().mimeTypeForName("text/plain");
+}
+
+QJsonObject RoomMessageEvent::toJson() const
{
- if (_content)
- delete _content;
+ QJsonObject obj = _content ? _content->toJson() : QJsonObject();
+ obj.insert("msgtype", msgTypeToJson(msgtype()));
+ obj.insert("body", plainBody());
+ return obj;
}
+QJsonObject Base::toJson() const
+{
+ QJsonObject o;
+ fillJson(&o);
+ return o;
+}
+
+QJsonObject InfoBase::toInfoJson() const
+{
+ QJsonObject info;
+ fillInfoJson(&info);
+ return info;
+}
+
+TextContent::TextContent(const QString& text, const QString& contentType)
+ : Base(QMimeDatabase().mimeTypeForName(contentType)), body(text)
+{ }
+
TextContent::TextContent(const QJsonObject& json)
{
QMimeDatabase db;
@@ -103,39 +155,82 @@ TextContent::TextContent(const QJsonObject& json)
} else {
// Falling back to plain text, as there's no standard way to describe
// rich text in messages.
- body = json["body"].toString();
mimeType = db.mimeTypeForName("text/plain");
+ body = json["body"].toString();
}
}
-FileInfo::FileInfo(QUrl u, const QJsonObject& infoJson, QString originalFilename)
- : url(u)
- , fileSize(infoJson["size"].toInt())
- , mimetype(QMimeDatabase().mimeTypeForName(infoJson["mimetype"].toString()))
+void TextContent::fillJson(QJsonObject* json) const
+{
+ Q_ASSERT(json);
+ json->insert("format", QStringLiteral("org.matrix.custom.html"));
+ json->insert("formatted_body", body);
+}
+
+FileInfo::FileInfo(const QUrl& u, int payloadSize, const QMimeType& mimeType,
+ const QString& originalFilename)
+ : InfoBase(mimeType), url(u), payloadSize(payloadSize)
, originalName(originalFilename)
+{ }
+
+FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson,
+ const QString& originalFilename)
+ : FileInfo(u, infoJson["size"].toInt(),
+ QMimeDatabase().mimeTypeForName(infoJson["mimetype"].toString()),
+ originalFilename)
+{
+ if (!mimeType.isValid())
+ mimeType = QMimeDatabase().mimeTypeForData(QByteArray());
+}
+
+void FileInfo::fillInfoJson(QJsonObject* infoJson) const
{
- if (!mimetype.isValid())
- mimetype = QMimeDatabase().mimeTypeForData(QByteArray());
+ Q_ASSERT(infoJson);
+ infoJson->insert("size", payloadSize);
+ infoJson->insert("mimetype", mimeType.name());
}
-ImageInfo::ImageInfo(QUrl u, const QJsonObject& infoJson)
- : FileInfo(u, infoJson)
- , imageSize(infoJson["w"].toInt(), infoJson["h"].toInt())
+void FileInfo::fillJson(QJsonObject* json) const
+{
+ Q_ASSERT(json);
+ json->insert("url", url.toString());
+ if (!originalName.isEmpty())
+ json->insert("filename", originalName);
+ json->insert("info", toInfoJson());
+}
+
+LocationContent::LocationContent(const QString& geoUri,
+ const ImageInfo<>& thumbnail)
+ : Thumbnailed<>(thumbnail), geoUri(geoUri)
{ }
LocationContent::LocationContent(const QJsonObject& json)
- : geoUri(json["geo_uri"].toString())
- , thumbnail(json["thumbnail_url"].toString(),
- json["thumbnail_info"].toObject())
+ : Thumbnailed<>(json["info"].toObject())
+ , geoUri(json["geo_uri"].toString())
{ }
-VideoInfo::VideoInfo(QUrl u, const QJsonObject& infoJson)
- : FileInfo(u, infoJson)
- , duration(infoJson["duration"].toInt())
- , imageSize(infoJson["w"].toInt(), infoJson["h"].toInt())
+void LocationContent::fillJson(QJsonObject* o) const
+{
+ Q_ASSERT(o);
+ o->insert("geo_uri", geoUri);
+ o->insert("info", Thumbnailed::toInfoJson());
+}
+
+PlayableInfo::PlayableInfo(const QUrl& u, int fileSize,
+ const QMimeType& mimeType, int duration,
+ const QString& originalFilename)
+ : FileInfo(u, fileSize, mimeType, originalFilename)
+ , duration(duration)
{ }
-AudioInfo::AudioInfo(QUrl u, const QJsonObject& infoJson)
- : FileInfo(u, infoJson)
+PlayableInfo::PlayableInfo(const QUrl& u, const QJsonObject& infoJson,
+ const QString& originalFilename)
+ : FileInfo(u, infoJson, originalFilename)
, duration(infoJson["duration"].toInt())
{ }
+
+void PlayableInfo::fillInfoJson(QJsonObject* infoJson) const
+{
+ FileInfo::fillInfoJson(infoJson);
+ infoJson->insert("duration", duration);
+}
diff --git a/events/roommessageevent.h b/events/roommessageevent.h
index 299b1b19..74e0defb 100644
--- a/events/roommessageevent.h
+++ b/events/roommessageevent.h
@@ -24,122 +24,361 @@
#include <QtCore/QMimeType>
#include <QtCore/QSize>
-#include <memory>
-
namespace QMatrixClient
{
namespace MessageEventContent
{
+ /**
+ * A base class for all content types that can be stored
+ * in a RoomMessageEvent
+ *
+ * Each content type class should have a constructor taking
+ * a QJsonObject and override fillJson() with an implementation
+ * that will fill the target QJsonObject with stored values. It is
+ * assumed but not required that a content object can also be created
+ * from plain data. fillJson() should only fill the main JSON object
+ * but not the "info" subobject if it exists for a certain content type;
+ * use \p InfoBase to de/serialize "info" parts with an optional URL
+ * on the top level.
+ */
class Base
{
- Q_GADGET
public:
- enum class Type
- {
- Text, Emote, Notice, Image, File, Location, Video, Audio, Unknown
- };
-
virtual ~Base() = default;
- REGISTER_ENUM(Type)
+ QJsonObject toJson() const;
+
+ QMimeType mimeType;
+
+ protected:
+ Base() = default;
+ explicit Base(const QMimeType& type) : mimeType(type) { }
+
+ virtual void fillJson(QJsonObject* o) const = 0;
+ };
+
+ /**
+ * A base class for content types that have an "info" object in their
+ * JSON representation
+ *
+ * These include most multimedia types currently in the CS API spec.
+ * Derived classes should override fillInfoJson() to fill the "info"
+ * subobject, BUT NOT the main JSON object. Most but not all "info"
+ * classes (specifically, those deriving from UrlInfo) should also
+ * have a constructor that accepts two parameters, QUrl and QJsonObject,
+ * in order to load the URL+info part from JSON.
+ */
+ class InfoBase: public Base
+ {
+ public:
+ QJsonObject toInfoJson() const;
+
+ protected:
+ using Base::Base;
+
+ virtual void fillInfoJson(QJsonObject* infoJson) const { }
};
- using CType = Base::Type;
} // namespace MessageEventContent
- using MessageEventType = MessageEventContent::CType;
+ /**
+ * The event class corresponding to m.room.message events
+ */
class RoomMessageEvent: public RoomEvent
{
+ Q_GADGET
public:
+ enum class MsgType
+ {
+ Text, Emote, Notice, Image, File, Location, Video, Audio, Unknown
+ };
+
+ RoomMessageEvent(const QString& plainBody,
+ const QString& jsonMsgType,
+ MessageEventContent::Base* content = nullptr)
+ : RoomEvent(Type::RoomMessage)
+ , _msgtype(jsonMsgType), _plainBody(plainBody), _content(content)
+ { }
+ explicit RoomMessageEvent(const QString& plainBody,
+ MsgType msgType = MsgType::Text,
+ MessageEventContent::Base* content = nullptr);
explicit RoomMessageEvent(const QJsonObject& obj);
- ~RoomMessageEvent();
- MessageEventType msgtype() const { return _msgtype; }
- const QString& plainBody() const { return _plainBody; }
- const MessageEventContent::Base* content() const { return _content; }
+ MsgType msgtype() const;
+ QString rawMsgtype() const { return _msgtype; }
+ const QString& plainBody() const { return _plainBody; }
+ const MessageEventContent::Base* content() const
+ { return _content.data(); }
+ QMimeType mimeType() const;
+
+ QJsonObject toJson() const;
+
+ static constexpr const char* TypeId = "m.room.message";
private:
- MessageEventType _msgtype;
+ QString _msgtype;
QString _plainBody;
- MessageEventContent::Base* _content;
+ QScopedPointer<MessageEventContent::Base> _content;
+
+ REGISTER_ENUM(MsgType)
};
+ using MessageEventType = RoomMessageEvent::MsgType;
namespace MessageEventContent
{
// The below structures fairly follow CS spec 11.2.1.6. The overall
// set of attributes for each content types is a superset of the spec
- // but specific aggregation structure is altered.
+ // but specific aggregation structure is altered. See doc comments to
+ // each type for the list of available attributes.
+ /**
+ * Rich text content for m.text, m.emote, m.notice
+ *
+ * Available fields: mimeType, body. The body can be either rich text
+ * or plain text, depending on what mimeType specifies.
+ */
class TextContent: public Base
{
public:
+ TextContent(const QString& text, const QString& contentType);
explicit TextContent(const QJsonObject& json);
- QMimeType mimeType;
+ void fillJson(QJsonObject* json) const override;
+
QString body;
};
- class FileInfo: public Base
+ /**
+ * Base class for content types that consist of a URL along with
+ * additional information
+ *
+ * All message types except the (hyper)text mentioned above and
+ * m.location fall under this category.
+ */
+ class FileInfo: public InfoBase
{
public:
- FileInfo(QUrl u, const QJsonObject& infoJson,
- QString originalFilename = QString());
+ explicit FileInfo(const QUrl& u, int payloadSize = -1,
+ const QMimeType& mimeType = {},
+ const QString& originalFilename = {});
+ FileInfo(const QUrl& u, const QJsonObject& infoJson,
+ const QString& originalFilename = {});
QUrl url;
- int fileSize;
- QMimeType mimetype;
+ int payloadSize;
QString originalName;
+
+ protected:
+ void fillJson(QJsonObject* json) const override;
+ void fillInfoJson(QJsonObject* infoJson) const override;
};
- class ImageInfo: public FileInfo
+ /**
+ * A base class for image info types: image, thumbnail, video
+ *
+ * \tparam InfoT base info class; should derive from \p InfoBase
+ */
+ template <class InfoT = FileInfo>
+ class ImageInfo : public InfoT
{
public:
- ImageInfo(QUrl u, const QJsonObject& infoJson);
+ explicit ImageInfo(const QUrl& u, int fileSize = -1,
+ QMimeType mimeType = {},
+ const QSize& imageSize = {})
+ : InfoT(u, fileSize, mimeType), imageSize(imageSize)
+ { }
+ ImageInfo(const QUrl& u, const QJsonObject& infoJson,
+ const QString& originalFilename = {})
+ : InfoT(u, infoJson, originalFilename)
+ , imageSize(infoJson["w"].toInt(), infoJson["h"].toInt())
+ { }
+
+ void fillInfoJson(QJsonObject* infoJson) const /* override */
+ {
+ InfoT::fillInfoJson(infoJson);
+ infoJson->insert("w", imageSize.width());
+ infoJson->insert("h", imageSize.height());
+ }
QSize imageSize;
};
- template <class ContentInfoT>
- class ThumbnailedContent: public ContentInfoT
+ /**
+ * A base class for an info type that carries a thumbnail
+ *
+ * This class provides a means to save/load a thumbnail to/from "info"
+ * subobject of the JSON representation of a message; namely,
+ * "info/thumbnail_url" and "info/thumbnail_info" fields are used.
+ *
+ * \tparam InfoT base info class; should derive from \p InfoBase
+ */
+ template <class InfoT = InfoBase>
+ class Thumbnailed : public InfoT
{
public:
- explicit ThumbnailedContent(const QJsonObject& json)
- : ContentInfoT(json["url"].toString(), json["info"].toObject())
- , thumbnail(json["thumbnail_url"].toString(),
- json["thumbnail_info"].toObject())
+ template <typename... ArgTs>
+ explicit Thumbnailed(const ImageInfo<>& thumbnail,
+ ArgTs&&... infoArgs)
+ : InfoT(std::forward<ArgTs>(infoArgs)...)
+ , thumbnail(thumbnail)
{ }
- ImageInfo thumbnail;
+ explicit Thumbnailed(const QJsonObject& infoJson)
+ : thumbnail(infoJson["thumbnail_url"].toString(),
+ infoJson["thumbnail_info"].toObject())
+ { }
+
+ Thumbnailed(const QUrl& u, const QJsonObject& infoJson,
+ const QString& originalFilename = {})
+ : InfoT(u, infoJson, originalFilename)
+ , thumbnail(infoJson["thumbnail_url"].toString(),
+ infoJson["thumbnail_info"].toObject())
+ { }
+
+ void fillInfoJson(QJsonObject* infoJson) const /* override */
+ {
+ InfoT::fillInfoJson(infoJson);
+ infoJson->insert("thumbnail_url", thumbnail.url.toString());
+ infoJson->insert("thumbnail_info", thumbnail.toInfoJson());
+ }
+
+ ImageInfo<> thumbnail;
+ };
+
+ /**
+ * One more facility base class for content types that have a URL and
+ * additional info
+ *
+ * The assumed layout for types enabled by a combination of UrlInfo and
+ * UrlWith<> is the following: "url" and, optionally, "filename" in the
+ * top-level JSON and the rest of information inside the "info" subobject.
+ *
+ * \tparam InfoT base info class; should derive from \p UrlInfo or
+ * provide a constructor with a compatible signature
+ */
+ template <class InfoT> // InfoT : public FileInfo
+ class UrlWith : public InfoT
+ {
+ public:
+ using InfoT::InfoT;
+ explicit UrlWith(const QJsonObject& json)
+ : InfoT(json["url"].toString(), json["info"].toObject(),
+ json["filename"].toString())
+ { }
};
- using ImageContent = ThumbnailedContent<ImageInfo>;
- using FileContent = ThumbnailedContent<FileInfo>;
+ /**
+ * Content class for m.image
+ *
+ * Available fields:
+ * - corresponding to the top-level JSON:
+ * - url
+ * - filename (extension to the spec)
+ * - corresponding to the "info" subobject:
+ * - payloadSize ("size" in JSON)
+ * - mimeType ("mimetype" in JSON)
+ * - imageSize (QSize for a combination of "h" and "w" in JSON)
+ * - thumbnail.url ("thumbnail_url" in JSON)
+ * - corresponding to the "info/thumbnail_info" subobject: contents of
+ * thumbnail field, in the same vein as for the main image:
+ * - payloadSize
+ * - mimeType
+ * - imageSize
+ */
+ using ImageContent = UrlWith<Thumbnailed<ImageInfo<>>>;
- class LocationContent: public Base
+ /**
+ * Content class for m.file
+ *
+ * Available fields:
+ * - corresponding to the top-level JSON:
+ * - url
+ * - filename
+ * - corresponding to the "info" subobject:
+ * - payloadSize ("size" in JSON)
+ * - mimeType ("mimetype" in JSON)
+ * - thumbnail.url ("thumbnail_url" in JSON)
+ * - corresponding to the "info/thumbnail_info" subobject:
+ * - thumbnail.payloadSize
+ * - thumbnail.mimeType
+ * - thumbnail.imageSize (QSize for "h" and "w" in JSON)
+ */
+ using FileContent = UrlWith<Thumbnailed<FileInfo>>;
+
+ /**
+ * Content class for m.location
+ *
+ * Available fields:
+ * - corresponding to the top-level JSON:
+ * - geoUri ("geo_uri" in JSON)
+ * - corresponding to the "info" subobject:
+ * - thumbnail.url ("thumbnail_url" in JSON)
+ * - corresponding to the "info/thumbnail_info" subobject:
+ * - thumbnail.payloadSize
+ * - thumbnail.mimeType
+ * - thumbnail.imageSize
+ */
+ class LocationContent: public Thumbnailed<>
{
public:
+ LocationContent(const QString& geoUri,
+ const ImageInfo<>& thumbnail);
explicit LocationContent(const QJsonObject& json);
+ void fillJson(QJsonObject* o) const override;
+
QString geoUri;
- ImageInfo thumbnail;
};
- class VideoInfo: public FileInfo
+ /**
+ * A base class for "playable" info types: audio and video
+ */
+ class PlayableInfo : public FileInfo
{
public:
- VideoInfo(QUrl u, const QJsonObject& infoJson);
+ explicit PlayableInfo(const QUrl& u, int fileSize,
+ const QMimeType& mimeType, int duration,
+ const QString& originalFilename = {});
+ PlayableInfo(const QUrl& u, const QJsonObject& infoJson,
+ const QString& originalFilename = {});
+
+ void fillInfoJson(QJsonObject* infoJson) const override;
int duration;
- QSize imageSize;
};
- using VideoContent = ThumbnailedContent<VideoInfo>;
- class AudioInfo: public FileInfo
- {
- public:
- AudioInfo(QUrl u, const QJsonObject& infoJson);
+ /**
+ * Content class for m.video
+ *
+ * Available fields:
+ * - corresponding to the top-level JSON:
+ * - url
+ * - filename (extension to the CS API spec)
+ * - corresponding to the "info" subobject:
+ * - payloadSize ("size" in JSON)
+ * - mimeType ("mimetype" in JSON)
+ * - duration
+ * - imageSize (QSize for a combination of "h" and "w" in JSON)
+ * - thumbnail.url ("thumbnail_url" in JSON)
+ * - corresponding to the "info/thumbnail_info" subobject: contents of
+ * thumbnail field, in the same vein as for "info":
+ * - payloadSize
+ * - mimeType
+ * - imageSize
+ */
+ using VideoContent = UrlWith<Thumbnailed<ImageInfo<PlayableInfo>>>;
- int duration;
- };
- using AudioContent = ThumbnailedContent<AudioInfo>;
+ /**
+ * Content class for m.audio
+ *
+ * Available fields:
+ * - corresponding to the top-level JSON:
+ * - url
+ * - filename (extension to the CS API spec)
+ * - corresponding to the "info" subobject:
+ * - payloadSize ("size" in JSON)
+ * - mimeType ("mimetype" in JSON)
+ * - duration
+ */
+ using AudioContent = UrlWith<PlayableInfo>;
} // namespace MessageEventContent
} // namespace QMatrixClient
diff --git a/jobs/basejob.h b/jobs/basejob.h
index 2be4577f..0ec40a7a 100644
--- a/jobs/basejob.h
+++ b/jobs/basejob.h
@@ -63,7 +63,7 @@ namespace QMatrixClient
public:
using QUrlQuery::QUrlQuery;
Query() = default;
- explicit Query(const QList< QPair<QString, QString> >& l)
+ explicit Query(const std::initializer_list< QPair<QString, QString> >& l)
{
setQueryItems(l);
}
@@ -78,11 +78,16 @@ namespace QMatrixClient
{
public:
Data() = default;
- explicit Data(const QList< QPair<QString, QString> >& l)
+ Data(const QJsonObject& o) : QJsonObject(o) { }
+ Data(QJsonObject&& o) : QJsonObject(std::move(o)) { }
+#if (QT_VERSION < QT_VERSION_CHECK(5, 4, 0))
+ // This method exists in QJsonObject of newer Qt versions
+ explicit Data(const std::initializer_list< QPair<QString, QString> >& l)
{
for (auto i: l)
insert(i.first, i.second);
}
+#endif
QByteArray serialize() const
{
return QJsonDocument(*this).toJson();
diff --git a/jobs/postmessagejob.cpp b/jobs/postmessagejob.cpp
deleted file mode 100644
index df30614c..00000000
--- a/jobs/postmessagejob.cpp
+++ /dev/null
@@ -1,64 +0,0 @@
-/******************************************************************************
- * 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
- */
-
-#include "postmessagejob.h"
-#include "util.h"
-
-using namespace QMatrixClient;
-
-class PostMessageJob::Private
-{
- public:
- QString eventId; // unused yet
-};
-
-PostMessageJob::PostMessageJob(const ConnectionData* connection,
- const QString& roomId, const QString& type,
- const QString& plainText)
- : BaseJob(connection, HttpVerb::Post, "PostMessageJob",
- QStringLiteral("_matrix/client/r0/rooms/%1/send/m.room.message").arg(roomId),
- Query(),
- Data({ { "msgtype", type }, { "body", plainText } }) )
- , d(new Private)
-{ }
-
-PostMessageJob::PostMessageJob(const ConnectionData* connection,
- const QString& roomId, const QString& type,
- const QString& plainText, const QString& richText)
- : BaseJob(connection, HttpVerb::Post, "PostMessageJob",
- QStringLiteral("_matrix/client/r0/rooms/%1/send/m.room.message").arg(roomId),
- Query(),
- Data({ { "msgtype", type }, { "body", plainText }
- , { "format", QStringLiteral("org.matrix.custom.html") }
- , { "formatted_body", richText } }) )
- , d(new Private)
-{ }
-
-PostMessageJob::~PostMessageJob()
-{
- delete d;
-}
-
-BaseJob::Status PostMessageJob::parseJson(const QJsonDocument& data)
-{
- if( data.object().contains("event_id") )
- return Success;
-
- qCDebug(JOBS) << data;
- return { UserDefinedError, "No event_id in the JSON response" };
-}
diff --git a/jobs/sendeventjob.cpp b/jobs/sendeventjob.cpp
new file mode 100644
index 00000000..f3c95fe8
--- /dev/null
+++ b/jobs/sendeventjob.cpp
@@ -0,0 +1,41 @@
+/******************************************************************************
+ * 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
+ */
+
+#include "sendeventjob.h"
+
+#include "events/roommessageevent.h"
+
+using namespace QMatrixClient;
+
+SendEventJob::SendEventJob(const ConnectionData* connection,
+ const QString& roomId, const QString& type,
+ const QString& plainText)
+ : SendEventJob(connection, roomId,
+ new RoomMessageEvent(plainText, type))
+{ }
+
+BaseJob::Status SendEventJob::parseJson(const QJsonDocument& data)
+{
+ _eventId = data.object().value("event_id").toString();
+ if (!_eventId.isEmpty())
+ return Success;
+
+ qCDebug(JOBS) << data;
+ return { UserDefinedError, "No event_id in the JSON response" };
+}
+
diff --git a/jobs/postmessagejob.h b/jobs/sendeventjob.h
index f4ae809b..180fdd19 100644
--- a/jobs/postmessagejob.h
+++ b/jobs/sendeventjob.h
@@ -20,27 +20,38 @@
#include "basejob.h"
+#include "connectiondata.h"
+
namespace QMatrixClient
{
- class PostMessageJob: public BaseJob
+ class SendEventJob: public BaseJob
{
public:
- /** Constructs a plain text message job */
- PostMessageJob(const ConnectionData* connection, const QString& roomId,
- const QString& type, const QString& plainText);
- /** Constructs a rich text message job */
- PostMessageJob(const ConnectionData* connection, const QString& roomId,
- const QString& type, const QString& plainText,
- const QString& richText);
- virtual ~PostMessageJob();
+ /** Constructs a job that sends an arbitrary room event */
+ template <typename EvT>
+ SendEventJob(const ConnectionData* connection, const QString& roomId,
+ const EvT* event)
+ : BaseJob(connection, HttpVerb::Put, "SendEventJob",
+ QStringLiteral("_matrix/client/r0/rooms/%1/send/%2/%3")
+ .arg(roomId).arg(EvT::TypeId)
+ .arg(event->transactionId()),
+ Query(),
+ Data(event->toJson()))
+ { }
+
+ /**
+ * Constructs a plain text message job (for compatibility with
+ * the old PostMessageJob API).
+ */
+ SendEventJob(const ConnectionData* connection, const QString& roomId,
+ const QString& type, const QString& plainText);
- //bool success();
+ QString eventId() const { return _eventId; }
protected:
Status parseJson(const QJsonDocument& data) override;
private:
- class Private;
- Private* d;
+ QString _eventId;
};
} // namespace QMatrixClient
diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri
index d2d4c088..c352e186 100644
--- a/libqmatrixclient.pri
+++ b/libqmatrixclient.pri
@@ -21,7 +21,7 @@ HEADERS += \
$$PWD/jobs/basejob.h \
$$PWD/jobs/checkauthmethods.h \
$$PWD/jobs/passwordlogin.h \
- $$PWD/jobs/postmessagejob.h \
+ $$PWD/jobs/sendeventjob.h \
$$PWD/jobs/postreceiptjob.h \
$$PWD/jobs/joinroomjob.h \
$$PWD/jobs/leaveroomjob.h \
@@ -49,7 +49,7 @@ SOURCES += \
$$PWD/jobs/basejob.cpp \
$$PWD/jobs/checkauthmethods.cpp \
$$PWD/jobs/passwordlogin.cpp \
- $$PWD/jobs/postmessagejob.cpp \
+ $$PWD/jobs/sendeventjob.cpp \
$$PWD/jobs/postreceiptjob.cpp \
$$PWD/jobs/joinroomjob.cpp \
$$PWD/jobs/leaveroomjob.cpp \
diff --git a/room.cpp b/room.cpp
index 24745b9b..cfdd33ac 100644
--- a/room.cpp
+++ b/room.cpp
@@ -21,14 +21,12 @@
#include <array>
#include <QtCore/QHash>
-#include <QtCore/QJsonArray>
#include <QtCore/QStringBuilder> // for efficient string concats (operator%)
#include <QtCore/QElapsedTimer>
#include "connection.h"
#include "state.h"
#include "user.h"
-#include "events/roommessageevent.h"
#include "events/roomnameevent.h"
#include "events/roomaliasesevent.h"
#include "events/roomcanonicalaliasevent.h"
@@ -36,7 +34,7 @@
#include "events/roommemberevent.h"
#include "events/typingevent.h"
#include "events/receiptevent.h"
-#include "jobs/postmessagejob.h"
+#include "jobs/sendeventjob.h"
#include "jobs/roommessagesjob.h"
#include "jobs/postreceiptjob.h"
#include "jobs/leaveroomjob.h"
@@ -560,13 +558,18 @@ void Room::updateData(SyncRoomData&& data)
void Room::postMessage(const QString& type, const QString& plainText)
{
- connection()->callApi<PostMessageJob>(id(), type, plainText);
+ connection()->callApi<SendEventJob>(id(), type, plainText);
}
-void Room::postMessage(const QString& type, const QString& plainText,
- const QString& richText)
+void Room::postMessage(const QString& plainText, MessageEventType type)
{
- connection()->callApi<PostMessageJob>(id(), type, plainText, richText);
+ RoomMessageEvent rme(plainText, type);
+ postMessage(&rme);
+}
+
+void Room::postMessage(RoomMessageEvent* event)
+{
+ connection()->callApi<SendEventJob>(id(), event);
}
void Room::getPreviousContent(int limit)
diff --git a/room.h b/room.h
index 60278107..03827a55 100644
--- a/room.h
+++ b/room.h
@@ -27,6 +27,7 @@
#include <QtCore/QJsonObject>
#include "jobs/syncjob.h"
+#include "events/roommessageevent.h"
#include "joinstate.h"
namespace QMatrixClient
@@ -142,9 +143,12 @@ namespace QMatrixClient
MemberSorter memberSorter() const;
public slots:
+ void postMessage(const QString& plainText,
+ MessageEventType type = MessageEventType::Text);
+ void postMessage(RoomMessageEvent* event);
+ /** @deprecated */
void postMessage(const QString& type, const QString& plainText);
- void postMessage(const QString& type, const QString& plainText,
- const QString& richText);
+
void getPreviousContent(int limit = 10);
void leaveRoom() const;