Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F140546
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
51 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/client/room/room.cpp b/src/client/room/room.cpp
index b07c048..4b30985 100644
--- a/src/client/room/room.cpp
+++ b/src/client/room/room.cpp
@@ -1,720 +1,728 @@
/*
* 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());
}
- lager::reader<RoomModel> Room::roomCursor() const
+ 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::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::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];
}
}
diff --git a/src/client/room/room.hpp b/src/client/room/room.hpp
index 5e6a0f9..a806d70 100644
--- a/src/client/room/room.hpp
+++ b/src/client/room/room.hpp
@@ -1,658 +1,660 @@
/*
* 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"
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 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>;
/* lager::reader<RangeT<std::string>> */
inline auto members() const {
return roomCursor().xform(zug::map([=](auto room) {
return room.joinedMemberIds();
}));
}
/**
* 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>>;
/**
* 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>>;
private:
const lager::reader<SdkModel> &sdkCursor() const;
- lager::reader<RoomModel> roomCursor() 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);
};
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Jan 20, 12:07 AM (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55537
Default Alt Text
(51 KB)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline
Log In to Comment