Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F140379
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
52 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/client/room/room.cpp b/src/client/room/room.cpp
index 16af1c1..1660795 100644
--- a/src/client/room/room.cpp
+++ b/src/client/room/room.cpp
@@ -1,760 +1,760 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021-2023 tusooa <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)
, m_roomCursor(makeRoomCursor())
{
}
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)
, m_roomCursor(makeRoomCursor())
{
}
Room::Room(InEventLoopTag, std::string roomId, ContextT ctx, DepsT deps)
: m_sdk(std::nullopt)
, m_roomId(roomId)
, m_ctx(ctx)
, m_deps(deps)
, m_roomCursor(makeRoomCursor())
#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());
}
const lager::reader<RoomModel> &Room::roomCursor() const
{
return m_roomCursor;
}
lager::reader<RoomModel> Room::makeRoomCursor() 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::message(lager::reader<std::string> eventId) const -> lager::reader<Event>
{
return lager::with(roomCursor()[&RoomModel::messages], eventId)
.map([](const auto &msgs, const auto &id) {
return msgs[id];
});
}
auto Room::members() const -> lager::reader<immer::flex_vector<std::string>>
{
return roomCursor().map([](auto room) {
return room.joinedMemberIds();
});
}
auto Room::joinedMemberEvents() const -> lager::reader<EventList>
{
return roomCursor().map([](auto room) {
return room.joinedMemberEvents();
});
}
auto Room::memberEventByCursor(lager::reader<std::string> userId) const -> lager::reader<Event>
{
return inviteStateOrStateEvent(userId.map([](auto id) {
return KeyOfState{"m.room.member", id};
}));
}
auto Room::memberEventFor(std::string userId) const -> lager::reader<Event>
{
return memberEventByCursor(lager::make_constant(userId));
}
lager::reader<immer::map<KeyOfState, Event>> Room::inviteStateOrState() const
{
return lager::with(stateEvents(), inviteState(), membership())
.map([](const auto &stateEv, const auto &inviteSt, const auto &mem) {
if (mem == RoomMembership::Invite) {
return inviteSt;
} else {
return stateEv;
}
});
}
lager::reader<Event> Room::inviteStateOrStateEvent(lager::reader<KeyOfState> key) const
{
return lager::with(stateEvents(), inviteState(), membership(), key)
.map([](const auto &stateEv, const auto &inviteSt, const auto &mem, const auto &k) {
if (mem == RoomMembership::Invite) {
auto maybePtr = inviteSt.find(k);
return maybePtr ? *maybePtr : stateEv[k];
} else {
return stateEv[k];
}
});
}
auto Room::inviteState() const -> lager::reader<immer::map<KeyOfState, Event>>
{
return roomCursor()[&RoomModel::inviteState];
}
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 inviteStateOrState()
[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;
}));
}
auto Room::name() const -> lager::reader<std::string>
{
using namespace lager::lenses;
return nameOpt()[value_or("<no name>")];
}
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::joinedMemberCount() const -> lager::reader<std::size_t>
{
return roomCursor()[&RoomModel::joinedMemberCount];
}
auto Room::invitedMemberCount() const -> lager::reader<std::size_t>
{
return roomCursor()[&RoomModel::invitedMemberCount];
}
auto Room::avatarMxcUri() const -> lager::reader<std::string>
{
using namespace lager::lenses;
return inviteStateOrState()
[KeyOfState{"m.room.avatar", ""}]
[or_default]
.map([](Event ev) {
auto content = ev.content().get();
return content.contains("url")
? std::string(content["url"])
: "";
});
}
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 {
if (! status) { return ctx.createResolvedPromise(status); }
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, event, 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([rid, 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(PrepareForSharingRoomKeyAction{
rid,
devicesToSend, keyEv,
rg.generateRange<RandomData>(EncryptOlmEventAction::randomSize(devicesToSend))
});
})
.then([ctx, r, devicesToSend, rid](auto status) {
if (! status) { return ctx.createResolvedPromise({}); }
auto txnId = status.dataStr("txnId");
return r.sendPendingKeyEvent(txnId);
});
});
}
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});
});
};
auto maybeSetStatusToFailed = [ctx, saveLocalEchoPromise, rid](const auto &st) mutable {
if (st.success()) {
return ctx.createResolvedPromise(st);
}
return saveLocalEchoPromise
.then([rid, ctx](const auto &saveLocalEchoStatus) {
auto txnId = saveLocalEchoStatus.dataStr("txnId");
return ctx.dispatch(UpdateLocalEchoStatusAction{rid, txnId, LocalEchoDesc::Failed});
})
.then([st](const auto &) { // can't fail
return st;
});
};
return promise
.then(encryptEvent)
.then(saveEncryptedLocalEcho)
.then(maybeSendKeys)
.then(sendEncryptedEvent)
.then(maybeSetStatusToFailed);
}
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()](const auto &) {
kzo.client.dbg() << "resending all pending key events" << std::endl;
return that.sendAllPendingKeyEvents();
})
.then([that=toEventLoop(), txnId](const auto &sendKeyStatus) {
if (!sendKeyStatus.success()) {
kzo.client.warn() << "resending all pending key events failed" << std::endl;
return that.m_ctx.createResolvedPromise(sendKeyStatus);
}
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{rid, event, localEcho.txnId});
} else {
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::redactEvent(std::string eventId, std::optional<std::string> reason) const -> PromiseT
{
return m_ctx.dispatch(RedactEventAction{
roomId().make().get(),
eventId,
reason
});
}
auto Room::sendPendingKeyEvent(std::string txnId) const -> PromiseT
{
return m_ctx.createResolvedPromise({})
.then([txnId, r=toEventLoop()](auto &&) {
auto ctx = r.m_ctx;
auto rid = r.roomId().make().get();
kzo.client.dbg() << "sending key event as to-device message" << std::endl;
kzo.client.dbg() << "txnId of key event: " << txnId << std::endl;
auto maybePending = r.roomCursor().make().get().getPendingRoomKeyEventByTxnId(txnId);
if (!maybePending.has_value()) {
kzo.client.warn() << "No such pending room key event";
return ctx.createResolvedPromise(EffectStatus(/* succ = */ false, json::object(
{{"errorCode", "MOE_KAZV_MXC_NO_SUCH_PENDING_ROOM_KEY_EVENT"},
{"error", "No such pending room key event"}}
)));
}
auto pending = maybePending.value();
auto event = pending.event;
return ctx.dispatch(SendToDeviceMessageAction{event, pending.devices})
.then([ctx, rid, txnId](const auto &sendToDeviceStatus) {
if (!sendToDeviceStatus.success()) {
return ctx.createResolvedPromise(sendToDeviceStatus);
} else {
return ctx.dispatch(UpdateRoomAction{rid, RemovePendingRoomKeyAction{txnId}});
}
});
});
}
auto Room::sendAllPendingKeyEvents() const -> PromiseT
{
return m_ctx.createResolvedPromise({})
.then([r=toEventLoop()](auto &&) {
auto pendingEvents = r.pendingRoomKeyEvents().make().get();
if (pendingEvents.empty()) {
return r.m_ctx.createResolvedPromise(EffectStatus(/* succ = */ true));
} else {
auto txnId = pendingEvents[0].txnId;
return r.sendPendingKeyEvent(txnId)
.then([r, txnId](const auto &stat) {
if (!stat.success()) {
kzo.client.warn() << "Can't send pending key event of txnId " << txnId << std::endl;
return r.m_ctx.createResolvedPromise(stat);
}
return r.sendAllPendingKeyEvents();
});
}
});
}
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::typingUsers() const -> lager::reader<immer::flex_vector<std::string>>
{
using namespace lager::lenses;
return ephemeral("m.typing")
.xform(eventContent
| jsonAtOr("user_ids", immer::flex_vector<std::string>{}));
}
auto Room::typingMemberEvents() const -> lager::reader<EventList>
{
return lager::with(typingUsers(), roomCursor()[&RoomModel::stateEvents])
.map([](const auto &userIds, const auto &events) {
return intoImmer(EventList{}, zug::map([events](const auto &id) {
return events[KeyOfState{"m.room.member", id}];
}), userIds);
});
}
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::setAccountData(Event accountDataEvent) const -> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(SetAccountDataPerRoomAction{+roomId(), accountDataEvent});
}
auto Room::tags() const -> lager::reader<immer::map<std::string, double>>
{
return roomCursor().map(&RoomModel::tags);
}
auto Room::addOrSetTag(std::string tagId, std::optional<double> order) const -> PromiseT
{
using namespace CursorOp;
return m_ctx.createResolvedPromise({})
.then([that=toEventLoop(), tagId, order](auto) {
return that.setAccountData((+that.roomCursor()).makeAddTagEvent(tagId, order));
});
}
auto Room::removeTag(std::string tagId) const -> PromiseT
{
using namespace CursorOp;
return m_ctx.createResolvedPromise({})
.then([that=toEventLoop(), tagId](auto) {
return that.setAccountData((+that.roomCursor()).makeRemoveTagEvent(tagId));
});
}
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];
}
auto Room::pendingRoomKeyEvents() const -> lager::reader<immer::flex_vector<PendingRoomKeyEvent>>
{
return roomCursor()[&RoomModel::pendingRoomKeyEvents];
}
auto Room::powerLevels() const -> lager::reader<PowerLevelsDesc>
{
- return state({"m.power_levels", ""})
+ return state({"m.room.power_levels", ""})
.map([](const Event &event) {
return PowerLevelsDesc(event);
});
}
}
diff --git a/src/tests/client/power-levels-desc-test.cpp b/src/tests/client/power-levels-desc-test.cpp
index 93710db..55d57b0 100644
--- a/src/tests/client/power-levels-desc-test.cpp
+++ b/src/tests/client/power-levels-desc-test.cpp
@@ -1,277 +1,277 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <catch2/catch_test_macros.hpp>
#include <types.hpp>
#include <factory.hpp>
#include <power-levels-desc.hpp>
using namespace Kazv;
using namespace Kazv::Factory;
static auto examplePowerLevelsJson = R"({
"ban": 10,
"events": {
"m.room.message": 20,
"moe.kazv.mxc.some-type": 50
},
"events_default": 1,
"invite": 1,
"kick": 1,
"notifications": {"room": 99},
"redact": 49,
"state_default": 100,
"users": {
"@mew:example.com": 100
},
"users_default": 1
})"_json;
static auto examplePowerLevels = makeEvent(
- withEventType("m.power_levels")
+ withEventType("m.room.power_levels")
| withStateKey("")
| withEventContent(examplePowerLevelsJson)
);
TEST_CASE("Parse Power Levels", "[client][power-levels]")
{
auto powerLevels = PowerLevelsDesc(examplePowerLevels);
REQUIRE(powerLevels.normalizedEvent().content().get()
== examplePowerLevels.content().get());
SECTION("additional keys are discarded")
{
auto jsonWithAdditionalKeys = examplePowerLevelsJson;
jsonWithAdditionalKeys["moe.kazv.mxc.something-else"];
auto eventWithAdditionalKeys = makeEvent(
- withEventType("m.power_levels")
+ withEventType("m.room.power_levels")
| withStateKey("")
| withEventContent(jsonWithAdditionalKeys)
);
auto powerLevels = PowerLevelsDesc(eventWithAdditionalKeys);
REQUIRE(powerLevels.normalizedEvent().content().get()
== examplePowerLevels.content().get());
}
SECTION("additional keys in notifications are discarded")
{
auto jsonWithAdditionalKeys = examplePowerLevelsJson;
jsonWithAdditionalKeys["notifications"]["moe.kazv.mxc.something-else"];
auto eventWithAdditionalKeys = makeEvent(
- withEventType("m.power_levels")
+ withEventType("m.room.power_levels")
| withStateKey("")
| withEventContent(jsonWithAdditionalKeys)
);
auto powerLevels = PowerLevelsDesc(eventWithAdditionalKeys);
REQUIRE(powerLevels.normalizedEvent().content().get()
== examplePowerLevels.content().get());
}
SECTION("string values are converted to numbers")
{
auto jsonWithStringValues = examplePowerLevelsJson;
jsonWithStringValues["ban"] = "10";
auto eventWithStringValues = makeEvent(
- withEventType("m.power_levels")
+ withEventType("m.room.power_levels")
| withStateKey("")
| withEventContent(jsonWithStringValues)
);
auto powerLevels = PowerLevelsDesc(eventWithStringValues);
REQUIRE(powerLevels.normalizedEvent().content().get()
== examplePowerLevels.content().get());
}
SECTION("string values that cannot be converted are replaced by default values")
{
auto jsonWithStringValues = examplePowerLevelsJson;
jsonWithStringValues["ban"] = "10s";
auto expected = examplePowerLevelsJson;
expected["ban"] = 50;
auto eventWithStringValues = makeEvent(
- withEventType("m.power_levels")
+ withEventType("m.room.power_levels")
| withStateKey("")
| withEventContent(jsonWithStringValues)
);
auto powerLevels = PowerLevelsDesc(eventWithStringValues);
REQUIRE(powerLevels.normalizedEvent().content().get()
== expected);
}
SECTION("string values in events that cannot be converted are replaced by default values")
{
auto jsonWithStringValues = examplePowerLevelsJson;
jsonWithStringValues["events"]["ttt"] = "10s";
auto expected = examplePowerLevelsJson;
auto eventWithStringValues = makeEvent(
- withEventType("m.power_levels")
+ withEventType("m.room.power_levels")
| withStateKey("")
| withEventContent(jsonWithStringValues)
);
auto powerLevels = PowerLevelsDesc(eventWithStringValues);
REQUIRE(powerLevels.normalizedEvent().content().get()
== expected);
}
SECTION("string values in users that cannot be converted are replaced by default values")
{
auto jsonWithStringValues = examplePowerLevelsJson;
jsonWithStringValues["users"]["@ttt:example.com"] = "10s";
auto expected = examplePowerLevelsJson;
auto eventWithStringValues = makeEvent(
- withEventType("m.power_levels")
+ withEventType("m.room.power_levels")
| withStateKey("")
| withEventContent(jsonWithStringValues)
);
auto powerLevels = PowerLevelsDesc(eventWithStringValues);
REQUIRE(powerLevels.normalizedEvent().content().get()
== expected);
}
}
TEST_CASE("powerLevelOfUser()", "[client][power-levels]")
{
auto powerLevels = PowerLevelsDesc(examplePowerLevels);
REQUIRE(powerLevels.powerLevelOfUser("@mew:example.com") == 100);
REQUIRE(powerLevels.powerLevelOfUser("@mewmew:example.com") == 1);
SECTION("default power levels object")
{
powerLevels = PowerLevelsDesc(json::object({}));
REQUIRE(powerLevels.powerLevelOfUser("@mew:example.com") == 0);
REQUIRE(powerLevels.powerLevelOfUser("@mewmew:example.com") == 0);
}
}
TEST_CASE("canSendMessage()", "[client][power-levels]")
{
auto powerLevels = PowerLevelsDesc(examplePowerLevels);
REQUIRE(powerLevels.canSendMessage("@mew:example.com", "m.room.message"));
REQUIRE(!powerLevels.canSendMessage("@mewmew:example.com", "m.room.message"));
REQUIRE(powerLevels.canSendMessage("@mew:example.com", "m.room.encrypted"));
REQUIRE(powerLevels.canSendMessage("@mewmew:example.com", "m.room.encrypted"));
}
TEST_CASE("canSendState()", "[client][power-levels]")
{
auto powerLevels = PowerLevelsDesc(examplePowerLevels);
REQUIRE(powerLevels.canSendState("@mew:example.com", "moe.kazv.mxc.some-type"));
REQUIRE(!powerLevels.canSendState("@mewmew:example.com", "moe.kazv.mxc.some-type"));
REQUIRE(powerLevels.canSendState("@mew:example.com", "moe.kazv.mxc.some-other-type"));
REQUIRE(!powerLevels.canSendState("@mewmew:example.com", "moe.kazv.mxc.some-other-type"));
}
TEST_CASE("canInvite()", "[client][power-levels]")
{
auto powerLevels = PowerLevelsDesc(examplePowerLevels);
REQUIRE(powerLevels.canInvite("@mew:example.com"));
REQUIRE(powerLevels.canInvite("@mewmew:example.com"));
}
TEST_CASE("canKick()", "[client][power-levels]")
{
auto powerLevels = PowerLevelsDesc(examplePowerLevels);
REQUIRE(powerLevels.canKick("@mew:example.com"));
REQUIRE(powerLevels.canKick("@mewmew:example.com"));
}
TEST_CASE("canBan()", "[client][power-levels]")
{
auto powerLevels = PowerLevelsDesc(examplePowerLevels);
REQUIRE(powerLevels.canBan("@mew:example.com"));
REQUIRE(!powerLevels.canBan("@mewmew:example.com"));
}
TEST_CASE("canRedact()", "[client][power-levels]")
{
auto powerLevels = PowerLevelsDesc(examplePowerLevels);
REQUIRE(powerLevels.canRedact("@mew:example.com"));
REQUIRE(!powerLevels.canRedact("@mewmew:example.com"));
}
TEST_CASE("setBan()", "[client][power-levels]")
{
const auto powerLevels = PowerLevelsDesc(Event());
auto next = powerLevels.setBan(100);
REQUIRE(next.originalEvent().content().get() == json{{"ban", 100}});
next = next.setBan(std::nullopt);
REQUIRE(next.originalEvent().content().get() == json::object());
}
TEST_CASE("setKick()", "[client][power-levels]")
{
const auto powerLevels = PowerLevelsDesc(Event());
auto next = powerLevels.setKick(100);
REQUIRE(next.originalEvent().content().get() == json{{"kick", 100}});
next = next.setKick(std::nullopt);
REQUIRE(next.originalEvent().content().get() == json::object());
}
TEST_CASE("setInvite()", "[client][power-levels]")
{
const auto powerLevels = PowerLevelsDesc(Event());
auto next = powerLevels.setInvite(100);
REQUIRE(next.originalEvent().content().get() == json{{"invite", 100}});
next = next.setInvite(std::nullopt);
REQUIRE(next.originalEvent().content().get() == json::object());
}
TEST_CASE("setRedact()", "[client][power-levels]")
{
const auto powerLevels = PowerLevelsDesc(Event());
auto next = powerLevels.setRedact(100);
REQUIRE(next.originalEvent().content().get() == json{{"redact", 100}});
next = next.setRedact(std::nullopt);
REQUIRE(next.originalEvent().content().get() == json::object());
}
TEST_CASE("setEventsDefault()", "[client][power-levels]")
{
const auto powerLevels = PowerLevelsDesc(Event());
auto next = powerLevels.setEventsDefault(100);
REQUIRE(next.originalEvent().content().get() == json{{"events_default", 100}});
next = next.setEventsDefault(std::nullopt);
REQUIRE(next.originalEvent().content().get() == json::object());
}
TEST_CASE("setStateDefault()", "[client][power-levels]")
{
const auto powerLevels = PowerLevelsDesc(Event());
auto next = powerLevels.setStateDefault(100);
REQUIRE(next.originalEvent().content().get() == json{{"state_default", 100}});
next = next.setStateDefault(std::nullopt);
REQUIRE(next.originalEvent().content().get() == json::object());
}
TEST_CASE("setUsersDefault()", "[client][power-levels]")
{
const auto powerLevels = PowerLevelsDesc(Event());
auto next = powerLevels.setUsersDefault(100);
REQUIRE(next.originalEvent().content().get() == json{{"users_default", 100}});
next = next.setUsersDefault(std::nullopt);
REQUIRE(next.originalEvent().content().get() == json::object());
}
TEST_CASE("setEvent()", "[client][power-levels]")
{
const auto powerLevels = PowerLevelsDesc(Event());
auto next = powerLevels.setEvent("moe.kazv.mxc.some-type", 100);
REQUIRE(next.originalEvent().content().get() == json{{"events", {{"moe.kazv.mxc.some-type", 100}}}});
next = next.setEvent("moe.kazv.mxc.some-type", std::nullopt);
REQUIRE(next.originalEvent().content().get() == json{{"events", json::object()}});
}
TEST_CASE("setUser()", "[client][power-levels]")
{
const auto powerLevels = PowerLevelsDesc(Event());
auto next = powerLevels.setUser("@foo:example.com", 100);
REQUIRE(next.originalEvent().content().get() == json{{"users", {{"@foo:example.com", 100}}}});
next = next.setUser("@foo:example.com", std::nullopt);
REQUIRE(next.originalEvent().content().get() == json{{"users", json::object()}});
}
diff --git a/src/tests/client/room-test.cpp b/src/tests/client/room-test.cpp
index 0269e35..ccd5611 100644
--- a/src/tests/client/room-test.cpp
+++ b/src/tests/client/room-test.cpp
@@ -1,412 +1,412 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <catch2/catch_all.hpp>
#include <lager/event_loop/boost_asio.hpp>
#include <boost/asio.hpp>
#include <sdk.hpp>
#include <cprjobhandler.hpp>
#include <lagerstoreeventemitter.hpp>
#include <asio-promise-handler.hpp>
#include "client-test-util.hpp"
#include "factory.hpp"
using namespace Kazv;
using namespace Kazv::Factory;
TEST_CASE("Result of Room::toEventLoop() should not vary with the original cursor", "[client][room]")
{
auto io = boost::asio::io_context{};
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto roomWithId = [](std::string roomId) {
auto r = RoomModel{};
r.roomId = roomId;
return r;
};
auto initModel = Kazv::SdkModel{};
initModel.client.roomList.rooms = std::move(initModel.client.roomList.rooms)
.set("!foo:example.org", roomWithId("!foo:example.org"))
.set("!bar:example.org", roomWithId("!bar:example.org"));
auto sdk = Kazv::makeSdk(
initModel,
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
auto client = sdk.client();
auto roomId = lager::make_state(std::string("!foo:example.org"), lager::automatic_tag{});
auto room = client.roomByCursor(roomId);
using namespace Kazv::CursorOp;
REQUIRE(+room.roomId() == "!foo:example.org");
auto roomInEventLoop = room.toEventLoop();
REQUIRE(+roomInEventLoop.roomId() == "!foo:example.org");
roomId.set("!bar:example.org");
REQUIRE(+room.roomId() == "!bar:example.org");
REQUIRE(+roomInEventLoop.roomId() == "!foo:example.org");
}
static const std::string exampleRoomId = "!example:example.org";
static auto memberFoo = Event(R"(
{
"content": {
"displayname": "foo's name",
"membership": "join"
},
"event_id": "some id2",
"room_id": "!example:example.org",
"type": "m.room.member",
"state_key": "@foo:example.org"
})"_json);
static auto memberBar = Event(R"(
{
"content": {
"displayname": "bar's name",
"membership": "join"
},
"event_id": "some id3",
"room_id": "!example:example.org",
"type": "m.room.member",
"state_key": "@foo:example.org"
})"_json);
static auto exampleRoomWithoutNameEvent()
{
RoomModel model;
model.membership = RoomMembership::Join;
model.roomId = "!example:example.org";
model.stateEvents = model.stateEvents
.set({"m.room.member", "@foo:example.org"}, memberFoo)
.set({"m.room.member", "@bar:example.org"}, memberBar);
model.heroIds = {"@foo:example.org", "@bar:example.org"};
return model;
}
static auto roomNameEvent = Event(R"(
{
"content": {
"name": "some name"
},
"event_id": "some id",
"room_id": "!example:example.org",
"type": "m.room.name",
"state_key": ""
})"_json);
// https://spec.matrix.org/v1.7/client-server-api/#mroomavatar
static Event avatarEvent = R"({
"content": {
"info": {
"h": 398,
"mimetype": "image/jpeg",
"size": 31037,
"w": 394
},
"url": "mxc://example.org/JWEIFJgwEIhweiWJE"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "",
"type": "m.room.avatar",
"unsigned": {
"age": 1234
}
})"_json;
static auto exampleRoomWithNameEvent()
{
auto model = exampleRoomWithoutNameEvent();
model.stateEvents = model.stateEvents
.set({"m.room.name", ""}, roomNameEvent);
return model;
};
static auto sdkWith(RoomModel room)
{
SdkModel model;
model.client.roomList.rooms = model.client.roomList.rooms.set(room.roomId, room);
return model;
}
static auto makeRoomWithDumbContext(RoomModel room)
{
auto cursor = lager::make_constant(sdkWith(room));
auto nameCursor = lager::make_constant(room.roomId);
return Room(cursor, nameCursor, dumbContext());
}
TEST_CASE("Room::nameOpt()", "[client][room][getter]")
{
WHEN("the room has a name") {
auto room = makeRoomWithDumbContext(exampleRoomWithNameEvent());
THEN("it should give out the name") {
auto val = room.nameOpt().make().get();
REQUIRE(val.has_value());
REQUIRE(val.value() == "some name");
}
}
WHEN("the room has no name") {
auto room = makeRoomWithDumbContext(exampleRoomWithoutNameEvent());
THEN("it should give out nullopt") {
auto val = room.nameOpt().make().get();
REQUIRE(!val.has_value());
}
}
}
TEST_CASE("Room::heroMemberEvents()", "[client][room][getter]")
{
auto room = makeRoomWithDumbContext(exampleRoomWithNameEvent());
THEN("it should give out the events") {
auto val = room.heroMemberEvents().make().get();
REQUIRE(val.size() == 2);
REQUIRE(std::any_of(val.begin(), val.end(),
[](const auto &v) {
return v.id() == "some id2";
}));
REQUIRE(std::any_of(val.begin(), val.end(),
[](const auto &v) {
return v.id() == "some id3";
}));
}
}
TEST_CASE("Room::heroDisplayNames()", "[client][room][getter]")
{
auto room = makeRoomWithDumbContext(exampleRoomWithNameEvent());
THEN("it should give out the names") {
auto val = room.heroDisplayNames().make().get();
REQUIRE(val.size() == 2);
REQUIRE(std::any_of(val.begin(), val.end(),
[](const auto &v) {
return v == "foo's name";
}));
REQUIRE(std::any_of(val.begin(), val.end(),
[](const auto &v) {
return v == "bar's name";
}));
}
}
TEST_CASE("Room::encrypted()", "[client][room][getter]")
{
auto roomModel = exampleRoomWithNameEvent();
WHEN("the room is encrypted") {
roomModel.encrypted = true;
auto room = makeRoomWithDumbContext(roomModel);
THEN("it should give out true") {
auto val = room.encrypted().make().get();
REQUIRE(val);
}
}
WHEN("the room is not encrypted") {
roomModel.encrypted = false;
auto room = makeRoomWithDumbContext(roomModel);
THEN("it should give out false") {
auto val = room.encrypted().make().get();
REQUIRE(!val);
}
}
}
TEST_CASE("Room::avatarMxcUri()", "[client][room][getter]")
{
auto roomModel = exampleRoomWithoutNameEvent();
roomModel.stateEvents = roomModel.stateEvents.set(KeyOfState{"m.room.avatar", ""}, avatarEvent);
auto room = makeRoomWithDumbContext(roomModel);
REQUIRE(room.avatarMxcUri().make().get() == "mxc://example.org/JWEIFJgwEIhweiWJE");
}
TEST_CASE("Room::typingMemberEvents()", "[client][room][getter][ephemeral]")
{
auto roomModel = exampleRoomWithoutNameEvent();
auto typingJson = R"({
"content": {
"user_ids": ["@foo:example.org", "@bar:example.org"]
},
"type": "m.typing"
})"_json;
roomModel.ephemeral = roomModel.ephemeral.set("m.typing", Event{typingJson});
auto room = makeRoomWithDumbContext(roomModel);
REQUIRE(room.typingMemberEvents().make().get() == EventList{
room.memberEventFor("@foo:example.org").make().get(),
room.memberEventFor("@bar:example.org").make().get(),
});
}
TEST_CASE("Room::message()", "[client][room][getter][timeline]")
{
auto roomModel = exampleRoomWithoutNameEvent();
auto messageEvent = Event(R"({
"content": {
"body": "This is an example text message",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>"
},
"type": "m.room.message",
"event_id": "$anothermessageevent:example.org",
"room_id": "!726s6s6q:example.com",
"sender": "@example:example.org",
"origin_server_ts": 1432735824653,
"unsigned": {
"age": 1234
}
})"_json);
roomModel.messages = roomModel.messages.set(messageEvent.id(), messageEvent);
auto room = makeRoomWithDumbContext(roomModel);
REQUIRE(room.message(lager::make_constant(messageEvent.id())).make().get() == messageEvent);
}
TEST_CASE("Room::memberEventFor(), Room::memberEventByCursor", "[client][room][getter][state]")
{
auto roomModel = exampleRoomWithoutNameEvent();
auto memberFooJson = memberFoo.raw().get();
memberFooJson["content"]["displayname"] = "some other name";
auto newMemberFooEvent = Event(memberFooJson);
roomModel.inviteState = roomModel.inviteState
.set({"m.room.member", "@foo:example.org"}, newMemberFooEvent);
WHEN("membership is Invite")
{
roomModel.membership = RoomMembership::Invite;
auto room = makeRoomWithDumbContext(roomModel);
THEN("should prefer inviteState")
{
REQUIRE(room.memberEventFor("@foo:example.org").make().get() == newMemberFooEvent);
REQUIRE(room.memberEventFor("@bar:example.org").make().get() == memberBar);
}
}
WHEN("membership is not Invite")
{
roomModel.membership = RoomMembership::Leave;
auto room = makeRoomWithDumbContext(roomModel);
THEN("should ignore inviteState")
{
REQUIRE(room.memberEventFor("@foo:example.org").make().get() == memberFoo);
REQUIRE(room.memberEventFor("@bar:example.org").make().get() == memberBar);
}
}
}
TEST_CASE("Room::name(), Room::nameOpt()", "[client][room][getter][state]")
{
auto roomModel = exampleRoomWithoutNameEvent();
WHEN("membership is invite and invite state contains name event")
{
roomModel.membership = RoomMembership::Invite;
roomModel.inviteState = roomModel.inviteState.set({"m.room.name", ""}, roomNameEvent);
auto room = makeRoomWithDumbContext(roomModel);
THEN("it should give that name")
{
auto expectedName = roomNameEvent.content().get()["name"].template get<std::string>();
REQUIRE(room.nameOpt().make().get().value() == expectedName);
REQUIRE(room.name().make().get() == expectedName);
}
}
WHEN("membership is not invite and invite state contains the name event")
{
roomModel.membership = RoomMembership::Join;
roomModel.inviteState = roomModel.inviteState.set({"m.room.name", ""}, roomNameEvent);
auto room = makeRoomWithDumbContext(roomModel);
THEN("it should ignore that name")
{
REQUIRE(!room.nameOpt().make().get().has_value());
REQUIRE(room.name().make().get() == "<no name>");
}
}
}
TEST_CASE("Room::avatarMxcUri()", "[client][room][getter][state]")
{
auto roomModel = exampleRoomWithoutNameEvent();
roomModel.inviteState = roomModel.inviteState.set({"m.room.avatar", ""}, avatarEvent);
WHEN("membership is invite and invite state contains name event")
{
roomModel.membership = RoomMembership::Invite;
auto room = makeRoomWithDumbContext(roomModel);
THEN("it should give that avatar")
{
auto expectedAvatar = avatarEvent.content().get()["url"].template get<std::string>();
REQUIRE(room.avatarMxcUri().make().get() == expectedAvatar);
}
}
WHEN("membership is not invite and invite state contains the name event")
{
roomModel.membership = RoomMembership::Join;
auto room = makeRoomWithDumbContext(roomModel);
THEN("it should ignore that avatar")
{
REQUIRE(room.avatarMxcUri().make().get() == "");
}
}
}
TEST_CASE("Room::joinedMemberEvents()", "[client][room][getter][state]")
{
auto members = immer::flex_vector<Event>{makeMemberEvent(), makeMemberEvent(), makeMemberEvent()};
auto roomModel = makeRoom(
withRoomState(members)
| withRoomState({
makeMemberEvent(withMembership("leave")),
makeMemberEvent(withMembership("invite")),
makeMemberEvent(withMembership("ban")),
})
);
auto room = makeRoomWithDumbContext(roomModel);
THEN("it returns the joined members only")
{
REQUIRE_THAT(room.joinedMemberEvents().make().get(), Catch::Matchers::UnorderedRangeEquals(members));
}
}
TEST_CASE("Room::powerLevels()", "[client][room][state][power-levels]")
{
auto content = json{
{"users", {{"@foo:example.com", 40}}},
};
auto powerLevelsEvent = makeEvent(
- withEventType("m.power_levels")
+ withEventType("m.room.power_levels")
| withEventContent(content)
);
auto roomModel = makeRoom(withRoomState({powerLevelsEvent}));
auto room = makeRoomWithDumbContext(roomModel);
REQUIRE(room.powerLevels().make().get().originalEvent() == powerLevelsEvent);
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 19, 6:56 PM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55399
Default Alt Text
(52 KB)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline
Log In to Comment