Page MenuHomePhorge

No OneTemporary

Size
125 KB
Referenced Files
None
Subscribers
None
diff --git a/src/client/actions/ephemeral.cpp b/src/client/actions/ephemeral.cpp
index 7e46a62..2b38820 100644
--- a/src/client/actions/ephemeral.cpp
+++ b/src/client/actions/ephemeral.cpp
@@ -1,95 +1,101 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <status-utils.hpp>
#include "ephemeral.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, SetTypingAction a)
{
auto job = m.job<SetTypingJob>().make(
m.userId,
a.roomId,
a.typing,
a.timeoutMs)
.withData(json{{"roomId", a.roomId}});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, SetTypingResponse r)
{
auto roomId = r.dataStr("roomId");
if (! r.success()) {
m.addTrigger(SetTypingFailed{roomId, r.errorCode(), r.errorMessage()});
return { std::move(m), failWithResponse(r) };
}
m.addTrigger(SetTypingSuccessful{roomId});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, PostReceiptAction a)
{
auto job = m.job<PostReceiptJob>().make(
a.roomId,
/* receiptType = */ "m.read"s,
a.eventId,
/* receipt = */ json::object())
.withData(json{{"roomId", a.roomId}});
m.addJob(std::move(job));
- return { std::move(m), lager::noop };
+ auto userId = m.userId;
+ auto [next, _] = ClientModel::update(std::move(m), UpdateRoomAction{
+ a.roomId,
+ UpdateLocalReadMarkerAction{a.eventId, userId},
+ });
+
+ return { std::move(next), lager::noop };
}
ClientResult processResponse(ClientModel m, PostReceiptResponse r)
{
auto roomId = r.dataStr("roomId");
if (! r.success()) {
m.addTrigger(PostReceiptFailed{roomId, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
m.addTrigger(PostReceiptSuccessful{roomId});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, SetReadMarkerAction a)
{
auto job = m.job<SetReadMarkerJob>().make(
a.roomId,
/* mFullyRead = */ a.eventId)
.withData(json{{"roomId", a.roomId}});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, SetReadMarkerResponse r)
{
auto roomId = r.dataStr("roomId");
if (! r.success()) {
m.addTrigger(SetReadMarkerFailed{roomId, r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
m.addTrigger(SetReadMarkerSuccessful{roomId});
return { std::move(m), lager::noop };
}
}
diff --git a/src/client/room/room-model.cpp b/src/client/room/room-model.cpp
index 961e075..64f9551 100644
--- a/src/client/room/room-model.cpp
+++ b/src/client/room/room-model.cpp
@@ -1,680 +1,703 @@
/*
* 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};
}
bool operator==(const ReadReceipt &a, const ReadReceipt &b)
{
return a.eventId == b.eventId && a.timestamp == b.timestamp;
}
bool operator!=(const ReadReceipt &a, const ReadReceipt &b)
{
return !(a == b);
}
bool operator==(const EventReader &a, const EventReader &b)
{
return a.userId == b.userId && a.timestamp == b.timestamp;
}
bool operator!=(const EventReader &a, const EventReader &b)
{
return !(a == b);
}
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;
},
[&](AddToTimelineAction a) {
auto eventIds = intoImmer(immer::flex_vector<std::string>(),
zug::map(keyOfTimeline), a.events);
auto oldMessages = r.messages;
r.messages = merge(std::move(r.messages), a.events, keyOfTimeline);
auto exists =
[=](auto eventId) -> bool {
return !! oldMessages.find(eventId);
};
auto key =
[=](auto eventId) {
// sort first by timestamp, then by id
return sortKeyForTimelineEvent(r.messages[eventId]);
};
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);
r.timeline = sortedUniqueMerge(r.timeline, eventIds, exists, key);
// If this is a pagination request, gapEventId
// should have value. If this is a sync request,
// gapEventId does not have value. The pagination
// request does not have the limited field, and
// whether it has more paginate back token is determined
// by the presence of the prevBatch parameter.
// In sync request, limited may not be specified,
// and thus, if limited does not have value, it means
// it is not limited.
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());
}
}
// 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);
});
}
}
// 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;
},
[&](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;
},
[&](SetLocalDraftAction a) {
r.localDraft = a.localDraft;
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 readReceiptForCurrentUser = r.readReceipts.count(a.myUserId) ? r.readReceipts[a.myUserId].eventId : ""s;
+ 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 k = [r](const auto &id) {
- return sortKeyForTimelineEvent(r.messages[id]);
- };
-
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) {
- if (!r.readReceipts.count(a.myUserId)) {
- return r;
- }
-
- auto rr = r.readReceipts[a.myUserId].eventId;
-
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;
}
);
}
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));
}
}
diff --git a/src/client/room/room-model.hpp b/src/client/room/room-model.hpp
index 4496cde..e3f5af1 100644
--- a/src/client/room/room-model.hpp
+++ b/src/client/room/room-model.hpp
@@ -1,487 +1,495 @@
/*
* 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 <serialization/immer-flex-vector.hpp>
#include <serialization/immer-box.hpp>
#include <serialization/immer-map.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"
namespace Kazv
{
struct PendingRoomKeyEvent
{
std::string txnId;
immer::map<std::string, immer::map<std::string, Event>> messages;
};
PendingRoomKeyEvent makePendingRoomKeyEventV0(std::string txnId, Event event, immer::map<std::string, immer::flex_vector<std::string>> devices);
struct ReadReceipt
{
std::string eventId;
Timestamp timestamp;
};
template<class Archive>
void serialize(Archive &ar, ReadReceipt &r, std::uint32_t const /* version */)
{
ar & r.eventId & r.timestamp;
}
bool operator==(const ReadReceipt &a, const ReadReceipt &b);
bool operator!=(const ReadReceipt &a, const ReadReceipt &b);
struct EventReader
{
std::string userId;
Timestamp timestamp;
};
bool operator==(const EventReader &a, const EventReader &b);
bool operator!=(const EventReader &a, const EventReader &b);
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;
};
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
{
std::string localDraft;
};
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;
+ };
+
inline bool operator==(const PendingRoomKeyEvent &a, const PendingRoomKeyEvent &b)
{
return a.txnId == b.txnId && a.messages == b.messages;
}
inline bool operator!=(const PendingRoomKeyEvent &a, const PendingRoomKeyEvent &b)
{
return !(a == b);
}
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>;
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;
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;
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();
using Action = std::variant<
AddStateEventsAction,
MaybeAddStateEventsAction,
AddToTimelineAction,
AddAccountDataAction,
ChangeMembershipAction,
ChangeInviteStateAction,
AddEphemeralAction,
SetLocalDraftAction,
SetRoomEncryptionAction,
MarkMembersFullyLoadedAction,
SetHeroIdsAction,
AddLocalEchoAction,
RemoveLocalEchoAction,
AddPendingRoomKeyAction,
RemovePendingRoomKeyAction,
UpdateJoinedMemberCountAction,
UpdateInvitedMemberCountAction,
AddLocalNotificationsAction,
- RemoveReadLocalNotificationsAction
+ RemoveReadLocalNotificationsAction,
+ UpdateLocalReadMarkerAction
>;
static RoomModel update(RoomModel r, Action a);
};
using RoomAction = RoomModel::Action;
inline bool operator==(const RoomModel &a, const RoomModel &b)
{
return a.roomId == b.roomId
&& a.stateEvents == b.stateEvents
&& a.inviteState == b.inviteState
&& a.timeline == b.timeline
&& a.messages == b.messages
&& a.accountData == b.accountData
&& a.membership == b.membership
&& a.paginateBackToken == b.paginateBackToken
&& a.canPaginateBack == b.canPaginateBack
&& a.timelineGaps == b.timelineGaps
&& a.ephemeral == b.ephemeral
&& a.localDraft == b.localDraft
&& a.encrypted == b.encrypted
&& a.shouldRotateSessionKey == b.shouldRotateSessionKey
&& a.membersFullyLoaded == b.membersFullyLoaded
&& a.heroIds == b.heroIds
&& a.localEchoes == b.localEchoes
&& a.pendingRoomKeyEvents == b.pendingRoomKeyEvents
&& a.reverseEventRelationships == b.reverseEventRelationships
&& a.joinedMemberCount == b.joinedMemberCount
&& a.localReadMarker == b.localReadMarker
&& a.localUnreadCount == b.localUnreadCount
&& a.localNotificationCount == b.localNotificationCount
&& a.readReceipts == b.readReceipts
&& a.eventReadUsers == b.eventReadUsers
&& a.undecryptedEvents == b.undecryptedEvents
&& a.unreadNotificationEventIds == b.unreadNotificationEventIds
;
}
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);
};
using RoomListAction = RoomListModel::Action;
inline bool operator==(const RoomListModel &a, const RoomListModel &b)
{
return a.rooms == b.rooms;
}
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
& 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;
}
}
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, 8)
BOOST_CLASS_VERSION(Kazv::RoomListModel, 0)
diff --git a/src/client/room/room.cpp b/src/client/room/room.cpp
index 0ca5e40..2aef687 100644
--- a/src/client/room/room.cpp
+++ b/src/client/room/room.cpp
@@ -1,941 +1,947 @@
/*
* 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"
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}});
}
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];
}
}
diff --git a/src/client/room/room.hpp b/src/client/room/room.hpp
index aa73584..b7fd521 100644
--- a/src/client/room/room.hpp
+++ b/src/client/room/room.hpp
@@ -1,827 +1,834 @@
/*
* 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<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 .
*
* @param localDraft The local draft to send.
* @return A Promise that resolves when the local draft
* has been set, or when there is an error.
*/
PromiseT setLocalDraft(std::string localDraft) const;
/**
* Send an event to this room.
*
* @param msg The message to send
* @return A Promise that resolves when the event has been sent,
* or when there is an error.
*/
PromiseT sendMessage(Event msg) const;
/**
* Send a text message to this room.
*
* @param text The text
* @return A Promise that resolves when the text message has
* been sent, or when there is an error.
*/
PromiseT sendTextMessage(std::string text) const;
/**
* Resend an event to this room.
*
* @param txnId The transaction id of the unsent message
* @return A Promise that resolves when the event has been sent,
* or when there is an error.
*/
PromiseT resendMessage(std::string txnId) const;
/**
* Redact an event.
*
* @param eventId The event id of the event you want to redact
* @param reason The reason of redaction
* @return A Promise that resolves when the event has been sent,
* or when there is an error.
*/
PromiseT redactEvent(std::string eventId, std::optional<std::string> reason) const;
/**
* Send one pending key event in this room.
*
* @param txnId The transaction id of the pending key event
* @return A Promise that resolves when the event has been sent,
* or when there is an error.
*/
PromiseT sendPendingKeyEvent(std::string txnId) const;
/**
* Send all pending key events in this room.
*
* @return A Promise that resolves when all events has been sent,
* or when there is an error.
*/
PromiseT sendAllPendingKeyEvents() const;
/**
* Get the full state of this room.
*
* This method will update the Client as needed.
*
* After the returned Promise resolves successfully,
* @c stateEvents() will contain the fetched state.
*
* @return A Promise that resolves when the room state
* has been fetched, or when there is an error.
*/
PromiseT refreshRoomState() const;
/**
* Get one state event with @c type and @c stateKey .
*
* This method will update the Client as needed.
*
* After the returned Promise resolves successfully,
* @c state({type,stateKey}) will contain the fetched
* state event.
*
* @return A Promise that resolves when the state
* event has been fetched, or when there is an error.
*/
PromiseT getStateEvent(std::string type, std::string stateKey) const;
/**
* Send a state event to this room.
*
* @param state The state event to send.
* @return A Promise that resolves when the state event
* has been sent, or when there is an error.
*/
PromiseT sendStateEvent(Event state) const;
/**
* Set the room name.
*
* @param name The new name for this room.
* @return A Promise that resolves when the state event
* for the name change has been sent, or when there is an error.
*/
PromiseT setName(std::string name) const;
// lager::reader<std::string>
inline auto topic() const {
using namespace lager::lenses;
return stateEvents()
[KeyOfState{"m.room.topic", ""}]
[or_default]
.xform(eventContent
| jsonAtOr("topic"s, ""s));
}
/**
* Set the room topic.
*
* @param topic The new topic for this room.
* @return A Promise that resolves when the state event
* for the topic change has been sent, or when there is an error.
*/
PromiseT setTopic(std::string topic) const;
/**
* Invite a user to this room
*
* @param userId The user id for the user to invite.
* @return A Promise that resolves when the state event
* for the invite has been sent, or when there is an error.
*/
PromiseT invite(std::string userId) const;
/* lager::reader<MapT<std::string, Event>> */
inline auto ephemeralEvents() const {
return roomCursor()
[&RoomModel::ephemeral];
}
/* lager::reader<std::optional<Event>> */
inline auto ephemeralOpt(std::string type) const {
return roomCursor()
[&RoomModel::ephemeral]
[type];
}
/* lager::reader<Event> */
inline auto ephemeral(std::string type) const {
return roomCursor()
[&RoomModel::ephemeral]
[type]
[lager::lenses::or_default];
}
/**
* Get the ids of all typing users in this room.
*
* @return A lager::reader of an RangeT of std::string of ids of all typing users.
*/
auto typingUsers() const -> lager::reader<immer::flex_vector<std::string>>;
/**
* Get the member events of all typing users in this room.
*
* @return A lager::reader of an EventList of member events of all typing users.
*/
auto typingMemberEvents() const -> lager::reader<EventList>;
/**
* Set the typing status of the current user in this room.
*
* @param typing Whether the user is now typing.
* @param timeoutMs How long this typing status should last,
* in milliseconds.
* @return A Promise that resolves when the typing status
* has been sent, or when there is an error.
*/
PromiseT setTyping(bool typing, std::optional<int> timeoutMs) const;
/* lager::reader<MapT<std::string, Event>> */
inline auto accountDataEvents() const {
return roomCursor()
[&RoomModel::accountData];
}
/* lager::reader<std::optional<Event>> */
inline auto accountDataOpt(std::string type) const {
return roomCursor()
[&RoomModel::accountData]
[type];
}
/* lager::reader<Event> */
inline auto accountData(std::string type) const {
return roomCursor()
[&RoomModel::accountData]
[type]
[lager::lenses::or_default];
}
/* lager::reader<std::string> */
inline auto readMarker() const {
using namespace lager::lenses;
return accountData("m.fully_read")
.xform(eventContent
| jsonAtOr("event_id", std::string{}));
}
/**
* Set the account data for this room.
*
* @return A Promise that resolves when the account data
* has been set, or when there is an error.
*/
PromiseT setAccountData(Event accountDataEvent) const;
/**
* Get the tags of the current room.
*
* @return A lager::reader of a map from tag id of this room to
* the corresponding order.
*/
auto tags() const -> lager::reader<immer::map<std::string, double>>;
/**
* Add or set a tag to this room.
*
* @param tagId The tag id to add or set.
* @param order The order to specify.
*
* @return A Promise that resolves when the tag
* has been set, or when there is an error.
*/
PromiseT addOrSetTag(std::string tagId, std::optional<double> order = std::nullopt) const;
/**
* Remove a tag from this room.
*
* @param tagId The tag id to remove.
*
* @return A Promise that resolves when the tag
* has been removed, or when there is an error.
*/
PromiseT removeTag(std::string tagId) const;
/**
* Leave this room.
*
* @return A Promise that resolves when the state event
* for the leaving has been sent, or when there is an error.
*/
PromiseT leave() const;
/**
* Forget this room.
*
* One can only forget a room when they have already left it.
*
* @return A Promise that resolves when the room has been
* forgot, or when there is an error.
*/
PromiseT forget() const;
/**
* Kick a user from this room.
*
* You must have enough power levels in this room to do so.
*
* @param userId The id of the user that will be kicked.
* @param reason The reason to explain this kick.
*
* @return A Promise that resolves when the kick is done,
* or when there is an error.
*/
PromiseT kick(std::string userId, std::optional<std::string> reason = std::nullopt) const;
/**
* Ban a user from this room.
*
* You must have enough power levels in this room to do so.
*
* @param userId The id of the user that will be banned.
* @param reason The reason to explain this ban.
*
* @return A Promise that resolves when the ban is done,
* or when there is an error.
*/
PromiseT ban(std::string userId, std::optional<std::string> reason = std::nullopt) const;
// TODO: v1.1 adds reason field
/**
* Unban a user from this room.
*
* You must have enough power levels in this room to do so.
*
* @param userId The id of the user that will be unbanned.
*
* @return A Promise that resolves when the unban is done,
* or when there is an error.
*/
PromiseT unban(std::string userId/*, std::optional<std::string> reason = std::nullopt*/) const;
/* lager::reader<JsonWrap> */
inline auto avatar() const {
return state(KeyOfState{"m.room.avatar", ""})
.xform(eventContent);
}
/**
* 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>>;
private:
const lager::reader<SdkModel> &sdkCursor() const;
const lager::reader<RoomModel> &roomCursor() const;
lager::reader<RoomModel> makeRoomCursor() const;
std::string currentRoomId() const;
/// if membership is invite, return inviteState; otherwise return stateEvents
lager::reader<immer::map<KeyOfState, Event>> inviteStateOrState() const;
/// if membership is invite and inviteState[key] exists, return inviteState[key], otherwise return stateEvents[key]
lager::reader<Event> inviteStateOrStateEvent(lager::reader<KeyOfState> key) const;
std::optional<lager::reader<SdkModel>> m_sdk;
std::variant<lager::reader<std::string>, std::string> m_roomId;
ContextT m_ctx;
std::optional<DepsT> m_deps;
lager::reader<RoomModel> m_roomCursor;
KAZV_DECLARE_THREAD_ID();
KAZV_DECLARE_EVENT_LOOP_THREAD_ID_KEEPER(m_deps.has_value() ? &lager::get<EventLoopThreadIdKeeper &>(m_deps.value()) : 0);
};
}
diff --git a/src/tests/client/room/read-receipt-test.cpp b/src/tests/client/room/read-receipt-test.cpp
index cf62f36..da09f83 100644
--- a/src/tests/client/room/read-receipt-test.cpp
+++ b/src/tests/client/room/read-receipt-test.cpp
@@ -1,347 +1,410 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <boost/asio.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_range_equals.hpp>
#include <lager/event_loop/boost_asio.hpp>
#include <asio-promise-handler.hpp>
#include <room/room-model.hpp>
#include <sdk-model.hpp>
#include <client/client.hpp>
#include <client/actions/ephemeral.hpp>
#include <cprjobhandler.hpp>
#include <lagerstoreeventemitter.hpp>
#include <testfixtures/factory.hpp>
#include "client-test-util.hpp"
#include "action-mock-utils.hpp"
#include "push-rules-test-util.hpp"
using namespace Kazv;
using namespace Kazv::Factory;
TEST_CASE("Adding an m.receipt ephemeral event", "[client][room][receipt]")
{
auto room = makeRoom();
auto receiptEvent = makeEvent(
withEventType("m.receipt")
// https://spec.matrix.org/v1.8/client-server-api/#events-5
| withEventContent(R"({
"$1435641916114394fHBLK:matrix.org": {
"m.read": {
"@rikj:jki.re": {
"ts": 1436451550453
}
},
"m.read.private": {
"@self:example.org": {
"ts": 1661384801651
}
}
}
})"_json));
room = RoomModel::update(room, AddEphemeralAction{{receiptEvent}});
auto receipt = room.readReceipts.at("@rikj:jki.re");
REQUIRE(
room.readReceipts.at("@rikj:jki.re")
==
ReadReceipt{"$1435641916114394fHBLK:matrix.org", 1436451550453}
);
REQUIRE(
room.eventReadUsers.at("$1435641916114394fHBLK:matrix.org")
==
immer::flex_vector<std::string>{"@rikj:jki.re", "@self:example.org"}
);
auto anotherReceiptEvent = makeEvent(
withEventType("m.receipt")
| withEventContent(json{
{"$123", {
{"m.read", {
{"@foo:example.com", {{"ts", 1796451550450}}}
}},
}},
}));
room = RoomModel::update(room, AddEphemeralAction{{anotherReceiptEvent}});
// the receipt that was there should still be there.
REQUIRE(
room.readReceipts.at("@rikj:jki.re")
==
ReadReceipt{"$1435641916114394fHBLK:matrix.org", 1436451550453}
);
REQUIRE(
room.readReceipts.at("@foo:example.com")
==
ReadReceipt{"$123", 1796451550450}
);
boost::asio::io_context io;
AsioPromiseHandler ph{io.get_executor()};
auto model = makeClient(withRoom(room));
auto store = createTestClientStoreFrom(model, ph);
auto client = Client(store.reader().map([](auto c) { return SdkModel{c}; }),
store,
std::nullopt);
auto r = client.room(room.roomId);
auto readers1 = r.eventReaders(lager::make_constant<std::string>("$1435641916114394fHBLK:matrix.org")).make().get();
auto expected1 = immer::flex_vector<EventReader>{{"@rikj:jki.re", 1436451550453}, {"@self:example.org", 1661384801651}};
REQUIRE_THAT(readers1, Catch::Matchers::UnorderedRangeEquals(expected1));
auto readers2 = r.eventReaders(lager::make_constant<std::string>("$123")).make().get();
auto expected2 = immer::flex_vector<EventReader>{{"@foo:example.com", 1796451550450}};
REQUIRE(readers2 == expected2);
}
TEST_CASE("Update a receipt for some user", "[client][room][receipt]")
{
auto room = makeRoom();
auto receiptEvent = makeEvent(
withEventType("m.receipt")
// https://spec.matrix.org/v1.8/client-server-api/#events-5
| withEventContent(R"({
"$1435641916114394fHBLK:matrix.org": {
"m.read": {
"@rikj:jki.re": {
"ts": 1436451550453
}
}
}
})"_json));
room = RoomModel::update(room, AddEphemeralAction{{receiptEvent}});
auto anotherReceiptEvent = makeEvent(
withEventType("m.receipt")
| withEventContent(json{
{"$123", {
{"m.read", {
{"@rikj:jki.re", {{"ts", 1796451550450}}}
}},
}},
}));
room = RoomModel::update(room, AddEphemeralAction{{anotherReceiptEvent}});
REQUIRE(
room.readReceipts.at("@rikj:jki.re")
==
ReadReceipt{"$123", 1796451550450}
);
REQUIRE(room.eventReadUsers.count("$1435641916114394fHBLK:matrix.org") == 0);
REQUIRE(room.eventReadUsers.count("$123") == 1);
boost::asio::io_context io;
AsioPromiseHandler ph{io.get_executor()};
auto model = makeClient(withRoom(room));
auto store = createTestClientStoreFrom(model, ph);
auto client = Client(store.reader().map([](auto c) { return SdkModel{c}; }),
store,
std::nullopt);
auto r = client.room(room.roomId);
auto readers1 = r.eventReaders(lager::make_constant<std::string>("$1435641916114394fHBLK:matrix.org")).make().get();
auto expected1 = immer::flex_vector<EventReader>{};
REQUIRE(readers1 == expected1);
auto readers2 = r.eventReaders(lager::make_constant<std::string>("$123")).make().get();
auto expected2 = immer::flex_vector<EventReader>{{"@rikj:jki.re", 1796451550450}};
REQUIRE(readers2 == expected2);
}
+TEST_CASE("localReadMarker", "[client][room][receipt]")
+{
+ boost::asio::io_context io;
+ AsioPromiseHandler ph{io.get_executor()};
+
+ auto tl = EventList{
+ makeEvent(),
+ };
+ auto room = makeRoom(withRoomTimeline(tl));
+ room.localReadMarker = tl[0].id();
+ auto model = makeClient(withRoom(room));
+ auto store = createTestClientStoreFrom(model, ph);
+ auto client = Client(store.reader().map([](auto c) { return SdkModel{c}; }),
+ store,
+ std::nullopt);
+ auto r = client.room(room.roomId);
+ REQUIRE(r.localReadMarker().get() == tl[0].id());
+}
+
TEST_CASE("Posting receipts", "[client][room][receipt]")
{
auto r = makeRoom();
auto m = makeClient(withRoom(r));
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
auto dispatcher = getMockDispatcher(
sgph,
ctx,
returnEmpty<PostReceiptAction>()
);
auto mockContext = getMockContext(sgph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto room = client.room(r.roomId);
room.postReceipt("$1")
.then([&io](auto) {
io.stop();
});
io.run();
REQUIRE(dispatcher.template calledTimes<PostReceiptAction>() == 1);
auto action = dispatcher.template of<PostReceiptAction>()[0];
REQUIRE(action.roomId == r.roomId);
REQUIRE(action.eventId == "$1");
}
TEST_CASE("PostReceiptAction", "[client][room][receipt]")
{
auto m = makeClient();
auto [next, _ignore] = updateClient(m, PostReceiptAction{"!someroom:example.com", "$someevent"});
assert1Job(next);
for1stJob(next, [](const BaseJob &job) {
REQUIRE(job.jobId() == "PostReceipt");
REQUIRE(job.url().find("rooms/!someroom:example.com/receipt/m.read/$someevent") != std::string::npos);
REQUIRE(json::parse(std::get<Bytes>(job.requestBody())) == json::object());
});
}
+static auto pushRules = PushRulesDesc(Event(R"({
+ "type": "m.push_rules",
+ "content": {
+ "global": {
+ "override": [{
+ "rule_id": "moe.kazv.mxc.some-rule",
+ "default": true,
+ "enabled": true,
+ "conditions": [{
+ "kind": "event_match",
+ "key": "type",
+ "pattern": "moe.kazv.mxc.test-event"
+ }],
+ "actions": ["notify"]
+ }]
+ }
+ }
+})"_json));
+
TEST_CASE("AddLocalNotificationsAction", "[client][room][receipt]")
{
auto myUserId = "@mew:example.com"s;
auto newEvents = EventList{
makeEvent(withEventType("moe.kazv.mxc.test-event")),
makeEvent(),
makeEvent(withEventType("moe.kazv.mxc.test-event")),
makeEvent(withEventType("moe.kazv.mxc.test-event") | withEventSenderId(myUserId)),
makeEvent(withEventType("moe.kazv.mxc.test-event")),
};
auto r = makeRoom(withRoomTimeline(newEvents));
r.readReceipts = {{myUserId, ReadReceipt{newEvents[0].id(), 0}}};
WHEN("push rules does not notify") {
auto next = RoomModel::update(r, AddLocalNotificationsAction{
newEvents,
PushRulesDesc(Event()),
myUserId,
});
REQUIRE(next.unreadNotificationEventIds == immer::flex_vector<std::string>{});
}
WHEN("push rules notifies some") {
auto next = RoomModel::update(r, AddLocalNotificationsAction{
newEvents,
- PushRulesDesc(Event(R"({
- "type": "m.push_rules",
- "content": {
- "global": {
- "override": [{
- "rule_id": "moe.kazv.mxc.some-rule",
- "default": true,
- "enabled": true,
- "conditions": [{
- "kind": "event_match",
- "key": "type",
- "pattern": "moe.kazv.mxc.test-event"
- }],
- "actions": ["notify"]
- }]
- }
- }
-})"_json)),
+ pushRules,
myUserId,
});
REQUIRE(
next.unreadNotificationEventIds
== immer::flex_vector<std::string>{
newEvents[2].id(),
newEvents[4].id(),
}
);
}
+
+ WHEN("push rules notifies some but is before local read marker, only things after local read marker is added") {
+ r.localReadMarker = newEvents[3].id();
+ auto next = RoomModel::update(r, AddLocalNotificationsAction{
+ newEvents,
+ pushRules,
+ myUserId,
+ });
+
+ REQUIRE(
+ next.unreadNotificationEventIds
+ == immer::flex_vector<std::string>{
+ newEvents[4].id(),
+ }
+ );
+ }
+}
+
+TEST_CASE("PostReceiptAction should update local read marker and remove read notifications")
+{
+ auto myUserId = "@mew:example.com"s;
+
+ auto newEvents = EventList{
+ makeEvent(withEventType("moe.kazv.mxc.test-event")),
+ makeEvent(withEventType("moe.kazv.mxc.test-event")),
+ makeEvent(withEventType("moe.kazv.mxc.test-event")),
+ };
+ auto r = makeRoom(withRoomTimeline(newEvents));
+ r.readReceipts = {{myUserId, ReadReceipt{newEvents[0].id(), 0}}};
+ r = RoomModel::update(r, AddLocalNotificationsAction{
+ newEvents,
+ pushRules,
+ myUserId,
+ });
+
+ auto client = makeClient(withRoom(r));
+ client.userId = myUserId;
+ auto receiptPos = newEvents[1].id();
+ auto [next, _] = ClientModel::update(client, PostReceiptAction{r.roomId, receiptPos});
+ auto nextRoom = next.roomList.rooms.at(r.roomId);
+ REQUIRE(nextRoom.localReadMarker == receiptPos);
+ REQUIRE(nextRoom.unreadNotificationEventIds == immer::flex_vector<std::string>{newEvents[2].id()});
}
TEST_CASE("RemoveReadLocalNotificationsAction", "[client][room][receipt]")
{
auto myUserId = "@mew:example.com"s;
auto oldEvents = EventList{
makeEvent(),
makeEvent(),
};
auto newEvents = EventList{
makeEvent(),
makeEvent(),
makeEvent(),
makeEvent(),
makeEvent(),
makeEvent(),
};
auto timeline = oldEvents + newEvents;
auto r = makeRoom(withRoomTimeline(timeline));
r.unreadNotificationEventIds = intoImmer(
immer::flex_vector<std::string>(),
zug::map(&Event::id),
newEvents.erase(5)
);
WHEN("a read receipt is in the middle") {
r.readReceipts = {{myUserId, ReadReceipt{newEvents[2].id(), 0}}};
auto next = RoomModel::update(r, RemoveReadLocalNotificationsAction{myUserId});
REQUIRE(next.unreadNotificationEventIds == immer::flex_vector<std::string>{newEvents[3].id(), newEvents[4].id()});
}
WHEN("a read receipt is before the beginning") {
r.readReceipts = {{myUserId, ReadReceipt{oldEvents[0].id(), 0}}};
auto next = RoomModel::update(r, RemoveReadLocalNotificationsAction{myUserId});
REQUIRE(next.unreadNotificationEventIds == r.unreadNotificationEventIds);
}
WHEN("a read receipt does not exist") {
r.readReceipts = {};
auto next = RoomModel::update(r, RemoveReadLocalNotificationsAction{myUserId});
REQUIRE(next.unreadNotificationEventIds == r.unreadNotificationEventIds);
}
WHEN("a read receipt is at the end") {
r.readReceipts = {{myUserId, ReadReceipt{newEvents[5].id(), 0}}};
auto next = RoomModel::update(r, RemoveReadLocalNotificationsAction{myUserId});
REQUIRE(next.unreadNotificationEventIds == immer::flex_vector<std::string>{});
}
WHEN("a read receipt is after the end") {
r.readReceipts = {{myUserId, ReadReceipt{newEvents[5].id(), 0}}};
auto next = RoomModel::update(r, RemoveReadLocalNotificationsAction{myUserId});
REQUIRE(next.unreadNotificationEventIds == immer::flex_vector<std::string>{});
}
}
TEST_CASE("Room::unreadNotificationEventIds()", "[client][room][receipt]")
{
boost::asio::io_context io;
AsioPromiseHandler ph{io.get_executor()};
auto room = makeRoom();
room.unreadNotificationEventIds = {"$foo", "$bar"};
auto model = makeClient(withRoom(room));
auto store = createTestClientStoreFrom(model, ph);
auto client = Client(store.reader().map([](auto c) { return SdkModel{c}; }),
store,
std::nullopt);
auto r = client.room(room.roomId);
REQUIRE(r.unreadNotificationEventIds().get()
== room.unreadNotificationEventIds);
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Oct 8, 10:01 PM (14 h, 34 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784311
Default Alt Text
(125 KB)

Event Timeline