Page MenuHomePhorge

No OneTemporary

Size
142 KB
Referenced Files
None
Subscribers
None
diff --git a/src/client/room/local-draft.hpp b/src/client/room/local-draft.hpp
new file mode 100644
index 0000000..9b4bcf7
--- /dev/null
+++ b/src/client/room/local-draft.hpp
@@ -0,0 +1,41 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2026 nannanko <nannankokazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <libkazv-config.hpp>
+
+#include <cstdint>
+#include <string>
+
+#include <boost/serialization/split_free.hpp>
+#include <boost/serialization/version.hpp>
+
+namespace Kazv
+{
+ /**
+ * Local draft.
+ */
+ struct LocalDraft
+ {
+ std::string text;
+ std::string relType;
+ std::string relatedEventId;
+ friend bool operator==(const LocalDraft &a, const LocalDraft &b) = default;
+ friend bool operator!=(const LocalDraft &a, const LocalDraft &b) = default;
+ };
+
+ template<class Archive>
+ void serialize(Archive &ar, LocalDraft &draft, std::uint32_t const /*version*/)
+ {
+ ar
+ & draft.relType
+ & draft.relatedEventId
+ & draft.text
+ ;
+ }
+}
+
+BOOST_CLASS_VERSION(Kazv::LocalDraft, 0)
diff --git a/src/client/room/room-model.cpp b/src/client/room/room-model.cpp
index fc3b25a..6760d1c 100644
--- a/src/client/room/room-model.cpp
+++ b/src/client/room/room-model.cpp
@@ -1,771 +1,818 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <lager/util.hpp>
#include <zug/sequence.hpp>
#include <zug/transducer/map.hpp>
#include <zug/transducer/filter.hpp>
#include "debug.hpp"
#include "room-model.hpp"
#include "cursorutil.hpp"
#include "immer-utils.hpp"
inline const auto receiptTypes = immer::flex_vector<std::string>{"m.read", "m.read.private"};
template<class Func>
static std::string getMaxInTimeline(std::string a, std::string b, Func sortKey)
{
if (a.empty() && b.empty()) {
return std::string();
} else {
// for an unexisting event, event id is empty and timestamp is 0
// for an existing event, event id is not empty and timestamp >= 0
// so this handles all cases even when we do not have the corresponding event
return std::max(a, b, [=](const std::string &x, const std::string &y) {
return sortKey(x) < sortKey(y);
});
}
}
namespace Kazv
{
PendingRoomKeyEvent makePendingRoomKeyEventV0(std::string txnId, Event event, immer::map<std::string, immer::flex_vector<std::string>> devices)
{
immer::map<std::string, immer::map<std::string, Event>> messages;
for (auto [userId, deviceIds] : devices) {
messages = setIn(std::move(messages), immer::map<std::string, Event>(), userId);
for (auto deviceId : deviceIds) {
messages = setIn(
std::move(messages),
event,
userId, deviceId
);
}
}
return PendingRoomKeyEvent{txnId, messages};
}
auto sortKeyForTimelineEvent(Event e) -> std::tuple<Timestamp, std::string>
{
return std::make_tuple(e.originServerTs(), e.id());
}
RoomModel RoomModel::update(RoomModel r, Action a)
{
return lager::match(std::move(a))(
[&](AddStateEventsAction a) {
r.stateEvents = merge(std::move(r.stateEvents), a.stateEvents, keyOfState);
// If m.room.encryption state event appears,
// configure the room to use encryption.
if (r.stateEvents.find(KeyOfState{"m.room.encryption", ""})) {
auto newRoom = update(std::move(r), SetRoomEncryptionAction{});
r = std::move(newRoom);
}
return r;
},
[&](MaybeAddStateEventsAction a) {
for (auto it = a.stateEvents.rbegin();
it != a.stateEvents.rend();
++it) {
const auto &e = *it;
auto k = keyOfState(e);
if (!r.stateEvents.count(k)) {
r.stateEvents = std::move(r.stateEvents).set(k, e);
}
}
return r;
},
[&](AddMessagesAction a) {
r.messages = merge(std::move(r.messages), a.events, keyOfTimeline);
if (!a.alsoInTimeline) {
for (const auto &e : a.events) {
r.nonTimelineEvents = std::move(r.nonTimelineEvents).insert(keyOfTimeline(e));
}
}
auto handleRedaction =
[&r](const auto &event) {
if (event.type() == "m.room.redaction") {
auto origJson = event.originalJson().get();
if (origJson.contains("redacts") && origJson.at("redacts").is_string()) {
auto redactedEventId = origJson.at("redacts").template get<std::string>();
if (r.messages.find(redactedEventId)) {
r.messages = std::move(r.messages).update(redactedEventId, [&origJson](const auto &eventToBeRedacted) {
auto newJson = eventToBeRedacted.originalJson().get();
newJson.merge_patch(json{
{"unsigned", {{"redacted_because", std::move(origJson)}}},
});
newJson["content"] = json::object();
return Event(newJson);
});
}
}
}
return event;
};
immer::for_each(a.events, handleRedaction);
// remove all local echoes that are received
for (const auto &e : a.events) {
auto jw = e.originalJson();
const auto &json = jw.get();
if (json.contains("unsigned")
&& json["unsigned"].contains("transaction_id")
&& json["unsigned"]["transaction_id"].is_string()) {
r = update(std::move(r), RemoveLocalEchoAction{json["unsigned"]["transaction_id"].template get<std::string>()});
}
}
// calculate event relationships
r.generateRelationships(a.events);
r.addToUndecryptedEvents(a.events);
return r;
},
[&](AddToTimelineAction a) {
auto eventIds = intoImmer(immer::flex_vector<std::string>(),
zug::map(keyOfTimeline), a.events);
auto oldMessages = r.messages;
auto next = RoomModel::update(std::move(r), AddMessagesAction{a.events, /* alsoInTimeline = */ true});
r = std::move(next);
auto key =
[=](auto eventId) {
// sort first by timestamp, then by id
return sortKeyForTimelineEvent(r.messages[eventId]);
};
// Things in messages do not always appear in the timeline.
// Let `exists` function to always return false, so that it is checked for duplicates automatically.
r.timeline = sortedUniqueMerge(r.timeline, eventIds, [](auto &&) { return false; }, key);
for (const auto &e : eventIds) {
r.nonTimelineEvents = std::move(r.nonTimelineEvents).erase(e);
}
// We have 3 possibilities for the source of calling this action:
// pagination, sync, or load from storage.
//
// In pagination: `gapEventId.has_value() && !limited.has_value()`
// If we can paginate back: `prevBatch.has_value()`
//
// In sync: `!gapEventId.has_value()`
// If limited: `limited.has_value() && limited.value()`
// If we can paginate back: `prevBatch.has_value()` (should always be present, but we do not add a Gap if it is not limited)
//
// In load from storage: `!gapEventId.has_value() && !limited.has_value() && !prevBatch.has_value()` (Because of actions/storage.cpp)
// Only pagination and sync can add a Gap
if (((a.limited.has_value() && a.limited.value())
|| a.gapEventId.has_value())
&& a.prevBatch.has_value()) {
// this sync is limited, add a Gap here
if (!eventIds.empty()) {
r.timelineGaps = std::move(r.timelineGaps).set(eventIds[0], a.prevBatch.value());
}
}
// Only pagination can remove Gaps
// remove the original Gap, as it is resolved
if (a.gapEventId.has_value()) {
r.timelineGaps = std::move(r.timelineGaps).erase(a.gapEventId.value());
}
// remove all Gaps between the gapped event and the first event in this batch
if (!eventIds.empty() && a.gapEventId.has_value()) {
auto cmp = [=](auto a, auto b) {
return key(a) < key(b);
};
auto thisBatchStart = std::equal_range(r.timeline.begin(), r.timeline.end(), eventIds[0], cmp).first;
auto origBatchStart = std::equal_range(thisBatchStart, r.timeline.end(), a.gapEventId.value(), cmp).first;
// Safety assert: we do not want to execute the for_each if the range is empty,
// or it will go out of bounds.
if (thisBatchStart.index() < origBatchStart.index()) {
std::for_each(thisBatchStart + 1, origBatchStart,
[&](auto eventId) {
r.timelineGaps = std::move(r.timelineGaps).erase(eventId);
});
}
}
return r;
},
[&](AddAccountDataAction a) {
r.accountData = merge(std::move(r.accountData), a.events, keyOfAccountData);
return r;
},
[&](ChangeMembershipAction a) {
r.membership = a.membership;
return r;
},
[&](ChangeInviteStateAction a) {
r.inviteState = merge(immer::map<KeyOfState, Event>{}, a.events, keyOfState);
return r;
},
[&](AddEphemeralAction a) {
auto processReceipt = [&](Event e) {
const auto content = e.content().get();
for (auto [eventId, receipts] : content.items()) {
if (!receipts.is_object()) {
continue;
}
for (auto receiptType : receiptTypes) {
if (!(receipts.contains(receiptType)
&& receipts[receiptType].is_object())) {
continue;
}
for (auto [user, receipt]: receipts[receiptType].items()) {
ReadReceipt readReceipt{
eventId,
0,
};
if (receipt.is_object() && receipt.contains("ts")
&& receipt["ts"].is_number()) {
readReceipt.timestamp = receipt["ts"].template get<Timestamp>();
}
// Remove old receipts
if (r.readReceipts.count(user)) {
auto oldReceiptEventId = r.readReceipts[user].eventId;
if (r.eventReadUsers.count(oldReceiptEventId)) {
auto remaining =
intoImmer(
immer::flex_vector<std::string>{},
zug::filter([user=user](auto userId) {
return userId != user;
}),
r.eventReadUsers[oldReceiptEventId]
);
if (remaining.empty()) {
r.eventReadUsers = std::move(r.eventReadUsers).erase(oldReceiptEventId);
} else {
r.eventReadUsers = std::move(r.eventReadUsers).set(oldReceiptEventId, remaining);
}
}
}
// Add new receipt
r.readReceipts = std::move(r.readReceipts).set(user, readReceipt);
auto oldReadUsers = r.eventReadUsers[eventId];
r.eventReadUsers = std::move(r.eventReadUsers).set(eventId, oldReadUsers.push_back(user));
}
}
}
};
for (auto e : a.events) {
if (e.type() == "m.receipt") {
processReceipt(e);
}
}
r.ephemeral = merge(std::move(r.ephemeral), a.events, keyOfEphemeral);
return r;
},
+ [&](SetLocalDraftTextAction a) {
+ r.localDrafts = r.localDrafts.update(0,
+ [&a](auto localDraft) {
+ localDraft.text = a.localDraft;
+ return localDraft;
+ });
+ return r;
+ },
+ [&](SetLocalDraftRelAction a) {
+ r.localDrafts = r.localDrafts.update(0,
+ [&a](auto localDraft) {
+ localDraft.relType = a.relType;
+ localDraft.relatedEventId = a.relatedEventId;
+ return localDraft;
+ });
+ return r;
+ },
[&](SetLocalDraftAction a) {
- r.localDraft = a.localDraft;
+ r.localDrafts = r.localDrafts.update(0,
+ [&a](auto localDraft) {
+ localDraft.text = a.text;
+ localDraft.relType = a.relType;
+ localDraft.relatedEventId = a.relatedEventId;
+ return localDraft;
+ });
+ return r;
+ },
+ [&](PushLocalDraftAction a) {
+ r.localDrafts = r.localDrafts.push_front({
+ a.text,
+ a.relType,
+ a.relatedEventId
+ });
+ return r;
+ },
+ [&](PopLocalDraftAction) {
+ r.localDrafts = r.localDrafts.drop(1);
+ if (r.localDrafts.size() == 0) {
+ r.localDrafts = r.localDrafts.push_back(LocalDraft {
+ "", "", ""
+ });
+ }
+ // If the current draft is not associated with any event,
+ // kazv cannot continue to pop the remaining drafts.
+ // Clear the remaining drafts here to avoid accumulation.
+ auto draft = r.localDrafts[0];
+ if (draft.relType == "" || draft.relatedEventId == "") {
+ r.localDrafts = {draft};
+ }
return r;
},
[&](SetRoomEncryptionAction) {
r.encrypted = true;
return r;
},
[&](MarkMembersFullyLoadedAction) {
r.membersFullyLoaded = true;
return r;
},
[&](SetHeroIdsAction a) {
r.heroIds = a.heroIds;
return r;
},
[&](AddLocalEchoAction a) {
auto it = std::find_if(r.localEchoes.begin(), r.localEchoes.end(), [a](const auto &desc) {
return desc.txnId == a.localEcho.txnId;
});
if (it == r.localEchoes.end()) {
r.localEchoes = std::move(r.localEchoes).push_back(a.localEcho);
} else {
r.localEchoes = std::move(r.localEchoes).set(it.index(), a.localEcho);
}
return r;
},
[&](RemoveLocalEchoAction a) {
auto it = std::find_if(r.localEchoes.begin(), r.localEchoes.end(), [a](const auto &desc) {
return desc.txnId == a.txnId;
});
if (it != r.localEchoes.end()) {
r.localEchoes = std::move(r.localEchoes).erase(it.index());
}
return r;
},
[&](AddPendingRoomKeyAction a) {
auto it = std::find_if(r.pendingRoomKeyEvents.begin(), r.pendingRoomKeyEvents.end(), [a](const auto &p) {
return p.txnId == a.pendingRoomKeyEvent.txnId;
});
if (it == r.pendingRoomKeyEvents.end()) {
r.pendingRoomKeyEvents = std::move(r.pendingRoomKeyEvents).push_back(a.pendingRoomKeyEvent);
} else {
r.pendingRoomKeyEvents = std::move(r.pendingRoomKeyEvents).set(it.index(), a.pendingRoomKeyEvent);
}
return r;
},
[&](RemovePendingRoomKeyAction a) {
auto it = std::find_if(r.pendingRoomKeyEvents.begin(), r.pendingRoomKeyEvents.end(), [a](const auto &desc) {
return desc.txnId == a.txnId;
});
if (it != r.pendingRoomKeyEvents.end()) {
r.pendingRoomKeyEvents = std::move(r.pendingRoomKeyEvents).erase(it.index());
}
return r;
},
[&](UpdateJoinedMemberCountAction a) {
r.joinedMemberCount = a.joinedMemberCount;
return r;
},
[&](UpdateInvitedMemberCountAction a) {
r.invitedMemberCount = a.invitedMemberCount;
return r;
},
[&](AddLocalNotificationsAction a) {
auto k = [r](const auto &id) {
return sortKeyForTimelineEvent(r.messages[id]);
};
auto readReceiptForCurrentUser = getMaxInTimeline(r.readReceipts[a.myUserId].eventId, r.localReadMarker, k);
auto newEventIds = intoImmer(
immer::flex_vector<std::string>{},
zug::map(&Event::id),
a.newEvents
);
auto needToAddPredicate = [a, r, k, readReceiptForCurrentUser](const auto &eid) {
if (!readReceiptForCurrentUser.empty()
&& k(readReceiptForCurrentUser) >= k(eid)) {
// this means this event is already read
return false;
}
auto e = r.messages[eid];
return e.sender() != a.myUserId
&& a.pushRulesDesc.handle(e, r).shouldNotify;
};
r.unreadNotificationEventIds = sortedUniqueMerge(std::move(r.unreadNotificationEventIds), newEventIds, [needToAddPredicate](const auto &e) { return !needToAddPredicate(e); }, k);
return r;
},
[&](RemoveReadLocalNotificationsAction a) {
auto k = [r](const auto &id) {
return sortKeyForTimelineEvent(r.messages[id]);
};
auto cmp = [k](const auto &a, const auto &b) {
return k(a) < k(b);
};
auto rr = getMaxInTimeline(
r.readReceipts[a.myUserId].eventId,
r.localReadMarker,
k
);
if (rr.empty()) {
return r;
}
auto it = std::upper_bound(
r.unreadNotificationEventIds.begin(),
r.unreadNotificationEventIds.end(),
rr,
cmp
);
// *it > rr, *(it - 1) <= rr (if it - 1 is valid)
if (it == r.unreadNotificationEventIds.end()) {
// If it == end(), it means everything is read
r.unreadNotificationEventIds = {};
} else if (it == r.unreadNotificationEventIds.begin()) {
// If it == begin(), it means everything is unread, so nothing to do
} else {
// it is somewhere in the middle, pointing to the first element that is unread
r.unreadNotificationEventIds = std::move(r.unreadNotificationEventIds).erase(0, it.index());
}
return r;
},
[&](UpdateLocalReadMarkerAction a) {
r.localReadMarker = a.localReadMarker;
auto next = RoomModel::update(std::move(r), RemoveReadLocalNotificationsAction{a.myUserId});
return next;
},
[&](PurgeEventsAction a) {
if (r.timeline.size() <= a.maxToKeep) {
return r;
}
auto numToDrop = r.timeline.size() - a.maxToKeep;
auto keepEvents = intoImmer(
EventList{},
zug::map([&r](const auto &eventId) {
return r.messages[eventId];
}),
std::move(r.timeline).drop(numToDrop)
);
auto origMessages = r.messages;
r.reverseEventRelationships = {};
r.timeline = {};
r.messages = {};
r.undecryptedEvents = {};
auto msgs = intoImmer(
r.localReadMarker.empty() ? EventList{} : EventList{origMessages[r.localReadMarker]},
zug::filter([l=r.localReadMarker](const auto &eventId) {
return eventId != l;
})
| zug::map([&origMessages](const auto &eventId) {
return origMessages[eventId];
}),
r.unreadNotificationEventIds
);
auto next = update(std::move(r), AddMessagesAction{msgs});
return update(std::move(next), AddToTimelineAction{
keepEvents,
std::nullopt,
std::nullopt,
std::nullopt,
});
}
);
}
RoomListModel RoomListModel::update(RoomListModel l, Action a)
{
return lager::match(std::move(a))(
[&](UpdateRoomAction a) {
l.rooms = std::move(l.rooms)
.update(a.roomId,
[=](RoomModel oldRoom) {
oldRoom.roomId = a.roomId; // in case it is a new room
return RoomModel::update(std::move(oldRoom), a.roomAction);
});
return l;
}
);
}
static auto membershipTransducer(const std::string &membership)
{
return zug::filter([](auto val) {
auto [k, v] = val;
auto [type, stateKey] = k;
return type == "m.room.member"s;
})
| zug::map([](auto val) {
auto [k, v] = val;
auto [type, stateKey] = k;
return std::pair<std::string, Kazv::Event>{stateKey, v};
})
| zug::filter([&membership](auto val) {
auto [stateKey, ev] = val;
return ev.content().get()
.at("membership"s) == membership;
});
}
static auto memberIdsByMembership(immer::map<KeyOfState, Event> stateEvents, const std::string &membership)
{
return intoImmer(
immer::flex_vector<std::string>{},
membershipTransducer(membership)
| zug::map([](auto val) {
auto [stateKey, ev] = val;
return stateKey;
}),
stateEvents);
}
auto memberEventsByMembership(immer::map<KeyOfState, Event> stateEvents, const std::string &membership)
{
return intoImmer(
EventList{},
membershipTransducer(membership)
| zug::map([](auto val) {
auto [stateKey, ev] = val;
return ev;
}),
stateEvents);
}
immer::flex_vector<std::string> RoomModel::joinedMemberIds() const
{
return memberIdsByMembership(stateEvents, "join"s);
}
immer::flex_vector<std::string> RoomModel::invitedMemberIds() const
{
return memberIdsByMembership(stateEvents, "invite"s);
}
immer::flex_vector<std::string> RoomModel::knockedMemberIds() const
{
return memberIdsByMembership(stateEvents, "knock"s);
}
immer::flex_vector<std::string> RoomModel::leftMemberIds() const
{
return memberIdsByMembership(stateEvents, "leave"s);
}
immer::flex_vector<std::string> RoomModel::bannedMemberIds() const
{
return memberIdsByMembership(stateEvents, "ban"s);
}
EventList RoomModel::joinedMemberEvents() const
{
return memberEventsByMembership(stateEvents, "join"s);
}
EventList RoomModel::invitedMemberEvents() const
{
return memberEventsByMembership(stateEvents, "invite"s);
}
EventList RoomModel::knockedMemberEvents() const
{
return memberEventsByMembership(stateEvents, "knock"s);
}
EventList RoomModel::leftMemberEvents() const
{
return memberEventsByMembership(stateEvents, "leave"s);
}
EventList RoomModel::bannedMemberEvents() const
{
return memberEventsByMembership(stateEvents, "ban"s);
}
EventList RoomModel::heroMemberEvents() const
{
return intoImmer(
EventList{},
zug::filter([heroIds=heroIds](auto val) {
auto [k, ev] = val;
auto [type, stateKey] = k;
return type == "m.room.member"s &&
std::find(heroIds.begin(), heroIds.end(), stateKey) != heroIds.end();
})
| zug::map([](auto val) {
auto [_, ev] = val;
return ev;
}),
stateEvents);
}
static Timestamp defaultRotateMs = 604800000;
static int defaultRotateMsgs = 100;
MegOlmSessionRotateDesc RoomModel::sessionRotateDesc() const
{
auto k = KeyOfState{"m.room.encryption", ""};
auto content = stateEvents[k].content().get();
auto ms = content.contains("rotation_period_ms")
? content["rotation_period_ms"].get<Timestamp>()
: defaultRotateMs;
auto msgs = content.contains("rotation_period_msgs")
? content["rotation_period_msgs"].get<int>()
: defaultRotateMsgs;
return MegOlmSessionRotateDesc{ ms, msgs };
}
bool RoomModel::hasUser(std::string userId) const
{
try {
auto ev = stateEvents.at(KeyOfState{"m.room.member", userId});
if (ev.content().get().at("membership") == "join") {
return true;
}
} catch (const std::exception &) {
return false;
}
return false;
}
std::optional<LocalEchoDesc> RoomModel::getLocalEchoByTxnId(std::string txnId) const
{
auto it = std::find_if(localEchoes.begin(), localEchoes.end(), [txnId](const auto &desc) {
return txnId == desc.txnId;
});
if (it != localEchoes.end()) {
return *it;
} else {
return std::nullopt;
}
}
std::optional<PendingRoomKeyEvent> RoomModel::getPendingRoomKeyEventByTxnId(std::string txnId) const
{
auto it = std::find_if(pendingRoomKeyEvents.begin(), pendingRoomKeyEvents.end(), [txnId](const auto &desc) {
return txnId == desc.txnId;
});
if (it != pendingRoomKeyEvents.end()) {
return *it;
} else {
return std::nullopt;
}
}
static double getTagOrder(const json &tag)
{
// https://spec.matrix.org/v1.7/client-server-api/#events-12
// If a room has a tag without an order key then it should appear after the rooms with that tag that have an order key.
return tag.contains("order") && tag["order"].is_number()
? tag["order"].template get<double>()
: ROOM_TAG_DEFAULT_ORDER;
}
immer::map<std::string, double> RoomModel::tags() const
{
auto content = accountData["m.tag"].content().get();
if (!content.contains("tags") || !content["tags"].is_object()) {
return {};
}
auto tagsObject = content["tags"];
auto tagsItems = tagsObject.items();
return std::accumulate(tagsItems.begin(), tagsItems.end(), immer::map<std::string, double>(),
[=](auto acc, const auto &cur) {
auto [id, tag] = cur;
return std::move(acc).set(id, getTagOrder(tag));
}
);
}
static auto normalizeTagEventJson(Event e)
{
auto content = e.content().get();
if (!content.contains("tags") || !content["tags"].is_object()) {
content["tags"] = json::object();
}
return json{
{"content", content},
{"type", "m.tag"},
};
}
Event RoomModel::makeAddTagEvent(std::string tagId, std::optional<double> order) const
{
auto eventJson = normalizeTagEventJson(accountData["m.tag"]);
auto tag = json::object();
if (order.has_value()) {
tag["order"] = order.value();
}
eventJson["content"]["tags"][tagId] = tag;
return Event(eventJson);
}
Event RoomModel::makeRemoveTagEvent(std::string tagId) const
{
auto eventJson = normalizeTagEventJson(accountData["m.tag"]);
eventJson["content"]["tags"].erase(tagId);
return Event(eventJson);
}
void RoomModel::generateRelationships(EventList newEvents)
{
for (const auto &event: newEvents) {
auto [relType, eventId] = event.relationship();
if (!relType.empty()) {
reverseEventRelationships = updateIn(std::move(reverseEventRelationships), [event](auto &&evs) {
return evs.push_back(event.id());
}, eventId, relType);
}
}
}
void RoomModel::regenerateRelationships()
{
generateRelationships(intoImmer(EventList{}, zug::map([](const auto &kv) {
return kv.second;
}), messages));
}
void RoomModel::addToUndecryptedEvents(EventList newEvents)
{
if (!encrypted) {
return;
}
for (auto event : newEvents) {
if (event.encrypted() && !event.decrypted()) {
auto original = event.originalJson();
const auto &o = original.get();
if (o.contains("content")
&& o["content"].contains("session_id")
&& o["content"]["session_id"].is_string()
) {
auto sessionId = o["content"]["session_id"].template get<std::string>();
undecryptedEvents = std::move(undecryptedEvents)
.update(sessionId, [id=event.id()](const auto &v) {
return v.push_back(id);
});
}
}
}
}
void RoomModel::recalculateUndecryptedEvents()
{
if (!encrypted) {
return;
}
addToUndecryptedEvents(intoImmer(EventList{}, zug::map([](const auto &kv) {
return kv.second;
}), messages));
}
bool RoomModel::checkInvariants() const
{
auto inMessages = [this](const std::string &eventId) {
return !!messages.count(eventId);
};
return immer::all_of(timeline, inMessages)
&& immer::all_of(unreadNotificationEventIds, inMessages)
&& std::all_of(undecryptedEvents.begin(), undecryptedEvents.end(), [&inMessages](const auto &p) {
return immer::all_of(p.second, inMessages);
})
&& (localReadMarker.empty() || messages.count(localReadMarker))
&& std::all_of(reverseEventRelationships.begin(), reverseEventRelationships.end(), [&inMessages](const auto &p) {
const auto &relTypeToEventsMap = p.second;
return std::all_of(relTypeToEventsMap.begin(), relTypeToEventsMap.end(), [&inMessages](const auto &p2) {
const auto &events = p2.second;
return immer::all_of(events, inMessages);
});
});
}
bool RoomModel::isInTimeline(const std::string &eventId) const
{
return !nonTimelineEvents.count(eventId) && messages.count(eventId);
}
bool RoomModel::isReadBy(const std::string &eventId, const std::string &userId) const
{
auto readReceiptAt = messages[readReceipts[userId].eventId];
auto event = messages[eventId];
return sortKeyForTimelineEvent(readReceiptAt) >= sortKeyForTimelineEvent(event);
}
}
diff --git a/src/client/room/room-model.hpp b/src/client/room/room-model.hpp
index 8e14717..edd0b99 100644
--- a/src/client/room/room-model.hpp
+++ b/src/client/room/room-model.hpp
@@ -1,516 +1,554 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <string>
#include <variant>
#include <immer/flex_vector.hpp>
#include <immer/map.hpp>
#include <immer/set.hpp>
#include <serialization/immer-flex-vector.hpp>
#include <serialization/immer-box.hpp>
#include <serialization/immer-map.hpp>
#include <serialization/immer-set.hpp>
#include <serialization/immer-array.hpp>
#include <csapi/sync.hpp>
#include <event.hpp>
#include <crypto.hpp>
#include "push-rules-desc.hpp"
#include "local-echo.hpp"
#include "clientutil.hpp"
+#include "local-draft.hpp"
namespace Kazv
{
struct PendingRoomKeyEvent
{
std::string txnId;
immer::map<std::string, immer::map<std::string, Event>> messages;
friend bool operator==(const PendingRoomKeyEvent &a, const PendingRoomKeyEvent &b) = default;
friend bool operator!=(const PendingRoomKeyEvent &a, const PendingRoomKeyEvent &b) = default;
};
PendingRoomKeyEvent makePendingRoomKeyEventV0(std::string txnId, Event event, immer::map<std::string, immer::flex_vector<std::string>> devices);
struct ReadReceipt
{
std::string eventId;
Timestamp timestamp;
friend bool operator==(const ReadReceipt &a, const ReadReceipt &b) = default;
friend bool operator!=(const ReadReceipt &a, const ReadReceipt &b) = default;
};
template<class Archive>
void serialize(Archive &ar, ReadReceipt &r, std::uint32_t const /* version */)
{
ar & r.eventId & r.timestamp;
}
struct EventReader
{
std::string userId;
Timestamp timestamp;
friend bool operator==(const EventReader &a, const EventReader &b) = default;
friend bool operator!=(const EventReader &a, const EventReader &b) = default;
};
struct AddStateEventsAction
{
immer::flex_vector<Event> stateEvents;
};
/// Go from the back of stateEvents to the beginning,
/// adding the event to room state only if the room
/// has no state event with that state key.
struct MaybeAddStateEventsAction
{
immer::flex_vector<Event> stateEvents;
};
/// Add events to the messages map, but not the timeline.
/// Usually because their position in the timeline is not known.
struct AddMessagesAction
{
EventList events;
/// @internal only to be used by AddToTimelineAction
bool alsoInTimeline{false};
};
struct AddToTimelineAction
{
/// Events from oldest to latest
immer::flex_vector<Event> events;
std::optional<std::string> prevBatch;
std::optional<bool> limited;
std::optional<std::string> gapEventId;
};
struct AddAccountDataAction
{
immer::flex_vector<Event> events;
};
struct ChangeMembershipAction
{
RoomMembership membership;
};
struct ChangeInviteStateAction
{
immer::flex_vector<Event> events;
};
struct AddEphemeralAction
{
EventList events;
};
- struct SetLocalDraftAction
+ struct SetLocalDraftTextAction
{
std::string localDraft;
};
+ struct SetLocalDraftRelAction
+ {
+ std::string relType;
+ std::string relatedEventId;
+ };
+
+ struct SetLocalDraftAction
+ {
+ std::string text;
+ std::string relType;
+ std::string relatedEventId;
+ };
+
+ struct PushLocalDraftAction
+ {
+ std::string text;
+ std::string relType;
+ std::string relatedEventId;
+ };
+
+ struct PopLocalDraftAction
+ {
+ };
+
struct SetRoomEncryptionAction
{
};
struct MarkMembersFullyLoadedAction
{
};
struct SetHeroIdsAction
{
immer::flex_vector<std::string> heroIds;
};
struct AddLocalEchoAction
{
LocalEchoDesc localEcho;
};
struct RemoveLocalEchoAction
{
std::string txnId;
};
struct AddPendingRoomKeyAction
{
PendingRoomKeyEvent pendingRoomKeyEvent;
};
struct RemovePendingRoomKeyAction
{
std::string txnId;
};
struct UpdateJoinedMemberCountAction
{
std::size_t joinedMemberCount;
};
struct UpdateInvitedMemberCountAction
{
std::size_t invitedMemberCount;
};
/// Update local notifications to include the new events
///
/// Precondition: newEvents are already in room.messages
struct AddLocalNotificationsAction
{
EventList newEvents;
PushRulesDesc pushRulesDesc;
std::string myUserId;
};
/// Remove local notifications that are already read
struct RemoveReadLocalNotificationsAction
{
std::string myUserId;
};
/// Update the local read marker, removing any read notifications before it.
struct UpdateLocalReadMarkerAction
{
std::string localReadMarker;
std::string myUserId;
};
/// Remove events from the model, retaining only the latest `maxToKeep` events.
struct PurgeEventsAction
{
std::size_t maxToKeep;
};
inline const double ROOM_TAG_DEFAULT_ORDER = 2;
template<class Archive>
void serialize(Archive &ar, PendingRoomKeyEvent &e, std::uint32_t const version)
{
if (version < 1) {
// loading an older version where there is only one event
std::string txnId;
Event event;
immer::map<std::string, immer::flex_vector<std::string>> devices;
ar & txnId & event & devices;
e = makePendingRoomKeyEventV0(
std::move(txnId), std::move(event), std::move(devices));
} else {
ar & e.txnId & e.messages;
}
}
/**
* Get the sort key for a timeline event.
*
* If the key is larger, the event should be placed
* at the more recent end of the timeline.
*
* @param e The event to get the sort key for.
* @return The sort key. You MUST use `auto` to store the
* result.
*/
auto sortKeyForTimelineEvent(Event e) -> std::tuple<Timestamp, std::string>;
/**
* The model to store information about a room.
*
* Room invariants:
* Any event in timeline is in messages.
* Any event in undecryptedEvents is in messages.
* Any event in unreadNotificationEventIds is in messages.
* Any relater (i.e. child) event in reverseEventRelationships is in messages.
* localReadMarker (if not empty) is in messages.
*/
struct RoomModel
{
using Membership = RoomMembership;
using ReverseEventRelationshipMap = immer::map<
std::string /* related event id */,
immer::map<std::string /* relation type */, immer::flex_vector<std::string /* relater event id */>>>;
std::string roomId;
immer::map<KeyOfState, Event> stateEvents;
immer::map<KeyOfState, Event> inviteState;
// Smaller indices mean earlier events
// (oldest) 0 --------> n (latest)
immer::flex_vector<std::string> timeline;
immer::map<std::string, Event> messages;
immer::map<std::string, Event> accountData;
Membership membership{};
std::string paginateBackToken;
/// whether this room has earlier events to be fetched
bool canPaginateBack{true};
immer::map<std::string /* eventId */, std::string /* prevBatch */> timelineGaps;
immer::map<std::string, Event> ephemeral;
- std::string localDraft;
+ immer::flex_vector<LocalDraft> localDrafts{
+ Kazv::LocalDraft{"", "", ""}
+ };
bool encrypted{false};
/// a marker to indicate whether we need to rotate
/// the session key earlier than it expires
/// (e.g. when a user in the room's device list changed
/// or when someone joins or leaves)
bool shouldRotateSessionKey{true};
bool membersFullyLoaded{false};
immer::flex_vector<std::string> heroIds;
immer::flex_vector<LocalEchoDesc> localEchoes;
immer::flex_vector<PendingRoomKeyEvent> pendingRoomKeyEvents;
ReverseEventRelationshipMap reverseEventRelationships;
std::size_t joinedMemberCount{0};
std::size_t invitedMemberCount{0};
/// The local read marker for this room. Indicates that
/// you have read up to this event.
std::string localReadMarker;
/// The local unread count for this room.
std::size_t localUnreadCount{0};
/// The local unread notification count for this room.
/// XXX this is never used.
std::size_t localNotificationCount{0};
/// Read receipts for all users
immer::map<std::string /* userId */, ReadReceipt> readReceipts;
/// A map from event id to a list of users that has read
/// receipt at that point
immer::map<
std::string /* eventId */,
immer::flex_vector<std::string /* userId */>> eventReadUsers;
/// A map from the session id to a list of event ids of events
/// that cannot (yet) be decrypted.
immer::map<
std::string /* sessionId */,
immer::flex_vector<std::string /* eventId */>> undecryptedEvents;
immer::flex_vector<std::string> unreadNotificationEventIds;
/// The set of event ids that are in `messages` but not in `timeline`.
/// The rationale is that non-timeline events are sparse, so
/// instead of recording events that are in the timeline, we
/// record those not in the timeline.
immer::set<std::string> nonTimelineEvents;
immer::flex_vector<std::string> joinedMemberIds() const;
immer::flex_vector<std::string> invitedMemberIds() const;
immer::flex_vector<std::string> knockedMemberIds() const;
immer::flex_vector<std::string> leftMemberIds() const;
immer::flex_vector<std::string> bannedMemberIds() const;
EventList joinedMemberEvents() const;
EventList invitedMemberEvents() const;
EventList knockedMemberEvents() const;
EventList leftMemberEvents() const;
EventList bannedMemberEvents() const;
EventList heroMemberEvents() const;
MegOlmSessionRotateDesc sessionRotateDesc() const;
bool hasUser(std::string userId) const;
std::optional<LocalEchoDesc> getLocalEchoByTxnId(std::string txnId) const;
std::optional<PendingRoomKeyEvent> getPendingRoomKeyEventByTxnId(std::string txnId) const;
immer::map<std::string, double> tags() const;
Event makeAddTagEvent(std::string tagId, std::optional<double> order) const;
Event makeRemoveTagEvent(std::string tagId) const;
/**
* Fill in reverseEventRelationships by gathering
* the relationships specified in `newEvents`
*
* @param newEvents The events that just came in after last time event relationships
* are gathered.
*/
void generateRelationships(EventList newEvents);
void regenerateRelationships();
/**
* Fill in undecryptedEvents by gathering
* the session ids specified in `newEvents`.
*
* @param newEvents New incoming events.
*/
void addToUndecryptedEvents(EventList newEvents);
void recalculateUndecryptedEvents();
/**
* Check if the invariants in the model are satisfied.
* @return true iff the invariants are satisfied.
*/
bool checkInvariants() const;
/**
* Check if the event is in the timeline.
*
* This function takes constant time.
* @param eventId The id of the event to check.
* @return true iff the event is in the timeline.
*/
bool isInTimeline(const std::string &eventId) const;
/**
* Check if an event has been read by the given user
*
* @param eventId eventId of the event to check
* @param userId userId of the given user
* @return true iff the event is read by the user
*/
bool isReadBy(const std::string &eventId, const std::string &userId) const;
using Action = std::variant<
AddStateEventsAction,
MaybeAddStateEventsAction,
AddMessagesAction,
AddToTimelineAction,
AddAccountDataAction,
ChangeMembershipAction,
ChangeInviteStateAction,
AddEphemeralAction,
+ SetLocalDraftTextAction,
+ SetLocalDraftRelAction,
SetLocalDraftAction,
+ PushLocalDraftAction,
+ PopLocalDraftAction,
SetRoomEncryptionAction,
MarkMembersFullyLoadedAction,
SetHeroIdsAction,
AddLocalEchoAction,
RemoveLocalEchoAction,
AddPendingRoomKeyAction,
RemovePendingRoomKeyAction,
UpdateJoinedMemberCountAction,
UpdateInvitedMemberCountAction,
AddLocalNotificationsAction,
RemoveReadLocalNotificationsAction,
UpdateLocalReadMarkerAction,
PurgeEventsAction
>;
static RoomModel update(RoomModel r, Action a);
friend bool operator==(const RoomModel &a, const RoomModel &b) = default;
};
using RoomAction = RoomModel::Action;
struct UpdateRoomAction
{
std::string roomId;
RoomAction roomAction;
};
struct RoomListModel
{
immer::map<std::string, RoomModel> rooms;
inline auto at(std::string id) const { return rooms.at(id); }
inline auto operator[](std::string id) const { return rooms[id]; }
inline bool has(std::string id) const { return rooms.find(id); }
using Action = std::variant<
UpdateRoomAction
>;
static RoomListModel update(RoomListModel l, Action a);
friend bool operator==(const RoomListModel &a, const RoomListModel &b) = default;
};
using RoomListAction = RoomListModel::Action;
template<class Archive>
void serialize(Archive &ar, RoomModel &r, std::uint32_t const version)
{
ar
& r.roomId
& r.stateEvents
& r.inviteState
& r.timeline
& r.messages
& r.accountData
& r.membership
& r.paginateBackToken
& r.canPaginateBack
& r.timelineGaps
& r.ephemeral
-
- & r.localDraft
-
+ ;
+ if (version < 10) {
+ std::string oldVerLocalDraft;
+ ar & oldVerLocalDraft;
+ r.localDrafts = {LocalDraft{oldVerLocalDraft, "", ""}};
+ }
+ ar
& r.encrypted
& r.shouldRotateSessionKey
& r.membersFullyLoaded
;
if (version >= 1) {
ar
& r.heroIds
;
}
if (version >= 2) {
ar & r.localEchoes;
}
if (version >= 3) {
ar & r.pendingRoomKeyEvents;
}
if (version >= 4) {
ar & r.reverseEventRelationships;
} else { // must be reading from an older version
if constexpr (typename Archive::is_loading()) {
r.regenerateRelationships();
}
}
if (version >= 5) {
ar & r.joinedMemberCount & r.invitedMemberCount;
}
if (version >= 6) {
ar
& r.localReadMarker
& r.localUnreadCount
& r.localNotificationCount
& r.readReceipts
& r.eventReadUsers;
}
if (version >= 7) {
ar & r.undecryptedEvents;
} else {
if constexpr (typename Archive::is_loading()) {
r.recalculateUndecryptedEvents();
}
}
if (version >= 8) {
ar & r.unreadNotificationEventIds;
}
if (version >= 9) {
ar & r.nonTimelineEvents;
}
+ if (version >= 10) {
+ ar & r.localDrafts;
+ }
}
template<class Archive>
void serialize(Archive &ar, RoomListModel &l, std::uint32_t const /*version*/)
{
ar & l.rooms;
}
}
BOOST_CLASS_VERSION(Kazv::PendingRoomKeyEvent, 1)
BOOST_CLASS_VERSION(Kazv::ReadReceipt, 0)
-BOOST_CLASS_VERSION(Kazv::RoomModel, 9)
+BOOST_CLASS_VERSION(Kazv::RoomModel, 10)
BOOST_CLASS_VERSION(Kazv::RoomListModel, 0)
diff --git a/src/client/room/room.cpp b/src/client/room/room.cpp
index ef2d02d..9cd653d 100644
--- a/src/client/room/room.cpp
+++ b/src/client/room/room.cpp
@@ -1,952 +1,986 @@
/*
* 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 <zug/into_vector.hpp>
#include <immer/algorithm.hpp>
#include <debug.hpp>
#include "room.hpp"
+#include "sdk-model-cursor-tag.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::localEcho(lager::reader<std::string> txnId) const -> lager::reader<LocalEchoDesc>
{
return lager::with(roomCursor()[&RoomModel::localEchoes], txnId)
.map([](const auto localEchoes, const auto &id) {
auto localEchoIt = std::find_if(
localEchoes.begin(), localEchoes.end(),
[id](const auto localEcho) {
return localEcho.txnId == id;
});
if (localEchoIt == localEchoes.end()) {
return LocalEchoDesc {};
}
return *localEchoIt;
});
}
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;
return roomCursor().map(&RoomModel::heroMemberEvents);
}
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::localReadMarker() const
-> lager::reader<std::string>
{
return roomCursor()[&RoomModel::localReadMarker];
}
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}});
+ return m_ctx.dispatch(UpdateRoomAction{
+ +roomId(), SetLocalDraftTextAction{localDraft}});
+ }
+
+ auto Room::setLocalDraft(std::string text,
+ std::string relType, std::string relatedEventId) const -> PromiseT
+ {
+ using namespace CursorOp;
+ return m_ctx.dispatch(UpdateRoomAction(
+ +roomId(), SetLocalDraftAction{text, relType, relatedEventId}
+ ));
+ }
+
+ auto Room::setLocalDraftRel(std::string relType,
+ std::string relatedEventId) const -> PromiseT
+ {
+ using namespace CursorOp;
+ return m_ctx.dispatch(UpdateRoomAction(
+ +roomId(), SetLocalDraftRelAction{relType, relatedEventId}));
+ }
+
+ auto Room::pushLocalDraft(std::string text,
+ std::string relType, std::string relatedEventId) const -> PromiseT
+ {
+ using namespace CursorOp;
+ return m_ctx.dispatch(UpdateRoomAction{+roomId(), PushLocalDraftAction{
+ text, relType, relatedEventId}});
+ }
+
+ auto Room::popLocalDraft() const -> PromiseT
+ {
+ using namespace CursorOp;
+ return m_ctx.dispatch(
+ UpdateRoomAction{+roomId(), PopLocalDraftAction{}});
}
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>(PrepareForSharingRoomKeyAction::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();
return ctx.dispatch(SendMultipleToDeviceMessagesAction{pending.messages})
.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::pinnedEvents() const -> lager::reader<immer::flex_vector<std::string>>
{
return state(KeyOfState{"m.room.pinned_events", ""})
.xform(eventContent | jsonAtOr("pinned", immer::flex_vector<std::string>{}));
}
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::pinEvents(immer::flex_vector<std::string> eventIds) const -> PromiseT
{
return m_ctx.createResolvedPromise({})
.then([room=toEventLoop(), eventIds](auto) {
auto curPinnedEvents = room.pinnedEvents().get();
auto newPinnedEvents = intoImmer(
curPinnedEvents,
zug::filter([curPinnedEvents](const auto &eventId) {
return std::find(curPinnedEvents.begin(), curPinnedEvents.end(), eventId) == curPinnedEvents.end();
}),
eventIds
);
return room.setPinnedEvents(newPinnedEvents);
});
}
auto Room::unpinEvents(immer::flex_vector<std::string> eventIds) const -> PromiseT
{
return m_ctx.createResolvedPromise({})
.then([room=toEventLoop(), eventIds](auto) {
auto curPinnedEvents = room.pinnedEvents().get();
auto newPinnedEvents = intoImmer(
immer::flex_vector<std::string>(),
zug::filter([eventIds](const auto &eventId) {
return std::find(eventIds.begin(), eventIds.end(), eventId) == eventIds.end();
}),
curPinnedEvents
);
return room.setPinnedEvents(newPinnedEvents);
});
}
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::removeLocalEcho(std::string txnId) const -> PromiseT
{
return m_ctx.dispatch(RoomListAction{UpdateRoomAction{
currentRoomId(),
RemoveLocalEchoAction{txnId},
}});
}
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);
});
}
auto Room::relatedEvents(lager::reader<std::string> eventId, std::string relType) const -> lager::reader<EventList>
{
return lager::with(
eventId,
roomCursor()[&RoomModel::reverseEventRelationships],
roomCursor()[&RoomModel::messages]
).map([relType](const auto &eid, const auto &rels, const auto &msgs) {
auto relatedEvents = zug::into_vector(
zug::map([msgs](const auto &id) {
return msgs[id];
}),
rels[eid][relType]
);
std::sort(relatedEvents.begin(), relatedEvents.end(), [](const Event &a, const Event &b) {
return std::make_tuple(a.originServerTs(), a.id()) < std::make_tuple(b.originServerTs(), b.id());
});
return EventList(relatedEvents.begin(), relatedEvents.end());
}).make();
}
auto Room::eventReaders(lager::reader<std::string> eventId) const -> lager::reader<immer::flex_vector<EventReader>>
{
return lager::with(
roomCursor()[&RoomModel::readReceipts],
roomCursor()[&RoomModel::eventReadUsers],
eventId
).map([](const auto &receipts, const auto &readUsers, const auto &eid) {
auto userIds = readUsers[eid];
return intoImmer(
immer::flex_vector<EventReader>{},
zug::map([receipts](const auto &userId) {
return EventReader{userId, receipts[userId].timestamp};
}),
userIds
);
});
}
auto Room::postReceipt(std::string eventId) const -> PromiseT
{
return m_ctx.dispatch(PostReceiptAction{roomId().make().get(), eventId});
}
auto Room::unreadNotificationEventIds() const -> lager::reader<immer::flex_vector<std::string>>
{
return m_roomCursor[&RoomModel::unreadNotificationEventIds];
}
bool Room::isReadBy(const std::string &eventId, const std::string &userId)
{
return m_roomCursor.get().isReadBy(eventId, userId);
}
}
diff --git a/src/client/room/room.hpp b/src/client/room/room.hpp
index d6bd3b7..75b1d68 100644
--- a/src/client/room/room.hpp
+++ b/src/client/room/room.hpp
@@ -1,843 +1,898 @@
/*
* 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];
}
/**
* 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 one local echo by the txnId.
*
* @param A lager::reader of the txnId.
*
* @return A lager::reader of the local echo.
*/
auto localEcho(lager::reader<std::string> txnId) const -> lager::reader<LocalEchoDesc>;
/**
* 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<immer::flex_vector<Kazv::LocalDraft>>*/
+ KAZV_WRAP_ATTR(RoomModel, roomCursor(), localDrafts);
/* lager::reader<bool> */
KAZV_WRAP_ATTR(RoomModel, roomCursor(), membersFullyLoaded);
/**
* Get the local read marker in this room.
*
* @return a lager::reader of a std::string of the local read marker.
*/
auto localReadMarker() const -> lager::reader<std::string>;
/**
* 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 .
+ * @c localDrafts() will contain @c localDrafts.
*
* @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;
+ /**
+ * Set current local draft and related event for this room.
+ *
+ * After the returned Promise is resolved,
+ * @c localDrafts() will contain @c localDrafts.
+ *
+ * @param text The local draft to send.
+ * @param relType Such as m.in_reply_to, refer to https://spec.matrix.org/
+ * @param relatedEventId The eventId related to the local draft.
+ * @return A Promise that resolves when the local draft
+ * has been set, or when there is an error.
+ */
+ PromiseT setLocalDraft(std::string text,
+ std::string relType, std::string relatedEventId) const;
+
+ /**
+ * Set event related to local draft for this room.
+ *
+ * After the returned Promise is resolved,
+ * @c localDrafts() will contain @c localDrafts.
+ *
+ * @param relType Such as m.in_reply_to, refer to https://spec.matrix.org/
+ * @param relatedEventId The eventId related to the local draft.
+ * @return A Promise that resolves when the event
+ * has been set, or when there is an error.
+ */
+ PromiseT setLocalDraftRel(std::string relType,
+ std::string relatedEventId) const;
+
+ /**
+ * Push a local draft and related event for this room.
+ *
+ * After the returned Promise is resolved,
+ * @c localDrafts() will contain @c localDrafts.
+ *
+ * @param text The local draft to send.
+ * @param relType Such as m.in_reply_to, refer to https://spec.matrix.org/
+ * @param relatedEventId The eventId related to the local draft.
+ * @return A Promise that resolves when the local draft
+ * has been set, or when there is an error.
+ */
+ PromiseT pushLocalDraft(std::string text,
+ std::string relType, std::string relatedEventId) const;
+
+ /**
+ * Pop a Kazv::LocalDraft in RoomModel::localDrafts.
+ *
+ * After the returned Promise is resolved,
+ * @c localDrafts() will contain @c localDrafts.
+ *
+ * @return A Promise that resolves when the local drafts
+ * has been popped, or when there is an error.
+ */
+ PromiseT popLocalDraft() 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);
}
/**
* Get pinned events of this room
*
* @return A lager::reader of RangeT of the pinned event ids.
*/
auto pinnedEvents() const -> lager::reader<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;
/**
* Add eventIds to the pinned events of this room
*
* @param eventIds The ids of events you want to add to the 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 pinEvents(immer::flex_vector<std::string> eventIds) const;
/**
* Remove eventIds from the pinned events of this room
*
* @param eventIds The ids of events you want to remove from the 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 unpinEvents(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>>;
/**
* Remove a local echo from this room.
*
* @param txnId The transaction id associated with that local echo.
*
* @return A Promise that resolves when the local echo is removed.
*/
PromiseT removeLocalEcho(std::string txnId) const;
/**
* 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>;
/**
* Get a list of child events of a specified event.
*
* @param eventId The id of the event.
* @param relType The type of the relationship.
* @return A lager::reader of a RangeT of events that are related to the event via `relType`. It is sorted in the same order as the timeline.
*/
auto relatedEvents(lager::reader<std::string> eventId, std::string relType) const -> lager::reader<EventList>;
/**
* Get a list of read receipts of some event in this room.
*
* @param eventId The id of the event.
* @return A lager::reader of a RangeT of matrix id and timestamp of receipts of the event.
*/
auto eventReaders(lager::reader<std::string> eventId) const -> lager::reader<immer::flex_vector<EventReader>>;
/**
* Post a read receipt for this room.
*
* @param eventId The event id the user has read up to.
*
* @return A Promise that resolves when the read receipt is sent.
*/
PromiseT postReceipt(std::string eventId) const;
/**
* Get a list of event ids of unread notifications.
*
* @return A lager::reader of a RangeT of event ids that
* has an unread notification.
*/
auto unreadNotificationEventIds() const -> lager::reader<immer::flex_vector<std::string>>;
/**
* Check if an event has been read by the given user
*
* @param eventId eventId of the event to check
* @param userId userId of the given user
* @return true iff the event is read by the user
*/
bool isReadBy(const std::string &eventId, const std::string &userId);
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/CMakeLists.txt b/src/tests/CMakeLists.txt
index f90e5dd..cd9fead 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -1,133 +1,134 @@
include(CTest)
set(KAZVTEST_RESPATH ${CMAKE_CURRENT_SOURCE_DIR}/resources)
configure_file(kazvtest-respath.hpp.in kazvtest-respath.hpp)
function(libkazv_add_tests)
set(options "")
set(oneValueArgs "")
set(multiValueArgs EXTRA_LINK_LIBRARIES EXTRA_INCLUDE_DIRECTORIES)
cmake_parse_arguments(PARSE_ARGV 0 libkazv_add_tests "${options}" "${oneValueArgs}" "${multiValueArgs}")
foreach(test_source ${libkazv_add_tests_UNPARSED_ARGUMENTS})
string(REGEX REPLACE "\\.cpp$" "" test_executable "${test_source}")
string(REGEX REPLACE "/|\\\\" "--" test_executable "${test_executable}")
message(STATUS "Test ${test_executable} added")
add_executable("${test_executable}" "${test_source}")
target_link_libraries("${test_executable}"
PRIVATE Catch2::Catch2WithMain
Threads::Threads
${libkazv_add_tests_EXTRA_LINK_LIBRARIES}
)
target_include_directories(
"${test_executable}"
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
${libkazv_add_tests_EXTRA_INCLUDE_DIRECTORIES}
)
target_compile_definitions("${test_executable}" PRIVATE CATCH_CONFIG_ENABLE_ALL_STRINGMAKERS)
add_test(NAME "${test_executable}" COMMAND "${test_executable}" "--allow-running-no-tests" "~[needs-internet]")
endforeach()
endfunction()
libkazv_add_tests(
event-test.cpp
cursorutiltest.cpp
base/serialization-test.cpp
base/types-test.cpp
base/immer-utils-test.cpp
base/json-utils-test.cpp
EXTRA_LINK_LIBRARIES kazvbase
)
add_library(client-test-lib SHARED client/client-test-util.cpp)
target_link_libraries(client-test-lib PUBLIC kazvjob kazvclient)
libkazv_add_tests(
client/discovery-test.cpp
client/sync-test.cpp
client/content-test.cpp
client/paginate-test.cpp
client/storage-actions-test.cpp
client/util-test.cpp
client/serialization-test.cpp
client/encrypted-file-test.cpp
client/sdk-test.cpp
client/thread-safety-test.cpp
client/room-test.cpp
client/random-generator-test.cpp
client/profile-test.cpp
client/kick-test.cpp
client/ban-test.cpp
client/join-test.cpp
client/keys-test.cpp
client/device-ops-test.cpp
client/send-test.cpp
client/encryption-test.cpp
client/redact-test.cpp
client/tagging-test.cpp
client/account-data-test.cpp
client/room/room-actions-test.cpp
client/room/local-echo-test.cpp
client/room/event-relationships-test.cpp
client/room/member-membership-test.cpp
client/room/purge-test.cpp
client/push-rules-desc-test.cpp
client/notification-handler-test.cpp
client/validator-test.cpp
client/power-levels-desc-test.cpp
client/client-test.cpp
client/create-room-test.cpp
client/device-list-tracker-test.cpp
client/device-list-tracker-benchmark-test.cpp
client/room/read-receipt-test.cpp
client/room/undecrypted-events-test.cpp
client/encryption-benchmark-test.cpp
client/login-test.cpp
client/logout-test.cpp
client/room/pinned-events-test.cpp
client/get-versions-test.cpp
client/alias-test.cpp
client/encode-test.cpp
client/maybe-add-save-events-trigger-benchmark-test.cpp
client/verification-processing-test.cpp
+ client/room/local-draft.cpp
EXTRA_LINK_LIBRARIES kazvclient kazveventemitter kazvjob client-test-lib kazvtestfixtures
EXTRA_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/client
)
libkazv_add_tests(
basejobtest.cpp
kazvjobtest.cpp
file-desc-test.cpp
EXTRA_LINK_LIBRARIES kazvbase kazvjob
)
libkazv_add_tests(
promise-test.cpp
EXTRA_LINK_LIBRARIES kazvbase kazvjob kazvstore
)
libkazv_add_tests(
event-emitter-test.cpp
EXTRA_LINK_LIBRARIES kazvbase kazveventemitter
)
libkazv_add_tests(
crypto-test.cpp
crypto/inbound-group-session-test.cpp
crypto/outbound-group-session-test.cpp
crypto/session-test.cpp
crypto/key-export-test.cpp
crypto/verification-process-test.cpp
crypto/verification-utils-test.cpp
crypto/verification-tracker-test.cpp
EXTRA_LINK_LIBRARIES kazvcrypto
)
libkazv_add_tests(
store-test.cpp
EXTRA_LINK_LIBRARIES kazvstore kazvjob
)
diff --git a/src/tests/client/room/local-draft.cpp b/src/tests/client/room/local-draft.cpp
new file mode 100644
index 0000000..91a835e
--- /dev/null
+++ b/src/tests/client/room/local-draft.cpp
@@ -0,0 +1,268 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2026 nannanko <nannanko@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+
+#include "client/action-mock-utils.hpp"
+
+#include <promise-interface.hpp>
+#include <context.hpp>
+#include <asio-promise-handler.hpp>
+#include <cprjobhandler.hpp>
+#include <lagerstoreeventemitter.hpp>
+#include <factory.hpp>
+#include <sdk.hpp>
+#include <sdk-model.hpp>
+#include <client-model.hpp>
+#include <room/room-model.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+#include <lager/event_loop/boost_asio.hpp>
+#include <boost/asio/io_context.hpp>
+
+#include <variant>
+
+TEST_CASE("Room::setLocalDraft(std::string)", "[client][room]")
+{
+ boost::asio::io_context io;
+ Kazv::SingleTypePromiseInterface<Kazv::EffectStatus> sgph{
+ Kazv::AsioPromiseHandler{io.get_executor()}
+ };
+
+ auto roomModel = Kazv::Factory::makeRoom();
+ auto clientModel = Kazv::Factory::makeClient(
+ Kazv::Factory::withRoom(roomModel));
+
+ auto jh = Kazv::CprJobHandler{io.get_executor()};
+ auto ee = Kazv::LagerStoreEventEmitter(
+ lager::with_boost_asio_event_loop{io.get_executor()});
+
+ auto sdk = Kazv::makeSdk(Kazv::SdkModel{clientModel}, jh, ee,
+ Kazv::AsioPromiseHandler{io.get_executor()}, zug::identity);
+ auto ctx = sdk.context();
+ auto dispatcher = getMockDispatcher(
+ sgph,
+ ctx,
+ passDown<Kazv::RoomListAction>()
+ );
+ auto mockContext = getMockContext(sgph, dispatcher);
+ auto client = Kazv::Client(Kazv::Client::InEventLoopTag{},
+ mockContext, sdk.context());
+ auto room = client.room(roomModel.roomId);
+
+ room.setLocalDraft("some draft")
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcher.template calledTimes<
+ Kazv::RoomListAction>() == 1);
+ auto action = dispatcher.template of<Kazv::RoomListAction>()[0];
+ REQUIRE(std::holds_alternative<Kazv::UpdateRoomAction>(action));
+ auto updateRoomAction = std::get<Kazv::UpdateRoomAction>(action);
+ REQUIRE(std::holds_alternative<Kazv::SetLocalDraftTextAction>(
+ updateRoomAction.roomAction));
+ REQUIRE(updateRoomAction.roomId == roomModel.roomId);
+ auto setLocalDraftTextAction = std::get<
+ Kazv::SetLocalDraftTextAction>(updateRoomAction.roomAction);
+ REQUIRE(setLocalDraftTextAction.localDraft == "some draft");
+ io.stop();
+ });
+
+ io.run();
+}
+
+TEST_CASE("Room::setLocalDraft(std::string, std::string, std::string)", "[client][room]")
+{
+ boost::asio::io_context io;
+ Kazv::SingleTypePromiseInterface<Kazv::EffectStatus> sgph{
+ Kazv::AsioPromiseHandler{io.get_executor()}
+ };
+
+ auto roomModel = Kazv::Factory::makeRoom();
+ auto clientModel = Kazv::Factory::makeClient(
+ Kazv::Factory::withRoom(roomModel));
+
+ auto jh = Kazv::CprJobHandler{io.get_executor()};
+ auto ee = Kazv::LagerStoreEventEmitter(
+ lager::with_boost_asio_event_loop{io.get_executor()});
+
+ auto sdk = Kazv::makeSdk(Kazv::SdkModel{clientModel}, jh, ee,
+ Kazv::AsioPromiseHandler{io.get_executor()}, zug::identity);
+ auto ctx = sdk.context();
+ auto dispatcher = getMockDispatcher(
+ sgph,
+ ctx,
+ passDown<Kazv::RoomListAction>()
+ );
+ auto mockContext = getMockContext(sgph, dispatcher);
+ auto client = Kazv::Client(Kazv::Client::InEventLoopTag{},
+ mockContext, sdk.context());
+ auto room = client.room(roomModel.roomId);
+
+ room.setLocalDraft("some draft", "m.reply", "$0")
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcher.template calledTimes<
+ Kazv::RoomListAction>() == 1);
+ auto action = dispatcher.template of<Kazv::RoomListAction>()[0];
+ REQUIRE(std::holds_alternative<Kazv::UpdateRoomAction>(action));
+ auto updateRoomAction = std::get<Kazv::UpdateRoomAction>(action);
+ REQUIRE(updateRoomAction.roomId == roomModel.roomId);
+ REQUIRE(std::holds_alternative<Kazv::SetLocalDraftAction>(
+ updateRoomAction.roomAction));
+ auto setLocalDraftAction = std::get<
+ Kazv::SetLocalDraftAction>(updateRoomAction.roomAction);
+ REQUIRE(setLocalDraftAction.text == "some draft");
+ REQUIRE(setLocalDraftAction.relType == "m.reply");
+ REQUIRE(setLocalDraftAction.relatedEventId == "$0");
+ io.stop();
+ });
+
+ io.run();
+}
+
+TEST_CASE("Room::setLocalDraftRel(std::string, std::string)", "[client][room]")
+{
+ boost::asio::io_context io;
+ Kazv::SingleTypePromiseInterface<Kazv::EffectStatus> sgph{
+ Kazv::AsioPromiseHandler{io.get_executor()}
+ };
+
+ auto roomModel = Kazv::Factory::makeRoom();
+ auto clientModel = Kazv::Factory::makeClient(
+ Kazv::Factory::withRoom(roomModel));
+
+ auto jh = Kazv::CprJobHandler{io.get_executor()};
+ auto ee = Kazv::LagerStoreEventEmitter(
+ lager::with_boost_asio_event_loop{io.get_executor()});
+
+ auto sdk = Kazv::makeSdk(Kazv::SdkModel{clientModel}, jh, ee,
+ Kazv::AsioPromiseHandler{io.get_executor()}, zug::identity);
+ auto ctx = sdk.context();
+ auto dispatcher = getMockDispatcher(
+ sgph,
+ ctx,
+ passDown<Kazv::RoomListAction>()
+ );
+ auto mockContext = getMockContext(sgph, dispatcher);
+ auto client = Kazv::Client(Kazv::Client::InEventLoopTag{},
+ mockContext, sdk.context());
+ auto room = client.room(roomModel.roomId);
+
+ room.setLocalDraftRel("m.reply", "$0")
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcher.template calledTimes<
+ Kazv::RoomListAction>() == 1);
+ auto action = dispatcher.template of<Kazv::RoomListAction>()[0];
+ REQUIRE(std::holds_alternative<Kazv::UpdateRoomAction>(action));
+ auto updateRoomAction = std::get<Kazv::UpdateRoomAction>(action);
+ REQUIRE(updateRoomAction.roomId == roomModel.roomId);
+ REQUIRE(std::holds_alternative<Kazv::SetLocalDraftRelAction>(
+ updateRoomAction.roomAction));
+ auto setLocalDraftRelAction = std::get<
+ Kazv::SetLocalDraftRelAction>(updateRoomAction.roomAction);
+ REQUIRE(setLocalDraftRelAction.relType == "m.reply");
+ REQUIRE(setLocalDraftRelAction.relatedEventId == "$0");
+ io.stop();
+ });
+
+ io.run();
+}
+
+TEST_CASE("Room::pushLocalDraft(std::string, std::string, std::string)", "[client][room]")
+{
+ boost::asio::io_context io;
+ Kazv::SingleTypePromiseInterface<Kazv::EffectStatus> sgph{
+ Kazv::AsioPromiseHandler{io.get_executor()}
+ };
+
+ auto roomModel = Kazv::Factory::makeRoom();
+ auto clientModel = Kazv::Factory::makeClient(
+ Kazv::Factory::withRoom(roomModel));
+
+ auto jh = Kazv::CprJobHandler{io.get_executor()};
+ auto ee = Kazv::LagerStoreEventEmitter(
+ lager::with_boost_asio_event_loop{io.get_executor()});
+
+ auto sdk = Kazv::makeSdk(Kazv::SdkModel{clientModel}, jh, ee,
+ Kazv::AsioPromiseHandler{io.get_executor()}, zug::identity);
+ auto ctx = sdk.context();
+ auto dispatcher = getMockDispatcher(
+ sgph,
+ ctx,
+ passDown<Kazv::RoomListAction>()
+ );
+ auto mockContext = getMockContext(sgph, dispatcher);
+ auto client = Kazv::Client(Kazv::Client::InEventLoopTag{},
+ mockContext, sdk.context());
+ auto room = client.room(roomModel.roomId);
+
+ room.pushLocalDraft("some draft", "m.reply", "$0")
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcher.template calledTimes<
+ Kazv::RoomListAction>() == 1);
+ auto action = dispatcher.template of<Kazv::RoomListAction>()[0];
+ REQUIRE(std::holds_alternative<Kazv::UpdateRoomAction>(action));
+ auto updateRoomAction = std::get<Kazv::UpdateRoomAction>(action);
+ REQUIRE(updateRoomAction.roomId == roomModel.roomId);
+ REQUIRE(std::holds_alternative<Kazv::PushLocalDraftAction>(
+ updateRoomAction.roomAction));
+ auto pushLocalDraftAction = std::get<
+ Kazv::PushLocalDraftAction>(updateRoomAction.roomAction);
+ REQUIRE(pushLocalDraftAction.text == "some draft");
+ REQUIRE(pushLocalDraftAction.relType == "m.reply");
+ REQUIRE(pushLocalDraftAction.relatedEventId == "$0");
+ io.stop();
+ });
+
+ io.run();
+}
+
+TEST_CASE("Room::popLocalDraft()", "[client][room]")
+{
+ boost::asio::io_context io;
+ Kazv::SingleTypePromiseInterface<Kazv::EffectStatus> sgph{
+ Kazv::AsioPromiseHandler{io.get_executor()}
+ };
+
+ auto roomModel = Kazv::Factory::makeRoom();
+ auto clientModel = Kazv::Factory::makeClient(
+ Kazv::Factory::withRoom(roomModel));
+
+ auto jh = Kazv::CprJobHandler{io.get_executor()};
+ auto ee = Kazv::LagerStoreEventEmitter(
+ lager::with_boost_asio_event_loop{io.get_executor()});
+
+ auto sdk = Kazv::makeSdk(Kazv::SdkModel{clientModel}, jh, ee,
+ Kazv::AsioPromiseHandler{io.get_executor()}, zug::identity);
+ auto ctx = sdk.context();
+ auto dispatcher = getMockDispatcher(
+ sgph,
+ ctx,
+ passDown<Kazv::RoomListAction>()
+ );
+ auto mockContext = getMockContext(sgph, dispatcher);
+ auto client = Kazv::Client(Kazv::Client::InEventLoopTag{},
+ mockContext, sdk.context());
+ auto room = client.room(roomModel.roomId);
+
+ room.popLocalDraft()
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ REQUIRE(dispatcher.template calledTimes<
+ Kazv::RoomListAction>() == 1);
+ auto action = dispatcher.template of<Kazv::RoomListAction>()[0];
+ REQUIRE(std::holds_alternative<Kazv::UpdateRoomAction>(action));
+ auto updateRoomAction = std::get<Kazv::UpdateRoomAction>(action);
+ REQUIRE(updateRoomAction.roomId == roomModel.roomId);
+ REQUIRE(std::holds_alternative<Kazv::PopLocalDraftAction>(
+ updateRoomAction.roomAction));
+ io.stop();
+ });
+
+ io.run();
+}
diff --git a/src/tests/client/room/room-actions-test.cpp b/src/tests/client/room/room-actions-test.cpp
index 8755326..c301f7b 100644
--- a/src/tests/client/room/room-actions-test.cpp
+++ b/src/tests/client/room/room-actions-test.cpp
@@ -1,143 +1,212 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <catch2/catch_all.hpp>
#include <room/room-model.hpp>
#include <testfixtures/factory.hpp>
using namespace Kazv;
using namespace Kazv::Factory;
TEST_CASE("SetHeroIdsAction", "[client][room]")
{
RoomModel r;
r.heroIds = immer::flex_vector<std::string>{"@u:test.org"};
auto newHeroes = immer::flex_vector<std::string>{"@a:test.org", "@b:test.org"};
WHEN("We update the model") {
auto res = RoomModel::update(r, SetHeroIdsAction{newHeroes});
THEN("it should update hero ids") {
REQUIRE(res.heroIds == newHeroes);
}
THEN("it should compare different than the original") {
REQUIRE(!(res == r));
}
}
}
TEST_CASE("AddToTimelineAction: redactions do not affect stateEvents", "[client][room]")
{
RoomModel r;
r.roomId = "!some-room-id:tusooa.xyz"s;
auto eventId = "$some-event-to-be-redacted"s;
auto userId = "@some:tusooa.xyz"s;
auto event = Event{json{
{"event_id", eventId},
{"room_id", r.roomId},
{"sender", userId},
{"type", "m.room.member"},
{"state_key", userId},
{"content", {
{"displayname", "some"},
{"membership", "join"},
}},
}};
r.messages = r.messages.set(eventId, event);
r.stateEvents = r.stateEvents.set({"m.room.member", userId}, event);
r.timeline = immer::flex_vector<std::string>{eventId};
auto redactEvent = Event{json{
{"event_id", "$some-other-event-id"},
{"room_id", r.roomId},
{"sender", userId},
{"type", "m.room.redaction"},
{"redacts", eventId},
{"content", {
{"reason", "lol"},
}},
}};
auto res = RoomModel::update(r, AddToTimelineAction{
{redactEvent},
/* prevBatch = */ std::nullopt,
/* limited = */ false,
/* gapEventId = */ std::nullopt
});
REQUIRE(res.timeline.size() == 2);
REQUIRE(res.messages[eventId].content().get() == json::object());
REQUIRE(res.messages[eventId].redacted());
REQUIRE(res.stateEvents[{"m.room.member", userId}] == event);
}
TEST_CASE("AddToTimelineAction: should handle redactions", "[client][room]")
{
RoomModel r;
r.roomId = "!some-room-id:tusooa.xyz"s;
auto eventId = "$some-event-to-be-redacted"s;
auto userId = "@some:tusooa.xyz"s;
auto event = Event{json{
{"event_id", eventId},
{"room_id", r.roomId},
{"sender", userId},
{"type", "m.room.message"},
{"content", {
{"msgtype", "m.text"},
{"body", "mew"},
}},
}};
r.messages = r.messages.set(eventId, event);
r.timeline = immer::flex_vector<std::string>{eventId};
auto redactEvent = Event{json{
{"event_id", "$some-other-event-id"},
{"room_id", r.roomId},
{"sender", userId},
{"type", "m.room.redaction"},
{"redacts", eventId},
{"content", {
{"reason", "lol"},
}},
}};
auto res = RoomModel::update(r, AddToTimelineAction{
{redactEvent},
/* prevBatch = */ std::nullopt,
/* limited = */ false,
/* gapEventId = */ std::nullopt
});
REQUIRE(res.timeline.size() == 2);
REQUIRE(res.messages[eventId].content().get() == json::object());
REQUIRE(res.messages[eventId].redacted());
}
TEST_CASE("MaybeAddStateEventsAction", "[client][room]")
{
auto r = makeRoom(withRoomState({
makeEvent(withEventType("m.room.member") | withStateKey("@foo:example.com") | withEventContent(json{{"membership", "join"}})),
}));
auto next = RoomModel::update(r, MaybeAddStateEventsAction{{
// already exists, should not add
makeEvent(withEventType("m.room.member") | withStateKey("@foo:example.com") | withEventContent(json{{"membership", "leave"}})),
// does not exist yet but the one below should be used
makeEvent(withEventType("m.room.member") | withStateKey("@bar:example.com") | withEventContent(json{{"membership", "leave"}})),
// does not exist, should add
makeEvent(withEventType("m.room.member") | withStateKey("@bar:example.com") | withEventContent(json{{"membership", "join"}})),
}});
REQUIRE(next.stateEvents.count(KeyOfState{"m.room.member", "@foo:example.com"}));
REQUIRE(next.stateEvents[KeyOfState{"m.room.member", "@foo:example.com"}].content().get()["membership"] == "join");
REQUIRE(next.stateEvents.count(KeyOfState{"m.room.member", "@bar:example.com"}));
REQUIRE(next.stateEvents[KeyOfState{"m.room.member", "@bar:example.com"}].content().get()["membership"] == "join");
}
+
+TEST_CASE("SetLocalDraftTextAction", "[client][room]")
+{
+ RoomModel r;
+
+ auto res = RoomModel::update(r, SetLocalDraftTextAction("some draft"));
+
+ REQUIRE(res.localDrafts.front().text == "some draft");
+}
+
+TEST_CASE("SetLocalDraftRelAction", "[client][room]")
+{
+ RoomModel r;
+
+ auto res = RoomModel::update(r,
+ SetLocalDraftRelAction("m.replace", "$eventid"));
+
+ REQUIRE(res.localDrafts.front().text == "");
+ REQUIRE(res.localDrafts.front().relType == "m.replace");
+ REQUIRE(res.localDrafts.front().relatedEventId == "$eventid");
+}
+
+TEST_CASE("SetLocalDraftAction", "[client][room]")
+{
+ RoomModel r;
+
+ auto res = RoomModel::update(r,
+ SetLocalDraftAction("some draft", "m.replace", "$eventid"));
+
+ REQUIRE(res.localDrafts.front().text == "some draft");
+ REQUIRE(res.localDrafts.front().relType == "m.replace");
+ REQUIRE(res.localDrafts.front().relatedEventId == "$eventid");
+}
+
+TEST_CASE("pushLocalDraftAction", "[client][room]")
+{
+ RoomModel r;
+
+ auto res = RoomModel::update(r,
+ PushLocalDraftAction("some draft", "m.replace", "$eventid"));
+
+ REQUIRE(res.localDrafts.size() == 2);
+ REQUIRE(res.localDrafts.front().text == "some draft");
+ REQUIRE(res.localDrafts.front().relType == "m.replace");
+ REQUIRE(res.localDrafts.front().relatedEventId == "$eventid");
+}
+
+TEST_CASE("popLocalDraftAction", "[client][room]")
+{
+ RoomModel r;
+
+ auto res = RoomModel::update(r,
+ PushLocalDraftAction("some draft", "m.replace", "$eventid"));
+
+ REQUIRE(res.localDrafts.size() == 2);
+ REQUIRE(res.localDrafts.front().text == "some draft");
+ REQUIRE(res.localDrafts.front().relType == "m.replace");
+ REQUIRE(res.localDrafts.front().relatedEventId == "$eventid");
+
+ res = RoomModel::update(r, PopLocalDraftAction{});
+ REQUIRE(res.localDrafts.size() == 1);
+ REQUIRE(res.localDrafts.front().text == "");
+ REQUIRE(res.localDrafts.front().relType == "");
+ REQUIRE(res.localDrafts.front().relatedEventId == "");
+
+ // The size of localDrafts is at least 1
+ res = RoomModel::update(r, PopLocalDraftAction{});
+ REQUIRE(res.localDrafts.size() == 1);
+}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 10, 3:49 PM (1 d, 6 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785338
Default Alt Text
(142 KB)

Event Timeline