diff options
author | Kitsune Ral <Kitsune-Ral@users.sf.net> | 2017-11-28 12:42:03 +0900 |
---|---|---|
committer | Kitsune Ral <Kitsune-Ral@users.sf.net> | 2017-11-28 12:42:03 +0900 |
commit | 357625eb55e2f4569bb487ffe14a9236188e25f3 (patch) | |
tree | d39340ab74f25a23a5855f679973628f7457fd87 | |
parent | 94e6636d8225a0561ed7df3fa8081c5b0183610c (diff) | |
parent | 8f762a2458db773f6db24b568b2e944427297c2b (diff) | |
download | libquotient-357625eb55e2f4569bb487ffe14a9236188e25f3.tar.gz libquotient-357625eb55e2f4569bb487ffe14a9236188e25f3.zip |
Merge branch 'master' into kitsune-gtad
54 files changed, 1286 insertions, 984 deletions
@@ -1,2 +1,10 @@ build .kdev4 + +# Qt Creator project file +*.user + +# qmake derivatives +Makefile* +object_script.* +.qmake*
\ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 2abf0e2b..c4cd4326 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,10 @@ before_script: - cmake -DMATRIX_DOC_PATH="matrix-doc" -DGTAD_PATH="gtad/gtad" -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} .. - cmake --build . --target update-api -script: cmake --build . +script: + - cmake --build . + - cd .. + - qmake qmc-example.pro && make all notifications: webhooks: diff --git a/CMakeLists.txt b/CMakeLists.txt index d359214e..2be54cb0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,17 +65,15 @@ set(libqmatrixclient_SRCS logging.cpp room.cpp user.cpp + avatar.cpp settings.cpp events/event.cpp + events/eventcontent.cpp events/roommessageevent.cpp - events/roomnameevent.cpp - events/roomaliasesevent.cpp - events/roomcanonicalaliasevent.cpp events/roommemberevent.cpp - events/roomtopicevent.cpp + events/roomavatarevent.cpp events/typingevent.cpp events/receiptevent.cpp - events/encryptedevent.cpp jobs/basejob.cpp jobs/checkauthmethods.cpp jobs/passwordlogin.cpp @@ -113,7 +111,7 @@ aux_source_directory(jobs/generated libqmatrixclient_job_SRCS) set(example_SRCS examples/qmc-example.cpp) add_library(qmatrixclient ${libqmatrixclient_SRCS} ${libqmatrixclient_job_SRCS}) -set_property(TARGET qmatrixclient PROPERTY VERSION "0.0.0") +set_property(TARGET qmatrixclient PROPERTY VERSION "0.2.0") set_property(TARGET qmatrixclient PROPERTY SOVERSION 0 ) target_link_libraries(qmatrixclient Qt5::Core Qt5::Network Qt5::Gui) @@ -1,42 +1,46 @@ # Libqmatrixclient [![license](https://img.shields.io/github/license/QMatrixClient/libqmatrixclient.svg)](https://github.com/QMatrixClient/libqmatrixclient/blob/master/COPYING) +![status](https://img.shields.io/badge/status-beta-yellow.svg) +[![release](https://img.shields.io/github/release/QMatrixClient/libqmatrixclient/all.svg)](https://github.com/QMatrixClient/libqmatrixclient/releases/latest) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) -libqmatrixclient is a Qt-based library to make IM clients for the [Matrix](https://matrix.org) protocol. It is used by the Quaternion client and is a part of the Quaternion project. The below instructions are the same for Quaternion and libqmatrixclient (the source tree of Quaternion has most up-to-date instructions but this source tree strives to closely follow). +libqmatrixclient is a Qt5-based library to make IM clients for the [Matrix](https://matrix.org) protocol. It is the backbone of [Quaternion](https://github.com/QMatrixClient/Quaternion), [Tensor](https://matrix.org/docs/projects/client/tensor.html) and some other projects. ## Contacts -You can find authors of libqmatrixclient in the Quaternion Matrix room: [#quaternion:matrix.org](https://matrix.to/#/#quaternion:matrix.org). +You can find authors of libqmatrixclient in the Matrix room: [#qmatrixclient:matrix.org](https://matrix.to/#/#qmatrixclient:matrix.org). -Issues should be submitted to [the project's issue tracker](https://github.com/Fxrh/libqmatrixclient/issues). We do not guarantee a response but we usually try to at least acknowledge the issue +You can also file issues at [the project's issue tracker](https://github.com/QMatrixClient/libqmatrixclient/issues). If you have what looks like a security issue, please see respective instructions in CONTRIBUTING.md. +## Building and usage +So far the library is typically used as a git submodule of another project (such as Quaternion); however it can be built separately (both as a static and as a dynamic library). There is no specific installation sequence outside of other projects but since it's a CMake-based project, your mileage should be fairly short (in case it's not, issues and PRs are most welcome). + +The source code is hosted at GitHub: https://github.com/QMatrixClient/libqmatrixclient - checking out a certain commit or tag from GitHub (rather than downloading the archive) is the recommended way for packagers. + +There are very few tags so far; there will be more, as new versions are released. ## Pre-requisites a Linux, MacOS or Windows system (desktop versions tried; mobile Linux/Windows might work too) - a Git client (to check out this repo) -- CMake (from your package management system or [the official website](https://cmake.org/download/)) -- Qt 5 (either Open Source or Commercial), version 5.2.1 or higher as of this writing (check the CMakeLists.txt for most up-to-date information). Qt 5.3 or higher recommended on Windows. +- Qt 5 (either Open Source or Commercial), version 5.2.1 or higher as of this writing (check `CMakeLists.txt` for most up-to-date information) +- qmake (from the Qt 5 installation) or CMake (from your package management system or [the official website](https://cmake.org/download/)). - a C++ toolchain supported by your version of Qt (see a link for your platform at [the Qt's platform requirements page](http://doc.qt.io/qt-5/gettingstarted.html#platform-requirements)) - GCC 4.8, Clang 3.5.0, Visual C++ 2015 are the oldest officially supported as of this writing -## Installing pre-requisites -### Linux -Just install things from "Pre-requisites" using your preferred package manager. If your Qt package base is fine-grained you might want to take a look at `CMakeLists.txt` to figure out which specific libraries libqmatrixclient uses (or blindly run cmake and look at error messages). +#### Linux +Just install things from the list above using your preferred package manager. If your Qt package base is fine-grained you might want to take a look at `CMakeLists.txt` to figure out which specific libraries libqmatrixclient uses (or blindly run cmake and look at error messages). The library is entirely offscreen (Qt::Core and Qt::Network are essential) but it also depends on Qt::Gui in order to operate with avatar thumbnails. -### OS X -`brew install qt5` should get you Qt5. You may need to tell CMake about the path to Qt by passing `-DCMAKE_PREFIX_PATH=<where-Qt-installed>` +#### OS X +`brew install qt5` should get you Qt5. If you plan to use CMake, you may need to tell it about the path to Qt by passing `-DCMAKE_PREFIX_PATH=<where-Qt-installed>` -### Windows -1. Install a Git client and CMake. The commands here imply that git and cmake are in your PATH - otherwise you have to prepend them with your actual paths. -1. Install Qt5, using their official installer. If for some reason you need to use Qt 5.2.1, select its Add-ons component in the installer as well; for later versions, no extras are needed. If you don't have a toolchain and/or IDE, you can easily get one by selecting Qt Creator and at least one toolchain under Qt Creator. -1. Make sure CMake knows about Qt and the toolchain - the easiest way is to run a qtenv2.bat script that can be found in `C:\Qt\<Qt version>\<toolchain>\bin` (assuming you installed Qt to `C:\Qt`). The only thing it does is adding necessary paths to PATH - you might not want to run it on system startup but it's very handy to setup environment before building. Setting CMAKE_PREFIX_PATH, the same way as for OS X (see above), also helps. +#### Windows +1. Install Qt5, using their official installer. If for some reason you need to use Qt 5.2.1, select its Add-ons component in the installer as well; for later versions, no extras are needed. If you don't have a toolchain and/or IDE, you can easily get one by selecting Qt Creator and at least one toolchain under Qt Creator. Qt 5.3 is recommended on Windows; `windeployqt` in Qt 5.2.1 is not functional enough to generate a proper list of files for installing. +1. If you plan to build with CMake, install CMake; if you're ok with qmake, you don't need to install anything on top of Qt. The commands in further sections imply that cmake/qmake is in your PATH - otherwise you have to prepend those commands with actual paths. As an option, it's a good idea to run a `qtenv2.bat` script that can be found in `C:\Qt\<Qt version>\<toolchain>\bin` (assuming you installed Qt to `C:\Qt`); the only thing it does is adding necessary paths to PATH. You might not want to run that script on system startup but it's very handy to setup the environment before building. For CMake, setting `CMAKE_PREFIX_PATH` in the same way as for OS X (see above), also helps. -There are no official MinGW-based 64-bit packages for Qt. If you're determined to build 64-bit libqmatrixclient, either use a Visual Studio toolchain or build Qt5 yourself as described in Qt documentation. +There are no official MinGW-based 64-bit packages for Qt. If you're determined to build a 64-bit library, either use a Visual Studio toolchain or build Qt5 yourself as described in Qt documentation. -## Source code -To get all necessary sources, simply clone the GitHub repo. If you have cloned Quaternion or Tensor sources with `--recursive`, you already have libqmatrixclient in the respective `lib` subdirectory. - -## Building +## Build +### CMake-based In the root directory of the project sources: ``` mkdir build_dir @@ -46,6 +50,14 @@ cmake --build . --target all ``` This will get you the compiled library in `build_dir` inside your project sources. Only static builds of libqmatrixclient are tested at the moment; experiments with dynamic builds are welcome. The two known projects to link with libqmatrixclient are Tensor and Quaternion; you should take a look at their source code before doing anything with libqmatrixclient on your own. +### qmake-based +The library only provides a .pri file with an intention to be included from a bigger project's .pro file. As a starting point you can use `qmc-example.pro` that will build a minimal example of library usage for you. In the root directory of the project sources: +``` +qmake qmc-example.pro +make all +``` +This will get you `debug/qmc-example` and `release/qmc-example` console executables that login to the Matrix server at matrix.org with credentials of your choosing (pass the username and password as arguments) and run a sync long-polling loop, showing some information about received events. + ## Troubleshooting If `cmake` fails with... @@ -69,4 +81,4 @@ where `*` can be used as a wildcard for any part between two dots, and comma is used for a separator. Latter statements override former ones, so if you want to switch on all debug logs except `jobs` you can set ``` QT_LOGGING_RULES="libqmatrixclient.*.debug=true,libqmatrixclient.jobs.debug=false" -```
\ No newline at end of file +``` diff --git a/avatar.cpp b/avatar.cpp new file mode 100644 index 00000000..f5101ddb --- /dev/null +++ b/avatar.cpp @@ -0,0 +1,84 @@ +/****************************************************************************** + * 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" + +using namespace QMatrixClient; + +QPixmap Avatar::get(int width, int height, Avatar::notifier_t notifier) +{ + QSize size(width, height); + + // 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( ( !(_valid || _ongoingRequest) + || width > _requestedSize.width() + || height > _requestedSize.height() ) && _url.isValid() ) + { + qCDebug(MAIN) << "Getting avatar from" << _url.toString(); + _requestedSize = size; + if (_ongoingRequest) + _ongoingRequest->abandon(); + notifiers.emplace_back(std::move(notifier)); + _ongoingRequest = _connection->callApi<MediaThumbnailJob>(_url, size); + _ongoingRequest->connect( _ongoingRequest, &MediaThumbnailJob::finished, + _connection, [=]() { + if (_ongoingRequest->status().good()) + { + _valid = true; + _originalPixmap = _ongoingRequest->scaledThumbnail(_requestedSize); + _scaledPixmaps.clear(); + for (auto n: notifiers) + n(); + } + _ongoingRequest = nullptr; + }); + } + + if( _originalPixmap.isNull() ) + { + if (_defaultIcon.isNull()) + return _originalPixmap; + + _originalPixmap = _defaultIcon.pixmap(size); + } + + for (auto p: _scaledPixmaps) + if (p.first == size) + return p.second; + auto pixmap = _originalPixmap.scaled(size, + Qt::KeepAspectRatio, Qt::SmoothTransformation); + _scaledPixmaps.emplace_back(size, pixmap); + return pixmap; +} + +bool Avatar::updateUrl(const QUrl& newUrl) +{ + if (newUrl == _url) + return false; + + _url = newUrl; + _valid = false; + return true; +} diff --git a/events/roomtopicevent.h b/avatar.h index 95ad0e04..60cf3779 100644 --- a/events/roomtopicevent.h +++ b/avatar.h @@ -1,5 +1,5 @@ /****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> + * 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 @@ -18,33 +18,41 @@ #pragma once -#include "event.h" +#include <QtGui/QIcon> +#include <QtCore/QUrl> + +#include <functional> namespace QMatrixClient { - class RoomTopicEvent: public RoomEvent + class MediaThumbnailJob; + class Connection; + + class Avatar { public: - explicit RoomTopicEvent(const QString& topic) - : RoomEvent(Type::RoomTopic), _topic(topic) - { } - explicit RoomTopicEvent(const QJsonObject& obj) - : RoomEvent(Type::RoomTopic, obj) - , _topic(contentJson()["topic"].toString()) + explicit Avatar(Connection* connection, QIcon defaultIcon = {}) + : _defaultIcon(std::move(defaultIcon)), _connection(connection) { } - QString topic() const { return _topic; } + using notifier_t = std::function<void()>; - QJsonObject toJson() const - { - QJsonObject obj; - obj.insert("topic", _topic); - return obj; - } + QPixmap get(int w, int h, notifier_t notifier); - static constexpr const char* TypeId = "m.room.topic"; + QUrl url() const { return _url; } + bool updateUrl(const QUrl& newUrl); private: - QString _topic; + QUrl _url; + QPixmap _originalPixmap; + QIcon _defaultIcon; + + std::vector<QPair<QSize, QPixmap>> _scaledPixmaps; + + QSize _requestedSize; + bool _valid = false; + Connection* _connection; + MediaThumbnailJob* _ongoingRequest = nullptr; + std::vector<notifier_t> notifiers; }; } // namespace QMatrixClient diff --git a/connection.cpp b/connection.cpp index 427118c9..d2641353 100644 --- a/connection.cpp +++ b/connection.cpp @@ -37,6 +37,7 @@ #include <QtCore/QStandardPaths> #include <QtCore/QStringBuilder> #include <QtCore/QElapsedTimer> +#include <QtCore/QRegularExpression> using namespace QMatrixClient; @@ -46,15 +47,13 @@ class Connection::Private explicit Private(const QUrl& serverUrl) : q(nullptr) , data(new ConnectionData(serverUrl)) - , syncJob(nullptr) { } Q_DISABLE_COPY(Private) Private(Private&&) = delete; Private operator=(Private&&) = delete; - ~Private() { delete data; } Connection* q; - ConnectionData* data; + 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 @@ -63,9 +62,12 @@ class Connection::Private QHash<QString, User*> userMap; QString userId; - SyncJob* syncJob; + SyncJob* syncJob = nullptr; bool cacheState = true; + + void connectWithToken(const QString& user, const QString& accessToken, + const QString& deviceId); }; Connection::Connection(const QUrl& server, QObject* parent) @@ -87,55 +89,135 @@ Connection::~Connection() delete d; } -void Connection::resolveServer(const QString& domain) +void Connection::resolveServer(const QString& mxidOrDomain) { - // Find the Matrix server for the given domain. - QScopedPointer<QDnsLookup, QScopedPointerDeleteLater> dns { new QDnsLookup() }; + // 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)); + if (!match.hasMatch() || !maybeBaseUrl.isValid()) + { + emit resolveError( + tr("%1 is not a valid homeserver address") + .arg(maybeBaseUrl.toString())); + return; + } + + maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http" + if (maybeBaseUrl.port() != -1) + { + setHomeserver(maybeBaseUrl); + emit resolved(); + return; + } + + auto domain = maybeBaseUrl.host(); + qCDebug(MAIN) << "Resolving 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); - dns->lookup(); - connect(dns.data(), &QDnsLookup::finished, [&]() { - // Check the lookup succeeded. - if (dns->error() != QDnsLookup::NoError || - dns->serviceRecords().isEmpty()) { - emit resolveError("DNS lookup failed"); - return; + 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"; } - - // Handle the results. - auto record = dns->serviceRecords().front(); - d->data->setHost(record.target()); - d->data->setPort(record.port()); + setHomeserver(baseUrl); emit resolved(); + dns->deleteLater(); }); + dns->lookup(); } void Connection::connectToServer(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*/ "", + 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, [=] () { - connectWithToken(loginJob->user_id(), loginJob->access_token(), - loginJob->device_id()); - }); - connect( loginJob, &BaseJob::failure, [=] () { - emit loginError(loginJob->errorString()); - }); + connect(loginJob, &BaseJob::success, this, + [=] { + d->connectWithToken(loginJob->user_id(), loginJob->access_token(), + loginJob->device_id()); + }); + connect(loginJob, &BaseJob::failure, this, + [=] { + emit loginError(loginJob->errorString()); + }); } void Connection::connectWithToken(const QString& userId, - const QString& accessToken, const QString& deviceId) + 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) { - d->userId = userId; - d->data->setToken(accessToken); - d->data->setDeviceId(deviceId); - qCDebug(MAIN) << "Using server" << d->data->baseUrl() << "by user" << userId - << "from device" << deviceId; - emit connected(); + userId = user; + data->setToken(accessToken.toLatin1()); + data->setDeviceId(deviceId); + qCDebug(MAIN) << "Using server" << data->baseUrl() << "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() @@ -290,7 +372,7 @@ QString Connection::userId() const return d->userId; } -const QString& Connection::deviceId() const +QString Connection::deviceId() const { return d->data->deviceId(); } @@ -331,7 +413,7 @@ QHash< QPair<QString, bool>, Room* > Connection::roomMap() const const ConnectionData* Connection::connectionData() const { - return d->data; + return d->data.get(); } Room* Connection::provideRoom(const QString& id, JoinState joinState) @@ -343,22 +425,11 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState) return nullptr; } - // Room transitions: - // 1. none -> Invite: r=createRoom, emit invitedRoom(r,null) - // 2. none -> Join: r=createRoom, emit joinedRoom(r,null) - // 3. none -> Leave: r=createRoom, emit leftRoom(r,null) - // 4. inv=Invite -> Join: r=createRoom, emit joinedRoom(r,inv), delete Invite - // 4a. Leave, inv=Invite -> Join: change state, emit joinedRoom(r,inv), delete Invite - // 5. inv=Invite -> Leave: r=createRoom, emit leftRoom(r,inv), delete Invite - // 5a. r=Leave, inv=Invite -> Leave: emit leftRoom(r,inv), delete Invite - // 6. Join -> Leave: change state - // 7. r=Leave -> Invite: inv=createRoom, emit invitedRoom(inv,r) - // 8. Leave -> (changes to) Join 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) above + // 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) @@ -408,11 +479,24 @@ Connection::room_factory_t Connection::createRoom = Connection::user_factory_t Connection::createUser = [](Connection* c, const QString& id) { return new User(id, c); }; + QByteArray Connection::generateTxnId() { return d->data->generateTxnId(); } +void Connection::setHomeserver(const QUrl& url) +{ + if (d->data->baseUrl() == url) + return; + + d->data->setBaseUrl(url); + emit homeserverChanged(url); +} + +static constexpr int CACHE_VERSION_MAJOR = 1; +static constexpr int CACHE_VERSION_MINOR = 0; + void Connection::saveState(const QUrl &toFile) const { if (!d->cacheState) @@ -458,6 +542,11 @@ void Connection::saveState(const QUrl &toFile) const rootObj.insert("next_batch", d->data->lastEvent()); rootObj.insert("rooms", roomObj); + QJsonObject versionObj; + versionObj.insert("major", CACHE_VERSION_MAJOR); + versionObj.insert("minor", CACHE_VERSION_MINOR); + rootObj.insert("cache_version", versionObj); + QByteArray data = QJsonDocument(rootObj).toJson(QJsonDocument::Compact); qCDebug(MAIN) << "Writing state to file" << outfile.fileName(); @@ -483,8 +572,22 @@ void Connection::loadState(const QUrl &fromFile) file.open(QFile::ReadOnly); QByteArray data = file.readAll(); + auto jsonDoc = QJsonDocument::fromJson(data); + 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(QJsonDocument::fromJson(data)); + sync.parseJson(jsonDoc); onSyncSuccess(std::move(sync)); qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et.elapsed() << "ms"; @@ -511,3 +614,4 @@ void Connection::setCacheState(bool newValue) emit cacheStateChanged(); } } + diff --git a/connection.h b/connection.h index 2a107b43..256dbd5f 100644 --- a/connection.h +++ b/connection.h @@ -27,6 +27,8 @@ #include <functional> +class QDnsLookup; + namespace QMatrixClient { class Room; @@ -61,27 +63,6 @@ namespace QMatrixClient QHash<QPair<QString, bool>, Room*> roomMap() const; - // Old API that will be abolished any time soon. DO NOT USE. - - /** @deprecated Use callApi<PostMessageJob>() or Room::postMessage() instead */ - Q_INVOKABLE virtual void postMessage(Room* room, const QString& type, - const QString& message) const; - /** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead */ - Q_INVOKABLE virtual PostReceiptJob* postReceipt(Room* room, - RoomEvent* event) const; - /** @deprecated Use callApi<JoinRoomJob>() instead */ - Q_INVOKABLE virtual JoinRoomJob* joinRoom(const QString& roomAlias); - /** @deprecated Use callApi<LeaveRoomJob>() or Room::leaveRoom() instead */ - Q_INVOKABLE virtual void leaveRoom( Room* room ); - /** @deprecated User callApi<RoomMessagesJob>() or Room::getPreviousContent() instead */ - Q_INVOKABLE virtual RoomMessagesJob* getMessages(Room* room, - const QString& from) const; - /** @deprecated Use callApi<MediaThumbnailJob>() instead */ - virtual MediaThumbnailJob* getThumbnail(const QUrl& url, - QSize requestedSize) const; - /** @deprecated Use callApi<MediaThumbnailJob>() instead */ - MediaThumbnailJob* getThumbnail(const QUrl& url, int requestedWidth, - int requestedHeight) const; /** 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. @@ -96,11 +77,13 @@ namespace QMatrixClient */ ForgetRoomJob* forgetRoom(const QString& id); + // FIXME: Convert Q_INVOKABLEs to Q_PROPERTIES + // (breaks back-compatibility) Q_INVOKABLE QUrl homeserver() const; Q_INVOKABLE User* user(const QString& userId); Q_INVOKABLE User* user(); Q_INVOKABLE QString userId() const; - Q_INVOKABLE const QString& deviceId() const; + Q_INVOKABLE QString deviceId() const; /** @deprecated Use accessToken() instead. */ Q_INVOKABLE QString token() const; Q_INVOKABLE QString accessToken() const; @@ -174,12 +157,17 @@ namespace QMatrixClient } public slots: - void resolveServer(const QString& domain); + /** 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 = {}); + const QString& initialDeviceName, + const QString& deviceId = {}); void connectWithToken(const QString& userId, const QString& accessToken, - const QString& deviceId); + const QString& deviceId); /** @deprecated Use stopSync() instead */ void disconnectFromServer() { stopSync(); } @@ -188,24 +176,109 @@ namespace QMatrixClient void sync(int timeout = -1); void stopSync(); + virtual MediaThumbnailJob* getThumbnail(const QUrl& url, + QSize requestedSize) const; + MediaThumbnailJob* getThumbnail(const QUrl& url, + int requestedWidth, + int requestedHeight) const; + + // 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<JoinRoomJob>() instead */ + virtual JoinRoomJob* joinRoom(const QString& roomAlias); + /** @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(); + void reconnected(); //< Unused; use connected() instead void loggedOut(); + void loginError(QString error); + void networkError(size_t nextAttempt, int inMilliseconds); void syncDone(); + void syncError(QString error); + + /** + * \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); - void aboutToDeleteRoom(Room* room); - void loginError(QString error); - void networkError(size_t nextAttempt, int inMilliseconds); - void resolveError(QString error); - void syncError(QString error); - //void jobError(BaseJob* job); + /** The room object is about to be deleted */ + void aboutToDeleteRoom(Room* room); void cacheStateChanged(); @@ -236,6 +309,23 @@ namespace QMatrixClient class Private; 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 createRoom; static user_factory_t createUser; }; diff --git a/connectiondata.cpp b/connectiondata.cpp index 9b9b6e04..70791952 100644 --- a/connectiondata.cpp +++ b/connectiondata.cpp @@ -24,16 +24,22 @@ using namespace QMatrixClient; -QNetworkAccessManager* getNam() +QNetworkAccessManager* createNam() { - static QNetworkAccessManager* _nam = new QNetworkAccessManager(); - return _nam; + auto nam = new QNetworkAccessManager(); + // 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; } struct ConnectionData::Private { QUrl baseUrl; - QString accessToken; + QByteArray accessToken; QString lastEvent; QString deviceId; @@ -44,6 +50,7 @@ struct ConnectionData::Private ConnectionData::ConnectionData(QUrl baseUrl) : d(new Private) { + nam(); // Just to ensure NAM is created d->baseUrl = baseUrl; } @@ -52,7 +59,7 @@ ConnectionData::~ConnectionData() delete d; } -QString ConnectionData::accessToken() const +QByteArray ConnectionData::accessToken() const { return d->accessToken; } @@ -64,10 +71,17 @@ QUrl ConnectionData::baseUrl() const QNetworkAccessManager* ConnectionData::nam() const { - return getNam(); + static auto nam = createNam(); + return nam; } -void ConnectionData::setToken(QString token) +void ConnectionData::setBaseUrl(QUrl baseUrl) +{ + d->baseUrl = baseUrl; + qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; +} + +void ConnectionData::setToken(QByteArray token) { d->accessToken = token; } diff --git a/connectiondata.h b/connectiondata.h index 52a7461c..530a52ee 100644 --- a/connectiondata.h +++ b/connectiondata.h @@ -30,12 +30,13 @@ namespace QMatrixClient explicit ConnectionData(QUrl baseUrl); virtual ~ConnectionData(); - QString accessToken() const; + QByteArray accessToken() const; QUrl baseUrl() const; const QString& deviceId() const; QNetworkAccessManager* nam() const; - void setToken( QString accessToken ); + void setBaseUrl(QUrl baseUrl); + void setToken(QByteArray accessToken); void setHost( QString host ); void setPort( int port ); void setDeviceId(const QString& deviceId); diff --git a/jobs/converters.h b/converters.h index f6e850c6..733c2c0e 100644 --- a/jobs/converters.h +++ b/converters.h @@ -118,4 +118,19 @@ namespace QMatrixClient return vect; } }; + + template <typename T> struct FromJson<QList<T>> + { + QList<T> operator()(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>> { }; + } // namespace QMatrixClient diff --git a/events/encryptedevent.cpp b/events/encryptedevent.cpp deleted file mode 100644 index 90e77c36..00000000 --- a/events/encryptedevent.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// -// Created by rusakov on 26/09/2017. -// - -#include "encryptedevent.h" diff --git a/events/event.cpp b/events/event.cpp index 304f2af6..44b742c1 100644 --- a/events/event.cpp +++ b/events/event.cpp @@ -19,14 +19,11 @@ #include "event.h" #include "roommessageevent.h" -#include "roomnameevent.h" -#include "roomaliasesevent.h" -#include "roomcanonicalaliasevent.h" +#include "simplestateevents.h" #include "roommemberevent.h" -#include "roomtopicevent.h" +#include "roomavatarevent.h" #include "typingevent.h" #include "receiptevent.h" -#include "encryptedevent.h" #include "logging.h" #include <QtCore/QJsonDocument> @@ -53,32 +50,24 @@ QJsonObject Event::originalJsonObject() const return _originalJson; } -QDateTime Event::toTimestamp(const QJsonValue& v) +const QJsonObject Event::contentJson() const { - Q_ASSERT(v.isDouble() || v.isNull() || v.isUndefined()); - return QDateTime::fromMSecsSinceEpoch( - static_cast<long long int>(v.toDouble()), Qt::UTC); + return _originalJson["content"].toObject(); } -QStringList Event::toStringList(const QJsonValue& v) +template <typename BaseEventT> +inline BaseEventT* makeIfMatches(const QJsonObject&, const QString&) { - Q_ASSERT(v.isArray() || v.isNull() || v.isUndefined()); - - QStringList l; - for( const QJsonValue& e : v.toArray() ) - l.push_back(e.toString()); - return l; + return nullptr; } -const QJsonObject Event::contentJson() const +template <typename BaseEventT, typename EventT, typename... EventTs> +inline BaseEventT* makeIfMatches(const QJsonObject& o, const QString& selector) { - return _originalJson["content"].toObject(); -} + if (selector == EventT::TypeId) + return new EventT(o); -template <typename EventT> -EventT* make(const QJsonObject& o) -{ - return new EventT(o); + return makeIfMatches<BaseEventT, EventTs...>(o, selector); } Event* Event::fromJson(const QJsonObject& obj) @@ -87,17 +76,14 @@ Event* Event::fromJson(const QJsonObject& obj) if (auto e = RoomEvent::fromJson(obj)) return e; - return dispatch<Event*>(obj).to(obj["type"].toString(), - "m.typing", make<TypingEvent>, - "m.receipt", make<ReceiptEvent>, - /* Insert new event types (except room events) BEFORE this line */ - nullptr - ); + return makeIfMatches<Event, + TypingEvent, ReceiptEvent>(obj, obj["type"].toString()); } RoomEvent::RoomEvent(Type type, const QJsonObject& rep) : Event(type, rep), _id(rep["event_id"].toString()) - , _serverTimestamp(toTimestamp(rep["origin_server_ts"])) + , _serverTimestamp( + QMatrixClient::fromJson<QDateTime>(rep["origin_server_ts"])) , _roomId(rep["room_id"].toString()) , _senderId(rep["sender"].toString()) , _txnId(rep["unsigned"].toObject().value("transactionId").toString()) @@ -129,15 +115,8 @@ void RoomEvent::addId(const QString& id) RoomEvent* RoomEvent::fromJson(const QJsonObject& obj) { - return dispatch<RoomEvent*>(obj).to(obj["type"].toString(), - "m.room.message", make<RoomMessageEvent>, - "m.room.name", make<RoomNameEvent>, - "m.room.aliases", make<RoomAliasesEvent>, - "m.room.canonical_alias", make<RoomCanonicalAliasEvent>, - "m.room.member", make<RoomMemberEvent>, - "m.room.topic", make<RoomTopicEvent>, - "m.room.encryption", make<EncryptionEvent>, - /* Insert new ROOM event types BEFORE this line */ - nullptr - ); + return makeIfMatches<RoomEvent, + RoomMessageEvent, RoomNameEvent, RoomAliasesEvent, + RoomCanonicalAliasEvent, RoomMemberEvent, RoomTopicEvent, + RoomAvatarEvent, EncryptionEvent>(obj, obj["type"].toString()); } diff --git a/events/event.h b/events/event.h index ec993522..cc99b57b 100644 --- a/events/event.h +++ b/events/event.h @@ -31,11 +31,18 @@ namespace QMatrixClient { Q_GADGET public: - enum class Type + enum class Type : quint16 { - RoomMessage, RoomName, RoomAliases, RoomCanonicalAlias, - RoomMember, RoomTopic, RoomEncryption, RoomEncryptedMessage, - Typing, Receipt, Unknown + Unknown = 0, + Typing, Receipt, + RoomEventBase = 0x1000, + RoomMessage = RoomEventBase + 1, + RoomEncryptedMessage, + RoomStateEventBase = 0x1800, + RoomName = RoomStateEventBase + 1, + RoomAliases, RoomCanonicalAlias, RoomMember, RoomTopic, + RoomAvatar, RoomEncryption, + Reserved = 0x2000 }; explicit Event(Type type) : _type(type) { } @@ -43,6 +50,10 @@ namespace QMatrixClient Event(const Event&) = delete; Type type() const { return _type; } + bool isStateEvent() const + { + return (quint16(_type) & 0x1800) == 0x1800; + } QByteArray originalJson() const; QJsonObject originalJsonObject() const; @@ -52,12 +63,13 @@ namespace QMatrixClient // (and in most cases it will be a combination of other fields // instead of "content" field). + /** Create an event with proper type from a JSON object + * Use this factory to detect the type from the JSON object contents + * and create an event object of that type. + */ static Event* fromJson(const QJsonObject& obj); protected: - static QDateTime toTimestamp(const QJsonValue& v); - static QStringList toStringList(const QJsonValue& v); - const QJsonObject contentJson() const; private: @@ -69,31 +81,27 @@ namespace QMatrixClient Q_PROPERTY(QJsonObject contentJson READ contentJson CONSTANT) }; using EventType = Event::Type; - template <typename EventT> - using EventsBatch = std::vector<EventT*>; - using Events = EventsBatch<Event>; - template <typename BaseEventT> - BaseEventT* makeEvent(const QJsonObject& obj) - { - if (auto e = BaseEventT::fromJson(obj)) - return e; - - return new BaseEventT(EventType::Unknown, obj); - } - - template <typename BaseEventT = Event, - typename BatchT = EventsBatch<BaseEventT> > - BatchT makeEvents(const QJsonArray& objs) + template <typename EventT> + class EventsBatch : public std::vector<EventT*> { - BatchT evs; - // The below line accommodates the difference in size types of - // STL and Qt containers. - evs.reserve(static_cast<typename BatchT::size_type>(objs.size())); - for (auto obj: objs) - evs.push_back(makeEvent<BaseEventT>(obj.toObject())); - return evs; - } + public: + void fromJson(const QJsonObject& container, const QString& node) + { + const auto objs = container.value(node).toArray(); + using size_type = typename std::vector<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) + { + const auto o = objValue.toObject(); + auto e = EventT::fromJson(o); + this->push_back(e ? e : new EventT(EventType::Unknown, o)); + } + } + }; + using Events = EventsBatch<Event>; /** This class corresponds to m.room.* events */ class RoomEvent : public Event @@ -146,6 +154,41 @@ namespace QMatrixClient QString _txnId; }; using RoomEvents = EventsBatch<RoomEvent>; + + template <typename ContentT> + class StateEvent: public RoomEvent + { + public: + using content_type = ContentT; + + template <typename... ContentParamTs> + explicit StateEvent(Type type, const QJsonObject& obj, + ContentParamTs&&... contentParams) + : RoomEvent(obj.contains("state_key") ? type : Type::Unknown, + obj) + , _content(contentJson(), + std::forward<ContentParamTs>(contentParams)...) + { + if (obj.contains("prev_content")) + _prev.reset(new ContentT( + obj["prev_content"].toObject(), + std::forward<ContentParamTs>(contentParams)...)); + } + template <typename... ContentParamTs> + explicit StateEvent(Type type, ContentParamTs&&... contentParams) + : RoomEvent(type) + , _content(std::forward<ContentParamTs>(contentParams)...) + { } + + QJsonObject toJson() const { return _content.toJson(); } + + ContentT content() const { return _content; } + ContentT* prev_content() const { return _prev.data(); } + + protected: + ContentT _content; + QScopedPointer<ContentT> _prev; + }; } // namespace QMatrixClient Q_DECLARE_OPAQUE_POINTER(QMatrixClient::Event*) Q_DECLARE_METATYPE(QMatrixClient::Event*) diff --git a/events/eventcontent.cpp b/events/eventcontent.cpp new file mode 100644 index 00000000..dcbccf08 --- /dev/null +++ b/events/eventcontent.cpp @@ -0,0 +1,63 @@ +/****************************************************************************** + * 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; +} + +QJsonObject InfoBase::toInfoJson() const +{ + QJsonObject info; + fillInfoJson(&info); + return info; +} + +void InfoBase::fillInfoJson(QJsonObject*) const { } + +FileInfo::FileInfo(const QUrl& u, int payloadSize, const QMimeType& mimeType, + const QString& originalFilename) + : InfoBase(mimeType), url(u), payloadSize(payloadSize) + , originalName(originalFilename) +{ } + +FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, + const QString& originalFilename) + : FileInfo(u, infoJson["size"].toInt(), + QMimeDatabase().mimeTypeForName(infoJson["mimetype"].toString()), + originalFilename) +{ + if (!mimeType.isValid()) + mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); +} + +void FileInfo::fillInfoJson(QJsonObject* infoJson) const +{ + Q_ASSERT(infoJson); + infoJson->insert("size", payloadSize); + infoJson->insert("mimetype", mimeType.name()); +} diff --git a/events/eventcontent.h b/events/eventcontent.h new file mode 100644 index 00000000..60437995 --- /dev/null +++ b/events/eventcontent.h @@ -0,0 +1,295 @@ +/****************************************************************************** + * 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/QJsonObject> +#include <QtCore/QMimeType> +#include <QtCore/QUrl> +#include <QtCore/QSize> + +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. fillJson() should only fill the main JSON object + * but not the "info" subobject if it exists for a certain content type; + * use \p InfoBase to de/serialize "info" parts with an optional URL + * on the top level. + */ + class Base + { + public: + virtual ~Base() = default; + + QJsonObject toJson() const; + + protected: + virtual void fillJson(QJsonObject* o) const = 0; + }; + + class TypedBase: public Base + { + public: + virtual QMimeType type() 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) + : value(QMatrixClient::fromJson<T>(json[keyName])) + , key(std::move(keyName)) + { } + + T value; + + protected: + QString key; + + private: + void fillJson(QJsonObject* json) const override + { + Q_ASSERT(json); + json->insert(key, QMatrixClient::toJson(value)); + } + }; + + /** + * A base class for content types that have an "info" object in their + * JSON representation + * + * These include most multimedia types currently in the CS API spec. + * Derived classes should override fillInfoJson() to fill the "info" + * subobject, BUT NOT the main JSON object. Most but not all "info" + * classes (specifically, those deriving from FileInfo) should also + * have a constructor that accepts two parameters, QUrl and QJsonObject, + * in order to load the URL+info part from JSON. + */ + class InfoBase + { + public: + virtual ~InfoBase() = default; + + QJsonObject toInfoJson() const; + + QMimeType mimeType; + + protected: + InfoBase() = default; + explicit InfoBase(const QMimeType& type) : mimeType(type) { } + + virtual void fillInfoJson(QJsonObject* /*infoJson*/) const = 0; + }; + + // 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. + + /** + * Base class for content types that consist of a URL along with + * additional information. Most of message types except textual fall + * under this category. + */ + class FileInfo: public InfoBase + { + 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 = {}); + + QUrl url; + int payloadSize; + QString originalName; + + protected: + void fillInfoJson(QJsonObject* infoJson) const override; + }; + + /** + * A base class for image info types: image, thumbnail, video + * + * \tparam InfoT base info class; should derive from \p InfoBase + */ + template <class InfoT = FileInfo> + class ImageInfo : public InfoT + { + public: + explicit ImageInfo(const QUrl& u, int fileSize = -1, + QMimeType mimeType = {}, + const QSize& imageSize = {}) + : InfoT(u, fileSize, mimeType), imageSize(imageSize) + { } + ImageInfo(const QUrl& u, const QJsonObject& infoJson, + const QString& originalFilename = {}) + : InfoT(u, infoJson, originalFilename) + , imageSize(infoJson["w"].toInt(), infoJson["h"].toInt()) + { } + + QSize imageSize; + + protected: + void fillInfoJson(QJsonObject* infoJson) const override + { + InfoT::fillInfoJson(infoJson); + infoJson->insert("w", imageSize.width()); + infoJson->insert("h", imageSize.height()); + } + }; + + /** + * A base class for an info type that carries a thumbnail + * + * This class decorates the underlying type, adding ability to save/load + * a thumbnail to/from "info" subobject of the JSON representation of + * event content; namely, "info/thumbnail_url" and "info/thumbnail_info" + * fields are used. + * + * \tparam InfoT base info class; should derive from \p InfoBase + */ + template <class InfoT = InfoBase> + class Thumbnailed : public InfoT + { + public: + template <typename... ArgTs> + explicit Thumbnailed(const ImageInfo<>& thumbnail, + ArgTs&&... infoArgs) + : InfoT(std::forward<ArgTs>(infoArgs)...) + , thumbnail(thumbnail) + { } + + explicit Thumbnailed(const QJsonObject& infoJson) + : thumbnail(infoJson["thumbnail_url"].toString(), + infoJson["thumbnail_info"].toObject()) + { } + + Thumbnailed(const QUrl& u, const QJsonObject& infoJson, + const QString& originalFilename = {}) + : InfoT(u, infoJson, originalFilename) + , thumbnail(infoJson["thumbnail_url"].toString(), + infoJson["thumbnail_info"].toObject()) + { } + + ImageInfo<> thumbnail; + + protected: + void fillInfoJson(QJsonObject* infoJson) const override + { + InfoT::fillInfoJson(infoJson); + infoJson->insert("thumbnail_url", thumbnail.url.toString()); + infoJson->insert("thumbnail_info", thumbnail.toInfoJson()); + } + }; + + /** + * One more facility base class for content types that have a URL and + * additional info + * + * Types that derive from UrlWith<InfoT> take "url" and, optionally, + * "filename" values from the top-level JSON object and the rest of + * information from the "info" subobject. + * + * \tparam InfoT base info class; should derive from \p FileInfo or + * provide a constructor with a compatible signature + */ + template <class InfoT> // InfoT : public FileInfo + class UrlWith : public TypedBase, public InfoT + { + public: + using InfoT::InfoT; + explicit UrlWith(const QJsonObject& json) + : InfoT(json["url"].toString(), json["info"].toObject(), + json["filename"].toString()) + { } + + QMimeType type() const override { return InfoT::mimeType; } + + 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", InfoT::toInfoJson()); + } + }; + + /** + * Content class for m.image + * + * Available fields: + * - corresponding to the top-level JSON: + * - url + * - filename (extension to the spec) + * - corresponding to the "info" subobject: + * - payloadSize ("size" in JSON) + * - mimeType ("mimetype" in JSON) + * - imageSize (QSize for a combination of "h" and "w" in JSON) + * - thumbnail.url ("thumbnail_url" in JSON) + * - corresponding to the "info/thumbnail_info" subobject: contents of + * thumbnail field, in the same vein as for the main image: + * - payloadSize + * - mimeType + * - imageSize + */ + using ImageContent = UrlWith<Thumbnailed<ImageInfo<>>>; + + /** + * Content class for m.file + * + * Available fields: + * - corresponding to the top-level JSON: + * - url + * - filename + * - corresponding to the "info" subobject: + * - payloadSize ("size" in JSON) + * - mimeType ("mimetype" in JSON) + * - thumbnail.url ("thumbnail_url" in JSON) + * - corresponding to the "info/thumbnail_info" subobject: + * - thumbnail.payloadSize + * - thumbnail.mimeType + * - thumbnail.imageSize (QSize for "h" and "w" in JSON) + */ + using FileContent = UrlWith<Thumbnailed<FileInfo>>; + } // namespace EventContent +} // namespace QMatrixClient diff --git a/events/receiptevent.cpp b/events/receiptevent.cpp index 646bb989..b36ddb23 100644 --- a/events/receiptevent.cpp +++ b/events/receiptevent.cpp @@ -35,10 +35,9 @@ Example of a Receipt Event: #include "receiptevent.h" +#include "converters.h" #include "logging.h" -#include <QtCore/QJsonArray> - using namespace QMatrixClient; ReceiptEvent::ReceiptEvent(const QJsonObject& obj) @@ -62,7 +61,8 @@ ReceiptEvent::ReceiptEvent(const QJsonObject& obj) for( auto userIt = reads.begin(); userIt != reads.end(); ++userIt ) { const QJsonObject user = userIt.value().toObject(); - receipts.push_back({userIt.key(), toTimestamp(user["ts"])}); + receipts.push_back({userIt.key(), + QMatrixClient::fromJson<QDateTime>(user["ts"])}); } _eventsWithReceipts.push_back({eventIt.key(), receipts}); } diff --git a/events/receiptevent.h b/events/receiptevent.h index cbe36b10..15fdf946 100644 --- a/events/receiptevent.h +++ b/events/receiptevent.h @@ -43,6 +43,8 @@ namespace QMatrixClient { return _eventsWithReceipts; } bool unreadMessages() const { return _unreadMessages; } + static constexpr const char* const TypeId = "m.receipt"; + private: EventsWithReceipts _eventsWithReceipts; bool _unreadMessages; // Spec extension for caching purposes diff --git a/events/roomaliasesevent.cpp b/events/roomaliasesevent.cpp deleted file mode 100644 index 344b4367..00000000 --- a/events/roomaliasesevent.cpp +++ /dev/null @@ -1,43 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -// Example of a RoomAliases Event: -// -// { -// "age":3758857346, -// "content":{ -// "aliases":["#freenode_#testest376:matrix.org"] -// }, -// "event_id":"$1439989428122drFjY:matrix.org", -// "origin_server_ts":1439989428910, -// "replaces_state":"$143613875199223YYPrN:matrix.org", -// "room_id":"!UoqtanuuSGTMvNRfDG:matrix.org", -// "state_key":"matrix.org", -// "type":"m.room.aliases", -// "user_id":"@appservice-irc:matrix.org" -// } - -#include "roomaliasesevent.h" - -using namespace QMatrixClient; - -RoomAliasesEvent::RoomAliasesEvent(const QJsonObject& obj) - : RoomEvent(Type::RoomAliases, obj) - , _aliases(toStringList(contentJson()["aliases"])) -{ } - diff --git a/events/roomaliasesevent.h b/events/roomaliasesevent.h deleted file mode 100644 index efafcb30..00000000 --- a/events/roomaliasesevent.h +++ /dev/null @@ -1,37 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include "event.h" - -#include <QtCore/QStringList> - -namespace QMatrixClient -{ - class RoomAliasesEvent: public RoomEvent - { - public: - explicit RoomAliasesEvent(const QJsonObject& obj); - - QStringList aliases() const { return _aliases; } - - private: - QStringList _aliases; - }; -} // namespace QMatrixClient diff --git a/events/roomtopicevent.cpp b/events/roomavatarevent.cpp index 26677e78..7a5f82a1 100644 --- a/events/roomtopicevent.cpp +++ b/events/roomavatarevent.cpp @@ -1,5 +1,5 @@ /****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> + * 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 @@ -16,7 +16,8 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#include "roomtopicevent.h" +#include "roomavatarevent.h" using namespace QMatrixClient; + diff --git a/events/encryptedevent.h b/events/roomavatarevent.h index 9db462e1..ccfe8fbf 100644 --- a/events/encryptedevent.h +++ b/events/roomavatarevent.h @@ -20,20 +20,24 @@ #include "event.h" +#include <utility> + +#include "eventcontent.h" + namespace QMatrixClient { - class EncryptionEvent : public RoomEvent + 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 EncryptionEvent(const QJsonObject& obj) - : RoomEvent(Type::RoomEncryption, obj) - , _algorithm(contentJson()["algorithm"].toString()) + explicit RoomAvatarEvent(const QJsonObject& obj) + : StateEvent(Type::RoomAvatar, obj) { } - QString algorithm() const { return _algorithm; } - - private: - QString _algorithm; + static constexpr const char* TypeId = "m.room.avatar"; }; -} // namespace QMatrixClient +} // namespace QMatrixClient diff --git a/events/roomcanonicalaliasevent.cpp b/events/roomcanonicalaliasevent.cpp deleted file mode 100644 index 6884bc15..00000000 --- a/events/roomcanonicalaliasevent.cpp +++ /dev/null @@ -1,21 +0,0 @@ -/****************************************************************************** - * 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 "roomcanonicalaliasevent.h" - -using namespace QMatrixClient; diff --git a/events/roomcanonicalaliasevent.h b/events/roomcanonicalaliasevent.h deleted file mode 100644 index 72620d74..00000000 --- a/events/roomcanonicalaliasevent.h +++ /dev/null @@ -1,38 +0,0 @@ -/****************************************************************************** - * 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 -{ - class RoomCanonicalAliasEvent : public RoomEvent - { - public: - explicit RoomCanonicalAliasEvent(const QJsonObject& obj) - : RoomEvent(Type::RoomCanonicalAlias, obj) - , _canonicalAlias(contentJson()["alias"].toString()) - { } - - QString alias() const { return _canonicalAlias; } - - private: - QString _canonicalAlias; - }; -} // namespace QMatrixClient diff --git a/events/roommemberevent.cpp b/events/roommemberevent.cpp index 19f116d2..76df5f2e 100644 --- a/events/roommemberevent.cpp +++ b/events/roommemberevent.cpp @@ -20,23 +20,45 @@ #include "logging.h" +#include <array> + using namespace QMatrixClient; -static const auto membershipStrings = - { "invite", "join", "knock", "leave", "ban" }; +static const std::array<QString, 5> membershipStrings = { { + QStringLiteral("invite"), QStringLiteral("join"), + QStringLiteral("knock"), QStringLiteral("leave"), + QStringLiteral("ban") +} }; -RoomMemberEvent::RoomMemberEvent(const QJsonObject& obj) - : RoomEvent(Type::RoomMember, obj), _userId(obj["state_key"].toString()) +namespace QMatrixClient { - const auto contentObj = contentJson(); - _displayName = contentObj["displayname"].toString(); - _avatarUrl = contentObj["avatar_url"].toString(); - QString membershipString = contentObj["membership"].toString(); - for (auto it = membershipStrings.begin(); it != membershipStrings.end(); ++it) - if (membershipString == *it) + template <> + struct FromJson<MembershipType> + { + MembershipType operator()(const QJsonValue& jv) const { - _membership = MembershipType(it - membershipStrings.begin()); - return; + 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::Join; } - qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString; + }; +} + +MemberEventContent::MemberEventContent(const QJsonObject& json) + : membership(fromJson<MembershipType>(json["membership"])) + , displayName(json["displayname"].toString()) + , avatarUrl(json["avatar_url"].toString()) +{ } + +void MemberEventContent::fillJson(QJsonObject* o) const +{ + Q_ASSERT(o); + o->insert("membership", membershipStrings[membership]); + o->insert("displayname", displayName); + o->insert("avatar_url", avatarUrl.toString()); } diff --git a/events/roommemberevent.h b/events/roommemberevent.h index 9ebb75ee..d0c63f15 100644 --- a/events/roommemberevent.h +++ b/events/roommemberevent.h @@ -20,30 +20,49 @@ #include "event.h" +#include "eventcontent.h" + #include <QtCore/QUrl> namespace QMatrixClient { - class RoomMemberEvent: public RoomEvent + class MemberEventContent: public EventContent::Base + { + public: + enum MembershipType : size_t {Invite = 0, Join, Knock, Leave, Ban}; + + MemberEventContent(const QJsonObject& json); + + MembershipType membership; + QString displayName; + QUrl avatarUrl; + + protected: + void fillJson(QJsonObject* o) const override; + }; + + using MembershipType = MemberEventContent::MembershipType; + + class RoomMemberEvent: public StateEvent<MemberEventContent> { Q_GADGET public: - enum MembershipType : int {Invite = 0, Join, Knock, Leave, Ban}; + static constexpr const char* TypeId = "m.room.member"; - explicit RoomMemberEvent(const QJsonObject& obj); + using MembershipType = MemberEventContent::MembershipType; - MembershipType membership() const { return _membership; } - const QString& userId() const { return _userId; } - const QString& displayName() const { return _displayName; } - const QUrl& avatarUrl() const { return _avatarUrl; } + explicit RoomMemberEvent(const QJsonObject& obj) + : StateEvent(Type::RoomMember, obj) + , _userId(obj["state_key"].toString()) + { } + + MembershipType membership() const { return content().membership; } + QString userId() const { return _userId; } + QString displayName() const { return content().displayName; } + QUrl avatarUrl() const { return content().avatarUrl; } private: - MembershipType _membership; QString _userId; - QString _displayName; - QUrl _avatarUrl; - REGISTER_ENUM(MembershipType) }; - using MembershipType = RoomMemberEvent::MembershipType; } // namespace QMatrixClient diff --git a/events/roommessageevent.cpp b/events/roommessageevent.cpp index 3fb0226a..f06474e9 100644 --- a/events/roommessageevent.cpp +++ b/events/roommessageevent.cpp @@ -23,12 +23,12 @@ #include <QtCore/QMimeDatabase> using namespace QMatrixClient; -using namespace MessageEventContent; +using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; template <typename ContentT> -Base* make(const QJsonObject& json) +TypedBase* make(const QJsonObject& json) { return new ContentT(json); } @@ -37,7 +37,7 @@ struct MsgTypeDesc { QString jsonType; MsgType enumType; - Base* (*maker)(const QJsonObject&); + TypedBase* (*maker)(const QJsonObject&); }; const std::vector<MsgTypeDesc> msgTypes = @@ -74,7 +74,7 @@ MsgType jsonToMsgType(const QString& jsonType) } RoomMessageEvent::RoomMessageEvent(const QString& plainBody, - MsgType msgType, Base* content) + MsgType msgType, TypedBase* content) : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content) { } @@ -112,7 +112,7 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const QMimeType RoomMessageEvent::mimeType() const { - return _content ? _content->mimeType : + return _content ? _content->type() : QMimeDatabase().mimeTypeForName("text/plain"); } @@ -124,22 +124,8 @@ QJsonObject RoomMessageEvent::toJson() const return obj; } -QJsonObject Base::toJson() const -{ - QJsonObject o; - fillJson(&o); - return o; -} - -QJsonObject InfoBase::toInfoJson() const -{ - QJsonObject info; - fillInfoJson(&info); - return info; -} - TextContent::TextContent(const QString& text, const QString& contentType) - : Base(QMimeDatabase().mimeTypeForName(contentType)), body(text) + : mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text) { } TextContent::TextContent(const QJsonObject& json) @@ -167,38 +153,6 @@ void TextContent::fillJson(QJsonObject* json) const json->insert("formatted_body", body); } -FileInfo::FileInfo(const QUrl& u, int payloadSize, const QMimeType& mimeType, - const QString& originalFilename) - : InfoBase(mimeType), url(u), payloadSize(payloadSize) - , originalName(originalFilename) -{ } - -FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename) - : FileInfo(u, infoJson["size"].toInt(), - QMimeDatabase().mimeTypeForName(infoJson["mimetype"].toString()), - originalFilename) -{ - if (!mimeType.isValid()) - mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); -} - -void FileInfo::fillInfoJson(QJsonObject* infoJson) const -{ - Q_ASSERT(infoJson); - infoJson->insert("size", payloadSize); - infoJson->insert("mimetype", mimeType.name()); -} - -void FileInfo::fillJson(QJsonObject* json) const -{ - Q_ASSERT(json); - json->insert("url", url.toString()); - if (!originalName.isEmpty()) - json->insert("filename", originalName); - json->insert("info", toInfoJson()); -} - LocationContent::LocationContent(const QString& geoUri, const ImageInfo<>& thumbnail) : Thumbnailed<>(thumbnail), geoUri(geoUri) @@ -216,6 +170,11 @@ void LocationContent::fillJson(QJsonObject* o) const o->insert("info", Thumbnailed::toInfoJson()); } +QMimeType LocationContent::type() const +{ + return QMimeDatabase().mimeTypeForData(geoUri.toLatin1()); +} + PlayableInfo::PlayableInfo(const QUrl& u, int fileSize, const QMimeType& mimeType, int duration, const QString& originalFilename) diff --git a/events/roommessageevent.h b/events/roommessageevent.h index 74e0defb..eef6b657 100644 --- a/events/roommessageevent.h +++ b/events/roommessageevent.h @@ -20,65 +20,11 @@ #include "event.h" -#include <QtCore/QUrl> -#include <QtCore/QMimeType> -#include <QtCore/QSize> +#include "eventcontent.h" namespace QMatrixClient { - namespace MessageEventContent - { - /** - * A base class for all content types that can be stored - * in a RoomMessageEvent - * - * Each content type class should have a constructor taking - * a QJsonObject and override fillJson() with an implementation - * that will fill the target QJsonObject with stored values. It is - * assumed but not required that a content object can also be created - * from plain data. fillJson() should only fill the main JSON object - * but not the "info" subobject if it exists for a certain content type; - * use \p InfoBase to de/serialize "info" parts with an optional URL - * on the top level. - */ - class Base - { - public: - virtual ~Base() = default; - - QJsonObject toJson() const; - - QMimeType mimeType; - - protected: - Base() = default; - explicit Base(const QMimeType& type) : mimeType(type) { } - - virtual void fillJson(QJsonObject* o) const = 0; - }; - - /** - * A base class for content types that have an "info" object in their - * JSON representation - * - * These include most multimedia types currently in the CS API spec. - * Derived classes should override fillInfoJson() to fill the "info" - * subobject, BUT NOT the main JSON object. Most but not all "info" - * classes (specifically, those deriving from UrlInfo) should also - * have a constructor that accepts two parameters, QUrl and QJsonObject, - * in order to load the URL+info part from JSON. - */ - class InfoBase: public Base - { - public: - QJsonObject toInfoJson() const; - - protected: - using Base::Base; - - virtual void fillInfoJson(QJsonObject* infoJson) const { } - }; - } // namespace MessageEventContent + namespace MessageEventContent = EventContent; // Back-compatibility /** * The event class corresponding to m.room.message events @@ -94,19 +40,19 @@ namespace QMatrixClient RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, - MessageEventContent::Base* content = nullptr) + EventContent::TypedBase* content = nullptr) : RoomEvent(Type::RoomMessage) , _msgtype(jsonMsgType), _plainBody(plainBody), _content(content) { } explicit RoomMessageEvent(const QString& plainBody, MsgType msgType = MsgType::Text, - MessageEventContent::Base* content = nullptr); + EventContent::TypedBase* content = nullptr); explicit RoomMessageEvent(const QJsonObject& obj); MsgType msgtype() const; QString rawMsgtype() const { return _msgtype; } const QString& plainBody() const { return _plainBody; } - const MessageEventContent::Base* content() const + const EventContent::TypedBase* content() const { return _content.data(); } QMimeType mimeType() const; @@ -117,18 +63,15 @@ namespace QMatrixClient private: QString _msgtype; QString _plainBody; - QScopedPointer<MessageEventContent::Base> _content; + QScopedPointer<EventContent::TypedBase> _content; REGISTER_ENUM(MsgType) }; using MessageEventType = RoomMessageEvent::MsgType; - namespace MessageEventContent + namespace EventContent { - // 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. + // Additional event content types /** * Rich text content for m.text, m.emote, m.notice @@ -136,175 +79,22 @@ namespace QMatrixClient * Available fields: mimeType, body. The body can be either rich text * or plain text, depending on what mimeType specifies. */ - class TextContent: public Base + class TextContent: public TypedBase { public: TextContent(const QString& text, const QString& contentType); explicit TextContent(const QJsonObject& json); - void fillJson(QJsonObject* json) const override; + QMimeType type() const override { return mimeType; } + QMimeType mimeType; QString body; - }; - - /** - * Base class for content types that consist of a URL along with - * additional information - * - * All message types except the (hyper)text mentioned above and - * m.location fall under this category. - */ - class FileInfo: public InfoBase - { - public: - explicit FileInfo(const QUrl& u, int payloadSize = -1, - const QMimeType& mimeType = {}, - const QString& originalFilename = {}); - FileInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename = {}); - - QUrl url; - int payloadSize; - QString originalName; protected: void fillJson(QJsonObject* json) const override; - void fillInfoJson(QJsonObject* infoJson) const override; - }; - - /** - * A base class for image info types: image, thumbnail, video - * - * \tparam InfoT base info class; should derive from \p InfoBase - */ - template <class InfoT = FileInfo> - class ImageInfo : public InfoT - { - public: - explicit ImageInfo(const QUrl& u, int fileSize = -1, - QMimeType mimeType = {}, - const QSize& imageSize = {}) - : InfoT(u, fileSize, mimeType), imageSize(imageSize) - { } - ImageInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename = {}) - : InfoT(u, infoJson, originalFilename) - , imageSize(infoJson["w"].toInt(), infoJson["h"].toInt()) - { } - - void fillInfoJson(QJsonObject* infoJson) const /* override */ - { - InfoT::fillInfoJson(infoJson); - infoJson->insert("w", imageSize.width()); - infoJson->insert("h", imageSize.height()); - } - - QSize imageSize; - }; - - /** - * A base class for an info type that carries a thumbnail - * - * This class provides a means to save/load a thumbnail to/from "info" - * subobject of the JSON representation of a message; namely, - * "info/thumbnail_url" and "info/thumbnail_info" fields are used. - * - * \tparam InfoT base info class; should derive from \p InfoBase - */ - template <class InfoT = InfoBase> - class Thumbnailed : public InfoT - { - public: - template <typename... ArgTs> - explicit Thumbnailed(const ImageInfo<>& thumbnail, - ArgTs&&... infoArgs) - : InfoT(std::forward<ArgTs>(infoArgs)...) - , thumbnail(thumbnail) - { } - - explicit Thumbnailed(const QJsonObject& infoJson) - : thumbnail(infoJson["thumbnail_url"].toString(), - infoJson["thumbnail_info"].toObject()) - { } - - Thumbnailed(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename = {}) - : InfoT(u, infoJson, originalFilename) - , thumbnail(infoJson["thumbnail_url"].toString(), - infoJson["thumbnail_info"].toObject()) - { } - - void fillInfoJson(QJsonObject* infoJson) const /* override */ - { - InfoT::fillInfoJson(infoJson); - infoJson->insert("thumbnail_url", thumbnail.url.toString()); - infoJson->insert("thumbnail_info", thumbnail.toInfoJson()); - } - - ImageInfo<> thumbnail; }; /** - * One more facility base class for content types that have a URL and - * additional info - * - * The assumed layout for types enabled by a combination of UrlInfo and - * UrlWith<> is the following: "url" and, optionally, "filename" in the - * top-level JSON and the rest of information inside the "info" subobject. - * - * \tparam InfoT base info class; should derive from \p UrlInfo or - * provide a constructor with a compatible signature - */ - template <class InfoT> // InfoT : public FileInfo - class UrlWith : public InfoT - { - public: - using InfoT::InfoT; - explicit UrlWith(const QJsonObject& json) - : InfoT(json["url"].toString(), json["info"].toObject(), - json["filename"].toString()) - { } - }; - - /** - * Content class for m.image - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename (extension to the spec) - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - imageSize (QSize for a combination of "h" and "w" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: contents of - * thumbnail field, in the same vein as for the main image: - * - payloadSize - * - mimeType - * - imageSize - */ - using ImageContent = UrlWith<Thumbnailed<ImageInfo<>>>; - - /** - * Content class for m.file - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: - * - thumbnail.payloadSize - * - thumbnail.mimeType - * - thumbnail.imageSize (QSize for "h" and "w" in JSON) - */ - using FileContent = UrlWith<Thumbnailed<FileInfo>>; - - /** * Content class for m.location * * Available fields: @@ -317,16 +107,19 @@ namespace QMatrixClient * - thumbnail.mimeType * - thumbnail.imageSize */ - class LocationContent: public Thumbnailed<> + class LocationContent: public TypedBase, public Thumbnailed<> { public: LocationContent(const QString& geoUri, const ImageInfo<>& thumbnail); explicit LocationContent(const QJsonObject& json); - void fillJson(QJsonObject* o) const override; + QMimeType type() const override; QString geoUri; + + protected: + void fillJson(QJsonObject* o) const override; }; /** @@ -341,9 +134,10 @@ namespace QMatrixClient PlayableInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename = {}); - void fillInfoJson(QJsonObject* infoJson) const override; - int duration; + + protected: + void fillInfoJson(QJsonObject* infoJson) const override; }; /** @@ -380,5 +174,5 @@ namespace QMatrixClient * - duration */ using AudioContent = UrlWith<PlayableInfo>; - } // namespace MessageEventContent + } // namespace EventContent } // namespace QMatrixClient diff --git a/events/roomnameevent.cpp b/events/roomnameevent.cpp deleted file mode 100644 index c202d17a..00000000 --- a/events/roomnameevent.cpp +++ /dev/null @@ -1,22 +0,0 @@ -/******************************************************************************
- * Copyright (C) 2015 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 "roomnameevent.h"
-
-using namespace QMatrixClient;
-
diff --git a/events/roomnameevent.h b/events/roomnameevent.h deleted file mode 100644 index bb823933..00000000 --- a/events/roomnameevent.h +++ /dev/null @@ -1,38 +0,0 @@ -/******************************************************************************
- * Copyright (C) 2015 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 RoomNameEvent : public RoomEvent
- {
- public:
- explicit RoomNameEvent(const QJsonObject& obj)
- : RoomEvent(Type::RoomName, obj)
- , _name(contentJson()["name"].toString())
- { }
-
- QString name() const { return _name; }
-
- private:
- QString _name{};
- };
-} // namespace QMatrixClient
diff --git a/events/simplestateevents.h b/events/simplestateevents.h new file mode 100644 index 00000000..d5841bdc --- /dev/null +++ b/events/simplestateevents.h @@ -0,0 +1,54 @@ +/****************************************************************************** + * 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 DECLARE_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, #_ContentKey) \ + { } \ + template <typename T> \ + explicit _Name(T&& value) \ + : StateEvent(_EnumType, #_ContentKey, \ + std::forward<T>(value)) \ + { } \ + _ContentType _ContentKey() const { return content().value; } \ + }; + + DECLARE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", + Event::Type::RoomName, QString, name) + DECLARE_SIMPLE_STATE_EVENT(RoomAliasesEvent, "m.room.aliases", + Event::Type::RoomAliases, QStringList, aliases) + DECLARE_SIMPLE_STATE_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias", + Event::Type::RoomCanonicalAlias, QString, alias) + DECLARE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", + Event::Type::RoomTopic, QString, topic) + DECLARE_SIMPLE_STATE_EVENT(EncryptionEvent, "m.room.encryption", + Event::Type::RoomEncryption, QString, algorithm) +} // namespace QMatrixClient diff --git a/events/typingevent.h b/events/typingevent.h index b12d224e..8c9551a4 100644 --- a/events/typingevent.h +++ b/events/typingevent.h @@ -27,6 +27,8 @@ namespace QMatrixClient class TypingEvent: public Event { public: + static constexpr const char* const TypeId = "m.typing"; + TypingEvent(const QJsonObject& obj); QStringList users() const { return _users; } diff --git a/events/unknownevent.cpp b/events/unknownevent.cpp deleted file mode 100644 index 1670ff1d..00000000 --- a/events/unknownevent.cpp +++ /dev/null @@ -1,64 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "unknownevent.h" - -#include "logging.h" - -#include <QtCore/QJsonDocument> - -using namespace QMatrixClient; - -class UnknownEvent::Private -{ - public: - QString type; - QString content; -}; - -UnknownEvent::UnknownEvent() - : Event(EventType::Unknown) - , d(new Private) -{ -} - -UnknownEvent::~UnknownEvent() -{ - delete d; -} - -QString UnknownEvent::typeString() const -{ - return d->type; -} - -QString UnknownEvent::content() const -{ - return d->content; -} - -UnknownEvent* UnknownEvent::fromJson(const QJsonObject& obj) -{ - UnknownEvent* e = new UnknownEvent(); - e->parseJson(obj); - e->d->type = obj.value("type").toString(); - e->d->content = QString::fromUtf8(QJsonDocument(obj).toJson()); - qCDebug(EVENTS) << "UnknownEvent, JSON follows:"; - qCDebug(EVENTS) << formatJson << obj; - return e; -} diff --git a/events/unknownevent.h b/events/unknownevent.h deleted file mode 100644 index 51f2c4be..00000000 --- a/events/unknownevent.h +++ /dev/null @@ -1,40 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include "event.h" - -namespace QMatrixClient -{ - class UnknownEvent: public Event - { - public: - UnknownEvent(); - virtual ~UnknownEvent(); - - QString typeString() const; - QString content() const; - - static UnknownEvent* fromJson(const QJsonObject& obj); - - private: - class Private; - Private* d; - }; -} diff --git a/jobs/basejob.cpp b/jobs/basejob.cpp index 7794337e..2f5c381a 100644 --- a/jobs/basejob.cpp +++ b/jobs/basejob.cpp @@ -81,9 +81,9 @@ inline QDebug operator<<(QDebug dbg, const BaseJob* j) QDebug QMatrixClient::operator<<(QDebug dbg, const BaseJob::Status& s) { - QRegularExpression filter { "(access_token)=[-_A-Za-z0-9]+" }; + QRegularExpression filter { "(access_token)(=|: )[-_A-Za-z0-9]+" }; return dbg << s.code << ':' - << QString(s.message).replace(filter, "\1=HIDDEN"); + << QString(s.message).replace(filter, "\\1 HIDDEN"); } BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, @@ -136,14 +136,17 @@ void BaseJob::setRequestData(const BaseJob::Data& data) void BaseJob::Private::sendRequest() { QUrl url = connection->baseUrl(); - url.setPath( url.path() + "/" + apiEndpoint ); - QUrlQuery q = requestQuery; - if (needsToken) - q.addQueryItem("access_token", connection->accessToken()); - url.setQuery(q); + QString path = url.path(); + if (!path.endsWith('/') && !apiEndpoint.startsWith('/')) + path.push_back('/'); + + url.setPath( path + apiEndpoint ); + url.setQuery(requestQuery); QNetworkRequest req {url}; 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); @@ -207,6 +210,7 @@ void BaseJob::gotReply() BaseJob::Status BaseJob::checkReply(QNetworkReply* reply) const { + qCDebug(d->logCat) << this << "returned from" << reply->url().toDisplayString(); if (reply->error() != QNetworkReply::NoError) qCDebug(d->logCat) << this << "returned" << reply->error(); switch( reply->error() ) diff --git a/jobs/generated/banning.cpp b/jobs/generated/banning.cpp index 7efc2a85..ebb4c96c 100644 --- a/jobs/generated/banning.cpp +++ b/jobs/generated/banning.cpp @@ -5,7 +5,7 @@ #include "banning.h" -#include "jobs/converters.h" +#include "converters.h" #include <QtCore/QStringBuilder> using namespace QMatrixClient; diff --git a/jobs/generated/inviting.cpp b/jobs/generated/inviting.cpp index 91760e57..73c73076 100644 --- a/jobs/generated/inviting.cpp +++ b/jobs/generated/inviting.cpp @@ -5,7 +5,7 @@ #include "inviting.h" -#include "jobs/converters.h" +#include "converters.h" #include <QtCore/QStringBuilder> using namespace QMatrixClient; diff --git a/jobs/generated/kicking.cpp b/jobs/generated/kicking.cpp index 1a544c39..28d51d05 100644 --- a/jobs/generated/kicking.cpp +++ b/jobs/generated/kicking.cpp @@ -5,7 +5,7 @@ #include "kicking.h" -#include "jobs/converters.h" +#include "converters.h" #include <QtCore/QStringBuilder> using namespace QMatrixClient; diff --git a/jobs/generated/leaving.cpp b/jobs/generated/leaving.cpp index a86714ac..392f1ca8 100644 --- a/jobs/generated/leaving.cpp +++ b/jobs/generated/leaving.cpp @@ -5,7 +5,7 @@ #include "leaving.h" -#include "jobs/converters.h" +#include "converters.h" #include <QtCore/QStringBuilder> using namespace QMatrixClient; diff --git a/jobs/generated/login.cpp b/jobs/generated/login.cpp index 6e8294e7..0c57c684 100644 --- a/jobs/generated/login.cpp +++ b/jobs/generated/login.cpp @@ -5,7 +5,7 @@ #include "login.h" -#include "jobs/converters.h" +#include "converters.h" #include <QtCore/QStringBuilder> using namespace QMatrixClient; diff --git a/jobs/generated/logout.cpp b/jobs/generated/logout.cpp index b807012a..c2480ff9 100644 --- a/jobs/generated/logout.cpp +++ b/jobs/generated/logout.cpp @@ -5,7 +5,7 @@ #include "logout.h" -#include "jobs/converters.h" +#include "converters.h" #include <QtCore/QStringBuilder> using namespace QMatrixClient; diff --git a/jobs/generated/profile.cpp b/jobs/generated/profile.cpp index 7ef0577b..f24db15a 100644 --- a/jobs/generated/profile.cpp +++ b/jobs/generated/profile.cpp @@ -5,7 +5,7 @@ #include "profile.h" -#include "jobs/converters.h" +#include "converters.h" #include <QtCore/QStringBuilder> using namespace QMatrixClient; diff --git a/jobs/joinroomjob.cpp b/jobs/joinroomjob.cpp index d465dd42..66a75089 100644 --- a/jobs/joinroomjob.cpp +++ b/jobs/joinroomjob.cpp @@ -29,7 +29,7 @@ class JoinRoomJob::Private JoinRoomJob::JoinRoomJob(const QString& roomAlias) : BaseJob(HttpVerb::Post, "JoinRoomJob", - QString("_matrix/client/r0/join/%1").arg(roomAlias)) + QStringLiteral("_matrix/client/r0/join/%1").arg(roomAlias)) , d(new Private) { } diff --git a/jobs/roommessagesjob.cpp b/jobs/roommessagesjob.cpp index 078c692a..9af1b3a6 100644 --- a/jobs/roommessagesjob.cpp +++ b/jobs/roommessagesjob.cpp @@ -30,7 +30,7 @@ class RoomMessagesJob::Private RoomMessagesJob::RoomMessagesJob(const QString& roomId, const QString& from, int limit, FetchDirection dir) : BaseJob(HttpVerb::Get, "RoomMessagesJob", - QString("/_matrix/client/r0/rooms/%1/messages").arg(roomId), + QStringLiteral("/_matrix/client/r0/rooms/%1/messages").arg(roomId), Query( { { "from", from } , { "dir", dir == FetchDirection::Backward ? "b" : "f" } @@ -58,8 +58,8 @@ QString RoomMessagesJob::end() const BaseJob::Status RoomMessagesJob::parseJson(const QJsonDocument& data) { - QJsonObject obj = data.object(); - d->events.assign(makeEvents<RoomEvent>(obj.value("chunk").toArray())); + const auto obj = data.object(); + d->events.fromJson(obj, "chunk"); d->end = obj.value("end").toString(); return Success; } diff --git a/jobs/syncjob.h b/jobs/syncjob.h index b1db914d..08bd773e 100644 --- a/jobs/syncjob.h +++ b/jobs/syncjob.h @@ -36,11 +36,10 @@ namespace QMatrixClient explicit Batch(QString k) : jsonKey(std::move(k)) { } void fromJson(const QJsonObject& roomContents) { - this->assign(makeEvents<EventT>( - roomContents[jsonKey].toObject()["events"].toArray())); + EventsBatch<EventT>::fromJson( + roomContents[jsonKey].toObject(), "events"); } - private: QString jsonKey; }; diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index 36f6429c..86648860 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -1,5 +1,5 @@ QT += network -CONFIG += c++11 +CONFIG += c++11 warn_on rtti_off INCLUDEPATH += $$PWD @@ -8,14 +8,14 @@ HEADERS += \ $$PWD/connection.h \ $$PWD/room.h \ $$PWD/user.h \ - $$PWD/state.h \ + $$PWD/avatar.h \ + $$PWD/util.h \ $$PWD/events/event.h \ + $$PWD/events/eventcontent.h \ $$PWD/events/roommessageevent.h \ - $$PWD/events/roomnameevent.h \ - $$PWD/events/roomaliasesevent.h \ - $$PWD/events/roomcanonicalaliasevent.h \ + $$PWD/events/simplestateevents.h \ $$PWD/events/roommemberevent.h \ - $$PWD/events/roomtopicevent.h \ + $$PWD/events/roomavatarevent.h \ $$PWD/events/typingevent.h \ $$PWD/events/receiptevent.h \ $$PWD/jobs/basejob.h \ @@ -24,14 +24,11 @@ HEADERS += \ $$PWD/jobs/sendeventjob.h \ $$PWD/jobs/postreceiptjob.h \ $$PWD/jobs/joinroomjob.h \ - $$PWD/jobs/leaveroomjob.h \ $$PWD/jobs/roommessagesjob.h \ $$PWD/jobs/syncjob.h \ $$PWD/jobs/mediathumbnailjob.h \ - $$PWD/jobs/logoutjob.h \ $$PWD/jobs/setroomstatejob.h \ - $$PWD/jobs/generated/inviting.h \ - $$PWD/jobs/generated/kicking.h \ + $$files($$PWD/jobs/generated/*.h, false) \ $$PWD/logging.h \ $$PWD/settings.h @@ -40,13 +37,11 @@ SOURCES += \ $$PWD/connection.cpp \ $$PWD/room.cpp \ $$PWD/user.cpp \ + $$PWD/avatar.cpp \ $$PWD/events/event.cpp \ + $$PWD/events/eventcontent.cpp \ $$PWD/events/roommessageevent.cpp \ - $$PWD/events/roomnameevent.cpp \ - $$PWD/events/roomaliasesevent.cpp \ - $$PWD/events/roomcanonicalaliasevent.cpp \ $$PWD/events/roommemberevent.cpp \ - $$PWD/events/roomtopicevent.cpp \ $$PWD/events/typingevent.cpp \ $$PWD/events/receiptevent.cpp \ $$PWD/jobs/basejob.cpp \ @@ -55,13 +50,10 @@ SOURCES += \ $$PWD/jobs/sendeventjob.cpp \ $$PWD/jobs/postreceiptjob.cpp \ $$PWD/jobs/joinroomjob.cpp \ - $$PWD/jobs/leaveroomjob.cpp \ $$PWD/jobs/roommessagesjob.cpp \ $$PWD/jobs/syncjob.cpp \ $$PWD/jobs/mediathumbnailjob.cpp \ - $$PWD/jobs/logoutjob.cpp \ $$PWD/jobs/setroomstatejob.cpp \ - $$PWD/jobs/generated/inviting.cpp \ - $$PWD/jobs/generated/kicking.cpp \ + $$files($$PWD/jobs/generated/*.cpp, false) \ $$PWD/logging.cpp \ $$PWD/settings.cpp diff --git a/qmc-example.pro b/qmc-example.pro new file mode 100644 index 00000000..4dc3fed1 --- /dev/null +++ b/qmc-example.pro @@ -0,0 +1,7 @@ +TEMPLATE = app + +windows { CONFIG += console } + +include(libqmatrixclient.pri) + +SOURCES += examples/qmc-example.cpp @@ -23,16 +23,15 @@ #include "jobs/generated/banning.h" #include "jobs/generated/leaving.h" #include "jobs/setroomstatejob.h" -#include "events/roomnameevent.h" -#include "events/roomaliasesevent.h" -#include "events/roomcanonicalaliasevent.h" -#include "events/roomtopicevent.h" +#include "events/simplestateevents.h" +#include "events/roomavatarevent.h" #include "events/roommemberevent.h" #include "events/typingevent.h" #include "events/receiptevent.h" #include "jobs/sendeventjob.h" #include "jobs/roommessagesjob.h" #include "jobs/postreceiptjob.h" +#include "avatar.h" #include "connection.h" #include "user.h" @@ -53,7 +52,7 @@ class Room::Private Private(Connection* c, QString id_, JoinState initialJoinState) : q(nullptr), connection(c), id(std::move(id_)) - , joinState(initialJoinState), unreadMessages(false) + , avatar(c), joinState(initialJoinState), unreadMessages(false) , highlightCount(0), notificationCount(0), roomMessagesJob(nullptr) { } @@ -73,6 +72,7 @@ class Room::Private QString name; QString displayname; QString topic; + Avatar avatar; JoinState joinState; bool unreadMessages; int highlightCount; @@ -189,6 +189,23 @@ QString Room::topic() const return d->topic; } +QPixmap Room::avatar(int width, int height) +{ + if (!d->avatar.url().isEmpty()) + return d->avatar.get(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.value()->avatarObject() + .get(width, height, [=] { emit avatarChanged(); }); + } + return {}; +} + JoinState Room::joinState() const { return d->joinState; @@ -394,6 +411,11 @@ int Room::memberCount() const return d->membersMap.size(); } +int Room::timelineSize() const +{ + return int(d->timeline.size()); +} + void Room::Private::insertMemberIntoMap(User *u) { auto namesakes = membersMap.values(u->name()); @@ -778,6 +800,17 @@ void Room::processStateEvents(const RoomEvents& events) 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); // Can't use d->member() below because the user may be not a member (yet) @@ -939,17 +972,22 @@ void Room::Private::updateDisplayname() emit q->displaynameChanged(q); } -QJsonObject stateEventToJson(const QString& type, const QString& name, - const QJsonValue& content) +template <typename T> +void appendStateEvent(QJsonArray& events, const QString& type, + const QString& name, const T& content) { + if (content.isEmpty()) + return; + QJsonObject contentObj; contentObj.insert(name, content); QJsonObject eventObj; eventObj.insert("type", type); eventObj.insert("content", contentObj); + eventObj.insert("state_key", {}); // Mandatory for state events - return eventObj; + events.append(eventObj); } QJsonObject Room::Private::toJson() const @@ -958,12 +996,14 @@ QJsonObject Room::Private::toJson() const { QJsonArray stateEvents; - stateEvents.append(stateEventToJson("m.room.name", "name", name)); - stateEvents.append(stateEventToJson("m.room.topic", "topic", topic)); - stateEvents.append(stateEventToJson("m.room.aliases", "aliases", - QJsonArray::fromStringList(aliases))); - stateEvents.append(stateEventToJson("m.room.canonical_alias", "alias", - canonicalAlias)); + appendStateEvent(stateEvents, "m.room.name", "name", name); + appendStateEvent(stateEvents, "m.room.topic", "topic", topic); + appendStateEvent(stateEvents, "m.room.avatar", "url", + avatar.url().toString()); + appendStateEvent(stateEvents, "m.room.aliases", "aliases", + QJsonArray::fromStringList(aliases)); + appendStateEvent(stateEvents, "m.room.canonical_alias", "alias", + canonicalAlias); for (const auto &i : membersMap) { @@ -25,6 +25,7 @@ #include <QtCore/QStringList> #include <QtCore/QObject> #include <QtCore/QJsonObject> +#include <QtGui/QPixmap> #include "jobs/syncjob.h" #include "events/roommessageevent.h" @@ -79,7 +80,7 @@ namespace QMatrixClient using rev_iter_t = Timeline::const_reverse_iterator; Room(Connection* connection, QString id, JoinState initialJoinState); - virtual ~Room(); + ~Room() override; Connection* connection() const; User* localUser() const; @@ -96,8 +97,15 @@ namespace QMatrixClient Q_INVOKABLE QList<User*> users() const; Q_INVOKABLE QStringList memberNames() const; Q_INVOKABLE int memberCount() const; + Q_INVOKABLE int timelineSize() const; /** + * Returns a room avatar 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 QPixmap avatar(int width, int height); + /** * @brief Produces a disambiguated name for a given user in * the context of the room */ @@ -108,9 +116,6 @@ namespace QMatrixClient */ Q_INVOKABLE QString roomMembername(const QString& userId) const; - void updateData(SyncRoomData&& data ); - Q_INVOKABLE void setJoinState( JoinState state ); - const Timeline& messageEvents() const; /** * A convenience method returning the read marker to the before-oldest @@ -147,6 +152,8 @@ namespace QMatrixClient MemberSorter memberSorter() const; QJsonObject toJson() const; + void updateData(SyncRoomData&& data ); + void setJoinState( JoinState state ); public slots: void postMessage(const QString& plainText, @@ -181,6 +188,7 @@ namespace QMatrixClient /** @brief The room displayname changed */ void displaynameChanged(Room* room); void topicChanged(); + void avatarChanged(); void userAdded(User* user); void userRemoved(User* user); void memberRenamed(User* user); diff --git a/settings.cpp b/settings.cpp index 3a5f4d26..68914642 100644 --- a/settings.cpp +++ b/settings.cpp @@ -5,6 +5,16 @@ 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;
@@ -13,22 +23,39 @@ void Settings::setValue(const QString& key, const QVariant& value) QVariant Settings::value(const QString& key, const QVariant& defaultValue) const
{
- return QSettings::value(key, defaultValue);
+ 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);
+ Settings::setValue(groupPath + '/' + key, value);
}
bool SettingsGroup::contains(const QString& key) const
{
- return Settings::contains(groupPath + "/" + key);
+ return Settings::contains(groupPath + '/' + key);
}
QVariant SettingsGroup::value(const QString& key, const QVariant& defaultValue) const
{
- return Settings::value(groupPath + "/" + key, defaultValue);
+ return Settings::value(groupPath + '/' + key, defaultValue);
}
QString SettingsGroup::group() const
@@ -39,8 +66,10 @@ QString SettingsGroup::group() const QStringList SettingsGroup::childGroups() const
{
const_cast<SettingsGroup*>(this)->beginGroup(groupPath);
- QStringList l { Settings::childGroups() };
+ const_cast<QSettings&>(legacySettings).beginGroup(groupPath);
+ QStringList l = Settings::childGroups();
const_cast<SettingsGroup*>(this)->endGroup();
+ const_cast<QSettings&>(legacySettings).endGroup();
return l;
}
@@ -28,7 +28,17 @@ 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
@@ -41,6 +51,16 @@ namespace QMatrixClient 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:
+ const QSettings legacySettings { legacyOrganizationName,
+ legacyApplicationName };
};
class SettingsGroup: public Settings
@@ -69,7 +89,7 @@ namespace QMatrixClient class AccountSettings: public SettingsGroup
{
Q_OBJECT
- Q_PROPERTY(QString userId READ userId)
+ Q_PROPERTY(QString userId READ userId CONSTANT)
Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId)
Q_PROPERTY(QString deviceName READ deviceName WRITE setDeviceName)
Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver)
@@ -19,14 +19,12 @@ #include "user.h" #include "connection.h" +#include "avatar.h" #include "events/event.h" #include "events/roommemberevent.h" -#include "jobs/mediathumbnailjob.h" #include "jobs/generated/profile.h" #include <QtCore/QTimer> -#include <QtCore/QDebug> -#include <QtGui/QIcon> #include <QtCore/QRegularExpression> using namespace QMatrixClient; @@ -35,35 +33,20 @@ class User::Private { public: Private(QString userId, Connection* connection) - : q(nullptr), userId(std::move(userId)), connection(connection) - , defaultIcon(QIcon::fromTheme(QStringLiteral("user-available"))) - , avatarValid(false) , avatarOngoingRequest(false) + : userId(std::move(userId)), connection(connection) + , avatar(connection, QIcon::fromTheme(QStringLiteral("user-available"))) { } - User* q; QString userId; QString name; - QUrl avatarUrl; - Connection* connection; - - QPixmap avatar; - QIcon defaultIcon; - QSize requestedSize; - bool avatarValid; - bool avatarOngoingRequest; - /// Map of requested size to the actual pixmap used for it - /// (it's a shame that QSize has no predefined qHash()). - QHash<QPair<int,int>, QPixmap> scaledAvatars; QString bridged; - - void requestAvatar(); + Connection* connection; + Avatar avatar; }; User::User(QString userId, Connection* connection) - : QObject(connection), d(new Private(userId, connection)) -{ - d->q = this; // Initialization finished -} + : QObject(connection), d(new Private(std::move(userId), connection)) +{ } User::~User() { @@ -107,45 +90,19 @@ QString User::bridged() const { return d->bridged; } -QPixmap User::avatar(int width, int height) +Avatar& User::avatarObject() { - QSize size(width, height); - - // 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( (!d->avatarValid && d->avatarUrl.isValid() && !d->avatarOngoingRequest) - || width > d->requestedSize.width() - || height > d->requestedSize.height() ) - { - qCDebug(MAIN) << "Getting avatar for" << id() - << "from" << d->avatarUrl.toString(); - d->requestedSize = size; - d->avatarOngoingRequest = true; - QTimer::singleShot(0, this, SLOT(requestAvatar())); - } - - if( d->avatar.isNull() ) - { - if (d->defaultIcon.isNull()) - return d->avatar; - - d->avatar = d->defaultIcon.pixmap(size); - } + return d->avatar; +} - auto& pixmap = d->scaledAvatars[{width, height}]; // Create the entry if needed - if (pixmap.isNull()) - { - pixmap = d->avatar.scaled(width, height, - Qt::KeepAspectRatio, Qt::SmoothTransformation); - } - return pixmap; +QPixmap User::avatar(int width, int height) +{ + return d->avatar.get(width, height, [=] { emit avatarChanged(this); }); } -const QUrl& User::avatarUrl() const +QUrl User::avatarUrl() const { - return d->avatarUrl; + return d->avatar.url(); } void User::processEvent(Event* event) @@ -157,36 +114,15 @@ void User::processEvent(Event* event) return; auto newName = e->displayName(); - QRegularExpression reSuffix(" \\((IRC|Gitter)\\)$"); - auto match = reSuffix.match(d->name); + QRegularExpression reSuffix(" \\((IRC|Gitter|Telegram)\\)$"); + auto match = reSuffix.match(newName); if (match.hasMatch()) { d->bridged = match.captured(1); newName.truncate(match.capturedStart(0)); } updateName(newName); - if( d->avatarUrl != e->avatarUrl() ) - { - d->avatarUrl = e->avatarUrl(); - d->avatarValid = false; - } + if (d->avatar.updateUrl(e->avatarUrl())) + emit avatarChanged(this); } } - -void User::requestAvatar() -{ - d->requestAvatar(); -} - -void User::Private::requestAvatar() -{ - auto* job = connection->callApi<MediaThumbnailJob>(avatarUrl, requestedSize); - connect( job, &MediaThumbnailJob::success, [=]() { - avatarOngoingRequest = false; - avatarValid = true; - avatar = job->scaledThumbnail(requestedSize); - scaledAvatars.clear(); - emit q->avatarChanged(q); - }); -} - @@ -20,6 +20,7 @@ #include <QtCore/QString> #include <QtCore/QObject> +#include "avatar.h" namespace QMatrixClient { @@ -30,7 +31,7 @@ namespace QMatrixClient Q_OBJECT public: User(QString userId, Connection* connection); - virtual ~User(); + ~User() override; /** * Returns the id of the user @@ -52,14 +53,14 @@ namespace QMatrixClient */ Q_INVOKABLE QString bridged() const; + Avatar& avatarObject(); QPixmap avatar(int requestedWidth, int requestedHeight); - const QUrl& avatarUrl() const; + QUrl avatarUrl() const; void processEvent(Event* event); public slots: - void requestAvatar(); void rename(const QString& newName); signals: @@ -39,8 +39,8 @@ namespace QMatrixClient { public: Owning() = default; - Owning(Owning&) = delete; - Owning(Owning&& other) = default; + Owning(const Owning&) = delete; + Owning(Owning&&) = default; Owning& operator=(Owning&& other) { assign(other.release()); |