aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorKitsune Ral <Kitsune-Ral@users.sf.net>2020-07-22 21:04:58 +0200
committerGitHub <noreply@github.com>2020-07-22 21:04:58 +0200
commit894755776f0d100032eb4346ea4a1f07defb61cb (patch)
tree89754430a385e9412732f1ba9ac99c8511edd831 /lib
parent9f9577ccdebad84faf96766f8e5b07e2f2b605c5 (diff)
parentbd74588539d8a5922e9f51eb691052c06c02a5ed (diff)
downloadlibquotient-894755776f0d100032eb4346ea4a1f07defb61cb.tar.gz
libquotient-894755776f0d100032eb4346ea4a1f07defb61cb.zip
Merge pull request #407 from quotient-im/kitsune-resource-resolver
Matrix URIs and resolving them
Diffstat (limited to 'lib')
-rw-r--r--lib/quotient_common.h14
-rw-r--r--lib/uri.cpp191
-rw-r--r--lib/uri.h85
-rw-r--r--lib/uriresolver.cpp112
-rw-r--r--lib/uriresolver.h168
5 files changed, 563 insertions, 7 deletions
diff --git a/lib/quotient_common.h b/lib/quotient_common.h
index 44541b42..bb05af05 100644
--- a/lib/quotient_common.h
+++ b/lib/quotient_common.h
@@ -14,15 +14,15 @@ enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 };
Q_ENUM_NS(RunningPolicy)
-enum ResourceResolveResult : short {
+enum UriResolveResult : short {
StillResolving = -1,
- Resolved = 0,
- UnknownMatrixId,
- MalformedMatrixId,
- NoAccount,
- EmptyMatrixId
+ UriResolved = 0,
+ CouldNotResolve,
+ IncorrectAction,
+ InvalidUri,
+ NoAccount
};
-Q_ENUM_NS(ResourceResolveResult)
+Q_ENUM_NS(UriResolveResult)
} // namespace Quotient
/// \deprecated Use namespace Quotient instead
diff --git a/lib/uri.cpp b/lib/uri.cpp
new file mode 100644
index 00000000..f813794c
--- /dev/null
+++ b/lib/uri.cpp
@@ -0,0 +1,191 @@
+#include "uri.h"
+
+#include "logging.h"
+
+#include <QtCore/QRegularExpression>
+
+using namespace Quotient;
+
+struct ReplacePair { QByteArray uriString; char sigil; };
+/// Defines bi-directional mapping of path prefixes and sigils
+static const auto replacePairs = {
+ ReplacePair { "user/", '@' },
+ { "roomid/", '!' },
+ { "room/", '#' },
+ // The notation for bare event ids is not proposed in MSC2312 but there's
+ // https://github.com/matrix-org/matrix-doc/pull/2644
+ { "event/", '$' }
+};
+
+Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query)
+{
+ if (primaryId.isEmpty())
+ primaryType_ = Empty;
+ else {
+ setScheme("matrix");
+ QString pathToBe;
+ primaryType_ = Invalid;
+ if (primaryId.size() < 2) // There should be something after sigil
+ return;
+ for (const auto& p: replacePairs)
+ if (primaryId[0] == p.sigil) {
+ primaryType_ = Type(p.sigil);
+ pathToBe = p.uriString + primaryId.mid(1);
+ break;
+ }
+ if (!secondaryId.isEmpty()) {
+ if (secondaryId.size() < 2) {
+ primaryType_ = Invalid;
+ return;
+ }
+ 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..ec30512c
--- /dev/null
+++ b/lib/uriresolver.cpp
@@ -0,0 +1,112 @@
+#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);
+ return visitUser(user, uri.action());
+ }
+ 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;
+ }
+}
+
+// This template is only instantiated once, for Quotient::visitResource()
+template <typename... FnTs>
+class StaticUriDispatcher : public UriResolverBase {
+public:
+ StaticUriDispatcher(const FnTs&... fns) : fns_(fns...) {}
+
+private:
+ UriResolveResult visitUser(User* user, const QString& action) override
+ {
+ return 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<UriResolveResult(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);
+}
+
+UriResolveResult UriDispatcher::visitUser(User *user, const QString &action)
+{
+ emit userAction(user, action);
+ return UriResolved;
+}
+
+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..9b2ced9d
--- /dev/null
+++ b/lib/uriresolver.h
@@ -0,0 +1,168 @@
+#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
+ /*!
+ * \return IncorrectAction if the action is not correct or not supported;
+ * UriResolved if it is accepted; other values are disallowed
+ */
+ virtual UriResolveResult visitUser(User* user, const QString& action)
+ {
+ return IncorrectAction;
+ }
+ /// 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<UriResolveResult(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) { return UriResolved; }, [](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:
+ UriResolveResult 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
+
+