Page MenuHomePhorge

No OneTemporary

Size
72 KB
Referenced Files
None
Subscribers
None
diff --git a/changelogs/72.add b/changelogs/72.add
new file mode 100644
index 0000000..89d0d8d
--- /dev/null
+++ b/changelogs/72.add
@@ -0,0 +1 @@
+Add constant-time cursors for room timeline
diff --git a/src/client/room/room.cpp b/src/client/room/room.cpp
index 259f320..026cec0 100644
--- a/src/client/room/room.cpp
+++ b/src/client/room/room.cpp
@@ -1,816 +1,841 @@
/*
* 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::invitedMembers() const -> lager::reader<immer::flex_vector<std::string>>
{
return roomCursor().map([](auto room) {
return room.invitedMemberIds();
});
}
auto Room::knockedMembers() const -> lager::reader<immer::flex_vector<std::string>>
{
return roomCursor().map([](auto room) {
return room.knockedMemberIds();
});
}
auto Room::leftMembers() const -> lager::reader<immer::flex_vector<std::string>>
{
return roomCursor().map([](auto room) {
return room.leftMemberIds();
});
}
auto Room::bannedMembers() const -> lager::reader<immer::flex_vector<std::string>>
{
return roomCursor().map([](auto room) {
return room.bannedMemberIds();
});
}
auto Room::joinedMemberEvents() const -> lager::reader<EventList>
{
return roomCursor().map([](auto room) {
return room.joinedMemberEvents();
});
}
auto Room::invitedMemberEvents() const -> lager::reader<EventList>
{
return roomCursor().map([](auto room) {
return room.invitedMemberEvents();
});
}
auto Room::knockedMemberEvents() const -> lager::reader<EventList>
{
return roomCursor().map([](auto room) {
return room.knockedMemberEvents();
});
}
auto Room::leftMemberEvents() const -> lager::reader<EventList>
{
return roomCursor().map([](auto room) {
return room.leftMemberEvents();
});
}
auto Room::bannedMemberEvents() const -> lager::reader<EventList>
{
return roomCursor().map([](auto room) {
return room.bannedMemberEvents();
});
}
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, msg](auto status) mutable {
if (! status) { return ctx.createResolvedPromise(status); }
return saveLocalEchoPromise.then([ctx, rid, status, msg](auto st) {
auto txnId = st.dataStr("txnId");
kzo.client.dbg() << "sending encrypted message 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(SendMessageAction{rid, event, 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::timelineEventIds() const -> lager::reader<immer::flex_vector<std::string>>
+ {
+ return roomCursor()[&RoomModel::timeline];
+ }
+
+ auto Room::messagesMap() const -> lager::reader<immer::map<std::string, Event>>
+ {
+ return roomCursor()[&RoomModel::messages];
+ }
+
+ auto Room::timelineEvents() const -> lager::reader<immer::flex_vector<Event>>
+ {
+ return roomCursor()
+ .xform(zug::map([](auto r) {
+ auto messages = r.messages;
+ auto timeline = r.timeline;
+ return intoImmer(
+ immer::flex_vector<Event>{},
+ zug::map([=](auto eventId) {
+ return messages[eventId];
+ }),
+ timeline);
+ }));
+ }
+
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.room.power_levels", ""})
.map([](const Event &event) {
return PowerLevelsDesc(event);
});
}
}
diff --git a/src/client/room/room.hpp b/src/client/room/room.hpp
index 642e054..1f6ff1f 100644
--- a/src/client/room/room.hpp
+++ b/src/client/room/room.hpp
@@ -1,747 +1,757 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <lager/reader.hpp>
#include <lager/context.hpp>
#include <lager/with.hpp>
#include <lager/constant.hpp>
#include <lager/lenses/optional.hpp>
#include <zug/transducer/map.hpp>
#include <zug/transducer/filter.hpp>
#include <zug/sequence.hpp>
#include <immer/flex_vector_transient.hpp>
#include "debug.hpp"
#include "sdk-model.hpp"
#include "client-model.hpp"
#include "room-model.hpp"
#include <cursorutil.hpp>
#include "sdk-model-cursor-tag.hpp"
#include "random-generator.hpp"
#include "power-levels-desc.hpp"
namespace Kazv
{
/**
* Represent a Matrix room.
*
* This class has the same constraints as Client.
*/
class Room
{
public:
using PromiseT = SingleTypePromise<DefaultRetType>;
using DepsT = lager::deps<SdkModelCursorKey, RandomInterface &
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, EventLoopThreadIdKeeper &
#endif
>;
using ContextT = Context<ClientAction>;
struct InEventLoopTag {};
/**
* Constructor.
*
* Construct the room with @c roomId .
*
* `sdk` and `roomId` must be cursors in the same thread.
*
* The constructed room will be in the same thread as `sdk` and `roomId`.
*
* @warning Do not use this directly. Use `Client::room()` and
* `Client::roomBycursor()` instead.
*/
Room(lager::reader<SdkModel> sdk,
lager::reader<std::string> roomId,
ContextT ctx);
/**
* Constructor.
*
* Construct the room with @c roomId and with Deps support.
*
* `sdk` and `roomId` must be cursors in the same thread.
*
* The constructed room will be in the same thread as `sdk` and `roomId`.
*
* @warning Do not use this directly. Use `Client::room()` and
* `Client::roomBycursor()` instead.
*/
Room(lager::reader<SdkModel> sdk,
lager::reader<std::string> roomId,
ContextT ctx, DepsT deps);
/**
* Construct a Room in the same thread as the event loop.
*
* The constructed Room is not constructed from a cursor,
* and thus copying-constructing from that is thread-safe as long as each thread
* calls with different objects.
*
* this must have Deps support.
*
* @warning Do not use this directly. Use `Client::room()` and
* `Client::roomBycursor()` instead.
*/
Room(InEventLoopTag, std::string roomId, ContextT ctx, DepsT deps);
/**
* Return a Room that represents the room *currently represented* by this,
* but suitable for use in the event loop of the context.
*
* This function can only be called from the thread where this belongs.
*
* Example:
*
* ```
* auto ctx = sdk.context();
* auto client = sdk.clientFromSecondaryRoot(sr);
* auto room = client.room("!room-id:domain.name");
* room.sendTextMessage("test")
* .then([r=room.toEventLoop(), ctx](auto &&st) {
* if (!st) {
* std::cerr << "Cannot send message" << std::endl;
* return ctx.createResolvedPromise(st);
* }
* return r.sendTextMessage("follow-up");
* });
* ```
*
* @sa Sdk::clientFromSecondaryRoot , Client::room
*/
Room toEventLoop() const;
/* lager::reader<MapT<KeyOfState, Event>> */
inline auto stateEvents() const {
return roomCursor()
[&RoomModel::stateEvents];
}
/**
* Get the invite_state of this room.
*
* @return A lager::reader containing a map from KeyOfState to the event.
*/
auto inviteState() const -> lager::reader<immer::map<KeyOfState, Event>>;
/* lager::reader<std::optional<Event>> */
inline auto stateOpt(KeyOfState k) const {
return stateEvents()
[std::move(k)];
}
/* lager::reader<Event> */
inline auto state(KeyOfState k) const {
return stateOpt(k)
[lager::lenses::or_default];
}
- /* lager::reader<RangeT<Event>> */
- inline auto timelineEvents() const {
- return roomCursor()
- .xform(zug::map([](auto r) {
- auto messages = r.messages;
- auto timeline = r.timeline;
- return intoImmer(
- immer::flex_vector<Event>{},
- zug::map([=](auto eventId) {
- return messages[eventId];
- }),
- timeline);
- }));
- }
+ /**
+ * Get the timeline event ids of this room in ascending timestamp order.
+ * It takes constant time for the cursor to be updated.
+ *
+ * @return A lager::reader of a RangeT of event ids.
+ */
+ auto timelineEventIds() const -> lager::reader<immer::flex_vector<std::string>>;
+
+ /**
+ * Get a map from event ids to events. It takes constant time for the
+ * cursor to be updated.
+ *
+ * @return A lager::reader of a map for all timeline events.
+ */
+ auto messagesMap() const -> lager::reader<immer::map<std::string, Event>>;
+
+ /**
+ * Get a list of timeline events in this room. It takes O(timeline.size())
+ * time for the cursor to be updated.
+ *
+ * @return A lager::reader<RangeT<Event>> of timeline events from
+ * the oldest to the latest.
+ */
+ auto timelineEvents() const -> lager::reader<immer::flex_vector<Event>>;
/**
* Get one message by the event id.
*
* @param A lager::reader of the event id.
*
* @return A lager::reader of the event.
*/
auto message(lager::reader<std::string> eventId) const -> lager::reader<Event>;
/**
* Get the member events of heroes this room.
*
* @return a lager::reader of a RangeT of Event containing the member events.
*/
auto heroMemberEvents() const -> lager::reader<immer::flex_vector<Event>>;
/**
* Get the member events of heroes this room.
*
* @return a lager::reader of a RangeT of std::string containing the member events.
*/
auto heroDisplayNames() const -> lager::reader<immer::flex_vector<std::string>>;
/**
* Get the name of this room.
*
* If there is a m.room.name state event, the name in it is used.
* If there is none, the returned cursor will hold std::nullopt.
*
* @return a lager::reader of an optional std::string.
*/
auto nameOpt() const -> lager::reader<std::optional<std::string>>;
/**
* Get the name of this room.
*
* If there is a m.room.name state event, the name in it is used.
* If there is none, the returned cursor will hold a placeholder string.
*
* @return a lager::reader of an std::string.
*/
auto name() const -> lager::reader<std::string>;
/**
* Get the avatar mxc uri of this room.
*
* @return A lager::reader containing the mxc uri of this room.
*/
auto avatarMxcUri() const -> lager::reader<std::string>;
/**
* Get the list of joined member ids.
*
* @return A lager::reader containing an RangeT of the joined members' ids.
*/
auto members() const -> lager::reader<immer::flex_vector<std::string>>;
/**
* Get the list of invited member ids.
*
* @return A lager::reader containing an RangeT of the invited members' ids.
*/
auto invitedMembers() const -> lager::reader<immer::flex_vector<std::string>>;
/**
* Get the list of knocked member ids.
*
* @return A lager::reader containing an RangeT of the knocked members' ids.
*/
auto knockedMembers() const -> lager::reader<immer::flex_vector<std::string>>;
/**
* Get the list of left member ids.
*
* @return A lager::reader containing an RangeT of the left members' ids.
*/
auto leftMembers() const -> lager::reader<immer::flex_vector<std::string>>;
/**
* Get the list of banned member ids.
*
* @return A lager::reader containing an RangeT of the banned members' id.
*/
auto bannedMembers() const -> lager::reader<immer::flex_vector<std::string>>;
/**
* Get the list of joined member events.
*
* @return A lager::reader containing an EventList of the joined members' events.
*/
auto joinedMemberEvents() const -> lager::reader<EventList>;
/**
* Get the list of invited member events.
*
* @return A lager::reader containing an EventList of the invited members' events.
*/
auto invitedMemberEvents() const -> lager::reader<EventList>;
/**
* Get the list of knocked member events.
*
* @return A lager::reader containing an EventList of the knocked members' events.
*/
auto knockedMemberEvents() const -> lager::reader<EventList>;
/**
* Get the list of left member events.
*
* @return A lager::reader containing an EventList of the left members' events.
*/
auto leftMemberEvents() const -> lager::reader<EventList>;
/**
* Get the list of banned member events.
*
* @return A lager::reader containing an EventList of the banned members' events.
*/
auto bannedMemberEvents() const -> lager::reader<EventList>;
/**
* Get the member event for userId.
*
* If membership of the current user is Invite, it prefers
* the event in inviteState to the one in stateEvents.
*
* @return A lager::reader containing the state event.
*/
auto memberEventByCursor(lager::reader<std::string> userId) const -> lager::reader<Event>;
/**
* Get the member event for userId.
*
* If membership of the current user is Invite, it prefers
* the event in inviteState to the one in stateEvents.
*
* @return A lager::reader containing the state event.
*/
auto memberEventFor(std::string userId) const -> lager::reader<Event>;
/**
* Get whether this room is encrypted.
*
* The encryption status is changed to true if the client
* receives a state event that turns on encryption.
* If that state event is removed later, the status will
* not be changed.
*
* @return A lager::reader<bool> that contains
* whether this room is encrypted.
*/
lager::reader<bool> encrypted() const;
/*lager::reader<std::string>*/
KAZV_WRAP_ATTR(RoomModel, roomCursor(), roomId);
/*lager::reader<RoomMembership>*/
KAZV_WRAP_ATTR(RoomModel, roomCursor(), membership);
/*lager::reader<std::string>*/
KAZV_WRAP_ATTR(RoomModel, roomCursor(), localDraft);
/* lager::reader<bool> */
KAZV_WRAP_ATTR(RoomModel, roomCursor(), membersFullyLoaded);
/**
* Get the ids of the heroes of the room.
*
* @return a lager::reader of a RangeT<std::string> containing
* the ids of the heroes of the room.
*/
auto heroIds() const -> lager::reader<immer::flex_vector<std::string>>;
/**
* Get the joined member count of this room.
*
* @return A lager::reader of std::size_t containing the joined
* member count for this room.
*/
auto joinedMemberCount() const -> lager::reader<std::size_t>;
/**
* Get the invited member count of this room.
*
* @return A lager::reader of std::size_t containing the invited
* member count for this room.
*/
auto invitedMemberCount() const -> lager::reader<std::size_t>;
/**
* Set local draft for this room.
*
* After the returned Promise is resolved,
* @c localDraft() will contain @c localDraft .
*
* @param localDraft The local draft to send.
* @return A Promise that resolves when the local draft
* has been set, or when there is an error.
*/
PromiseT setLocalDraft(std::string localDraft) const;
/**
* Send an event to this room.
*
* @param msg The message to send
* @return A Promise that resolves when the event has been sent,
* or when there is an error.
*/
PromiseT sendMessage(Event msg) const;
/**
* Send a text message to this room.
*
* @param text The text
* @return A Promise that resolves when the text message has
* been sent, or when there is an error.
*/
PromiseT sendTextMessage(std::string text) const;
/**
* Resend an event to this room.
*
* @param txnId The transaction id of the unsent message
* @return A Promise that resolves when the event has been sent,
* or when there is an error.
*/
PromiseT resendMessage(std::string txnId) const;
/**
* Redact an event.
*
* @param eventId The event id of the event you want to redact
* @param reason The reason of redaction
* @return A Promise that resolves when the event has been sent,
* or when there is an error.
*/
PromiseT redactEvent(std::string eventId, std::optional<std::string> reason) const;
/**
* Send one pending key event in this room.
*
* @param txnId The transaction id of the pending key event
* @return A Promise that resolves when the event has been sent,
* or when there is an error.
*/
PromiseT sendPendingKeyEvent(std::string txnId) const;
/**
* Send all pending key events in this room.
*
* @return A Promise that resolves when all events has been sent,
* or when there is an error.
*/
PromiseT sendAllPendingKeyEvents() const;
/**
* Get the full state of this room.
*
* This method will update the Client as needed.
*
* After the returned Promise resolves successfully,
* @c stateEvents() will contain the fetched state.
*
* @return A Promise that resolves when the room state
* has been fetched, or when there is an error.
*/
PromiseT refreshRoomState() const;
/**
* Get one state event with @c type and @c stateKey .
*
* This method will update the Client as needed.
*
* After the returned Promise resolves successfully,
* @c state({type,stateKey}) will contain the fetched
* state event.
*
* @return A Promise that resolves when the state
* event has been fetched, or when there is an error.
*/
PromiseT getStateEvent(std::string type, std::string stateKey) const;
/**
* Send a state event to this room.
*
* @param state The state event to send.
* @return A Promise that resolves when the state event
* has been sent, or when there is an error.
*/
PromiseT sendStateEvent(Event state) const;
/**
* Set the room name.
*
* @param name The new name for this room.
* @return A Promise that resolves when the state event
* for the name change has been sent, or when there is an error.
*/
PromiseT setName(std::string name) const;
// lager::reader<std::string>
inline auto topic() const {
using namespace lager::lenses;
return stateEvents()
[KeyOfState{"m.room.topic", ""}]
[or_default]
.xform(eventContent
| jsonAtOr("topic"s, ""s));
}
/**
* Set the room topic.
*
* @param topic The new topic for this room.
* @return A Promise that resolves when the state event
* for the topic change has been sent, or when there is an error.
*/
PromiseT setTopic(std::string topic) const;
/**
* Invite a user to this room
*
* @param userId The user id for the user to invite.
* @return A Promise that resolves when the state event
* for the invite has been sent, or when there is an error.
*/
PromiseT invite(std::string userId) const;
/* lager::reader<MapT<std::string, Event>> */
inline auto ephemeralEvents() const {
return roomCursor()
[&RoomModel::ephemeral];
}
/* lager::reader<std::optional<Event>> */
inline auto ephemeralOpt(std::string type) const {
return roomCursor()
[&RoomModel::ephemeral]
[type];
}
/* lager::reader<Event> */
inline auto ephemeral(std::string type) const {
return roomCursor()
[&RoomModel::ephemeral]
[type]
[lager::lenses::or_default];
}
/**
* Get the ids of all typing users in this room.
*
* @return A lager::reader of an RangeT of std::string of ids of all typing users.
*/
auto typingUsers() const -> lager::reader<immer::flex_vector<std::string>>;
/**
* Get the member events of all typing users in this room.
*
* @return A lager::reader of an EventList of member events of all typing users.
*/
auto typingMemberEvents() const -> lager::reader<EventList>;
/**
* Set the typing status of the current user in this room.
*
* @param typing Whether the user is now typing.
* @param timeoutMs How long this typing status should last,
* in milliseconds.
* @return A Promise that resolves when the typing status
* has been sent, or when there is an error.
*/
PromiseT setTyping(bool typing, std::optional<int> timeoutMs) const;
/* lager::reader<MapT<std::string, Event>> */
inline auto accountDataEvents() const {
return roomCursor()
[&RoomModel::accountData];
}
/* lager::reader<std::optional<Event>> */
inline auto accountDataOpt(std::string type) const {
return roomCursor()
[&RoomModel::accountData]
[type];
}
/* lager::reader<Event> */
inline auto accountData(std::string type) const {
return roomCursor()
[&RoomModel::accountData]
[type]
[lager::lenses::or_default];
}
/* lager::reader<std::string> */
inline auto readMarker() const {
using namespace lager::lenses;
return accountData("m.fully_read")
.xform(eventContent
| jsonAtOr("event_id", std::string{}));
}
/**
* Set the account data for this room.
*
* @return A Promise that resolves when the account data
* has been set, or when there is an error.
*/
PromiseT setAccountData(Event accountDataEvent) const;
/**
* Get the tags of the current room.
*
* @return A lager::reader of a map from tag id of this room to
* the corresponding order.
*/
auto tags() const -> lager::reader<immer::map<std::string, double>>;
/**
* Add or set a tag to this room.
*
* @param tagId The tag id to add or set.
* @param order The order to specify.
*
* @return A Promise that resolves when the tag
* has been set, or when there is an error.
*/
PromiseT addOrSetTag(std::string tagId, std::optional<double> order = std::nullopt) const;
/**
* Remove a tag from this room.
*
* @param tagId The tag id to remove.
*
* @return A Promise that resolves when the tag
* has been removed, or when there is an error.
*/
PromiseT removeTag(std::string tagId) const;
/**
* Leave this room.
*
* @return A Promise that resolves when the state event
* for the leaving has been sent, or when there is an error.
*/
PromiseT leave() const;
/**
* Forget this room.
*
* One can only forget a room when they have already left it.
*
* @return A Promise that resolves when the room has been
* forgot, or when there is an error.
*/
PromiseT forget() const;
/**
* Kick a user from this room.
*
* You must have enough power levels in this room to do so.
*
* @param userId The id of the user that will be kicked.
* @param reason The reason to explain this kick.
*
* @return A Promise that resolves when the kick is done,
* or when there is an error.
*/
PromiseT kick(std::string userId, std::optional<std::string> reason = std::nullopt) const;
/**
* Ban a user from this room.
*
* You must have enough power levels in this room to do so.
*
* @param userId The id of the user that will be banned.
* @param reason The reason to explain this ban.
*
* @return A Promise that resolves when the ban is done,
* or when there is an error.
*/
PromiseT ban(std::string userId, std::optional<std::string> reason = std::nullopt) const;
// TODO: v1.1 adds reason field
/**
* Unban a user from this room.
*
* You must have enough power levels in this room to do so.
*
* @param userId The id of the user that will be unbanned.
*
* @return A Promise that resolves when the unban is done,
* or when there is an error.
*/
PromiseT unban(std::string userId/*, std::optional<std::string> reason = std::nullopt*/) const;
/* lager::reader<JsonWrap> */
inline auto avatar() const {
return state(KeyOfState{"m.room.avatar", ""})
.xform(eventContent);
}
/* lager::reader<RangeT<std::string>> */
inline auto pinnedEvents() const {
return state(KeyOfState{"m.room.pinned_events", ""})
.xform(eventContent
| jsonAtOr("pinned", immer::flex_vector<std::string>{}));
}
/**
* Set pinned events of this room
*
* @param eventIds The event ids of the new pinned events
*
* @return A Promise that resolves when the state event
* for the pinned events change has been sent, or when there is an error.
*/
PromiseT setPinnedEvents(immer::flex_vector<std::string> eventIds) const;
/**
* Get the Gaps in the timeline for this room.
*
* Any key of the map in the returned reader can be send as
* an argument of paginateBackFromEvent() to try to fill the Gap
* at that event.
*
* @return A lager::reader that contains an evnetId-to-prevBatch map.
*/
lager::reader<immer::map<std::string /* eventId */, std::string /* prevBatch */>> timelineGaps() const;
/**
* Try to paginate back from @c eventId.
*
* @param eventId An event id that is in the key of `+timelineGaps()`.
*
* @return A Promise that resolves when the pagination is
* successful, or when there is an error. If it is successful,
* `+timelineGaps()` will no longer contain eventId as key, and
* `timeline()` will contain the events before eventId in the
* full event chain on the homeserver.
* If `eventId` is not in `+timelineGaps()`, it is considered
* to be failed.
*/
PromiseT paginateBackFromEvent(std::string eventId) const;
/**
* Get the list of local echoes in this room.
*
* @return A lager::reader that contains the list of all local echoes in the room.
* The event type will always be m.room.message and event id will not be meaningful.
*/
auto localEchoes() const -> lager::reader<immer::flex_vector<LocalEchoDesc>>;
/**
* Get the list of pending room key events in this room.
*
* @return A lager::reader that contains the list of all pending room key events in the room.
* The event type will always be m.room.encrypted and event id will not be meaningful.
*/
auto pendingRoomKeyEvents() const -> lager::reader<immer::flex_vector<PendingRoomKeyEvent>>;
/**
* Get the power levels of this room.
*
* @return A lager::reader that contains a PowerLevelDesc describing the power levels of this room.
*/
auto powerLevels() const -> lager::reader<PowerLevelsDesc>;
private:
const lager::reader<SdkModel> &sdkCursor() const;
const lager::reader<RoomModel> &roomCursor() const;
lager::reader<RoomModel> makeRoomCursor() const;
std::string currentRoomId() const;
/// if membership is invite, return inviteState; otherwise return stateEvents
lager::reader<immer::map<KeyOfState, Event>> inviteStateOrState() const;
/// if membership is invite and inviteState[key] exists, return inviteState[key], otherwise return stateEvents[key]
lager::reader<Event> inviteStateOrStateEvent(lager::reader<KeyOfState> key) const;
std::optional<lager::reader<SdkModel>> m_sdk;
std::variant<lager::reader<std::string>, std::string> m_roomId;
ContextT m_ctx;
std::optional<DepsT> m_deps;
lager::reader<RoomModel> m_roomCursor;
KAZV_DECLARE_THREAD_ID();
KAZV_DECLARE_EVENT_LOOP_THREAD_ID_KEEPER(m_deps.has_value() ? &lager::get<EventLoopThreadIdKeeper &>(m_deps.value()) : 0);
};
}
diff --git a/src/tests/client/room-test.cpp b/src/tests/client/room-test.cpp
index ccd5611..2f21adf 100644
--- a/src/tests/client/room-test.cpp
+++ b/src/tests/client/room-test.cpp
@@ -1,412 +1,435 @@
/*
* 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.room.power_levels")
| withEventContent(content)
);
auto roomModel = makeRoom(withRoomState({powerLevelsEvent}));
auto room = makeRoomWithDumbContext(roomModel);
REQUIRE(room.powerLevels().make().get().originalEvent() == powerLevelsEvent);
}
+
+TEST_CASE("Room::{timelineEvents,timelineEventIds,messagesMap}()", "[client][room][getter][timeline]")
+{
+ auto timelineEvents = immer::flex_vector<Event>{makeEvent(), makeEvent(), makeEvent()};
+ auto timelineEventIds = intoImmer(
+ immer::flex_vector<std::string>{},
+ zug::map([](const auto &ev) { return ev.id(); }),
+ timelineEvents
+ );
+ auto messagesMap = intoImmer(
+ immer::map<std::string, Event>{},
+ zug::map([](const auto &ev) { return std::make_pair(ev.id(), ev); }),
+ timelineEvents
+ );
+ auto roomModel = makeRoom(
+ withRoomTimeline(timelineEvents)
+ );
+
+ auto room = makeRoomWithDumbContext(roomModel);
+ REQUIRE(room.timelineEvents().make().get() == timelineEvents);
+ REQUIRE(room.timelineEventIds().make().get() == timelineEventIds);
+ REQUIRE(room.messagesMap().make().get() == messagesMap);
+}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 25, 1:46 PM (3 h, 34 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
553072
Default Alt Text
(72 KB)

Event Timeline