Page MenuHomePhorge

D226.1758368648.diff
No OneTemporary

Size
20 KB
Referenced Files
None
Subscribers
None

D226.1758368648.diff

diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt
--- a/src/client/CMakeLists.txt
+++ b/src/client/CMakeLists.txt
@@ -15,6 +15,7 @@
actions/content.cpp
actions/encryption.cpp
actions/profile.cpp
+ actions/alias.cpp
device-list-tracker.cpp
encrypted-file.cpp
diff --git a/src/client/actions/alias.hpp b/src/client/actions/alias.hpp
new file mode 100644
--- /dev/null
+++ b/src/client/actions/alias.hpp
@@ -0,0 +1,18 @@
+/*
+ * 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 <csapi/directory.hpp>
+
+#include "client-model.hpp"
+
+namespace Kazv
+{
+ ClientResult updateClient(ClientModel m, GetRoomIdByAliasAction a);
+ ClientResult processResponse(ClientModel m, GetRoomIdByAliasResponse r);
+}
diff --git a/src/client/actions/alias.cpp b/src/client/actions/alias.cpp
new file mode 100644
--- /dev/null
+++ b/src/client/actions/alias.cpp
@@ -0,0 +1,55 @@
+/*
+ * 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 <jsonwrap.hpp>
+
+#include <regex>
+
+#include "status-utils.hpp"
+#include "alias.hpp"
+
+using namespace std::string_literals;
+
+namespace Kazv
+{
+ ClientResult updateClient(ClientModel m, GetRoomIdByAliasAction a)
+ {
+ auto isValidAlias = [](std::string roomAlias) {
+ std::regex r{"^#.+:.+$"};
+ return std::regex_match(roomAlias, r);
+ };
+ if (!isValidAlias(a.roomAlias)) {
+ return {m, failEffect("MOE.KAZV.MXC_INVALID_PARAM", "Room alias invalid")};
+ }
+ // Replace "#" to "%23"
+ a.roomAlias.erase(0, 1);
+ a.roomAlias = "%23"s + a.roomAlias;
+ auto job = m.job<GetRoomIdByAliasJob>().make(a.roomAlias);
+ m.addJob(std::move(job));
+ return { m, lager::noop };
+ }
+
+ ClientResult processResponse(ClientModel m, GetRoomIdByAliasResponse r)
+ {
+ if (!r.success()) {
+ return { std::move(m), failWithResponse(r) };
+ }
+
+ auto servers = json::array();
+ for (auto s : r.servers()) {
+ servers.push_back(s);
+ }
+
+ return { std::move(m), [r, servers](const auto &ctx) {
+ return EffectStatus(/* succ = */ true, json{
+ {"roomId", r.roomId().value()},
+ {"servers", servers}
+ });
+ }};
+ }
+}
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/client-model.hpp b/src/client/client-model.hpp
--- a/src/client/client-model.hpp
+++ b/src/client/client-model.hpp
@@ -380,6 +380,11 @@
Event accountDataEvent;
};
+ struct GetRoomIdByAliasAction
+ {
+ std::string roomAlias;
+ };
+
struct SetTypingAction
{
std::string roomId;
diff --git a/src/client/client-model.cpp b/src/client/client-model.cpp
--- a/src/client/client-model.cpp
+++ b/src/client/client-model.cpp
@@ -34,6 +34,7 @@
#include "actions/content.hpp"
#include "actions/encryption.hpp"
#include "actions/profile.hpp"
+#include "actions/alias.hpp"
namespace Kazv
{
@@ -109,6 +110,8 @@
RESPONSE_FOR(GetUserProfile);
RESPONSE_FOR(SetAvatarUrl);
RESPONSE_FOR(SetDisplayName);
+ // Alias
+ RESPONSE_FOR(GetRoomIdByAlias);
m.addTrigger(UnrecognizedResponse{std::move(r)});
return { std::move(m), lager::noop };
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,26 @@
*/
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 room id by room alias.
+ *
+ * @param roomAlias The room alias.
+ * @return A Promise that resolves when the room id
+ * has been returned by server, or when there is an error.
+ *
+ */
+ PromiseT getRoomIdByAlias(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,12 @@
#include <libkazv-config.hpp>
#include <filesystem>
+#include <algorithm>
#include <lager/constant.hpp>
#include "client.hpp"
+#include "client-model.hpp"
namespace Kazv
{
@@ -467,4 +469,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::getRoomIdByAlias(std::string roomAlias) const -> PromiseT
+ {
+ return m_ctx.dispatch(GetRoomIdByAliasAction{roomAlias});
+ }
}
diff --git a/src/client/clientfwd.hpp b/src/client/clientfwd.hpp
--- a/src/client/clientfwd.hpp
+++ b/src/client/clientfwd.hpp
@@ -50,6 +50,7 @@
struct BanAction;
struct UnbanAction;
struct SetAccountDataPerRoomAction;
+ struct GetRoomIdByAliasAction;
struct ProcessResponseAction;
struct SetTypingAction;
@@ -106,6 +107,7 @@
GetStateEventAction,
InviteToRoomAction,
JoinRoomByIdAction,
+ GetRoomIdByAliasAction,
JoinRoomAction,
LeaveRoomAction,
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,7 @@
client/logout-test.cpp
client/room/pinned-events-test.cpp
client/get-versions-test.cpp
+ client/alias-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,137 @@
+/*
+ * 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 <catch2/catch_test_macros.hpp>
+#include <boost/asio/io_context.hpp>
+#include <lager/event_loop/boost_asio.hpp>
+#include <zug/util.hpp>
+#include <immer/array.hpp>
+
+#include <asio-promise-handler.hpp>
+#include <promise-interface.hpp>
+#include <cprjobhandler.hpp>
+#include <lagerstoreeventemitter.hpp>
+
+#include "factory.hpp"
+#include "client-model.hpp"
+#include "client-test-util.hpp"
+#include "client/actions/alias.hpp"
+#include "client/action-mock-utils.hpp"
+
+using namespace Kazv;
+using namespace Kazv::Factory;
+
+TEST_CASE("Get room id by room alias", "[client][room-alias]")
+{
+ auto client = makeClient();
+ auto [next, _] = updateClient(client,
+ GetRoomIdByAliasAction{"#room:example.org"});
+ assert1Job(next);
+ for1stJob(next, [](const BaseJob &job) {
+ REQUIRE(job.jobId() == "GetRoomIdByAlias");
+ std::cout << job.url();
+ REQUIRE(job.url().find("%23room:example.org") != std::string::npos);
+ });
+}
+
+TEST_CASE("Process GetRoomIdByAlias response", "[client][room-alias]")
+{
+ boost::asio::io_context io;
+ AsioPromiseHandler ph{io.get_executor()};
+ auto initialModel = makeClient();
+
+ auto store = createTestClientStoreFrom(initialModel, ph);
+ auto client = Client(
+ store.reader().map([](auto c) {return SdkModel{c}; }),
+ store, std::nullopt);
+
+ WHEN("Success response")
+ {
+ auto succResponse = makeResponse("GetRoomIdByAlias",
+ withResponseJsonBody(R"({
+ "room_id": "!room:example.org",
+ "servers": [
+ "server1.org", "server2.org"
+ ]
+ })"_json));
+ store.dispatch(ProcessResponseAction{succResponse})
+ .then([client] (auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(stat.dataStr("roomId") == "!room:example.org");
+ REQUIRE(stat.dataJson("servers") ==
+ json::array({"server1.org", "server2.org"}));
+ });
+ }
+
+ WHEN("Failed 400 response")
+ {
+ auto failResponse = makeResponse("GetRoomIdByAlias",
+ withResponseJsonBody(R"({
+ "errcode": "M_INVALID_PARAM",
+ "error": "Room alias invalid"
+ })"_json) | withResponseStatusCode(400));
+ store.dispatch(ProcessResponseAction{failResponse})
+ .then([] (auto stat) {
+ REQUIRE(!stat.success());
+ REQUIRE(stat.dataStr("error") == "Room alias invalid");
+ REQUIRE(stat.dataStr("errorCode") == "M_INVALID_PARAM");
+ });
+ }
+
+ WHEN("Failed 404 response")
+ {
+ auto failResponse = makeResponse("GetRoomIdByAlias",
+ withResponseJsonBody(R"({
+ "errcode": "M_NOT_FOUND",
+ "error": "Room alias #room:example.org not found."
+ })"_json) | withResponseStatusCode(404));
+ store.dispatch(ProcessResponseAction{failResponse})
+ .then([](auto stat) {
+ REQUIRE(!stat.success());
+ REQUIRE(stat.dataStr("error") ==
+ "Room alias #room:example.org not found.");
+ REQUIRE(stat.dataStr("errorCode") == "M_NOT_FOUND");
+ });
+ }
+
+ io.run();
+}
+
+TEST_CASE("Client::getRoomIdByAlias()", "[client][room-alias]")
+{
+ ClientModel m = makeClient();
+
+ boost::asio::io_context io;
+ SingleTypePromiseInterface<EffectStatus> ph{
+ AsioPromiseHandler{io.get_executor()}};
+
+ 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<GetRoomIdByAliasAction>());
+
+ auto client = Client(Client::InEventLoopTag{},
+ getMockContext(ph, dispatcher),
+ sdk.context());
+
+ client.getRoomIdByAlias("#room:example.org")
+ .then([&io](auto) {
+ io.stop();
+ });
+
+ io.run();
+
+ REQUIRE(dispatcher.template calledTimes<GetRoomIdByAliasAction>() == 1);
+ auto action = dispatcher.template of<GetRoomIdByAliasAction>()[0];
+ REQUIRE(action.roomAlias == "#room:example.org");
+}
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")

File Metadata

Mime Type
text/plain
Expires
Sat, Sep 20, 4:44 AM (15 h, 26 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
464476
Default Alt Text
D226.1758368648.diff (20 KB)

Event Timeline