Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85803643
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
24 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/client/matrix-uri.cpp b/src/client/matrix-uri.cpp
index fbfb0f0..b3f2d59 100644
--- a/src/client/matrix-uri.cpp
+++ b/src/client/matrix-uri.cpp
@@ -1,345 +1,356 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2022 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <boost/algorithm/string/split.hpp>
#include <immer/map.hpp>
#include "matrix-uri.hpp"
namespace Kazv
{
std::string desigilify(std::string id)
{
if (!id.empty() && (
id[0] == '#' || id[0] == '!' || id[0] == '@' || id[0] == '$'
)) {
id.erase(id.begin());
}
return id;
}
static const std::string matrixProtocol = "matrix";
struct MatrixUri::Private
{
enum Type
{
Invalid = 0x0,
RoomId = 0x1,
RoomAlias = 0x2,
User = 0x4,
Event = 0x8,
};
unsigned int type{Invalid};
std::string upperLevel{};
std::string lowerLevel{};
std::string action{};
immer::flex_vector<std::string> routing{};
immer::map<std::string, std::string> query{};
std::string fragment{};
};
MatrixUri::MatrixUri()
: m_d(std::make_unique<Private>())
{
}
MatrixUri::MatrixUri(std::unique_ptr<Private> d)
: m_d(std::move(d))
{
}
MatrixUri MatrixUri::withUser(std::string userId)
{
return MatrixUri(std::unique_ptr<Private>(new Private{Private::User, std::move(userId)}));
}
MatrixUri MatrixUri::withRoomById(std::string roomId)
{
return MatrixUri(std::unique_ptr<Private>(new Private{Private::RoomId, std::move(roomId)}));
}
MatrixUri MatrixUri::withRoomByAlias(std::string alias)
{
return MatrixUri(std::unique_ptr<Private>(new Private{Private::RoomAlias, std::move(alias)}));
}
+ MatrixUri MatrixUri::withEvent(std::string eventId) const
+ {
+ if (!isRoom()) {
+ return MatrixUri();
+ }
+
+ auto r = *this;
+ r.m_d->type |= Private::Event;
+ r.m_d->lowerLevel = eventId;
+ return r;
+ }
+
MatrixUri::~MatrixUri() = default;
KAZV_DEFINE_COPYABLE_UNIQUE_PTR(MatrixUri, m_d);
bool MatrixUri::isValid() const
{
- return m_d->type;
+ return m_d && m_d->type;
}
bool MatrixUri::isRoom() const
{
return m_d->type == Private::RoomId || m_d->type == Private::RoomAlias;
}
bool MatrixUri::isUser() const
{
return m_d->type == Private::User;
}
bool MatrixUri::isEvent() const
{
return m_d->type & Private::Event;
}
std::string MatrixUri::id() const
{
if (isEvent()) {
return m_d->lowerLevel;
} else {
return m_d->upperLevel;
}
}
std::string MatrixUri::userOrRoom() const
{
return m_d->upperLevel;
}
std::string MatrixUri::action() const
{
return m_d->action;
}
MatrixUri MatrixUri::withAction(std::string act) const
{
auto r = *this;
r.m_d->action = act;
return r;
}
auto MatrixUri::routing() const -> immer::flex_vector<std::string>
{
return m_d->routing;
}
MatrixUri MatrixUri::withVia(std::string authority) const
{
auto r = *this;
r.m_d->routing = std::move(r.m_d->routing).push_back(authority);
return r;
}
std::string MatrixUri::query(std::string key) const
{
if (key == "via") {
return m_d->routing.size() ? m_d->routing.back() : std::string();
} else if (key == "action") {
return m_d->action;
} else {
return m_d->query[key];
}
}
MatrixUri MatrixUri::withQuery(std::string key, std::string value) const
{
if (key == "via") {
return withVia(std::move(value));
} else if (key == "action") {
return withAction(std::move(value));
} else {
auto r = *this;
r.m_d->query = std::move(r.m_d->query).set(std::move(key), std::move(value));
return r;
}
}
std::string MatrixUri::fragment() const
{
return m_d->fragment;
}
MatrixUri MatrixUri::withFragment(std::string frag) const
{
auto r = *this;
r.m_d->fragment = frag;
return r;
}
std::string MatrixUri::toString() const
{
std::string res = matrixProtocol + ":";
bool hasQuery = false;
auto putUpperLevel = [this, &res]() {
if (m_d->type & Private::RoomId) {
res += "roomid/";
} else if (m_d->type & Private::RoomAlias) {
res += "r/";
} else if (m_d->type & Private::User) {
res += "u/";
}
res += desigilify(m_d->upperLevel);
};
auto putLowerLevel = [this, &res]() {
if (m_d->type & Private::Event) {
res += "/e/" + desigilify(m_d->lowerLevel);
}
};
auto putQueryPrefix = [this, &res, &hasQuery]() {
if (hasQuery) {
res += "&";
} else {
hasQuery = true;
res += "?";
}
};
auto putAction = [this, &res, putQueryPrefix]() {
if (!m_d->action.empty()) {
putQueryPrefix();
res += "action=" + m_d->action;
}
};
auto putRouting = [this, &res, putQueryPrefix]() {
for (const auto &value: m_d->routing) {
putQueryPrefix();
res += "via=" + value;
}
};
auto putCustomQuery = [this, &res, putQueryPrefix]() {
for (const auto &[key, value]: m_d->query) {
putQueryPrefix();
res += key + "=" + value;
}
};
auto putFragment = [this, &res]() {
if (!m_d->fragment.empty()) {
res += "#" + m_d->fragment;
}
};
putUpperLevel();
putLowerLevel();
putAction();
putRouting();
putCustomQuery();
putFragment();
return res;
}
MatrixUri MatrixUri::parse(std::string uri)
{
// Ref: https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2312-matrix-uri.md#recommended-implementation
MatrixUri res;
bool valid = true;
std::vector<std::string> entities;
std::string query;
std::string fragment;
auto verifyProto = [&res, &valid, &uri]() {
const auto &protoPart = matrixProtocol + ":";
if (uri.substr(0, protoPart.size()) != protoPart) {
valid = false;
}
uri.erase(uri.begin(), uri.begin() + protoPart.size());
};
auto withValid = [valid](auto func) {
if (valid) {
func();
}
};
auto splitParts = [&res, &valid, &uri, &entities, &query, &fragment]() {
auto fragPos = uri.find('#');
if (fragPos != std::string::npos) {
fragment = std::string(uri.begin() + fragPos + 1, uri.end());
uri.erase(uri.begin() + fragPos, uri.end());
}
auto queryPos = uri.find('?');
if (queryPos != std::string::npos) {
query = std::string(uri.begin() + queryPos + 1, uri.end());
uri.erase(uri.begin() + queryPos, uri.end());
}
boost::algorithm::split(entities, uri, [](char c) { return c == '/'; });
if (entities.size() != 2 && entities.size() != 4) {
valid = false;
}
};
auto putUpperLevel = [&entities, &res, &valid]() {
std::string sigil;
if (entities[0] == "room" || entities[0] == "r") {
res.m_d->type |= Private::RoomAlias;
sigil = "#";
} else if (entities[0] == "user" || entities[0] == "u") {
res.m_d->type |= Private::User;
sigil = "@";
} else if (entities[0] == "roomid") {
res.m_d->type |= Private::RoomId;
sigil = "!";
} else {
valid = false;
}
res.m_d->upperLevel = sigil + entities[1];
entities.erase(entities.begin(), entities.begin() + 2);
};
auto putLowerLevel = [&entities, &res, &valid]() {
if (entities.size() < 2) {
return;
}
if (entities[0] == "event" || entities[0] == "e") {
- res.m_d->type |= Private::Event;
- res.m_d->lowerLevel = "$" + entities[1];
+ res = res.withEvent("$" + entities[1]);
} else {
valid = false;
}
};
auto putQueryParams = [&query, &res]() {
std::vector<std::string> queryParams;
boost::algorithm::split(queryParams, query, [](char c) { return c == '&'; });
for (const auto &entry : queryParams) {
auto valPos = entry.find('=');
auto key = entry.substr(0, valPos);
auto value = entry.substr(valPos + 1);
res = res.withQuery(key, value);
}
};
auto putFragment = [&fragment, &res]() {
if (!fragment.empty()) {
res = res.withFragment(fragment);
}
};
verifyProto();
withValid(splitParts);
withValid(putUpperLevel);
withValid(putLowerLevel);
withValid(putQueryParams);
withValid(putFragment);
if (valid) {
return res;
} else {
return MatrixUri();
}
}
}
diff --git a/src/client/matrix-uri.hpp b/src/client/matrix-uri.hpp
index 62b6b14..5cdfa21 100644
--- a/src/client/matrix-uri.hpp
+++ b/src/client/matrix-uri.hpp
@@ -1,197 +1,206 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2022 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <string>
#include <immer/flex_vector.hpp>
#include <copy-helper.hpp>
namespace Kazv
{
/**
* Remove the sigil of an id.
*
* @param id The id with sigil.
* @return id without sigil.
*/
std::string desigilify(std::string id);
/**
* Represents a MatrixUri.
*
* https://spec.matrix.org/v1.2/appendices/#uris
*/
class MatrixUri
{
public:
/**
* Create a matrix uri by parsing a string.
*
* If uri is not well-formed, it returns an invalid MatrixUri.
* @param uri The uri to parse.
* @return The MatrixUri parsed.
*/
static MatrixUri parse(std::string uri);
/**
* Create a matrix uri for a room from the alias of it.
* @param alias The alias of the room.
* @return A MatrixUri representing the room.
*/
static MatrixUri withRoomByAlias(std::string alias);
/**
* Create a matrix uri for a user from the user id.
* @param userId The user id of the user.
* @rutern A MatrixUri representing the user.
*/
static MatrixUri withUser(std::string userId);
/**
* Create a matrix uri for a room from the id of it.
* @param roomId The id of the room.
* @return A MatrixUri representing the room.
*/
static MatrixUri withRoomById(std::string roomId);
/**
* Constructs an invalid MatrixUri.
*/
MatrixUri();
~MatrixUri();
KAZV_DECLARE_COPYABLE(MatrixUri);
/**
* Check whether this is valid.
* @return true iff this is valid.
*/
bool isValid() const;
/**
* Check whether this represents a room.
* @return true iff this represents a room.
*/
bool isRoom() const;
/**
* Check whether this represents a user.
* @return true iff this represents a user.
*/
bool isUser() const;
/**
* Check whether this represents an event.
* @return true iff this represents an event.
*/
bool isEvent() const;
+ /**
+ * Create a MatrixUri from this referring to the event.
+ *
+ * This must represent a room.
+ * @param eventId The event id.
+ * @return A MatrixUri representing the event in the room.
+ */
+ MatrixUri withEvent(std::string eventId) const;
+
/**
* Get the lowest-level id of this.
*
* @return If this represents an event, the event id.
* If this represents a user or room, the user id, room id, or room alias.
*/
std::string id() const;
/**
* Get the user id, room id or room alias of this.
* @return If this represents a user, the user id.
* If this represents a room, the room id or alias.
*/
std::string userOrRoom() const;
/**
* Get the action for this MatrixUri.
* @return The action. Or empty string if there is no action.
*/
std::string action() const;
/**
* Create a MatrixUri from this with action `act`.
* @param act The action to use.
* @return The resulting MatrixUri.
*/
MatrixUri withAction(std::string act) const;
/**
* Get the authorities specified in the MatrixUri.
* @return A RangeT of std::string containing the authorities,
* or an empty one if there are none.
*/
auto routing() const -> immer::flex_vector<std::string>;
/**
* Create a MatrixUri from this via `authority`.
* @param authority The server to use to discover the entity.
* @return The resulting MatrixUri.
*/
MatrixUri withVia(std::string authority) const;
/**
* Create a MatrixUri from this via `authorities`.
* @param authorities A RangeT of servers to use to discover the entity.
* @return The resulting MatrixUri.
*/
template<class RangeT, class = std::enable_if_t<!std::is_convertible_v<RangeT, std::string>>>
MatrixUri withVia(RangeT authorities) const
{
return std::accumulate(authorities.begin(), authorities.end(), *this,
[](MatrixUri acc, std::string next) {
return acc.withVia(std::move(next));
});
}
/**
* Get a query param of this.
* @param key The key to obtain.
* @return The value corresponding to key, or empty string if
* the key does not exist.
* If `key` is `"via"`, it returns the last value in the list.
*/
std::string query(std::string key) const;
/**
* Create a MatrixUri from this with a query parameter.
* If `key` is `"via"`, it appends value to the routing list.
* @param key The key of the query.
* @param value The value of the query.
* @return The resulting MatrixUri.
*/
MatrixUri withQuery(std::string key, std::string value) const;
/**
* Get the string representation of this.
* @return A formatted matrix uri.
*/
std::string toString() const;
/**
* Get the fragment of this.
* @return The fragment.
*/
std::string fragment() const;
/**
* Create a MatrixUri from this but with the fragment part replaced with `fragment`.
*
* @param fragment The fragment part. It does not contain `#`.
* @return The resulting MatrixUri.
*/
MatrixUri withFragment(std::string fragment) const;
private:
struct Private;
std::unique_ptr<Private> m_d;
MatrixUri(std::unique_ptr<Private> d);
};
}
diff --git a/src/tests/client/matrix-uri-test.cpp b/src/tests/client/matrix-uri-test.cpp
index 8472215..92dc4db 100644
--- a/src/tests/client/matrix-uri-test.cpp
+++ b/src/tests/client/matrix-uri-test.cpp
@@ -1,86 +1,253 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2022 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <catch2/catch.hpp>
#include <matrix-uri.hpp>
using namespace Kazv;
TEST_CASE("MatrixUri::parse()", "[client][matrix-uri]")
{
SECTION("room by alias")
{
auto uri = MatrixUri::parse("matrix:r/somewhere:example.org");
REQUIRE(uri.isValid());
REQUIRE(uri.isRoom());
REQUIRE(uri.id() == "#somewhere:example.org");
}
SECTION("room by id")
{
auto uri = MatrixUri::parse("matrix:roomid/somewhere:example.org?via=elsewhere.ca");
REQUIRE(uri.isValid());
REQUIRE(uri.isRoom());
REQUIRE(uri.id() == "!somewhere:example.org");
REQUIRE(uri.routing() == immer::flex_vector<std::string>{"elsewhere.ca"});
}
SECTION("room by alias with event")
{
auto uri = MatrixUri::parse("matrix:r/somewhere:example.org/e/event");
REQUIRE(uri.isValid());
REQUIRE(uri.isEvent());
REQUIRE(uri.userOrRoom() == "#somewhere:example.org");
REQUIRE(uri.id() == "$event");
}
SECTION("room by id with event")
{
auto uri = MatrixUri::parse("matrix:roomid/somewhere:example.org/e/event?via=elsewhere.ca");
REQUIRE(uri.isValid());
REQUIRE(uri.isEvent());
REQUIRE(uri.userOrRoom() == "!somewhere:example.org");
REQUIRE(uri.id() == "$event");
REQUIRE(uri.routing() == immer::flex_vector<std::string>{"elsewhere.ca"});
}
+ SECTION("multiple via")
+ {
+ auto uri = MatrixUri::parse("matrix:roomid/somewhere:example.org/e/event?via=a&via=b&via=c");
+ REQUIRE(uri.isValid());
+ REQUIRE(uri.isEvent());
+ REQUIRE(uri.routing() == immer::flex_vector<std::string>{"a", "b", "c"});
+ }
+
SECTION("user")
{
auto uri = MatrixUri::parse("matrix:u/alice:example.org?action=chat");
REQUIRE(uri.isValid());
REQUIRE(uri.isUser());
REQUIRE(uri.userOrRoom() == "@alice:example.org");
REQUIRE(uri.id() == "@alice:example.org");
REQUIRE(uri.action() == "chat");
}
+
+ SECTION("custom query")
+ {
+ auto uri = MatrixUri::parse("matrix:u/alice:example.org?lol=mew&lol=wow&foo=bar");
+ REQUIRE(uri.isValid());
+ REQUIRE(uri.isUser());
+ REQUIRE(uri.query("lol") == "wow");
+ REQUIRE(uri.query("foo") == "bar");
+ }
+
+ SECTION("fragment")
+ {
+ auto uri = MatrixUri::parse("matrix:u/alice:example.org#aaabbb");
+ REQUIRE(uri.isValid());
+ REQUIRE(uri.isUser());
+ REQUIRE(uri.fragment() == "aaabbb");
+ }
}
TEST_CASE("desigilify()", "[client][matrix-uri]")
{
SECTION("It removes #, !, @ and $")
{
REQUIRE(desigilify("#a") == "a");
REQUIRE(desigilify("!a") == "a");
REQUIRE(desigilify("@a") == "a");
REQUIRE(desigilify("$a") == "a");
}
SECTION("It does not remove other symbols")
{
REQUIRE(desigilify("*a") == "*a");
REQUIRE(desigilify("ua") == "ua");
REQUIRE(desigilify("喵喵") == "喵喵");
}
SECTION("It does not crash with empty identifiers")
{
REQUIRE(desigilify("@") == "");
REQUIRE(desigilify("") == "");
}
}
+
+TEST_CASE("MatrixUri is copyable and movable", "[client][matrix-uri]")
+{
+ auto a = MatrixUri::withRoomById("!aaa:example.org");
+ auto b = a;
+ REQUIRE(b.id() == "!aaa:example.org");
+ REQUIRE(a.id() == "!aaa:example.org");
+
+ a = b;
+ REQUIRE(b.id() == "!aaa:example.org");
+ REQUIRE(a.id() == "!aaa:example.org");
+
+ auto c = std::move(a);
+ REQUIRE(!a.isValid());
+ REQUIRE(c.id() == "!aaa:example.org");
+
+ a = std::move(c);
+ REQUIRE(!c.isValid());
+ REQUIRE(a.id() == "!aaa:example.org");
+}
+
+TEST_CASE("MatrixUri::withRoomByAlias()", "[client][matrix-uri]")
+{
+ auto uri = MatrixUri::withRoomByAlias("#aaa:example.org");
+ REQUIRE(uri.isValid());
+ REQUIRE(uri.isRoom());
+ REQUIRE(uri.userOrRoom() == "#aaa:example.org");
+ REQUIRE(uri.id() == "#aaa:example.org");
+}
+
+TEST_CASE("MatrixUri::withRoomById()", "[client][matrix-uri]")
+{
+ auto uri = MatrixUri::withRoomById("!aaa:example.org");
+ REQUIRE(uri.isValid());
+ REQUIRE(uri.isRoom());
+ REQUIRE(uri.userOrRoom() == "!aaa:example.org");
+ REQUIRE(uri.id() == "!aaa:example.org");
+}
+
+TEST_CASE("MatrixUri::withUser()", "[client][matrix-uri]")
+{
+ auto uri = MatrixUri::withUser("@aaa:example.org");
+ REQUIRE(uri.isValid());
+ REQUIRE(uri.isUser());
+ REQUIRE(uri.userOrRoom() == "@aaa:example.org");
+ REQUIRE(uri.id() == "@aaa:example.org");
+}
+
+TEST_CASE("MatrixUri::withEvent()", "[client][matrix-uri]")
+{
+ SECTION("in room by id")
+ {
+ auto uri = MatrixUri::withRoomById("!aaa:example.org").withEvent("$bbb");
+ REQUIRE(uri.isValid());
+ REQUIRE(!uri.isRoom());
+ REQUIRE(uri.isEvent());
+ REQUIRE(uri.userOrRoom() == "!aaa:example.org");
+ REQUIRE(uri.id() == "$bbb");
+ }
+
+ SECTION("in room by alias")
+ {
+ auto uri = MatrixUri::withRoomByAlias("#aaa:example.org").withEvent("$bbb");
+ REQUIRE(uri.isValid());
+ REQUIRE(!uri.isRoom());
+ REQUIRE(uri.isEvent());
+ REQUIRE(uri.userOrRoom() == "#aaa:example.org");
+ REQUIRE(uri.id() == "$bbb");
+ }
+
+ SECTION("in user")
+ {
+ auto uri = MatrixUri::withUser("@aaa:example.org").withEvent("$bbb");
+ REQUIRE(!uri.isValid());
+ }
+}
+
+TEST_CASE("MatrixUri::toString()", "[client][matrix-uri]")
+{
+ SECTION("room by id")
+ {
+ auto uri = MatrixUri::withRoomById("!aaa:example.org");
+ REQUIRE(uri.toString() == "matrix:roomid/aaa:example.org");
+
+ SECTION("event")
+ {
+ REQUIRE(uri.withEvent("$bbb").toString() == "matrix:roomid/aaa:example.org/e/bbb");
+ }
+ }
+
+ SECTION("room by alias")
+ {
+ auto uri = MatrixUri::withRoomByAlias("#aaa:example.org");
+ REQUIRE(uri.toString() == "matrix:r/aaa:example.org");
+
+ SECTION("event")
+ {
+ REQUIRE(uri.withEvent("$bbb").toString() == "matrix:r/aaa:example.org/e/bbb");
+ }
+ }
+
+ SECTION("user")
+ {
+ auto uri = MatrixUri::withUser("@aaa:example.org");
+ REQUIRE(uri.toString() == "matrix:u/aaa:example.org");
+ }
+
+ SECTION("query params")
+ {
+ auto uri =
+ MatrixUri::withUser("@aaa:example.org")
+ .withQuery("via", "a.org")
+ .withQuery("via", "b.org")
+ .withAction("chat")
+ .withQuery("foo", "1")
+ .withQuery("foo", "2");
+ REQUIRE(uri.toString() == "matrix:u/aaa:example.org?action=chat&via=a.org&via=b.org&foo=2");
+ }
+
+ SECTION("fragments")
+ {
+ auto uri =
+ MatrixUri::withUser("@aaa:example.org")
+ .withFragment("ccc");
+ REQUIRE(uri.toString() == "matrix:u/aaa:example.org#ccc");
+ REQUIRE(uri.withFragment("").toString() == "matrix:u/aaa:example.org");
+ }
+
+ SECTION("all combined")
+ {
+ auto uri =
+ MatrixUri::withRoomByAlias("#aaa:example.org")
+ .withFragment("ccc")
+ .withQuery("via", "a.org")
+ .withEvent("bbb")
+ .withQuery("via", "b.org")
+ .withAction("chat")
+ .withQuery("foo", "1")
+ .withQuery("foo", "2");
+ REQUIRE(uri.toString() == "matrix:r/aaa:example.org/e/bbb?action=chat&via=a.org&via=b.org&foo=2#ccc");
+ }
+}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Oct 9, 9:55 AM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784663
Default Alt Text
(24 KB)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline
Log In to Comment