aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorKitsune Ral <Kitsune-Ral@users.sf.net>2020-07-19 16:12:09 +0200
committerKitsune Ral <Kitsune-Ral@users.sf.net>2020-07-19 16:12:09 +0200
commitaf329351289606f3cb1ef865cb0cbe61c1d1711b (patch)
tree41b1f00b496b07e0c1f7ea5d576beb2f52376693 /lib
parent227d7c0ba26c3eb3e7394e0a5b7cc79544db7515 (diff)
downloadlibquotient-af329351289606f3cb1ef865cb0cbe61c1d1711b.tar.gz
libquotient-af329351289606f3cb1ef865cb0cbe61c1d1711b.zip
MatrixUri->Uri: Extend to non-Matrix URIs
Diffstat (limited to 'lib')
-rw-r--r--lib/quotient_common.h8
-rw-r--r--lib/resourceresolver.cpp236
-rw-r--r--lib/resourceresolver.h181
-rw-r--r--lib/uri.cpp181
-rw-r--r--lib/uri.h85
-rw-r--r--lib/uriresolver.cpp110
-rw-r--r--lib/uriresolver.h162
7 files changed, 542 insertions, 421 deletions
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
+
+