aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorKitsune Ral <Kitsune-Ral@users.sf.net>2018-03-31 13:16:02 +0900
committerKitsune Ral <Kitsune-Ral@users.sf.net>2018-03-31 14:23:55 +0900
commitefeb50a46ad824aa258472f6ac8da74810f05a55 (patch)
treea89c6f35d56986c60e73f870530c9d6ee0527e6d /lib
parent29093379b707bfe620234c2968b37aa86666542a (diff)
downloadlibquotient-efeb50a46ad824aa258472f6ac8da74810f05a55.tar.gz
libquotient-efeb50a46ad824aa258472f6ac8da74810f05a55.zip
Move source files to a separate folder
It's been long overdue to separate them from the rest of the stuff (docs etc.). Also, this allows installing to a directory within the checked out git tree (say, ./install/, similar to ./build/).
Diffstat (limited to 'lib')
-rw-r--r--lib/avatar.cpp187
-rw-r--r--lib/avatar.h61
-rw-r--r--lib/connection.cpp943
-rw-r--r--lib/connection.h475
-rw-r--r--lib/connectiondata.cpp108
-rw-r--r--lib/connectiondata.h55
-rw-r--r--lib/converters.h177
-rw-r--r--lib/events/accountdataevents.h78
-rw-r--r--lib/events/directchatevent.cpp36
-rw-r--r--lib/events/directchatevent.h34
-rw-r--r--lib/events/event.cpp182
-rw-r--r--lib/events/event.h314
-rw-r--r--lib/events/eventcontent.cpp85
-rw-r--r--lib/events/eventcontent.h314
-rw-r--r--lib/events/receiptevent.cpp70
-rw-r--r--lib/events/receiptevent.h50
-rw-r--r--lib/events/redactionevent.cpp1
-rw-r--r--lib/events/redactionevent.h43
-rw-r--r--lib/events/roomavatarevent.cpp23
-rw-r--r--lib/events/roomavatarevent.h43
-rw-r--r--lib/events/roommemberevent.cpp69
-rw-r--r--lib/events/roommemberevent.h78
-rw-r--r--lib/events/roommessageevent.cpp193
-rw-r--r--lib/events/roommessageevent.h194
-rw-r--r--lib/events/simplestateevents.h53
-rw-r--r--lib/events/typingevent.cpp32
-rw-r--r--lib/events/typingevent.h39
-rw-r--r--lib/jobs/basejob.cpp508
-rw-r--r--lib/jobs/basejob.h303
-rw-r--r--lib/jobs/checkauthmethods.cpp53
-rw-r--r--lib/jobs/checkauthmethods.h40
-rw-r--r--lib/jobs/downloadfilejob.cpp120
-rw-r--r--lib/jobs/downloadfilejob.h30
-rw-r--r--lib/jobs/generated/account-data.cpp28
-rw-r--r--lib/jobs/generated/account-data.h27
-rw-r--r--lib/jobs/generated/administrative_contact.cpp122
-rw-r--r--lib/jobs/generated/administrative_contact.h83
-rw-r--r--lib/jobs/generated/banning.cpp34
-rw-r--r--lib/jobs/generated/banning.h26
-rw-r--r--lib/jobs/generated/content-repo.cpp254
-rw-r--r--lib/jobs/generated/content-repo.h129
-rw-r--r--lib/jobs/generated/create_room.cpp115
-rw-r--r--lib/jobs/generated/create_room.h55
-rw-r--r--lib/jobs/generated/directory.cpp76
-rw-r--r--lib/jobs/generated/directory.h58
-rw-r--r--lib/jobs/generated/inviting.cpp23
-rw-r--r--lib/jobs/generated/inviting.h20
-rw-r--r--lib/jobs/generated/kicking.cpp25
-rw-r--r--lib/jobs/generated/kicking.h20
-rw-r--r--lib/jobs/generated/leaving.cpp38
-rw-r--r--lib/jobs/generated/leaving.h40
-rw-r--r--lib/jobs/generated/list_public_rooms.cpp266
-rw-r--r--lib/jobs/generated/list_public_rooms.h106
-rw-r--r--lib/jobs/generated/login.cpp79
-rw-r--r--lib/jobs/generated/login.h33
-rw-r--r--lib/jobs/generated/logout.cpp26
-rw-r--r--lib/jobs/generated/logout.h27
-rw-r--r--lib/jobs/generated/profile.cpp140
-rw-r--r--lib/jobs/generated/profile.h96
-rw-r--r--lib/jobs/generated/receipts.cpp21
-rw-r--r--lib/jobs/generated/receipts.h21
-rw-r--r--lib/jobs/generated/redaction.cpp45
-rw-r--r--lib/jobs/generated/redaction.h30
-rw-r--r--lib/jobs/generated/third_party_membership.cpp25
-rw-r--r--lib/jobs/generated/third_party_membership.h20
-rw-r--r--lib/jobs/generated/typing.cpp24
-rw-r--r--lib/jobs/generated/typing.h20
-rw-r--r--lib/jobs/generated/versions.cpp47
-rw-r--r--lib/jobs/generated/versions.h38
-rw-r--r--lib/jobs/generated/whoami.cpp50
-rw-r--r--lib/jobs/generated/whoami.h37
-rw-r--r--lib/jobs/joinroomjob.cpp58
-rw-r--r--lib/jobs/joinroomjob.h40
-rw-r--r--lib/jobs/mediathumbnailjob.cpp63
-rw-r--r--lib/jobs/mediathumbnailjob.h47
-rw-r--r--lib/jobs/passwordlogin.cpp74
-rw-r--r--lib/jobs/passwordlogin.h42
-rw-r--r--lib/jobs/postreadmarkersjob.h37
-rw-r--r--lib/jobs/postreceiptjob.cpp27
-rw-r--r--lib/jobs/postreceiptjob.h30
-rw-r--r--lib/jobs/requestdata.cpp38
-rw-r--r--lib/jobs/requestdata.h59
-rw-r--r--lib/jobs/roommessagesjob.cpp65
-rw-r--r--lib/jobs/roommessagesjob.h47
-rw-r--r--lib/jobs/sendeventjob.cpp45
-rw-r--r--lib/jobs/sendeventjob.h57
-rw-r--r--lib/jobs/setroomstatejob.cpp32
-rw-r--r--lib/jobs/setroomstatejob.h64
-rw-r--r--lib/jobs/syncjob.cpp133
-rw-r--r--lib/jobs/syncjob.h99
-rw-r--r--lib/joinstate.h48
-rw-r--r--lib/logging.cpp33
-rw-r--r--lib/logging.h78
-rw-r--r--lib/networkaccessmanager.cpp75
-rw-r--r--lib/networkaccessmanager.h49
-rw-r--r--lib/networksettings.cpp31
-rw-r--r--lib/networksettings.h44
-rw-r--r--lib/room.cpp1851
-rw-r--r--lib/room.h424
-rw-r--r--lib/settings.cpp123
-rw-r--r--lib/settings.h134
-rw-r--r--lib/user.cpp399
-rw-r--r--lib/user.h125
-rw-r--r--lib/util.h205
104 files changed, 11991 insertions, 0 deletions
diff --git a/lib/avatar.cpp b/lib/avatar.cpp
new file mode 100644
index 00000000..1ff2aae1
--- /dev/null
+++ b/lib/avatar.cpp
@@ -0,0 +1,187 @@
+/******************************************************************************
+ * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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 "avatar.h"
+
+#include "jobs/mediathumbnailjob.h"
+#include "events/eventcontent.h"
+#include "connection.h"
+
+#include <QtGui/QPainter>
+#include <QtCore/QPointer>
+
+using namespace QMatrixClient;
+
+class Avatar::Private
+{
+ public:
+ explicit Private(QIcon di, QUrl url = {})
+ : _defaultIcon(di), _url(url)
+ { }
+ QImage get(Connection* connection, QSize size,
+ get_callback_t callback) const;
+ bool upload(UploadContentJob* job, upload_callback_t callback);
+
+ bool checkUrl(QUrl url) const;
+
+ const QIcon _defaultIcon;
+ QUrl _url;
+
+ // The below are related to image caching, hence mutable
+ mutable QImage _originalImage;
+ mutable std::vector<QPair<QSize, QImage>> _scaledImages;
+ mutable QSize _requestedSize;
+ mutable bool _bannedUrl = false;
+ mutable bool _fetched = false;
+ mutable QPointer<MediaThumbnailJob> _thumbnailRequest = nullptr;
+ mutable QPointer<BaseJob> _uploadRequest = nullptr;
+ mutable std::vector<get_callback_t> callbacks;
+ mutable get_callback_t uploadCallback;
+};
+
+Avatar::Avatar(QIcon defaultIcon)
+ : d(std::make_unique<Private>(std::move(defaultIcon)))
+{ }
+
+Avatar::Avatar(QUrl url, QIcon defaultIcon)
+ : d(std::make_unique<Private>(std::move(defaultIcon), std::move(url)))
+{ }
+
+Avatar::Avatar(Avatar&&) = default;
+
+Avatar::~Avatar() = default;
+
+Avatar& Avatar::operator=(Avatar&&) = default;
+
+QImage Avatar::get(Connection* connection, int dimension,
+ get_callback_t callback) const
+{
+ return d->get(connection, {dimension, dimension}, callback);
+}
+
+QImage Avatar::get(Connection* connection, int width, int height,
+ get_callback_t callback) const
+{
+ return d->get(connection, {width, height}, callback);
+}
+
+bool Avatar::upload(Connection* connection, const QString& fileName,
+ upload_callback_t callback) const
+{
+ if (isJobRunning(d->_uploadRequest))
+ return false;
+ return d->upload(connection->uploadFile(fileName), callback);
+}
+
+bool Avatar::upload(Connection* connection, QIODevice* source,
+ upload_callback_t callback) const
+{
+ if (isJobRunning(d->_uploadRequest) || !source->isReadable())
+ return false;
+ return d->upload(connection->uploadContent(source), callback);
+}
+
+QString Avatar::mediaId() const
+{
+ return d->_url.authority() + d->_url.path();
+}
+
+QImage Avatar::Private::get(Connection* connection, QSize size,
+ get_callback_t callback) const
+{
+ // FIXME: Alternating between longer-width and longer-height requests
+ // is a sure way to trick the below code into constantly getting another
+ // image from the server because the existing one is alleged unsatisfactory.
+ // This is plain abuse by the client, though; so not critical for now.
+ if( ( !(_fetched || _thumbnailRequest)
+ || size.width() > _requestedSize.width()
+ || size.height() > _requestedSize.height() ) && checkUrl(_url) )
+ {
+ qCDebug(MAIN) << "Getting avatar from" << _url.toString();
+ _requestedSize = size;
+ if (isJobRunning(_thumbnailRequest))
+ _thumbnailRequest->abandon();
+ callbacks.emplace_back(std::move(callback));
+ _thumbnailRequest = connection->getThumbnail(_url, size);
+ QObject::connect( _thumbnailRequest, &MediaThumbnailJob::success, [this]
+ {
+ _fetched = true;
+ _originalImage = _thumbnailRequest->scaledThumbnail(_requestedSize);
+ _scaledImages.clear();
+ for (auto n: callbacks)
+ n();
+ });
+ }
+
+ if( _originalImage.isNull() )
+ {
+ if (_defaultIcon.isNull())
+ return _originalImage;
+
+ QPainter p { &_originalImage };
+ _defaultIcon.paint(&p, { QPoint(), _defaultIcon.actualSize(size) });
+ }
+
+ for (auto p: _scaledImages)
+ if (p.first == size)
+ return p.second;
+ auto result = _originalImage.scaled(size,
+ Qt::KeepAspectRatio, Qt::SmoothTransformation);
+ _scaledImages.emplace_back(size, result);
+ return result;
+}
+
+bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t callback)
+{
+ _uploadRequest = job;
+ if (!isJobRunning(_uploadRequest))
+ return false;
+ _uploadRequest->connect(_uploadRequest, &BaseJob::success,
+ [job,callback] { callback(job->contentUri()); });
+ return true;
+}
+
+bool Avatar::Private::checkUrl(QUrl url) const
+{
+ if (_bannedUrl || url.isEmpty())
+ return false;
+
+ // FIXME: Make "mxc" a library-wide constant and maybe even make
+ // the URL checker a Connection(?) method.
+ _bannedUrl = !(url.isValid() &&
+ url.scheme() == "mxc" && url.path().count('/') == 1);
+ if (_bannedUrl)
+ qCWarning(MAIN) << "Avatar URL is invalid or not mxc-based:"
+ << url.toDisplayString();
+ return !_bannedUrl;
+}
+
+QUrl Avatar::url() const { return d->_url; }
+
+bool Avatar::updateUrl(const QUrl& newUrl)
+{
+ if (newUrl == d->_url)
+ return false;
+
+ d->_url = newUrl;
+ d->_fetched = false;
+ if (isJobRunning(d->_thumbnailRequest))
+ d->_thumbnailRequest->abandon();
+ return true;
+}
+
diff --git a/lib/avatar.h b/lib/avatar.h
new file mode 100644
index 00000000..0166ae9e
--- /dev/null
+++ b/lib/avatar.h
@@ -0,0 +1,61 @@
+/******************************************************************************
+ * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include <QtGui/QIcon>
+#include <QtCore/QUrl>
+
+#include <functional>
+#include <memory>
+
+namespace QMatrixClient
+{
+ class Connection;
+
+ class Avatar
+ {
+ public:
+ explicit Avatar(QIcon defaultIcon = {});
+ explicit Avatar(QUrl url, QIcon defaultIcon = {});
+ Avatar(Avatar&&);
+ ~Avatar();
+ Avatar& operator=(Avatar&&);
+
+ using get_callback_t = std::function<void()>;
+ using upload_callback_t = std::function<void(QString)>;
+
+ QImage get(Connection* connection, int dimension,
+ get_callback_t callback) const;
+ QImage get(Connection* connection, int w, int h,
+ get_callback_t callback) const;
+
+ bool upload(Connection* connection, const QString& fileName,
+ upload_callback_t callback) const;
+ bool upload(Connection* connection, QIODevice* source,
+ upload_callback_t callback) const;
+
+ QString mediaId() const;
+ QUrl url() const;
+ bool updateUrl(const QUrl& newUrl);
+
+ private:
+ class Private;
+ std::unique_ptr<Private> d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/connection.cpp b/lib/connection.cpp
new file mode 100644
index 00000000..2d7235b9
--- /dev/null
+++ b/lib/connection.cpp
@@ -0,0 +1,943 @@
+/******************************************************************************
+ * 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 "connection.h"
+#include "connectiondata.h"
+#include "user.h"
+#include "events/event.h"
+#include "events/directchatevent.h"
+#include "room.h"
+#include "settings.h"
+#include "jobs/generated/login.h"
+#include "jobs/generated/logout.h"
+#include "jobs/generated/receipts.h"
+#include "jobs/generated/leaving.h"
+#include "jobs/generated/account-data.h"
+#include "jobs/sendeventjob.h"
+#include "jobs/joinroomjob.h"
+#include "jobs/roommessagesjob.h"
+#include "jobs/syncjob.h"
+#include "jobs/mediathumbnailjob.h"
+#include "jobs/downloadfilejob.h"
+
+#include <QtNetwork/QDnsLookup>
+#include <QtCore/QFile>
+#include <QtCore/QDir>
+#include <QtCore/QFileInfo>
+#include <QtCore/QStandardPaths>
+#include <QtCore/QStringBuilder>
+#include <QtCore/QElapsedTimer>
+#include <QtCore/QRegularExpression>
+#include <QtCore/QCoreApplication>
+
+using namespace QMatrixClient;
+
+using DirectChatsMap = QMultiHash<const User*, QString>;
+
+class Connection::Private
+{
+ public:
+ explicit Private(std::unique_ptr<ConnectionData>&& connection)
+ : data(move(connection))
+ { }
+ Q_DISABLE_COPY(Private)
+ Private(Private&&) = delete;
+ Private operator=(Private&&) = delete;
+
+ Connection* q = nullptr;
+ std::unique_ptr<ConnectionData> data;
+ // A complex key below is a pair of room name and whether its
+ // state is Invited. The spec mandates to keep Invited room state
+ // separately so we should, e.g., keep objects for Invite and
+ // Leave state of the same room.
+ QHash<QPair<QString, bool>, Room*> roomMap;
+ QVector<QString> roomIdsToForget;
+ QMap<QString, User*> userMap;
+ DirectChatsMap directChats;
+ QHash<QString, QVariantHash> accountData;
+ QString userId;
+
+ SyncJob* syncJob = nullptr;
+
+ bool cacheState = true;
+ bool cacheToBinary = SettingsGroup("libqmatrixclient")
+ .value("cache_type").toString() != "json";
+
+ void connectWithToken(const QString& user, const QString& accessToken,
+ const QString& deviceId);
+ void broadcastDirectChatUpdates();
+};
+
+Connection::Connection(const QUrl& server, QObject* parent)
+ : QObject(parent)
+ , d(std::make_unique<Private>(std::make_unique<ConnectionData>(server)))
+{
+ d->q = this; // All d initialization should occur before this line
+}
+
+Connection::Connection(QObject* parent)
+ : Connection({}, parent)
+{ }
+
+Connection::~Connection()
+{
+ qCDebug(MAIN) << "deconstructing connection object for" << d->userId;
+ stopSync();
+}
+
+void Connection::resolveServer(const QString& mxidOrDomain)
+{
+ // At this point we may have something as complex as
+ // @username:[IPv6:address]:port, or as simple as a plain domain name.
+
+ // Try to parse as an FQID; if there's no @ part, assume it's a domain name.
+ QRegularExpression parser(
+ "^(@.+?:)?" // Optional username (allow everything for compatibility)
+ "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address
+ "(:\\d{1,5})?$", // Optional port
+ QRegularExpression::UseUnicodePropertiesOption); // Because asian digits
+ auto match = parser.match(mxidOrDomain);
+
+ QUrl maybeBaseUrl = QUrl::fromUserInput(match.captured(2));
+ maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http"
+ if (!match.hasMatch() || !maybeBaseUrl.isValid())
+ {
+ emit resolveError(
+ tr("%1 is not a valid homeserver address")
+ .arg(maybeBaseUrl.toString()));
+ return;
+ }
+
+ setHomeserver(maybeBaseUrl);
+ emit resolved();
+ return;
+
+ // FIXME, #178: The below code is incorrect and is no more executed. The
+ // correct server resolution should be done from .well-known/matrix/client
+ auto domain = maybeBaseUrl.host();
+ qCDebug(MAIN) << "Finding the server" << domain;
+ // Check if the Matrix server has a dedicated service record.
+ QDnsLookup* dns = new QDnsLookup();
+ dns->setType(QDnsLookup::SRV);
+ dns->setName("_matrix._tcp." + domain);
+
+ connect(dns, &QDnsLookup::finished, [this,dns,maybeBaseUrl]() {
+ QUrl baseUrl { maybeBaseUrl };
+ if (dns->error() == QDnsLookup::NoError &&
+ dns->serviceRecords().isEmpty())
+ {
+ auto record = dns->serviceRecords().front();
+ baseUrl.setHost(record.target());
+ baseUrl.setPort(record.port());
+ qCDebug(MAIN) << "SRV record for" << maybeBaseUrl.host()
+ << "is" << baseUrl.authority();
+ } else {
+ qCDebug(MAIN) << baseUrl.host() << "doesn't have SRV record"
+ << dns->name() << "- using the hostname as is";
+ }
+ setHomeserver(baseUrl);
+ emit resolved();
+ dns->deleteLater();
+ });
+ dns->lookup();
+}
+
+void Connection::connectToServer(const QString& user, const QString& password,
+ const QString& initialDeviceName,
+ const QString& deviceId)
+{
+ checkAndConnect(user,
+ [=] {
+ doConnectToServer(user, password, initialDeviceName, deviceId);
+ });
+}
+void Connection::doConnectToServer(const QString& user, const QString& password,
+ const QString& initialDeviceName,
+ const QString& deviceId)
+{
+ auto loginJob = callApi<LoginJob>(QStringLiteral("m.login.password"),
+ user, /*medium*/ "", /*address*/ "", password, /*token*/ "",
+ deviceId, initialDeviceName);
+ connect(loginJob, &BaseJob::success, this,
+ [this, loginJob] {
+ d->connectWithToken(loginJob->userId(), loginJob->accessToken(),
+ loginJob->deviceId());
+ });
+ connect(loginJob, &BaseJob::failure, this,
+ [this, loginJob] {
+ emit loginError(loginJob->errorString());
+ });
+}
+
+void Connection::connectWithToken(const QString& userId,
+ const QString& accessToken,
+ const QString& deviceId)
+{
+ checkAndConnect(userId,
+ [=] { d->connectWithToken(userId, accessToken, deviceId); });
+}
+
+void Connection::Private::connectWithToken(const QString& user,
+ const QString& accessToken,
+ const QString& deviceId)
+{
+ userId = user;
+ data->setToken(accessToken.toLatin1());
+ data->setDeviceId(deviceId);
+ qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString()
+ << "by user" << userId << "from device" << deviceId;
+ emit q->connected();
+
+}
+
+void Connection::checkAndConnect(const QString& userId,
+ std::function<void()> connectFn)
+{
+ if (d->data->baseUrl().isValid())
+ {
+ connectFn();
+ return;
+ }
+ // Not good to go, try to fix the homeserver URL.
+ if (userId.startsWith('@') && userId.indexOf(':') != -1)
+ {
+ // The below construct makes a single-shot connection that triggers
+ // on the signal and then self-disconnects.
+ // NB: doResolveServer can emit resolveError, so this is a part of
+ // checkAndConnect function contract.
+ QMetaObject::Connection connection;
+ connection = connect(this, &Connection::homeserverChanged,
+ this, [=] { connectFn(); disconnect(connection); });
+ resolveServer(userId);
+ } else
+ emit resolveError(
+ tr("%1 is an invalid homeserver URL")
+ .arg(d->data->baseUrl().toString()));
+}
+
+void Connection::logout()
+{
+ auto job = callApi<LogoutJob>();
+ connect( job, &LogoutJob::success, this, [this] {
+ stopSync();
+ emit loggedOut();
+ });
+}
+
+void Connection::sync(int timeout)
+{
+ if (d->syncJob)
+ return;
+
+ // Raw string: http://en.cppreference.com/w/cpp/language/string_literal
+ const QString filter { R"({"room": { "timeline": { "limit": 100 } } })" };
+ auto job = d->syncJob =
+ callApi<SyncJob>(d->data->lastEvent(), filter, timeout);
+ connect( job, &SyncJob::success, [this, job] {
+ onSyncSuccess(job->takeData());
+ d->syncJob = nullptr;
+ emit syncDone();
+ });
+ connect( job, &SyncJob::retryScheduled, this, &Connection::networkError);
+ connect( job, &SyncJob::failure, [this, job] {
+ d->syncJob = nullptr;
+ if (job->error() == BaseJob::ContentAccessError)
+ emit loginError(job->errorString());
+ else
+ emit syncError(job->errorString());
+ });
+}
+
+void Connection::onSyncSuccess(SyncData &&data) {
+ d->data->setLastEvent(data.nextBatch());
+ for (auto&& roomData: data.takeRoomData())
+ {
+ const auto forgetIdx = d->roomIdsToForget.indexOf(roomData.roomId);
+ if (forgetIdx != -1)
+ {
+ d->roomIdsToForget.removeAt(forgetIdx);
+ if (roomData.joinState == JoinState::Leave)
+ {
+ qDebug(MAIN) << "Room" << roomData.roomId
+ << "has been forgotten, ignoring /sync response for it";
+ continue;
+ }
+ qWarning(MAIN) << "Room" << roomData.roomId
+ << "has just been forgotten but /sync returned it in"
+ << toCString(roomData.joinState)
+ << "state - suspiciously fast turnaround";
+ }
+ if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) )
+ r->updateData(std::move(roomData));
+ QCoreApplication::processEvents();
+ }
+ for (auto&& accountEvent: data.takeAccountData())
+ {
+ if (accountEvent->type() == EventType::DirectChat)
+ {
+ DirectChatsMap newDirectChats;
+ const auto* event = static_cast<DirectChatEvent*>(accountEvent.get());
+ auto usersToDCs = event->usersToDirectChats();
+ for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it)
+ {
+ newDirectChats.insert(user(it.key()), it.value());
+ qCDebug(MAIN) << "Marked room" << it.value()
+ << "as a direct chat with" << it.key();
+ }
+ if (newDirectChats != d->directChats)
+ {
+ d->directChats = newDirectChats;
+ emit directChatsListChanged();
+ }
+ continue;
+ }
+ d->accountData[accountEvent->jsonType()] =
+ accountEvent->contentJson().toVariantHash();
+ }
+}
+
+void Connection::stopSync()
+{
+ if (d->syncJob)
+ {
+ d->syncJob->abandon();
+ d->syncJob = nullptr;
+ }
+}
+
+void Connection::postMessage(Room* room, const QString& type, const QString& message) const
+{
+ callApi<SendEventJob>(room->id(), type, message);
+}
+
+PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const
+{
+ return callApi<PostReceiptJob>(room->id(), "m.read", event->id());
+}
+
+JoinRoomJob* Connection::joinRoom(const QString& roomAlias)
+{
+ auto job = callApi<JoinRoomJob>(roomAlias);
+ connect(job, &JoinRoomJob::success,
+ this, [this, job] { provideRoom(job->roomId(), JoinState::Join); });
+ return job;
+}
+
+void Connection::leaveRoom(Room* room)
+{
+ callApi<LeaveRoomJob>(room->id());
+}
+
+RoomMessagesJob* Connection::getMessages(Room* room, const QString& from) const
+{
+ return callApi<RoomMessagesJob>(room->id(), from);
+}
+
+inline auto splitMediaId(const QString& mediaId)
+{
+ auto idParts = mediaId.split('/');
+ Q_ASSERT_X(idParts.size() == 2, __FUNCTION__,
+ ("'" + mediaId +
+ "' doesn't look like 'serverName/localMediaId'").toLatin1());
+ return idParts;
+}
+
+MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId, QSize requestedSize) const
+{
+ auto idParts = splitMediaId(mediaId);
+ return callApi<MediaThumbnailJob>(idParts.front(), idParts.back(),
+ requestedSize);
+}
+
+MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, QSize requestedSize) const
+{
+ return getThumbnail(url.authority() + url.path(), requestedSize);
+}
+
+MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, int requestedWidth,
+ int requestedHeight) const
+{
+ return getThumbnail(url, QSize(requestedWidth, requestedHeight));
+}
+
+UploadContentJob* Connection::uploadContent(QIODevice* contentSource,
+ const QString& filename, const QString& contentType) const
+{
+ return callApi<UploadContentJob>(contentSource, filename, contentType);
+}
+
+UploadContentJob* Connection::uploadFile(const QString& fileName,
+ const QString& contentType)
+{
+ auto sourceFile = new QFile(fileName);
+ if (sourceFile->open(QIODevice::ReadOnly))
+ {
+ qCWarning(MAIN) << "Couldn't open" << sourceFile->fileName()
+ << "for reading";
+ return nullptr;
+ }
+ return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(),
+ contentType);
+}
+
+GetContentJob* Connection::getContent(const QString& mediaId) const
+{
+ auto idParts = splitMediaId(mediaId);
+ return callApi<GetContentJob>(idParts.front(), idParts.back());
+}
+
+GetContentJob* Connection::getContent(const QUrl& url) const
+{
+ return getContent(url.authority() + url.path());
+}
+
+DownloadFileJob* Connection::downloadFile(const QUrl& url,
+ const QString& localFilename) const
+{
+ auto mediaId = url.authority() + url.path();
+ auto idParts = splitMediaId(mediaId);
+ auto* job = callApi<DownloadFileJob>(idParts.front(), idParts.back(),
+ localFilename);
+ return job;
+}
+
+CreateRoomJob* Connection::createRoom(RoomVisibility visibility,
+ const QString& alias, const QString& name, const QString& topic,
+ const QVector<QString>& invites, const QString& presetName,
+ bool isDirect, bool guestsCanJoin,
+ const QVector<CreateRoomJob::StateEvent>& initialState,
+ const QVector<CreateRoomJob::Invite3pid>& invite3pids,
+ const QJsonObject creationContent)
+{
+ auto job = callApi<CreateRoomJob>(
+ visibility == PublishRoom ? "public" : "private", alias, name,
+ topic, invites, invite3pids, creationContent, initialState,
+ presetName, isDirect, guestsCanJoin);
+ connect(job, &BaseJob::success, this, [this,job] {
+ emit createdRoom(provideRoom(job->roomId(), JoinState::Join));
+ });
+ return job;
+}
+
+void Connection::requestDirectChat(const QString& userId)
+{
+ doInDirectChat(userId, [this] (Room* r) { emit directChatAvailable(r); });
+}
+
+void Connection::doInDirectChat(const QString& userId,
+ std::function<void (Room*)> operation)
+{
+ // There can be more than one DC; find the first valid, and delete invalid
+ // (left/forgotten) ones along the way.
+ for (auto roomId: d->directChats.values(user(userId)))
+ {
+ if (auto r = room(roomId, JoinState::Join))
+ {
+ Q_ASSERT(r->id() == roomId);
+ qCDebug(MAIN) << "Requested direct chat with" << userId
+ << "is already available as" << r->id();
+ operation(r);
+ return;
+ }
+ if (auto ir = invitation(roomId))
+ {
+ Q_ASSERT(ir->id() == roomId);
+ auto j = joinRoom(ir->id());
+ connect(j, &BaseJob::success, this, [this,roomId,userId,operation] {
+ qCDebug(MAIN) << "Joined the already invited direct chat with"
+ << userId << "as" << roomId;
+ operation(room(roomId, JoinState::Join));
+ });
+ }
+ qCWarning(MAIN) << "Direct chat with" << userId << "known as room"
+ << roomId << "is not valid, discarding it";
+ removeFromDirectChats(roomId);
+ }
+
+ auto j = createDirectChat(userId);
+ connect(j, &BaseJob::success, this, [this,j,userId,operation] {
+ qCDebug(MAIN) << "Direct chat with" << userId
+ << "has been created as" << j->roomId();
+ operation(room(j->roomId(), JoinState::Join));
+ });
+}
+
+CreateRoomJob* Connection::createDirectChat(const QString& userId,
+ const QString& topic, const QString& name)
+{
+ return createRoom(UnpublishRoom, "", name, topic, {userId},
+ "trusted_private_chat", true);
+}
+
+ForgetRoomJob* Connection::forgetRoom(const QString& id)
+{
+ // To forget is hard :) First we should ensure the local user is not
+ // in the room (by leaving it, if necessary); once it's done, the /forget
+ // endpoint can be called; and once this is through, the local Room object
+ // (if any existed) is deleted. At the same time, we still have to
+ // (basically immediately) return a pointer to ForgetRoomJob. Therefore
+ // a ForgetRoomJob is created in advance and can be returned in a probably
+ // not-yet-started state (it will start once /leave completes).
+ auto forgetJob = new ForgetRoomJob(id);
+ auto room = d->roomMap.value({id, false});
+ if (!room)
+ room = d->roomMap.value({id, true});
+ if (room && room->joinState() != JoinState::Leave)
+ {
+ auto leaveJob = room->leaveRoom();
+ connect(leaveJob, &BaseJob::success, this, [this, forgetJob, room] {
+ forgetJob->start(connectionData());
+ // If the matching /sync response hasn't arrived yet, mark the room
+ // for explicit deletion
+ if (room->joinState() != JoinState::Leave)
+ d->roomIdsToForget.push_back(room->id());
+ });
+ connect(leaveJob, &BaseJob::failure, forgetJob, &BaseJob::abandon);
+ }
+ else
+ forgetJob->start(connectionData());
+ connect(forgetJob, &BaseJob::success, this, [this, id]
+ {
+ // If the room is in the map (possibly in both forms), delete all forms.
+ for (auto f: {false, true})
+ if (auto r = d->roomMap.take({ id, f }))
+ {
+ emit aboutToDeleteRoom(r);
+ qCDebug(MAIN) << "Room" << id
+ << "in join state" << toCString(r->joinState())
+ << "will be deleted";
+ r->deleteLater();
+ }
+ });
+ return forgetJob;
+}
+
+QUrl Connection::homeserver() const
+{
+ return d->data->baseUrl();
+}
+
+Room* Connection::room(const QString& roomId, JoinStates states) const
+{
+ Room* room = d->roomMap.value({roomId, false}, nullptr);
+ if (states.testFlag(JoinState::Join) &&
+ room && room->joinState() == JoinState::Join)
+ return room;
+
+ if (states.testFlag(JoinState::Invite))
+ if (Room* invRoom = invitation(roomId))
+ return invRoom;
+
+ if (states.testFlag(JoinState::Leave) &&
+ room && room->joinState() == JoinState::Leave)
+ return room;
+
+ return nullptr;
+}
+
+Room* Connection::invitation(const QString& roomId) const
+{
+ return d->roomMap.value({roomId, true}, nullptr);
+}
+
+User* Connection::user(const QString& userId)
+{
+ if( d->userMap.contains(userId) )
+ return d->userMap.value(userId);
+ auto* user = userFactory(this, userId);
+ d->userMap.insert(userId, user);
+ emit newUser(user);
+ return user;
+}
+
+const User* Connection::user() const
+{
+ return d->userId.isEmpty() ? nullptr : d->userMap.value(d->userId, nullptr);
+}
+
+User* Connection::user()
+{
+ return d->userId.isEmpty() ? nullptr : user(d->userId);
+}
+
+QString Connection::userId() const
+{
+ return d->userId;
+}
+
+QString Connection::deviceId() const
+{
+ return d->data->deviceId();
+}
+
+QString Connection::token() const
+{
+ return accessToken();
+}
+
+QByteArray Connection::accessToken() const
+{
+ return d->data->accessToken();
+}
+
+SyncJob* Connection::syncJob() const
+{
+ return d->syncJob;
+}
+
+int Connection::millisToReconnect() const
+{
+ return d->syncJob ? d->syncJob->millisToRetry() : 0;
+}
+
+QHash< QPair<QString, bool>, Room* > Connection::roomMap() const
+{
+ // Copy-on-write-and-remove-elements is faster than copying elements one by one.
+ QHash< QPair<QString, bool>, Room* > roomMap = d->roomMap;
+ for (auto it = roomMap.begin(); it != roomMap.end(); )
+ {
+ if (it.value()->joinState() == JoinState::Leave)
+ it = roomMap.erase(it);
+ else
+ ++it;
+ }
+ return roomMap;
+}
+
+QHash<QString, QVector<Room*>> Connection::tagsToRooms() const
+{
+ QHash<QString, QVector<Room*>> result;
+ for (auto* r: d->roomMap)
+ {
+ for (const auto& tagName: r->tagNames())
+ result[tagName].push_back(r);
+ }
+ for (auto it = result.begin(); it != result.end(); ++it)
+ std::sort(it->begin(), it->end(),
+ [t=it.key()] (Room* r1, Room* r2) {
+ return r1->tags().value(t).order < r2->tags().value(t).order;
+ });
+ return result;
+}
+
+QStringList Connection::tagNames() const
+{
+ QStringList tags ({FavouriteTag});
+ for (auto* r: d->roomMap)
+ for (const auto& tag: r->tagNames())
+ if (tag != LowPriorityTag && !tags.contains(tag))
+ tags.push_back(tag);
+ tags.push_back(LowPriorityTag);
+ return tags;
+}
+
+QVector<Room*> Connection::roomsWithTag(const QString& tagName) const
+{
+ QVector<Room*> rooms;
+ std::copy_if(d->roomMap.begin(), d->roomMap.end(), std::back_inserter(rooms),
+ [&tagName] (Room* r) { return r->tags().contains(tagName); });
+ return rooms;
+}
+
+QJsonObject toJson(const DirectChatsMap& directChats)
+{
+ QJsonObject json;
+ for (auto it = directChats.keyBegin(); it != directChats.keyEnd(); ++it)
+ json.insert((*it)->id(), toJson(directChats.values(*it)));
+ return json;
+}
+
+void Connection::Private::broadcastDirectChatUpdates()
+{
+ q->callApi<SetAccountDataJob>(userId, QStringLiteral("m.direct"),
+ toJson(directChats));
+ emit q->directChatsListChanged();
+}
+
+void Connection::addToDirectChats(const Room* room, const User* user)
+{
+ Q_ASSERT(room != nullptr && user != nullptr);
+ if (d->directChats.contains(user, room->id()))
+ return;
+ d->directChats.insert(user, room->id());
+ d->broadcastDirectChatUpdates();
+}
+
+void Connection::removeFromDirectChats(const QString& roomId, const User* user)
+{
+ Q_ASSERT(!roomId.isEmpty());
+ if ((user != nullptr && !d->directChats.contains(user, roomId)) ||
+ d->directChats.key(roomId) == nullptr)
+ return;
+ if (user != nullptr)
+ d->directChats.remove(user, roomId);
+ else
+ for (auto it = d->directChats.begin(); it != d->directChats.end();)
+ {
+ if (it.value() == roomId)
+ it = d->directChats.erase(it);
+ else
+ ++it;
+ }
+ d->broadcastDirectChatUpdates();
+}
+
+bool Connection::isDirectChat(const QString& roomId) const
+{
+ return d->directChats.key(roomId) != nullptr;
+}
+
+QList<const User*> Connection::directChatUsers(const Room* room) const
+{
+ Q_ASSERT(room != nullptr);
+ return d->directChats.keys(room->id());
+}
+
+QMap<QString, User*> Connection::users() const
+{
+ return d->userMap;
+}
+
+const ConnectionData* Connection::connectionData() const
+{
+ return d->data.get();
+}
+
+Room* Connection::provideRoom(const QString& id, JoinState joinState)
+{
+ // TODO: This whole function is a strong case for a RoomManager class.
+ Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id");
+
+ const auto roomKey = qMakePair(id, joinState == JoinState::Invite);
+ auto* room = d->roomMap.value(roomKey, nullptr);
+ if (room)
+ {
+ // Leave is a special case because in transition (5a) (see the .h file)
+ // joinState == room->joinState but we still have to preempt the Invite
+ // and emit a signal. For Invite and Join, there's no such problem.
+ if (room->joinState() == joinState && joinState != JoinState::Leave)
+ return room;
+ }
+ else
+ {
+ room = roomFactory(this, id, joinState);
+ if (!room)
+ {
+ qCCritical(MAIN) << "Failed to create a room" << id;
+ return nullptr;
+ }
+ d->roomMap.insert(roomKey, room);
+ emit newRoom(room);
+ }
+ if (joinState == JoinState::Invite)
+ {
+ // prev is either Leave or nullptr
+ auto* prev = d->roomMap.value({id, false}, nullptr);
+ emit invitedRoom(room, prev);
+ }
+ else
+ {
+ room->setJoinState(joinState);
+ // Preempt the Invite room (if any) with a room in Join/Leave state.
+ auto* prevInvite = d->roomMap.take({id, true});
+ if (joinState == JoinState::Join)
+ emit joinedRoom(room, prevInvite);
+ else if (joinState == JoinState::Leave)
+ emit leftRoom(room, prevInvite);
+ if (prevInvite)
+ {
+ qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id();
+ emit aboutToDeleteRoom(prevInvite);
+ prevInvite->deleteLater();
+ }
+ }
+
+ return room;
+}
+
+Connection::room_factory_t Connection::roomFactory =
+ [](Connection* c, const QString& id, JoinState joinState)
+ { return new Room(c, id, joinState); };
+
+Connection::user_factory_t Connection::userFactory =
+ [](Connection* c, const QString& id) { return new User(id, c); };
+
+QByteArray Connection::generateTxnId()
+{
+ return d->data->generateTxnId();
+}
+
+void Connection::setHomeserver(const QUrl& url)
+{
+ if (homeserver() == url)
+ return;
+
+ d->data->setBaseUrl(url);
+ emit homeserverChanged(homeserver());
+}
+
+static constexpr int CACHE_VERSION_MAJOR = 7;
+static constexpr int CACHE_VERSION_MINOR = 0;
+
+void Connection::saveState(const QUrl &toFile) const
+{
+ if (!d->cacheState)
+ return;
+
+ QElapsedTimer et; et.start();
+
+ QFileInfo stateFile {
+ toFile.isEmpty() ? stateCachePath() : toFile.toLocalFile()
+ };
+ if (!stateFile.dir().exists())
+ stateFile.dir().mkpath(".");
+
+ QFile outfile { stateFile.absoluteFilePath() };
+ if (!outfile.open(QFile::WriteOnly))
+ {
+ qCWarning(MAIN) << "Error opening" << stateFile.absoluteFilePath()
+ << ":" << outfile.errorString();
+ qCWarning(MAIN) << "Caching the rooms state disabled";
+ d->cacheState = false;
+ return;
+ }
+
+ QJsonObject rootObj;
+ {
+ QJsonObject rooms;
+ QJsonObject inviteRooms;
+ for (const auto* i : roomMap()) // Pass on rooms in Leave state
+ {
+ if (i->joinState() == JoinState::Invite)
+ inviteRooms.insert(i->id(), i->toJson());
+ else
+ rooms.insert(i->id(), i->toJson());
+ QElapsedTimer et1; et1.start();
+ QCoreApplication::processEvents();
+ if (et1.elapsed() > 1)
+ qCDebug(PROFILER) << "processEvents() borrowed" << et1;
+ }
+
+ QJsonObject roomObj;
+ if (!rooms.isEmpty())
+ roomObj.insert("join", rooms);
+ if (!inviteRooms.isEmpty())
+ roomObj.insert("invite", inviteRooms);
+
+ rootObj.insert("next_batch", d->data->lastEvent());
+ rootObj.insert("rooms", roomObj);
+ }
+ {
+ QJsonArray accountDataEvents {
+ QJsonObject {
+ { QStringLiteral("type"), QStringLiteral("m.direct") },
+ { QStringLiteral("content"), toJson(d->directChats) }
+ }
+ };
+
+ for (auto it = d->accountData.begin(); it != d->accountData.end(); ++it)
+ accountDataEvents.append(QJsonObject {
+ {"type", it.key()},
+ {"content", QJsonObject::fromVariantHash(it.value())}
+ });
+ rootObj.insert("account_data",
+ QJsonObject {{ QStringLiteral("events"), accountDataEvents }});
+ }
+
+ QJsonObject versionObj;
+ versionObj.insert("major", CACHE_VERSION_MAJOR);
+ versionObj.insert("minor", CACHE_VERSION_MINOR);
+ rootObj.insert("cache_version", versionObj);
+
+ QJsonDocument json { rootObj };
+ auto data = d->cacheToBinary ? json.toBinaryData() :
+ json.toJson(QJsonDocument::Compact);
+ qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et;
+
+ outfile.write(data.data(), data.size());
+ qCDebug(MAIN) << "State cache saved to" << outfile.fileName();
+}
+
+void Connection::loadState(const QUrl &fromFile)
+{
+ if (!d->cacheState)
+ return;
+
+ QElapsedTimer et; et.start();
+ QFile file {
+ fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile()
+ };
+ if (!file.exists())
+ {
+ qCDebug(MAIN) << "No state cache file found";
+ return;
+ }
+ if(!file.open(QFile::ReadOnly))
+ {
+ qCWarning(MAIN) << "file " << file.fileName() << "failed to open for read";
+ return;
+ }
+ QByteArray data = file.readAll();
+
+ auto jsonDoc = d->cacheToBinary ? QJsonDocument::fromBinaryData(data) :
+ QJsonDocument::fromJson(data);
+ if (jsonDoc.isNull())
+ {
+ qCWarning(MAIN) << "Cache file broken, discarding";
+ return;
+ }
+ auto actualCacheVersionMajor =
+ jsonDoc.object()
+ .value("cache_version").toObject()
+ .value("major").toInt();
+ if (actualCacheVersionMajor < CACHE_VERSION_MAJOR)
+ {
+ qCWarning(MAIN)
+ << "Major version of the cache file is" << actualCacheVersionMajor
+ << "but" << CACHE_VERSION_MAJOR << "required; discarding the cache";
+ return;
+ }
+
+ SyncData sync;
+ sync.parseJson(jsonDoc);
+ onSyncSuccess(std::move(sync));
+ qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et;
+}
+
+QString Connection::stateCachePath() const
+{
+ auto safeUserId = userId();
+ safeUserId.replace(':', '_');
+ return QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
+ % '/' % safeUserId % "_state.json";
+}
+
+bool Connection::cacheState() const
+{
+ return d->cacheState;
+}
+
+void Connection::setCacheState(bool newValue)
+{
+ if (d->cacheState != newValue)
+ {
+ d->cacheState = newValue;
+ emit cacheStateChanged();
+ }
+}
+
diff --git a/lib/connection.h b/lib/connection.h
new file mode 100644
index 00000000..c6d543ec
--- /dev/null
+++ b/lib/connection.h
@@ -0,0 +1,475 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include "jobs/generated/create_room.h"
+#include "joinstate.h"
+
+#include <QtCore/QObject>
+#include <QtCore/QUrl>
+#include <QtCore/QSize>
+
+#include <functional>
+#include <memory>
+
+namespace QMatrixClient
+{
+ class Room;
+ class User;
+ class RoomEvent;
+ class ConnectionData;
+
+ class SyncJob;
+ class SyncData;
+ class RoomMessagesJob;
+ class PostReceiptJob;
+ class ForgetRoomJob;
+ class MediaThumbnailJob;
+ class JoinRoomJob;
+ class UploadContentJob;
+ class GetContentJob;
+ class DownloadFileJob;
+
+ class Connection: public QObject {
+ Q_OBJECT
+
+ /** Whether or not the rooms state should be cached locally
+ * \sa loadState(), saveState()
+ */
+ Q_PROPERTY(User* localUser READ user CONSTANT)
+ Q_PROPERTY(QString localUserId READ userId CONSTANT)
+ Q_PROPERTY(QString deviceId READ deviceId CONSTANT)
+ Q_PROPERTY(QByteArray accessToken READ accessToken CONSTANT)
+ Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged)
+ Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged)
+ public:
+ using room_factory_t =
+ std::function<Room*(Connection*, const QString&, JoinState joinState)>;
+ using user_factory_t =
+ std::function<User*(Connection*, const QString&)>;
+
+ enum RoomVisibility { PublishRoom, UnpublishRoom }; // FIXME: Should go inside CreateRoomJob
+
+ explicit Connection(QObject* parent = nullptr);
+ explicit Connection(const QUrl& server, QObject* parent = nullptr);
+ virtual ~Connection();
+
+ /** Get all Invited and Joined rooms
+ * \return a hashmap from a composite key - room name and whether
+ * it's an Invite rather than Join - to room pointers
+ */
+ QHash<QPair<QString, bool>, Room*> roomMap() const;
+
+ /** Get all Invited and Joined rooms grouped by tag
+ * \return a hashmap from tag name to a vector of room pointers,
+ * sorted by their order in the tag - details are at
+ * https://matrix.org/speculator/spec/drafts%2Fe2e/client_server/unstable.html#id95
+ */
+ QHash<QString, QVector<Room*>> tagsToRooms() const;
+
+ /** Get all room tags known on this connection */
+ QStringList tagNames() const;
+
+ /** Get the list of rooms with the specified tag */
+ QVector<Room*> roomsWithTag(const QString& tagName) const;
+
+ /** Mark the room as a direct chat with the user
+ * This function marks \p room as a direct chat with \p user.
+ * Emits the signal synchronously, without waiting to complete
+ * synchronisation with the server.
+ *
+ * \sa directChatsListChanged
+ */
+ void addToDirectChats(const Room* room, const User* user);
+
+ /** Unmark the room from direct chats
+ * This function removes the room id from direct chats either for
+ * a specific \p user or for all users if \p user in nullptr.
+ * The room id is used to allow removal of, e.g., ids of forgotten
+ * rooms; a Room object need not exist. Emits the signal
+ * immediately, without waiting to complete synchronisation with
+ * the server.
+ *
+ * \sa directChatsListChanged
+ */
+ void removeFromDirectChats(const QString& roomId,
+ const User* user = nullptr);
+
+ /** Check whether the room id corresponds to a direct chat */
+ bool isDirectChat(const QString& roomId) const;
+
+ /** Retrieve the list of users the room is a direct chat with
+ * @return The list of users for which this room is marked as
+ * a direct chat; an empty list if the room is not a direct chat
+ */
+ QList<const User*> directChatUsers(const Room* room) const;
+
+ QMap<QString, User*> users() const;
+
+ // FIXME: Convert Q_INVOKABLEs to Q_PROPERTIES
+ // (breaks back-compatibility)
+ QUrl homeserver() const;
+ Q_INVOKABLE Room* room(const QString& roomId,
+ JoinStates states = JoinState::Invite|JoinState::Join) const;
+ Q_INVOKABLE Room* invitation(const QString& roomId) const;
+ Q_INVOKABLE User* user(const QString& userId);
+ const User* user() const;
+ User* user();
+ QString userId() const;
+ QString deviceId() const;
+ /** @deprecated Use accessToken() instead. */
+ Q_INVOKABLE QString token() const;
+ QByteArray accessToken() const;
+ Q_INVOKABLE SyncJob* syncJob() const;
+ Q_INVOKABLE int millisToReconnect() const;
+
+ /**
+ * Call this before first sync to load from previously saved file.
+ *
+ * \param fromFile A local path to read the state from. Uses QUrl
+ * to be QML-friendly. Empty parameter means using a path
+ * defined by stateCachePath().
+ */
+ Q_INVOKABLE void loadState(const QUrl &fromFile = {});
+ /**
+ * This method saves the current state of rooms (but not messages
+ * in them) to a local cache file, so that it could be loaded by
+ * loadState() on a next run of the client.
+ *
+ * \param toFile A local path to save the state to. Uses QUrl to be
+ * QML-friendly. Empty parameter means using a path defined by
+ * stateCachePath().
+ */
+ Q_INVOKABLE void saveState(const QUrl &toFile = {}) const;
+
+ /**
+ * The default path to store the cached room state, defined as
+ * follows:
+ * QStandardPaths::writeableLocation(QStandardPaths::CacheLocation) + _safeUserId + "_state.json"
+ * where `_safeUserId` is userId() with `:` (colon) replaced with
+ * `_` (underscore)
+ * /see loadState(), saveState()
+ */
+ Q_INVOKABLE QString stateCachePath() const;
+
+ bool cacheState() const;
+ void setCacheState(bool newValue);
+
+ /**
+ * This is a universal method to start a job of a type passed
+ * as a template parameter. Arguments to callApi() are arguments
+ * to the job constructor _except_ the first ConnectionData*
+ * argument - callApi() will pass it automatically.
+ */
+ template <typename JobT, typename... JobArgTs>
+ JobT* callApi(JobArgTs&&... jobArgs) const
+ {
+ auto job = new JobT(std::forward<JobArgTs>(jobArgs)...);
+ job->start(connectionData());
+ return job;
+ }
+
+ /** Generates a new transaction id. Transaction id's are unique within
+ * a single Connection object
+ */
+ Q_INVOKABLE QByteArray generateTxnId();
+
+ template <typename T = Room>
+ static void setRoomType()
+ {
+ roomFactory =
+ [](Connection* c, const QString& id, JoinState joinState)
+ { return new T(c, id, joinState); };
+ }
+
+ template <typename T = User>
+ static void setUserType()
+ {
+ userFactory =
+ [](Connection* c, const QString& id) { return new T(id, c); };
+ }
+
+ public slots:
+ /** Set the homeserver base URL */
+ void setHomeserver(const QUrl& baseUrl);
+
+ /** Determine and set the homeserver from domain or MXID */
+ void resolveServer(const QString& mxidOrDomain);
+
+ void connectToServer(const QString& user, const QString& password,
+ const QString& initialDeviceName,
+ const QString& deviceId = {});
+ void connectWithToken(const QString& userId, const QString& accessToken,
+ const QString& deviceId);
+
+ /** @deprecated Use stopSync() instead */
+ void disconnectFromServer() { stopSync(); }
+ void logout();
+
+ void sync(int timeout = -1);
+ void stopSync();
+
+ virtual MediaThumbnailJob* getThumbnail(const QString& mediaId,
+ QSize requestedSize) const;
+ MediaThumbnailJob* getThumbnail(const QUrl& url,
+ QSize requestedSize) const;
+ MediaThumbnailJob* getThumbnail(const QUrl& url,
+ int requestedWidth,
+ int requestedHeight) const;
+
+ // QIODevice* should already be open
+ UploadContentJob* uploadContent(QIODevice* contentSource,
+ const QString& filename = {},
+ const QString& contentType = {}) const;
+ UploadContentJob* uploadFile(const QString& fileName,
+ const QString& contentType = {});
+ GetContentJob* getContent(const QString& mediaId) const;
+ GetContentJob* getContent(const QUrl& url) const;
+ // If localFilename is empty, a temporary file will be created
+ DownloadFileJob* downloadFile(const QUrl& url,
+ const QString& localFilename = {}) const;
+
+ /**
+ * \brief Create a room (generic method)
+ * This method allows to customize room entirely to your liking,
+ * providing all the attributes the original CS API provides.
+ */
+ CreateRoomJob* createRoom(RoomVisibility visibility,
+ const QString& alias, const QString& name, const QString& topic,
+ const QVector<QString>& invites, const QString& presetName = {}, bool isDirect = false,
+ bool guestsCanJoin = false,
+ const QVector<CreateRoomJob::StateEvent>& initialState = {},
+ const QVector<CreateRoomJob::Invite3pid>& invite3pids = {},
+ const QJsonObject creationContent = {});
+
+ /** Get a direct chat with a single user
+ * This method may return synchronously or asynchoronously depending
+ * on whether a direct chat room with the respective person exists
+ * already.
+ *
+ * \sa directChatAvailable
+ */
+ Q_INVOKABLE void requestDirectChat(const QString& userId);
+
+ /** Run an operation in a direct chat with the user
+ * This method may return synchronously or asynchoronously depending
+ * on whether a direct chat room with the respective person exists
+ * already. Instead of emitting a signal it executes the passed
+ * function object with the direct chat room as its parameter.
+ */
+ Q_INVOKABLE void doInDirectChat(const QString& userId,
+ std::function<void(Room*)> operation);
+
+ /** Create a direct chat with a single user, optional name and topic
+ * A room will always be created, unlike in requestDirectChat.
+ * It is advised to use requestDirectChat as a default way of getting
+ * one-on-one with a person, and only use createDirectChat when
+ * a new creation is explicitly desired.
+ */
+ CreateRoomJob* createDirectChat(const QString& userId,
+ const QString& topic = {}, const QString& name = {});
+
+ virtual JoinRoomJob* joinRoom(const QString& roomAlias);
+
+ /** Sends /forget to the server and also deletes room locally.
+ * This method is in Connection, not in Room, since it's a
+ * room lifecycle operation, and Connection is an acting room manager.
+ * It ensures that the local user is not a member of a room (running /leave,
+ * if necessary) then issues a /forget request and if that one doesn't fail
+ * deletion of the local Room object is ensured.
+ * \param id - the room id to forget
+ * \return - the ongoing /forget request to the server; note that the
+ * success() signal of this request is connected to deleteLater()
+ * of a respective room so by the moment this finishes, there might be no
+ * Room object anymore.
+ */
+ ForgetRoomJob* forgetRoom(const QString& id);
+
+ // Old API that will be abolished any time soon. DO NOT USE.
+
+ /** @deprecated Use callApi<PostMessageJob>() or Room::postMessage() instead */
+ virtual void postMessage(Room* room, const QString& type,
+ const QString& message) const;
+ /** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead */
+ virtual PostReceiptJob* postReceipt(Room* room,
+ RoomEvent* event) const;
+ /** @deprecated Use callApi<LeaveRoomJob>() or Room::leaveRoom() instead */
+ virtual void leaveRoom( Room* room );
+ /** @deprecated User callApi<RoomMessagesJob>() or Room::getPreviousContent() instead */
+ virtual RoomMessagesJob* getMessages(Room* room,
+ const QString& from) const;
+ signals:
+ /**
+ * @deprecated
+ * This was a signal resulting from a successful resolveServer().
+ * Since Connection now provides setHomeserver(), the HS URL
+ * may change even without resolveServer() invocation. Use
+ * homeserverChanged() instead of resolved(). You can also use
+ * connectToServer and connectWithToken without the HS URL set in
+ * advance (i.e. without calling resolveServer), as they now trigger
+ * server name resolution from MXID if the server URL is not valid.
+ */
+ void resolved();
+ void resolveError(QString error);
+
+ void homeserverChanged(QUrl baseUrl);
+
+ void connected();
+ void reconnected(); //< Unused; use connected() instead
+ void loggedOut();
+ void loginError(QString error);
+ void networkError(int retriesTaken, int inMilliseconds);
+
+ void syncDone();
+ void syncError(QString error);
+
+ void newUser(User* user);
+
+ /**
+ * \group Signals emitted on room transitions
+ *
+ * Note: Rooms in Invite state are always stored separately from
+ * rooms in Join/Leave state, because of special treatment of
+ * invite_state in Matrix CS API (see The Spec on /sync for details).
+ * Therefore, objects below are: r - room in Join/Leave state;
+ * i - room in Invite state
+ *
+ * 1. none -> Invite: newRoom(r), invitedRoom(r,nullptr)
+ * 2. none -> Join: newRoom(r), joinedRoom(r,nullptr)
+ * 3. none -> Leave: newRoom(r), leftRoom(r,nullptr)
+ * 4. Invite -> Join:
+ * newRoom(r), joinedRoom(r,i), aboutToDeleteRoom(i)
+ * 4a. Leave and Invite -> Join:
+ * joinedRoom(r,i), aboutToDeleteRoom(i)
+ * 5. Invite -> Leave:
+ * newRoom(r), leftRoom(r,i), aboutToDeleteRoom(i)
+ * 5a. Leave and Invite -> Leave:
+ * leftRoom(r,i), aboutToDeleteRoom(i)
+ * 6. Join -> Leave: leftRoom(r)
+ * 7. Leave -> Invite: newRoom(i), invitedRoom(i,r)
+ * 8. Leave -> Join: joinedRoom(r)
+ * The following transitions are only possible via forgetRoom()
+ * so far; if a room gets forgotten externally, sync won't tell
+ * about it:
+ * 9. any -> none: as any -> Leave, then aboutToDeleteRoom(r)
+ */
+
+ /** A new room object has been created */
+ void newRoom(Room* room);
+
+ /** Invitation to a room received
+ *
+ * If the same room is in Left state, it's passed in prev.
+ */
+ void invitedRoom(Room* room, Room* prev);
+
+ /** A room has just been joined
+ *
+ * It's not the same as receiving a room in "join" section of sync
+ * response (rooms will be there even after joining). If this room
+ * was in Invite state before, the respective object is passed in
+ * prev (and it will be deleted shortly afterwards).
+ */
+ void joinedRoom(Room* room, Room* prev);
+
+ /** A room has just been left
+ *
+ * If this room has been in Invite state (as in case of rejecting
+ * an invitation), the respective object will be passed in prev
+ * (and will be deleted shortly afterwards).
+ */
+ void leftRoom(Room* room, Room* prev);
+
+ /** The room object is about to be deleted */
+ void aboutToDeleteRoom(Room* room);
+
+ /** The room has just been created by createRoom or requestDirectChat
+ *
+ * This signal is not emitted in usual room state transitions,
+ * only as an outcome of room creation operations invoked by
+ * the client.
+ * \note requestDirectChat doesn't necessarily create a new chat;
+ * use directChatAvailable signal if you just need to obtain
+ * a direct chat room.
+ */
+ void createdRoom(Room* room);
+
+ /** The direct chat room is ready for using
+ * This signal is emitted upon any successful outcome from
+ * requestDirectChat.
+ */
+ void directChatAvailable(Room* directChat);
+
+ /** The list of direct chats has changed
+ * This signal is emitted every time when the mapping of users
+ * to direct chat rooms is changed (because of either local updates
+ * or a different list arrived from the server).
+ */
+ void directChatsListChanged();
+
+ void cacheStateChanged();
+
+ protected:
+ /**
+ * @brief Access the underlying ConnectionData class
+ */
+ const ConnectionData* connectionData() const;
+
+ /**
+ * @brief Find a (possibly new) Room object for the specified id
+ * Use this method whenever you need to find a Room object in
+ * the local list of rooms. Note that this does not interact with
+ * the server; in particular, does not automatically create rooms
+ * on the server.
+ * @return a pointer to a Room object with the specified id; nullptr
+ * if roomId is empty if roomFactory() failed to create a Room object.
+ */
+ Room* provideRoom(const QString& roomId, JoinState joinState);
+
+ /**
+ * Completes loading sync data.
+ */
+ void onSyncSuccess(SyncData &&data);
+
+ private:
+ class Private;
+ std::unique_ptr<Private> d;
+
+ /**
+ * A single entry for functions that need to check whether the
+ * homeserver is valid before running. May either execute connectFn
+ * synchronously or asynchronously (if tryResolve is true and
+ * a DNS lookup is initiated); in case of errors, emits resolveError
+ * if the homeserver URL is not valid and cannot be resolved from
+ * userId.
+ *
+ * @param userId - fully-qualified MXID to resolve HS from
+ * @param connectFn - a function to execute once the HS URL is good
+ */
+ void checkAndConnect(const QString& userId,
+ std::function<void()> connectFn);
+ void doConnectToServer(const QString& user, const QString& password,
+ const QString& initialDeviceName,
+ const QString& deviceId = {});
+
+ static room_factory_t roomFactory;
+ static user_factory_t userFactory;
+ };
+} // namespace QMatrixClient
+Q_DECLARE_METATYPE(QMatrixClient::Connection*)
diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp
new file mode 100644
index 00000000..4e9bc77e
--- /dev/null
+++ b/lib/connectiondata.cpp
@@ -0,0 +1,108 @@
+/******************************************************************************
+ * 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 "connectiondata.h"
+
+#include "networkaccessmanager.h"
+#include "logging.h"
+
+using namespace QMatrixClient;
+
+struct ConnectionData::Private
+{
+ explicit Private(const QUrl& url) : baseUrl(url) { }
+
+ QUrl baseUrl;
+ QByteArray accessToken;
+ QString lastEvent;
+ QString deviceId;
+
+ mutable unsigned int txnCounter = 0;
+ const qint64 id = QDateTime::currentMSecsSinceEpoch();
+};
+
+ConnectionData::ConnectionData(QUrl baseUrl)
+ : d(std::make_unique<Private>(baseUrl))
+{ }
+
+ConnectionData::~ConnectionData() = default;
+
+QByteArray ConnectionData::accessToken() const
+{
+ return d->accessToken;
+}
+
+QUrl ConnectionData::baseUrl() const
+{
+ return d->baseUrl;
+}
+
+QNetworkAccessManager* ConnectionData::nam() const
+{
+ return NetworkAccessManager::instance();
+}
+
+void ConnectionData::setBaseUrl(QUrl baseUrl)
+{
+ d->baseUrl = baseUrl;
+ qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl;
+}
+
+void ConnectionData::setToken(QByteArray token)
+{
+ d->accessToken = token;
+}
+
+void ConnectionData::setHost(QString host)
+{
+ d->baseUrl.setHost(host);
+ qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl;
+}
+
+void ConnectionData::setPort(int port)
+{
+ d->baseUrl.setPort(port);
+ qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl;
+}
+
+const QString& ConnectionData::deviceId() const
+{
+ return d->deviceId;
+}
+
+void ConnectionData::setDeviceId(const QString& deviceId)
+{
+ d->deviceId = deviceId;
+ qCDebug(MAIN) << "updated deviceId to" << d->deviceId;
+}
+
+QString ConnectionData::lastEvent() const
+{
+ return d->lastEvent;
+}
+
+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/lib/connectiondata.h b/lib/connectiondata.h
new file mode 100644
index 00000000..7a2f2e90
--- /dev/null
+++ b/lib/connectiondata.h
@@ -0,0 +1,55 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include <QtCore/QUrl>
+
+#include <memory>
+
+class QNetworkAccessManager;
+
+namespace QMatrixClient
+{
+ class ConnectionData
+ {
+ public:
+ explicit ConnectionData(QUrl baseUrl);
+ virtual ~ConnectionData();
+
+ QByteArray accessToken() const;
+ QUrl baseUrl() const;
+ const QString& deviceId() const;
+
+ QNetworkAccessManager* nam() const;
+ void setBaseUrl(QUrl baseUrl);
+ void setToken(QByteArray accessToken);
+ void setHost( QString host );
+ void setPort( int port );
+ void setDeviceId(const QString& deviceId);
+
+ QString lastEvent() const;
+ void setLastEvent( QString identifier );
+
+ QByteArray generateTxnId() const;
+
+ private:
+ struct Private;
+ std::unique_ptr<Private> d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/converters.h b/lib/converters.h
new file mode 100644
index 00000000..bba298e0
--- /dev/null
+++ b/lib/converters.h
@@ -0,0 +1,177 @@
+/******************************************************************************
+* Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+*
+* 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
+*/
+
+#pragma once
+
+#include <QtCore/QJsonObject>
+#include <QtCore/QJsonArray> // Includes <QtCore/QJsonValue>
+#include <QtCore/QDate>
+
+namespace QMatrixClient
+{
+ // This catches anything implicitly convertible to QJsonValue/Object/Array
+ inline QJsonValue toJson(const QJsonValue& val) { return val; }
+ inline QJsonObject toJson(const QJsonObject& o) { return o; }
+ inline QJsonArray toJson(const QJsonArray& arr) { return arr; }
+#ifdef _MSC_VER // MSVC gets lost and doesn't know which overload to use
+ inline QJsonValue toJson(const QString& s) { return s; }
+#endif
+
+ template <typename T>
+ inline QJsonArray toJson(const QVector<T>& vals)
+ {
+ QJsonArray ar;
+ for (const auto& v: vals)
+ ar.push_back(toJson(v));
+ return ar;
+ }
+
+ inline QJsonArray toJson(const QStringList& strings)
+ {
+ return QJsonArray::fromStringList(strings);
+ }
+
+ inline QJsonValue toJson(const QByteArray& bytes)
+ {
+ return QJsonValue(bytes.constData());
+ }
+
+ template <typename T>
+ inline QJsonObject toJson(const QHash<QString, T>& hashMap)
+ {
+ QJsonObject json;
+ for (auto it = hashMap.begin(); it != hashMap.end(); ++it)
+ json.insert(it.key(), toJson(it.value()));
+ return json;
+ }
+
+ template <typename T>
+ struct FromJson
+ {
+ T operator()(const QJsonValue& jv) const { return static_cast<T>(jv); }
+ };
+
+ template <typename T>
+ inline T fromJson(const QJsonValue& jv)
+ {
+ return FromJson<T>()(jv);
+ }
+
+ template <> struct FromJson<bool>
+ {
+ bool operator()(const QJsonValue& jv) const { return jv.toBool(); }
+ };
+
+ template <> struct FromJson<int>
+ {
+ int operator()(const QJsonValue& jv) const { return jv.toInt(); }
+ };
+
+ template <> struct FromJson<double>
+ {
+ double operator()(const QJsonValue& jv) const { return jv.toDouble(); }
+ };
+
+ template <> struct FromJson<qint64>
+ {
+ qint64 operator()(const QJsonValue& jv) const { return qint64(jv.toDouble()); }
+ };
+
+ template <> struct FromJson<QString>
+ {
+ QString operator()(const QJsonValue& jv) const { return jv.toString(); }
+ };
+
+ template <> struct FromJson<QDateTime>
+ {
+ QDateTime operator()(const QJsonValue& jv) const
+ {
+ return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC);
+ }
+ };
+
+ template <> struct FromJson<QDate>
+ {
+ QDate operator()(const QJsonValue& jv) const
+ {
+ return fromJson<QDateTime>(jv).date();
+ }
+ };
+
+ template <> struct FromJson<QJsonObject>
+ {
+ QJsonObject operator()(const QJsonValue& jv) const
+ {
+ return jv.toObject();
+ }
+ };
+
+ template <> struct FromJson<QJsonArray>
+ {
+ QJsonArray operator()(const QJsonValue& jv) const
+ {
+ return jv.toArray();
+ }
+ };
+
+ template <typename T> struct FromJson<QVector<T>>
+ {
+ QVector<T> operator()(const QJsonValue& jv) const
+ {
+ const auto jsonArray = jv.toArray();
+ QVector<T> vect; vect.resize(jsonArray.size());
+ std::transform(jsonArray.begin(), jsonArray.end(),
+ vect.begin(), FromJson<T>());
+ return vect;
+ }
+ };
+
+ template <typename T> struct FromJson<QList<T>>
+ {
+ QList<T> operator()(const QJsonValue& jv) const
+ {
+ const auto jsonArray = jv.toArray();
+ QList<T> sl; sl.reserve(jsonArray.size());
+ std::transform(jsonArray.begin(), jsonArray.end(),
+ std::back_inserter(sl), FromJson<T>());
+ return sl;
+ }
+ };
+
+ template <> struct FromJson<QStringList> : FromJson<QList<QString>> { };
+
+ template <> struct FromJson<QByteArray>
+ {
+ inline QByteArray operator()(const QJsonValue& jv) const
+ {
+ return fromJson<QString>(jv).toLatin1();
+ }
+ };
+
+ template <typename T> struct FromJson<QHash<QString, T>>
+ {
+ QHash<QString, T> operator()(const QJsonValue& jv) const
+ {
+ const auto json = jv.toObject();
+ QHash<QString, T> h; h.reserve(json.size());
+ for (auto it = json.begin(); it != json.end(); ++it)
+ h.insert(it.key(), fromJson<T>(it.value()));
+ return h;
+ }
+ };
+} // namespace QMatrixClient
diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h
new file mode 100644
index 00000000..f3ba27bb
--- /dev/null
+++ b/lib/events/accountdataevents.h
@@ -0,0 +1,78 @@
+#include <utility>
+
+/******************************************************************************
+ * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include "event.h"
+#include "eventcontent.h"
+
+namespace QMatrixClient
+{
+ static constexpr const char* FavouriteTag = "m.favourite";
+ static constexpr const char* LowPriorityTag = "m.lowpriority";
+
+ struct TagRecord
+ {
+ TagRecord (QString order = {}) : order(std::move(order)) { }
+ explicit TagRecord(const QJsonValue& jv)
+ : order(jv.toObject().value("order").toString())
+ { }
+
+ QString order;
+
+ bool operator==(const TagRecord& other) const
+ { return order == other.order; }
+ bool operator!=(const TagRecord& other) const
+ { return !operator==(other); }
+ };
+
+ inline QJsonValue toJson(const TagRecord& rec)
+ {
+ return QJsonObject {{ QStringLiteral("order"), rec.order }};
+ }
+
+ using TagsMap = QHash<QString, TagRecord>;
+
+#define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _EnumType, _ContentType, _ContentKey) \
+ class _Name : public Event \
+ { \
+ public: \
+ static constexpr const char* TypeId = _TypeId; \
+ static const char* typeId() { return TypeId; } \
+ explicit _Name(const QJsonObject& obj) \
+ : Event((_EnumType), obj) \
+ , _content(contentJson(), QStringLiteral(#_ContentKey)) \
+ { } \
+ template <typename... Ts> \
+ explicit _Name(Ts&&... contentArgs) \
+ : Event(_EnumType) \
+ , _content(QStringLiteral(#_ContentKey), \
+ std::forward<Ts>(contentArgs)...) \
+ { } \
+ const _ContentType& _ContentKey() const { return _content.value; } \
+ QJsonObject toJson() const { return _content.toJson(); } \
+ protected: \
+ EventContent::SimpleContent<_ContentType> _content; \
+ };
+
+ DEFINE_SIMPLE_EVENT(TagEvent, "m.tag", EventType::Tag, TagsMap, tags)
+ DEFINE_SIMPLE_EVENT(ReadMarkerEvent, "m.fully_read", EventType::ReadMarker,
+ QString, event_id)
+}
diff --git a/lib/events/directchatevent.cpp b/lib/events/directchatevent.cpp
new file mode 100644
index 00000000..7049d967
--- /dev/null
+++ b/lib/events/directchatevent.cpp
@@ -0,0 +1,36 @@
+/******************************************************************************
+ * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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 "directchatevent.h"
+
+#include "converters.h"
+
+using namespace QMatrixClient;
+
+DirectChatEvent::DirectChatEvent(const QJsonObject& obj)
+ : Event(Type::DirectChat, obj)
+{ }
+
+QMultiHash<QString, QString> DirectChatEvent::usersToDirectChats() const
+{
+ QMultiHash<QString, QString> result;
+ for (auto it = contentJson().begin(); it != contentJson().end(); ++it)
+ for (auto roomIdValue: it.value().toArray())
+ result.insert(it.key(), roomIdValue.toString());
+ return result;
+}
diff --git a/lib/events/directchatevent.h b/lib/events/directchatevent.h
new file mode 100644
index 00000000..2b0ad0a0
--- /dev/null
+++ b/lib/events/directchatevent.h
@@ -0,0 +1,34 @@
+/******************************************************************************
+ * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include "event.h"
+
+namespace QMatrixClient
+{
+ class DirectChatEvent : public Event
+ {
+ public:
+ explicit DirectChatEvent(const QJsonObject& obj);
+
+ QMultiHash<QString, QString> usersToDirectChats() const;
+
+ static constexpr const char * TypeId = "m.direct";
+ };
+}
diff --git a/lib/events/event.cpp b/lib/events/event.cpp
new file mode 100644
index 00000000..8ddf3945
--- /dev/null
+++ b/lib/events/event.cpp
@@ -0,0 +1,182 @@
+/******************************************************************************
+ * 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 "event.h"
+
+#include "roommessageevent.h"
+#include "simplestateevents.h"
+#include "roommemberevent.h"
+#include "roomavatarevent.h"
+#include "typingevent.h"
+#include "receiptevent.h"
+#include "accountdataevents.h"
+#include "directchatevent.h"
+#include "redactionevent.h"
+#include "logging.h"
+
+#include <QtCore/QJsonDocument>
+
+using namespace QMatrixClient;
+
+Event::Event(Type type, const QJsonObject& rep)
+ : _type(type), _originalJson(rep)
+{
+ if (!rep.contains("content") &&
+ !rep.value("unsigned").toObject().contains("redacted_because"))
+ {
+ qCWarning(EVENTS) << "Event without 'content' node";
+ qCWarning(EVENTS) << formatJson << rep;
+ }
+}
+
+Event::~Event() = default;
+
+QString Event::jsonType() const
+{
+ return originalJsonObject().value("type").toString();
+}
+
+QByteArray Event::originalJson() const
+{
+ return QJsonDocument(_originalJson).toJson();
+}
+
+QJsonObject Event::originalJsonObject() const
+{
+ return _originalJson;
+}
+
+const QJsonObject Event::contentJson() const
+{
+ return _originalJson["content"].toObject();
+}
+
+template <typename BaseEventT>
+inline BaseEventT* makeIfMatches(const QJsonObject&, const QString&)
+{
+ return nullptr;
+}
+
+template <typename BaseEventT, typename EventT, typename... EventTs>
+inline BaseEventT* makeIfMatches(const QJsonObject& o, const QString& selector)
+{
+ if (selector == EventT::TypeId)
+ return new EventT(o);
+
+ return makeIfMatches<BaseEventT, EventTs...>(o, selector);
+}
+
+template <>
+EventPtr _impl::doMakeEvent<Event>(const QJsonObject& obj)
+{
+ // Check more specific event types first
+ if (auto e = doMakeEvent<RoomEvent>(obj))
+ return EventPtr(move(e));
+
+ return EventPtr { makeIfMatches<Event,
+ TypingEvent, ReceiptEvent, TagEvent, ReadMarkerEvent, DirectChatEvent>(
+ obj, obj["type"].toString()) };
+}
+
+RoomEvent::RoomEvent(Event::Type type) : Event(type) { }
+
+RoomEvent::RoomEvent(Type type, const QJsonObject& rep)
+ : Event(type, rep)
+ , _id(rep["event_id"].toString())
+// , _roomId(rep["room_id"].toString())
+// , _senderId(rep["sender"].toString())
+// , _serverTimestamp(
+// QMatrixClient::fromJson<QDateTime>(rep["origin_server_ts"]))
+{
+// if (_id.isEmpty())
+// {
+// qCWarning(EVENTS) << "Can't find event_id in a room event";
+// qCWarning(EVENTS) << formatJson << rep;
+// }
+// if (!rep.contains("origin_server_ts"))
+// {
+// qCWarning(EVENTS) << "Can't find server timestamp in a room event";
+// qCWarning(EVENTS) << formatJson << rep;
+// }
+// if (_senderId.isEmpty())
+// {
+// qCWarning(EVENTS) << "Can't find sender in a room event";
+// qCWarning(EVENTS) << formatJson << rep;
+// }
+ auto unsignedData = rep["unsigned"].toObject();
+ auto redaction = unsignedData.value("redacted_because");
+ if (redaction.isObject())
+ {
+ _redactedBecause =
+ std::make_unique<RedactionEvent>(redaction.toObject());
+ return;
+ }
+
+ _txnId = unsignedData.value("transactionId").toString();
+ if (!_txnId.isEmpty())
+ qCDebug(EVENTS) << "Event transactionId:" << _txnId;
+}
+
+RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job
+
+QDateTime RoomEvent::timestamp() const
+{
+ return QMatrixClient::fromJson<QDateTime>(
+ originalJsonObject().value("origin_server_ts"));
+}
+
+QString RoomEvent::roomId() const
+{
+ return originalJsonObject().value("room_id").toString();
+}
+
+QString RoomEvent::senderId() const
+{
+ return originalJsonObject().value("sender").toString();
+}
+
+QString RoomEvent::redactionReason() const
+{
+ return isRedacted() ? _redactedBecause->reason() : QString{};
+}
+
+void RoomEvent::addId(const QString& id)
+{
+ Q_ASSERT(_id.isEmpty()); Q_ASSERT(!id.isEmpty());
+ _id = id;
+}
+
+template <>
+RoomEventPtr _impl::doMakeEvent(const QJsonObject& obj)
+{
+ return RoomEventPtr { makeIfMatches<RoomEvent,
+ RoomMessageEvent, RoomNameEvent, RoomAliasesEvent,
+ RoomCanonicalAliasEvent, RoomMemberEvent, RoomTopicEvent,
+ RoomAvatarEvent, EncryptionEvent, RedactionEvent>
+ (obj, obj["type"].toString()) };
+}
+
+StateEventBase::~StateEventBase() = default;
+
+bool StateEventBase::repeatsState() const
+{
+ auto contentJson = originalJsonObject().value("content");
+ auto prevContentJson = originalJsonObject().value("unsigned")
+ .toObject().value("prev_content");
+ return contentJson == prevContentJson;
+}
diff --git a/lib/events/event.h b/lib/events/event.h
new file mode 100644
index 00000000..eccfec41
--- /dev/null
+++ b/lib/events/event.h
@@ -0,0 +1,314 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include <QtCore/QString>
+#include <QtCore/QDateTime>
+#include <QtCore/QJsonObject>
+#include <QtCore/QJsonArray>
+
+#include "util.h"
+
+#include <memory>
+
+namespace QMatrixClient
+{
+ template <typename EventT>
+ using event_ptr_tt = std::unique_ptr<EventT>;
+
+ namespace _impl
+ {
+ template <typename EventT>
+ event_ptr_tt<EventT> doMakeEvent(const QJsonObject& obj);
+ }
+
+ class Event
+ {
+ Q_GADGET
+ public:
+ enum class Type : quint16
+ {
+ Unknown = 0,
+ Typing, Receipt, Tag, DirectChat, ReadMarker,
+ RoomEventBase = 0x1000,
+ RoomMessage = RoomEventBase + 1,
+ RoomEncryptedMessage, Redaction,
+ RoomStateEventBase = 0x1800,
+ RoomName = RoomStateEventBase + 1,
+ RoomAliases, RoomCanonicalAlias, RoomMember, RoomTopic,
+ RoomAvatar, RoomEncryption, RoomCreate, RoomJoinRules,
+ RoomPowerLevels,
+ Reserved = 0x2000
+ };
+
+ explicit Event(Type type) : _type(type) { }
+ Event(Type type, const QJsonObject& rep);
+ Event(const Event&) = delete;
+ virtual ~Event();
+
+ Type type() const { return _type; }
+ QString jsonType() const;
+ bool isStateEvent() const
+ {
+ return (quint16(_type) & 0x1800) == 0x1800;
+ }
+ QByteArray originalJson() const;
+ QJsonObject originalJsonObject() const;
+
+ // 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).
+
+ const QJsonObject contentJson() const;
+
+ private:
+ Type _type;
+ QJsonObject _originalJson;
+
+ REGISTER_ENUM(Type)
+ Q_PROPERTY(Type type READ type CONSTANT)
+ Q_PROPERTY(QJsonObject contentJson READ contentJson CONSTANT)
+ };
+ using EventType = Event::Type;
+ using EventPtr = event_ptr_tt<Event>;
+
+ /** Create an event with proper type from a JSON object
+ * Use this factory template to detect the type from the JSON object
+ * contents (the detected event type should derive from the template
+ * parameter type) and create an event object of that type.
+ */
+ template <typename EventT>
+ event_ptr_tt<EventT> makeEvent(const QJsonObject& obj)
+ {
+ auto e = _impl::doMakeEvent<EventT>(obj);
+ if (!e)
+ e = std::make_unique<EventT>(EventType::Unknown, obj);
+ return e;
+ }
+
+ namespace _impl
+ {
+ template <>
+ EventPtr doMakeEvent<Event>(const QJsonObject& obj);
+ }
+
+ /**
+ * \brief A vector of pointers to events with deserialisation capabilities
+ *
+ * This is a simple wrapper over a generic vector type that adds
+ * a convenience method to deserialise events from QJsonArray.
+ * \tparam EventT base type of all events in the vector
+ */
+ template <typename EventT>
+ class EventsBatch : public std::vector<event_ptr_tt<EventT>>
+ {
+ public:
+ /**
+ * \brief Deserialise events from an array
+ *
+ * Given the following JSON construct, creates events from
+ * the array stored at key "node":
+ * \code
+ * "container": {
+ * "node": [ { "event_id": "!evt1:srv.org", ... }, ... ]
+ * }
+ * \endcode
+ * \param container - the wrapping JSON object
+ * \param node - the key in container that holds the array of events
+ */
+ void fromJson(const QJsonObject& container, const QString& node)
+ {
+ const auto objs = container.value(node).toArray();
+ using size_type = typename std::vector<event_ptr_tt<EventT>>::size_type;
+ // The below line accommodates the difference in size types of
+ // STL and Qt containers.
+ this->reserve(static_cast<size_type>(objs.size()));
+ for (auto objValue: objs)
+ this->emplace_back(makeEvent<EventT>(objValue.toObject()));
+ }
+ };
+ using Events = EventsBatch<Event>;
+
+ class RedactionEvent;
+
+ /** This class corresponds to m.room.* events */
+ 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 redactionReason READ redactionReason)
+ Q_PROPERTY(bool isRedacted READ isRedacted)
+ Q_PROPERTY(QString transactionId READ transactionId)
+ public:
+ // RedactionEvent is an incomplete type here so we cannot inline
+ // constructors and destructors
+ explicit RoomEvent(Type type);
+ RoomEvent(Type type, const QJsonObject& rep);
+ ~RoomEvent();
+
+ QString id() const { return _id; }
+ QDateTime timestamp() const;
+ QString roomId() const;
+ QString senderId() const;
+ bool isRedacted() const { return bool(_redactedBecause); }
+ const RedactionEvent* redactedBecause() const
+ {
+ return _redactedBecause.get();
+ }
+ QString redactionReason() const;
+ 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);
+
+ private:
+ QString _id;
+// QString _roomId;
+// QString _senderId;
+// QDateTime _serverTimestamp;
+ event_ptr_tt<RedactionEvent> _redactedBecause;
+ QString _txnId;
+ };
+ using RoomEvents = EventsBatch<RoomEvent>;
+ using RoomEventPtr = event_ptr_tt<RoomEvent>;
+
+ namespace _impl
+ {
+ template <>
+ RoomEventPtr doMakeEvent<RoomEvent>(const QJsonObject& obj);
+ }
+
+ /**
+ * Conceptually similar to QStringView (but much more primitive), it's a
+ * simple abstraction over a pair of RoomEvents::const_iterator values
+ * referring to the beginning and the end of a range in a RoomEvents
+ * container.
+ */
+ struct RoomEventsRange
+ {
+ RoomEvents::iterator from;
+ RoomEvents::iterator to;
+
+ RoomEvents::size_type size() const
+ {
+ Q_ASSERT(std::distance(from, to) >= 0);
+ return RoomEvents::size_type(std::distance(from, to));
+ }
+ bool empty() const { return from == to; }
+ RoomEvents::const_iterator begin() const { return from; }
+ RoomEvents::const_iterator end() const { return to; }
+ RoomEvents::iterator begin() { return from; }
+ RoomEvents::iterator end() { return to; }
+ };
+
+ class StateEventBase: public RoomEvent
+ {
+ public:
+ explicit StateEventBase(Type type, const QJsonObject& obj)
+ : RoomEvent(obj.contains("state_key") ? type : Type::Unknown,
+ obj)
+ { }
+ explicit StateEventBase(Type type)
+ : RoomEvent(type)
+ { }
+ ~StateEventBase() override = 0;
+
+ virtual bool repeatsState() const;
+ };
+
+ template <typename ContentT>
+ struct Prev
+ {
+ template <typename... ContentParamTs>
+ explicit Prev(const QJsonObject& unsignedJson,
+ ContentParamTs&&... contentParams)
+ : senderId(unsignedJson.value("prev_sender").toString())
+ , content(unsignedJson.value("prev_content").toObject(),
+ std::forward<ContentParamTs>(contentParams)...)
+ { }
+
+ QString senderId;
+ ContentT content;
+ };
+
+ template <typename ContentT>
+ class StateEvent: public StateEventBase
+ {
+ public:
+ using content_type = ContentT;
+
+ template <typename... ContentParamTs>
+ explicit StateEvent(Type type, const QJsonObject& obj,
+ ContentParamTs&&... contentParams)
+ : StateEventBase(type, obj)
+ , _content(contentJson(),
+ std::forward<ContentParamTs>(contentParams)...)
+ {
+ auto unsignedData = obj.value("unsigned").toObject();
+ if (unsignedData.contains("prev_content"))
+ _prev = std::make_unique<Prev<ContentT>>(unsignedData,
+ std::forward<ContentParamTs>(contentParams)...);
+ }
+ template <typename... ContentParamTs>
+ explicit StateEvent(Type type, ContentParamTs&&... contentParams)
+ : StateEventBase(type)
+ , _content(std::forward<ContentParamTs>(contentParams)...)
+ { }
+
+ QJsonObject toJson() const { return _content.toJson(); }
+
+ const ContentT& content() const { return _content; }
+ /** @deprecated Use prevContent instead */
+ const ContentT* prev_content() const { return prevContent(); }
+ const ContentT* prevContent() const
+ { return _prev ? &_prev->content : nullptr; }
+ QString prevSenderId() const { return _prev ? _prev->senderId : ""; }
+
+ protected:
+ ContentT _content;
+ std::unique_ptr<Prev<ContentT>> _prev;
+ };
+} // namespace QMatrixClient
+Q_DECLARE_METATYPE(QMatrixClient::Event*)
+Q_DECLARE_METATYPE(QMatrixClient::RoomEvent*)
+Q_DECLARE_METATYPE(const QMatrixClient::Event*)
+Q_DECLARE_METATYPE(const QMatrixClient::RoomEvent*)
diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp
new file mode 100644
index 00000000..f5974b46
--- /dev/null
+++ b/lib/events/eventcontent.cpp
@@ -0,0 +1,85 @@
+/******************************************************************************
+ * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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 "eventcontent.h"
+
+#include <QtCore/QUrl>
+#include <QtCore/QMimeDatabase>
+
+using namespace QMatrixClient::EventContent;
+
+QJsonObject Base::toJson() const
+{
+ QJsonObject o;
+ fillJson(&o);
+ return o;
+}
+
+FileInfo::FileInfo(const QUrl& u, int payloadSize, const QMimeType& mimeType,
+ const QString& originalFilename)
+ : mimeType(mimeType), url(u), payloadSize(payloadSize)
+ , originalName(originalFilename)
+{ }
+
+FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson,
+ const QString& originalFilename)
+ : originalInfoJson(infoJson)
+ , mimeType(QMimeDatabase().mimeTypeForName(infoJson["mimetype"].toString()))
+ , url(u)
+ , payloadSize(infoJson["size"].toInt())
+ , originalName(originalFilename)
+{
+ if (!mimeType.isValid())
+ mimeType = QMimeDatabase().mimeTypeForData(QByteArray());
+}
+
+void FileInfo::fillInfoJson(QJsonObject* infoJson) const
+{
+ Q_ASSERT(infoJson);
+ infoJson->insert("size", payloadSize);
+ infoJson->insert("mimetype", mimeType.name());
+}
+
+ImageInfo::ImageInfo(const QUrl& u, int fileSize, QMimeType mimeType,
+ const QSize& imageSize)
+ : FileInfo(u, fileSize, mimeType), imageSize(imageSize)
+{ }
+
+ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson,
+ const QString& originalFilename)
+ : FileInfo(u, infoJson, originalFilename)
+ , imageSize(infoJson["w"].toInt(), infoJson["h"].toInt())
+{ }
+
+void ImageInfo::fillInfoJson(QJsonObject* infoJson) const
+{
+ FileInfo::fillInfoJson(infoJson);
+ infoJson->insert("w", imageSize.width());
+ infoJson->insert("h", imageSize.height());
+}
+
+Thumbnail::Thumbnail(const QJsonObject& infoJson)
+ : ImageInfo(infoJson["thumbnail_url"].toString(),
+ infoJson["thumbnail_info"].toObject())
+{ }
+
+void Thumbnail::fillInfoJson(QJsonObject* infoJson) const
+{
+ infoJson->insert("thumbnail_url", url.toString());
+ infoJson->insert("thumbnail_info", toInfoJson<ImageInfo>(*this));
+}
diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h
new file mode 100644
index 00000000..9d44aec0
--- /dev/null
+++ b/lib/events/eventcontent.h
@@ -0,0 +1,314 @@
+/******************************************************************************
+ * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+// This file contains generic event content definitions, applicable to room
+// message events as well as other events (e.g., avatars).
+
+#include "converters.h"
+
+#include <QtCore/QMimeType>
+#include <QtCore/QUrl>
+#include <QtCore/QSize>
+
+#include <functional>
+
+namespace QMatrixClient
+{
+ namespace EventContent
+ {
+ /**
+ * 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.
+ */
+ class Base
+ {
+ public:
+ explicit Base (const QJsonObject& o = {}) : originalJson(o) { }
+ virtual ~Base() = default;
+
+ QJsonObject toJson() const;
+
+ public:
+ QJsonObject originalJson;
+
+ protected:
+ virtual void fillJson(QJsonObject* o) const = 0;
+ };
+
+ template <typename T = QString>
+ class SimpleContent: public Base
+ {
+ public:
+ using value_type = T;
+
+ // The constructor is templated to enable perfect forwarding
+ template <typename TT>
+ SimpleContent(QString keyName, TT&& value)
+ : value(std::forward<TT>(value)), key(std::move(keyName))
+ { }
+ SimpleContent(const QJsonObject& json, QString keyName)
+ : Base(json)
+ , value(QMatrixClient::fromJson<T>(json[keyName]))
+ , key(std::move(keyName))
+ { }
+
+ public:
+ T value;
+
+ protected:
+ QString key;
+
+ private:
+ void fillJson(QJsonObject* json) const override
+ {
+ Q_ASSERT(json);
+ json->insert(key, QMatrixClient::toJson(value));
+ }
+ };
+
+ // 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. See doc comments to
+ // each type for the list of available attributes.
+
+ // A quick classes inheritance structure follows:
+ // FileInfo
+ // FileContent : UrlBasedContent<FileInfo, Thumbnail>
+ // AudioContent : UrlBasedContent<FileInfo, Duration>
+ // ImageInfo : FileInfo + imageSize attribute
+ // ImageContent : UrlBasedContent<ImageInfo, Thumbnail>
+ // VideoContent : UrlBasedContent<ImageInfo, Thumbnail, Duration>
+
+ /**
+ * A base/mixin class for structures representing an "info" object for
+ * some content types. These include most attachment types currently in
+ * the CS API spec.
+ *
+ * In order to use it in a content class, derive both from TypedBase
+ * (or Base) and from FileInfo (or its derivative, such as \p ImageInfo)
+ * and call fillInfoJson() to fill the "info" subobject. Make sure
+ * to pass an "info" part of JSON to FileInfo constructor, not the whole
+ * JSON content, as well as contents of "url" (or a similar key) and
+ * optionally "filename" node from the main JSON content. Assuming you
+ * don't do unusual things, you should use \p UrlBasedContent<> instead
+ * of doing multiple inheritance and overriding Base::fillJson() by hand.
+ *
+ * This class is not polymorphic.
+ */
+ class FileInfo
+ {
+ public:
+ explicit FileInfo(const QUrl& u, int payloadSize = -1,
+ const QMimeType& mimeType = {},
+ const QString& originalFilename = {});
+ FileInfo(const QUrl& u, const QJsonObject& infoJson,
+ const QString& originalFilename = {});
+
+ void fillInfoJson(QJsonObject* infoJson) const;
+
+ /**
+ * \brief Extract media id from the URL
+ *
+ * This can be used, e.g., to construct a QML-facing image://
+ * URI as follows:
+ * \code "image://provider/" + info.mediaId() \endcode
+ */
+ QString mediaId() const { return url.authority() + url.path(); }
+
+ public:
+ QJsonObject originalInfoJson;
+ QMimeType mimeType;
+ QUrl url;
+ int payloadSize;
+ QString originalName;
+ };
+
+ template <typename InfoT>
+ QJsonObject toInfoJson(const InfoT& info)
+ {
+ QJsonObject infoJson;
+ info.fillInfoJson(&infoJson);
+ return infoJson;
+ }
+
+ /**
+ * A content info class for image content types: image, thumbnail, video
+ */
+ class ImageInfo : public FileInfo
+ {
+ public:
+ explicit ImageInfo(const QUrl& u, int fileSize = -1,
+ QMimeType mimeType = {},
+ const QSize& imageSize = {});
+ ImageInfo(const QUrl& u, const QJsonObject& infoJson,
+ const QString& originalFilename = {});
+
+ void fillInfoJson(QJsonObject* infoJson) const;
+
+ public:
+ QSize imageSize;
+ };
+
+ /**
+ * An auxiliary class for an info type that carries a thumbnail
+ *
+ * This class saves/loads a thumbnail to/from "info" subobject of
+ * the JSON representation of event content; namely,
+ * "info/thumbnail_url" and "info/thumbnail_info" fields are used.
+ */
+ class Thumbnail : public ImageInfo
+ {
+ public:
+ Thumbnail(const QJsonObject& infoJson);
+ Thumbnail(const ImageInfo& info)
+ : ImageInfo(info)
+ { }
+
+ /**
+ * Writes thumbnail information to "thumbnail_info" subobject
+ * and thumbnail URL to "thumbnail_url" node inside "info".
+ */
+ void fillInfoJson(QJsonObject* infoJson) const;
+ };
+
+ class TypedBase: public Base
+ {
+ public:
+ explicit TypedBase(const QJsonObject& o = {}) : Base(o) { }
+ virtual QMimeType type() const = 0;
+ virtual const FileInfo* fileInfo() const { return nullptr; }
+ virtual const Thumbnail* thumbnailInfo() const { return nullptr; }
+ };
+
+ /**
+ * A base class for content types that have a URL and additional info
+ *
+ * Types that derive from this class template take "url" and,
+ * optionally, "filename" values from the top-level JSON object and
+ * the rest of information from the "info" subobject, as defined by
+ * the parameter type.
+ *
+ * \tparam InfoT base info class
+ */
+ template <class InfoT>
+ class UrlBasedContent : public TypedBase, public InfoT
+ {
+ public:
+ UrlBasedContent(QUrl url, InfoT&& info, QString filename = {})
+ : InfoT(url, std::forward<InfoT>(info), filename)
+ { }
+ explicit UrlBasedContent(const QJsonObject& json)
+ : TypedBase(json)
+ , InfoT(json["url"].toString(), json["info"].toObject(),
+ json["filename"].toString())
+ {
+ // A small hack to facilitate links creation in QML.
+ originalJson.insert("mediaId", InfoT::mediaId());
+ }
+
+ QMimeType type() const override { return InfoT::mimeType; }
+ const FileInfo* fileInfo() const override { return this; }
+
+ protected:
+ void fillJson(QJsonObject* json) const override
+ {
+ Q_ASSERT(json);
+ json->insert("url", InfoT::url.toString());
+ if (!InfoT::originalName.isEmpty())
+ json->insert("filename", InfoT::originalName);
+ json->insert("info", toInfoJson<InfoT>(*this));
+ }
+ };
+
+ template <typename InfoT>
+ class UrlWithThumbnailContent : public UrlBasedContent<InfoT>
+ {
+ public:
+ // TODO: POD constructor
+ explicit UrlWithThumbnailContent(const QJsonObject& json)
+ : UrlBasedContent<InfoT>(json)
+ , thumbnail(InfoT::originalInfoJson)
+ {
+ // Another small hack, to simplify making a thumbnail link
+ UrlBasedContent<InfoT>::originalJson.insert(
+ "thumbnailMediaId", thumbnail.mediaId());
+ }
+
+ const Thumbnail* thumbnailInfo() const override
+ { return &thumbnail; }
+
+ public:
+ Thumbnail thumbnail;
+
+ protected:
+ void fillJson(QJsonObject* json) const override
+ {
+ UrlBasedContent<InfoT>::fillJson(json);
+ auto infoJson = json->take("info").toObject();
+ thumbnail.fillInfoJson(&infoJson);
+ json->insert("info", infoJson);
+ }
+ };
+
+ /**
+ * 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 = UrlWithThumbnailContent<ImageInfo>;
+
+ /**
+ * 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 = UrlWithThumbnailContent<FileInfo>;
+ } // namespace EventContent
+} // namespace QMatrixClient
diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp
new file mode 100644
index 00000000..7555db82
--- /dev/null
+++ b/lib/events/receiptevent.cpp
@@ -0,0 +1,70 @@
+/******************************************************************************
+ * Copyright (C) 2016 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
+ */
+
+/*
+Example of a Receipt Event:
+{
+ "content": {
+ "$1435641916114394fHBLK:matrix.org": {
+ "m.read": {
+ "@rikj:jki.re": {
+ "ts": 1436451550453
+ }
+ }
+ }
+ },
+ "room_id": "!KpjVgQyZpzBwvMBsnT:matrix.org",
+ "type": "m.receipt"
+}
+*/
+
+#include "receiptevent.h"
+
+#include "converters.h"
+#include "logging.h"
+
+using namespace QMatrixClient;
+
+ReceiptEvent::ReceiptEvent(const QJsonObject& obj)
+ : Event(Type::Receipt, obj)
+{
+ Q_ASSERT(obj["type"].toString() == TypeId);
+
+ const QJsonObject contents = contentJson();
+ _eventsWithReceipts.reserve(contents.size());
+ for( auto eventIt = contents.begin(); eventIt != contents.end(); ++eventIt )
+ {
+ if (eventIt.key().isEmpty())
+ {
+ qCWarning(EPHEMERAL) << "ReceiptEvent has an empty event id, skipping";
+ qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << contents;
+ continue;
+ }
+ const QJsonObject reads = eventIt.value().toObject().value("m.read").toObject();
+ QVector<Receipt> receipts;
+ receipts.reserve(reads.size());
+ for( auto userIt = reads.begin(); userIt != reads.end(); ++userIt )
+ {
+ const QJsonObject user = userIt.value().toObject();
+ receipts.push_back({userIt.key(),
+ QMatrixClient::fromJson<QDateTime>(user["ts"])});
+ }
+ _eventsWithReceipts.push_back({eventIt.key(), std::move(receipts)});
+ }
+}
+
diff --git a/lib/events/receiptevent.h b/lib/events/receiptevent.h
new file mode 100644
index 00000000..5b99ae3f
--- /dev/null
+++ b/lib/events/receiptevent.h
@@ -0,0 +1,50 @@
+/******************************************************************************
+ * Copyright (C) 2016 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
+ */
+
+#pragma once
+
+#include "event.h"
+
+namespace QMatrixClient
+{
+ struct Receipt
+ {
+ QString userId;
+ QDateTime timestamp;
+ };
+ struct ReceiptsForEvent
+ {
+ QString evtId;
+ QVector<Receipt> receipts;
+ };
+ using EventsWithReceipts = QVector<ReceiptsForEvent>;
+
+ class ReceiptEvent: public Event
+ {
+ public:
+ explicit ReceiptEvent(const QJsonObject& obj);
+
+ EventsWithReceipts eventsWithReceipts() const
+ { return _eventsWithReceipts; }
+
+ static constexpr const char* const TypeId = "m.receipt";
+
+ private:
+ EventsWithReceipts _eventsWithReceipts;
+ };
+} // namespace QMatrixClient
diff --git a/lib/events/redactionevent.cpp b/lib/events/redactionevent.cpp
new file mode 100644
index 00000000..bf467718
--- /dev/null
+++ b/lib/events/redactionevent.cpp
@@ -0,0 +1 @@
+#include "redactionevent.h"
diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h
new file mode 100644
index 00000000..fa6902ab
--- /dev/null
+++ b/lib/events/redactionevent.h
@@ -0,0 +1,43 @@
+/******************************************************************************
+ * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include "event.h"
+
+namespace QMatrixClient
+{
+ class RedactionEvent : public RoomEvent
+ {
+ public:
+ static constexpr const char* const TypeId = "m.room.redaction";
+
+ RedactionEvent(const QJsonObject& obj)
+ : RoomEvent(Type::Redaction, obj)
+ , _redactedEvent(obj.value("redacts").toString())
+ , _reason(contentJson().value("reason").toString())
+ { }
+
+ const QString& redactedEvent() const { return _redactedEvent; }
+ const QString& reason() const { return _reason; }
+
+ private:
+ QString _redactedEvent;
+ QString _reason;
+ };
+} // namespace QMatrixClient
diff --git a/lib/events/roomavatarevent.cpp b/lib/events/roomavatarevent.cpp
new file mode 100644
index 00000000..7a5f82a1
--- /dev/null
+++ b/lib/events/roomavatarevent.cpp
@@ -0,0 +1,23 @@
+/******************************************************************************
+ * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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 "roomavatarevent.h"
+
+using namespace QMatrixClient;
+
+
diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h
new file mode 100644
index 00000000..ccfe8fbf
--- /dev/null
+++ b/lib/events/roomavatarevent.h
@@ -0,0 +1,43 @@
+/******************************************************************************
+ * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include "event.h"
+
+#include <utility>
+
+#include "eventcontent.h"
+
+namespace QMatrixClient
+{
+ class RoomAvatarEvent: public StateEvent<EventContent::ImageContent>
+ {
+ // It's a bit of an overkill to use a full-fledged ImageContent
+ // because in reality m.room.avatar usually only has a single URL,
+ // without a thumbnail. But The Spec says there be thumbnails, and
+ // we follow The Spec.
+ public:
+ explicit RoomAvatarEvent(const QJsonObject& obj)
+ : StateEvent(Type::RoomAvatar, obj)
+ { }
+
+ static constexpr const char* TypeId = "m.room.avatar";
+ };
+
+} // namespace QMatrixClient
diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp
new file mode 100644
index 00000000..76b003c2
--- /dev/null
+++ b/lib/events/roommemberevent.cpp
@@ -0,0 +1,69 @@
+/******************************************************************************
+ * 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 "roommemberevent.h"
+
+#include "logging.h"
+
+#include <array>
+
+using namespace QMatrixClient;
+
+static const std::array<QString, 5> membershipStrings = { {
+ QStringLiteral("invite"), QStringLiteral("join"),
+ QStringLiteral("knock"), QStringLiteral("leave"),
+ QStringLiteral("ban")
+} };
+
+namespace QMatrixClient
+{
+ template <>
+ struct FromJson<MembershipType>
+ {
+ MembershipType operator()(const QJsonValue& jv) const
+ {
+ const auto& membershipString = jv.toString();
+ for (auto it = membershipStrings.begin();
+ it != membershipStrings.end(); ++it)
+ if (membershipString == *it)
+ return MembershipType(it - membershipStrings.begin());
+
+ qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString;
+ return MembershipType::Undefined;
+ }
+ };
+}
+
+MemberEventContent::MemberEventContent(const QJsonObject& json)
+ : membership(fromJson<MembershipType>(json["membership"]))
+ , isDirect(json["is_direct"].toBool())
+ , displayName(json["displayname"].toString())
+ , avatarUrl(json["avatar_url"].toString())
+{ }
+
+void MemberEventContent::fillJson(QJsonObject* o) const
+{
+ Q_ASSERT(o);
+ Q_ASSERT_X(membership != MembershipType::Undefined, __FUNCTION__,
+ "The key 'membership' must be explicit in MemberEventContent");
+ if (membership != MembershipType::Undefined)
+ o->insert("membership", membershipStrings[membership]);
+ o->insert("displayname", displayName);
+ if (avatarUrl.isValid())
+ o->insert("avatar_url", avatarUrl.toString());
+}
diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h
new file mode 100644
index 00000000..89b970c9
--- /dev/null
+++ b/lib/events/roommemberevent.h
@@ -0,0 +1,78 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include "event.h"
+
+#include "eventcontent.h"
+
+#include <QtCore/QUrl>
+
+namespace QMatrixClient
+{
+ class MemberEventContent: public EventContent::Base
+ {
+ public:
+ enum MembershipType : size_t { Invite = 0, Join, Knock, Leave, Ban,
+ Undefined };
+
+ explicit MemberEventContent(MembershipType mt = MembershipType::Join)
+ : membership(mt)
+ { }
+ explicit MemberEventContent(const QJsonObject& json);
+
+ MembershipType membership;
+ bool isDirect = false;
+ QString displayName;
+ QUrl avatarUrl;
+
+ protected:
+ void fillJson(QJsonObject* o) const override;
+ };
+
+ using MembershipType = MemberEventContent::MembershipType;
+
+ class RoomMemberEvent: public StateEvent<MemberEventContent>
+ {
+ Q_GADGET
+ public:
+ static constexpr const char* TypeId = "m.room.member";
+
+ using MembershipType = MemberEventContent::MembershipType;
+
+ RoomMemberEvent(MemberEventContent&& c)
+ : StateEvent(Type::RoomMember, c)
+ { }
+ explicit RoomMemberEvent(const QJsonObject& obj)
+ : StateEvent(Type::RoomMember, obj)
+// , _userId(obj["state_key"].toString())
+ { }
+
+ MembershipType membership() const { return content().membership; }
+ QString userId() const
+ { return originalJsonObject().value("state_key").toString(); }
+ bool isDirect() const { return content().isDirect; }
+ QString displayName() const { return content().displayName; }
+ QUrl avatarUrl() const { return content().avatarUrl; }
+
+ private:
+// QString _userId;
+ REGISTER_ENUM(MembershipType)
+ };
+} // namespace QMatrixClient
diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp
new file mode 100644
index 00000000..dec0ca50
--- /dev/null
+++ b/lib/events/roommessageevent.cpp
@@ -0,0 +1,193 @@
+/******************************************************************************
+ * 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 "roommessageevent.h"
+
+#include "logging.h"
+
+#include <QtCore/QMimeDatabase>
+
+using namespace QMatrixClient;
+using namespace EventContent;
+
+using MsgType = RoomMessageEvent::MsgType;
+
+template <typename ContentT>
+TypedBase* make(const QJsonObject& json)
+{
+ return new ContentT(json);
+}
+
+struct MsgTypeDesc
+{
+ QString jsonType;
+ 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> }
+ };
+
+QString msgTypeToJson(MsgType enumType)
+{
+ auto it = std::find_if(msgTypes.begin(), msgTypes.end(),
+ [=](const MsgTypeDesc& mtd) { return mtd.enumType == enumType; });
+ if (it != msgTypes.end())
+ return it->jsonType;
+
+ 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;
+
+ return MsgType::Unknown;
+}
+
+RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
+ MsgType msgType, TypedBase* content)
+ : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content)
+{ }
+
+RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj)
+ : RoomEvent(Type::RoomMessage, obj), _content(nullptr)
+{
+ if (isRedacted())
+ return;
+ const QJsonObject content = contentJson();
+ if ( content.contains("msgtype") && content.contains("body") )
+ {
+ _plainBody = content["body"].toString();
+
+ _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
+ {
+ qCWarning(EVENTS) << "No body or msgtype in room message event";
+ qCWarning(EVENTS) << formatJson << obj;
+ }
+}
+
+RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const
+{
+ return jsonToMsgType(_msgtype);
+}
+
+QMimeType RoomMessageEvent::mimeType() const
+{
+ return _content ? _content->type() :
+ QMimeDatabase().mimeTypeForName("text/plain");
+}
+
+bool RoomMessageEvent::hasTextContent() const
+{
+ return content() &&
+ (msgtype() == MsgType::Text || msgtype() == MsgType::Emote ||
+ msgtype() == MsgType::Notice); // FIXME: Unbind from specific msgtypes
+}
+
+bool RoomMessageEvent::hasFileContent() const
+{
+ return content() && content()->fileInfo();
+}
+
+bool RoomMessageEvent::hasThumbnail() const
+{
+ return content() && content()->thumbnailInfo();
+}
+
+QJsonObject RoomMessageEvent::toJson() const
+{
+ QJsonObject obj = _content ? _content->toJson() : QJsonObject();
+ obj.insert("msgtype", msgTypeToJson(msgtype()));
+ obj.insert("body", plainBody());
+ return obj;
+}
+
+TextContent::TextContent(const QString& text, const QString& contentType)
+ : mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text)
+{ }
+
+TextContent::TextContent(const QJsonObject& json)
+{
+ QMimeDatabase db;
+
+ // Special-casing the custom matrix.org's (actually, Riot's) way
+ // of sending HTML messages.
+ if (json["format"].toString() == "org.matrix.custom.html")
+ {
+ mimeType = db.mimeTypeForName("text/html");
+ body = json["formatted_body"].toString();
+ } else {
+ // Falling back to plain text, as there's no standard way to describe
+ // rich text in messages.
+ mimeType = db.mimeTypeForName("text/plain");
+ body = json["body"].toString();
+ }
+}
+
+void TextContent::fillJson(QJsonObject* json) const
+{
+ Q_ASSERT(json);
+ json->insert("format", QStringLiteral("org.matrix.custom.html"));
+ json->insert("formatted_body", body);
+}
+
+LocationContent::LocationContent(const QString& geoUri, const ImageInfo& thumbnail)
+ : geoUri(geoUri), thumbnail(thumbnail)
+{ }
+
+LocationContent::LocationContent(const QJsonObject& json)
+ : TypedBase(json)
+ , geoUri(json["geo_uri"].toString())
+ , thumbnail(json["info"].toObject())
+{ }
+
+QMimeType LocationContent::type() const
+{
+ return QMimeDatabase().mimeTypeForData(geoUri.toLatin1());
+}
+
+void LocationContent::fillJson(QJsonObject* o) const
+{
+ Q_ASSERT(o);
+ o->insert("geo_uri", geoUri);
+ o->insert("info", toInfoJson(thumbnail));
+}
diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h
new file mode 100644
index 00000000..a55564ed
--- /dev/null
+++ b/lib/events/roommessageevent.h
@@ -0,0 +1,194 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include "event.h"
+
+#include "eventcontent.h"
+
+namespace QMatrixClient
+{
+ namespace MessageEventContent = EventContent; // Back-compatibility
+
+ /**
+ * The event class corresponding to m.room.message events
+ */
+ class RoomMessageEvent: public RoomEvent
+ {
+ Q_GADGET
+ Q_PROPERTY(QString msgType READ rawMsgtype CONSTANT)
+ Q_PROPERTY(QString plainBody READ plainBody CONSTANT)
+ Q_PROPERTY(QMimeType mimeType READ mimeType STORED false CONSTANT)
+ Q_PROPERTY(EventContent::TypedBase* content READ content CONSTANT)
+ public:
+ enum class MsgType
+ {
+ Text, Emote, Notice, Image, File, Location, Video, Audio, Unknown
+ };
+
+ RoomMessageEvent(const QString& plainBody,
+ const QString& jsonMsgType,
+ EventContent::TypedBase* content = nullptr)
+ : RoomEvent(Type::RoomMessage)
+ , _msgtype(jsonMsgType), _plainBody(plainBody), _content(content)
+ { }
+ explicit RoomMessageEvent(const QString& plainBody,
+ MsgType msgType = MsgType::Text,
+ EventContent::TypedBase* content = nullptr);
+ explicit RoomMessageEvent(const QJsonObject& obj);
+
+ MsgType msgtype() const;
+ QString rawMsgtype() const { return _msgtype; }
+ const QString& plainBody() const { return _plainBody; }
+ EventContent::TypedBase* content() const
+ { return _content.data(); }
+ QMimeType mimeType() const;
+ bool hasTextContent() const;
+ bool hasFileContent() const;
+ bool hasThumbnail() const;
+
+ QJsonObject toJson() const;
+
+ static constexpr const char* TypeId = "m.room.message";
+
+ private:
+ QString _msgtype;
+ QString _plainBody;
+ QScopedPointer<EventContent::TypedBase> _content;
+
+ REGISTER_ENUM(MsgType)
+ };
+ using MessageEventType = RoomMessageEvent::MsgType;
+
+ namespace EventContent
+ {
+ // Additional event content types
+
+ /**
+ * 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 TypedBase
+ {
+ public:
+ TextContent(const QString& text, const QString& contentType);
+ explicit TextContent(const QJsonObject& json);
+
+ QMimeType type() const override { return mimeType; }
+
+ QMimeType mimeType;
+ QString body;
+
+ protected:
+ void fillJson(QJsonObject* json) const override;
+ };
+
+ /**
+ * 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 TypedBase
+ {
+ public:
+ LocationContent(const QString& geoUri,
+ const ImageInfo& thumbnail);
+ explicit LocationContent(const QJsonObject& json);
+
+ QMimeType type() const override;
+
+ public:
+ QString geoUri;
+ Thumbnail thumbnail;
+
+ protected:
+ void fillJson(QJsonObject* o) const override;
+ };
+
+ /**
+ * A base class for info types that include duration: audio and video
+ */
+ template <typename ContentT>
+ class PlayableContent : public ContentT
+ {
+ public:
+ PlayableContent(const QJsonObject& json)
+ : ContentT(json)
+ , duration(ContentT::originalInfoJson["duration"].toInt())
+ { }
+
+ protected:
+ void fillJson(QJsonObject* json) const override
+ {
+ ContentT::fillJson(json);
+ auto infoJson = json->take("info").toObject();
+ infoJson.insert("duration", duration);
+ json->insert("info", infoJson);
+ }
+
+ public:
+ int duration;
+ };
+
+ /**
+ * 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 = PlayableContent<UrlWithThumbnailContent<ImageInfo>>;
+
+ /**
+ * 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 = PlayableContent<UrlBasedContent<FileInfo>>;
+ } // namespace EventContent
+} // namespace QMatrixClient
diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h
new file mode 100644
index 00000000..6b0cd51a
--- /dev/null
+++ b/lib/events/simplestateevents.h
@@ -0,0 +1,53 @@
+/******************************************************************************
+ * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include "event.h"
+#include "eventcontent.h"
+
+namespace QMatrixClient
+{
+#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _EnumType, _ContentType, _ContentKey) \
+ class _Name \
+ : public StateEvent<EventContent::SimpleContent<_ContentType>> \
+ { \
+ public: \
+ static constexpr const char* TypeId = _TypeId; \
+ explicit _Name(const QJsonObject& obj) \
+ : StateEvent(_EnumType, obj, QStringLiteral(#_ContentKey)) \
+ { } \
+ template <typename T> \
+ explicit _Name(T&& value) \
+ : StateEvent(_EnumType, QStringLiteral(#_ContentKey), \
+ std::forward<T>(value)) \
+ { } \
+ const _ContentType& _ContentKey() const { return content().value; } \
+ };
+
+ DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name",
+ Event::Type::RoomName, QString, name)
+ DEFINE_SIMPLE_STATE_EVENT(RoomAliasesEvent, "m.room.aliases",
+ Event::Type::RoomAliases, QStringList, aliases)
+ DEFINE_SIMPLE_STATE_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias",
+ Event::Type::RoomCanonicalAlias, QString, alias)
+ DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic",
+ Event::Type::RoomTopic, QString, topic)
+ DEFINE_SIMPLE_STATE_EVENT(EncryptionEvent, "m.room.encryption",
+ Event::Type::RoomEncryption, QString, algorithm)
+} // namespace QMatrixClient
diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp
new file mode 100644
index 00000000..a4d3bae4
--- /dev/null
+++ b/lib/events/typingevent.cpp
@@ -0,0 +1,32 @@
+/******************************************************************************
+ * 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 "typingevent.h"
+
+using namespace QMatrixClient;
+
+TypingEvent::TypingEvent(const QJsonObject& obj)
+ : Event(Type::Typing, obj)
+{
+ QJsonValue result;
+ result= contentJson()["user_ids"];
+ QJsonArray array = result.toArray();
+ for( const QJsonValue& user: array )
+ _users.push_back(user.toString());
+}
+
diff --git a/lib/events/typingevent.h b/lib/events/typingevent.h
new file mode 100644
index 00000000..8c9551a4
--- /dev/null
+++ b/lib/events/typingevent.h
@@ -0,0 +1,39 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include "event.h"
+
+#include <QtCore/QStringList>
+
+namespace QMatrixClient
+{
+ class TypingEvent: public Event
+ {
+ public:
+ static constexpr const char* const TypeId = "m.typing";
+
+ TypingEvent(const QJsonObject& obj);
+
+ QStringList users() const { return _users; }
+
+ private:
+ QStringList _users;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp
new file mode 100644
index 00000000..3cde7c50
--- /dev/null
+++ b/lib/jobs/basejob.cpp
@@ -0,0 +1,508 @@
+/******************************************************************************
+ * 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 "basejob.h"
+
+#include "connectiondata.h"
+
+#include <QtNetwork/QNetworkAccessManager>
+#include <QtNetwork/QNetworkRequest>
+#include <QtNetwork/QNetworkReply>
+#include <QtCore/QTimer>
+#include <QtCore/QRegularExpression>
+//#include <QtCore/QStringBuilder>
+
+#include <array>
+
+using namespace QMatrixClient;
+
+struct NetworkReplyDeleter : public QScopedPointerDeleteLater
+{
+ static inline void cleanup(QNetworkReply* reply)
+ {
+ if (reply && reply->isRunning())
+ reply->abort();
+ QScopedPointerDeleteLater::cleanup(reply);
+ }
+};
+
+class BaseJob::Private
+{
+ public:
+ // Using an idiom from clang-tidy:
+ // http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html
+ Private(HttpVerb v, QString endpoint, QUrlQuery q, Data&& data, bool nt)
+ : verb(v), apiEndpoint(std::move(endpoint))
+ , requestQuery(std::move(q)), requestData(std::move(data))
+ , needsToken(nt)
+ { }
+
+ void sendRequest();
+ const JobTimeoutConfig& getCurrentTimeoutConfig() const;
+
+ const ConnectionData* connection = nullptr;
+
+ // Contents for the network request
+ HttpVerb verb;
+ QString apiEndpoint;
+ QHash<QByteArray, QByteArray> requestHeaders;
+ QUrlQuery requestQuery;
+ Data requestData;
+ bool needsToken;
+
+ // There's no use of QMimeType here because we don't want to match
+ // content types against the known MIME type hierarchy; and at the same
+ // type QMimeType is of little help with MIME type globs (`text/*` etc.)
+ QByteArrayList expectedContentTypes;
+
+ QScopedPointer<QNetworkReply, NetworkReplyDeleter> reply;
+ Status status = Pending;
+
+ QTimer timer;
+ QTimer retryTimer;
+
+ QVector<JobTimeoutConfig> errorStrategy =
+ { { 90, 5 }, { 90, 10 }, { 120, 30 } };
+ int maxRetries = errorStrategy.size();
+ int retriesTaken = 0;
+
+ LoggingCategory logCat = JOBS;
+};
+
+BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, bool needsToken)
+ : BaseJob(verb, name, endpoint, Query { }, Data { }, needsToken)
+{ }
+
+BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
+ const Query& query, Data&& data, bool needsToken)
+ : d(new Private(verb, endpoint, query, std::move(data), needsToken))
+{
+ setObjectName(name);
+ setExpectedContentTypes({ "application/json" });
+ d->timer.setSingleShot(true);
+ connect (&d->timer, &QTimer::timeout, this, &BaseJob::timeout);
+ d->retryTimer.setSingleShot(true);
+ connect (&d->retryTimer, &QTimer::timeout, this, &BaseJob::sendRequest);
+}
+
+BaseJob::~BaseJob()
+{
+ stop();
+ qCDebug(d->logCat) << this << "destroyed";
+}
+
+const QString& BaseJob::apiEndpoint() const
+{
+ return d->apiEndpoint;
+}
+
+void BaseJob::setApiEndpoint(const QString& apiEndpoint)
+{
+ d->apiEndpoint = apiEndpoint;
+}
+
+const BaseJob::headers_t&BaseJob::requestHeaders() const
+{
+ return d->requestHeaders;
+}
+
+void BaseJob::setRequestHeader(const headers_t::key_type& headerName,
+ const headers_t::mapped_type& headerValue)
+{
+ d->requestHeaders[headerName] = headerValue;
+}
+
+void BaseJob::setRequestHeaders(const BaseJob::headers_t& headers)
+{
+ d->requestHeaders = headers;
+}
+
+const QUrlQuery& BaseJob::query() const
+{
+ return d->requestQuery;
+}
+
+void BaseJob::setRequestQuery(const QUrlQuery& query)
+{
+ d->requestQuery = query;
+}
+
+const BaseJob::Data& BaseJob::requestData() const
+{
+ return d->requestData;
+}
+
+void BaseJob::setRequestData(Data&& data)
+{
+ std::swap(d->requestData, data);
+}
+
+const QByteArrayList& BaseJob::expectedContentTypes() const
+{
+ return d->expectedContentTypes;
+}
+
+void BaseJob::addExpectedContentType(const QByteArray& contentType)
+{
+ d->expectedContentTypes << contentType;
+}
+
+void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes)
+{
+ d->expectedContentTypes = contentTypes;
+}
+
+QUrl BaseJob::makeRequestUrl(QUrl baseUrl,
+ const QString& path, const QUrlQuery& query)
+{
+ auto pathBase = baseUrl.path();
+ if (!pathBase.endsWith('/') && !path.startsWith('/'))
+ pathBase.push_back('/');
+
+ baseUrl.setPath( pathBase + path );
+ baseUrl.setQuery(query);
+ return baseUrl;
+}
+
+void BaseJob::Private::sendRequest()
+{
+ QNetworkRequest req
+ { makeRequestUrl(connection->baseUrl(), apiEndpoint, requestQuery) };
+ if (!requestHeaders.contains("Content-Type"))
+ req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ req.setRawHeader(QByteArray("Authorization"),
+ QByteArray("Bearer ") + connection->accessToken());
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
+ req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ req.setMaximumRedirectsAllowed(10);
+#endif
+ for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it)
+ req.setRawHeader(it.key(), it.value());
+ switch( verb )
+ {
+ case HttpVerb::Get:
+ reply.reset( connection->nam()->get(req) );
+ break;
+ case HttpVerb::Post:
+ reply.reset( connection->nam()->post(req, requestData.source()) );
+ break;
+ case HttpVerb::Put:
+ reply.reset( connection->nam()->put(req, requestData.source()) );
+ break;
+ case HttpVerb::Delete:
+ reply.reset( connection->nam()->deleteResource(req) );
+ break;
+ }
+}
+
+void BaseJob::beforeStart(const ConnectionData*)
+{ }
+
+void BaseJob::afterStart(const ConnectionData*, QNetworkReply*)
+{ }
+
+void BaseJob::beforeAbandon(QNetworkReply*)
+{ }
+
+void BaseJob::start(const ConnectionData* connData)
+{
+ d->connection = connData;
+ beforeStart(connData);
+ if (status().good())
+ sendRequest();
+ if (status().good())
+ afterStart(connData, d->reply.data());
+ if (!status().good())
+ QTimer::singleShot(0, this, &BaseJob::finishJob);
+}
+
+void BaseJob::sendRequest()
+{
+ emit aboutToStart();
+ d->retryTimer.stop(); // In case we were counting down at the moment
+ qCDebug(d->logCat) << this << "sending request to" << d->apiEndpoint;
+ if (!d->requestQuery.isEmpty())
+ qCDebug(d->logCat) << " query:" << d->requestQuery.toString();
+ d->sendRequest();
+ connect( d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply );
+ if (d->reply->isRunning())
+ {
+ connect( d->reply.data(), &QNetworkReply::uploadProgress,
+ this, &BaseJob::uploadProgress);
+ connect( d->reply.data(), &QNetworkReply::downloadProgress,
+ this, &BaseJob::downloadProgress);
+ d->timer.start(getCurrentTimeout());
+ qCDebug(d->logCat) << this << "request has been sent";
+ emit started();
+ }
+ else
+ qCWarning(d->logCat) << this << "request could not start";
+}
+
+void BaseJob::gotReply()
+{
+ setStatus(checkReply(d->reply.data()));
+ if (status().good())
+ setStatus(parseReply(d->reply.data()));
+ else
+ {
+ const auto body = d->reply->readAll();
+ if (!body.isEmpty())
+ {
+ qCDebug(d->logCat).noquote() << "Error body:" << body;
+ auto json = QJsonDocument::fromJson(body).object();
+ if (json.isEmpty())
+ setStatus(IncorrectRequestError, body);
+ else {
+ if (error() == TooManyRequestsError ||
+ json.value("errcode").toString() == "M_LIMIT_EXCEEDED")
+ {
+ QString msg = tr("Too many requests");
+ auto retryInterval = json.value("retry_after_ms").toInt(-1);
+ if (retryInterval != -1)
+ msg += tr(", next retry advised after %1 ms")
+ .arg(retryInterval);
+ else // We still have to figure some reasonable interval
+ retryInterval = getNextRetryInterval();
+
+ setStatus(TooManyRequestsError, msg);
+
+ // Shortcut to retry instead of executing finishJob()
+ stop();
+ qCWarning(d->logCat)
+ << this << "will retry in" << retryInterval;
+ d->retryTimer.start(retryInterval);
+ emit retryScheduled(d->retriesTaken, retryInterval);
+ return;
+ }
+ setStatus(IncorrectRequestError, json.value("error").toString());
+ }
+ }
+ }
+
+ finishJob();
+}
+
+bool checkContentType(const QByteArray& type, const QByteArrayList& patterns)
+{
+ if (patterns.isEmpty())
+ return true;
+
+ // ignore possible appendixes of the content type
+ const auto ctype = type.split(';').front();
+
+ for (const auto& pattern: patterns)
+ {
+ if (pattern.startsWith('*') || ctype == pattern) // Fast lane
+ return true;
+
+ auto patternParts = pattern.split('/');
+ Q_ASSERT_X(patternParts.size() <= 2, __FUNCTION__,
+ "BaseJob: Expected content type should have up to two"
+ " /-separated parts; violating pattern: " + pattern);
+
+ if (ctype.split('/').front() == patternParts.front() &&
+ patternParts.back() == "*")
+ return true; // Exact match already went on fast lane
+ }
+
+ return false;
+}
+
+BaseJob::Status BaseJob::checkReply(QNetworkReply* reply) const
+{
+ const auto httpCode =
+ reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ qCDebug(d->logCat).nospace().noquote() << this << " returned HTTP code "
+ << httpCode << ": "
+ << (reply->error() == QNetworkReply::NoError ?
+ "Success" : reply->errorString())
+ << " (URL: " << reply->url().toDisplayString() << ")";
+
+ if (httpCode == 429) // Qt doesn't know about it yet
+ return { TooManyRequestsError, tr("Too many requests") };
+
+ // Should we check httpCode instead? Maybe even use it in BaseJob::Status?
+ // That would make codes in logs slightly more readable.
+ switch( reply->error() )
+ {
+ case QNetworkReply::NoError:
+ if (checkContentType(reply->rawHeader("Content-Type"),
+ d->expectedContentTypes))
+ return NoError;
+ else // A warning in the logs might be more proper instead
+ return { IncorrectResponseError,
+ "Incorrect content type of the response" };
+
+ case QNetworkReply::AuthenticationRequiredError:
+ case QNetworkReply::ContentAccessDenied:
+ case QNetworkReply::ContentOperationNotPermittedError:
+ return { ContentAccessError, reply->errorString() };
+
+ case QNetworkReply::ProtocolInvalidOperationError:
+ case QNetworkReply::UnknownContentError:
+ return { IncorrectRequestError, reply->errorString() };
+
+ case QNetworkReply::ContentNotFoundError:
+ return { NotFoundError, reply->errorString() };
+
+ default:
+ return { NetworkError, reply->errorString() };
+ }
+}
+
+BaseJob::Status BaseJob::parseReply(QNetworkReply* reply)
+{
+ QJsonParseError error;
+ QJsonDocument json = QJsonDocument::fromJson(reply->readAll(), &error);
+ if( error.error == QJsonParseError::NoError )
+ return parseJson(json);
+ else
+ return { JsonParseError, error.errorString() };
+}
+
+BaseJob::Status BaseJob::parseJson(const QJsonDocument&)
+{
+ return Success;
+}
+
+void BaseJob::stop()
+{
+ d->timer.stop();
+ if (d->reply)
+ {
+ d->reply->disconnect(this); // Ignore whatever comes from the reply
+ if (d->reply->isRunning())
+ {
+ qCWarning(d->logCat) << this << "stopped without ready network reply";
+ d->reply->abort();
+ }
+ }
+ else
+ qCWarning(d->logCat) << this << "stopped with empty network reply";
+}
+
+void BaseJob::finishJob()
+{
+ stop();
+ if ((error() == NetworkError || error() == TimeoutError)
+ && d->retriesTaken < d->maxRetries)
+ {
+ // TODO: The whole retrying thing should be put to ConnectionManager
+ // otherwise independently retrying jobs make a bit of notification
+ // storm towards the UI.
+ const auto retryInterval =
+ error() == TimeoutError ? 0 : getNextRetryInterval();
+ ++d->retriesTaken;
+ qCWarning(d->logCat) << this << "will retry" << d->retriesTaken
+ << "in" << retryInterval/1000 << "s";
+ d->retryTimer.start(retryInterval);
+ emit retryScheduled(d->retriesTaken, retryInterval);
+ return;
+ }
+
+ // Notify those interested in any completion of the job (including killing)
+ emit finished(this);
+
+ emit result(this);
+ if (error())
+ emit failure(this);
+ else
+ emit success(this);
+
+ deleteLater();
+}
+
+const JobTimeoutConfig& BaseJob::Private::getCurrentTimeoutConfig() const
+{
+ return errorStrategy[std::min(retriesTaken, errorStrategy.size() - 1)];
+}
+
+BaseJob::duration_t BaseJob::getCurrentTimeout() const
+{
+ return d->getCurrentTimeoutConfig().jobTimeout * 1000;
+}
+
+BaseJob::duration_t BaseJob::getNextRetryInterval() const
+{
+ return d->getCurrentTimeoutConfig().nextRetryInterval * 1000;
+}
+
+BaseJob::duration_t BaseJob::millisToRetry() const
+{
+ return d->retryTimer.isActive() ? d->retryTimer.remainingTime() : 0;
+}
+
+int BaseJob::maxRetries() const
+{
+ return d->maxRetries;
+}
+
+void BaseJob::setMaxRetries(int newMaxRetries)
+{
+ d->maxRetries = newMaxRetries;
+}
+
+BaseJob::Status BaseJob::status() const
+{
+ return d->status;
+}
+
+int BaseJob::error() const
+{
+ return d->status.code;
+}
+
+QString BaseJob::errorString() const
+{
+ return d->status.message;
+}
+
+void BaseJob::setStatus(Status s)
+{
+ d->status = s;
+ if (!s.good())
+ qCWarning(d->logCat) << this << "status" << s;
+}
+
+void BaseJob::setStatus(int code, QString message)
+{
+ message.replace(d->connection->accessToken(), "(REDACTED)");
+ setStatus({ code, message });
+}
+
+void BaseJob::abandon()
+{
+ beforeAbandon(d->reply.data());
+ setStatus(Abandoned);
+ this->disconnect();
+ if (d->reply)
+ d->reply->disconnect(this);
+ deleteLater();
+}
+
+void BaseJob::timeout()
+{
+ setStatus( TimeoutError, "The job has timed out" );
+ finishJob();
+}
+
+void BaseJob::setLoggingCategory(LoggingCategory lcf)
+{
+ d->logCat = lcf;
+}
diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h
new file mode 100644
index 00000000..ed630a67
--- /dev/null
+++ b/lib/jobs/basejob.h
@@ -0,0 +1,303 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include "../logging.h"
+#include "requestdata.h"
+
+#include <QtCore/QObject>
+#include <QtCore/QUrlQuery>
+
+// Any job that parses the response will need the below two.
+#include <QtCore/QJsonDocument>
+#include <QtCore/QJsonObject>
+
+class QNetworkReply;
+class QSslError;
+
+namespace QMatrixClient
+{
+ class ConnectionData;
+
+ enum class HttpVerb { Get, Put, Post, Delete };
+
+ struct JobTimeoutConfig
+ {
+ int jobTimeout;
+ int nextRetryInterval;
+ };
+
+ class BaseJob: public QObject
+ {
+ Q_OBJECT
+ Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries)
+ public:
+ /* Just in case, the values are compatible with KJob
+ * (which BaseJob used to inherit from). */
+ enum StatusCode { NoError = 0 // To be compatible with Qt conventions
+ , Success = 0
+ , Pending = 1
+ , Abandoned = 50 //< A very brief period between abandoning and object deletion
+ , ErrorLevel = 100 //< Errors have codes starting from this
+ , NetworkError = 100
+ , JsonParseError
+ , TimeoutError
+ , ContentAccessError
+ , NotFoundError
+ , IncorrectRequestError
+ , IncorrectResponseError
+ , TooManyRequestsError
+ , UserDefinedError = 200
+ };
+
+ /**
+ * A simple wrapper around QUrlQuery that allows its creation from
+ * a list of string pairs
+ */
+ class Query : public QUrlQuery
+ {
+ public:
+ using QUrlQuery::QUrlQuery;
+ Query() = default;
+ Query(const std::initializer_list< QPair<QString, QString> >& l)
+ {
+ setQueryItems(l);
+ }
+ };
+
+ using Data = RequestData;
+
+ /**
+ * This structure stores the status of a server call job. The status consists
+ * of a code, that is described (but not delimited) by the respective enum,
+ * and a freeform message.
+ *
+ * To extend the list of error codes, define an (anonymous) enum
+ * along the lines of StatusCode, with additional values
+ * starting at UserDefinedError
+ */
+ class Status
+ {
+ public:
+ Status(StatusCode c) : code(c) { }
+ Status(int c, QString m) : code(c), message(std::move(m)) { }
+
+ bool good() const { return code < ErrorLevel; }
+ friend QDebug operator<<(QDebug dbg, const Status& s)
+ {
+ QDebug(dbg).noquote().nospace()
+ << s.code << ": " << s.message;
+ return dbg;
+ }
+
+ int code;
+ QString message;
+ };
+
+ using duration_t = int; // milliseconds
+
+ public:
+ BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
+ bool needsToken = true);
+ BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
+ const Query& query, Data&& data = {},
+ bool needsToken = true);
+
+ Status status() const;
+ int error() const;
+ virtual QString errorString() const;
+
+ int maxRetries() const;
+ void setMaxRetries(int newMaxRetries);
+
+ Q_INVOKABLE duration_t getCurrentTimeout() const;
+ Q_INVOKABLE duration_t getNextRetryInterval() const;
+ Q_INVOKABLE duration_t millisToRetry() const;
+
+ friend QDebug operator<<(QDebug dbg, const BaseJob* j)
+ {
+ return dbg << j->objectName();
+ }
+
+ public slots:
+ void start(const ConnectionData* connData);
+
+ /**
+ * Abandons the result of this job, arrived or unarrived.
+ *
+ * This aborts waiting for a reply from the server (if there was
+ * any pending) and deletes the job object. It is always done quietly
+ * (as opposed to KJob::kill() that can trigger emitting the result).
+ */
+ void abandon();
+
+ signals:
+ /** The job is about to send a network request */
+ void aboutToStart();
+
+ /** The job has sent a network request */
+ void started();
+
+ /**
+ * The previous network request has failed; the next attempt will
+ * be done in the specified time
+ * @param nextAttempt the 1-based number of attempt (will always be more than 1)
+ * @param inMilliseconds the interval after which the next attempt will be taken
+ */
+ void retryScheduled(int nextAttempt, int inMilliseconds);
+
+ /**
+ * Emitted when the job is finished, in any case. It is used to notify
+ * observers that the job is terminated and that progress can be hidden.
+ *
+ * This should not be emitted directly by subclasses;
+ * use finishJob() instead.
+ *
+ * In general, to be notified of a job's completion, client code
+ * should connect to success() and failure()
+ * rather than finished(), so that kill() is indeed quiet.
+ * However if you store a list of jobs and they might get killed
+ * silently, then you must connect to this instead of result(),
+ * to avoid dangling pointers in your list.
+ *
+ * @param job the job that emitted this signal
+ *
+ * @see success, failure
+ */
+ void finished(BaseJob* job);
+
+ /**
+ * Emitted when the job is finished (except when killed).
+ *
+ * Use error() to know if the job was finished with error.
+ *
+ * @param job the job that emitted this signal
+ *
+ * @see success, failure
+ */
+ void result(BaseJob* job);
+
+ /**
+ * Emitted together with result() but only if there's no error.
+ */
+ void success(BaseJob*);
+
+ /**
+ * Emitted together with result() if there's an error.
+ * Same as result(), this won't be emitted in case of kill().
+ */
+ void failure(BaseJob*);
+
+ void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
+
+ protected:
+ using headers_t = QHash<QByteArray, QByteArray>;
+
+ const QString& apiEndpoint() const;
+ void setApiEndpoint(const QString& apiEndpoint);
+ const headers_t& requestHeaders() const;
+ void setRequestHeader(const headers_t::key_type& headerName,
+ const headers_t::mapped_type& headerValue);
+ void setRequestHeaders(const headers_t& headers);
+ const QUrlQuery& query() const;
+ void setRequestQuery(const QUrlQuery& query);
+ const Data& requestData() const;
+ void setRequestData(Data&& data);
+ const QByteArrayList& expectedContentTypes() const;
+ void addExpectedContentType(const QByteArray& contentType);
+ void setExpectedContentTypes(const QByteArrayList& contentTypes);
+
+ /** Construct a URL out of baseUrl, path and query
+ * The function automatically adds '/' between baseUrl's path and
+ * \p path if necessary. The query component of \p baseUrl
+ * is ignored.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& path,
+ const QUrlQuery& query = {});
+
+ virtual void beforeStart(const ConnectionData* connData);
+ virtual void afterStart(const ConnectionData* connData,
+ QNetworkReply* reply);
+ virtual void beforeAbandon(QNetworkReply*);
+
+ /**
+ * Used by gotReply() to check the received reply for general
+ * issues such as network errors or access denial.
+ * Returning anything except NoError/Success prevents
+ * further parseReply()/parseJson() invocation.
+ *
+ * @param reply the reply received from the server
+ * @return the result of checking the reply
+ *
+ * @see gotReply
+ */
+ virtual Status checkReply(QNetworkReply* reply) const;
+
+ /**
+ * Processes the reply. By default, parses the reply into
+ * a QJsonDocument and calls parseJson() if it's a valid JSON.
+ *
+ * @param reply raw contents of a HTTP reply from the server (without headers)
+ *
+ * @see gotReply, parseJson
+ */
+ virtual Status parseReply(QNetworkReply* reply);
+
+ /**
+ * Processes the JSON document received from the Matrix server.
+ * By default returns succesful status without analysing the JSON.
+ *
+ * @param json valid JSON document received from the server
+ *
+ * @see parseReply
+ */
+ virtual Status parseJson(const QJsonDocument&);
+
+ void setStatus(Status s);
+ void setStatus(int code, QString message);
+
+ // Q_DECLARE_LOGGING_CATEGORY return different function types
+ // in different versions
+ using LoggingCategory = decltype(JOBS)*;
+ void setLoggingCategory(LoggingCategory lcf);
+
+ // Job objects should only be deleted via QObject::deleteLater
+ ~BaseJob() override;
+
+ protected slots:
+ void timeout();
+
+ private slots:
+ void sendRequest();
+ void gotReply();
+
+ private:
+ void stop();
+ void finishJob();
+
+ class Private;
+ QScopedPointer<Private> d;
+ };
+
+ inline bool isJobRunning(BaseJob* job)
+ {
+ return job && job->error() == BaseJob::Pending;
+ }
+} // namespace QMatrixClient
diff --git a/lib/jobs/checkauthmethods.cpp b/lib/jobs/checkauthmethods.cpp
new file mode 100644
index 00000000..117def89
--- /dev/null
+++ b/lib/jobs/checkauthmethods.cpp
@@ -0,0 +1,53 @@
+/******************************************************************************
+ * 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 "checkauthmethods.h"
+
+#include <QtCore/QJsonDocument>
+#include <QtCore/QJsonObject>
+
+using namespace QMatrixClient;
+
+class CheckAuthMethods::Private
+{
+ public:
+ QString session;
+};
+
+CheckAuthMethods::CheckAuthMethods()
+ : BaseJob(HttpVerb::Get, "CheckAuthMethods",
+ QStringLiteral("_matrix/client/r0/login"), Query(), Data(), false)
+ , d(new Private)
+{
+}
+
+CheckAuthMethods::~CheckAuthMethods()
+{
+ delete d;
+}
+
+QString CheckAuthMethods::session()
+{
+ return d->session;
+}
+
+BaseJob::Status CheckAuthMethods::parseJson(const QJsonDocument& data)
+{
+ // TODO
+ return { BaseJob::StatusCode::UserDefinedError, "Not implemented" };
+}
diff --git a/lib/jobs/checkauthmethods.h b/lib/jobs/checkauthmethods.h
new file mode 100644
index 00000000..647f3db6
--- /dev/null
+++ b/lib/jobs/checkauthmethods.h
@@ -0,0 +1,40 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include "basejob.h"
+
+namespace QMatrixClient
+{
+ class CheckAuthMethods : public BaseJob
+ {
+ public:
+ CheckAuthMethods();
+ virtual ~CheckAuthMethods();
+
+ QString session();
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ Private* d;
+ };
+}
diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp
new file mode 100644
index 00000000..6a3d8483
--- /dev/null
+++ b/lib/jobs/downloadfilejob.cpp
@@ -0,0 +1,120 @@
+#include "downloadfilejob.h"
+
+#include <QtNetwork/QNetworkReply>
+#include <QtCore/QFile>
+#include <QtCore/QTemporaryFile>
+
+using namespace QMatrixClient;
+
+class DownloadFileJob::Private
+{
+ public:
+ Private() : tempFile(new QTemporaryFile()) { }
+
+ explicit Private(const QString& localFilename)
+ : targetFile(new QFile(localFilename))
+ , tempFile(new QFile(targetFile->fileName() + ".qmcdownload"))
+ { }
+
+ QScopedPointer<QFile> targetFile;
+ QScopedPointer<QFile> tempFile;
+};
+
+QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri)
+{
+ return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1));
+}
+
+DownloadFileJob::DownloadFileJob(const QString& serverName,
+ const QString& mediaId,
+ const QString& localFilename)
+ : GetContentJob(serverName, mediaId)
+ , d(localFilename.isEmpty() ? new Private : new Private(localFilename))
+{
+ setObjectName("DownloadFileJob");
+}
+
+QString DownloadFileJob::targetFileName() const
+{
+ return (d->targetFile ? d->targetFile : d->tempFile)->fileName();
+}
+
+void DownloadFileJob::beforeStart(const ConnectionData*)
+{
+ if (d->targetFile && !d->targetFile->isReadable() &&
+ !d->targetFile->open(QIODevice::WriteOnly))
+ {
+ qCWarning(JOBS) << "Couldn't open the file"
+ << d->targetFile->fileName() << "for writing";
+ setStatus(FileError, "Could not open the target file for writing");
+ return;
+ }
+ if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly))
+ {
+ qCWarning(JOBS) << "Couldn't open the temporary file"
+ << d->tempFile->fileName() << "for writing";
+ setStatus(FileError, "Could not open the temporary download file");
+ return;
+ }
+ qCDebug(JOBS) << "Downloading to" << d->tempFile->fileName();
+}
+
+void DownloadFileJob::afterStart(const ConnectionData*, QNetworkReply* reply)
+{
+ connect(reply, &QNetworkReply::metaDataChanged, this, [this,reply] {
+ auto sizeHeader = reply->header(QNetworkRequest::ContentLengthHeader);
+ if (sizeHeader.isValid())
+ {
+ auto targetSize = sizeHeader.value<qint64>();
+ if (targetSize != -1)
+ if (!d->tempFile->resize(targetSize))
+ {
+ qCWarning(JOBS) << "Failed to allocate" << targetSize
+ << "bytes for" << d->tempFile->fileName();
+ setStatus(FileError,
+ "Could not reserve disk space for download");
+ }
+ }
+ });
+ connect(reply, &QIODevice::readyRead, this, [this,reply] {
+ auto bytes = reply->read(reply->bytesAvailable());
+ if (bytes.isEmpty())
+ {
+ qCWarning(JOBS)
+ << "Unexpected empty chunk when downloading from"
+ << reply->url() << "to" << d->tempFile->fileName();
+ } else {
+ d->tempFile->write(bytes);
+ }
+ });
+}
+
+void DownloadFileJob::beforeAbandon(QNetworkReply*)
+{
+ if (d->targetFile)
+ d->targetFile->remove();
+ d->tempFile->remove();
+}
+
+BaseJob::Status DownloadFileJob::parseReply(QNetworkReply*)
+{
+ if (d->targetFile)
+ {
+ d->targetFile->close();
+ if (!d->targetFile->remove())
+ {
+ qCWarning(JOBS) << "Failed to remove the target file placeholder";
+ return { FileError, "Couldn't finalise the download" };
+ }
+ if (!d->tempFile->rename(d->targetFile->fileName()))
+ {
+ qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName()
+ << "to" << d->targetFile->fileName();
+ return { FileError, "Couldn't finalise the download" };
+ }
+ }
+ else
+ d->tempFile->close();
+ qCDebug(JOBS) << "Saved a file as" << targetFileName();
+ return Success;
+}
diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h
new file mode 100644
index 00000000..1815a7c8
--- /dev/null
+++ b/lib/jobs/downloadfilejob.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "generated/content-repo.h"
+
+namespace QMatrixClient
+{
+ class DownloadFileJob : public GetContentJob
+ {
+ public:
+ enum { FileError = BaseJob::UserDefinedError + 1 };
+
+ using GetContentJob::makeRequestUrl;
+ static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri);
+
+ DownloadFileJob(const QString& serverName, const QString& mediaId,
+ const QString& localFilename = {});
+
+ QString targetFileName() const;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+
+ void beforeStart(const ConnectionData*) override;
+ void afterStart(const ConnectionData*,
+ QNetworkReply* reply) override;
+ void beforeAbandon(QNetworkReply*) override;
+ Status parseReply(QNetworkReply*) override;
+ };
+}
diff --git a/lib/jobs/generated/account-data.cpp b/lib/jobs/generated/account-data.cpp
new file mode 100644
index 00000000..35ee94c0
--- /dev/null
+++ b/lib/jobs/generated/account-data.cpp
@@ -0,0 +1,28 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "account-data.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content)
+ : BaseJob(HttpVerb::Put, "SetAccountDataJob",
+ basePath % "/user/" % userId % "/account_data/" % type)
+{
+ setRequestData(Data(content));
+}
+
+SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content)
+ : BaseJob(HttpVerb::Put, "SetAccountDataPerRoomJob",
+ basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type)
+{
+ setRequestData(Data(content));
+}
+
diff --git a/lib/jobs/generated/account-data.h b/lib/jobs/generated/account-data.h
new file mode 100644
index 00000000..69ad9fb4
--- /dev/null
+++ b/lib/jobs/generated/account-data.h
@@ -0,0 +1,27 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+#include <QtCore/QJsonObject>
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class SetAccountDataJob : public BaseJob
+ {
+ public:
+ explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {});
+ };
+
+ class SetAccountDataPerRoomJob : public BaseJob
+ {
+ public:
+ explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {});
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/administrative_contact.cpp b/lib/jobs/generated/administrative_contact.cpp
new file mode 100644
index 00000000..1af57941
--- /dev/null
+++ b/lib/jobs/generated/administrative_contact.cpp
@@ -0,0 +1,122 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "administrative_contact.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+GetAccount3PIDsJob::ThirdPartyIdentifier::operator QJsonObject() const
+{
+ QJsonObject o;
+ o.insert("medium", toJson(medium));
+ o.insert("address", toJson(address));
+
+ return o;
+}
+namespace QMatrixClient
+{
+ template <> struct FromJson<GetAccount3PIDsJob::ThirdPartyIdentifier>
+ {
+ GetAccount3PIDsJob::ThirdPartyIdentifier operator()(QJsonValue jv)
+ {
+ QJsonObject o = jv.toObject();
+ GetAccount3PIDsJob::ThirdPartyIdentifier result;
+ result.medium =
+ fromJson<QString>(o.value("medium"));
+ result.address =
+ fromJson<QString>(o.value("address"));
+
+ return result;
+ }
+ };
+} // namespace QMatrixClient
+
+class GetAccount3PIDsJob::Private
+{
+ public:
+ QVector<ThirdPartyIdentifier> threepids;
+};
+
+QUrl GetAccount3PIDsJob::makeRequestUrl(QUrl baseUrl)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/account/3pid");
+}
+
+GetAccount3PIDsJob::GetAccount3PIDsJob()
+ : BaseJob(HttpVerb::Get, "GetAccount3PIDsJob",
+ basePath % "/account/3pid")
+ , d(new Private)
+{
+}
+
+GetAccount3PIDsJob::~GetAccount3PIDsJob() = default;
+
+const QVector<GetAccount3PIDsJob::ThirdPartyIdentifier>& GetAccount3PIDsJob::threepids() const
+{
+ return d->threepids;
+}
+
+BaseJob::Status GetAccount3PIDsJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ d->threepids = fromJson<QVector<ThirdPartyIdentifier>>(json.value("threepids"));
+ return Success;
+}
+
+Post3PIDsJob::ThreePidCredentials::operator QJsonObject() const
+{
+ QJsonObject o;
+ o.insert("client_secret", toJson(clientSecret));
+ o.insert("id_server", toJson(idServer));
+ o.insert("sid", toJson(sid));
+
+ return o;
+}
+namespace QMatrixClient
+{
+ template <> struct FromJson<Post3PIDsJob::ThreePidCredentials>
+ {
+ Post3PIDsJob::ThreePidCredentials operator()(QJsonValue jv)
+ {
+ QJsonObject o = jv.toObject();
+ Post3PIDsJob::ThreePidCredentials result;
+ result.clientSecret =
+ fromJson<QString>(o.value("client_secret"));
+ result.idServer =
+ fromJson<QString>(o.value("id_server"));
+ result.sid =
+ fromJson<QString>(o.value("sid"));
+
+ return result;
+ }
+ };
+} // namespace QMatrixClient
+
+Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind)
+ : BaseJob(HttpVerb::Post, "Post3PIDsJob",
+ basePath % "/account/3pid")
+{
+ QJsonObject _data;
+ _data.insert("three_pid_creds", toJson(threePidCreds));
+ _data.insert("bind", toJson(bind));
+ setRequestData(_data);
+}
+
+QUrl RequestTokenTo3PIDJob::makeRequestUrl(QUrl baseUrl)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/account/3pid/email/requestToken");
+}
+
+RequestTokenTo3PIDJob::RequestTokenTo3PIDJob()
+ : BaseJob(HttpVerb::Post, "RequestTokenTo3PIDJob",
+ basePath % "/account/3pid/email/requestToken", false)
+{
+}
+
diff --git a/lib/jobs/generated/administrative_contact.h b/lib/jobs/generated/administrative_contact.h
new file mode 100644
index 00000000..c8429d39
--- /dev/null
+++ b/lib/jobs/generated/administrative_contact.h
@@ -0,0 +1,83 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+#include <QtCore/QVector>
+
+#include "converters.h"
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class GetAccount3PIDsJob : public BaseJob
+ {
+ public:
+ // Inner data structures
+
+ struct ThirdPartyIdentifier
+ {
+ QString medium;
+ QString address;
+
+ operator QJsonObject() const;
+ };
+
+ // End of inner data structures
+
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * GetAccount3PIDsJob. This function can be used when
+ * a URL for GetAccount3PIDsJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl);
+
+ explicit GetAccount3PIDsJob();
+ ~GetAccount3PIDsJob() override;
+
+ const QVector<ThirdPartyIdentifier>& threepids() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+
+ class Post3PIDsJob : public BaseJob
+ {
+ public:
+ // Inner data structures
+
+ struct ThreePidCredentials
+ {
+ QString clientSecret;
+ QString idServer;
+ QString sid;
+
+ operator QJsonObject() const;
+ };
+
+ // End of inner data structures
+
+ explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind = {});
+ };
+
+ class RequestTokenTo3PIDJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * RequestTokenTo3PIDJob. This function can be used when
+ * a URL for RequestTokenTo3PIDJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl);
+
+ explicit RequestTokenTo3PIDJob();
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/banning.cpp b/lib/jobs/generated/banning.cpp
new file mode 100644
index 00000000..f66b27b6
--- /dev/null
+++ b/lib/jobs/generated/banning.cpp
@@ -0,0 +1,34 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "banning.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+BanJob::BanJob(const QString& roomId, const QString& userId, const QString& reason)
+ : BaseJob(HttpVerb::Post, "BanJob",
+ basePath % "/rooms/" % roomId % "/ban")
+{
+ QJsonObject _data;
+ _data.insert("user_id", toJson(userId));
+ if (!reason.isEmpty())
+ _data.insert("reason", toJson(reason));
+ setRequestData(_data);
+}
+
+UnbanJob::UnbanJob(const QString& roomId, const QString& userId)
+ : BaseJob(HttpVerb::Post, "UnbanJob",
+ basePath % "/rooms/" % roomId % "/unban")
+{
+ QJsonObject _data;
+ _data.insert("user_id", toJson(userId));
+ setRequestData(_data);
+}
+
diff --git a/lib/jobs/generated/banning.h b/lib/jobs/generated/banning.h
new file mode 100644
index 00000000..2d6fbd9b
--- /dev/null
+++ b/lib/jobs/generated/banning.h
@@ -0,0 +1,26 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class BanJob : public BaseJob
+ {
+ public:
+ explicit BanJob(const QString& roomId, const QString& userId, const QString& reason = {});
+ };
+
+ class UnbanJob : public BaseJob
+ {
+ public:
+ explicit UnbanJob(const QString& roomId, const QString& userId);
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/content-repo.cpp b/lib/jobs/generated/content-repo.cpp
new file mode 100644
index 00000000..51011251
--- /dev/null
+++ b/lib/jobs/generated/content-repo.cpp
@@ -0,0 +1,254 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "content-repo.h"
+
+#include "converters.h"
+
+#include <QtNetwork/QNetworkReply>
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/media/r0");
+
+class UploadContentJob::Private
+{
+ public:
+ QString contentUri;
+};
+
+BaseJob::Query queryToUploadContent(const QString& filename)
+{
+ BaseJob::Query _q;
+ if (!filename.isEmpty())
+ _q.addQueryItem("filename", filename);
+ return _q;
+}
+
+UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, const QString& contentType)
+ : BaseJob(HttpVerb::Post, "UploadContentJob",
+ basePath % "/upload",
+ queryToUploadContent(filename))
+ , d(new Private)
+{
+ setRequestHeader("Content-Type", contentType.toLatin1());
+
+ setRequestData(Data(content));
+}
+
+UploadContentJob::~UploadContentJob() = default;
+
+const QString& UploadContentJob::contentUri() const
+{
+ return d->contentUri;
+}
+
+BaseJob::Status UploadContentJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ if (!json.contains("content_uri"))
+ return { JsonParseError,
+ "The key 'content_uri' not found in the response" };
+ d->contentUri = fromJson<QString>(json.value("content_uri"));
+ return Success;
+}
+
+class GetContentJob::Private
+{
+ public:
+ QString contentType;
+ QString contentDisposition;
+ QIODevice* content;
+};
+
+QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/download/" % serverName % "/" % mediaId);
+}
+
+GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId)
+ : BaseJob(HttpVerb::Get, "GetContentJob",
+ basePath % "/download/" % serverName % "/" % mediaId, false)
+ , d(new Private)
+{
+ setExpectedContentTypes({ "*/*" });
+}
+
+GetContentJob::~GetContentJob() = default;
+
+const QString& GetContentJob::contentType() const
+{
+ return d->contentType;
+}
+
+const QString& GetContentJob::contentDisposition() const
+{
+ return d->contentDisposition;
+}
+
+QIODevice* GetContentJob::content() const
+{
+ return d->content;
+}
+
+BaseJob::Status GetContentJob::parseReply(QNetworkReply* reply)
+{
+ d->contentType = reply->rawHeader("Content-Type");
+ d->contentDisposition = reply->rawHeader("Content-Disposition");
+ d->content = reply;
+ return Success;
+}
+
+class GetContentOverrideNameJob::Private
+{
+ public:
+ QString contentType;
+ QString contentDisposition;
+ QIODevice* content;
+};
+
+QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, const QString& fileName)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/download/" % serverName % "/" % mediaId % "/" % fileName);
+}
+
+GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, const QString& mediaId, const QString& fileName)
+ : BaseJob(HttpVerb::Get, "GetContentOverrideNameJob",
+ basePath % "/download/" % serverName % "/" % mediaId % "/" % fileName, false)
+ , d(new Private)
+{
+ setExpectedContentTypes({ "*/*" });
+}
+
+GetContentOverrideNameJob::~GetContentOverrideNameJob() = default;
+
+const QString& GetContentOverrideNameJob::contentType() const
+{
+ return d->contentType;
+}
+
+const QString& GetContentOverrideNameJob::contentDisposition() const
+{
+ return d->contentDisposition;
+}
+
+QIODevice* GetContentOverrideNameJob::content() const
+{
+ return d->content;
+}
+
+BaseJob::Status GetContentOverrideNameJob::parseReply(QNetworkReply* reply)
+{
+ d->contentType = reply->rawHeader("Content-Type");
+ d->contentDisposition = reply->rawHeader("Content-Disposition");
+ d->content = reply;
+ return Success;
+}
+
+class GetContentThumbnailJob::Private
+{
+ public:
+ QString contentType;
+ QIODevice* content;
+};
+
+BaseJob::Query queryToGetContentThumbnail(int width, int height, const QString& method)
+{
+ BaseJob::Query _q;
+ _q.addQueryItem("width", QString("%1").arg(width));
+ _q.addQueryItem("height", QString("%1").arg(height));
+ if (!method.isEmpty())
+ _q.addQueryItem("method", method);
+ return _q;
+}
+
+QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, int width, int height, const QString& method)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/thumbnail/" % serverName % "/" % mediaId,
+ queryToGetContentThumbnail(width, height, method));
+}
+
+GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, const QString& mediaId, int width, int height, const QString& method)
+ : BaseJob(HttpVerb::Get, "GetContentThumbnailJob",
+ basePath % "/thumbnail/" % serverName % "/" % mediaId,
+ queryToGetContentThumbnail(width, height, method),
+ {}, false)
+ , d(new Private)
+{
+ setExpectedContentTypes({ "image/jpeg", "image/png" });
+}
+
+GetContentThumbnailJob::~GetContentThumbnailJob() = default;
+
+const QString& GetContentThumbnailJob::contentType() const
+{
+ return d->contentType;
+}
+
+QIODevice* GetContentThumbnailJob::content() const
+{
+ return d->content;
+}
+
+BaseJob::Status GetContentThumbnailJob::parseReply(QNetworkReply* reply)
+{
+ d->contentType = reply->rawHeader("Content-Type");
+ d->content = reply;
+ return Success;
+}
+
+class GetUrlPreviewJob::Private
+{
+ public:
+ double matrixImageSize;
+ QString ogImage;
+};
+
+BaseJob::Query queryToGetUrlPreview(const QString& url, double ts)
+{
+ BaseJob::Query _q;
+ _q.addQueryItem("url", url);
+ _q.addQueryItem("ts", QString("%1").arg(ts));
+ return _q;
+}
+
+QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QString& url, double ts)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/preview_url",
+ queryToGetUrlPreview(url, ts));
+}
+
+GetUrlPreviewJob::GetUrlPreviewJob(const QString& url, double ts)
+ : BaseJob(HttpVerb::Get, "GetUrlPreviewJob",
+ basePath % "/preview_url",
+ queryToGetUrlPreview(url, ts))
+ , d(new Private)
+{
+}
+
+GetUrlPreviewJob::~GetUrlPreviewJob() = default;
+
+double GetUrlPreviewJob::matrixImageSize() const
+{
+ return d->matrixImageSize;
+}
+
+const QString& GetUrlPreviewJob::ogImage() const
+{
+ return d->ogImage;
+}
+
+BaseJob::Status GetUrlPreviewJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ d->matrixImageSize = fromJson<double>(json.value("matrix:image:size"));
+ d->ogImage = fromJson<QString>(json.value("og:image"));
+ return Success;
+}
+
diff --git a/lib/jobs/generated/content-repo.h b/lib/jobs/generated/content-repo.h
new file mode 100644
index 00000000..b4ea562f
--- /dev/null
+++ b/lib/jobs/generated/content-repo.h
@@ -0,0 +1,129 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+#include <QtCore/QIODevice>
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class UploadContentJob : public BaseJob
+ {
+ public:
+ explicit UploadContentJob(QIODevice* content, const QString& filename = {}, const QString& contentType = {});
+ ~UploadContentJob() override;
+
+ const QString& contentUri() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+
+ class GetContentJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * GetContentJob. This function can be used when
+ * a URL for GetContentJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId);
+
+ explicit GetContentJob(const QString& serverName, const QString& mediaId);
+ ~GetContentJob() override;
+
+ const QString& contentType() const;
+ const QString& contentDisposition() const;
+ QIODevice* content() const;
+
+ protected:
+ Status parseReply(QNetworkReply* reply) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+
+ class GetContentOverrideNameJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * GetContentOverrideNameJob. This function can be used when
+ * a URL for GetContentOverrideNameJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, const QString& fileName);
+
+ explicit GetContentOverrideNameJob(const QString& serverName, const QString& mediaId, const QString& fileName);
+ ~GetContentOverrideNameJob() override;
+
+ const QString& contentType() const;
+ const QString& contentDisposition() const;
+ QIODevice* content() const;
+
+ protected:
+ Status parseReply(QNetworkReply* reply) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+
+ class GetContentThumbnailJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * GetContentThumbnailJob. This function can be used when
+ * a URL for GetContentThumbnailJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, int width = {}, int height = {}, const QString& method = {});
+
+ explicit GetContentThumbnailJob(const QString& serverName, const QString& mediaId, int width = {}, int height = {}, const QString& method = {});
+ ~GetContentThumbnailJob() override;
+
+ const QString& contentType() const;
+ QIODevice* content() const;
+
+ protected:
+ Status parseReply(QNetworkReply* reply) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+
+ class GetUrlPreviewJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * GetUrlPreviewJob. This function can be used when
+ * a URL for GetUrlPreviewJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& url, double ts = {});
+
+ explicit GetUrlPreviewJob(const QString& url, double ts = {});
+ ~GetUrlPreviewJob() override;
+
+ double matrixImageSize() const;
+ const QString& ogImage() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/create_room.cpp b/lib/jobs/generated/create_room.cpp
new file mode 100644
index 00000000..de7807b5
--- /dev/null
+++ b/lib/jobs/generated/create_room.cpp
@@ -0,0 +1,115 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "create_room.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+CreateRoomJob::Invite3pid::operator QJsonObject() const
+{
+ QJsonObject o;
+ o.insert("id_server", toJson(idServer));
+ o.insert("medium", toJson(medium));
+ o.insert("address", toJson(address));
+
+ return o;
+}
+namespace QMatrixClient
+{
+ template <> struct FromJson<CreateRoomJob::Invite3pid>
+ {
+ CreateRoomJob::Invite3pid operator()(QJsonValue jv)
+ {
+ QJsonObject o = jv.toObject();
+ CreateRoomJob::Invite3pid result;
+ result.idServer =
+ fromJson<QString>(o.value("id_server"));
+ result.medium =
+ fromJson<QString>(o.value("medium"));
+ result.address =
+ fromJson<QString>(o.value("address"));
+
+ return result;
+ }
+ };
+} // namespace QMatrixClient
+
+CreateRoomJob::StateEvent::operator QJsonObject() const
+{
+ QJsonObject o;
+ o.insert("type", toJson(type));
+ o.insert("state_key", toJson(stateKey));
+ o.insert("content", toJson(content));
+
+ return o;
+}
+namespace QMatrixClient
+{
+ template <> struct FromJson<CreateRoomJob::StateEvent>
+ {
+ CreateRoomJob::StateEvent operator()(QJsonValue jv)
+ {
+ QJsonObject o = jv.toObject();
+ CreateRoomJob::StateEvent result;
+ result.type =
+ fromJson<QString>(o.value("type"));
+ result.stateKey =
+ fromJson<QString>(o.value("state_key"));
+ result.content =
+ fromJson<QJsonObject>(o.value("content"));
+
+ return result;
+ }
+ };
+} // namespace QMatrixClient
+
+class CreateRoomJob::Private
+{
+ public:
+ QString roomId;
+};
+
+CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QVector<QString>& invite, const QVector<Invite3pid>& invite3pid, const QJsonObject& creationContent, const QVector<StateEvent>& initialState, const QString& preset, bool isDirect, bool guestCanJoin)
+ : BaseJob(HttpVerb::Post, "CreateRoomJob",
+ basePath % "/createRoom")
+ , d(new Private)
+{
+ QJsonObject _data;
+ if (!visibility.isEmpty())
+ _data.insert("visibility", toJson(visibility));
+ if (!roomAliasName.isEmpty())
+ _data.insert("room_alias_name", toJson(roomAliasName));
+ if (!name.isEmpty())
+ _data.insert("name", toJson(name));
+ if (!topic.isEmpty())
+ _data.insert("topic", toJson(topic));
+ _data.insert("invite", toJson(invite));
+ _data.insert("invite_3pid", toJson(invite3pid));
+ _data.insert("creation_content", toJson(creationContent));
+ _data.insert("initial_state", toJson(initialState));
+ if (!preset.isEmpty())
+ _data.insert("preset", toJson(preset));
+ _data.insert("is_direct", toJson(isDirect));
+ _data.insert("guest_can_join", toJson(guestCanJoin));
+ setRequestData(_data);
+}
+
+CreateRoomJob::~CreateRoomJob() = default;
+
+const QString& CreateRoomJob::roomId() const
+{
+ return d->roomId;
+}
+
+BaseJob::Status CreateRoomJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ d->roomId = fromJson<QString>(json.value("room_id"));
+ return Success;
+}
+
diff --git a/lib/jobs/generated/create_room.h b/lib/jobs/generated/create_room.h
new file mode 100644
index 00000000..b479615a
--- /dev/null
+++ b/lib/jobs/generated/create_room.h
@@ -0,0 +1,55 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+#include <QtCore/QJsonObject>
+#include <QtCore/QVector>
+
+#include "converters.h"
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class CreateRoomJob : public BaseJob
+ {
+ public:
+ // Inner data structures
+
+ struct Invite3pid
+ {
+ QString idServer;
+ QString medium;
+ QString address;
+
+ operator QJsonObject() const;
+ };
+
+ struct StateEvent
+ {
+ QString type;
+ QString stateKey;
+ QJsonObject content;
+
+ operator QJsonObject() const;
+ };
+
+ // End of inner data structures
+
+ explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QVector<QString>& invite = {}, const QVector<Invite3pid>& invite3pid = {}, const QJsonObject& creationContent = {}, const QVector<StateEvent>& initialState = {}, const QString& preset = {}, bool isDirect = {}, bool guestCanJoin = {});
+ ~CreateRoomJob() override;
+
+ const QString& roomId() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/directory.cpp b/lib/jobs/generated/directory.cpp
new file mode 100644
index 00000000..9428dcee
--- /dev/null
+++ b/lib/jobs/generated/directory.cpp
@@ -0,0 +1,76 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "directory.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0/directory");
+
+SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId)
+ : BaseJob(HttpVerb::Put, "SetRoomAliasJob",
+ basePath % "/room/" % roomAlias)
+{
+ QJsonObject _data;
+ if (!roomId.isEmpty())
+ _data.insert("room_id", toJson(roomId));
+ setRequestData(_data);
+}
+
+class GetRoomIdByAliasJob::Private
+{
+ public:
+ QString roomId;
+ QVector<QString> servers;
+};
+
+QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/room/" % roomAlias);
+}
+
+GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias)
+ : BaseJob(HttpVerb::Get, "GetRoomIdByAliasJob",
+ basePath % "/room/" % roomAlias, false)
+ , d(new Private)
+{
+}
+
+GetRoomIdByAliasJob::~GetRoomIdByAliasJob() = default;
+
+const QString& GetRoomIdByAliasJob::roomId() const
+{
+ return d->roomId;
+}
+
+const QVector<QString>& GetRoomIdByAliasJob::servers() const
+{
+ return d->servers;
+}
+
+BaseJob::Status GetRoomIdByAliasJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ d->roomId = fromJson<QString>(json.value("room_id"));
+ d->servers = fromJson<QVector<QString>>(json.value("servers"));
+ return Success;
+}
+
+QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/room/" % roomAlias);
+}
+
+DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias)
+ : BaseJob(HttpVerb::Delete, "DeleteRoomAliasJob",
+ basePath % "/room/" % roomAlias)
+{
+}
+
diff --git a/lib/jobs/generated/directory.h b/lib/jobs/generated/directory.h
new file mode 100644
index 00000000..87591240
--- /dev/null
+++ b/lib/jobs/generated/directory.h
@@ -0,0 +1,58 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+#include <QtCore/QVector>
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class SetRoomAliasJob : public BaseJob
+ {
+ public:
+ explicit SetRoomAliasJob(const QString& roomAlias, const QString& roomId = {});
+ };
+
+ class GetRoomIdByAliasJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * GetRoomIdByAliasJob. This function can be used when
+ * a URL for GetRoomIdByAliasJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias);
+
+ explicit GetRoomIdByAliasJob(const QString& roomAlias);
+ ~GetRoomIdByAliasJob() override;
+
+ const QString& roomId() const;
+ const QVector<QString>& servers() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+
+ class DeleteRoomAliasJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * DeleteRoomAliasJob. This function can be used when
+ * a URL for DeleteRoomAliasJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias);
+
+ explicit DeleteRoomAliasJob(const QString& roomAlias);
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/inviting.cpp b/lib/jobs/generated/inviting.cpp
new file mode 100644
index 00000000..d2ee2107
--- /dev/null
+++ b/lib/jobs/generated/inviting.cpp
@@ -0,0 +1,23 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "inviting.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId)
+ : BaseJob(HttpVerb::Post, "InviteUserJob",
+ basePath % "/rooms/" % roomId % "/invite")
+{
+ QJsonObject _data;
+ _data.insert("user_id", toJson(userId));
+ setRequestData(_data);
+}
+
diff --git a/lib/jobs/generated/inviting.h b/lib/jobs/generated/inviting.h
new file mode 100644
index 00000000..eaa884df
--- /dev/null
+++ b/lib/jobs/generated/inviting.h
@@ -0,0 +1,20 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class InviteUserJob : public BaseJob
+ {
+ public:
+ explicit InviteUserJob(const QString& roomId, const QString& userId);
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/kicking.cpp b/lib/jobs/generated/kicking.cpp
new file mode 100644
index 00000000..bf2490b7
--- /dev/null
+++ b/lib/jobs/generated/kicking.cpp
@@ -0,0 +1,25 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "kicking.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+KickJob::KickJob(const QString& roomId, const QString& userId, const QString& reason)
+ : BaseJob(HttpVerb::Post, "KickJob",
+ basePath % "/rooms/" % roomId % "/kick")
+{
+ QJsonObject _data;
+ _data.insert("user_id", toJson(userId));
+ if (!reason.isEmpty())
+ _data.insert("reason", toJson(reason));
+ setRequestData(_data);
+}
+
diff --git a/lib/jobs/generated/kicking.h b/lib/jobs/generated/kicking.h
new file mode 100644
index 00000000..3814bef7
--- /dev/null
+++ b/lib/jobs/generated/kicking.h
@@ -0,0 +1,20 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class KickJob : public BaseJob
+ {
+ public:
+ explicit KickJob(const QString& roomId, const QString& userId, const QString& reason = {});
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/leaving.cpp b/lib/jobs/generated/leaving.cpp
new file mode 100644
index 00000000..fbc40d11
--- /dev/null
+++ b/lib/jobs/generated/leaving.cpp
@@ -0,0 +1,38 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "leaving.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+QUrl LeaveRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/rooms/" % roomId % "/leave");
+}
+
+LeaveRoomJob::LeaveRoomJob(const QString& roomId)
+ : BaseJob(HttpVerb::Post, "LeaveRoomJob",
+ basePath % "/rooms/" % roomId % "/leave")
+{
+}
+
+QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/rooms/" % roomId % "/forget");
+}
+
+ForgetRoomJob::ForgetRoomJob(const QString& roomId)
+ : BaseJob(HttpVerb::Post, "ForgetRoomJob",
+ basePath % "/rooms/" % roomId % "/forget")
+{
+}
+
diff --git a/lib/jobs/generated/leaving.h b/lib/jobs/generated/leaving.h
new file mode 100644
index 00000000..9bae2363
--- /dev/null
+++ b/lib/jobs/generated/leaving.h
@@ -0,0 +1,40 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class LeaveRoomJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * LeaveRoomJob. This function can be used when
+ * a URL for LeaveRoomJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId);
+
+ explicit LeaveRoomJob(const QString& roomId);
+ };
+
+ class ForgetRoomJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * ForgetRoomJob. This function can be used when
+ * a URL for ForgetRoomJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId);
+
+ explicit ForgetRoomJob(const QString& roomId);
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/list_public_rooms.cpp b/lib/jobs/generated/list_public_rooms.cpp
new file mode 100644
index 00000000..39653300
--- /dev/null
+++ b/lib/jobs/generated/list_public_rooms.cpp
@@ -0,0 +1,266 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "list_public_rooms.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+GetPublicRoomsJob::PublicRoomsChunk::operator QJsonObject() const
+{
+ QJsonObject o;
+ o.insert("aliases", toJson(aliases));
+ o.insert("canonical_alias", toJson(canonicalAlias));
+ o.insert("name", toJson(name));
+ o.insert("num_joined_members", toJson(numJoinedMembers));
+ o.insert("room_id", toJson(roomId));
+ o.insert("topic", toJson(topic));
+ o.insert("world_readable", toJson(worldReadable));
+ o.insert("guest_can_join", toJson(guestCanJoin));
+ o.insert("avatar_url", toJson(avatarUrl));
+
+ return o;
+}
+namespace QMatrixClient
+{
+ template <> struct FromJson<GetPublicRoomsJob::PublicRoomsChunk>
+ {
+ GetPublicRoomsJob::PublicRoomsChunk operator()(QJsonValue jv)
+ {
+ QJsonObject o = jv.toObject();
+ GetPublicRoomsJob::PublicRoomsChunk result;
+ result.aliases =
+ fromJson<QVector<QString>>(o.value("aliases"));
+ result.canonicalAlias =
+ fromJson<QString>(o.value("canonical_alias"));
+ result.name =
+ fromJson<QString>(o.value("name"));
+ result.numJoinedMembers =
+ fromJson<double>(o.value("num_joined_members"));
+ result.roomId =
+ fromJson<QString>(o.value("room_id"));
+ result.topic =
+ fromJson<QString>(o.value("topic"));
+ result.worldReadable =
+ fromJson<bool>(o.value("world_readable"));
+ result.guestCanJoin =
+ fromJson<bool>(o.value("guest_can_join"));
+ result.avatarUrl =
+ fromJson<QString>(o.value("avatar_url"));
+
+ return result;
+ }
+ };
+} // namespace QMatrixClient
+
+class GetPublicRoomsJob::Private
+{
+ public:
+ QVector<PublicRoomsChunk> chunk;
+ QString nextBatch;
+ QString prevBatch;
+ double totalRoomCountEstimate;
+};
+
+BaseJob::Query queryToGetPublicRooms(double limit, const QString& since, const QString& server)
+{
+ BaseJob::Query _q;
+ _q.addQueryItem("limit", QString("%1").arg(limit));
+ if (!since.isEmpty())
+ _q.addQueryItem("since", since);
+ if (!server.isEmpty())
+ _q.addQueryItem("server", server);
+ return _q;
+}
+
+QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, double limit, const QString& since, const QString& server)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/publicRooms",
+ queryToGetPublicRooms(limit, since, server));
+}
+
+GetPublicRoomsJob::GetPublicRoomsJob(double limit, const QString& since, const QString& server)
+ : BaseJob(HttpVerb::Get, "GetPublicRoomsJob",
+ basePath % "/publicRooms",
+ queryToGetPublicRooms(limit, since, server),
+ {}, false)
+ , d(new Private)
+{
+}
+
+GetPublicRoomsJob::~GetPublicRoomsJob() = default;
+
+const QVector<GetPublicRoomsJob::PublicRoomsChunk>& GetPublicRoomsJob::chunk() const
+{
+ return d->chunk;
+}
+
+const QString& GetPublicRoomsJob::nextBatch() const
+{
+ return d->nextBatch;
+}
+
+const QString& GetPublicRoomsJob::prevBatch() const
+{
+ return d->prevBatch;
+}
+
+double GetPublicRoomsJob::totalRoomCountEstimate() const
+{
+ return d->totalRoomCountEstimate;
+}
+
+BaseJob::Status GetPublicRoomsJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ if (!json.contains("chunk"))
+ return { JsonParseError,
+ "The key 'chunk' not found in the response" };
+ d->chunk = fromJson<QVector<PublicRoomsChunk>>(json.value("chunk"));
+ d->nextBatch = fromJson<QString>(json.value("next_batch"));
+ d->prevBatch = fromJson<QString>(json.value("prev_batch"));
+ d->totalRoomCountEstimate = fromJson<double>(json.value("total_room_count_estimate"));
+ return Success;
+}
+
+QueryPublicRoomsJob::Filter::operator QJsonObject() const
+{
+ QJsonObject o;
+ o.insert("generic_search_term", toJson(genericSearchTerm));
+
+ return o;
+}
+namespace QMatrixClient
+{
+ template <> struct FromJson<QueryPublicRoomsJob::Filter>
+ {
+ QueryPublicRoomsJob::Filter operator()(QJsonValue jv)
+ {
+ QJsonObject o = jv.toObject();
+ QueryPublicRoomsJob::Filter result;
+ result.genericSearchTerm =
+ fromJson<QString>(o.value("generic_search_term"));
+
+ return result;
+ }
+ };
+} // namespace QMatrixClient
+
+QueryPublicRoomsJob::PublicRoomsChunk::operator QJsonObject() const
+{
+ QJsonObject o;
+ o.insert("aliases", toJson(aliases));
+ o.insert("canonical_alias", toJson(canonicalAlias));
+ o.insert("name", toJson(name));
+ o.insert("num_joined_members", toJson(numJoinedMembers));
+ o.insert("room_id", toJson(roomId));
+ o.insert("topic", toJson(topic));
+ o.insert("world_readable", toJson(worldReadable));
+ o.insert("guest_can_join", toJson(guestCanJoin));
+ o.insert("avatar_url", toJson(avatarUrl));
+
+ return o;
+}
+namespace QMatrixClient
+{
+ template <> struct FromJson<QueryPublicRoomsJob::PublicRoomsChunk>
+ {
+ QueryPublicRoomsJob::PublicRoomsChunk operator()(QJsonValue jv)
+ {
+ QJsonObject o = jv.toObject();
+ QueryPublicRoomsJob::PublicRoomsChunk result;
+ result.aliases =
+ fromJson<QVector<QString>>(o.value("aliases"));
+ result.canonicalAlias =
+ fromJson<QString>(o.value("canonical_alias"));
+ result.name =
+ fromJson<QString>(o.value("name"));
+ result.numJoinedMembers =
+ fromJson<double>(o.value("num_joined_members"));
+ result.roomId =
+ fromJson<QString>(o.value("room_id"));
+ result.topic =
+ fromJson<QString>(o.value("topic"));
+ result.worldReadable =
+ fromJson<bool>(o.value("world_readable"));
+ result.guestCanJoin =
+ fromJson<bool>(o.value("guest_can_join"));
+ result.avatarUrl =
+ fromJson<QString>(o.value("avatar_url"));
+
+ return result;
+ }
+ };
+} // namespace QMatrixClient
+
+class QueryPublicRoomsJob::Private
+{
+ public:
+ QVector<PublicRoomsChunk> chunk;
+ QString nextBatch;
+ QString prevBatch;
+ double totalRoomCountEstimate;
+};
+
+BaseJob::Query queryToQueryPublicRooms(const QString& server)
+{
+ BaseJob::Query _q;
+ if (!server.isEmpty())
+ _q.addQueryItem("server", server);
+ return _q;
+}
+
+QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, double limit, const QString& since, const Filter& filter)
+ : BaseJob(HttpVerb::Post, "QueryPublicRoomsJob",
+ basePath % "/publicRooms",
+ queryToQueryPublicRooms(server))
+ , d(new Private)
+{
+ QJsonObject _data;
+ _data.insert("limit", toJson(limit));
+ if (!since.isEmpty())
+ _data.insert("since", toJson(since));
+ _data.insert("filter", toJson(filter));
+ setRequestData(_data);
+}
+
+QueryPublicRoomsJob::~QueryPublicRoomsJob() = default;
+
+const QVector<QueryPublicRoomsJob::PublicRoomsChunk>& QueryPublicRoomsJob::chunk() const
+{
+ return d->chunk;
+}
+
+const QString& QueryPublicRoomsJob::nextBatch() const
+{
+ return d->nextBatch;
+}
+
+const QString& QueryPublicRoomsJob::prevBatch() const
+{
+ return d->prevBatch;
+}
+
+double QueryPublicRoomsJob::totalRoomCountEstimate() const
+{
+ return d->totalRoomCountEstimate;
+}
+
+BaseJob::Status QueryPublicRoomsJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ if (!json.contains("chunk"))
+ return { JsonParseError,
+ "The key 'chunk' not found in the response" };
+ d->chunk = fromJson<QVector<PublicRoomsChunk>>(json.value("chunk"));
+ d->nextBatch = fromJson<QString>(json.value("next_batch"));
+ d->prevBatch = fromJson<QString>(json.value("prev_batch"));
+ d->totalRoomCountEstimate = fromJson<double>(json.value("total_room_count_estimate"));
+ return Success;
+}
+
diff --git a/lib/jobs/generated/list_public_rooms.h b/lib/jobs/generated/list_public_rooms.h
new file mode 100644
index 00000000..5c281de3
--- /dev/null
+++ b/lib/jobs/generated/list_public_rooms.h
@@ -0,0 +1,106 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+#include <QtCore/QVector>
+
+#include "converters.h"
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class GetPublicRoomsJob : public BaseJob
+ {
+ public:
+ // Inner data structures
+
+ struct PublicRoomsChunk
+ {
+ QVector<QString> aliases;
+ QString canonicalAlias;
+ QString name;
+ double numJoinedMembers;
+ QString roomId;
+ QString topic;
+ bool worldReadable;
+ bool guestCanJoin;
+ QString avatarUrl;
+
+ operator QJsonObject() const;
+ };
+
+ // End of inner data structures
+
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * GetPublicRoomsJob. This function can be used when
+ * a URL for GetPublicRoomsJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, double limit = {}, const QString& since = {}, const QString& server = {});
+
+ explicit GetPublicRoomsJob(double limit = {}, const QString& since = {}, const QString& server = {});
+ ~GetPublicRoomsJob() override;
+
+ const QVector<PublicRoomsChunk>& chunk() const;
+ const QString& nextBatch() const;
+ const QString& prevBatch() const;
+ double totalRoomCountEstimate() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+
+ class QueryPublicRoomsJob : public BaseJob
+ {
+ public:
+ // Inner data structures
+
+ struct Filter
+ {
+ QString genericSearchTerm;
+
+ operator QJsonObject() const;
+ };
+
+ struct PublicRoomsChunk
+ {
+ QVector<QString> aliases;
+ QString canonicalAlias;
+ QString name;
+ double numJoinedMembers;
+ QString roomId;
+ QString topic;
+ bool worldReadable;
+ bool guestCanJoin;
+ QString avatarUrl;
+
+ operator QJsonObject() const;
+ };
+
+ // End of inner data structures
+
+ explicit QueryPublicRoomsJob(const QString& server = {}, double limit = {}, const QString& since = {}, const Filter& filter = {});
+ ~QueryPublicRoomsJob() override;
+
+ const QVector<PublicRoomsChunk>& chunk() const;
+ const QString& nextBatch() const;
+ const QString& prevBatch() const;
+ double totalRoomCountEstimate() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/login.cpp b/lib/jobs/generated/login.cpp
new file mode 100644
index 00000000..a4dab428
--- /dev/null
+++ b/lib/jobs/generated/login.cpp
@@ -0,0 +1,79 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "login.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+class LoginJob::Private
+{
+ public:
+ QString userId;
+ QString accessToken;
+ QString homeServer;
+ QString deviceId;
+};
+
+LoginJob::LoginJob(const QString& type, const QString& user, const QString& medium, const QString& address, const QString& password, const QString& token, const QString& deviceId, const QString& initialDeviceDisplayName)
+ : BaseJob(HttpVerb::Post, "LoginJob",
+ basePath % "/login", false)
+ , d(new Private)
+{
+ QJsonObject _data;
+ _data.insert("type", toJson(type));
+ if (!user.isEmpty())
+ _data.insert("user", toJson(user));
+ if (!medium.isEmpty())
+ _data.insert("medium", toJson(medium));
+ if (!address.isEmpty())
+ _data.insert("address", toJson(address));
+ if (!password.isEmpty())
+ _data.insert("password", toJson(password));
+ if (!token.isEmpty())
+ _data.insert("token", toJson(token));
+ if (!deviceId.isEmpty())
+ _data.insert("device_id", toJson(deviceId));
+ if (!initialDeviceDisplayName.isEmpty())
+ _data.insert("initial_device_display_name", toJson(initialDeviceDisplayName));
+ setRequestData(_data);
+}
+
+LoginJob::~LoginJob() = default;
+
+const QString& LoginJob::userId() const
+{
+ return d->userId;
+}
+
+const QString& LoginJob::accessToken() const
+{
+ return d->accessToken;
+}
+
+const QString& LoginJob::homeServer() const
+{
+ return d->homeServer;
+}
+
+const QString& LoginJob::deviceId() const
+{
+ return d->deviceId;
+}
+
+BaseJob::Status LoginJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ d->userId = fromJson<QString>(json.value("user_id"));
+ d->accessToken = fromJson<QString>(json.value("access_token"));
+ d->homeServer = fromJson<QString>(json.value("home_server"));
+ d->deviceId = fromJson<QString>(json.value("device_id"));
+ return Success;
+}
+
diff --git a/lib/jobs/generated/login.h b/lib/jobs/generated/login.h
new file mode 100644
index 00000000..3ac955d4
--- /dev/null
+++ b/lib/jobs/generated/login.h
@@ -0,0 +1,33 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class LoginJob : public BaseJob
+ {
+ public:
+ explicit LoginJob(const QString& type, const QString& user = {}, const QString& medium = {}, const QString& address = {}, const QString& password = {}, const QString& token = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {});
+ ~LoginJob() override;
+
+ const QString& userId() const;
+ const QString& accessToken() const;
+ const QString& homeServer() const;
+ const QString& deviceId() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/logout.cpp b/lib/jobs/generated/logout.cpp
new file mode 100644
index 00000000..83139842
--- /dev/null
+++ b/lib/jobs/generated/logout.cpp
@@ -0,0 +1,26 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "logout.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+QUrl LogoutJob::makeRequestUrl(QUrl baseUrl)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/logout");
+}
+
+LogoutJob::LogoutJob()
+ : BaseJob(HttpVerb::Post, "LogoutJob",
+ basePath % "/logout")
+{
+}
+
diff --git a/lib/jobs/generated/logout.h b/lib/jobs/generated/logout.h
new file mode 100644
index 00000000..7640ba55
--- /dev/null
+++ b/lib/jobs/generated/logout.h
@@ -0,0 +1,27 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class LogoutJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * LogoutJob. This function can be used when
+ * a URL for LogoutJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl);
+
+ explicit LogoutJob();
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/profile.cpp b/lib/jobs/generated/profile.cpp
new file mode 100644
index 00000000..1f7092d7
--- /dev/null
+++ b/lib/jobs/generated/profile.cpp
@@ -0,0 +1,140 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "profile.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+SetDisplayNameJob::SetDisplayNameJob(const QString& userId, const QString& displayname)
+ : BaseJob(HttpVerb::Put, "SetDisplayNameJob",
+ basePath % "/profile/" % userId % "/displayname")
+{
+ QJsonObject _data;
+ if (!displayname.isEmpty())
+ _data.insert("displayname", toJson(displayname));
+ setRequestData(_data);
+}
+
+class GetDisplayNameJob::Private
+{
+ public:
+ QString displayname;
+};
+
+QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/profile/" % userId % "/displayname");
+}
+
+GetDisplayNameJob::GetDisplayNameJob(const QString& userId)
+ : BaseJob(HttpVerb::Get, "GetDisplayNameJob",
+ basePath % "/profile/" % userId % "/displayname", false)
+ , d(new Private)
+{
+}
+
+GetDisplayNameJob::~GetDisplayNameJob() = default;
+
+const QString& GetDisplayNameJob::displayname() const
+{
+ return d->displayname;
+}
+
+BaseJob::Status GetDisplayNameJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ d->displayname = fromJson<QString>(json.value("displayname"));
+ return Success;
+}
+
+SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl)
+ : BaseJob(HttpVerb::Put, "SetAvatarUrlJob",
+ basePath % "/profile/" % userId % "/avatar_url")
+{
+ QJsonObject _data;
+ if (!avatarUrl.isEmpty())
+ _data.insert("avatar_url", toJson(avatarUrl));
+ setRequestData(_data);
+}
+
+class GetAvatarUrlJob::Private
+{
+ public:
+ QString avatarUrl;
+};
+
+QUrl GetAvatarUrlJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/profile/" % userId % "/avatar_url");
+}
+
+GetAvatarUrlJob::GetAvatarUrlJob(const QString& userId)
+ : BaseJob(HttpVerb::Get, "GetAvatarUrlJob",
+ basePath % "/profile/" % userId % "/avatar_url", false)
+ , d(new Private)
+{
+}
+
+GetAvatarUrlJob::~GetAvatarUrlJob() = default;
+
+const QString& GetAvatarUrlJob::avatarUrl() const
+{
+ return d->avatarUrl;
+}
+
+BaseJob::Status GetAvatarUrlJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ d->avatarUrl = fromJson<QString>(json.value("avatar_url"));
+ return Success;
+}
+
+class GetUserProfileJob::Private
+{
+ public:
+ QString avatarUrl;
+ QString displayname;
+};
+
+QUrl GetUserProfileJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/profile/" % userId);
+}
+
+GetUserProfileJob::GetUserProfileJob(const QString& userId)
+ : BaseJob(HttpVerb::Get, "GetUserProfileJob",
+ basePath % "/profile/" % userId, false)
+ , d(new Private)
+{
+}
+
+GetUserProfileJob::~GetUserProfileJob() = default;
+
+const QString& GetUserProfileJob::avatarUrl() const
+{
+ return d->avatarUrl;
+}
+
+const QString& GetUserProfileJob::displayname() const
+{
+ return d->displayname;
+}
+
+BaseJob::Status GetUserProfileJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ d->avatarUrl = fromJson<QString>(json.value("avatar_url"));
+ d->displayname = fromJson<QString>(json.value("displayname"));
+ return Success;
+}
+
diff --git a/lib/jobs/generated/profile.h b/lib/jobs/generated/profile.h
new file mode 100644
index 00000000..024130f5
--- /dev/null
+++ b/lib/jobs/generated/profile.h
@@ -0,0 +1,96 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class SetDisplayNameJob : public BaseJob
+ {
+ public:
+ explicit SetDisplayNameJob(const QString& userId, const QString& displayname = {});
+ };
+
+ class GetDisplayNameJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * GetDisplayNameJob. This function can be used when
+ * a URL for GetDisplayNameJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId);
+
+ explicit GetDisplayNameJob(const QString& userId);
+ ~GetDisplayNameJob() override;
+
+ const QString& displayname() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+
+ class SetAvatarUrlJob : public BaseJob
+ {
+ public:
+ explicit SetAvatarUrlJob(const QString& userId, const QString& avatarUrl = {});
+ };
+
+ class GetAvatarUrlJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * GetAvatarUrlJob. This function can be used when
+ * a URL for GetAvatarUrlJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId);
+
+ explicit GetAvatarUrlJob(const QString& userId);
+ ~GetAvatarUrlJob() override;
+
+ const QString& avatarUrl() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+
+ class GetUserProfileJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * GetUserProfileJob. This function can be used when
+ * a URL for GetUserProfileJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId);
+
+ explicit GetUserProfileJob(const QString& userId);
+ ~GetUserProfileJob() override;
+
+ const QString& avatarUrl() const;
+ const QString& displayname() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/receipts.cpp b/lib/jobs/generated/receipts.cpp
new file mode 100644
index 00000000..83c38b6f
--- /dev/null
+++ b/lib/jobs/generated/receipts.cpp
@@ -0,0 +1,21 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "receipts.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, const QJsonObject& receipt)
+ : BaseJob(HttpVerb::Post, "PostReceiptJob",
+ basePath % "/rooms/" % roomId % "/receipt/" % receiptType % "/" % eventId)
+{
+ setRequestData(Data(receipt));
+}
+
diff --git a/lib/jobs/generated/receipts.h b/lib/jobs/generated/receipts.h
new file mode 100644
index 00000000..9eb7a489
--- /dev/null
+++ b/lib/jobs/generated/receipts.h
@@ -0,0 +1,21 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+#include <QtCore/QJsonObject>
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class PostReceiptJob : public BaseJob
+ {
+ public:
+ explicit PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, const QJsonObject& receipt = {});
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/redaction.cpp b/lib/jobs/generated/redaction.cpp
new file mode 100644
index 00000000..0da35dfc
--- /dev/null
+++ b/lib/jobs/generated/redaction.cpp
@@ -0,0 +1,45 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "redaction.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+class RedactEventJob::Private
+{
+ public:
+ QString eventId;
+};
+
+RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason)
+ : BaseJob(HttpVerb::Put, "RedactEventJob",
+ basePath % "/rooms/" % roomId % "/redact/" % eventId % "/" % txnId)
+ , d(new Private)
+{
+ QJsonObject _data;
+ if (!reason.isEmpty())
+ _data.insert("reason", toJson(reason));
+ setRequestData(_data);
+}
+
+RedactEventJob::~RedactEventJob() = default;
+
+const QString& RedactEventJob::eventId() const
+{
+ return d->eventId;
+}
+
+BaseJob::Status RedactEventJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ d->eventId = fromJson<QString>(json.value("event_id"));
+ return Success;
+}
+
diff --git a/lib/jobs/generated/redaction.h b/lib/jobs/generated/redaction.h
new file mode 100644
index 00000000..e3b3ff4f
--- /dev/null
+++ b/lib/jobs/generated/redaction.h
@@ -0,0 +1,30 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class RedactEventJob : public BaseJob
+ {
+ public:
+ explicit RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason = {});
+ ~RedactEventJob() override;
+
+ const QString& eventId() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/third_party_membership.cpp b/lib/jobs/generated/third_party_membership.cpp
new file mode 100644
index 00000000..b637d481
--- /dev/null
+++ b/lib/jobs/generated/third_party_membership.cpp
@@ -0,0 +1,25 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "third_party_membership.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& medium, const QString& address)
+ : BaseJob(HttpVerb::Post, "InviteBy3PIDJob",
+ basePath % "/rooms/" % roomId % "/invite")
+{
+ QJsonObject _data;
+ _data.insert("id_server", toJson(idServer));
+ _data.insert("medium", toJson(medium));
+ _data.insert("address", toJson(address));
+ setRequestData(_data);
+}
+
diff --git a/lib/jobs/generated/third_party_membership.h b/lib/jobs/generated/third_party_membership.h
new file mode 100644
index 00000000..c7b5214e
--- /dev/null
+++ b/lib/jobs/generated/third_party_membership.h
@@ -0,0 +1,20 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class InviteBy3PIDJob : public BaseJob
+ {
+ public:
+ explicit InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& medium, const QString& address);
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/typing.cpp b/lib/jobs/generated/typing.cpp
new file mode 100644
index 00000000..fa700290
--- /dev/null
+++ b/lib/jobs/generated/typing.cpp
@@ -0,0 +1,24 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "typing.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId, bool typing, int timeout)
+ : BaseJob(HttpVerb::Put, "SetTypingJob",
+ basePath % "/rooms/" % roomId % "/typing/" % userId)
+{
+ QJsonObject _data;
+ _data.insert("typing", toJson(typing));
+ _data.insert("timeout", toJson(timeout));
+ setRequestData(_data);
+}
+
diff --git a/lib/jobs/generated/typing.h b/lib/jobs/generated/typing.h
new file mode 100644
index 00000000..0495ed0a
--- /dev/null
+++ b/lib/jobs/generated/typing.h
@@ -0,0 +1,20 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class SetTypingJob : public BaseJob
+ {
+ public:
+ explicit SetTypingJob(const QString& userId, const QString& roomId, bool typing, int timeout = {});
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/versions.cpp b/lib/jobs/generated/versions.cpp
new file mode 100644
index 00000000..b12594ca
--- /dev/null
+++ b/lib/jobs/generated/versions.cpp
@@ -0,0 +1,47 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "versions.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client");
+
+class GetVersionsJob::Private
+{
+ public:
+ QVector<QString> versions;
+};
+
+QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/versions");
+}
+
+GetVersionsJob::GetVersionsJob()
+ : BaseJob(HttpVerb::Get, "GetVersionsJob",
+ basePath % "/versions", false)
+ , d(new Private)
+{
+}
+
+GetVersionsJob::~GetVersionsJob() = default;
+
+const QVector<QString>& GetVersionsJob::versions() const
+{
+ return d->versions;
+}
+
+BaseJob::Status GetVersionsJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ d->versions = fromJson<QVector<QString>>(json.value("versions"));
+ return Success;
+}
+
diff --git a/lib/jobs/generated/versions.h b/lib/jobs/generated/versions.h
new file mode 100644
index 00000000..18f6bb44
--- /dev/null
+++ b/lib/jobs/generated/versions.h
@@ -0,0 +1,38 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+#include <QtCore/QVector>
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class GetVersionsJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * GetVersionsJob. This function can be used when
+ * a URL for GetVersionsJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl);
+
+ explicit GetVersionsJob();
+ ~GetVersionsJob() override;
+
+ const QVector<QString>& versions() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/generated/whoami.cpp b/lib/jobs/generated/whoami.cpp
new file mode 100644
index 00000000..cc38fa4d
--- /dev/null
+++ b/lib/jobs/generated/whoami.cpp
@@ -0,0 +1,50 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "whoami.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+class GetTokenOwnerJob::Private
+{
+ public:
+ QString userId;
+};
+
+QUrl GetTokenOwnerJob::makeRequestUrl(QUrl baseUrl)
+{
+ return BaseJob::makeRequestUrl(baseUrl,
+ basePath % "/account/whoami");
+}
+
+GetTokenOwnerJob::GetTokenOwnerJob()
+ : BaseJob(HttpVerb::Get, "GetTokenOwnerJob",
+ basePath % "/account/whoami")
+ , d(new Private)
+{
+}
+
+GetTokenOwnerJob::~GetTokenOwnerJob() = default;
+
+const QString& GetTokenOwnerJob::userId() const
+{
+ return d->userId;
+}
+
+BaseJob::Status GetTokenOwnerJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ if (!json.contains("user_id"))
+ return { JsonParseError,
+ "The key 'user_id' not found in the response" };
+ d->userId = fromJson<QString>(json.value("user_id"));
+ return Success;
+}
+
diff --git a/lib/jobs/generated/whoami.h b/lib/jobs/generated/whoami.h
new file mode 100644
index 00000000..835232ee
--- /dev/null
+++ b/lib/jobs/generated/whoami.h
@@ -0,0 +1,37 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ class GetTokenOwnerJob : public BaseJob
+ {
+ public:
+ /** Construct a URL out of baseUrl and usual parameters passed to
+ * GetTokenOwnerJob. This function can be used when
+ * a URL for GetTokenOwnerJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl);
+
+ explicit GetTokenOwnerJob();
+ ~GetTokenOwnerJob() override;
+
+ const QString& userId() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/joinroomjob.cpp b/lib/jobs/joinroomjob.cpp
new file mode 100644
index 00000000..66a75089
--- /dev/null
+++ b/lib/jobs/joinroomjob.cpp
@@ -0,0 +1,58 @@
+/******************************************************************************
+ * 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 "joinroomjob.h"
+#include "util.h"
+
+using namespace QMatrixClient;
+
+class JoinRoomJob::Private
+{
+ public:
+ QString roomId;
+};
+
+JoinRoomJob::JoinRoomJob(const QString& roomAlias)
+ : BaseJob(HttpVerb::Post, "JoinRoomJob",
+ QStringLiteral("_matrix/client/r0/join/%1").arg(roomAlias))
+ , d(new Private)
+{
+}
+
+JoinRoomJob::~JoinRoomJob()
+{
+ delete d;
+}
+
+QString JoinRoomJob::roomId()
+{
+ return d->roomId;
+}
+
+BaseJob::Status JoinRoomJob::parseJson(const QJsonDocument& data)
+{
+ QJsonObject json = data.object();
+ if( json.contains("room_id") )
+ {
+ d->roomId = json.value("room_id").toString();
+ return Success;
+ }
+
+ qCDebug(JOBS) << data;
+ return { UserDefinedError, "No room_id in the JSON response" };
+}
diff --git a/lib/jobs/joinroomjob.h b/lib/jobs/joinroomjob.h
new file mode 100644
index 00000000..f3ba216f
--- /dev/null
+++ b/lib/jobs/joinroomjob.h
@@ -0,0 +1,40 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include "basejob.h"
+
+namespace QMatrixClient
+{
+ class JoinRoomJob: public BaseJob
+ {
+ public:
+ explicit JoinRoomJob(const QString& roomAlias);
+ virtual ~JoinRoomJob();
+
+ QString roomId();
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ Private* d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp
new file mode 100644
index 00000000..dda1cdb4
--- /dev/null
+++ b/lib/jobs/mediathumbnailjob.cpp
@@ -0,0 +1,63 @@
+/******************************************************************************
+ * Copyright (C) 2016 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 "mediathumbnailjob.h"
+
+using namespace QMatrixClient;
+
+QUrl MediaThumbnailJob::makeRequestUrl(QUrl baseUrl,
+ const QUrl& mxcUri, QSize requestedSize)
+{
+ return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1),
+ requestedSize.width(), requestedSize.height());
+}
+
+MediaThumbnailJob::MediaThumbnailJob(const QString& serverName,
+ const QString& mediaId, QSize requestedSize)
+ : GetContentThumbnailJob(serverName, mediaId,
+ requestedSize.width(), requestedSize.height())
+{ }
+
+MediaThumbnailJob::MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize)
+ : GetContentThumbnailJob(mxcUri.authority(),
+ mxcUri.path().mid(1), // sans leading '/'
+ requestedSize.width(), requestedSize.height())
+{ }
+
+QImage MediaThumbnailJob::thumbnail() const
+{
+ return _thumbnail;
+}
+
+QImage MediaThumbnailJob::scaledThumbnail(QSize toSize) const
+{
+ return _thumbnail.scaled(toSize,
+ Qt::KeepAspectRatio, Qt::SmoothTransformation);
+}
+
+BaseJob::Status MediaThumbnailJob::parseReply(QNetworkReply* reply)
+{
+ auto result = GetContentThumbnailJob::parseReply(reply);
+ if (!result.good())
+ return result;
+
+ if( _thumbnail.loadFromData(content()->readAll()) )
+ return Success;
+
+ return { IncorrectResponseError, "Could not read image data" };
+}
diff --git a/lib/jobs/mediathumbnailjob.h b/lib/jobs/mediathumbnailjob.h
new file mode 100644
index 00000000..6e0b94f3
--- /dev/null
+++ b/lib/jobs/mediathumbnailjob.h
@@ -0,0 +1,47 @@
+/******************************************************************************
+ * Copyright (C) 2016 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
+ */
+
+#pragma once
+
+#include "generated/content-repo.h"
+
+#include <QtGui/QPixmap>
+
+namespace QMatrixClient
+{
+ class MediaThumbnailJob: public GetContentThumbnailJob
+ {
+ public:
+ using GetContentThumbnailJob::makeRequestUrl;
+ static QUrl makeRequestUrl(QUrl baseUrl,
+ const QUrl& mxcUri, QSize requestedSize);
+
+ MediaThumbnailJob(const QString& serverName, const QString& mediaId,
+ QSize requestedSize);
+ MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize);
+
+ QImage thumbnail() const;
+ QImage scaledThumbnail(QSize toSize) const;
+
+ protected:
+ Status parseReply(QNetworkReply* reply) override;
+
+ private:
+ QImage _thumbnail;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/passwordlogin.cpp b/lib/jobs/passwordlogin.cpp
new file mode 100644
index 00000000..8abfe66a
--- /dev/null
+++ b/lib/jobs/passwordlogin.cpp
@@ -0,0 +1,74 @@
+/******************************************************************************
+ * 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 "passwordlogin.h"
+
+using namespace QMatrixClient;
+
+class PasswordLogin::Private
+{
+ public:
+ QString returned_id;
+ QString returned_server;
+ QString returned_token;
+};
+
+PasswordLogin::PasswordLogin(QString user, QString password)
+ : BaseJob(HttpVerb::Post, "PasswordLogin",
+ "_matrix/client/r0/login", Query(), Data(), false)
+ , d(new Private)
+{
+ QJsonObject _data;
+ _data.insert("type", QStringLiteral("m.login.password"));
+ _data.insert("user", user);
+ _data.insert("password", password);
+ setRequestData(_data);
+}
+
+PasswordLogin::~PasswordLogin()
+{
+ delete d;
+}
+
+QString PasswordLogin::token() const
+{
+ return d->returned_token;
+}
+
+QString PasswordLogin::id() const
+{
+ return d->returned_id;
+}
+
+QString PasswordLogin::server() const
+{
+ return d->returned_server;
+}
+
+BaseJob::Status PasswordLogin::parseJson(const QJsonDocument& data)
+{
+ QJsonObject json = data.object();
+ if( !json.contains("access_token") || !json.contains("home_server") || !json.contains("user_id") )
+ {
+ return { UserDefinedError, "No expected data" };
+ }
+ d->returned_token = json.value("access_token").toString();
+ d->returned_server = json.value("home_server").toString();
+ d->returned_id = json.value("user_id").toString();
+ return Success;
+}
diff --git a/lib/jobs/passwordlogin.h b/lib/jobs/passwordlogin.h
new file mode 100644
index 00000000..fb8777a3
--- /dev/null
+++ b/lib/jobs/passwordlogin.h
@@ -0,0 +1,42 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include "basejob.h"
+
+namespace QMatrixClient
+{
+ class PasswordLogin : public BaseJob
+ {
+ public:
+ PasswordLogin(QString user, QString password);
+ virtual ~PasswordLogin();
+
+ QString token() const;
+ QString id() const;
+ QString server() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ Private* d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/postreadmarkersjob.h b/lib/jobs/postreadmarkersjob.h
new file mode 100644
index 00000000..d0198821
--- /dev/null
+++ b/lib/jobs/postreadmarkersjob.h
@@ -0,0 +1,37 @@
+/******************************************************************************
+ * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include "basejob.h"
+
+using namespace QMatrixClient;
+
+class PostReadMarkersJob : public BaseJob
+{
+ public:
+ explicit PostReadMarkersJob(const QString& roomId,
+ const QString& readUpToEventId)
+ : BaseJob(HttpVerb::Post, "PostReadMarkersJob",
+ QStringLiteral("_matrix/client/r0/rooms/%1/read_markers")
+ .arg(roomId))
+ {
+ setRequestData(QJsonObject {{
+ QStringLiteral("m.fully_read"), readUpToEventId }});
+ }
+};
diff --git a/lib/jobs/postreceiptjob.cpp b/lib/jobs/postreceiptjob.cpp
new file mode 100644
index 00000000..4572d74c
--- /dev/null
+++ b/lib/jobs/postreceiptjob.cpp
@@ -0,0 +1,27 @@
+/******************************************************************************
+ * Copyright (C) 2016 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 "postreceiptjob.h"
+
+using namespace QMatrixClient;
+
+PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& eventId)
+ : BaseJob(HttpVerb::Post, "PostReceiptJob",
+ QStringLiteral("/_matrix/client/r0/rooms/%1/receipt/m.read/%2")
+ .arg(roomId, eventId))
+{ }
diff --git a/lib/jobs/postreceiptjob.h b/lib/jobs/postreceiptjob.h
new file mode 100644
index 00000000..23df7c05
--- /dev/null
+++ b/lib/jobs/postreceiptjob.h
@@ -0,0 +1,30 @@
+/******************************************************************************
+ * Copyright (C) 2016 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
+ */
+
+#pragma once
+
+#include "basejob.h"
+
+namespace QMatrixClient
+{
+ class PostReceiptJob: public BaseJob
+ {
+ public:
+ PostReceiptJob(const QString& roomId, const QString& eventId);
+ };
+}
diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp
new file mode 100644
index 00000000..5cb62221
--- /dev/null
+++ b/lib/jobs/requestdata.cpp
@@ -0,0 +1,38 @@
+#include "requestdata.h"
+
+#include <QtCore/QByteArray>
+#include <QtCore/QJsonObject>
+#include <QtCore/QJsonArray>
+#include <QtCore/QJsonDocument>
+#include <QtCore/QBuffer>
+
+using namespace QMatrixClient;
+
+auto fromData(const QByteArray& data)
+{
+ auto source = std::make_unique<QBuffer>();
+ source->open(QIODevice::WriteOnly);
+ source->write(data);
+ source->close();
+ return source;
+}
+
+template <typename JsonDataT>
+inline auto fromJson(const JsonDataT& jdata)
+{
+ return fromData(QJsonDocument(jdata).toJson(QJsonDocument::Compact));
+}
+
+RequestData::RequestData(const QByteArray& a)
+ : _source(fromData(a))
+{ }
+
+RequestData::RequestData(const QJsonObject& jo)
+ : _source(fromJson(jo))
+{ }
+
+RequestData::RequestData(const QJsonArray& ja)
+ : _source(fromJson(ja))
+{ }
+
+RequestData::~RequestData() = default;
diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h
new file mode 100644
index 00000000..aa03b744
--- /dev/null
+++ b/lib/jobs/requestdata.h
@@ -0,0 +1,59 @@
+/******************************************************************************
+ * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include <memory>
+
+class QByteArray;
+class QJsonObject;
+class QJsonArray;
+class QJsonDocument;
+class QIODevice;
+
+namespace QMatrixClient
+{
+ /**
+ * A simple wrapper that represents the request body.
+ * Provides a unified interface to dump an unstructured byte stream
+ * as well as JSON (and possibly other structures in the future) to
+ * a QByteArray consumed by QNetworkAccessManager request methods.
+ */
+ class RequestData
+ {
+ public:
+ RequestData() = default;
+ RequestData(const QByteArray& a);
+ RequestData(const QJsonObject& jo);
+ RequestData(const QJsonArray& ja);
+ RequestData(QIODevice* source)
+ : _source(std::unique_ptr<QIODevice>(source))
+ { }
+ RequestData(RequestData&&) = default;
+ RequestData& operator=(RequestData&&) = default;
+ ~RequestData();
+
+ QIODevice* source() const
+ {
+ return _source.get();
+ }
+
+ private:
+ std::unique_ptr<QIODevice> _source;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/roommessagesjob.cpp b/lib/jobs/roommessagesjob.cpp
new file mode 100644
index 00000000..e5568f17
--- /dev/null
+++ b/lib/jobs/roommessagesjob.cpp
@@ -0,0 +1,65 @@
+/******************************************************************************
+ * Copyright (C) 2016 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 "roommessagesjob.h"
+
+using namespace QMatrixClient;
+
+class RoomMessagesJob::Private
+{
+ public:
+ RoomEvents events;
+ QString end;
+};
+
+RoomMessagesJob::RoomMessagesJob(const QString& roomId, const QString& from,
+ int limit, FetchDirection dir)
+ : BaseJob(HttpVerb::Get, "RoomMessagesJob",
+ QStringLiteral("/_matrix/client/r0/rooms/%1/messages").arg(roomId),
+ Query(
+ { { "from", from }
+ , { "dir", dir == FetchDirection::Backward ? "b" : "f" }
+ , { "limit", QString::number(limit) }
+ }))
+ , d(new Private)
+{
+ qCDebug(JOBS) << "Room messages query:" << query().toString(QUrl::PrettyDecoded);
+}
+
+RoomMessagesJob::~RoomMessagesJob()
+{
+ delete d;
+}
+
+RoomEvents&& RoomMessagesJob::releaseEvents()
+{
+ return move(d->events);
+}
+
+QString RoomMessagesJob::end() const
+{
+ return d->end;
+}
+
+BaseJob::Status RoomMessagesJob::parseJson(const QJsonDocument& data)
+{
+ const auto obj = data.object();
+ d->events.fromJson(obj, "chunk");
+ d->end = obj.value("end").toString();
+ return Success;
+}
diff --git a/lib/jobs/roommessagesjob.h b/lib/jobs/roommessagesjob.h
new file mode 100644
index 00000000..7b3fd9c9
--- /dev/null
+++ b/lib/jobs/roommessagesjob.h
@@ -0,0 +1,47 @@
+/******************************************************************************
+ * Copyright (C) 2016 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
+ */
+
+#pragma once
+
+#include "basejob.h"
+
+#include "../events/event.h"
+
+namespace QMatrixClient
+{
+ enum class FetchDirection { Backward, Forward };
+
+ class RoomMessagesJob: public BaseJob
+ {
+ public:
+ RoomMessagesJob(const QString& roomId, const QString& from,
+ int limit = 10,
+ FetchDirection dir = FetchDirection::Backward);
+ virtual ~RoomMessagesJob();
+
+ RoomEvents&& releaseEvents();
+ QString end() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ Private* d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/sendeventjob.cpp b/lib/jobs/sendeventjob.cpp
new file mode 100644
index 00000000..f5190d4b
--- /dev/null
+++ b/lib/jobs/sendeventjob.cpp
@@ -0,0 +1,45 @@
+/******************************************************************************
+ * 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 QString& roomId, const QString& type,
+ const QString& plainText)
+ : SendEventJob(roomId, RoomMessageEvent(plainText, type))
+{ }
+
+void SendEventJob::beforeStart(const ConnectionData* connData)
+{
+ BaseJob::beforeStart(connData);
+ setApiEndpoint(apiEndpoint() + connData->generateTxnId());
+}
+
+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/lib/jobs/sendeventjob.h b/lib/jobs/sendeventjob.h
new file mode 100644
index 00000000..3a11eb6a
--- /dev/null
+++ b/lib/jobs/sendeventjob.h
@@ -0,0 +1,57 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include "basejob.h"
+
+#include "connectiondata.h"
+
+namespace QMatrixClient
+{
+ class SendEventJob: public BaseJob
+ {
+ public:
+ /** Constructs a job that sends an arbitrary room event */
+ template <typename EvT>
+ SendEventJob(const QString& roomId, const EvT& event)
+ : BaseJob(HttpVerb::Put, QStringLiteral("SendEventJob"),
+ QStringLiteral("_matrix/client/r0/rooms/%1/send/%2/")
+ .arg(roomId, EvT::TypeId), // See also beforeStart()
+ Query(),
+ Data(event.toJson()))
+ { }
+
+ /**
+ * Constructs a plain text message job (for compatibility with
+ * the old PostMessageJob API).
+ */
+ SendEventJob(const QString& roomId, const QString& type,
+ const QString& plainText);
+
+ QString eventId() const { return _eventId; }
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ QString _eventId;
+
+ void beforeStart(const ConnectionData* connData) override;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/setroomstatejob.cpp b/lib/jobs/setroomstatejob.cpp
new file mode 100644
index 00000000..c2beb87b
--- /dev/null
+++ b/lib/jobs/setroomstatejob.cpp
@@ -0,0 +1,32 @@
+/******************************************************************************
+ * 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 "setroomstatejob.h"
+
+using namespace QMatrixClient;
+
+BaseJob::Status SetRoomStateJob::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/lib/jobs/setroomstatejob.h b/lib/jobs/setroomstatejob.h
new file mode 100644
index 00000000..b7e6d4a1
--- /dev/null
+++ b/lib/jobs/setroomstatejob.h
@@ -0,0 +1,64 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include "basejob.h"
+
+#include "connectiondata.h"
+
+namespace QMatrixClient
+{
+ class SetRoomStateJob: public BaseJob
+ {
+ public:
+ /**
+ * Constructs a job that sets a state using an arbitrary room event
+ * with a state key.
+ */
+ template <typename EvT>
+ SetRoomStateJob(const QString& roomId, const QString& stateKey,
+ const EvT& event)
+ : BaseJob(HttpVerb::Put, "SetRoomStateJob",
+ QStringLiteral("_matrix/client/r0/rooms/%1/state/%2/%3")
+ .arg(roomId, EvT::TypeId, stateKey),
+ Query(),
+ Data(event.toJson()))
+ { }
+ /**
+ * Constructs a job that sets a state using an arbitrary room event
+ * without a state key.
+ */
+ template <typename EvT>
+ SetRoomStateJob(const QString& roomId, const EvT& event)
+ : BaseJob(HttpVerb::Put, "SetRoomStateJob",
+ QStringLiteral("_matrix/client/r0/rooms/%1/state/%2")
+ .arg(roomId, EvT::TypeId),
+ Query(),
+ Data(event.toJson()))
+ { }
+
+ QString eventId() const { return _eventId; }
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ QString _eventId;
+ };
+} // namespace QMatrixClient
diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp
new file mode 100644
index 00000000..435dfd0e
--- /dev/null
+++ b/lib/jobs/syncjob.cpp
@@ -0,0 +1,133 @@
+/******************************************************************************
+ * Copyright (C) 2016 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 "syncjob.h"
+
+#include <QtCore/QElapsedTimer>
+
+using namespace QMatrixClient;
+
+static size_t jobId = 0;
+
+SyncJob::SyncJob(const QString& since, const QString& filter, int timeout,
+ const QString& presence)
+ : BaseJob(HttpVerb::Get, QStringLiteral("SyncJob-%1").arg(++jobId),
+ QStringLiteral("_matrix/client/r0/sync"))
+{
+ setLoggingCategory(SYNCJOB);
+ QUrlQuery query;
+ if( !filter.isEmpty() )
+ query.addQueryItem("filter", filter);
+ if( !presence.isEmpty() )
+ query.addQueryItem("set_presence", presence);
+ if( timeout >= 0 )
+ query.addQueryItem("timeout", QString::number(timeout));
+ if( !since.isEmpty() )
+ query.addQueryItem("since", since);
+ setRequestQuery(query);
+
+ setMaxRetries(std::numeric_limits<int>::max());
+}
+
+QString SyncData::nextBatch() const
+{
+ return nextBatch_;
+}
+
+SyncDataList&& SyncData::takeRoomData()
+{
+ return std::move(roomData);
+}
+
+SyncBatch<Event>&& SyncData::takeAccountData()
+{
+ return std::move(accountData);
+}
+
+BaseJob::Status SyncJob::parseJson(const QJsonDocument& data)
+{
+ return d.parseJson(data);
+}
+
+BaseJob::Status SyncData::parseJson(const QJsonDocument &data)
+{
+ QElapsedTimer et; et.start();
+
+ auto json = data.object();
+ nextBatch_ = json.value("next_batch").toString();
+ // TODO: presence
+ accountData.fromJson(json);
+
+ QJsonObject rooms = json.value("rooms").toObject();
+ JoinStates::Int ii = 1; // ii is used to make a JoinState value
+ for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1)
+ {
+ const auto rs = rooms.value(JoinStateStrings[i]).toObject();
+ // We have a Qt container on the right and an STL one on the left
+ roomData.reserve(static_cast<size_t>(rs.size()));
+ for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt)
+ roomData.emplace_back(roomIt.key(), JoinState(ii),
+ roomIt.value().toObject());
+ }
+ qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with"
+ << rooms.size() << "room(s) in" << et;
+ return BaseJob::Success;
+}
+
+const QString SyncRoomData::UnreadCountKey =
+ QStringLiteral("x-qmatrixclient.unread_count");
+
+SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
+ const QJsonObject& room_)
+ : roomId(roomId_)
+ , joinState(joinState_)
+ , state(joinState == JoinState::Invite ? "invite_state" : "state")
+ , timeline("timeline")
+ , ephemeral("ephemeral")
+ , accountData("account_data")
+{
+ switch (joinState) {
+ case JoinState::Invite:
+ state.fromJson(room_);
+ break;
+ case JoinState::Join:
+ state.fromJson(room_);
+ timeline.fromJson(room_);
+ ephemeral.fromJson(room_);
+ accountData.fromJson(room_);
+ break;
+ case JoinState::Leave:
+ state.fromJson(room_);
+ timeline.fromJson(room_);
+ break;
+ default:
+ qCWarning(SYNCJOB) << "SyncRoomData: Unknown JoinState value, ignoring:" << int(joinState);
+ }
+
+ auto timelineJson = room_.value("timeline").toObject();
+ timelineLimited = timelineJson.value("limited").toBool();
+ timelinePrevBatch = timelineJson.value("prev_batch").toString();
+
+ auto unreadJson = room_.value("unread_notifications").toObject();
+ unreadCount = unreadJson.value(UnreadCountKey).toInt(-2);
+ highlightCount = unreadJson.value("highlight_count").toInt();
+ notificationCount = unreadJson.value("notification_count").toInt();
+ if (highlightCount > 0 || notificationCount > 0)
+ qCDebug(SYNCJOB) << "Highlights: " << highlightCount
+ << " Notifications:" << notificationCount;
+}
diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h
new file mode 100644
index 00000000..919060be
--- /dev/null
+++ b/lib/jobs/syncjob.h
@@ -0,0 +1,99 @@
+/******************************************************************************
+ * Copyright (C) 2016 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
+ */
+
+#pragma once
+
+#include "basejob.h"
+
+#include "joinstate.h"
+#include "events/event.h"
+#include "util.h"
+
+namespace QMatrixClient
+{
+ template <typename EventT>
+ class SyncBatch : public EventsBatch<EventT>
+ {
+ public:
+ explicit SyncBatch(QString k) : jsonKey(std::move(k)) { }
+ void fromJson(const QJsonObject& roomContents)
+ {
+ EventsBatch<EventT>::fromJson(
+ roomContents[jsonKey].toObject(), "events");
+ }
+
+ private:
+ QString jsonKey;
+ };
+
+ class SyncRoomData
+ {
+ public:
+ QString roomId;
+ JoinState joinState;
+ SyncBatch<RoomEvent> state;
+ SyncBatch<RoomEvent> timeline;
+ SyncBatch<Event> ephemeral;
+ SyncBatch<Event> accountData;
+
+ bool timelineLimited;
+ QString timelinePrevBatch;
+ int unreadCount;
+ int highlightCount;
+ int notificationCount;
+
+ SyncRoomData(const QString& roomId, JoinState joinState_,
+ const QJsonObject& room_);
+ SyncRoomData(SyncRoomData&&) = default;
+ SyncRoomData& operator=(SyncRoomData&&) = default;
+
+ static const QString UnreadCountKey;
+ };
+ // QVector cannot work with non-copiable objects, std::vector can.
+ using SyncDataList = std::vector<SyncRoomData>;
+
+ class SyncData
+ {
+ public:
+ BaseJob::Status parseJson(const QJsonDocument &data);
+ SyncBatch<Event>&& takeAccountData();
+ SyncDataList&& takeRoomData();
+ QString nextBatch() const;
+
+ private:
+ QString nextBatch_;
+ SyncBatch<Event> accountData { "account_data" };
+ SyncDataList roomData;
+ };
+
+ class SyncJob: public BaseJob
+ {
+ public:
+ explicit SyncJob(const QString& since = {},
+ const QString& filter = {},
+ int timeout = -1, const QString& presence = {});
+
+ SyncData &&takeData() { return std::move(d); }
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ SyncData d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/joinstate.h b/lib/joinstate.h
new file mode 100644
index 00000000..42613895
--- /dev/null
+++ b/lib/joinstate.h
@@ -0,0 +1,48 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include <QtCore/QFlags>
+
+#include <array>
+
+namespace QMatrixClient
+{
+ enum class JoinState
+ {
+ Join = 0x1,
+ Invite = 0x2,
+ Leave = 0x4
+ };
+
+ Q_DECLARE_FLAGS(JoinStates, JoinState)
+
+ // We cannot use REGISTER_ENUM outside of a Q_OBJECT and besides, we want
+ // to use strings that match respective JSON keys.
+ static const std::array<const char*, 3> JoinStateStrings
+ { { "join", "invite", "leave" } };
+
+ inline const char* toCString(JoinState js)
+ {
+ size_t state = size_t(js), index = 0;
+ while (state >>= 1) ++index;
+ return JoinStateStrings[index];
+ }
+} // namespace QMatrixClient
+Q_DECLARE_OPERATORS_FOR_FLAGS(QMatrixClient::JoinStates)
diff --git a/lib/logging.cpp b/lib/logging.cpp
new file mode 100644
index 00000000..7476781f
--- /dev/null
+++ b/lib/logging.cpp
@@ -0,0 +1,33 @@
+/******************************************************************************
+ * Copyright (C) 2017 Elvis Angelaccio <elvid.angelaccio@kde.org>
+ *
+ * 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 "logging.h"
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)
+#define LOGGING_CATEGORY(Name, Id) Q_LOGGING_CATEGORY((Name), (Id), QtInfoMsg)
+#else
+#define LOGGING_CATEGORY(Name, Id) Q_LOGGING_CATEGORY((Name), (Id))
+#endif
+
+// Use LOGGING_CATEGORY instead of Q_LOGGING_CATEGORY in the rest of the code
+LOGGING_CATEGORY(MAIN, "libqmatrixclient.main")
+LOGGING_CATEGORY(PROFILER, "libqmatrixclient.profiler")
+LOGGING_CATEGORY(EVENTS, "libqmatrixclient.events")
+LOGGING_CATEGORY(EPHEMERAL, "libqmatrixclient.events.ephemeral")
+LOGGING_CATEGORY(JOBS, "libqmatrixclient.jobs")
+LOGGING_CATEGORY(SYNCJOB, "libqmatrixclient.jobs.sync")
diff --git a/lib/logging.h b/lib/logging.h
new file mode 100644
index 00000000..8dbfdf30
--- /dev/null
+++ b/lib/logging.h
@@ -0,0 +1,78 @@
+/******************************************************************************
+ * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include <QtCore/QElapsedTimer>
+#include <QtCore/QLoggingCategory>
+
+Q_DECLARE_LOGGING_CATEGORY(MAIN)
+Q_DECLARE_LOGGING_CATEGORY(PROFILER)
+Q_DECLARE_LOGGING_CATEGORY(EVENTS)
+Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL)
+Q_DECLARE_LOGGING_CATEGORY(JOBS)
+Q_DECLARE_LOGGING_CATEGORY(SYNCJOB)
+
+namespace QMatrixClient
+{
+ // QDebug manipulators
+
+ using QDebugManip = QDebug (*)(QDebug);
+
+ /**
+ * @brief QDebug manipulator to setup the stream for JSON output
+ *
+ * Originally made to encapsulate the change in QDebug behavior in Qt 5.4
+ * and the respective addition of QDebug::noquote().
+ * Together with the operator<<() helper, the proposed usage is
+ * (similar to std:: I/O manipulators):
+ *
+ * @example qCDebug() << formatJson << json_object; // (QJsonObject, etc.)
+ */
+ inline QDebug formatJson(QDebug debug_object)
+ {
+#if QT_VERSION < QT_VERSION_CHECK(5, 4, 0)
+ return debug_object;
+#else
+ return debug_object.noquote();
+#endif
+ }
+
+ /**
+ * @brief A helper operator to facilitate usage of formatJson (and possibly
+ * other manipulators)
+ *
+ * @param debug_object to output the json to
+ * @param qdm a QDebug manipulator
+ * @return a copy of debug_object that has its mode altered by qdm
+ */
+ inline QDebug operator<< (QDebug debug_object, QDebugManip qdm)
+ {
+ return qdm(debug_object);
+ }
+}
+
+inline QDebug operator<< (QDebug debug_object, const QElapsedTimer& et)
+{
+ auto val = et.nsecsElapsed() / 1000;
+ if (val < 1000)
+ debug_object << val << "µs";
+ else
+ debug_object << val / 1000 << "ms";
+ return debug_object;
+}
diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp
new file mode 100644
index 00000000..89967a8a
--- /dev/null
+++ b/lib/networkaccessmanager.cpp
@@ -0,0 +1,75 @@
+/******************************************************************************
+ * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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 "networkaccessmanager.h"
+
+#include <QtNetwork/QNetworkReply>
+#include <QtCore/QCoreApplication>
+
+using namespace QMatrixClient;
+
+class NetworkAccessManager::Private
+{
+ public:
+ QList<QSslError> ignoredSslErrors;
+};
+
+NetworkAccessManager::NetworkAccessManager(QObject* parent) : d(std::make_unique<Private>())
+{ }
+
+QList<QSslError> NetworkAccessManager::ignoredSslErrors() const
+{
+ return d->ignoredSslErrors;
+}
+
+void NetworkAccessManager::addIgnoredSslError(const QSslError& error)
+{
+ d->ignoredSslErrors << error;
+}
+
+void NetworkAccessManager::clearIgnoredSslErrors()
+{
+ d->ignoredSslErrors.clear();
+}
+
+static NetworkAccessManager* createNam()
+{
+ auto nam = new NetworkAccessManager(QCoreApplication::instance());
+ // See #109. Once Qt bearer management gets better, this workaround
+ // should become unnecessary.
+ nam->connect(nam, &QNetworkAccessManager::networkAccessibleChanged,
+ [nam] { nam->setNetworkAccessible(QNetworkAccessManager::Accessible); });
+ return nam;
+}
+
+NetworkAccessManager* NetworkAccessManager::instance()
+{
+ static auto* nam = createNam();
+ return nam;
+}
+
+NetworkAccessManager::~NetworkAccessManager() = default;
+
+QNetworkReply* NetworkAccessManager::createRequest(Operation op,
+ const QNetworkRequest& request, QIODevice* outgoingData)
+{
+ auto reply =
+ QNetworkAccessManager::createRequest(op, request, outgoingData);
+ reply->ignoreSslErrors(d->ignoredSslErrors);
+ return reply;
+}
diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h
new file mode 100644
index 00000000..ae847582
--- /dev/null
+++ b/lib/networkaccessmanager.h
@@ -0,0 +1,49 @@
+/******************************************************************************
+ * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include <QtNetwork/QNetworkAccessManager>
+
+#include <memory>
+
+namespace QMatrixClient
+{
+ class NetworkAccessManager : public QNetworkAccessManager
+ {
+ Q_OBJECT
+ public:
+ NetworkAccessManager(QObject* parent = nullptr);
+ ~NetworkAccessManager() override;
+
+ QList<QSslError> ignoredSslErrors() const;
+ void addIgnoredSslError(const QSslError& error);
+ void clearIgnoredSslErrors();
+
+ /** Get a pointer to the singleton */
+ static NetworkAccessManager* instance();
+
+ private:
+ QNetworkReply * createRequest(Operation op,
+ const QNetworkRequest &request,
+ QIODevice *outgoingData = Q_NULLPTR) override;
+
+ class Private;
+ std::unique_ptr<Private> d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp
new file mode 100644
index 00000000..48bd09f3
--- /dev/null
+++ b/lib/networksettings.cpp
@@ -0,0 +1,31 @@
+/******************************************************************************
+ * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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 "networksettings.h"
+
+using namespace QMatrixClient;
+
+void NetworkSettings::setupApplicationProxy() const
+{
+ QNetworkProxy::setApplicationProxy(
+ { proxyType(), proxyHostName(), proxyPort() });
+}
+
+QMC_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType)
+QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", "", setProxyHostName)
+QMC_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort)
diff --git a/lib/networksettings.h b/lib/networksettings.h
new file mode 100644
index 00000000..83613060
--- /dev/null
+++ b/lib/networksettings.h
@@ -0,0 +1,44 @@
+/******************************************************************************
+ * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include "settings.h"
+
+#include <QtNetwork/QNetworkProxy>
+
+Q_DECLARE_METATYPE(QNetworkProxy::ProxyType)
+
+namespace QMatrixClient {
+ class NetworkSettings: public SettingsGroup
+ {
+ Q_OBJECT
+ QMC_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType)
+ QMC_DECLARE_SETTING(QString, proxyHostName, setProxyHostName)
+ QMC_DECLARE_SETTING(quint16, proxyPort, setProxyPort)
+ Q_PROPERTY(QString proxyHost READ proxyHostName WRITE setProxyHostName)
+ public:
+ template <typename... ArgTs>
+ explicit NetworkSettings(ArgTs... qsettingsArgs)
+ : SettingsGroup(QStringLiteral("Network"), qsettingsArgs...)
+ { }
+ ~NetworkSettings() override = default;
+
+ Q_INVOKABLE void setupApplicationProxy() const;
+ };
+}
diff --git a/lib/room.cpp b/lib/room.cpp
new file mode 100644
index 00000000..25669889
--- /dev/null
+++ b/lib/room.cpp
@@ -0,0 +1,1851 @@
+/******************************************************************************
+ * 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 "room.h"
+
+#include "jobs/generated/kicking.h"
+#include "jobs/generated/inviting.h"
+#include "jobs/generated/banning.h"
+#include "jobs/generated/leaving.h"
+#include "jobs/generated/receipts.h"
+#include "jobs/generated/redaction.h"
+#include "jobs/generated/account-data.h"
+#include "jobs/setroomstatejob.h"
+#include "events/simplestateevents.h"
+#include "events/roomavatarevent.h"
+#include "events/roommemberevent.h"
+#include "events/typingevent.h"
+#include "events/receiptevent.h"
+#include "events/redactionevent.h"
+#include "jobs/sendeventjob.h"
+#include "jobs/roommessagesjob.h"
+#include "jobs/mediathumbnailjob.h"
+#include "jobs/downloadfilejob.h"
+#include "jobs/postreadmarkersjob.h"
+#include "avatar.h"
+#include "connection.h"
+#include "user.h"
+
+#include <QtCore/QHash>
+#include <QtCore/QStringBuilder> // for efficient string concats (operator%)
+#include <QtCore/QElapsedTimer>
+#include <QtCore/QPointer>
+#include <QtCore/QDir>
+#include <QtCore/QTemporaryFile>
+#include <QtCore/QRegularExpression>
+
+#include <array>
+#include <functional>
+#include <cmath>
+
+using namespace QMatrixClient;
+using namespace std::placeholders;
+#if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123)
+using std::llround;
+#endif
+
+enum EventsPlacement : int { Older = -1, Newer = 1 };
+
+// A workaround for MSVC 2015 that fails with "error C2440: 'return':
+// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'"
+#if (defined(_MSC_VER) && _MSC_VER < 1910) || (defined(__GNUC__) && __GNUC__ <= 4)
+# define WORKAROUND_EXTENDED_INITIALIZER_LIST
+#endif
+
+class Room::Private
+{
+ public:
+ /** Map of user names to users. User names potentially duplicate, hence a multi-hashmap. */
+ typedef QMultiHash<QString, User*> members_map_t;
+
+ Private(Connection* c, QString id_, JoinState initialJoinState)
+ : q(nullptr), connection(c), id(std::move(id_))
+ , joinState(initialJoinState)
+ { }
+
+ Room* q;
+
+ // This updates the room displayname field (which is the way a room
+ // should be shown in the room list) It should be called whenever the
+ // list of members or the room name (m.room.name) or canonical alias change.
+ void updateDisplayname();
+
+ Connection* connection;
+ Timeline timeline;
+ QHash<QString, TimelineItem::index_t> eventsIndex;
+ QString id;
+ QStringList aliases;
+ QString canonicalAlias;
+ QString name;
+ QString displayname;
+ QString topic;
+ QString encryptionAlgorithm;
+ Avatar avatar;
+ JoinState joinState;
+ int highlightCount = 0;
+ int notificationCount = 0;
+ members_map_t membersMap;
+ QList<User*> usersTyping;
+ QList<User*> membersLeft;
+ int unreadMessages = 0;
+ bool displayed = false;
+ QString firstDisplayedEventId;
+ QString lastDisplayedEventId;
+ QHash<const User*, QString> lastReadEventIds;
+ QString serverReadMarker;
+ TagsMap tags;
+ QHash<QString, QVariantHash> accountData;
+ QString prevBatch;
+ QPointer<RoomMessagesJob> roomMessagesJob;
+
+ struct FileTransferPrivateInfo
+ {
+#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST
+ FileTransferPrivateInfo() = default;
+ FileTransferPrivateInfo(BaseJob* j, QString fileName)
+ : job(j), localFileInfo(fileName)
+ { }
+#endif
+ QPointer<BaseJob> job = nullptr;
+ QFileInfo localFileInfo { };
+ FileTransferInfo::Status status = FileTransferInfo::Started;
+ qint64 progress = 0;
+ qint64 total = -1;
+
+ void update(qint64 p, qint64 t)
+ {
+ if (t == 0)
+ {
+ t = -1;
+ if (p == 0)
+ p = -1;
+ }
+ if (p != -1)
+ qCDebug(PROFILER) << "Transfer progress:" << p << "/" << t
+ << "=" << llround(double(p) / t * 100) << "%";
+ progress = p; total = t;
+ }
+ };
+ void failedTransfer(const QString& tid, const QString& errorMessage = {})
+ {
+ qCWarning(MAIN) << "File transfer failed for id" << tid;
+ if (!errorMessage.isEmpty())
+ qCWarning(MAIN) << "Message:" << errorMessage;
+ fileTransfers[tid].status = FileTransferInfo::Failed;
+ emit q->fileTransferFailed(tid, errorMessage);
+ }
+ // A map from event/txn ids to information about the long operation;
+ // used for both download and upload operations
+ QHash<QString, FileTransferPrivateInfo> fileTransfers;
+
+ const RoomMessageEvent* getEventWithFile(const QString& eventId) const;
+ QString fileNameToDownload(const RoomMessageEvent* event) const;
+
+ //void inviteUser(User* u); // We might get it at some point in time.
+ void insertMemberIntoMap(User* u);
+ void renameMember(User* u, QString oldName);
+ void removeMemberFromMap(const QString& username, User* u);
+
+ void getPreviousContent(int limit = 10);
+
+ bool isEventNotable(const TimelineItem& ti) const
+ {
+ return !ti->isRedacted() &&
+ ti->senderId() != connection->userId() &&
+ ti->type() == EventType::RoomMessage;
+ }
+
+ void addNewMessageEvents(RoomEvents&& events);
+ void addHistoricalMessageEvents(RoomEvents&& events);
+
+ /**
+ * @brief Move events into the timeline
+ *
+ * Insert events into the timeline, either new or historical.
+ * Pointers in the original container become empty, the ownership
+ * is passed to the timeline container.
+ * @param events - the range of events to be inserted
+ * @param placement - position and direction of insertion: Older for
+ * historical messages, Newer for new ones
+ */
+ Timeline::size_type insertEvents(RoomEventsRange&& events,
+ EventsPlacement placement);
+
+ /**
+ * Removes events from the passed container that are already in the timeline
+ */
+ void dropDuplicateEvents(RoomEvents* events) const;
+
+ void setLastReadEvent(User* u, const QString& eventId);
+ void updateUnreadCount(rev_iter_t from, rev_iter_t to);
+ void promoteReadMarker(User* u, rev_iter_t newMarker,
+ bool force = false);
+
+ void markMessagesAsRead(rev_iter_t upToMarker);
+
+ /**
+ * @brief Apply redaction to the timeline
+ *
+ * Tries to find an event in the timeline and redact it; deletes the
+ * redaction event whether the redacted event was found or not.
+ */
+ void processRedaction(RoomEventPtr redactionEvent);
+
+ void broadcastTagUpdates()
+ {
+ connection->callApi<SetAccountDataPerRoomJob>(
+ connection->userId(), id, TagEvent::typeId(),
+ TagEvent(tags).toJson());
+ emit q->tagsChanged();
+ }
+
+ QJsonObject toJson() const;
+
+ private:
+ QString calculateDisplayname() const;
+ QString roomNameFromMemberNames(const QList<User*>& userlist) const;
+
+ bool isLocalUser(const User* u) const
+ {
+ return u == q->localUser();
+ }
+};
+
+RoomEventPtr TimelineItem::replaceEvent(RoomEventPtr&& other)
+{
+ return std::exchange(evt, std::move(other));
+}
+
+Room::Room(Connection* connection, QString id, JoinState initialJoinState)
+ : QObject(connection), d(new Private(connection, id, initialJoinState))
+{
+ setObjectName(id);
+ // See "Accessing the Public Class" section in
+ // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/
+ d->q = this;
+ connect(this, &Room::userAdded, this, &Room::memberListChanged);
+ connect(this, &Room::userRemoved, this, &Room::memberListChanged);
+ connect(this, &Room::memberRenamed, this, &Room::memberListChanged);
+ qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id;
+}
+
+Room::~Room()
+{
+ delete d;
+}
+
+const QString& Room::id() const
+{
+ return d->id;
+}
+
+const Room::Timeline& Room::messageEvents() const
+{
+ return d->timeline;
+}
+
+QString Room::name() const
+{
+ return d->name;
+}
+
+QStringList Room::aliases() const
+{
+ return d->aliases;
+}
+
+QString Room::canonicalAlias() const
+{
+ return d->canonicalAlias;
+}
+
+QString Room::displayName() const
+{
+ return d->displayname;
+}
+
+QString Room::topic() const
+{
+ return d->topic;
+}
+
+QString Room::avatarMediaId() const
+{
+ return d->avatar.mediaId();
+}
+
+QUrl Room::avatarUrl() const
+{
+ return d->avatar.url();
+}
+
+QImage Room::avatar(int dimension)
+{
+ return avatar(dimension, dimension);
+}
+
+QImage Room::avatar(int width, int height)
+{
+ if (!d->avatar.url().isEmpty())
+ return d->avatar.get(connection(), width, height, [=] { emit avatarChanged(); });
+
+ // Use the other side's avatar for 1:1's
+ if (d->membersMap.size() == 2)
+ {
+ auto theOtherOneIt = d->membersMap.begin();
+ if (theOtherOneIt.value() == localUser())
+ ++theOtherOneIt;
+ return (*theOtherOneIt)->avatar(width, height, this,
+ [=] { emit avatarChanged(); });
+ }
+ return {};
+}
+
+User* Room::user(const QString& userId) const
+{
+ return connection()->user(userId);
+}
+
+JoinState Room::memberJoinState(User* user) const
+{
+ return
+ d->membersMap.contains(user->name(this), user) ? JoinState::Join :
+ JoinState::Leave;
+}
+
+JoinState Room::joinState() const
+{
+ return d->joinState;
+}
+
+void Room::setJoinState(JoinState state)
+{
+ JoinState oldState = d->joinState;
+ if( state == oldState )
+ return;
+ d->joinState = state;
+ qCDebug(MAIN) << "Room" << id() << "changed state: "
+ << int(oldState) << "->" << int(state);
+ emit joinStateChanged(oldState, state);
+}
+
+void Room::Private::setLastReadEvent(User* u, const QString& eventId)
+{
+ auto& storedId = lastReadEventIds[u];
+ if (storedId == eventId)
+ return;
+ storedId = eventId;
+ emit q->lastReadEventChanged(u);
+ if (isLocalUser(u))
+ {
+ if (eventId != serverReadMarker)
+ connection->callApi<PostReadMarkersJob>(id, eventId);
+ emit q->readMarkerMoved();
+ }
+}
+
+void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to)
+{
+ Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend());
+ Q_ASSERT(to >= from && to <= timeline.crend());
+
+ // Catch a special case when the last read event id refers to an event
+ // that has just arrived. In this case we should recalculate
+ // unreadMessages and might need to promote the read marker further
+ // over local-origin messages.
+ const auto readMarker = q->readMarker();
+ if (readMarker >= from && readMarker < to)
+ {
+ qCDebug(MAIN) << "Discovered last read event in room" << displayname;
+ promoteReadMarker(q->localUser(), readMarker, true);
+ return;
+ }
+
+ Q_ASSERT(to <= readMarker);
+
+ QElapsedTimer et; et.start();
+ const auto newUnreadMessages = count_if(from, to,
+ std::bind(&Room::Private::isEventNotable, this, _1));
+ if (et.nsecsElapsed() > 10000)
+ qCDebug(PROFILER) << "Counting gained unread messages took" << et;
+
+ if(newUnreadMessages > 0)
+ {
+ // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count
+ if (unreadMessages < 0)
+ unreadMessages = 0;
+
+ unreadMessages += newUnreadMessages;
+ qCDebug(MAIN) << "Room" << displayname << "has gained"
+ << newUnreadMessages << "unread message(s),"
+ << (q->readMarker() == timeline.crend() ?
+ "in total at least" : "in total")
+ << unreadMessages << "unread message(s)";
+ emit q->unreadMessagesChanged(q);
+ }
+}
+
+void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force)
+{
+ Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr");
+ Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend());
+
+ const auto prevMarker = q->readMarker(u);
+ if (!force && prevMarker <= newMarker) // Remember, we deal with reverse iterators
+ return;
+
+ Q_ASSERT(newMarker < timeline.crend());
+
+ // Try to auto-promote the read marker over the user's own messages
+ // (switch to direct iterators for that).
+ auto eagerMarker = find_if(newMarker.base(), timeline.cend(),
+ [=](const TimelineItem& ti) { return ti->senderId() != u->id(); });
+
+ setLastReadEvent(u, (*(eagerMarker - 1))->id());
+ if (isLocalUser(u))
+ {
+ const auto oldUnreadCount = unreadMessages;
+ QElapsedTimer et; et.start();
+ unreadMessages = count_if(eagerMarker, timeline.cend(),
+ std::bind(&Room::Private::isEventNotable, this, _1));
+ if (et.nsecsElapsed() > 10000)
+ qCDebug(PROFILER) << "Recounting unread messages took" << et;
+
+ // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count
+ if (unreadMessages == 0)
+ unreadMessages = -1;
+
+ if (force || unreadMessages != oldUnreadCount)
+ {
+ if (unreadMessages == -1)
+ {
+ qCDebug(MAIN) << "Room" << displayname
+ << "has no more unread messages";
+ } else
+ qCDebug(MAIN) << "Room" << displayname << "still has"
+ << unreadMessages << "unread message(s)";
+ emit q->unreadMessagesChanged(q);
+ }
+ }
+}
+
+void Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
+{
+ const auto prevMarker = q->readMarker();
+ promoteReadMarker(q->localUser(), upToMarker);
+ if (prevMarker != upToMarker)
+ qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker();
+
+ // We shouldn't send read receipts for the local user's own messages - so
+ // search earlier messages for the latest message not from the local user
+ // until the previous last-read message, whichever comes first.
+ for (; upToMarker < prevMarker; ++upToMarker)
+ {
+ if ((*upToMarker)->senderId() != q->localUser()->id())
+ {
+ connection->callApi<PostReceiptJob>(id, "m.read",
+ (*upToMarker)->id());
+ break;
+ }
+ }
+}
+
+void Room::markMessagesAsRead(QString uptoEventId)
+{
+ d->markMessagesAsRead(findInTimeline(uptoEventId));
+}
+
+void Room::markAllMessagesAsRead()
+{
+ if (!d->timeline.empty())
+ d->markMessagesAsRead(d->timeline.crbegin());
+}
+
+bool Room::hasUnreadMessages() const
+{
+ return unreadCount() >= 0;
+}
+
+int Room::unreadCount() const
+{
+ return d->unreadMessages;
+}
+
+Room::rev_iter_t Room::timelineEdge() const
+{
+ return d->timeline.crend();
+}
+
+TimelineItem::index_t Room::minTimelineIndex() const
+{
+ return d->timeline.empty() ? 0 : d->timeline.front().index();
+}
+
+TimelineItem::index_t Room::maxTimelineIndex() const
+{
+ return d->timeline.empty() ? 0 : d->timeline.back().index();
+}
+
+bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const
+{
+ return !d->timeline.empty() &&
+ timelineIndex >= minTimelineIndex() &&
+ timelineIndex <= maxTimelineIndex();
+}
+
+Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const
+{
+ return timelineEdge() -
+ (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0);
+}
+
+Room::rev_iter_t Room::findInTimeline(const QString& evtId) const
+{
+ if (!d->timeline.empty() && d->eventsIndex.contains(evtId))
+ return findInTimeline(d->eventsIndex.value(evtId));
+ return timelineEdge();
+}
+
+bool Room::displayed() const
+{
+ return d->displayed;
+}
+
+void Room::setDisplayed(bool displayed)
+{
+ if (d->displayed == displayed)
+ return;
+
+ d->displayed = displayed;
+ emit displayedChanged(displayed);
+ if( displayed )
+ {
+ resetHighlightCount();
+ resetNotificationCount();
+ }
+}
+
+QString Room::firstDisplayedEventId() const
+{
+ return d->firstDisplayedEventId;
+}
+
+Room::rev_iter_t Room::firstDisplayedMarker() const
+{
+ return findInTimeline(firstDisplayedEventId());
+}
+
+void Room::setFirstDisplayedEventId(const QString& eventId)
+{
+ if (d->firstDisplayedEventId == eventId)
+ return;
+
+ d->firstDisplayedEventId = eventId;
+ emit firstDisplayedEventChanged();
+}
+
+void Room::setFirstDisplayedEvent(TimelineItem::index_t index)
+{
+ Q_ASSERT(isValidIndex(index));
+ setFirstDisplayedEventId(findInTimeline(index)->event()->id());
+}
+
+QString Room::lastDisplayedEventId() const
+{
+ return d->lastDisplayedEventId;
+}
+
+Room::rev_iter_t Room::lastDisplayedMarker() const
+{
+ return findInTimeline(lastDisplayedEventId());
+}
+
+void Room::setLastDisplayedEventId(const QString& eventId)
+{
+ if (d->lastDisplayedEventId == eventId)
+ return;
+
+ d->lastDisplayedEventId = eventId;
+ emit lastDisplayedEventChanged();
+}
+
+void Room::setLastDisplayedEvent(TimelineItem::index_t index)
+{
+ Q_ASSERT(isValidIndex(index));
+ setLastDisplayedEventId(findInTimeline(index)->event()->id());
+}
+
+Room::rev_iter_t Room::readMarker(const User* user) const
+{
+ Q_ASSERT(user);
+ return findInTimeline(d->lastReadEventIds.value(user));
+}
+
+Room::rev_iter_t Room::readMarker() const
+{
+ return readMarker(localUser());
+}
+
+QString Room::readMarkerEventId() const
+{
+ return d->lastReadEventIds.value(localUser());
+}
+
+int Room::notificationCount() const
+{
+ return d->notificationCount;
+}
+
+void Room::resetNotificationCount()
+{
+ if( d->notificationCount == 0 )
+ return;
+ d->notificationCount = 0;
+ emit notificationCountChanged(this);
+}
+
+int Room::highlightCount() const
+{
+ return d->highlightCount;
+}
+
+void Room::resetHighlightCount()
+{
+ if( d->highlightCount == 0 )
+ return;
+ d->highlightCount = 0;
+ emit highlightCountChanged(this);
+}
+
+QStringList Room::tagNames() const
+{
+ return d->tags.keys();
+}
+
+TagsMap Room::tags() const
+{
+ return d->tags;
+}
+
+TagRecord Room::tag(const QString& name) const
+{
+ return d->tags.value(name);
+}
+
+void Room::addTag(const QString& name, const TagRecord& record)
+{
+ if (d->tags.contains(name))
+ return;
+
+ d->tags.insert(name, record);
+ d->broadcastTagUpdates();
+}
+
+void Room::removeTag(const QString& name)
+{
+ if (!d->tags.contains(name))
+ return;
+
+ d->tags.remove(name);
+ d->broadcastTagUpdates();
+}
+
+void Room::setTags(const TagsMap& newTags)
+{
+ if (newTags == d->tags)
+ return;
+ d->tags = newTags;
+ d->broadcastTagUpdates();
+}
+
+bool Room::isFavourite() const
+{
+ return d->tags.contains(FavouriteTag);
+}
+
+bool Room::isLowPriority() const
+{
+ return d->tags.contains(LowPriorityTag);
+}
+
+bool Room::isDirectChat() const
+{
+ return connection()->isDirectChat(id());
+}
+
+QList<const User*> Room::directChatUsers() const
+{
+ return connection()->directChatUsers(this);
+}
+
+const RoomMessageEvent*
+Room::Private::getEventWithFile(const QString& eventId) const
+{
+ auto evtIt = q->findInTimeline(eventId);
+ if (evtIt != timeline.rend() &&
+ evtIt->event()->type() == EventType::RoomMessage)
+ {
+ auto* event = static_cast<const RoomMessageEvent*>(evtIt->event());
+ if (event->hasFileContent())
+ return event;
+ }
+ qWarning() << "No files to download in event" << eventId;
+ return nullptr;
+}
+
+QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const
+{
+ Q_ASSERT(event->hasFileContent());
+ const auto* fileInfo = event->content()->fileInfo();
+ QString fileName;
+ if (!fileInfo->originalName.isEmpty())
+ {
+ fileName = QFileInfo(fileInfo->originalName).fileName();
+ }
+ else if (!event->plainBody().isEmpty())
+ {
+ // Having no better options, assume that the body has
+ // the original file URL or at least the file name.
+ QUrl u { event->plainBody() };
+ if (u.isValid())
+ fileName = QFileInfo(u.path()).fileName();
+ }
+ // Check the file name for sanity
+ if (fileName.isEmpty() || !QTemporaryFile(fileName).open())
+ return "file." % fileInfo->mimeType.preferredSuffix();
+
+ if (QSysInfo::productType() == "windows")
+ {
+ const auto& suffixes = fileInfo->mimeType.suffixes();
+ if (!suffixes.isEmpty() &&
+ std::none_of(suffixes.begin(), suffixes.end(),
+ [&fileName] (const QString& s) {
+ return fileName.endsWith(s); }))
+ return fileName % '.' % fileInfo->mimeType.preferredSuffix();
+ }
+ return fileName;
+}
+
+QUrl Room::urlToThumbnail(const QString& eventId)
+{
+ if (auto* event = d->getEventWithFile(eventId))
+ if (event->hasThumbnail())
+ {
+ auto* thumbnail = event->content()->thumbnailInfo();
+ Q_ASSERT(thumbnail != nullptr);
+ return MediaThumbnailJob::makeRequestUrl(connection()->homeserver(),
+ thumbnail->url, thumbnail->imageSize);
+ }
+ qDebug() << "Event" << eventId << "has no thumbnail";
+ return {};
+}
+
+QUrl Room::urlToDownload(const QString& eventId)
+{
+ if (auto* event = d->getEventWithFile(eventId))
+ {
+ auto* fileInfo = event->content()->fileInfo();
+ Q_ASSERT(fileInfo != nullptr);
+ return DownloadFileJob::makeRequestUrl(connection()->homeserver(),
+ fileInfo->url);
+ }
+ return {};
+}
+
+QString Room::fileNameToDownload(const QString& eventId)
+{
+ if (auto* event = d->getEventWithFile(eventId))
+ return d->fileNameToDownload(event);
+ return {};
+}
+
+FileTransferInfo Room::fileTransferInfo(const QString& id) const
+{
+ auto infoIt = d->fileTransfers.find(id);
+ if (infoIt == d->fileTransfers.end())
+ return {};
+
+ // FIXME: Add lib tests to make sure FileTransferInfo::status stays
+ // consistent with FileTransferInfo::job
+
+ qint64 progress = infoIt->progress;
+ qint64 total = infoIt->total;
+ if (total > INT_MAX)
+ {
+ // JavaScript doesn't deal with 64-bit integers; scale down if necessary
+ progress = llround(double(progress) / total * INT_MAX);
+ total = INT_MAX;
+ }
+
+#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST
+ FileTransferInfo fti;
+ fti.status = infoIt->status;
+ fti.progress = int(progress);
+ fti.total = int(total);
+ fti.localDir = QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath());
+ fti.localPath = QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath());
+ return fti;
+#else
+ return { infoIt->status, int(progress), int(total),
+ QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()),
+ QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath())
+ };
+#endif
+}
+
+static const auto RegExpOptions =
+ QRegularExpression::CaseInsensitiveOption
+ | QRegularExpression::OptimizeOnFirstUsageOption
+ | QRegularExpression::UseUnicodePropertiesOption;
+
+// regexp is originally taken from Konsole (https://github.com/KDE/konsole)
+// full url:
+// protocolname:// or www. followed by anything other than whitespaces,
+// <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, ), :,
+// comma or dot
+// Note: outer parentheses are a part of C++ raw string delimiters, not of
+// the regex (see http://en.cppreference.com/w/cpp/language/string_literal).
+static const QRegularExpression FullUrlRegExp(QStringLiteral(
+ R"(((www\.(?!\.)|[a-z][a-z0-9+.-]*://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"
+ ), RegExpOptions);
+// email address:
+// [word chars, dots or dashes]@[word chars, dots or dashes].[word chars]
+static const QRegularExpression EmailAddressRegExp(QStringLiteral(
+ R"((mailto:)?(\b(\w|\.|-)+@(\w|\.|-)+\.\w+\b))"
+ ), RegExpOptions);
+
+/** Converts all that looks like a URL into HTML links */
+static void linkifyUrls(QString& htmlEscapedText)
+{
+ // NOTE: htmlEscapedText is already HTML-escaped (no literal <,>,&)!
+
+ htmlEscapedText.replace(EmailAddressRegExp,
+ QStringLiteral(R"(<a href="mailto:\2">\1\2</a>)"));
+ htmlEscapedText.replace(FullUrlRegExp,
+ QStringLiteral(R"(<a href="\1">\1</a>)"));
+}
+
+QString Room::prettyPrint(const QString& plainText) const
+{
+ auto pt = QStringLiteral("<span style='white-space:pre-wrap'>") +
+ plainText.toHtmlEscaped() + QStringLiteral("</span>");
+ pt.replace('\n', "<br/>");
+
+ linkifyUrls(pt);
+ return pt;
+}
+
+QList< User* > Room::usersTyping() const
+{
+ return d->usersTyping;
+}
+
+QList< User* > Room::membersLeft() const
+{
+ return d->membersLeft;
+}
+
+QList< User* > Room::users() const
+{
+ return d->membersMap.values();
+}
+
+QStringList Room::memberNames() const
+{
+ QStringList res;
+ for (auto u : d->membersMap)
+ res.append( roomMembername(u) );
+
+ return res;
+}
+
+int Room::memberCount() const
+{
+ return d->membersMap.size();
+}
+
+int Room::timelineSize() const
+{
+ return int(d->timeline.size());
+}
+
+bool Room::usesEncryption() const
+{
+ return !d->encryptionAlgorithm.isEmpty();
+}
+
+void Room::Private::insertMemberIntoMap(User *u)
+{
+ const auto userName = u->name(q);
+ // If there is exactly one namesake of the added user, signal member renaming
+ // for that other one because the two should be disambiguated now.
+ auto namesakes = membersMap.values(userName);
+ if (namesakes.size() == 1)
+ emit q->memberAboutToRename(namesakes.front(),
+ namesakes.front()->fullName(q));
+ membersMap.insert(userName, u);
+ if (namesakes.size() == 1)
+ emit q->memberRenamed(namesakes.front());
+}
+
+void Room::Private::renameMember(User* u, QString oldName)
+{
+ if (u->name(q) == oldName)
+ {
+ qCWarning(MAIN) << "Room::Private::renameMember(): the user "
+ << u->fullName(q)
+ << "is already known in the room under a new name.";
+ }
+ else if (membersMap.contains(oldName, u))
+ {
+ removeMemberFromMap(oldName, u);
+ insertMemberIntoMap(u);
+ }
+ emit q->memberRenamed(u);
+}
+
+void Room::Private::removeMemberFromMap(const QString& username, User* u)
+{
+ User* namesake = nullptr;
+ auto namesakes = membersMap.values(username);
+ if (namesakes.size() == 2)
+ {
+ namesake = namesakes.front() == u ? namesakes.back() : namesakes.front();
+ Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken");
+ emit q->memberAboutToRename(namesake, username);
+ }
+ membersMap.remove(username, u);
+ // If there was one namesake besides the removed user, signal member renaming
+ // for it because it doesn't need to be disambiguated anymore.
+ // TODO: Think about left users.
+ if (namesake)
+ emit q->memberRenamed(namesake);
+}
+
+inline auto makeErrorStr(const Event& e, QByteArray msg)
+{
+ return msg.append("; event dump follows:\n").append(e.originalJson());
+}
+
+Room::Timeline::size_type Room::Private::insertEvents(RoomEventsRange&& events,
+ EventsPlacement placement)
+{
+ // Historical messages arrive in newest-to-oldest order, so the process for
+ // them is symmetric to the one for new messages.
+ auto index = timeline.empty() ? -int(placement) :
+ placement == Older ? timeline.front().index() :
+ timeline.back().index();
+ auto baseIndex = index;
+ for (auto&& e: events)
+ {
+ const auto eId = e->id();
+ Q_ASSERT_X(e, __FUNCTION__, "Attempt to add nullptr to timeline");
+ Q_ASSERT_X(!eId.isEmpty(), __FUNCTION__,
+ makeErrorStr(*e,
+ "Event with empty id cannot be in the timeline"));
+ Q_ASSERT_X(!eventsIndex.contains(eId), __FUNCTION__,
+ makeErrorStr(*e, "Event is already in the timeline; "
+ "incoming events were not properly deduplicated"));
+ if (placement == Older)
+ timeline.emplace_front(move(e), --index);
+ else
+ timeline.emplace_back(move(e), ++index);
+ eventsIndex.insert(eId, index);
+ Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId);
+ }
+ // Pointers in "events" are empty now, but events.size() didn't change
+ Q_ASSERT(int(events.size()) == (index - baseIndex) * int(placement));
+ return events.size();
+}
+
+QString Room::roomMembername(const User* u) const
+{
+ // See the CS spec, section 11.2.2.3
+
+ const auto username = u->name(this);
+ if (username.isEmpty())
+ return u->id();
+
+ // Get the list of users with the same display name. Most likely,
+ // there'll be one, but there's a chance there are more.
+ if (d->membersMap.count(username) == 1)
+ return username;
+
+ // We expect a user to be a member of the room - but technically it is
+ // possible to invoke roomMemberName() even for non-members. In such case
+ // we return the name _with_ id, to stay on a safe side.
+ // XXX: Causes a storm of false alarms when scrolling through older events
+ // with left users; commented out until we have a proper backtracking of
+ // room state ("room time machine").
+// if ( !namesakes.contains(u) )
+// {
+// qCWarning()
+// << "Room::roomMemberName(): user" << u->id()
+// << "is not a member of the room" << id();
+// }
+
+ // In case of more than one namesake, use the full name to disambiguate
+ return u->fullName(this);
+}
+
+QString Room::roomMembername(const QString& userId) const
+{
+ return roomMembername(user(userId));
+}
+
+void Room::updateData(SyncRoomData&& data)
+{
+ if( d->prevBatch.isEmpty() )
+ d->prevBatch = data.timelinePrevBatch;
+ setJoinState(data.joinState);
+
+ QElapsedTimer et; et.start();
+ for (auto&& event: data.accountData)
+ processAccountDataEvent(move(event));
+
+ if (!data.state.empty())
+ {
+ et.restart();
+ processStateEvents(data.state);
+ qCDebug(PROFILER) << "*** Room::processStateEvents(state):"
+ << data.state.size() << "event(s)," << et;
+ }
+ if (!data.timeline.empty())
+ {
+ et.restart();
+ // State changes can arrive in a timeline event; so check those.
+ processStateEvents(data.timeline);
+ qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):"
+ << data.timeline.size() << "event(s)," << et;
+
+ et.restart();
+ d->addNewMessageEvents(move(data.timeline));
+ qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et;
+ }
+ for( auto&& ephemeralEvent: data.ephemeral )
+ processEphemeralEvent(move(ephemeralEvent));
+
+ // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count
+ if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages)
+ {
+ qCDebug(MAIN) << "Setting unread_count to" << data.unreadCount;
+ d->unreadMessages = data.unreadCount;
+ emit unreadMessagesChanged(this);
+ }
+
+ if( data.highlightCount != d->highlightCount )
+ {
+ d->highlightCount = data.highlightCount;
+ emit highlightCountChanged(this);
+ }
+ if( data.notificationCount != d->notificationCount )
+ {
+ d->notificationCount = data.notificationCount;
+ emit notificationCountChanged(this);
+ }
+}
+
+void Room::postMessage(const QString& type, const QString& plainText)
+{
+ postMessage(RoomMessageEvent { plainText, type });
+}
+
+void Room::postMessage(const QString& plainText, MessageEventType type)
+{
+ postMessage(RoomMessageEvent { plainText, type });
+}
+
+void Room::postMessage(const RoomMessageEvent& event)
+{
+ if (usesEncryption())
+ {
+ qCCritical(MAIN) << "Room" << displayName()
+ << "enforces encryption; sending encrypted messages is not supported yet";
+ }
+ connection()->callApi<SendEventJob>(id(), event);
+}
+
+void Room::setName(const QString& newName)
+{
+ connection()->callApi<SetRoomStateJob>(id(), RoomNameEvent(newName));
+}
+
+void Room::setCanonicalAlias(const QString& newAlias)
+{
+ connection()->callApi<SetRoomStateJob>(id(),
+ RoomCanonicalAliasEvent(newAlias));
+}
+
+void Room::setTopic(const QString& newTopic)
+{
+ RoomTopicEvent evt(newTopic);
+ connection()->callApi<SetRoomStateJob>(id(), evt);
+}
+
+void Room::getPreviousContent(int limit)
+{
+ d->getPreviousContent(limit);
+}
+
+void Room::Private::getPreviousContent(int limit)
+{
+ if( !isJobRunning(roomMessagesJob) )
+ {
+ roomMessagesJob =
+ connection->callApi<RoomMessagesJob>(id, prevBatch, limit);
+ connect( roomMessagesJob, &RoomMessagesJob::success, [=] {
+ prevBatch = roomMessagesJob->end();
+ addHistoricalMessageEvents(roomMessagesJob->releaseEvents());
+ });
+ }
+}
+
+void Room::inviteToRoom(const QString& memberId)
+{
+ connection()->callApi<InviteUserJob>(id(), memberId);
+}
+
+LeaveRoomJob* Room::leaveRoom()
+{
+ return connection()->callApi<LeaveRoomJob>(id());
+}
+
+void Room::kickMember(const QString& memberId, const QString& reason)
+{
+ connection()->callApi<KickJob>(id(), memberId, reason);
+}
+
+void Room::ban(const QString& userId, const QString& reason)
+{
+ connection()->callApi<BanJob>(id(), userId, reason);
+}
+
+void Room::unban(const QString& userId)
+{
+ connection()->callApi<UnbanJob>(id(), userId);
+}
+
+void Room::redactEvent(const QString& eventId, const QString& reason)
+{
+ connection()->callApi<RedactEventJob>(
+ id(), eventId, connection()->generateTxnId(), reason);
+}
+
+void Room::uploadFile(const QString& id, const QUrl& localFilename,
+ const QString& overrideContentType)
+{
+ Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__,
+ "localFilename should point at a local file");
+ auto fileName = localFilename.toLocalFile();
+ auto job = connection()->uploadFile(fileName, overrideContentType);
+ if (isJobRunning(job))
+ {
+ d->fileTransfers.insert(id, { job, fileName });
+ connect(job, &BaseJob::uploadProgress, this,
+ [this,id] (qint64 sent, qint64 total) {
+ d->fileTransfers[id].update(sent, total);
+ emit fileTransferProgress(id, sent, total);
+ });
+ connect(job, &BaseJob::success, this, [this,id,localFilename,job] {
+ d->fileTransfers[id].status = FileTransferInfo::Completed;
+ emit fileTransferCompleted(id, localFilename, job->contentUri());
+ });
+ connect(job, &BaseJob::failure, this,
+ std::bind(&Private::failedTransfer, d, id, job->errorString()));
+ emit newFileTransfer(id, localFilename);
+ } else
+ d->failedTransfer(id);
+}
+
+void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
+{
+ auto ongoingTransfer = d->fileTransfers.find(eventId);
+ if (ongoingTransfer != d->fileTransfers.end() &&
+ ongoingTransfer->status == FileTransferInfo::Started)
+ {
+ qCWarning(MAIN) << "Download for" << eventId
+ << "already started; to restart, cancel it first";
+ return;
+ }
+
+ Q_ASSERT_X(localFilename.isEmpty() || localFilename.isLocalFile(),
+ __FUNCTION__, "localFilename should point at a local file");
+ const auto* event = d->getEventWithFile(eventId);
+ if (!event)
+ {
+ qCCritical(MAIN)
+ << eventId << "is not in the local timeline or has no file content";
+ Q_ASSERT(false);
+ return;
+ }
+ const auto fileUrl = event->content()->fileInfo()->url;
+ auto filePath = localFilename.toLocalFile();
+ if (filePath.isEmpty())
+ {
+ // Build our own file path, starting with temp directory and eventId.
+ filePath = eventId;
+ filePath = QDir::tempPath() % '/' % filePath.replace(':', '_') %
+ '#' % d->fileNameToDownload(event);
+ }
+ auto job = connection()->downloadFile(fileUrl, filePath);
+ if (isJobRunning(job))
+ {
+ // If there was a previous transfer (completed or failed), remove it.
+ d->fileTransfers.remove(eventId);
+ d->fileTransfers.insert(eventId, { job, job->targetFileName() });
+ connect(job, &BaseJob::downloadProgress, this,
+ [this,eventId] (qint64 received, qint64 total) {
+ d->fileTransfers[eventId].update(received, total);
+ emit fileTransferProgress(eventId, received, total);
+ });
+ connect(job, &BaseJob::success, this, [this,eventId,fileUrl,job] {
+ d->fileTransfers[eventId].status = FileTransferInfo::Completed;
+ emit fileTransferCompleted(eventId, fileUrl,
+ QUrl::fromLocalFile(job->targetFileName()));
+ });
+ connect(job, &BaseJob::failure, this,
+ std::bind(&Private::failedTransfer, d,
+ eventId, job->errorString()));
+ } else
+ d->failedTransfer(eventId);
+}
+
+void Room::cancelFileTransfer(const QString& id)
+{
+ auto it = d->fileTransfers.find(id);
+ if (it == d->fileTransfers.end())
+ {
+ qCWarning(MAIN) << "No information on file transfer" << id
+ << "in room" << d->id;
+ return;
+ }
+ if (isJobRunning(it->job))
+ it->job->abandon();
+ d->fileTransfers.remove(id);
+ emit fileTransferCancelled(id);
+}
+
+void Room::Private::dropDuplicateEvents(RoomEvents* events) const
+{
+ if (events->empty())
+ return;
+
+ // Multiple-remove (by different criteria), single-erase
+ // 1. Check for duplicates against the timeline.
+ auto dupsBegin = remove_if(events->begin(), events->end(),
+ [&] (const RoomEventPtr& e)
+ { return eventsIndex.contains(e->id()); });
+
+ // 2. Check for duplicates within the batch if there are still events.
+ for (auto eIt = events->begin(); distance(eIt, dupsBegin) > 1; ++eIt)
+ dupsBegin = remove_if(eIt + 1, dupsBegin,
+ [&] (const RoomEventPtr& e)
+ { return e->id() == (*eIt)->id(); });
+ if (dupsBegin == events->end())
+ return;
+
+ qCDebug(EVENTS) << "Dropping" << distance(dupsBegin, events->end())
+ << "duplicate event(s)";
+ events->erase(dupsBegin, events->end());
+}
+
+inline bool isRedaction(const RoomEventPtr& e)
+{
+ return e->type() == EventType::Redaction;
+}
+
+void Room::Private::processRedaction(RoomEventPtr redactionEvent)
+{
+ Q_ASSERT(redactionEvent && isRedaction(redactionEvent));
+ const auto& redaction =
+ static_cast<const RedactionEvent*>(redactionEvent.get());
+
+ const auto pIdx = eventsIndex.find(redaction->redactedEvent());
+ if (pIdx == eventsIndex.end())
+ {
+ qCDebug(MAIN) << "Redaction" << redaction->id()
+ << "ignored: target event not found";
+ return; // If the target events comes later, it comes already redacted.
+ }
+ Q_ASSERT(q->isValidIndex(*pIdx));
+
+ auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())];
+
+ // Apply the redaction procedure from chapter 6.5 of The Spec
+ auto originalJson = ti->originalJsonObject();
+ if (originalJson.value("unsigned").toObject()
+ .value("redacted_because").toObject()
+ .value("event_id") == redaction->id())
+ {
+ qCDebug(MAIN) << "Redaction" << redaction->id()
+ << "of event" << ti.event()->id() << "already done, skipping";
+ return;
+ }
+ static const QStringList keepKeys =
+ { "event_id", "type", "room_id", "sender", "state_key",
+ "prev_content", "content", "origin_server_ts" };
+ static const
+ std::vector<std::pair<EventType, QStringList>> keepContentKeysMap
+ { { Event::Type::RoomMember, { "membership" } }
+ , { Event::Type::RoomCreate, { "creator" } }
+ , { Event::Type::RoomJoinRules, { "join_rule" } }
+ , { Event::Type::RoomPowerLevels,
+ { "ban", "events", "events_default", "kick", "redact",
+ "state_default", "users", "users_default" } }
+ , { Event::Type::RoomAliases, { "alias" } }
+ };
+ for (auto it = originalJson.begin(); it != originalJson.end();)
+ {
+ if (!keepKeys.contains(it.key()))
+ it = originalJson.erase(it); // TODO: shred the value
+ else
+ ++it;
+ }
+ auto keepContentKeys =
+ find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(),
+ [&ti](const auto& t) { return ti->type() == t.first; } );
+ if (keepContentKeys == keepContentKeysMap.end())
+ {
+ originalJson.remove("content");
+ originalJson.remove("prev_content");
+ } else {
+ auto content = originalJson.take("content").toObject();
+ for (auto it = content.begin(); it != content.end(); )
+ {
+ if (!keepContentKeys->second.contains(it.key()))
+ it = content.erase(it);
+ else
+ ++it;
+ }
+ originalJson.insert("content", content);
+ }
+ auto unsignedData = originalJson.take("unsigned").toObject();
+ unsignedData["redacted_because"] = redaction->originalJsonObject();
+ originalJson.insert("unsigned", unsignedData);
+
+ // Make a new event from the redacted JSON, exchange events,
+ // notify everyone and delete the old event
+ RoomEventPtr oldEvent
+ { ti.replaceEvent(makeEvent<RoomEvent>(originalJson)) };
+ q->onRedaction(oldEvent.get(), ti.event());
+ qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction->id();
+ emit q->replacedEvent(ti.event(), oldEvent.get());
+}
+
+Connection* Room::connection() const
+{
+ Q_ASSERT(d->connection);
+ return d->connection;
+}
+
+User* Room::localUser() const
+{
+ return connection()->user();
+}
+
+void Room::Private::addNewMessageEvents(RoomEvents&& events)
+{
+ auto timelineSize = timeline.size();
+
+ dropDuplicateEvents(&events);
+ // We want to process redactions in the order of arrival (covering the
+ // case of one redaction superseding another one), hence stable partition.
+ const auto normalsBegin =
+ stable_partition(events.begin(), events.end(), isRedaction);
+ RoomEventsRange redactions { events.begin(), normalsBegin },
+ normalEvents { normalsBegin, events.end() };
+
+ if (!normalEvents.empty())
+ emit q->aboutToAddNewMessages(normalEvents);
+ const auto insertedSize = insertEvents(std::move(normalEvents), Newer);
+ const auto from = timeline.cend() - insertedSize;
+ if (insertedSize > 0)
+ {
+ qCDebug(MAIN)
+ << "Room" << displayname << "received" << insertedSize
+ << "new events; the last event is now" << timeline.back();
+ q->onAddNewTimelineEvents(from);
+ }
+ for (auto&& r: redactions)
+ processRedaction(move(r));
+ if (insertedSize > 0)
+ {
+ emit q->addedMessages();
+
+ // The first event in the just-added batch (referred to by `from`)
+ // defines whose read marker can possibly be promoted any further over
+ // the same author's events newly arrived. Others will need explicit
+ // read receipts from the server (or, for the local user,
+ // markMessagesAsRead() invocation) to promote their read markers over
+ // the new message events.
+ auto firstWriter = q->user((*from)->senderId());
+ if (q->readMarker(firstWriter) != timeline.crend())
+ {
+ promoteReadMarker(firstWriter, rev_iter_t(from) - 1);
+ qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id()
+ << "to" << *q->readMarker(firstWriter);
+ }
+
+ updateUnreadCount(timeline.crbegin(), rev_iter_t(from));
+ }
+
+ Q_ASSERT(timeline.size() == timelineSize + insertedSize);
+}
+
+void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
+{
+ const auto timelineSize = timeline.size();
+
+ dropDuplicateEvents(&events);
+ const auto redactionsBegin =
+ remove_if(events.begin(), events.end(), isRedaction);
+ RoomEventsRange normalEvents { events.begin(), redactionsBegin };
+ if (normalEvents.empty())
+ return;
+
+ emit q->aboutToAddHistoricalMessages(normalEvents);
+ const auto insertedSize = insertEvents(std::move(normalEvents), Older);
+ const auto from = timeline.crend() - insertedSize;
+
+ qCDebug(MAIN) << "Room" << displayname << "received" << insertedSize
+ << "past events; the oldest event is now" << timeline.front();
+ q->onAddHistoricalTimelineEvents(from);
+ emit q->addedMessages();
+
+ if (from <= q->readMarker())
+ updateUnreadCount(from, timeline.crend());
+
+ Q_ASSERT(timeline.size() == timelineSize + insertedSize);
+}
+
+void Room::processStateEvents(const RoomEvents& events)
+{
+ bool emitNamesChanged = false;
+ for (const auto& e: events)
+ {
+ auto* event = e.get();
+ switch (event->type())
+ {
+ case EventType::RoomName: {
+ auto nameEvent = static_cast<RoomNameEvent*>(event);
+ d->name = nameEvent->name();
+ qCDebug(MAIN) << "Room name updated:" << d->name;
+ emitNamesChanged = true;
+ break;
+ }
+ case EventType::RoomAliases: {
+ auto aliasesEvent = static_cast<RoomAliasesEvent*>(event);
+ d->aliases = aliasesEvent->aliases();
+ qCDebug(MAIN) << "Room aliases updated:" << d->aliases;
+ emitNamesChanged = true;
+ break;
+ }
+ case EventType::RoomCanonicalAlias: {
+ auto aliasEvent = static_cast<RoomCanonicalAliasEvent*>(event);
+ d->canonicalAlias = aliasEvent->alias();
+ setObjectName(d->canonicalAlias);
+ qCDebug(MAIN) << "Room canonical alias updated:" << d->canonicalAlias;
+ emitNamesChanged = true;
+ break;
+ }
+ case EventType::RoomTopic: {
+ auto topicEvent = static_cast<RoomTopicEvent*>(event);
+ d->topic = topicEvent->topic();
+ qCDebug(MAIN) << "Room topic updated:" << d->topic;
+ emit topicChanged();
+ break;
+ }
+ case EventType::RoomAvatar: {
+ const auto& avatarEventContent =
+ static_cast<RoomAvatarEvent*>(event)->content();
+ if (d->avatar.updateUrl(avatarEventContent.url))
+ {
+ qCDebug(MAIN) << "Room avatar URL updated:"
+ << avatarEventContent.url.toString();
+ emit avatarChanged();
+ }
+ break;
+ }
+ case EventType::RoomMember: {
+ auto memberEvent = static_cast<RoomMemberEvent*>(event);
+ auto u = user(memberEvent->userId());
+ u->processEvent(memberEvent, this);
+ if (u == localUser() && memberJoinState(u) == JoinState::Invite
+ && memberEvent->isDirect())
+ connection()->addToDirectChats(this,
+ user(memberEvent->senderId()));
+
+ if( memberEvent->membership() == MembershipType::Join )
+ {
+ if (memberJoinState(u) != JoinState::Join)
+ {
+ d->insertMemberIntoMap(u);
+ connect(u, &User::nameAboutToChange, this,
+ [=] (QString newName, QString, const Room* context) {
+ if (context == this)
+ emit memberAboutToRename(u, newName);
+ });
+ connect(u, &User::nameChanged, this,
+ [=] (QString, QString oldName, const Room* context) {
+ if (context == this)
+ d->renameMember(u, oldName);
+ });
+ emit userAdded(u);
+ }
+ }
+ else if( memberEvent->membership() == MembershipType::Leave )
+ {
+ if (memberJoinState(u) == JoinState::Join)
+ {
+ if (!d->membersLeft.contains(u))
+ d->membersLeft.append(u);
+ d->removeMemberFromMap(u->name(this), u);
+ emit userRemoved(u);
+ }
+ }
+ break;
+ }
+ case EventType::RoomEncryption:
+ {
+ d->encryptionAlgorithm =
+ static_cast<EncryptionEvent*>(event)->algorithm();
+ qCDebug(MAIN) << "Encryption switched on in" << displayName();
+ emit encryption();
+ break;
+ }
+ default: /* Ignore events of other types */;
+ }
+ }
+ if (emitNamesChanged) {
+ emit namesChanged(this);
+ }
+ d->updateDisplayname();
+}
+
+void Room::processEphemeralEvent(EventPtr event)
+{
+ QElapsedTimer et; et.start();
+ switch (event->type())
+ {
+ case EventType::Typing: {
+ auto typingEvent = static_cast<TypingEvent*>(event.get());
+ d->usersTyping.clear();
+ for( const QString& userId: typingEvent->users() )
+ {
+ auto u = user(userId);
+ if (memberJoinState(u) == JoinState::Join)
+ d->usersTyping.append(u);
+ }
+ if (!typingEvent->users().isEmpty())
+ qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):"
+ << typingEvent->users().size() << "users," << et;
+ emit typingChanged();
+ break;
+ }
+ case EventType::Receipt: {
+ auto receiptEvent = static_cast<ReceiptEvent*>(event.get());
+ for( const auto &p: receiptEvent->eventsWithReceipts() )
+ {
+ {
+ if (p.receipts.size() == 1)
+ qCDebug(EPHEMERAL) << "Marking" << p.evtId
+ << "as read for" << p.receipts[0].userId;
+ else
+ qCDebug(EPHEMERAL) << "Marking" << p.evtId
+ << "as read for"
+ << p.receipts.size() << "users";
+ }
+ const auto newMarker = findInTimeline(p.evtId);
+ if (newMarker != timelineEdge())
+ {
+ for( const Receipt& r: p.receipts )
+ {
+ if (r.userId == connection()->userId())
+ continue; // FIXME, #185
+ auto u = user(r.userId);
+ if (memberJoinState(u) == JoinState::Join)
+ d->promoteReadMarker(u, newMarker);
+ }
+ } else
+ {
+ qCDebug(EPHEMERAL) << "Event" << p.evtId
+ << "not found; saving read receipts anyway";
+ // If the event is not found (most likely, because it's too old
+ // and hasn't been fetched from the server yet), but there is
+ // a previous marker for a user, keep the previous marker.
+ // Otherwise, blindly store the event id for this user.
+ for( const Receipt& r: p.receipts )
+ {
+ if (r.userId == connection()->userId())
+ continue; // FIXME, #185
+ auto u = user(r.userId);
+ if (memberJoinState(u) == JoinState::Join &&
+ readMarker(u) == timelineEdge())
+ d->setLastReadEvent(u, p.evtId);
+ }
+ }
+ }
+ if (!receiptEvent->eventsWithReceipts().isEmpty())
+ qCDebug(PROFILER) << "*** Room::processEphemeralEvent(receipts):"
+ << receiptEvent->eventsWithReceipts().size()
+ << "events with receipts," << et;
+ break;
+ }
+ default:
+ qCWarning(EPHEMERAL) << "Unexpected event type in 'ephemeral' batch:"
+ << event->jsonType();
+ }
+}
+
+void Room::processAccountDataEvent(EventPtr event)
+{
+ switch (event->type())
+ {
+ case EventType::Tag:
+ {
+ auto newTags = static_cast<TagEvent*>(event.get())->tags();
+ if (newTags == d->tags)
+ break;
+ d->tags = newTags;
+ qCDebug(MAIN) << "Room" << id() << "is tagged with:"
+ << tagNames().join(", ");
+ emit tagsChanged();
+ break;
+ }
+ case EventType::ReadMarker:
+ {
+ const auto* rmEvent = static_cast<ReadMarkerEvent*>(event.get());
+ const auto& readEventId = rmEvent->event_id();
+ qCDebug(MAIN) << "Server-side read marker at" << readEventId;
+ d->serverReadMarker = readEventId;
+ const auto newMarker = findInTimeline(readEventId);
+ if (newMarker != timelineEdge())
+ d->markMessagesAsRead(newMarker);
+ else {
+ d->setLastReadEvent(localUser(), readEventId);
+ }
+ break;
+ }
+ default:
+ d->accountData[event->jsonType()] =
+ event->contentJson().toVariantHash();
+ }
+}
+
+QString Room::Private::roomNameFromMemberNames(const QList<User *> &userlist) const
+{
+ // This is part 3(i,ii,iii) in the room displayname algorithm described
+ // in the CS spec (see also Room::Private::updateDisplayname() ).
+ // The spec requires to sort users lexicographically by state_key (user id)
+ // and use disambiguated display names of two topmost users excluding
+ // the current one to render the name of the room.
+
+ // std::array is the leanest C++ container
+ std::array<User*, 2> first_two = { {nullptr, nullptr} };
+ std::partial_sort_copy(
+ userlist.begin(), userlist.end(),
+ first_two.begin(), first_two.end(),
+ [this](const User* u1, const User* u2) {
+ // Filter out the "me" user so that it never hits the room name
+ return isLocalUser(u2) || (!isLocalUser(u1) && u1->id() < u2->id());
+ }
+ );
+
+ // Spec extension. A single person in the chat but not the local user
+ // (the local user is apparently invited).
+ if (userlist.size() == 1 && !isLocalUser(first_two.front()))
+ return tr("Invitation from %1")
+ .arg(q->roomMembername(first_two.front()));
+
+ // i. One-on-one chat. first_two[1] == localUser() in this case.
+ if (userlist.size() == 2)
+ return q->roomMembername(first_two[0]);
+
+ // ii. Two users besides the current one.
+ if (userlist.size() == 3)
+ return tr("%1 and %2")
+ .arg(q->roomMembername(first_two[0]))
+ .arg(q->roomMembername(first_two[1]));
+
+ // iii. More users.
+ if (userlist.size() > 3)
+ return tr("%1 and %L2 others")
+ .arg(q->roomMembername(first_two[0]))
+ .arg(userlist.size() - 3);
+
+ // userlist.size() < 2 - apparently, there's only current user in the room
+ return QString();
+}
+
+QString Room::Private::calculateDisplayname() const
+{
+ // CS spec, section 11.2.2.5 Calculating the display name for a room
+ // Numbers below refer to respective parts in the spec.
+
+ // 1. Name (from m.room.name)
+ if (!name.isEmpty()) {
+ return name;
+ }
+
+ // 2. Canonical alias
+ if (!canonicalAlias.isEmpty())
+ return canonicalAlias;
+
+ // 3. Room members
+ QString topMemberNames = roomNameFromMemberNames(membersMap.values());
+ if (!topMemberNames.isEmpty())
+ return topMemberNames;
+
+ // 4. Users that previously left the room
+ topMemberNames = roomNameFromMemberNames(membersLeft);
+ if (!topMemberNames.isEmpty())
+ return tr("Empty room (was: %1)").arg(topMemberNames);
+
+ // 5. Fail miserably
+ return tr("Empty room (%1)").arg(id);
+
+ // Using m.room.aliases is explicitly discouraged by the spec
+ //if (!aliases.empty() && !aliases.at(0).isEmpty())
+ // displayname = aliases.at(0);
+}
+
+void Room::Private::updateDisplayname()
+{
+ const QString old_name = displayname;
+ displayname = calculateDisplayname();
+ if (old_name != displayname)
+ emit q->displaynameChanged(q);
+}
+
+void appendStateEvent(QJsonArray& events, const QString& type,
+ const QJsonObject& content, const QString& stateKey = {})
+{
+ if (!content.isEmpty() || !stateKey.isEmpty())
+ events.append(QJsonObject
+ { { QStringLiteral("type"), type }
+ , { QStringLiteral("content"), content }
+ , { QStringLiteral("state_key"), stateKey }
+ });
+}
+
+#define ADD_STATE_EVENT(events, type, name, content) \
+ appendStateEvent((events), QStringLiteral(type), \
+ {{ QStringLiteral(name), content }});
+
+void appendEvent(QJsonArray& events, const QString& type,
+ const QJsonObject& content)
+{
+ if (!content.isEmpty())
+ events.append(QJsonObject
+ { { QStringLiteral("type"), type }
+ , { QStringLiteral("content"), content }
+ });
+}
+
+template <typename EvtT>
+void appendEvent(QJsonArray& events, const EvtT& event)
+{
+ appendEvent(events, EvtT::TypeId, event.toJson());
+}
+
+QJsonObject Room::Private::toJson() const
+{
+ QElapsedTimer et; et.start();
+ QJsonObject result;
+ {
+ QJsonArray stateEvents;
+
+ ADD_STATE_EVENT(stateEvents, "m.room.name", "name", name);
+ ADD_STATE_EVENT(stateEvents, "m.room.topic", "topic", topic);
+ ADD_STATE_EVENT(stateEvents, "m.room.avatar", "url",
+ avatar.url().toString());
+ ADD_STATE_EVENT(stateEvents, "m.room.aliases", "aliases",
+ QJsonArray::fromStringList(aliases));
+ ADD_STATE_EVENT(stateEvents, "m.room.canonical_alias", "alias",
+ canonicalAlias);
+ ADD_STATE_EVENT(stateEvents, "m.room.encryption", "algorithm",
+ encryptionAlgorithm);
+
+ for (const auto *m : membersMap)
+ appendStateEvent(stateEvents, QStringLiteral("m.room.member"),
+ { { QStringLiteral("membership"), QStringLiteral("join") }
+ , { QStringLiteral("displayname"), m->name(q) }
+ , { QStringLiteral("avatar_url"), m->avatarUrl(q).toString() }
+ }, m->id());
+
+ const auto stateObjName = joinState == JoinState::Invite ?
+ QStringLiteral("invite_state") : QStringLiteral("state");
+ result.insert(stateObjName,
+ QJsonObject {{ QStringLiteral("events"), stateEvents }});
+ }
+
+ QJsonArray accountDataEvents;
+ if (!tags.empty())
+ appendEvent(accountDataEvents, TagEvent(tags));
+
+ if (!serverReadMarker.isEmpty())
+ appendEvent(accountDataEvents, ReadMarkerEvent(serverReadMarker));
+
+ if (!accountData.empty())
+ {
+ for (auto it = accountData.begin(); it != accountData.end(); ++it)
+ appendEvent(accountDataEvents, it.key(),
+ QJsonObject::fromVariantHash(it.value()));
+ }
+ result.insert("account_data", QJsonObject {{ "events", accountDataEvents }});
+
+ QJsonObject unreadNotificationsObj;
+
+ unreadNotificationsObj.insert(SyncRoomData::UnreadCountKey, unreadMessages);
+ if (highlightCount > 0)
+ unreadNotificationsObj.insert("highlight_count", highlightCount);
+ if (notificationCount > 0)
+ unreadNotificationsObj.insert("notification_count", notificationCount);
+
+ result.insert("unread_notifications", unreadNotificationsObj);
+
+ if (et.elapsed() > 50)
+ qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et;
+
+ return result;
+}
+
+QJsonObject Room::toJson() const
+{
+ return d->toJson();
+}
+
+MemberSorter Room::memberSorter() const
+{
+ return MemberSorter(this);
+}
+
+bool MemberSorter::operator()(User *u1, User *u2) const
+{
+ return operator()(u1, room->roomMembername(u2));
+}
+
+bool MemberSorter::operator ()(User* u1, const QString& u2name) const
+{
+ auto n1 = room->roomMembername(u1);
+ if (n1.startsWith('@'))
+ n1.remove(0, 1);
+ auto n2 = u2name.midRef(u2name.startsWith('@') ? 1 : 0);
+
+ return n1.localeAwareCompare(n2) < 0;
+}
diff --git a/lib/room.h b/lib/room.h
new file mode 100644
index 00000000..bdef04ee
--- /dev/null
+++ b/lib/room.h
@@ -0,0 +1,424 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include "jobs/syncjob.h"
+#include "events/roommessageevent.h"
+#include "events/accountdataevents.h"
+#include "joinstate.h"
+
+#include <QtGui/QPixmap>
+
+#include <memory>
+#include <deque>
+#include <utility>
+
+namespace QMatrixClient
+{
+ class Event;
+ class Connection;
+ class User;
+ class MemberSorter;
+ class LeaveRoomJob;
+ class RedactEventJob;
+
+ class TimelineItem
+ {
+ public:
+ // For compatibility with Qt containers, even though we use
+ // a std:: container now for the room timeline
+ using index_t = int;
+
+ TimelineItem(RoomEventPtr&& e, index_t number)
+ : evt(move(e)), idx(number) { }
+
+ RoomEvent* event() const { return evt.get(); }
+ RoomEvent* operator->() const { return evt.operator->(); }
+ index_t index() const { return idx; }
+
+ // Used for event redaction
+ RoomEventPtr replaceEvent(RoomEventPtr&& other);
+
+ private:
+ RoomEventPtr evt;
+ index_t idx;
+ };
+ inline QDebug& operator<<(QDebug& d, const TimelineItem& ti)
+ {
+ QDebugStateSaver dss(d);
+ d.nospace() << "(" << ti.index() << "|" << ti->id() << ")";
+ return d;
+ }
+
+ class FileTransferInfo
+ {
+ Q_GADGET
+ Q_PROPERTY(bool active READ active CONSTANT)
+ Q_PROPERTY(bool completed READ completed CONSTANT)
+ Q_PROPERTY(bool failed READ failed CONSTANT)
+ Q_PROPERTY(int progress MEMBER progress CONSTANT)
+ Q_PROPERTY(int total MEMBER total CONSTANT)
+ Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT)
+ Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT)
+ public:
+ enum Status { None, Started, Completed, Failed };
+ Status status = None;
+ int progress = 0;
+ int total = -1;
+ QUrl localDir { };
+ QUrl localPath { };
+
+ bool active() const
+ { return status == Started || status == Completed; }
+ bool completed() const { return status == Completed; }
+ bool failed() const { return status == Failed; }
+ };
+
+ class Room: public QObject
+ {
+ Q_OBJECT
+ Q_PROPERTY(Connection* connection READ connection CONSTANT)
+ Q_PROPERTY(User* localUser READ localUser CONSTANT)
+ Q_PROPERTY(QString id READ id CONSTANT)
+ Q_PROPERTY(QString name READ name NOTIFY namesChanged)
+ Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged)
+ Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged)
+ Q_PROPERTY(QString displayName READ displayName NOTIFY namesChanged)
+ Q_PROPERTY(QString topic READ topic NOTIFY topicChanged)
+ Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
+ Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged)
+ Q_PROPERTY(bool usesEncryption READ usesEncryption NOTIFY encryption)
+
+ Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages)
+ Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged)
+ Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged)
+
+ Q_PROPERTY(bool displayed READ displayed WRITE setDisplayed NOTIFY displayedChanged)
+ Q_PROPERTY(QString firstDisplayedEventId READ firstDisplayedEventId WRITE setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged)
+ Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE setLastDisplayedEventId NOTIFY lastDisplayedEventChanged)
+
+ Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved)
+ Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY unreadMessagesChanged)
+ Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged)
+ Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged)
+
+ public:
+ using Timeline = std::deque<TimelineItem>;
+ using rev_iter_t = Timeline::const_reverse_iterator;
+ using timeline_iter_t = Timeline::const_iterator;
+
+ Room(Connection* connection, QString id, JoinState initialJoinState);
+ ~Room() override;
+
+ // Property accessors
+
+ Connection* connection() const;
+ User* localUser() const;
+ const QString& id() const;
+ QString name() const;
+ QStringList aliases() const;
+ QString canonicalAlias() const;
+ QString displayName() const;
+ QString topic() const;
+ QString avatarMediaId() const;
+ QUrl avatarUrl() const;
+ Q_INVOKABLE JoinState joinState() const;
+ Q_INVOKABLE QList<User*> usersTyping() const;
+ QList<User*> membersLeft() const;
+
+ Q_INVOKABLE QList<User*> users() const;
+ QStringList memberNames() const;
+ int memberCount() const;
+ int timelineSize() const;
+ bool usesEncryption() const;
+
+ /**
+ * Returns a square room avatar with the given size and requests it
+ * from the network if needed
+ * @return a pixmap with the avatar or a placeholder if there's none
+ * available yet
+ */
+ Q_INVOKABLE QImage avatar(int dimension);
+ /**
+ * Returns a room avatar with the given dimensions and requests it
+ * from the network if needed
+ * @return a pixmap with the avatar or a placeholder if there's none
+ * available yet
+ */
+ Q_INVOKABLE QImage avatar(int width, int height);
+
+ /**
+ * @brief Get a user object for a given user id
+ * This is the recommended way to get a user object in a room
+ * context. The actual object type may be changed in further
+ * versions to provide room-specific user information (display name,
+ * avatar etc.).
+ * \note The method will return a valid user regardless of
+ * the membership.
+ */
+ Q_INVOKABLE User* user(const QString& userId) const;
+
+ /**
+ * \brief Check the join state of a given user in this room
+ *
+ * \note Banned and invited users are not tracked for now (Leave
+ * will be returned for them).
+ *
+ * \return either of Join, Leave, depending on the given
+ * user's state in the room
+ */
+ Q_INVOKABLE JoinState memberJoinState(User* user) const;
+
+ /**
+ * @brief Produces a disambiguated name for a given user in
+ * the context of the room
+ */
+ Q_INVOKABLE QString roomMembername(const User* u) const;
+ /**
+ * @brief Produces a disambiguated name for a user with this id in
+ * the context of the room
+ */
+ Q_INVOKABLE QString roomMembername(const QString& userId) const;
+
+ const Timeline& messageEvents() const;
+ /**
+ * A convenience method returning the read marker to the before-oldest
+ * message
+ */
+ rev_iter_t timelineEdge() const;
+ Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const;
+ Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const;
+ Q_INVOKABLE bool isValidIndex(TimelineItem::index_t timelineIndex) const;
+
+ rev_iter_t findInTimeline(TimelineItem::index_t index) const;
+ rev_iter_t findInTimeline(const QString& evtId) const;
+
+ bool displayed() const;
+ void setDisplayed(bool displayed = true);
+ QString firstDisplayedEventId() const;
+ rev_iter_t firstDisplayedMarker() const;
+ void setFirstDisplayedEventId(const QString& eventId);
+ void setFirstDisplayedEvent(TimelineItem::index_t index);
+ QString lastDisplayedEventId() const;
+ rev_iter_t lastDisplayedMarker() const;
+ void setLastDisplayedEventId(const QString& eventId);
+ void setLastDisplayedEvent(TimelineItem::index_t index);
+
+ rev_iter_t readMarker(const User* user) const;
+ rev_iter_t readMarker() const;
+ QString readMarkerEventId() const;
+ /**
+ * @brief Mark the event with uptoEventId as read
+ *
+ * Finds in the timeline and marks as read the event with
+ * the specified id; also posts a read receipt to the server either
+ * for this message or, if it's from the local user, for
+ * the nearest non-local message before. uptoEventId must be non-empty.
+ */
+ void markMessagesAsRead(QString uptoEventId);
+
+ /** Check whether there are unread messages in the room */
+ bool hasUnreadMessages() const;
+
+ /** Get the number of unread messages in the room
+ * Depending on the read marker state, this call may return either
+ * a precise or an estimate number of unread events. Only "notable"
+ * events (non-redacted message events from users other than local)
+ * are counted.
+ *
+ * In a case when readMarker() == timelineEdge() (the local read
+ * marker is beyond the local timeline) only the bottom limit of
+ * the unread messages number can be estimated (and even that may
+ * be slightly off due to, e.g., redactions of events not loaded
+ * to the local timeline).
+ *
+ * If all messages are read, this function will return -1 (_not_ 0,
+ * as zero may mean "zero or more unread messages" in a situation
+ * when the read marker is outside the local timeline.
+ */
+ int unreadCount() const;
+
+ Q_INVOKABLE int notificationCount() const;
+ Q_INVOKABLE void resetNotificationCount();
+ Q_INVOKABLE int highlightCount() const;
+ Q_INVOKABLE void resetHighlightCount();
+
+ QStringList tagNames() const;
+ TagsMap tags() const;
+ TagRecord tag(const QString& name) const;
+
+ /** Add a new tag to this room
+ * If this room already has this tag, nothing happens. If it's a new
+ * tag for the room, the respective tag record is added to the set
+ * of tags and the new set is sent to the server to update other
+ * clients.
+ */
+ void addTag(const QString& name, const TagRecord& record = {});
+
+ /** Remove a tag from the room */
+ void removeTag(const QString& name);
+
+ /** Overwrite the room's tags
+ * This completely replaces the existing room's tags with a set
+ * of new ones and updates the new set on the server. Unlike
+ * most other methods in Room, this one sends a signal about changes
+ * immediately, not waiting for confirmation from the server
+ * (because tags are saved in account data rather than in shared
+ * room state).
+ */
+ void setTags(const TagsMap& newTags);
+
+ /** Check whether the list of tags has m.favourite */
+ bool isFavourite() const;
+ /** Check whether the list of tags has m.lowpriority */
+ bool isLowPriority() const;
+
+ /** Check whether this room is a direct chat */
+ bool isDirectChat() const;
+
+ /** Get the list of users this room is a direct chat with */
+ QList<const User*> directChatUsers() const;
+
+ Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId);
+ Q_INVOKABLE QUrl urlToDownload(const QString& eventId);
+ Q_INVOKABLE QString fileNameToDownload(const QString& eventId);
+ Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const;
+
+ /** Pretty-prints plain text into HTML
+ * This includes HTML escaping of <,>,",& and URLs linkification.
+ */
+ QString prettyPrint(const QString& plainText) const;
+
+ MemberSorter memberSorter() const;
+
+ QJsonObject toJson() const;
+ void updateData(SyncRoomData&& data );
+ void setJoinState( JoinState state );
+
+ public slots:
+ void postMessage(const QString& plainText,
+ MessageEventType type = MessageEventType::Text);
+ void postMessage(const RoomMessageEvent& event);
+ /** @deprecated If you have a custom event type, construct the event
+ * and pass it as a whole to postMessage() */
+ void postMessage(const QString& type, const QString& plainText);
+ void setName(const QString& newName);
+ void setCanonicalAlias(const QString& newAlias);
+ void setTopic(const QString& newTopic);
+
+ void getPreviousContent(int limit = 10);
+
+ void inviteToRoom(const QString& memberId);
+ LeaveRoomJob* leaveRoom();
+ void kickMember(const QString& memberId, const QString& reason = {});
+ void ban(const QString& userId, const QString& reason = {});
+ void unban(const QString& userId);
+ void redactEvent(const QString& eventId,
+ const QString& reason = {});
+
+ void uploadFile(const QString& id, const QUrl& localFilename,
+ const QString& overrideContentType = {});
+ // If localFilename is empty a temporary file is created
+ void downloadFile(const QString& eventId,
+ const QUrl& localFilename = {});
+ void cancelFileTransfer(const QString& id);
+
+ /** Mark all messages in the room as read */
+ void markAllMessagesAsRead();
+
+ signals:
+ void aboutToAddHistoricalMessages(RoomEventsRange events);
+ void aboutToAddNewMessages(RoomEventsRange events);
+ void addedMessages();
+
+ /**
+ * @brief The room name, the canonical alias or other aliases changed
+ *
+ * Not triggered when displayname changes.
+ */
+ void namesChanged(Room* room);
+ /** @brief The room displayname changed */
+ void displaynameChanged(Room* room);
+ void topicChanged();
+ void avatarChanged();
+ void userAdded(User* user);
+ void userRemoved(User* user);
+ void memberAboutToRename(User* user, QString newName);
+ void memberRenamed(User* user);
+ void memberListChanged();
+ void encryption();
+
+ void joinStateChanged(JoinState oldState, JoinState newState);
+ void typingChanged();
+
+ void highlightCountChanged(Room* room);
+ void notificationCountChanged(Room* room);
+
+ void displayedChanged(bool displayed);
+ void firstDisplayedEventChanged();
+ void lastDisplayedEventChanged();
+ void lastReadEventChanged(User* user);
+ void readMarkerMoved();
+ void unreadMessagesChanged(Room* room);
+
+ void tagsChanged();
+
+ void replacedEvent(const RoomEvent* newEvent,
+ const RoomEvent* oldEvent);
+
+ void newFileTransfer(QString id, QUrl localFile);
+ void fileTransferProgress(QString id, qint64 progress, qint64 total);
+ void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl);
+ void fileTransferFailed(QString id, QString errorMessage = {});
+ void fileTransferCancelled(QString id);
+
+ protected:
+ virtual void processStateEvents(const RoomEvents& events);
+ virtual void processEphemeralEvent(EventPtr event);
+ virtual void processAccountDataEvent(EventPtr event);
+ virtual void onAddNewTimelineEvents(timeline_iter_t from) { }
+ virtual void onAddHistoricalTimelineEvents(rev_iter_t from) { }
+ virtual void onRedaction(const RoomEvent* prevEvent,
+ const RoomEvent* after) { }
+
+ private:
+ class Private;
+ Private* d;
+ };
+
+ class MemberSorter
+ {
+ public:
+ explicit MemberSorter(const Room* r) : room(r) { }
+
+ bool operator()(User* u1, User* u2) const;
+ bool operator()(User* u1, const QString& u2name) const;
+
+ template <typename ContT, typename ValT>
+ typename ContT::size_type lowerBoundIndex(const ContT& c,
+ const ValT& v) const
+ {
+ return std::lower_bound(c.begin(), c.end(), v, *this) - c.begin();
+ }
+
+ private:
+ const Room* room;
+ };
+} // namespace QMatrixClient
+Q_DECLARE_METATYPE(QMatrixClient::FileTransferInfo)
diff --git a/lib/settings.cpp b/lib/settings.cpp
new file mode 100644
index 00000000..852e19cb
--- /dev/null
+++ b/lib/settings.cpp
@@ -0,0 +1,123 @@
+#include "settings.h"
+
+#include "logging.h"
+
+#include <QtCore/QUrl>
+
+using namespace QMatrixClient;
+
+QString Settings::legacyOrganizationName {};
+QString Settings::legacyApplicationName {};
+
+void Settings::setLegacyNames(const QString& organizationName,
+ const QString& applicationName)
+{
+ legacyOrganizationName = organizationName;
+ legacyApplicationName = applicationName;
+}
+
+void Settings::setValue(const QString& key, const QVariant& value)
+{
+// qCDebug() << "Setting" << key << "to" << value;
+ QSettings::setValue(key, value);
+ if (legacySettings.contains(key))
+ legacySettings.remove(key);
+}
+
+QVariant Settings::value(const QString& key, const QVariant& defaultValue) const
+{
+ auto value = QSettings::value(key, legacySettings.value(key, defaultValue));
+ // QML's Qt.labs.Settings stores boolean values as strings, which, if loaded
+ // through the usual QSettings interface, confuses QML
+ // (QVariant("false") == true in JavaScript). Since we have a mixed
+ // environment where both QSettings and Qt.labs.Settings may potentially
+ // work with same settings, better ensure compatibility.
+ return value.toString() == QStringLiteral("false") ? QVariant(false) : value;
+}
+
+bool Settings::contains(const QString& key) const
+{
+ return QSettings::contains(key) || legacySettings.contains(key);
+}
+
+QStringList Settings::childGroups() const
+{
+ auto l = QSettings::childGroups();
+ return !l.isEmpty() ? l : legacySettings.childGroups();
+}
+
+void SettingsGroup::setValue(const QString& key, const QVariant& value)
+{
+ Settings::setValue(groupPath + '/' + key, value);
+}
+
+bool SettingsGroup::contains(const QString& key) const
+{
+ return Settings::contains(groupPath + '/' + key);
+}
+
+QVariant SettingsGroup::value(const QString& key, const QVariant& defaultValue) const
+{
+ return Settings::value(groupPath + '/' + key, defaultValue);
+}
+
+QString SettingsGroup::group() const
+{
+ return groupPath;
+}
+
+QStringList SettingsGroup::childGroups() const
+{
+ const_cast<SettingsGroup*>(this)->beginGroup(groupPath);
+ const_cast<QSettings&>(legacySettings).beginGroup(groupPath);
+ QStringList l = Settings::childGroups();
+ const_cast<SettingsGroup*>(this)->endGroup();
+ const_cast<QSettings&>(legacySettings).endGroup();
+ return l;
+}
+
+void SettingsGroup::remove(const QString& key)
+{
+ QString fullKey { groupPath };
+ if (!key.isEmpty())
+ fullKey += "/" + key;
+ Settings::remove(fullKey);
+}
+
+QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", "", setDeviceId)
+QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", "", setDeviceName)
+QMC_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn)
+
+QUrl AccountSettings::homeserver() const
+{
+ return QUrl::fromUserInput(value("homeserver").toString());
+}
+
+void AccountSettings::setHomeserver(const QUrl& url)
+{
+ setValue("homeserver", url.toString());
+}
+
+QString AccountSettings::userId() const
+{
+ return group().section('/', -1);
+}
+
+QString AccountSettings::accessToken() const
+{
+ return value("access_token").toString();
+}
+
+void AccountSettings::setAccessToken(const QString& accessToken)
+{
+ qCWarning(MAIN) << "Saving access_token to QSettings is insecure."
+ " Developers, please save access_token separately.";
+ setValue("access_token", accessToken);
+}
+
+void AccountSettings::clearAccessToken()
+{
+ legacySettings.remove("access_token");
+ legacySettings.remove("device_id"); // Force the server to re-issue it
+ remove("access_token");
+}
diff --git a/lib/settings.h b/lib/settings.h
new file mode 100644
index 00000000..27ec9ba5
--- /dev/null
+++ b/lib/settings.h
@@ -0,0 +1,134 @@
+/******************************************************************************
+ * Copyright (C) 2016 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include <QtCore/QSettings>
+#include <QtCore/QVector>
+#include <QtCore/QUrl>
+
+class QVariant;
+
+namespace QMatrixClient
+{
+ class Settings: public QSettings
+ {
+ Q_OBJECT
+ public:
+ /**
+ * Use this function before creating any Settings objects in order
+ * to setup a read-only location where configuration has previously
+ * been stored. This will provide an additional fallback in case of
+ * renaming the organisation/application.
+ */
+ static void setLegacyNames(const QString& organizationName,
+ const QString& applicationName = {});
+
+#if defined(_MSC_VER) && _MSC_VER < 1900
+ // VS 2013 (and probably older) aren't friends with 'using' statements
+ // that involve private constructors
+ explicit Settings(QObject* parent = 0) : QSettings(parent) { }
+#else
+ using QSettings::QSettings;
+#endif
+
+ Q_INVOKABLE void setValue(const QString &key,
+ const QVariant &value);
+ Q_INVOKABLE QVariant value(const QString &key,
+ const QVariant &defaultValue = {}) const;
+ Q_INVOKABLE bool contains(const QString& key) const;
+ Q_INVOKABLE QStringList childGroups() const;
+
+ private:
+ static QString legacyOrganizationName;
+ static QString legacyApplicationName;
+
+ protected:
+ QSettings legacySettings { legacyOrganizationName,
+ legacyApplicationName };
+ };
+
+ class SettingsGroup: public Settings
+ {
+ public:
+ template <typename... ArgTs>
+ explicit SettingsGroup(const QString& path, ArgTs... qsettingsArgs)
+ : Settings(qsettingsArgs...)
+ , groupPath(path)
+ { }
+
+ Q_INVOKABLE bool contains(const QString& key) const;
+ Q_INVOKABLE QVariant value(const QString &key,
+ const QVariant &defaultValue = {}) const;
+ Q_INVOKABLE QString group() const;
+ Q_INVOKABLE QStringList childGroups() const;
+ Q_INVOKABLE void setValue(const QString &key,
+ const QVariant &value);
+
+ Q_INVOKABLE void remove(const QString& key);
+
+ private:
+ QString groupPath;
+ };
+
+#define QMC_DECLARE_SETTING(type, propname, setter) \
+ Q_PROPERTY(type propname READ propname WRITE setter) \
+ public: \
+ type propname() const; \
+ void setter(type newValue); \
+ private:
+
+#define QMC_DEFINE_SETTING(classname, type, propname, qsettingname, defaultValue, setter) \
+type classname::propname() const \
+{ \
+ return value(QStringLiteral(qsettingname), defaultValue).value<type>(); \
+} \
+\
+void classname::setter(type newValue) \
+{ \
+ setValue(QStringLiteral(qsettingname), newValue); \
+} \
+
+ class AccountSettings: public SettingsGroup
+ {
+ Q_OBJECT
+ Q_PROPERTY(QString userId READ userId CONSTANT)
+ QMC_DECLARE_SETTING(QString, deviceId, setDeviceId)
+ QMC_DECLARE_SETTING(QString, deviceName, setDeviceName)
+ QMC_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn)
+ /** \deprecated \sa setAccessToken */
+ Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken)
+ public:
+ template <typename... ArgTs>
+ explicit AccountSettings(const QString& accountId, ArgTs... qsettingsArgs)
+ : SettingsGroup("Accounts/" + accountId, qsettingsArgs...)
+ { }
+
+ QString userId() const;
+
+ QUrl homeserver() const;
+ void setHomeserver(const QUrl& url);
+
+ /** \deprecated \sa setToken */
+ QString accessToken() const;
+ /** \deprecated Storing accessToken in QSettings is unsafe,
+ * see QMatrixClient/Quaternion#181 */
+ void setAccessToken(const QString& accessToken);
+ Q_INVOKABLE void clearAccessToken();
+ };
+} // namespace QMatrixClient
diff --git a/lib/user.cpp b/lib/user.cpp
new file mode 100644
index 00000000..7a6dbc73
--- /dev/null
+++ b/lib/user.cpp
@@ -0,0 +1,399 @@
+/******************************************************************************
+ * 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 "user.h"
+
+#include "connection.h"
+#include "room.h"
+#include "avatar.h"
+#include "events/event.h"
+#include "events/roommemberevent.h"
+#include "jobs/setroomstatejob.h"
+#include "jobs/generated/profile.h"
+#include "jobs/generated/content-repo.h"
+
+#include <QtCore/QTimer>
+#include <QtCore/QRegularExpression>
+#include <QtCore/QPointer>
+#include <QtCore/QStringBuilder>
+#include <QtCore/QElapsedTimer>
+
+#include <functional>
+
+using namespace QMatrixClient;
+using namespace std::placeholders;
+using std::move;
+
+class User::Private
+{
+ public:
+ static Avatar makeAvatar(QUrl url)
+ {
+ static const QIcon icon
+ { QIcon::fromTheme(QStringLiteral("user-available")) };
+ return Avatar(move(url), icon);
+ }
+
+ Private(QString userId, Connection* connection)
+ : userId(move(userId)), connection(connection)
+ { }
+
+ QString userId;
+ Connection* connection;
+
+ QString bridged;
+ QString mostUsedName;
+ QMultiHash<QString, const Room*> otherNames;
+ Avatar mostUsedAvatar { makeAvatar({}) };
+ std::vector<Avatar> otherAvatars;
+ auto otherAvatar(QUrl url)
+ {
+ return std::find_if(otherAvatars.begin(), otherAvatars.end(),
+ [&url] (const auto& av) { return av.url() == url; });
+ }
+ QMultiHash<QUrl, const Room*> avatarsToRooms;
+
+ mutable int totalRooms = 0;
+
+ QString nameForRoom(const Room* r, const QString& hint = {}) const;
+ void setNameForRoom(const Room* r, QString newName, QString oldName);
+ QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const;
+ void setAvatarForRoom(const Room* r, const QUrl& newUrl,
+ const QUrl& oldUrl);
+
+ void setAvatarOnServer(QString contentUri, User* q);
+
+};
+
+
+QString User::Private::nameForRoom(const Room* r, const QString& hint) const
+{
+ // If the hint is accurate, this function is O(1) instead of O(n)
+ if (hint == mostUsedName || otherNames.contains(hint, r))
+ return hint;
+ return otherNames.key(r, mostUsedName);
+}
+
+static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20;
+
+void User::Private::setNameForRoom(const Room* r, QString newName,
+ QString oldName)
+{
+ Q_ASSERT(oldName != newName);
+ Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r));
+ if (totalRooms < 2)
+ {
+ Q_ASSERT_X(totalRooms > 0 && otherNames.empty(), __FUNCTION__,
+ "Internal structures inconsistency");
+ mostUsedName = move(newName);
+ return;
+ }
+ otherNames.remove(oldName, r);
+ if (newName != mostUsedName)
+ {
+ // Check if the newName is about to become most used.
+ if (otherNames.count(newName) >= totalRooms - otherNames.size())
+ {
+ Q_ASSERT(totalRooms > 1);
+ QElapsedTimer et;
+ if (totalRooms > MIN_JOINED_ROOMS_TO_LOG)
+ {
+ qCDebug(MAIN) << "Switching the most used name of user" << userId
+ << "from" << mostUsedName << "to" << newName;
+ qCDebug(MAIN) << "The user is in" << totalRooms << "rooms";
+ et.start();
+ }
+
+ for (auto* r1: connection->roomMap())
+ if (nameForRoom(r1) == mostUsedName)
+ otherNames.insert(mostUsedName, r1);
+
+ mostUsedName = newName;
+ otherNames.remove(newName);
+ if (totalRooms > MIN_JOINED_ROOMS_TO_LOG)
+ qCDebug(PROFILER) << et << "to switch the most used name";
+ }
+ else
+ otherNames.insert(newName, r);
+ }
+}
+
+QUrl User::Private::avatarUrlForRoom(const Room* r, const QUrl& hint) const
+{
+ // If the hint is accurate, this function is O(1) instead of O(n)
+ if (hint == mostUsedAvatar.url() || avatarsToRooms.contains(hint, r))
+ return hint;
+ auto it = std::find(avatarsToRooms.begin(), avatarsToRooms.end(), r);
+ return it == avatarsToRooms.end() ? mostUsedAvatar.url() : it.key();
+}
+
+void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl,
+ const QUrl& oldUrl)
+{
+ Q_ASSERT(oldUrl != newUrl);
+ Q_ASSERT(oldUrl == mostUsedAvatar.url() ||
+ avatarsToRooms.contains(oldUrl, r));
+ if (totalRooms < 2)
+ {
+ Q_ASSERT_X(totalRooms > 0 && otherAvatars.empty(), __FUNCTION__,
+ "Internal structures inconsistency");
+ mostUsedAvatar.updateUrl(newUrl);
+ return;
+ }
+ avatarsToRooms.remove(oldUrl, r);
+ if (!avatarsToRooms.contains(oldUrl))
+ {
+ auto it = otherAvatar(oldUrl);
+ if (it != otherAvatars.end())
+ otherAvatars.erase(it);
+ }
+ if (newUrl != mostUsedAvatar.url())
+ {
+ // Check if the new avatar is about to become most used.
+ if (avatarsToRooms.count(newUrl) >= totalRooms - avatarsToRooms.size())
+ {
+ QElapsedTimer et;
+ if (totalRooms > MIN_JOINED_ROOMS_TO_LOG)
+ {
+ qCDebug(MAIN) << "Switching the most used avatar of user" << userId
+ << "from" << mostUsedAvatar.url().toDisplayString()
+ << "to" << newUrl.toDisplayString();
+ et.start();
+ }
+ avatarsToRooms.remove(newUrl);
+ auto nextMostUsedIt = otherAvatar(newUrl);
+ Q_ASSERT(nextMostUsedIt != otherAvatars.end());
+ std::swap(mostUsedAvatar, *nextMostUsedIt);
+ for (const auto* r1: connection->roomMap())
+ if (avatarUrlForRoom(r1) == nextMostUsedIt->url())
+ avatarsToRooms.insert(nextMostUsedIt->url(), r1);
+
+ if (totalRooms > MIN_JOINED_ROOMS_TO_LOG)
+ qCDebug(PROFILER) << et << "to switch the most used avatar";
+ } else {
+ if (otherAvatar(newUrl) == otherAvatars.end())
+ otherAvatars.emplace_back(makeAvatar(newUrl));
+ avatarsToRooms.insert(newUrl, r);
+ }
+ }
+}
+
+User::User(QString userId, Connection* connection)
+ : QObject(connection), d(new Private(move(userId), connection))
+{
+ setObjectName(userId);
+}
+
+User::~User() = default;
+
+QString User::id() const
+{
+ return d->userId;
+}
+
+bool User::isGuest() const
+{
+ Q_ASSERT(!d->userId.isEmpty() && d->userId.startsWith('@'));
+ auto it = std::find_if_not(d->userId.begin() + 1, d->userId.end(),
+ [] (QChar c) { return c.isDigit(); });
+ Q_ASSERT(it != d->userId.end());
+ return *it == ':';
+}
+
+QString User::name(const Room* room) const
+{
+ return d->nameForRoom(room);
+}
+
+void User::updateName(const QString& newName, const Room* room)
+{
+ updateName(newName, d->nameForRoom(room), room);
+}
+
+void User::updateName(const QString& newName, const QString& oldName,
+ const Room* room)
+{
+ Q_ASSERT(oldName == d->mostUsedName || d->otherNames.contains(oldName, room));
+ if (newName != oldName)
+ {
+ emit nameAboutToChange(newName, oldName, room);
+ d->setNameForRoom(room, newName, oldName);
+ setObjectName(displayname());
+ emit nameChanged(newName, oldName, room);
+ }
+}
+
+void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl,
+ const Room* room)
+{
+ Q_ASSERT(oldUrl == d->mostUsedAvatar.url() ||
+ d->avatarsToRooms.contains(oldUrl, room));
+ if (newUrl != oldUrl)
+ {
+ d->setAvatarForRoom(room, newUrl, oldUrl);
+ setObjectName(displayname());
+ emit avatarChanged(this, room);
+ }
+
+}
+
+void User::rename(const QString& newName)
+{
+ auto job = d->connection->callApi<SetDisplayNameJob>(id(), newName);
+ connect(job, &BaseJob::success, this, [=] { updateName(newName); });
+}
+
+void User::rename(const QString& newName, const Room* r)
+{
+ if (!r)
+ {
+ qCWarning(MAIN) << "Passing a null room to two-argument User::rename()"
+ "is incorrect; client developer, please fix it";
+ rename(newName);
+ }
+ Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__,
+ "Attempt to rename a user that's not a room member");
+ MemberEventContent evtC;
+ evtC.displayName = newName;
+ auto job = d->connection->callApi<SetRoomStateJob>(
+ r->id(), id(), RoomMemberEvent(move(evtC)));
+ connect(job, &BaseJob::success, this, [=] { updateName(newName, r); });
+}
+
+bool User::setAvatar(const QString& fileName)
+{
+ return avatarObject().upload(d->connection, fileName,
+ std::bind(&Private::setAvatarOnServer, d.data(), _1, this));
+}
+
+bool User::setAvatar(QIODevice* source)
+{
+ return avatarObject().upload(d->connection, source,
+ std::bind(&Private::setAvatarOnServer, d.data(), _1, this));
+}
+
+void User::requestDirectChat()
+{
+ Q_ASSERT(d->connection);
+ d->connection->requestDirectChat(d->userId);
+}
+
+void User::Private::setAvatarOnServer(QString contentUri, User* q)
+{
+ auto* j = connection->callApi<SetAvatarUrlJob>(userId, contentUri);
+ connect(j, &BaseJob::success, q,
+ [=] { q->updateAvatarUrl(contentUri, avatarUrlForRoom(nullptr)); });
+}
+
+QString User::displayname(const Room* room) const
+{
+ auto name = d->nameForRoom(room);
+ return name.isEmpty() ? d->userId :
+ room ? room->roomMembername(name) : name;
+}
+
+QString User::fullName(const Room* room) const
+{
+ auto name = d->nameForRoom(room);
+ return name.isEmpty() ? d->userId : name % " (" % d->userId % ')';
+}
+
+QString User::bridged() const
+{
+ return d->bridged;
+}
+
+const Avatar& User::avatarObject(const Room* room) const
+{
+ auto it = d->otherAvatar(d->avatarUrlForRoom(room));
+ return it != d->otherAvatars.end() ? *it : d->mostUsedAvatar;
+}
+
+QImage User::avatar(int dimension, const Room* room)
+{
+ return avatar(dimension, dimension, room);
+}
+
+QImage User::avatar(int width, int height, const Room* room)
+{
+ return avatar(width, height, room, []{});
+}
+
+QImage User::avatar(int width, int height, const Room* room,
+ Avatar::get_callback_t callback)
+{
+ return avatarObject(room).get(d->connection, width, height,
+ [=] { emit avatarChanged(this, room); callback(); });
+}
+
+QString User::avatarMediaId(const Room* room) const
+{
+ return avatarObject(room).mediaId();
+}
+
+QUrl User::avatarUrl(const Room* room) const
+{
+ return avatarObject(room).url();
+}
+
+void User::processEvent(RoomMemberEvent* event, const Room* room)
+{
+ if (event->membership() != MembershipType::Invite &&
+ event->membership() != MembershipType::Join)
+ return;
+
+ auto aboutToEnter = room->memberJoinState(this) == JoinState::Leave &&
+ (event->membership() == MembershipType::Join ||
+ event->membership() == MembershipType::Invite);
+ if (aboutToEnter)
+ ++d->totalRooms;
+
+ auto newName = event->displayName();
+ // `bridged` value uses the same notification signal as the name;
+ // it is assumed that first setting of the bridge occurs together with
+ // the first setting of the name, and further bridge updates are
+ // exceptionally rare (the only reasonable case being that the bridge
+ // changes the naming convention). For the same reason room-specific
+ // bridge tags are not supported at all.
+ QRegularExpression reSuffix(" \\((IRC|Gitter|Telegram)\\)$");
+ auto match = reSuffix.match(newName);
+ if (match.hasMatch())
+ {
+ if (d->bridged != match.captured(1))
+ {
+ if (!d->bridged.isEmpty())
+ qCWarning(MAIN) << "Bridge for user" << id() << "changed:"
+ << d->bridged << "->" << match.captured(1);
+ d->bridged = match.captured(1);
+ }
+ newName.truncate(match.capturedStart(0));
+ }
+ if (event->prevContent())
+ {
+ // FIXME: the hint doesn't work for bridged users
+ auto oldNameHint =
+ d->nameForRoom(room, event->prevContent()->displayName);
+ updateName(event->displayName(), oldNameHint, room);
+ updateAvatarUrl(event->avatarUrl(),
+ d->avatarUrlForRoom(room, event->prevContent()->avatarUrl),
+ room);
+ } else {
+ updateName(event->displayName(), room);
+ updateAvatarUrl(event->avatarUrl(), d->avatarUrlForRoom(room), room);
+ }
+}
diff --git a/lib/user.h b/lib/user.h
new file mode 100644
index 00000000..f76f9e0a
--- /dev/null
+++ b/lib/user.h
@@ -0,0 +1,125 @@
+/******************************************************************************
+ * 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
+ */
+
+#pragma once
+
+#include <QtCore/QString>
+#include <QtCore/QObject>
+#include "avatar.h"
+
+namespace QMatrixClient
+{
+ class Connection;
+ class Room;
+ class RoomMemberEvent;
+
+ class User: public QObject
+ {
+ Q_OBJECT
+ Q_PROPERTY(QString id READ id CONSTANT)
+ Q_PROPERTY(bool isGuest READ isGuest CONSTANT)
+ Q_PROPERTY(QString name READ name NOTIFY nameChanged)
+ Q_PROPERTY(QString displayName READ displayname NOTIFY nameChanged STORED false)
+ Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged STORED false)
+ Q_PROPERTY(QString bridgeName READ bridged NOTIFY nameChanged STORED false)
+ Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
+ Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged)
+ public:
+ User(QString userId, Connection* connection);
+ ~User() override;
+
+ /** Get unique stable user id
+ * User id is generated by the server and is not changed ever.
+ */
+ QString id() const;
+
+ /** Get the name chosen by the user
+ * This may be empty if the user didn't choose the name or cleared
+ * it.
+ * \sa displayName
+ */
+ QString name(const Room* room = nullptr) const;
+
+ /** Get the displayed user name
+ * This method returns the result of name() if its non-empty;
+ * otherwise it returns user id. This is convenient to show a user
+ * name outside of a room context. In a room context, user names
+ * should be disambiguated.
+ * \sa name, id, fullName Room::roomMembername
+ */
+ QString displayname(const Room* room = nullptr) const;
+
+ /** Get user name and id in one string
+ * The constructed string follows the format 'name (id)'
+ * used for users disambiguation in a room context and in other
+ * places.
+ * \sa displayName, Room::roomMembername
+ */
+ QString fullName(const Room* room = nullptr) const;
+
+ /**
+ * Returns the name of bridge the user is connected from or empty.
+ */
+ QString bridged() const;
+
+ /** Whether the user is a guest
+ * As of now, the function relies on the convention used in Synapse
+ * that guests and only guests have all-numeric IDs. This may or
+ * may not work with non-Synapse servers.
+ */
+ bool isGuest() const;
+
+ const Avatar& avatarObject(const Room* room = nullptr) const;
+ Q_INVOKABLE QImage avatar(int dimension, const Room* room = nullptr);
+ Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight,
+ const Room* room = nullptr);
+ QImage avatar(int width, int height, const Room* room,
+ Avatar::get_callback_t callback);
+
+ QString avatarMediaId(const Room* room = nullptr) const;
+ QUrl avatarUrl(const Room* room = nullptr) const;
+
+ void processEvent(RoomMemberEvent* event, const Room* r = nullptr);
+
+ public slots:
+ void rename(const QString& newName);
+ void rename(const QString& newName, const Room* r);
+ bool setAvatar(const QString& fileName);
+ bool setAvatar(QIODevice* source);
+ void requestDirectChat();
+
+ signals:
+ void nameAboutToChange(QString newName, QString oldName,
+ const Room* roomContext);
+ void nameChanged(QString newName, QString oldName,
+ const Room* roomContext);
+ void avatarChanged(User* user, const Room* roomContext);
+
+ private slots:
+ void updateName(const QString& newName, const Room* room = nullptr);
+ void updateName(const QString& newName, const QString& oldName,
+ const Room* room = nullptr);
+ void updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl,
+ const Room* room = nullptr);
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+}
+Q_DECLARE_METATYPE(QMatrixClient::User*)
diff --git a/lib/util.h b/lib/util.h
new file mode 100644
index 00000000..65de0610
--- /dev/null
+++ b/lib/util.h
@@ -0,0 +1,205 @@
+/******************************************************************************
+ * Copyright (C) 2016 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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
+ */
+
+#pragma once
+
+#include <QtCore/QMetaEnum>
+#include <QtCore/QDebug>
+
+#include <functional>
+
+namespace QMatrixClient
+{
+ /**
+ * @brief Lookup a value by a key in a varargs list
+ *
+ * This function template takes the value of its first argument (selector)
+ * as a key and searches for it in the key-value map passed in
+ * a parameter pack (every next pair of arguments forms a key-value pair).
+ * If a match is found, the respective value is returned; if no pairs
+ * matched, the last value (fallback) is returned.
+ *
+ * All options should be of the same type or implicitly castable to the
+ * type of the first option. If you need some specific type to cast to
+ * you can explicitly provide it as the ValueT template parameter
+ * (e.g. <code>lookup<void*>(parameters...)</code>). Note that pointers
+ * to methods of different classes and even to functions with different
+ * signatures are of different types. If their return types are castable
+ * to some common one, @see dispatch that deals with this by swallowing
+ * the method invocation.
+ *
+ * Below is an example of usage to select a parser depending on contents of
+ * a JSON object:
+ * {@code
+ * auto parser = lookup(obj.value["type"].toString(),
+ * "type1", fn1,
+ * "type2", fn2,
+ * fallbackFn);
+ * parser(obj);
+ * }
+ *
+ * The implementation is based on tail recursion; every recursion step
+ * removes 2 arguments (match and value). There's no selector value for the
+ * fallback option (the last one); therefore, the total number of lookup()
+ * arguments should be even: selector + n key-value pairs + fallback
+ *
+ * @note Beware of calling lookup() with a <code>const char*</code> selector
+ * (the first parameter) - most likely it won't do what you expect because
+ * of shallow comparison.
+ */
+ template <typename ValueT, typename SelectorT>
+ ValueT lookup(SelectorT/*unused*/, ValueT&& fallback)
+ {
+ return std::forward<ValueT>(fallback);
+ }
+
+ template <typename ValueT, typename SelectorT, typename KeyT, typename... Ts>
+ ValueT lookup(SelectorT&& selector, KeyT&& key, ValueT&& value, Ts&&... remainder)
+ {
+ if( selector == key )
+ return std::forward<ValueT>(value);
+
+ // Drop the failed key-value pair and recurse with 2 arguments less.
+ return lookup<ValueT>(std::forward<SelectorT>(selector),
+ std::forward<Ts>(remainder)...);
+ }
+
+ /**
+ * A wrapper around lookup() for functions of different types castable
+ * to a common std::function<> form
+ *
+ * This class uses std::function<> magic to first capture arguments of
+ * a yet-unknown function or function object, and then to coerce types of
+ * all functions/function objects passed for lookup to the type
+ * std::function<ResultT(ArgTs...). Without Dispatch<>, you would have
+ * to pass the specific function type to lookup, since your functions have
+ * different signatures. The type is not always obvious, and the resulting
+ * construct in client code would almost always be rather cumbersome.
+ * Dispatch<> deduces the necessary function type (well, almost - you still
+ * have to specify the result type) and hides the clumsiness. For more
+ * information on what std::function<> can wrap around, see
+ * https://cpptruths.blogspot.jp/2015/11/covariance-and-contravariance-in-c.html
+ *
+ * The function arguments are captured by value (i.e. copied) to avoid
+ * hard-to-find issues with dangling references in cases when a Dispatch<>
+ * object is passed across different contexts (e.g. returned from another
+ * function).
+ *
+ * \tparam ResultT - the desired type of a picked function invocation (mandatory)
+ * \tparam ArgTs - function argument types (deduced)
+ */
+#if __GNUC__ < 5 && __GNUC_MINOR__ < 9
+ // GCC 4.8 cannot cope with parameter packs inside lambdas; so provide a single
+ // argument version of Dispatch<> that we only need so far.
+ template <typename ResultT, typename ArgT>
+#else
+ template <typename ResultT, typename... ArgTs>
+#endif
+ class Dispatch
+ {
+ // The implementation takes a chapter from functional programming:
+ // Dispatch<> uses a function that in turn accepts a function as its
+ // argument. The sole purpose of the outer function (initialized by
+ // a lambda-expression in the constructor) is to store the arguments
+ // to any of the functions later looked up. The inner function (its
+ // type is defined by fn_t alias) is the one returned by lookup()
+ // invocation inside to().
+ //
+ // It's a bit counterintuitive to specify function parameters before
+ // the list of functions but otherwise it would take several overloads
+ // here to match all the ways a function-like behaviour can be done:
+ // reference-to-function, pointer-to-function, function object. This
+ // probably could be done as well but I preferred a more compact
+ // solution: you show what you have and if it's possible to bring all
+ // your functions to the same std::function<> based on what you have
+ // as parameters, the code will compile. If it's not possible, modern
+ // compilers are already good enough at pinpointing a specific place
+ // where types don't match.
+ public:
+#if __GNUC__ < 5 && __GNUC_MINOR__ < 9
+ using fn_t = std::function<ResultT(ArgT)>;
+ explicit Dispatch(ArgT&& arg)
+ : boundArgs([=](fn_t &&f) { return f(std::move(arg)); })
+ { }
+#else
+ using fn_t = std::function<ResultT(ArgTs...)>;
+ explicit Dispatch(ArgTs&&... args)
+ : boundArgs([=](fn_t &&f) { return f(std::move(args)...); })
+ { }
+#endif
+
+ template <typename... LookupParamTs>
+ ResultT to(LookupParamTs&&... lookupParams)
+ {
+ // Here's the magic, two pieces of it:
+ // 1. Specifying fn_t in lookup() wraps all functions in
+ // \p lookupParams into the same std::function<> type. This
+ // includes conversion of return types from more specific to more
+ // generic (because std::function is covariant by return types and
+ // contravariant by argument types (see the link in the Doxygen
+ // part of the comments).
+ auto fn = lookup<fn_t>(std::forward<LookupParamTs>(lookupParams)...);
+ // 2. Passing the result of lookup() to boundArgs() invokes the
+ // lambda-expression mentioned in the constructor, which simply
+ // invokes this passed function with a set of arguments captured
+ // by lambda.
+ if (fn)
+ return boundArgs(std::move(fn));
+
+ // A shortcut to allow passing nullptr for a function;
+ // a default-constructed ResultT will be returned
+ // (for pointers, it will be nullptr)
+ return {};
+ }
+
+ private:
+ std::function<ResultT(fn_t&&)> boundArgs;
+ };
+
+ /**
+ * Dispatch a set of parameters to one of a set of functions, depending on
+ * a selector value
+ *
+ * Use <code>dispatch<CommonType>(parameters).to(lookup parameters)</code>
+ * instead of lookup() if you need to pick one of several functions returning
+ * types castable to the same CommonType. See event.cpp for a typical use case.
+ *
+ * \see Dispatch
+ */
+ template <typename ResultT, typename... ArgTs>
+ Dispatch<ResultT, ArgTs...> dispatch(ArgTs&& ... args)
+ {
+ return Dispatch<ResultT, ArgTs...>(std::forward<ArgTs>(args)...);
+ }
+
+ // The below enables pretty-printing of enums in logs
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
+#define REGISTER_ENUM(EnumName) Q_ENUM(EnumName)
+#else
+ // Thanks to Olivier for spelling it and for making Q_ENUM to replace it:
+ // https://woboq.com/blog/q_enum.html
+#define REGISTER_ENUM(EnumName) \
+ Q_ENUMS(EnumName) \
+ friend QDebug operator<<(QDebug dbg, EnumName val) \
+ { \
+ static int enumIdx = staticMetaObject.indexOfEnumerator(#EnumName); \
+ return dbg << Event::staticMetaObject.enumerator(enumIdx).valueToKey(int(val)); \
+ }
+#endif
+} // namespace QMatrixClient
+