diff options
author | Kitsune Ral <Kitsune-Ral@users.sf.net> | 2017-07-15 13:07:53 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-07-15 13:07:53 +0900 |
commit | 3dfb74af71fe0cceaeb14d1ef0ade846f3a659f9 (patch) | |
tree | 40482bd167f1e086541ad71cdf634324b40383c6 | |
parent | 47c2015beb75693c7181fb7359a4aa166e926fe7 (diff) | |
parent | 749def2b983d2338272c0891d15de20df22e2eea (diff) | |
download | libquotient-3dfb74af71fe0cceaeb14d1ef0ade846f3a659f9.tar.gz libquotient-3dfb74af71fe0cceaeb14d1ef0ade846f3a659f9.zip |
Merge pull request #72 from QMatrixClient/kitsune-send-events
Events creation and sending
-rw-r--r-- | CMakeLists.txt | 2 | ||||
-rw-r--r-- | connection.cpp | 10 | ||||
-rw-r--r-- | connection.h | 5 | ||||
-rw-r--r-- | connectiondata.cpp | 10 | ||||
-rw-r--r-- | connectiondata.h | 8 | ||||
-rw-r--r-- | events/event.cpp | 13 | ||||
-rw-r--r-- | events/event.h | 50 | ||||
-rw-r--r-- | events/roommessageevent.cpp | 207 | ||||
-rw-r--r-- | events/roommessageevent.h | 335 | ||||
-rw-r--r-- | jobs/basejob.h | 9 | ||||
-rw-r--r-- | jobs/postmessagejob.cpp | 64 | ||||
-rw-r--r-- | jobs/sendeventjob.cpp | 41 | ||||
-rw-r--r-- | jobs/sendeventjob.h (renamed from jobs/postmessagejob.h) | 35 | ||||
-rw-r--r-- | libqmatrixclient.pri | 4 | ||||
-rw-r--r-- | room.cpp | 17 | ||||
-rw-r--r-- | room.h | 8 |
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 \ @@ -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) @@ -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; |