Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85710164
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
33 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/client/room/room.cpp b/src/client/room/room.cpp
index 151eb61..f81993d 100644
--- a/src/client/room/room.cpp
+++ b/src/client/room/room.cpp
@@ -1,510 +1,514 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <immer/algorithm.hpp>
#include <debug.hpp>
#include "room.hpp"
namespace Kazv
{
Room::Room(lager::reader<SdkModel> sdk,
lager::reader<std::string> roomId,
Context<ClientAction> ctx,
DepsT deps)
: m_sdk(sdk)
, m_roomId(roomId)
, m_ctx(ctx)
, m_deps(deps)
{
}
Room::Room(lager::reader<SdkModel> sdk,
lager::reader<std::string> roomId,
Context<ClientAction> ctx)
: m_sdk(sdk)
, m_roomId(roomId)
, m_ctx(ctx)
, m_deps(std::nullopt)
{
}
Room::Room(InEventLoopTag, std::string roomId, ContextT ctx, DepsT deps)
: m_sdk(std::nullopt)
, m_roomId(roomId)
, m_ctx(ctx)
, m_deps(deps)
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, KAZV_ON_EVENT_LOOP_VAR(true)
#endif
{
}
Room Room::toEventLoop() const
{
assert(m_deps.has_value());
return Room(InEventLoopTag{}, currentRoomId(), m_ctx, m_deps.value());
}
const lager::reader<SdkModel> &Room::sdkCursor() const
{
if (m_sdk.has_value()) { return m_sdk.value(); }
assert(m_deps.has_value());
return *lager::get<SdkModelCursorKey>(m_deps.value());
}
lager::reader<RoomModel> Room::roomCursor() const
{
return lager::match(m_roomId)(
[&](const lager::reader<std::string> &roomId) -> lager::reader<RoomModel> {
return lager::with(sdkCursor().map(&SdkModel::c)[&ClientModel::roomList], roomId)
.map([](auto rooms, auto id) {
return rooms[id];
}).make();
},
[&](const std::string &roomId) -> lager::reader<RoomModel> {
return sdkCursor().map(&SdkModel::c)[&ClientModel::roomList].map([roomId](auto rooms) { return rooms[roomId]; }).make();
});
}
std::string Room::currentRoomId() const
{
return lager::match(m_roomId)(
[&](const lager::reader<std::string> &roomId) {
return roomId.get();
},
[&](const std::string &roomId) {
return roomId;
});
}
auto Room::heroMemberEvents() const -> lager::reader<immer::flex_vector<Event>>
{
using namespace lager::lenses;
auto idsCursor = heroIds();
lager::reader<immer::map<KeyOfState, Event>> statesCursor = stateEvents();
return lager::with(idsCursor, statesCursor)
.xform(zug::map([](const auto &ids, const auto &states) {
return intoImmer(
immer::flex_vector<Event>{},
zug::map([states](const auto &id) {
return states[{"m.room.member", id}];
}),
ids);
}));
}
auto Room::heroDisplayNames() const
-> lager::reader<immer::flex_vector<std::string>>
{
return heroMemberEvents()
.xform(zug::map([](const auto &events) {
return intoImmer(
immer::flex_vector<std::string>{},
zug::map([](const auto &event) {
auto content = event.content();
return content.get().contains("displayname")
? content.get()["displayname"].template get<std::string>()
: std::string();
}),
events);
}));
}
auto Room::nameOpt() const -> lager::reader<std::optional<std::string>>
{
using namespace lager::lenses;
return stateEvents()
[KeyOfState{"m.room.name", ""}]
[or_default]
.xform(eventContent)
.xform(zug::map([](const JsonWrap &content) {
return content.get().contains("name")
? std::optional<std::string>(content.get()["name"])
: std::nullopt;
}));
}
lager::reader<bool> Room::encrypted() const
{
return roomCursor()[&RoomModel::encrypted];
}
auto Room::heroIds() const
-> lager::reader<immer::flex_vector<std::string>>
{
return roomCursor().map([](const auto &room) {
return room.heroIds;
});
}
auto Room::setLocalDraft(std::string localDraft) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(UpdateRoomAction{+roomId(), SetLocalDraftAction{localDraft}});
}
auto Room::sendMessage(Event msg) const
-> PromiseT
{
using namespace CursorOp;
auto hasCrypto = ~sdkCursor().map([](const auto &sdk) -> bool {
return sdk.c().crypto.has_value();
});
auto roomEncrypted = ~roomCursor()[&RoomModel::encrypted];
auto noFullMembers = ~roomCursor()[&RoomModel::membersFullyLoaded]
.map([](auto b) { return !b; });
auto rid = +roomId();
// Don't use m_ctx directly in the callbacks
// as `this` may have been destroyed when
// the callbacks are called.
auto ctx = m_ctx;
auto promise = ctx.createResolvedPromise(true);
// If we do not need encryption just send it as-is
if (! +allCursors(hasCrypto, roomEncrypted)) {
return promise
.then([ctx, rid, msg](auto succ) {
if (! succ) {
return ctx.createResolvedPromise(false);
}
return ctx.dispatch(SendMessageAction{rid, msg});
});
}
if (! m_deps) {
return ctx.createResolvedPromise({false, json{{"error", "missing-deps"}}});
}
auto deps = m_deps.value();
promise = promise.then([ctx, rid, msg](auto) {
return ctx.dispatch(SaveLocalEchoAction{rid, msg});
});
auto saveLocalEchoPromise = promise;
// If the room member list is not complete, load it fully first.
if (+allCursors(hasCrypto, roomEncrypted, noFullMembers)) {
kzo.client.dbg() << "The members of " << rid
<< " are not fully loaded." << std::endl;
promise = promise
.then([ctx, rid](auto) { // SaveLocalEchoAction can't fail
return ctx.dispatch(GetRoomStatesAction{rid});
})
.then([ctx, rid](auto succ) {
if (! succ) {
kzo.client.warn() << "Loading members of " << rid
<< " failed." << std::endl;
return ctx.createResolvedPromise(false);
} else {
// XXX remove the hard-coded initialSync parameter
return ctx.dispatch(QueryKeysAction{true})
.then([](auto succ) {
if (! succ) {
kzo.client.warn() << "Query keys failed" << std::endl;
}
return succ;
});
}
});
}
auto encryptEvent = [ctx, rid, msg, deps](auto &&status) {
if (! status) { return ctx.createResolvedPromise(status); }
kzo.client.dbg() << "encrypting megolm" << std::endl;
auto &rg = lager::get<RandomInterface &>(deps);
return ctx.dispatch(EncryptMegOlmEventAction{
rid,
msg,
currentTimeMs(),
rg.generateRange<RandomData>(EncryptMegOlmEventAction::maxRandomSize())
});
};
auto saveEncryptedLocalEcho = [saveLocalEchoPromise, msg, rid, ctx](auto &&status) mutable {
return saveLocalEchoPromise
.then([status, msg, rid, ctx](auto &&st) {
auto txnId = st.dataStr("txnId");
kzo.client.dbg() << "saving encrypted local echo with txn id " << txnId << std::endl;
auto encrypted = status.dataJson("encrypted");
auto decrypted = msg.originalJson();
auto event = Event(encrypted).setDecryptedJson(decrypted, Event::Decrypted);
return ctx.dispatch(SaveLocalEchoAction{rid, msg, txnId});
})
.then([status](auto &&) {
return status;
});
};
auto maybeSendKeys = [ctx, rid, r=toEventLoop(), deps](auto status) {
if (! status) { return ctx.createResolvedPromise(status); }
auto encryptedEvent = status.dataJson("encrypted");
auto content = encryptedEvent.at("content");
auto ret = ctx.createResolvedPromise({});
if (status.data().get().contains("key")) {
kzo.client.dbg() << "megolm session rotated, sending session key" << std::endl;
auto key = status.dataStr("key");
ret = ret
.then([rid, r, key, ctx, deps, sessionId=content.at("session_id")](auto &&) {
auto members = (+r.roomCursor()).joinedMemberIds();
auto client = (+r.sdkCursor()).c();
using DeviceMapT = immer::map<std::string, immer::flex_vector<std::string>>;
auto devicesToSend = accumulate(
members, DeviceMapT{}, [client](auto map, auto uid) {
return std::move(map)
.set(uid, client.devicesToSendKeys(uid));
});
auto &rg = lager::get<RandomInterface &>(deps);
return ctx.dispatch(ClaimKeysAction{
rid, sessionId, key, devicesToSend,
rg.generateRange<RandomData>(ClaimKeysAction::randomSize(devicesToSend))
})
.then([ctx, deps, devicesToSend](auto status) {
if (! status) { return ctx.createResolvedPromise({}); }
kzo.client.dbg() << "olm-encrypting key event" << std::endl;
auto keyEv = status.dataJson("keyEvent");
auto &rg = lager::get<RandomInterface &>(deps);
return ctx.dispatch(EncryptOlmEventAction{
devicesToSend, keyEv,
rg.generateRange<RandomData>(EncryptOlmEventAction::randomSize(devicesToSend))
});
})
.then([ctx, devicesToSend](auto status) {
if (! status) { return ctx.createResolvedPromise({}); }
kzo.client.dbg() << "sending key event as to-device message" << std::endl;
auto event = Event(status.dataJson("encrypted"));
return ctx.dispatch(SendToDeviceMessageAction{event, devicesToSend});
});
});
}
return ret
.then([ctx, prevStatus=status](auto status) {
if (! status) { return status; }
return prevStatus;
});
};
auto sendEncryptedEvent = [ctx, rid, saveLocalEchoPromise](auto status) mutable {
if (! status) { return ctx.createResolvedPromise(status); }
return saveLocalEchoPromise.then([ctx, rid, status](auto st) {
kzo.client.dbg() << "sending encrypted message" << std::endl;
kzo.client.dbg() << "data: " << st.data().get().dump() << std::endl;
auto txnId = st.dataStr("txnId");
kzo.client.dbg() << "sending encrypted message with txn id " << txnId << std::endl;
auto ev = Event(status.dataJson("encrypted"));
return ctx.dispatch(SendMessageAction{rid, ev, txnId});
});
};
return promise
.then(encryptEvent)
.then(saveEncryptedLocalEcho)
.then(maybeSendKeys)
.then(sendEncryptedEvent);
}
auto Room::sendTextMessage(std::string text) const
-> PromiseT
{
json j{
{"type", "m.room.message"},
{"content", {
{"msgtype", "m.text"},
{"body", text}
}
}
};
Event e{j};
return sendMessage(e);
}
auto Room::resendMessage(std::string txnId) const
-> PromiseT
{
return m_ctx.createResolvedPromise({})
.then([that=toEventLoop(), txnId](const auto &) {
auto maybeLocalEcho = that.roomCursor().map([txnId](const auto &room) {
return room.getLocalEchoByTxnId(txnId);
}).make();
auto roomEncrypted = that.roomCursor()[&RoomModel::encrypted].make();
if (!maybeLocalEcho.get()) {
return that.m_ctx.createResolvedPromise({false, json{
{"errorCode", "MOE_KAZV_MXC_NO_SUCH_TXNID"},
{"error", "No such txn id"}
}});
}
auto localEcho = maybeLocalEcho.get().value();
auto event = localEcho.event;
+ auto rid = that.roomId().make().get();
if ((roomEncrypted.get() && event.encrypted()) || !roomEncrypted.get()) {
kzo.client.info() << "resending message with txn id " << localEcho.txnId << std::endl;
- return that.m_ctx.dispatch(SendMessageAction{that.roomId().make().get(), event, localEcho.txnId});
+ return that.m_ctx.dispatch(SendMessageAction{rid, event, localEcho.txnId});
} else {
- kzo.client.warn() << "room encrypted but event isn't..." << std::endl;
- return that.m_ctx.createResolvedPromise(false);
+ kzo.client.info() << "room encrypted but event isn't, just resend everything" << std::endl;
+ return that.m_ctx.dispatch(RoomListAction{UpdateRoomAction{rid, RemoveLocalEchoAction{localEcho.txnId}}})
+ .then([localEcho, that](auto) { // Can't fail
+ return that.sendMessage(localEcho.event);
+ });
}
});
}
auto Room::refreshRoomState() const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(GetRoomStatesAction{+roomId()});
}
auto Room::getStateEvent(std::string type, std::string stateKey) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(GetStateEventAction{+roomId(), type, stateKey});
}
auto Room::sendStateEvent(Event state) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(SendStateEventAction{+roomId(), state});
}
auto Room::setName(std::string name) const
-> PromiseT
{
json j{
{"type", "m.room.name"},
{"content", {
{"name", name}
}
}
};
Event e{j};
return sendStateEvent(e);
}
auto Room::setTopic(std::string topic) const
-> PromiseT
{
json j{
{"type", "m.room.topic"},
{"content", {
{"topic", topic}
}
}
};
Event e{j};
return sendStateEvent(e);
}
auto Room::invite(std::string userId) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(InviteToRoomAction{+roomId(), userId});
}
auto Room::setTyping(bool typing, std::optional<int> timeoutMs) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(SetTypingAction{+roomId(), typing, timeoutMs});
}
auto Room::leave() const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(LeaveRoomAction{+roomId()});
}
auto Room::forget() const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(ForgetRoomAction{+roomId()});
}
auto Room::kick(std::string userId, std::optional<std::string> reason) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(KickAction{+roomId(), userId, reason});
}
auto Room::ban(std::string userId, std::optional<std::string> reason) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(BanAction{+roomId(), userId, reason});
}
auto Room::unban(std::string userId/*, std::optional<std::string> reason*/) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(UnbanAction{+roomId(), userId});
}
auto Room::setPinnedEvents(immer::flex_vector<std::string> eventIds) const
-> PromiseT
{
json j{
{"type", "m.room.pinned_events"},
{"content", {
{"pinned", eventIds}
}
}
};
Event e{j};
return sendStateEvent(e);
}
auto Room::timelineGaps() const
-> lager::reader<immer::map<std::string /* eventId */,
std::string /* prevBatch */>>
{
return roomCursor()[&RoomModel::timelineGaps];
}
auto Room::paginateBackFromEvent(std::string eventId) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(PaginateTimelineAction{
+roomId(), eventId, std::nullopt});
}
auto Room::localEchoes() const -> lager::reader<immer::flex_vector<LocalEchoDesc>>
{
return roomCursor()[&RoomModel::localEchoes];
}
}
diff --git a/src/tests/client/room/local-echo-test.cpp b/src/tests/client/room/local-echo-test.cpp
index 0d3fdce..8442148 100644
--- a/src/tests/client/room/local-echo-test.cpp
+++ b/src/tests/client/room/local-echo-test.cpp
@@ -1,334 +1,409 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <tuple>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_predicate.hpp>
#include <lager/event_loop/boost_asio.hpp>
#include <asio-promise-handler.hpp>
#include <cursorutil.hpp>
#include <sdk-model.hpp>
#include <client/client.hpp>
#include <crypto-util.hpp>
#include <cprjobhandler.hpp>
#include <lagerstoreeventemitter.hpp>
#include <debug.hpp>
#include <sdk.hpp>
#include "client-test-util.hpp"
using namespace Kazv;
using Catch::Matchers::Predicate;
inline auto eventJson = json{
{"content", {
{"foo", "bar"},
}},
{"type", "m.room.message"},
};
template<class Store, class Func>
static auto getMockContext(SingleTypePromiseInterface<EffectStatus> &ph, Store &store, Func func)
{
return typename Client::ContextT([&ph, &store, func](const auto &action) {
kzo.client.dbg() << "dispatched: index " << action.index() << std::endl;
func(action);
if (std::holds_alternative<GetRoomStatesAction>(action)
|| std::holds_alternative<QueryKeysAction>(action)
|| std::holds_alternative<SendToDeviceMessageAction>(action)
|| std::holds_alternative<SendMessageAction>(action)) {
return ph.createResolved(EffectStatus(true, json::object()));
} else if (std::holds_alternative<ClaimKeysAction>(action)) {
return ph.createResolved(EffectStatus(true, json::object({{"keyEvent", json::object({})}})));
} else if (std::holds_alternative<EncryptOlmEventAction>(action)) {
return ph.createResolved(EffectStatus{true, json::object({{"encrypted", json::object({{"type", ""}})}})});
} else if (std::holds_alternative<SaveLocalEchoAction>(action)
- || std::holds_alternative<EncryptMegOlmEventAction>(action)) {
+ || std::holds_alternative<EncryptMegOlmEventAction>(action)
+ || std::holds_alternative<RoomListAction>(action)) {
return store.dispatch(action);
} else {
kzo.client.err() << "Unhandled action: index " << action.index();
throw std::runtime_error{"unhandled action"};
}
}, ph, lager::deps<>{});
}
TEST_CASE("Local echo", "[client][room]")
{
RoomModel r;
auto next = RoomModel::update(r, AddLocalEchoAction{{"txnId1", Event(eventJson)}});
REQUIRE(!(next == r));
next = RoomModel::update(next, AddLocalEchoAction{{"txnId2", Event(eventJson)}});
REQUIRE(!(next == r));
REQUIRE(next.localEchoes.size() == 2);
REQUIRE_THAT(next.localEchoes[0], Predicate<LocalEchoDesc>([](const auto &desc) {
return desc.txnId == "txnId1";
}));
REQUIRE_THAT(next.localEchoes[1], Predicate<LocalEchoDesc>([](const auto &desc) {
return desc.txnId == "txnId2";
}));
}
TEST_CASE("Remove local echo", "[client][room]")
{
RoomModel r;
r.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"txnId1", Event(eventJson)},
{"txnId2", Event(eventJson)},
};
auto next = RoomModel::update(r, RemoveLocalEchoAction{"txnId1"});
REQUIRE(next.localEchoes.size() == 1);
REQUIRE_THAT(next.localEchoes[0], Predicate<LocalEchoDesc>([](const auto &desc) {
return desc.txnId == "txnId2";
}));
}
TEST_CASE("getLocalEchoByTxnId()", "[client][room]")
{
RoomModel r;
r.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"txnId1", Event(eventJson)},
{"txnId2", Event(eventJson)},
};
REQUIRE(r.getLocalEchoByTxnId("txnId1").value() == r.localEchoes[0]);
REQUIRE(!r.getLocalEchoByTxnId("txnId3").has_value());
}
TEST_CASE("Sending a message leaves a local echo", "[client][room]")
{
ClientModel m;
const auto roomId = "!foo:tusooa.xyz"s;
m.roomList.rooms = m.roomList.rooms.set(roomId, RoomModel{});
auto [next, dontCareEffect] = ClientModel::update(m, SendMessageAction{roomId, Event(eventJson)});
auto localEchoes = next.roomList.rooms[roomId].localEchoes;
REQUIRE(localEchoes.size() == 1);
assert1Job(next);
for1stJob(next, [localEchoes](const auto &job) {
REQUIRE(job.dataStr("txnId") == localEchoes[0].txnId);
});
}
TEST_CASE("Failed send changes the status of the local echo", "[client][room]")
{
ClientModel m;
const auto roomId = "!foo:tusooa.xyz"s;
m.roomList.rooms = m.roomList.rooms.set(roomId, RoomModel{});
auto [next, dontCareEffect] = ClientModel::update(m, SendMessageAction{roomId, Event(eventJson)});
auto resp = createResponse("SendMessage", json::object({}), json{
{"roomId", roomId},
{"txnId", next.roomList.rooms[roomId].localEchoes[0].txnId},
});
resp.statusCode = 500;
std::tie(next, dontCareEffect) = ClientModel::update(next, ProcessResponseAction{resp});
auto localEchoes = next.roomList.rooms[roomId].localEchoes;
REQUIRE(localEchoes.size() == 1);
REQUIRE(localEchoes[0].status == LocalEchoDesc::Failed);
}
TEST_CASE("Local echo with encrypted event", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
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 saveLocalEchoCalled = 0;
auto sendMessageCalled = 0;
auto txnId = std::string();
auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &txnId](const auto &a) {
if (std::holds_alternative<SaveLocalEchoAction>(a)) {
++saveLocalEchoCalled;
} else if (std::holds_alternative<SendMessageAction>(a)) {
++sendMessageCalled;
REQUIRE(std::get<SendMessageAction>(a).txnId.has_value());
txnId = std::get<SendMessageAction>(a).txnId.value();
}
});
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.sendTextMessage("test")
.then([&io](auto) {
kzo.client.dbg() << "ended" << std::endl;
io.stop();
});
io.run();
REQUIRE(saveLocalEchoCalled == 2);
REQUIRE(sendMessageCalled == 1);
REQUIRE(r.localEchoes().make().get().size() == 1);
REQUIRE(r.localEchoes().make().get()[0].txnId == txnId);
}
TEST_CASE("Encrypted room: Resend encrypted local echo", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
auto encryptedEvent = Event{json{
{"content", {{"foo", "bar"}}},
{"type", "m.room.encrypted"},
}};
auto decryptedJson = json{
{"content", {{"dec-foo", "dec-bar"}}},
{"type", "m.room.message"},
};
auto event = encryptedEvent.setDecryptedJson(decryptedJson, Event::Decrypted);
room.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"some-txn-id", event, LocalEchoDesc::Failed},
};
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
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 saveLocalEchoCalled = 0;
auto sendMessageCalled = 0;
auto megOlmEncryptCalled = 0;
auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &megOlmEncryptCalled](const auto &a) {
if (std::holds_alternative<SaveLocalEchoAction>(a)) {
++saveLocalEchoCalled;
} else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
++megOlmEncryptCalled;
} else if (std::holds_alternative<SendMessageAction>(a)) {
++sendMessageCalled;
REQUIRE(std::get<SendMessageAction>(a).txnId.has_value());
REQUIRE(std::get<SendMessageAction>(a).txnId.value() == "some-txn-id");
}
});
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.resendMessage("some-txn-id")
.then([&io](auto) {
kzo.client.dbg() << "ended" << std::endl;
io.stop();
});
io.run();
REQUIRE(saveLocalEchoCalled == 0);
REQUIRE(megOlmEncryptCalled == 0);
REQUIRE(sendMessageCalled == 1);
REQUIRE(r.localEchoes().make().get().size() == 1);
REQUIRE(r.localEchoes().make().get()[0].txnId == "some-txn-id");
}
+TEST_CASE("Encrypted room: Resend unencrypted local echo", "[client][room]")
+{
+ boost::asio::io_context io;
+ SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
+
+ ClientModel m;
+ m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
+ RoomModel room;
+ room.encrypted = true;
+ room.roomId = "!exampleroomid:example.com";
+ auto event = Event{json{
+ {"content", {{"dec-foo", "dec-bar"}}},
+ {"type", "m.room.message"},
+ }};
+ room.localEchoes = immer::flex_vector<LocalEchoDesc>{
+ {"some-txn-id", event, LocalEchoDesc::Failed},
+ {"some-other-txn-id", event, LocalEchoDesc::Failed},
+ };
+ m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
+ 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 saveLocalEchoCalled = 0;
+ auto sendMessageCalled = 0;
+ auto megOlmEncryptCalled = 0;
+ auto roomListActionCalled = 0;
+ auto txnId = std::string();
+ auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &megOlmEncryptCalled, &roomListActionCalled, &txnId](const auto &a) {
+ if (std::holds_alternative<SaveLocalEchoAction>(a)) {
+ ++saveLocalEchoCalled;
+ } else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
+ ++megOlmEncryptCalled;
+ } else if (std::holds_alternative<RoomListAction>(a)) {
+ ++roomListActionCalled;
+ } else if (std::holds_alternative<SendMessageAction>(a)) {
+ ++sendMessageCalled;
+ REQUIRE(std::get<SendMessageAction>(a).txnId.has_value());
+ txnId = std::get<SendMessageAction>(a).txnId.value();
+ }
+ });
+
+ auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
+
+ auto r = client.room("!exampleroomid:example.com");
+
+ REQUIRE(r.encrypted().make().get());
+
+ r.resendMessage("some-txn-id")
+ .then([&io](auto st) {
+ REQUIRE(st.success());
+ kzo.client.dbg() << "ended" << std::endl;
+ io.stop();
+ });
+
+ io.run();
+
+ REQUIRE(saveLocalEchoCalled == 2);
+ REQUIRE(roomListActionCalled == 1); // once for removing the existing local echo
+ REQUIRE(megOlmEncryptCalled == 1);
+ REQUIRE(sendMessageCalled == 1);
+ REQUIRE(r.localEchoes().make().get().size() == 2);
+ REQUIRE(r.localEchoes().make().get()[0].txnId == "some-other-txn-id");
+ REQUIRE(r.localEchoes().make().get()[1].txnId == txnId);
+}
+
TEST_CASE("Unencrypted room: Resend unencrypted local echo", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = false;
room.roomId = "!exampleroomid:example.com";
auto event = Event{json{
{"content", {{"dec-foo", "dec-bar"}}},
{"type", "m.room.message"},
}};
room.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"some-txn-id", event, LocalEchoDesc::Failed},
};
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
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 saveLocalEchoCalled = 0;
auto sendMessageCalled = 0;
auto megOlmEncryptCalled = 0;
auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &megOlmEncryptCalled](const auto &a) {
if (std::holds_alternative<SaveLocalEchoAction>(a)) {
++saveLocalEchoCalled;
} else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
++megOlmEncryptCalled;
} else if (std::holds_alternative<SendMessageAction>(a)) {
++sendMessageCalled;
REQUIRE(std::get<SendMessageAction>(a).txnId.has_value());
REQUIRE(std::get<SendMessageAction>(a).txnId.value() == "some-txn-id");
}
});
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(!r.encrypted().make().get());
r.resendMessage("some-txn-id")
.then([&io](auto) {
kzo.client.dbg() << "ended" << std::endl;
io.stop();
});
io.run();
REQUIRE(saveLocalEchoCalled == 0);
REQUIRE(megOlmEncryptCalled == 0);
REQUIRE(sendMessageCalled == 1);
REQUIRE(r.localEchoes().make().get().size() == 1);
REQUIRE(r.localEchoes().make().get()[0].txnId == "some-txn-id");
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Sep 18, 10:36 PM (1 h, 51 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768362
Default Alt Text
(33 KB)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline
Log In to Comment