diff options
-rw-r--r-- | CMakeLists.txt | 3 | ||||
-rw-r--r-- | lib/quotient_common.h | 8 | ||||
-rw-r--r-- | lib/resourceresolver.cpp | 236 | ||||
-rw-r--r-- | lib/resourceresolver.h | 181 | ||||
-rw-r--r-- | lib/uri.cpp | 181 | ||||
-rw-r--r-- | lib/uri.h | 85 | ||||
-rw-r--r-- | lib/uriresolver.cpp | 110 | ||||
-rw-r--r-- | lib/uriresolver.h | 162 | ||||
-rw-r--r-- | libquotient.pri | 6 | ||||
-rw-r--r-- | tests/quotest.cpp | 24 |
10 files changed, 560 insertions, 436 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 83075196..808899ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,7 +149,8 @@ set(lib_SRCS lib/room.cpp lib/user.cpp lib/avatar.cpp - lib/resourceresolver.cpp + lib/uri.cpp + lib/uriresolver.cpp lib/syncdata.cpp lib/settings.cpp lib/networksettings.cpp diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 446f628b..bb05af05 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -17,10 +17,10 @@ Q_ENUM_NS(RunningPolicy) enum UriResolveResult : short { StillResolving = -1, UriResolved = 0, - UnknownMatrixId, - MalformedUri, - NoAccount, - EmptyMatrixId + CouldNotResolve, + IncorrectAction, + InvalidUri, + NoAccount }; Q_ENUM_NS(UriResolveResult) diff --git a/lib/resourceresolver.cpp b/lib/resourceresolver.cpp deleted file mode 100644 index e7820061..00000000 --- a/lib/resourceresolver.cpp +++ /dev/null @@ -1,236 +0,0 @@ -#include "resourceresolver.h" - -#include "connection.h" -#include "logging.h" - -#include <QtCore/QRegularExpression> - -using namespace Quotient; - -struct ReplacePair { QByteArray uriString; char sigil; }; -static const auto replacePairs = { ReplacePair { "user/", '@' }, - { "roomid/", '!' }, - { "room/", '#' } }; - -MatrixUri::MatrixUri(QByteArray primaryId, QByteArray secondaryId, QString query) -{ - if (primaryId.isEmpty()) - primaryType_ = Empty; - else { - setScheme("matrix"); - QString pathToBe; - primaryType_ = Invalid; - for (const auto& p: replacePairs) - if (primaryId[0] == p.sigil) { - primaryType_ = Type(p.sigil); - pathToBe = p.uriString + primaryId.mid(1); - break; - } - if (!secondaryId.isEmpty()) - pathToBe += "/event/" + secondaryId.mid(1); - setPath(pathToBe); - } - setQuery(std::move(query)); -} - -MatrixUri::MatrixUri(QUrl url) : QUrl(std::move(url)) -{ - // NB: url is moved from and empty by now - if (isEmpty()) - return; // primaryType_ == None - - primaryType_ = Invalid; - if (!QUrl::isValid()) // MatrixUri::isValid() checks primaryType_ - return; - - if (scheme() == "matrix") { - // Check sanity as per https://github.com/matrix-org/matrix-doc/pull/2312 - const auto& urlPath = path(); - const auto& splitPath = urlPath.splitRef('/'); - switch (splitPath.size()) { - case 2: - break; - case 4: - if (splitPath[2] == "event") - break; - [[fallthrough]]; - default: - return; // Invalid - } - - for (const auto& p: replacePairs) - if (urlPath.startsWith(p.uriString)) { - primaryType_ = Type(p.sigil); - return; // The only valid return path for matrix: URIs - } - qCWarning(MAIN) << "Invalid matrix: URI passed to MatrixUri"; - } - if (scheme() == "https" && authority() == "matrix.to") { - // See https://matrix.org/docs/spec/appendices#matrix-to-navigation - static const QRegularExpression MatrixToUrlRE { - R"(^/(?<main>[^/?]+)(/(?<sec>[^?]+))?(\?(?<query>.+))?$)" - }; - // matrix.to accepts both literal sigils (as well as & and ? used in - // its "query" substitute) and their %-encoded forms; - // so force QUrl to decode everything. - auto f = fragment(QUrl::FullyDecoded); - if (auto&& m = MatrixToUrlRE.match(f); m.hasMatch()) - *this = MatrixUri { m.captured("main").toUtf8(), - m.captured("sec").toUtf8(), - m.captured("query") }; - } -} - -MatrixUri::MatrixUri(const QString &uriOrId) - : MatrixUri(fromUserInput(uriOrId)) -{ } - -MatrixUri MatrixUri::fromUserInput(const QString& uriOrId) -{ - if (uriOrId.isEmpty()) - return {}; // type() == None - - // A quick check if uriOrId is a plain Matrix id - if (QStringLiteral("!@#+").contains(uriOrId[0])) - return MatrixUri { uriOrId.toUtf8() }; - - // Bare event ids cannot be resolved without a room scope but are treated as - // valid anyway; in the future we might expose them as, say, - // matrix:event/eventid - if (uriOrId[0] == '$') - return MatrixUri { "", uriOrId.toUtf8() }; - - return MatrixUri { QUrl::fromUserInput(uriOrId) }; -} - -MatrixUri::Type MatrixUri::type() const { return primaryType_; } - -MatrixUri::SecondaryType MatrixUri::secondaryType() const -{ - return path().section('/', 2, 2) == "event" ? EventId : NoSecondaryId; -} - -QUrl MatrixUri::toUrl(UriForm form) const -{ - if (!isValid()) - return {}; - - if (form == CanonicalUri) - return *this; - - QUrl url; - url.setScheme("https"); - url.setHost("matrix.to"); - url.setPath("/"); - auto fragment = primaryId(); - if (const auto& secId = secondaryId(); !secId.isEmpty()) - fragment += '/' + secId; - if (const auto& q = query(); !q.isEmpty()) - fragment += '?' + q; - url.setFragment(fragment); - return url; -} - -QString MatrixUri::toDisplayString(MatrixUri::UriForm form) const -{ - return toUrl(form).toDisplayString(); -} - -QString MatrixUri::primaryId() const -{ - if (primaryType_ == Empty || primaryType_ == Invalid) - return {}; - - const auto& idStem = path().section('/', 1, 1); - return idStem.isEmpty() ? idStem : primaryType_ + idStem; -} - -QString MatrixUri::secondaryId() const -{ - const auto& idStem = path().section('/', 3); - return idStem.isEmpty() ? idStem : secondaryType() + idStem; -} - -QString MatrixUri::action() const -{ - return QUrlQuery { query() }.queryItemValue("action"); -} - -QStringList MatrixUri::viaServers() const -{ - return QUrlQuery { query() }.allQueryItemValues(QStringLiteral("via"), - QUrl::EncodeReserved); -} - -bool MatrixUri::isValid() const -{ - return primaryType_ != Empty && primaryType_ != Invalid; -} - -UriResolveResult Quotient::visitResource( - Connection* account, const MatrixUri& uri, - std::function<void(User*)> userHandler, - std::function<void(Room*, QString)> roomEventHandler, - std::function<void(Connection*, QString, QStringList)> joinHandler) -{ - Q_ASSERT_X(account != nullptr, __FUNCTION__, - "The Connection argument passed to visit/openResource must not " - "be nullptr"); - if (uri.action() == "join") { - if (uri.type() != MatrixUri::RoomAlias - && uri.type() != MatrixUri::RoomId) - return MalformedUri; - - joinHandler(account, uri.primaryId(), uri.viaServers()); - return UriResolved; - } - - Room* room = nullptr; - switch (uri.type()) { - case MatrixUri::Invalid: - return MalformedUri; - case MatrixUri::Empty: - return EmptyMatrixId; - case MatrixUri::UserId: - if (auto* user = account->user(uri.primaryId())) { - userHandler(user); - return UriResolved; - } - return MalformedUri; - case MatrixUri::RoomId: - if ((room = account->room(uri.primaryId()))) - break; - return UnknownMatrixId; - case MatrixUri::RoomAlias: - if ((room = account->roomByAlias(uri.primaryId()))) - break; - [[fallthrough]]; - default: - return UnknownMatrixId; - } - roomEventHandler(room, uri.secondaryId()); - return UriResolved; -} - -UriResolveResult -ResourceResolver::openResource(Connection* account, const QString& identifier, - const QString& action) -{ - return openResource(account, MatrixUri(identifier), action); -} - -UriResolveResult ResourceResolver::openResource(Connection* account, - const MatrixUri& uri, - const QString& overrideAction) -{ - return visitResource( - account, uri, - [this, &overrideAction](User* u) { emit userAction(u, overrideAction); }, - [this, &overrideAction](Room* room, const QString& eventId) { - emit roomAction(room, eventId, overrideAction); - }, - [this](Connection* account, const QString& roomAliasOrId, - const QStringList& viaServers) { - emit joinAction(account, roomAliasOrId, viaServers); - }); -} diff --git a/lib/resourceresolver.h b/lib/resourceresolver.h deleted file mode 100644 index fea07e97..00000000 --- a/lib/resourceresolver.h +++ /dev/null @@ -1,181 +0,0 @@ -#pragma once - -#include "quotient_common.h" - -#include <QtCore/QUrl> -#include <QtCore/QUrlQuery> -#include <QtCore/QObject> - -#include <functional> - -namespace Quotient { -class Connection; -class Room; -class User; - -/*! \brief A wrapper around a Matrix URI or identifier - * - * This class encapsulates a Matrix resource identifier, passed in either of - * 3 forms: a plain Matrix identifier (sigil, localpart, serverpart or, for - * modern event ids, sigil and base64 hash); an MSC2312 URI (aka matrix: URI); - * or a matrix.to URL. The input can be either encoded (serverparts with - * punycode, the rest with percent-encoding) or unencoded (in this case it is - * the caller's responsibility to resolve all possible ambiguities). - * - * The class provides functions to check the validity of the identifier, - * its type, and obtain components, also in either unencoded (for displaying) - * or encoded (for APIs) form. - */ -class MatrixUri : private QUrl { - Q_GADGET - Q_PROPERTY(QString primaryId READ primaryId CONSTANT) - Q_PROPERTY(QString secondaryId READ secondaryId CONSTANT) -// Q_PROPERTY(QUrlQuery query READ query CONSTANT) - Q_PROPERTY(QString action READ action CONSTANT) -// Q_PROPERTY(QStringList viaServers READ viaServers CONSTANT) -public: - enum Type : char { - Invalid = char(-1), - Empty = 0x0, - UserId = '@', - RoomId = '!', - RoomAlias = '#', - Group = '+' - }; - Q_ENUM(Type) - enum SecondaryType : char { - NoSecondaryId = 0x0, - EventId = '$' - }; - - enum UriForm : short { CanonicalUri, MatrixToUri }; - Q_ENUM(UriForm) - - /// Construct an empty Matrix URI - MatrixUri() = default; - /*! \brief Decode a user input string to a Matrix identifier - * - * Accepts plain Matrix ids, MSC2312 URIs (aka matrix: URIs) and - * matrix.to URLs. In case of URIs/URLs, it uses QUrl's TolerantMode - * parser to decode common mistakes/irregularities (see QUrl documentation - * for more details). - */ - MatrixUri(const QString& uriOrId); - - /// Construct a Matrix URI from components - explicit MatrixUri(QByteArray primaryId, QByteArray secondaryId = {}, - QString query = {}); - /// Construct a Matrix URI from matrix.to or MSC2312 (matrix:) URI - explicit MatrixUri(QUrl url); - - static MatrixUri fromUserInput(const QString& uriOrId); - static MatrixUri fromUrl(QUrl url); - - /// Get the primary type of the Matrix URI (user id, room id or alias) - /*! Note that this does not include an event as a separate type, since - * events can only be addressed inside of rooms, which, in turn, are - * addressed either by id or alias. If you need to check whether the URI - * is specifically an event URI, use secondaryType() instead. - */ - Type type() const; - SecondaryType secondaryType() const; - QUrl toUrl(UriForm form = CanonicalUri) const; - QString toDisplayString(UriForm form = CanonicalUri) const; - QString primaryId() const; - QString secondaryId() const; - QString action() const; - QStringList viaServers() const; - bool isValid() const; - using QUrl::isEmpty, QUrl::path, QUrl::query, QUrl::fragment; - -private: - - Type primaryType_ = Empty; -}; - -/*! \brief Resolve the resource and invoke an action on it, visitor style - * - * This template function encapsulates the logic of resolving a Matrix - * identifier or URI into a Quotient object (or objects) and applying an - * appropriate action handler from the set provided by the caller to it. - * A typical use case for that is opening a room or mentioning a user in - * response to clicking on a Matrix URI or identifier. - * - * \param account The connection used as a context to resolve the identifier - * - * \param uri The Matrix identifier or URI; MSC2312 URIs and classic Matrix IDs - * are supported - * - * \sa ResourceResolver - */ -UriResolveResult -visitResource(Connection* account, const MatrixUri& uri, - std::function<void(User*)> userHandler, - std::function<void(Room*, QString)> roomEventHandler, - std::function<void(Connection*, QString, QStringList)> joinHandler); - -/*! \brief Matrix resource resolver - * TODO: rewrite - * Similar to visitResource(), this class encapsulates the logic of resolving - * a Matrix identifier or a URI into Quotient object(s) and applying an action - * to the resolved object(s). Instead of using a C++ visitor pattern, it - * announces the request through Qt's signals passing the resolved object(s) - * through those (still in a typesafe way). - * - * This class is aimed primarily at clients where invoking the resolving/action - * and handling the action are happening in decoupled parts of the code; it's - * also useful to operate on Matrix identifiers and URIs from QML/JS code - * that cannot call visitResource due to QML/C++ interface limitations. - */ -class ResourceResolver : public QObject { - Q_OBJECT -public: - explicit ResourceResolver(QObject* parent = nullptr) : QObject(parent) - { } - - /*! \brief Resolve the resource and request an action on it, signal style - * - * This method: - * 1. Resolves \p identifier into an actual object (Room or User), with - * possible additional data such as event id, in the context of - * \p account. - * 2. If the resolving is successful, depending on the type of the object, - * emits the respective signal to which the client must connect in order - * to apply the action to the resource (open a room, mention a user etc.). - * 3. Returns the result of resolving the resource. - * - * Note that the action can be applied either synchronously or entirely - * asynchronously; ResourceResolver does not restrain the client code - * to use either method. The resource resolving part is entirely synchronous - * though. If the synchronous operation is chosen, only - * direct connections to ResourceResolver signals must be used, and - * the caller should check the future's state immediately after calling - * openResource() to process any feedback from the resolver and/or action - * handler. If asynchronous operation is needed then either direct or queued - * connections to ResourceResolver's signals can be used and the caller - * must both check the ResourceFuture state right after calling openResource - * and also connect to ResourceFuture::ready() signal in order to process - * the result of resolving and action. - */ - Q_INVOKABLE UriResolveResult openResource(Connection* account, - const QString& identifier, - const QString& action = {}); - Q_INVOKABLE UriResolveResult - openResource(Connection* account, const MatrixUri& uri, - const QString& overrideAction = {}); - -signals: - /// An action on a user has been requested - void userAction(Quotient::User* user, QString action); - - /// An action on a room has been requested, with optional event id - void roomAction(Quotient::Room* room, QString eventId, QString action); - - /// A join action has been requested, with optional 'via' servers - void joinAction(Quotient::Connection* account, QString roomAliasOrId, - QStringList viaServers); -}; - -} // namespace Quotient - - diff --git a/lib/uri.cpp b/lib/uri.cpp new file mode 100644 index 00000000..e81933dc --- /dev/null +++ b/lib/uri.cpp @@ -0,0 +1,181 @@ +#include "uri.h" + +#include "logging.h" + +#include <QtCore/QRegularExpression> + +using namespace Quotient; + +struct ReplacePair { QByteArray uriString; char sigil; }; +static const auto replacePairs = { ReplacePair { "user/", '@' }, + { "roomid/", '!' }, + { "room/", '#' }, + // The notation for bare event ids is not + // proposed in MSC2312 (and anywhere, as yet) + { "event/", '$' } }; + +Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query) +{ + if (primaryId.isEmpty()) + primaryType_ = Empty; + else { + setScheme("matrix"); + QString pathToBe; + primaryType_ = Invalid; + for (const auto& p: replacePairs) + if (primaryId[0] == p.sigil) { + primaryType_ = Type(p.sigil); + pathToBe = p.uriString + primaryId.mid(1); + break; + } + if (!secondaryId.isEmpty()) + pathToBe += "/event/" + secondaryId.mid(1); + setPath(pathToBe); + } + setQuery(std::move(query)); +} + +Uri::Uri(QUrl url) : QUrl(std::move(url)) +{ + // NB: don't try to use `url` from here on, it's moved-from and empty + if (isEmpty()) + return; // primaryType_ == Empty + + if (!QUrl::isValid()) { // MatrixUri::isValid() checks primaryType_ + primaryType_ = Invalid; + return; + } + + if (scheme() == "matrix") { + // Check sanity as per https://github.com/matrix-org/matrix-doc/pull/2312 + const auto& urlPath = path(); + const auto& splitPath = urlPath.splitRef('/'); + switch (splitPath.size()) { + case 2: + break; + case 4: + if (splitPath[2] == "event") + break; + [[fallthrough]]; + default: + return; // Invalid + } + + for (const auto& p: replacePairs) + if (urlPath.startsWith(p.uriString)) { + primaryType_ = Type(p.sigil); + return; // The only valid return path for matrix: URIs + } + qCDebug(MAIN) << "The matrix: URI is not recognised:" + << toDisplayString(); + return; + } + + primaryType_ = NonMatrix; // Default, unless overridden by the code below + if (scheme() == "https" && authority() == "matrix.to") { + // See https://matrix.org/docs/spec/appendices#matrix-to-navigation + static const QRegularExpression MatrixToUrlRE { + R"(^/(?<main>[^/?]+)(/(?<sec>[^?]+))?(\?(?<query>.+))?$)" + }; + // matrix.to accepts both literal sigils (as well as & and ? used in + // its "query" substitute) and their %-encoded forms; + // so force QUrl to decode everything. + auto f = fragment(QUrl::FullyDecoded); + if (auto&& m = MatrixToUrlRE.match(f); m.hasMatch()) + *this = Uri { m.captured("main").toUtf8(), + m.captured("sec").toUtf8(), m.captured("query") }; + } +} + +Uri::Uri(const QString& uriOrId) : Uri(fromUserInput(uriOrId)) {} + +Uri Uri::fromUserInput(const QString& uriOrId) +{ + if (uriOrId.isEmpty()) + return {}; // type() == None + + // A quick check if uriOrId is a plain Matrix id + // Bare event ids cannot be resolved without a room scope as per the current + // spec but there's a movement towards making them navigable (see, e.g., + // https://github.com/matrix-org/matrix-doc/pull/2644) - so treat them + // as valid + if (QStringLiteral("!@#+$").contains(uriOrId[0])) + return Uri { uriOrId.toUtf8() }; + + return Uri { QUrl::fromUserInput(uriOrId) }; +} + +Uri::Type Uri::type() const { return primaryType_; } + +Uri::SecondaryType Uri::secondaryType() const +{ + return path().section('/', 2, 2) == "event" ? EventId : NoSecondaryId; +} + +QUrl Uri::toUrl(UriForm form) const +{ + if (!isValid()) + return {}; + + if (form == CanonicalUri || type() == NonMatrix) + return *this; + + QUrl url; + url.setScheme("https"); + url.setHost("matrix.to"); + url.setPath("/"); + auto fragment = primaryId(); + if (const auto& secId = secondaryId(); !secId.isEmpty()) + fragment += '/' + secId; + if (const auto& q = query(); !q.isEmpty()) + fragment += '?' + q; + url.setFragment(fragment); + return url; +} + +QString Uri::primaryId() const +{ + if (primaryType_ == Empty || primaryType_ == Invalid) + return {}; + + const auto& idStem = path().section('/', 1, 1); + return idStem.isEmpty() ? idStem : primaryType_ + idStem; +} + +QString Uri::secondaryId() const +{ + const auto& idStem = path().section('/', 3); + return idStem.isEmpty() ? idStem : secondaryType() + idStem; +} + +static const auto ActionKey = QStringLiteral("action"); + +QString Uri::action() const +{ + return type() == NonMatrix || !isValid() + ? QString() + : QUrlQuery { query() }.queryItemValue(ActionKey); +} + +void Uri::setAction(const QString& newAction) +{ + if (!isValid()) { + qCWarning(MAIN) << "Cannot set an action on an invalid Quotient::Uri"; + return; + } + QUrlQuery q { query() }; + q.removeQueryItem(ActionKey); + q.addQueryItem(ActionKey, newAction); + setQuery(q); +} + +QStringList Uri::viaServers() const +{ + return QUrlQuery { query() }.allQueryItemValues(QStringLiteral("via"), + QUrl::EncodeReserved); +} + +bool Uri::isValid() const +{ + return primaryType_ != Empty && primaryType_ != Invalid; +} diff --git a/lib/uri.h b/lib/uri.h new file mode 100644 index 00000000..270766dd --- /dev/null +++ b/lib/uri.h @@ -0,0 +1,85 @@ +#pragma once + +#include "quotient_common.h" + +#include <QtCore/QUrl> +#include <QtCore/QUrlQuery> + +namespace Quotient { + +/*! \brief A wrapper around a Matrix URI or identifier + * + * This class encapsulates a Matrix resource identifier, passed in either of + * 3 forms: a plain Matrix identifier (sigil, localpart, serverpart or, for + * modern event ids, sigil and base64 hash); an MSC2312 URI (aka matrix: URI); + * or a matrix.to URL. The input can be either encoded (serverparts with + * punycode, the rest with percent-encoding) or unencoded (in this case it is + * the caller's responsibility to resolve all possible ambiguities). + * + * The class provides functions to check the validity of the identifier, + * its type, and obtain components, also in either unencoded (for displaying) + * or encoded (for APIs) form. + */ +class Uri : private QUrl { + Q_GADGET +public: + enum Type : char { + Invalid = char(-1), + Empty = 0x0, + UserId = '@', + RoomId = '!', + RoomAlias = '#', + Group = '+', + BareEventId = '$', // https://github.com/matrix-org/matrix-doc/pull/2644 + NonMatrix = ':' + }; + Q_ENUM(Type) + enum SecondaryType : char { NoSecondaryId = 0x0, EventId = '$' }; + Q_ENUM(SecondaryType) + + enum UriForm : short { CanonicalUri, MatrixToUri }; + Q_ENUM(UriForm) + + /// Construct an empty Matrix URI + Uri() = default; + /*! \brief Decode a user input string to a Matrix identifier + * + * Accepts plain Matrix ids, MSC2312 URIs (aka matrix: URIs) and + * matrix.to URLs. In case of URIs/URLs, it uses QUrl's TolerantMode + * parser to decode common mistakes/irregularities (see QUrl documentation + * for more details). + */ + Uri(const QString& uriOrId); + + /// Construct a Matrix URI from components + explicit Uri(QByteArray primaryId, QByteArray secondaryId = {}, + QString query = {}); + /// Construct a Matrix URI from matrix.to or MSC2312 (matrix:) URI + explicit Uri(QUrl url); + + static Uri fromUserInput(const QString& uriOrId); + static Uri fromUrl(QUrl url); + + /// Get the primary type of the Matrix URI (user id, room id or alias) + /*! Note that this does not include an event as a separate type, since + * events can only be addressed inside of rooms, which, in turn, are + * addressed either by id or alias. If you need to check whether the URI + * is specifically an event URI, use secondaryType() instead. + */ + Q_INVOKABLE Type type() const; + Q_INVOKABLE SecondaryType secondaryType() const; + Q_INVOKABLE QUrl toUrl(UriForm form = CanonicalUri) const; + Q_INVOKABLE QString primaryId() const; + Q_INVOKABLE QString secondaryId() const; + Q_INVOKABLE QString action() const; + Q_INVOKABLE void setAction(const QString& newAction); + Q_INVOKABLE QStringList viaServers() const; + Q_INVOKABLE bool isValid() const; + using QUrl::path, QUrl::query, QUrl::fragment; + using QUrl::isEmpty, QUrl::toDisplayString; + +private: + Type primaryType_ = Empty; +}; + +} diff --git a/lib/uriresolver.cpp b/lib/uriresolver.cpp new file mode 100644 index 00000000..5052890b --- /dev/null +++ b/lib/uriresolver.cpp @@ -0,0 +1,110 @@ +#include "uriresolver.h" + +#include "connection.h" +#include "user.h" + +using namespace Quotient; + +UriResolveResult UriResolverBase::visitResource(Connection* account, + const Uri& uri) +{ + switch (uri.type()) { + case Uri::NonMatrix: + return visitNonMatrix(uri.toUrl()) ? UriResolved : CouldNotResolve; + case Uri::Invalid: + case Uri::Empty: + return InvalidUri; + default:; + } + + if (!account) + return NoAccount; + + switch (uri.type()) { + case Uri::UserId: { + if (uri.action() == "join") + return IncorrectAction; + auto* user = account->user(uri.primaryId()); + Q_ASSERT(user != nullptr); + visitUser(user, uri.action()); + return UriResolved; + } + case Uri::RoomId: + case Uri::RoomAlias: { + auto* room = uri.type() == Uri::RoomId + ? account->room(uri.primaryId()) + : account->roomByAlias(uri.primaryId()); + if (room != nullptr) { + visitRoom(room, uri.secondaryId()); + return UriResolved; + } + if (uri.action() == "join") { + joinRoom(account, uri.primaryId(), uri.viaServers()); + return UriResolved; + } + [[fallthrough]]; + } + default: + return CouldNotResolve; + } +} + +template <typename... FnTs> +class StaticUriDispatcher : public UriResolverBase { +public: + StaticUriDispatcher(const FnTs&... fns) : fns_(fns...) {} + +private: + void visitUser(User* user, const QString& action) override + { + std::get<0>(fns_)(user, action); + } + void visitRoom(Room* room, const QString& eventId) override + { + std::get<1>(fns_)(room, eventId); + } + void joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers = {}) override + { + std::get<2>(fns_)(account, roomAliasOrId, viaServers); + } + bool visitNonMatrix(const QUrl& url) override + { + return std::get<3>(fns_)(url); + } + + std::tuple<FnTs...> fns_; +}; + +UriResolveResult Quotient::visitResource( + Connection* account, const Uri& uri, + std::function<void(User*, QString)> userHandler, + std::function<void(Room*, QString)> roomEventHandler, + std::function<void(Connection*, QString, QStringList)> joinHandler, + std::function<bool(const QUrl&)> nonMatrixHandler) +{ + return StaticUriDispatcher(userHandler, roomEventHandler, joinHandler, + nonMatrixHandler) + .visitResource(account, uri); +} + +void UriDispatcher::visitUser(User *user, const QString &action) +{ + emit userAction(user, action); +} + +void UriDispatcher::visitRoom(Room *room, const QString &eventId) +{ + emit roomAction(room, eventId); +} + +void UriDispatcher::joinRoom(Connection *account, const QString &roomAliasOrId, const QStringList &viaServers) +{ + emit joinAction(account, roomAliasOrId, viaServers); +} + +bool UriDispatcher::visitNonMatrix(const QUrl &url) +{ + emit nonMatrixAction(url); + return true; +} diff --git a/lib/uriresolver.h b/lib/uriresolver.h new file mode 100644 index 00000000..914ddf02 --- /dev/null +++ b/lib/uriresolver.h @@ -0,0 +1,162 @@ +#pragma once + +#include "uri.h" + +#include <QtCore/QObject> + +#include <functional> + +namespace Quotient { +class Connection; +class Room; +class User; + +/*! \brief Abstract class to resolve the resource and act on it + * + * This class encapsulates the logic of resolving a Matrix identifier or URI + * into a Quotient object (or objects) and calling an appropriate handler on it. + * It is a type-safe way of handling a URI with no prior context on its type + * in cases like, e.g., when a user clicks on a URI in the application. + * + * This class provides empty "handlers" for each type of URI to facilitate + * gradual implementation. Derived classes are encouraged to override as many + * of them as possible. + */ +class UriResolverBase { +public: + /*! \brief Resolve the resource and dispatch an action depending on its type + * + * This method: + * 1. Resolves \p uri into an actual object (e.g., Room or User), + * with possible additional data such as event id, in the context of + * \p account. + * 2. If the resolving is successful, depending on the type of the object, + * calls the appropriate virtual function (defined in a derived + * concrete class) to perform an action on the resource (open a room, + * mention a user etc.). + * 3. Returns the result of resolving the resource. + */ + UriResolveResult visitResource(Connection* account, const Uri& uri); + +protected: + /// Called by visitResource() when the passed URI identifies a Matrix user + virtual void visitUser(User* user, const QString& action) {} + /// Called by visitResource() when the passed URI identifies a room or + /// an event in a room + virtual void visitRoom(Room* room, const QString& eventId) {} + /// Called by visitResource() when the passed URI has `action() == "join"` + /// and identifies a room that the user defined by the Connection argument + /// is not a member of + virtual void joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers = {}) + {} + /// Called by visitResource() when the passed URI has `type() == NonMatrix` + /*! + * Should return true if the URI is considered resolved, false otherwise. + * A basic implementation in a graphical client can look like + * `return QDesktopServices::openUrl(url);` but it's strongly advised to + * ask for a user confirmation beforehand. + */ + virtual bool visitNonMatrix(const QUrl& url) { return false; } +}; + +/*! \brief Resolve the resource and invoke an action on it, via function objects + * + * This function encapsulates the logic of resolving a Matrix identifier or URI + * into a Quotient object (or objects) and calling an appropriate handler on it. + * Unlike UriResolverBase it accepts the list of handlers from + * the caller; internally it's uses a minimal UriResolverBase class + * + * \param account The connection used as a context to resolve the identifier + * + * \param uri A URI that can represent a Matrix entity + * + * \param userHandler Called when the passed URI identifies a Matrix user + * + * \param roomEventHandler Called when the passed URI identifies a room or + * an event in a room + * + * \param joinHandler Called when the passed URI has `action() == "join"` and + * identifies a room that the user defined by + * the Connection argument is not a member of + * + * \param nonMatrixHandler Called when the passed URI has `type() == NonMatrix`; + * should return true if the URI is considered resolved, + * false otherwise + * + * \sa UriResolverBase, UriDispatcher + */ +UriResolveResult +visitResource(Connection* account, const Uri& uri, + std::function<void(User*, QString)> userHandler, + std::function<void(Room*, QString)> roomEventHandler, + std::function<void(Connection*, QString, QStringList)> joinHandler, + std::function<bool(const QUrl&)> nonMatrixHandler); + +/*! \brief Check that the resource is resolvable with no action on it */ +inline UriResolveResult checkResource(Connection* account, + const Uri& uri) +{ + return visitResource( + account, uri, [](auto, auto) {}, [](auto, auto) {}, + [](auto, auto, auto) {}, [](auto) { return false; }); +} + +/*! \brief Resolve the resource and invoke an action on it, via Qt signals + * + * This is an implementation of UriResolverBase that is based on + * QObject and uses Qt signals instead of virtual functions to provide an + * open-ended interface for visitors. + * + * This class is aimed primarily at clients where invoking the resolving/action + * and handling the action are happening in decoupled parts of the code; it's + * also useful to operate on Matrix identifiers and URIs from QML/JS code + * that cannot call resolveResource() due to QML/C++ interface limitations. + * + * This class does not restrain the client code to a certain type of + * connections: both direct and queued (or a mix) will work fine. One limitation + * caused by that is there's no way to indicate if a non-Matrix URI has been + * successfully resolved - a signal always returns void. + * + * Note that in case of using (non-blocking) queued connections the code that + * calls resolveResource() should not expect the action to be performed + * synchronously - the returned value is the result of resolving the URI, + * not acting on it. + */ +class UriDispatcher : public QObject, public UriResolverBase { + Q_OBJECT +public: + explicit UriDispatcher(QObject* parent = nullptr) : QObject(parent) {} + + // It's actually UriResolverBase::visitResource() but with Q_INVOKABLE + Q_INVOKABLE UriResolveResult resolveResource(Connection* account, + const Uri& uri) + { + return UriResolverBase::visitResource(account, uri); + } + +signals: + /// An action on a user has been requested + void userAction(Quotient::User* user, QString action); + + /// An action on a room has been requested, with optional event id + void roomAction(Quotient::Room* room, QString eventId); + + /// A join action has been requested, with optional 'via' servers + void joinAction(Quotient::Connection* account, QString roomAliasOrId, + QStringList viaServers); + + /// An action on a non-Matrix URL has been requested + void nonMatrixAction(QUrl url); + +private: + void visitUser(User* user, const QString& action) override; + void visitRoom(Room* room, const QString& eventId) override; + void joinRoom(Connection* account, const QString& roomAliasOrId, + const QStringList& viaServers = {}) override; + bool visitNonMatrix(const QUrl& url) override; +}; + +} // namespace Quotient + + diff --git a/libquotient.pri b/libquotient.pri index f0057712..98fe3b03 100644 --- a/libquotient.pri +++ b/libquotient.pri @@ -34,7 +34,8 @@ HEADERS += \ $$SRCPATH/room.h \ $$SRCPATH/user.h \ $$SRCPATH/avatar.h \ - $$SRCPATH/resourceresolver.h \ + $$SRCPATH/uri.h \ + $$SRCPATH/uriresolver.h \ $$SRCPATH/syncdata.h \ $$SRCPATH/quotient_common.h \ $$SRCPATH/util.h \ @@ -91,7 +92,8 @@ SOURCES += \ $$SRCPATH/room.cpp \ $$SRCPATH/user.cpp \ $$SRCPATH/avatar.cpp \ - $$SRCPATH/resourceresolver.cpp \ + $$SRCPATH/uri.cpp \ + $$SRCPATH/uriresolver.cpp \ $$SRCPATH/syncdata.cpp \ $$SRCPATH/util.cpp \ $$SRCPATH/events/event.cpp \ diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 52915454..c2c50df2 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -2,7 +2,7 @@ #include "connection.h" #include "room.h" #include "user.h" -#include "resourceresolver.h" +#include "uriresolver.h" #include "csapi/joining.h" #include "csapi/leaving.h" @@ -622,7 +622,7 @@ TEST_IMPL(visitResources) // assumes that ResourceResolver::openResource is implemented in terms // of ResourceResolver::visitResource, so the latter doesn't need a // separate test. - static ResourceResolver rr; + static UriDispatcher ud; // This lambda returns true in case of error, false if it's fine so far auto testResourceResolver = [this, thisTest](const QStringList& uris, @@ -630,12 +630,12 @@ TEST_IMPL(visitResources) QVariantList otherArgs = {}) { int r = qRegisterMetaType<decltype(target)>(); Q_ASSERT(r != 0); - QSignalSpy spy(&rr, signal); + QSignalSpy spy(&ud, signal); for (const auto& uriString: uris) { - MatrixUri uri { uriString }; + Uri uri { uriString }; clog << "Checking " << uriString.toStdString() << " -> " << uri.toDisplayString().toStdString() << endl; - rr.openResource(connection(), uriString); + ud.visitResource(connection(), uriString); if (spy.count() != 1) { clog << "Wrong number of signal emissions (" << spy.count() << ')' << endl; @@ -666,7 +666,7 @@ TEST_IMPL(visitResources) // Basic tests QUrl invalidUrl { "https://" }; invalidUrl.setAuthority("---:@@@"); - const MatrixUri emptyUri {}, uriFromEmptyUrl {}, + const Uri emptyUri {}, uriFromEmptyUrl {}, invalidMatrixUri { QStringLiteral("matrix:&invalid@") }, matrixUriFromInvalidUrl { invalidUrl }; @@ -710,7 +710,7 @@ TEST_IMPL(visitResources) // exist, as yet) - only to be syntactically correct static const auto& joinRoomAlias = QStringLiteral("unjoined:example.org"); // # will be added below - QString joinQuery { "?action=join" }; + static const QString joinQuery { "?action=join" }; static const QStringList joinByAliasUris { "matrix:room/" + joinRoomAlias + joinQuery, "https://matrix.to/#/%23"/*`#`*/ + joinRoomAlias + joinQuery @@ -728,14 +728,14 @@ TEST_IMPL(visitResources) }; // If any test breaks, the breaking call will return true, and further // execution will be cut by ||'s short-circuiting - if (testResourceResolver(roomUris, &ResourceResolver::roomAction, room()) - || testResourceResolver(userUris, &ResourceResolver::userAction, + if (testResourceResolver(roomUris, &UriDispatcher::roomAction, room()) + || testResourceResolver(userUris, &UriDispatcher::userAction, connection()->user()) - || testResourceResolver(eventUris, &ResourceResolver::roomAction, + || testResourceResolver(eventUris, &UriDispatcher::roomAction, room(), { eventId }) - || testResourceResolver(joinByAliasUris, &ResourceResolver::joinAction, + || testResourceResolver(joinByAliasUris, &UriDispatcher::joinAction, connection(), { '#' + joinRoomAlias }) - || testResourceResolver(joinByIdUris, &ResourceResolver::joinAction, + || testResourceResolver(joinByIdUris, &UriDispatcher::joinAction, connection(), { joinRoomId, viaServers })) return true; // TODO: negative cases |