Page MenuHomePhorge

No OneTemporary

Size
24 KB
Referenced Files
None
Subscribers
None
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

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)

Event Timeline