Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F7756070
D226.1758368648.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
20 KB
Referenced Files
None
Subscribers
None
D226.1758368648.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D226: Add necessary features to Implement handle matrix uri in kazv
Attached
Detach File
Event Timeline
Log In to Comment