diff options
author | Kitsune Ral <Kitsune-Ral@users.sf.net> | 2018-03-31 13:16:02 +0900 |
---|---|---|
committer | Kitsune Ral <Kitsune-Ral@users.sf.net> | 2018-03-31 14:23:55 +0900 |
commit | efeb50a46ad824aa258472f6ac8da74810f05a55 (patch) | |
tree | a89c6f35d56986c60e73f870530c9d6ee0527e6d /lib | |
parent | 29093379b707bfe620234c2968b37aa86666542a (diff) | |
download | libquotient-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')
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 + |