Page MenuHomePhorge

D226.1759497120.diff
No OneTemporary

Size
20 KB
Referenced Files
None
Subscribers
None

D226.1759497120.diff

diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt
--- a/src/client/CMakeLists.txt
+++ b/src/client/CMakeLists.txt
@@ -26,6 +26,8 @@
power-levels-desc.cpp
get-content-job-v1.cpp
+ alias.cpp
+ encode.cpp
)
add_library(kazvclient ${kazvclient_SRCS})
diff --git a/src/client/actions/membership.cpp b/src/client/actions/membership.cpp
--- a/src/client/actions/membership.cpp
+++ b/src/client/actions/membership.cpp
@@ -94,7 +94,11 @@
}
m.addTrigger(CreateRoomSuccessful{r.roomId()});
- return { std::move(m), lager::noop };
+ return { std::move(m), [=](auto &&) {
+ return EffectStatus(/* succ = */ true, json{
+ {"roomId", r.roomId()},
+ });
+ }};
}
ClientResult updateClient(ClientModel m, InviteToRoomAction a)
diff --git a/src/client/alias.hpp b/src/client/alias.hpp
new file mode 100644
--- /dev/null
+++ b/src/client/alias.hpp
@@ -0,0 +1,21 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2025 nannanko <nannanko@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <libkazv-config.hpp>
+
+#include "client-model.hpp"
+
+#include <basejob.hpp>
+#include <csapi/directory.hpp>
+
+#include <string>
+
+namespace Kazv
+{
+ BaseJob getRoomIdByAliasJob(ClientModel m, std::string roomAlias);
+ EffectStatus parseGetRoomIdByAliasResponse(GetRoomIdByAliasResponse r);
+}
diff --git a/src/client/alias.cpp b/src/client/alias.cpp
new file mode 100644
--- /dev/null
+++ b/src/client/alias.cpp
@@ -0,0 +1,42 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2025 nannanko <nannanko@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+
+#include "alias.hpp"
+#include "status-utils.hpp"
+#include "encode.hpp"
+
+#include <csapi/directory.hpp>
+#include <context.hpp>
+
+namespace Kazv
+{
+ BaseJob getRoomIdByAliasJob(ClientModel m, std::string roomAlias)
+ {
+ roomAlias = percentEncode(roomAlias);
+ return m.job<GetRoomIdByAliasJob>().make(roomAlias);
+ }
+
+ EffectStatus parseGetRoomIdByAliasResponse(GetRoomIdByAliasResponse r)
+ {
+ if (!r.success()) {
+ return failWithResponse(r).effectStatus();
+ }
+
+ auto servers = json::array({});
+ for (auto s : r.servers()) {
+ servers.push_back(s);
+ }
+
+ return EffectStatus{
+ /* succ = */ true,
+ json{
+ {"roomId", r.roomId().value()},
+ {"servers", servers}}
+ };
+ }
+}
diff --git a/src/client/client.hpp b/src/client/client.hpp
--- a/src/client/client.hpp
+++ b/src/client/client.hpp
@@ -584,6 +584,25 @@
*/
auto supportVersions() const -> lager::reader<immer::array<std::string>>;
+ /**
+ * Mark a room as a direct chat by send the m.direct account data.
+ *
+ * @param userId The user id that direct to.
+ * @param roomId The direct chat room id.
+ * @return A Promise that resolves when the account data
+ * has been set, or when there is an error.
+ */
+ PromiseT addDirectRoom(std::string userId, std::string roomId) const;
+
+ /**
+ * Get a GetRoomIdByAliasJob.
+ * Use Kazv::parseGetRoomIdByAliasResponse to parse its response.
+ *
+ * @param roomAlias The room alias.
+ * @return A GetRoomIdByAliasJob.
+ */
+ BaseJob getRoomIdByAliasJob(std::string roomAlias) const;
+
private:
void syncForever(std::optional<int> retryTime = std::nullopt) const;
diff --git a/src/client/client.cpp b/src/client/client.cpp
--- a/src/client/client.cpp
+++ b/src/client/client.cpp
@@ -7,10 +7,13 @@
#include <libkazv-config.hpp>
#include <filesystem>
+#include <algorithm>
#include <lager/constant.hpp>
#include "client.hpp"
+#include "client-model.hpp"
+#include "alias.hpp"
namespace Kazv
{
@@ -467,4 +470,34 @@
{
return clientCursor()[&ClientModel::versions];
}
+
+ auto Client::addDirectRoom(std::string userId, std::string roomId) const -> PromiseT
+ {
+ auto content = this->accountData().get()["m.direct"].content().get();
+
+ if (content.contains(userId)) {
+ auto& rooms = content[userId];
+ if (rooms.is_array()) {
+ if (std::find(rooms.begin(), rooms.end(), roomId) != rooms.end()) {
+ // The roomId is already in the m.direct, do nothing
+ return m_ctx.createResolvedPromise(true);
+ }
+ } else {
+ rooms = json::array({});
+ }
+ } else {
+ content.emplace(userId, json::array({}));
+ }
+
+ content[userId].push_back(roomId);
+ return Client::setAccountData(json{
+ {"type", "m.direct"},
+ {"content", std::move(content)}
+ });
+ }
+
+ auto Client::getRoomIdByAliasJob(std::string roomAlias) const -> BaseJob
+ {
+ return Kazv::getRoomIdByAliasJob(clientCursor().get(), roomAlias);
+ }
}
diff --git a/src/client/encode.hpp b/src/client/encode.hpp
new file mode 100644
--- /dev/null
+++ b/src/client/encode.hpp
@@ -0,0 +1,15 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2025 nannanko <nannanko@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <libkazv-config.hpp>
+
+#include <string>
+
+namespace Kazv
+{
+ std::string percentEncode(std::string url);
+}
diff --git a/src/client/encode.cpp b/src/client/encode.cpp
new file mode 100644
--- /dev/null
+++ b/src/client/encode.cpp
@@ -0,0 +1,40 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2025 nannanko <nannanko@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+
+#include "encode.hpp"
+
+#include <array>
+#include <algorithm>
+#include <sstream>
+#include <iostream>
+
+namespace Kazv
+{
+ // https://www.rfc-editor.org/rfc/rfc3986#section-2.2
+ static const std::array<char, 18> reservedChars{
+ '!', '#', '$', '&', '\'', '(', ')', '*', '+',
+ ',', '/', ':', ';', '=', '?', '@', '[', ']'};
+ static auto isReservedChar = [](char c) -> bool {
+ return std::find(reservedChars.begin(), reservedChars.end(), c)
+ != reservedChars.end();
+ };
+
+ std::string percentEncode(std::string url)
+ {
+ std::ostringstream result;
+ result << std::hex;
+ for (char c : url) {
+ if (isReservedChar(c)) {
+ result << '%' << static_cast<unsigned int>(c);
+ } else {
+ result << c;
+ }
+ }
+ return result.str();
+ }
+}
diff --git a/src/client/status-utils.hpp b/src/client/status-utils.hpp
--- a/src/client/status-utils.hpp
+++ b/src/client/status-utils.hpp
@@ -30,6 +30,8 @@
{
return st;
};
+
+ EffectStatus effectStatus() const;
};
}
diff --git a/src/client/status-utils.cpp b/src/client/status-utils.cpp
--- a/src/client/status-utils.cpp
+++ b/src/client/status-utils.cpp
@@ -9,6 +9,13 @@
namespace Kazv
{
+ namespace detail {
+ EffectStatus ReturnEffectStatusT::effectStatus() const
+ {
+ return st;
+ }
+ }
+
detail::ReturnEffectStatusT failWithResponse(const BaseJob::Response &r)
{
auto code = r.errorCode();
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -87,6 +87,8 @@
client/logout-test.cpp
client/room/pinned-events-test.cpp
client/get-versions-test.cpp
+ client/alias-test.cpp
+ client/encode-test.cpp
EXTRA_LINK_LIBRARIES kazvclient kazveventemitter kazvjob client-test-lib kazvtestfixtures
EXTRA_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/client
)
diff --git a/src/tests/client/account-data-test.cpp b/src/tests/client/account-data-test.cpp
--- a/src/tests/client/account-data-test.cpp
+++ b/src/tests/client/account-data-test.cpp
@@ -394,3 +394,244 @@
auto a = dispatcher.template of<SetAccountDataAction>()[0];
REQUIRE(a.accountDataEvent == accountDataEvent);
}
+
+TEST_CASE("Client::addDirectRoom()", "[client][account-data]")
+{
+ boost::asio::io_context io;
+ SingleTypePromiseInterface<EffectStatus> ph{AsioPromiseHandler{io.get_executor()}};
+
+ ClientModel m = makeClient({});
+ auto jh = Kazv::CprJobHandler{io.get_executor()};
+ auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
+
+ auto sdk = Kazv::makeSdk(
+ SdkModel{m},
+ jh,
+ ee,
+ Kazv::AsioPromiseHandler{io.get_executor()},
+ zug::identity
+ );
+
+ auto ctx = sdk.context();
+ auto dispatcher = getMockDispatcher(
+ ph,
+ ctx,
+ returnEmpty<SetAccountDataAction>()
+ );
+ auto mockContext = getMockContext(ph, dispatcher);
+
+ auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
+ client.addDirectRoom("@mew:example.org", "!somewhere:example.org")
+ .then([&io](auto) {
+ io.stop();
+ });
+
+ io.run();
+
+ REQUIRE(dispatcher.template calledTimes<SetAccountDataAction>() == 1);
+ auto a = dispatcher.template of<SetAccountDataAction>()[0];
+ Event mDirectEvent = R"({
+ "type": "m.direct",
+ "content": {
+ "@mew:example.org": ["!somewhere:example.org"]
+ }
+ })"_json;
+ REQUIRE(a.accountDataEvent == mDirectEvent);
+}
+
+TEST_CASE("Client::addDirectRoom(), when roomId is already in m.direct", "[client][account-data]")
+{
+ boost::asio::io_context io;
+ SingleTypePromiseInterface<EffectStatus> ph{AsioPromiseHandler{io.get_executor()}};
+
+ ClientModel m = makeClient({});
+ Event mDirectEvent = R"({
+ "type": "m.direct",
+ "content": {
+ "@mew:example.org": ["!somewhere:example.org"]
+ }
+ })"_json;
+ m.accountData = m.accountData.set("m.direct", mDirectEvent);
+ auto jh = Kazv::CprJobHandler{io.get_executor()};
+ auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
+
+ auto sdk = Kazv::makeSdk(
+ SdkModel{m},
+ jh,
+ ee,
+ Kazv::AsioPromiseHandler{io.get_executor()},
+ zug::identity
+ );
+
+ auto ctx = sdk.context();
+ auto dispatcher = getMockDispatcher(
+ ph,
+ ctx,
+ returnEmpty<SetAccountDataAction>()
+ );
+ auto mockContext = getMockContext(ph, dispatcher);
+
+ auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
+ client.addDirectRoom("@mew:example.org", "!somewhere:example.org")
+ .then([&io](auto) {
+ io.stop();
+ });
+
+ io.run();
+
+ REQUIRE(dispatcher.template calledTimes<SetAccountDataAction>() == 0);
+}
+
+TEST_CASE("Client::addDirectRoom(), when m.direct content is ill-format", "[client][account-data]")
+{
+ boost::asio::io_context io;
+ SingleTypePromiseInterface<EffectStatus> ph{AsioPromiseHandler{io.get_executor()}};
+
+ ClientModel m = makeClient({});
+ Event mDirectEvent = R"({
+ "type": "m.direct",
+ "content": {
+ "@mew:example.org": "!somewrongwhere:example.org"
+ }
+ })"_json;
+ m.accountData = m.accountData.set("m.direct", mDirectEvent);
+ auto jh = Kazv::CprJobHandler{io.get_executor()};
+ auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
+
+ auto sdk = Kazv::makeSdk(
+ SdkModel{m},
+ jh,
+ ee,
+ Kazv::AsioPromiseHandler{io.get_executor()},
+ zug::identity
+ );
+
+ auto ctx = sdk.context();
+ auto dispatcher = getMockDispatcher(
+ ph,
+ ctx,
+ returnEmpty<SetAccountDataAction>()
+ );
+ auto mockContext = getMockContext(ph, dispatcher);
+
+ auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
+ client.addDirectRoom("@mew:example.org", "!somewhere:example.org")
+ .then([&io](auto) {
+ io.stop();
+ });
+
+ io.run();
+
+ REQUIRE(dispatcher.template calledTimes<SetAccountDataAction>() == 1);
+ auto a = dispatcher.template of<SetAccountDataAction>()[0];
+ Event mDirectEventNew = R"({
+ "type": "m.direct",
+ "content": {
+ "@mew:example.org": ["!somewhere:example.org"]
+ }
+ })"_json;
+ REQUIRE(a.accountDataEvent == mDirectEventNew);
+}
+
+TEST_CASE("Client::addDirectRoom(), when m.direct content contains other user's rooms", "[client][account-data]")
+{
+ boost::asio::io_context io;
+ SingleTypePromiseInterface<EffectStatus> ph{AsioPromiseHandler{io.get_executor()}};
+
+ ClientModel m = makeClient({});
+ Event mDirectEvent = R"({
+ "type": "m.direct",
+ "content": {
+ "@mew2:example.org": ["!somewhere2:example.org", "!somewhere3:example.org"]
+ }
+ })"_json;
+ m.accountData = m.accountData.set("m.direct", mDirectEvent);
+ auto jh = Kazv::CprJobHandler{io.get_executor()};
+ auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
+
+ auto sdk = Kazv::makeSdk(
+ SdkModel{m},
+ jh,
+ ee,
+ Kazv::AsioPromiseHandler{io.get_executor()},
+ zug::identity
+ );
+
+ auto ctx = sdk.context();
+ auto dispatcher = getMockDispatcher(
+ ph,
+ ctx,
+ returnEmpty<SetAccountDataAction>()
+ );
+ auto mockContext = getMockContext(ph, dispatcher);
+
+ auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
+ client.addDirectRoom("@mew:example.org", "!somewhere:example.org")
+ .then([&io](auto) {
+ io.stop();
+ });
+
+ io.run();
+
+ REQUIRE(dispatcher.template calledTimes<SetAccountDataAction>() == 1);
+ auto a = dispatcher.template of<SetAccountDataAction>()[0];
+ Event mDirectEventNew = R"({
+ "type": "m.direct",
+ "content": {
+ "@mew:example.org": ["!somewhere:example.org"],
+ "@mew2:example.org": ["!somewhere2:example.org", "!somewhere3:example.org"]
+ }
+ })"_json;
+ REQUIRE(a.accountDataEvent == mDirectEventNew);
+}
+
+TEST_CASE("Client::addDirectRoom(), when m.direct content contains the user's other rooms", "[client][account-data]")
+{
+ boost::asio::io_context io;
+ SingleTypePromiseInterface<EffectStatus> ph{AsioPromiseHandler{io.get_executor()}};
+
+ ClientModel m = makeClient({});
+ Event mDirectEvent = R"({
+ "type": "m.direct",
+ "content": {
+ "@mew:example.org": ["!somewhere4:example.org"]
+ }
+ })"_json;
+ m.accountData = m.accountData.set("m.direct", mDirectEvent);
+ auto jh = Kazv::CprJobHandler{io.get_executor()};
+ auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
+
+ auto sdk = Kazv::makeSdk(
+ SdkModel{m},
+ jh,
+ ee,
+ Kazv::AsioPromiseHandler{io.get_executor()},
+ zug::identity
+ );
+
+ auto ctx = sdk.context();
+ auto dispatcher = getMockDispatcher(
+ ph,
+ ctx,
+ returnEmpty<SetAccountDataAction>()
+ );
+ auto mockContext = getMockContext(ph, dispatcher);
+
+ auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
+ client.addDirectRoom("@mew:example.org", "!somewhere:example.org")
+ .then([&io](auto) {
+ io.stop();
+ });
+
+ io.run();
+
+ REQUIRE(dispatcher.template calledTimes<SetAccountDataAction>() == 1);
+ auto a = dispatcher.template of<SetAccountDataAction>()[0];
+ Event mDirectEventNew = R"({
+ "type": "m.direct",
+ "content": {
+ "@mew:example.org": ["!somewhere4:example.org", "!somewhere:example.org"]
+ }
+ })"_json;
+ REQUIRE(a.accountDataEvent == mDirectEventNew);
+}
diff --git a/src/tests/client/alias-test.cpp b/src/tests/client/alias-test.cpp
new file mode 100644
--- /dev/null
+++ b/src/tests/client/alias-test.cpp
@@ -0,0 +1,78 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2025 nannanko <nannanko@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+
+#include <factory.hpp>
+#include <alias.hpp>
+#include <encode.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+#include <nlohmann/json.hpp>
+
+#include <string>
+
+using namespace std::string_literals;
+using namespace Kazv::Factory;
+using namespace Kazv;
+using namespace nlohmann;
+
+TEST_CASE("Get room id by room alias", "[client][room-alias]")
+{
+ const auto roomAlias = "#room:example.org"s;
+ const auto client = makeClient();
+ const auto job = getRoomIdByAliasJob(client, roomAlias);
+ REQUIRE(job.jobId() == "GetRoomIdByAlias");
+ REQUIRE(job.url().find(percentEncode(roomAlias))
+ != std::string::npos);
+}
+
+TEST_CASE("Parse GetRoomIdByAlias response", "[client][room-alias]")
+{
+ WHEN("Success response")
+ {
+ const auto roomId = "!room:example.org"s;
+ const auto servers = json::array({"server1.org", "server2.org"});
+ const auto succResponse = makeResponse("GetRoomIdByAlias",
+ withResponseJsonBody(json{
+ {"room_id", roomId},
+ {"servers", servers}}));
+ const auto stat = parseGetRoomIdByAliasResponse(succResponse);
+ REQUIRE(stat.success());
+ REQUIRE(stat.dataStr("roomId") == roomId);
+ REQUIRE(stat.dataJson("servers") == servers);
+ }
+
+ WHEN("Failed 400 response")
+ {
+ const auto errorMsg = "Room alias invalid"s;
+ const auto errorCode = "M_INVALID_PARAM"s;
+ const auto failResponse = makeResponse("GetRoomIdByAlias",
+ withResponseJsonBody(json{
+ {"errorcode", errorCode},
+ {"error", errorMsg}})
+ | withResponseStatusCode(400));
+ const auto stat = parseGetRoomIdByAliasResponse(failResponse);
+ REQUIRE(!stat.success());
+ REQUIRE(stat.dataStr("error") == errorMsg);
+ REQUIRE(stat.dataStr("errorCode") == "400");
+ }
+
+ WHEN("Failed 404 response")
+ {
+ const auto errorMsg = "Room alias #room:example.org not found."s;
+ const auto errorCode = "M_NOT_FOUND"s;
+ const auto failResponse = makeResponse("GetRoomIdByAlias",
+ withResponseJsonBody(json{
+ {"errorcode", errorCode},
+ {"error", errorMsg}})
+ | withResponseStatusCode(404));
+ const auto stat = parseGetRoomIdByAliasResponse(failResponse);
+ REQUIRE(!stat.success());
+ REQUIRE(stat.dataStr("error") == errorMsg);
+ REQUIRE(stat.dataStr("errorCode") == "404");
+ }
+}
diff --git a/src/tests/client/create-room-test.cpp b/src/tests/client/create-room-test.cpp
--- a/src/tests/client/create-room-test.cpp
+++ b/src/tests/client/create-room-test.cpp
@@ -175,10 +175,12 @@
WHEN("Success response")
{
- auto succResponse = makeResponse("CreateRoom", withResponseJsonBody(json{{"room_id", "!some-room:example.com"}}));
+ const auto roomId = "!some-room:example.com";
+ auto succResponse = makeResponse("CreateRoom", withResponseJsonBody(json{{"room_id", roomId}}));
store.dispatch(ProcessResponseAction{succResponse})
- .then([&](auto stat) {
+ .then([&, roomId](auto stat) {
REQUIRE(stat.success());
+ REQUIRE(stat.dataStr("roomId") == roomId);
});
}
WHEN("Failed response")
diff --git a/src/tests/client/encode-test.cpp b/src/tests/client/encode-test.cpp
new file mode 100644
--- /dev/null
+++ b/src/tests/client/encode-test.cpp
@@ -0,0 +1,30 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2025 nannanko <nannanko@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+
+#include <encode.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <vector>
+#include <string>
+
+using namespace std;
+using namespace std::string_literals;
+using namespace Kazv;
+
+TEST_CASE("String percent-encoding", "[client][percent-encoding]")
+{
+ const std::vector<std::pair<string, string>> strs = {
+ {"!#$&'()*+,/:;=?@[]"s, "%21%23%24%26%27%28%29%2a%2b%2c%2f%3a%3b%3d%3f%40%5b%5d"s},
+ {"#room:example.org"s, "%23room%3aexample.org"s},
+ {"Unreserved_characters_string"s, "Unreserved_characters_string"s}
+ };
+ for (auto s : strs) {
+ REQUIRE(percentEncode(s.first) == s.second);
+ }
+}

File Metadata

Mime Type
text/plain
Expires
Fri, Oct 3, 6:12 AM (14 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
479786
Default Alt Text
D226.1759497120.diff (20 KB)

Event Timeline