Page MenuHomePhorge

D226.1761632733.diff
No OneTemporary

Size
18 KB
Referenced Files
None
Subscribers
None

D226.1761632733.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,7 @@
power-levels-desc.cpp
get-content-job-v1.cpp
+ alias.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,46 @@
+/*
+ * 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 <csapi/directory.hpp>
+#include <context.hpp>
+
+#include <boost/url/encode.hpp>
+#include <boost/url/rfc/unreserved_chars.hpp>
+
+using namespace boost::urls;
+
+namespace Kazv
+{
+ BaseJob getRoomIdByAliasJob(ClientModel m, std::string roomAlias)
+ {
+ roomAlias = encode(roomAlias, unreserved_chars);
+ 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,28 @@
*/
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;
+
+ 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/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,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,83 @@
+/*
+ * 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 <catch2/catch_test_macros.hpp>
+#include <boost/url/encode.hpp>
+#include <boost/url/rfc/unreserved_chars.hpp>
+#include <nlohmann/json.hpp>
+
+#include <string>
+#include <iostream>
+
+using namespace std::string_literals;
+using namespace Kazv::Factory;
+using namespace Kazv;
+using namespace boost::urls;
+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(encode(roomAlias, unreserved_chars))
+ != 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));
+ std::cout << "before parse\n";
+ const auto stat = parseGetRoomIdByAliasResponse(failResponse);
+ std::cout << "after parse\n";
+ 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")

File Metadata

Mime Type
text/plain
Expires
Mon, Oct 27, 11:25 PM (19 h, 30 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
566923
Default Alt Text
D226.1761632733.diff (18 KB)

Event Timeline