Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F140151
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
73 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/client/actions/sync.cpp b/src/client/actions/sync.cpp
index a8d1fe1..c578701 100644
--- a/src/client/actions/sync.cpp
+++ b/src/client/actions/sync.cpp
@@ -1,344 +1,357 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <lager/util.hpp>
#include <zug/transducer/map.hpp>
#include <zug/transducer/cat.hpp>
#include <zug/transducer/filter.hpp>
#include <zug/sequence.hpp>
#include <jobinterface.hpp>
#include <debug.hpp>
#include "cursorutil.hpp"
#include "sync.hpp"
#include "encryption.hpp"
#include "status-utils.hpp"
namespace Kazv
{
// Atomicity guaranteed: if the sync action is created
// before an action that reasonably changes Client
// (e.g. roll back to an earlier state, obtain other
// events), but executed
// after that action, the sync will still give continuous
// data about the events. (Sync will not "skip" events)
// This is because this function takes the sync token
// from the ClientModel model it is passed.
ClientResult updateClient(ClientModel m, SyncAction)
{
kzo.client.dbg() << "Start syncing with token " <<
(m.syncToken ? m.syncToken.value() : "<null>") << std::endl;
bool isInitialSync = ! m.syncToken;
m.syncing = true;
std::string filter = m.syncToken ? m.incrementalSyncFilterId : m.initialSyncFilterId;
m.addJob(m.job<SyncJob>()
.make(filter,
m.syncToken,
std::nullopt, // fullState
std::nullopt, // setPresence
// Let initial sync return immediately
isInitialSync ? 0 : m.syncTimeoutMs
)
.withData(json{{"is", isInitialSync ? "initial" : "incremental"}}));
return { m, lager::noop };
}
static KazvEventList loadRoomsFromSyncInPlace(ClientModel &m, SyncJob::Rooms rooms)
{
auto l = std::move(m.roomList);
auto eventsToEmit = KazvEventList{}.transient();
auto updateRoomImpl =
[&l](auto id, auto a) {
l = RoomListModel::update(
std::move(l),
UpdateRoomAction{std::move(id), std::move(a)});
};
auto updateSingleRoom =
[&, updateRoomImpl](const auto &id, const auto &room, auto membership) {
if (!l.has(id) || l[id].membership != membership) {
eventsToEmit.push_back(RoomMembershipChanged{membership, id});
}
updateRoomImpl(id, ChangeMembershipAction{membership});
auto timelineEvents =
intoImmer(
EventList{},
zug::map([=](Event e) {
return Event::fromSync(e, id);
}),
room.timeline.events);
eventsToEmit.append(
intoImmer(
KazvEventList{},
zug::map([=](Event e) -> KazvEvent {
return ReceivingRoomTimelineEvent{std::move(e), id};
}),
timelineEvents).transient());
updateRoomImpl(id, AddToTimelineAction{timelineEvents,
room.timeline.prevBatch,
room.timeline.limited,
std::nullopt // we do not have a gapEventId
});
if (room.state) {
eventsToEmit.append(
intoImmer(
KazvEventList{},
zug::map([=](Event e) -> KazvEvent {
return ReceivingRoomStateEvent{std::move(e), id};
}),
room.state.value().events).transient());
updateRoomImpl(id, AddStateEventsAction{room.state.value().events});
}
// Process state events in timeline, which should have arrived later
// than those in room.state .
updateRoomImpl(id, AddStateEventsAction{
intoImmer(EventList{},
zug::filter([=](Event e) {
return e.isState();
}),
timelineEvents)});
if (room.accountData) {
eventsToEmit.append(
intoImmer(
KazvEventList{},
zug::map([=](Event e) -> KazvEvent {
return ReceivingRoomAccountDataEvent{std::move(e), id};
}),
room.state.value().events).transient());
updateRoomImpl(id, AddAccountDataAction{room.accountData.value().events});
}
};
+ auto updateRoomSummary =
+ [=](const auto &id, const auto &room) {
+ if (!room.summary.has_value()) {
+ return;
+ }
+ if (!room.summary->mHeroes.empty()) {
+ auto newHeroes = room.summary->mHeroes;
+ updateRoomImpl(id, SetHeroIdsAction{immer::flex_vector<std::string>(newHeroes.begin(), newHeroes.end())});
+ }
+ };
+
auto updateJoinedRoom =
[=](const auto &id, const auto &room) {
updateSingleRoom(id, room, RoomMembership::Join);
if (room.ephemeral) {
updateRoomImpl(id, AddEphemeralAction{room.ephemeral.value().events});
}
// TODO update other info such as
// notification and summary
+
+ updateRoomSummary(id, room);
};
auto updateInvitedRoom =
[=](const auto &id, const auto &room) {
updateRoomImpl(id, ChangeMembershipAction{RoomMembership::Invite});
if (room.inviteState) {
updateRoomImpl(id, ChangeInviteStateAction{room.inviteState.value().events});
}
};
auto updateLeftRoom =
[=](const auto &id, const auto &room) {
updateSingleRoom(id, room, RoomMembership::Leave);
};
for (const auto &[id, room]: rooms.join) {
updateJoinedRoom(id, room);
}
// TODO update info for invited rooms
for (const auto &[id, room]: rooms.invite) {
updateInvitedRoom(id, room);
}
for (const auto &[id, room]: rooms.leave) {
updateLeftRoom(id, room);
}
m.roomList = std::move(l);
return eventsToEmit.persistent();
}
static KazvEventList loadPresenceFromSyncInPlace(ClientModel &m, EventList presence)
{
auto eventsToEmit = intoImmer(
KazvEventList{},
zug::map([](Event e) { return ReceivingPresenceEvent{e}; }),
presence);
m.presence = merge(std::move(m.presence), presence, keyOfPresence);
return eventsToEmit;
}
static KazvEventList loadAccountDataFromSyncInPlace(ClientModel &m, EventList accountData)
{
auto eventsToEmit = intoImmer(
KazvEventList{},
zug::map([](Event e) { return ReceivingPresenceEvent{e}; }),
accountData);
m.accountData = merge(std::move(m.accountData), accountData, keyOfAccountData);
return eventsToEmit;
}
static KazvEventList loadToDeviceFromSyncInPlace(ClientModel &m, JsonWrap toDevice)
{
if (toDevice.get().contains("events")) {
auto events = toDevice.get()["events"];
auto msgs = intoImmer(
EventList{},
zug::map([](const json &j) { return Event(j); }),
events);
m.toDevice = std::move(m.toDevice) + msgs;
return intoImmer(
KazvEventList{},
zug::map([](Event e) { return ReceivingToDeviceMessage{e}; }),
msgs);
}
return {};
}
ClientResult processResponse(ClientModel m, SyncResponse r)
{
if (! r.success()) {
m.addTrigger(SyncFailed{});
kzo.client.dbg() << "Sync failed" << std::endl;
kzo.client.dbg() << r.statusCode << std::endl;
if (isBodyJson(r.body)) {
auto j = r.jsonBody();
kzo.client.dbg() << "Json says: " << j.get().dump() << std::endl;
} else {
kzo.client.dbg() << "Response body: "
<< std::get<BaseJob::BytesBody>(r.body) << std::endl;
}
return { std::move(m), simpleFail };
}
kzo.client.dbg() << "Sync successful" << std::endl;
auto rooms = r.rooms();
auto accountData = r.accountData();
auto presence = r.presence();
// load the info that has been sync'd
m.syncToken = r.nextBatch();
if (rooms) {
m.addTriggers(loadRoomsFromSyncInPlace(m, std::move(rooms.value())));
}
if (presence) {
m.addTriggers(loadPresenceFromSyncInPlace(m, std::move(presence.value().events)));
}
if (accountData) {
m.addTriggers(loadAccountDataFromSyncInPlace(m, std::move(accountData.value().events)));
}
m.addTriggers(loadToDeviceFromSyncInPlace(m, r.toDevice()));
auto is = r.dataStr("is");
auto isInitialSync = is == "initial";
if (m.crypto) {
kzo.client.dbg() << "E2EE is on. Processing device lists and one-time key counts." << std::endl;
auto &crypto = m.crypto.value();
// process deviceLists
if (isInitialSync) {
auto encryptedUsers =
zug::sequence(
zug::map([](auto n) { return n.second; })
| zug::filter([](auto room) { return room.encrypted; })
| zug::map([](auto room) { return room.joinedMemberIds(); })
| zug::cat,
// no need to use distinct here as the map will overwrite
m.roomList.rooms);
m.deviceLists.track(std::move(encryptedUsers));
} else {
const auto &l = r.deviceLists().get();
if (l.contains("changed")) {
const auto &changed = l.at("changed");
m.deviceLists.track(changed);
}
if (l.contains("left")) {
const auto &left = l.at("left");
m.deviceLists.untrack(left);
}
}
// deviceOneTimeKeysCount
crypto.setUploadedOneTimeKeysCount(r.deviceOneTimeKeysCount());
auto model = tryDecryptEvents(std::move(m));
m = std::move(model);
}
m.addTrigger(SyncSuccessful{r.nextBatch()});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, SetShouldSyncAction a)
{
m.shouldSync = a.shouldSync;
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, PostInitialFiltersAction)
{
if (m.syncing) {
return { std::move(m), lager::noop };
}
Filter initialSyncFilter;
initialSyncFilter.room.timeline.limit = 1;
initialSyncFilter.room.state.lazyLoadMembers = true;
auto firstJob = m.job<DefineFilterJob>()
.make(m.userId, initialSyncFilter)
.withData(json{{"is", "initialSyncFilter"}})
.withQueue("post-filter", CancelFutureIfFailed);
m.addJob(firstJob);
Filter incrementalSyncFilter;
incrementalSyncFilter.room.timeline.limit = 20;
incrementalSyncFilter.room.state.lazyLoadMembers = true;
m.addJob(m.job<DefineFilterJob>()
.make(m.userId, incrementalSyncFilter)
.withData(json{{"is", "incrementalSyncFilter"}})
.withQueue("post-filter", CancelFutureIfFailed));
m.syncing = true;
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, DefineFilterResponse r)
{
auto is = r.dataStr("is");
if (! r.success()) {
m.syncing = false;
kzo.client.dbg() << "posting filter failed: " << r.errorCode() << r.errorMessage() << std::endl;
m.addTrigger(PostInitialFiltersFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), lager::noop };
}
kzo.client.dbg() << "filter " << is << " is posted" << std::endl;
if (is == "incrementalSyncFilter") {
m.incrementalSyncFilterId = r.filterId();
m.addTrigger(PostInitialFiltersSuccessful{});
} else {
m.initialSyncFilterId = r.filterId();
}
return { std::move(m), lager::noop };
}
}
diff --git a/src/client/room/room-model.cpp b/src/client/room/room-model.cpp
index e16f261..594cbfb 100644
--- a/src/client/room/room-model.cpp
+++ b/src/client/room/room-model.cpp
@@ -1,212 +1,216 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020 Tusooa Zhu
* 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"
namespace Kazv
{
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;
},
[&](AppendTimelineAction a) {
auto eventIds = intoImmer(immer::flex_vector<std::string>(),
zug::map(keyOfTimeline), a.events);
r.timeline = r.timeline + eventIds;
r.messages = merge(std::move(r.messages), a.events, keyOfTimeline);
return r;
},
[&](PrependTimelineAction a) {
auto eventIds = intoImmer(immer::flex_vector<std::string>(),
zug::map(keyOfTimeline), a.events);
r.timeline = eventIds + r.timeline;
r.messages = merge(std::move(r.messages), a.events, keyOfTimeline);
r.paginateBackToken = a.paginateBackToken;
// if there are no more events we should not allow further paginating
r.canPaginateBack = a.events.size() != 0;
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 std::make_tuple(r.messages[eventId].originServerTs(), eventId);
};
r.timeline = sortedUniqueMerge(r.timeline, eventIds, exists, key);
// TODO need other way to determine whether it is limited
// in a pagination request (/messages does not have that field)
if ((! a.limited.has_value() || a.limited.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;
std::for_each(thisBatchStart + 1, origBatchStart,
[&](auto eventId) {
r.timelineGaps = std::move(r.timelineGaps).erase(eventId);
});
}
return r;
},
[&](AddAccountDataAction a) {
r.accountData = merge(std::move(r.accountData), a.events, keyOfAccountData);
return r;
},
[&](ChangeMembershipAction a) {
r.membership = a.membership;
return r;
},
[&](ChangeInviteStateAction a) {
r.inviteState = merge(immer::map<KeyOfState, Event>{}, a.events, keyOfState);
return r;
},
[&](AddEphemeralAction a) {
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;
}
);
}
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;
}
);
}
immer::flex_vector<std::string> RoomModel::joinedMemberIds() const
{
using MemberNode = std::pair<std::string, Kazv::Event>;
auto memberNameTransducer =
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 MemberNode{stateKey, v};
})
| zug::filter(
[](auto val) {
auto [stateKey, ev] = val;
return ev.content().get()
.at("membership"s) == "join"s;
})
| zug::map(
[](auto val) {
auto [stateKey, ev] = val;
return stateKey;
});
return intoImmer(
immer::flex_vector<std::string>{},
memberNameTransducer,
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;
}
}
diff --git a/src/client/room/room-model.hpp b/src/client/room/room-model.hpp
index dff80da..e9dba62 100644
--- a/src/client/room/room-model.hpp
+++ b/src/client/room/room-model.hpp
@@ -1,226 +1,235 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021 Tusooa Zhu <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 "clientutil.hpp"
namespace Kazv
{
struct AddStateEventsAction
{
immer::flex_vector<Event> stateEvents;
};
struct AppendTimelineAction
{
immer::flex_vector<Event> events;
};
struct PrependTimelineAction
{
immer::flex_vector<Event> events;
std::string paginateBackToken;
};
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 RoomModel
{
using Membership = RoomMembership;
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<std::string> joinedMemberIds() const;
MegOlmSessionRotateDesc sessionRotateDesc() const;
bool hasUser(std::string userId) const;
using Action = std::variant<
AddStateEventsAction,
AppendTimelineAction,
PrependTimelineAction,
AddToTimelineAction,
AddAccountDataAction,
ChangeMembershipAction,
ChangeInviteStateAction,
AddEphemeralAction,
SetLocalDraftAction,
SetRoomEncryptionAction,
- MarkMembersFullyLoadedAction
+ MarkMembersFullyLoadedAction,
+ SetHeroIdsAction
>;
static RoomModel update(RoomModel r, Action a);
};
using RoomAction = RoomModel::Action;
inline bool operator==(RoomModel a, 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.membersFullyLoaded == b.membersFullyLoaded
+ && a.heroIds == b.heroIds;
}
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==(RoomListModel a, 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
+ & r.heroIds
;
}
template<class Archive>
void serialize(Archive &ar, RoomListModel &l, std::uint32_t const /*version*/)
{
ar & l.rooms;
}
}
BOOST_CLASS_VERSION(Kazv::RoomModel, 0)
BOOST_CLASS_VERSION(Kazv::RoomListModel, 0)
diff --git a/src/client/room/room.cpp b/src/client/room/room.cpp
index c0a343d..71de68d 100644
--- a/src/client/room/room.cpp
+++ b/src/client/room/room.cpp
@@ -1,386 +1,394 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <immer/algorithm.hpp>
#include <debug.hpp>
#include "room.hpp"
namespace Kazv
{
Room::Room(lager::reader<SdkModel> sdk,
lager::reader<std::string> roomId,
Context<ClientAction> ctx,
DepsT deps)
: m_sdk(sdk)
, m_roomId(roomId)
, m_ctx(ctx)
, m_deps(deps)
{
}
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)
{
}
Room::Room(InEventLoopTag, std::string roomId, ContextT ctx, DepsT deps)
: m_sdk(std::nullopt)
, m_roomId(roomId)
, m_ctx(ctx)
, m_deps(deps)
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, KAZV_ON_EVENT_LOOP_VAR(true)
#endif
{
}
Room Room::toEventLoop() const
{
assert(m_deps.has_value());
return Room(InEventLoopTag{}, currentRoomId(), m_ctx, m_deps.value());
}
const lager::reader<SdkModel> &Room::sdkCursor() const
{
if (m_sdk.has_value()) { return m_sdk.value(); }
assert(m_deps.has_value());
return *lager::get<SdkModelCursorKey>(m_deps.value());
}
lager::reader<RoomModel> Room::roomCursor() const
{
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::heroIds() const
+ -> lager::reader<immer::flex_vector<std::string>>
+ {
+ return roomCursor().map([](const auto &room) {
+ return room.heroIds;
+ });
+ }
+
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();
// 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) {
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;
});
}
});
}
return promise
// Encrypt the event and see whether the session was rotated.
.then([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())
});
})
// If the session was rotated, send the corresponding session key to other devices
.then([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([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(EncryptOlmEventAction{
devicesToSend, keyEv,
rg.generateRange<RandomData>(EncryptOlmEventAction::randomSize(devicesToSend))
});
})
.then([ctx, devicesToSend](auto status) {
if (! status) { return ctx.createResolvedPromise({}); }
kzo.client.dbg() << "sending key event as to-device message" << std::endl;
auto event = Event(status.dataJson("encrypted"));
return ctx.dispatch(SendToDeviceMessageAction{event, devicesToSend});
});
});
}
return ret
.then([ctx, prevStatus=status](auto status) {
if (! status) { return status; }
return prevStatus;
});
})
// Send the just encrypted event
.then([ctx, rid](auto status) {
if (! status) { return ctx.createResolvedPromise(status); }
kzo.client.dbg() << "sending encrypted message" << std::endl;
auto ev = Event(status.dataJson("encrypted"));
return ctx.dispatch(SendMessageAction{rid, ev});
});
}
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::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::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::setPinnedEvents(immer::flex_vector<std::string> eventIds) const
-> PromiseT
{
json j{
{"type", "m.room.pinned_events"},
{"content", {
{"pinned", eventIds}
}
}
};
Event e{j};
return sendStateEvent(e);
}
auto Room::timelineGaps() const
-> lager::reader<immer::map<std::string /* eventId */,
std::string /* prevBatch */>>
{
return roomCursor()[&RoomModel::timelineGaps];
}
auto Room::paginateBackFromEvent(std::string eventId) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(PaginateTimelineAction{
+roomId(), eventId, std::nullopt});
}
}
diff --git a/src/client/room/room.hpp b/src/client/room/room.hpp
index 639e245..7d86e3f 100644
--- a/src/client/room/room.hpp
+++ b/src/client/room/room.hpp
@@ -1,519 +1,527 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <lager/reader.hpp>
#include <lager/context.hpp>
#include <lager/with.hpp>
#include <lager/constant.hpp>
#include <lager/lenses/optional.hpp>
#include <zug/transducer/map.hpp>
#include <zug/transducer/filter.hpp>
#include <zug/sequence.hpp>
#include <immer/flex_vector_transient.hpp>
#include "debug.hpp"
#include "sdk-model.hpp"
#include "client-model.hpp"
#include "room-model.hpp"
#include <cursorutil.hpp>
#include "sdk-model-cursor-tag.hpp"
#include "random-generator.hpp"
namespace Kazv
{
/**
* Represent a Matrix room.
*
* This class has the same constraints as Client.
*/
class Room
{
public:
using PromiseT = SingleTypePromise<DefaultRetType>;
using DepsT = lager::deps<SdkModelCursorKey, RandomInterface &
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, EventLoopThreadIdKeeper &
#endif
>;
using ContextT = Context<ClientAction>;
struct InEventLoopTag {};
/**
* Constructor.
*
* Construct the room with @c roomId .
*
* `sdk` and `roomId` must be cursors in the same thread.
*
* The constructed room will be in the same thread as `sdk` and `roomId`.
*
* @warning Do not use this directly. Use `Client::room()` and
* `Client::roomBycursor()` instead.
*/
Room(lager::reader<SdkModel> sdk,
lager::reader<std::string> roomId,
ContextT ctx);
/**
* Constructor.
*
* Construct the room with @c roomId and with Deps support.
*
* `sdk` and `roomId` must be cursors in the same thread.
*
* The constructed room will be in the same thread as `sdk` and `roomId`.
*
* @warning Do not use this directly. Use `Client::room()` and
* `Client::roomBycursor()` instead.
*/
Room(lager::reader<SdkModel> sdk,
lager::reader<std::string> roomId,
ContextT ctx, DepsT deps);
/**
* Construct a Room in the same thread as the event loop.
*
* The constructed Room is not constructed from a cursor,
* and thus copying-constructing from that is thread-safe as long as each thread
* calls with different objects.
*
* this must have Deps support.
*
* @warning Do not use this directly. Use `Client::room()` and
* `Client::roomBycursor()` instead.
*/
Room(InEventLoopTag, std::string roomId, ContextT ctx, DepsT deps);
/**
* Return a Room that represents the room *currently represented* by this,
* but suitable for use in the event loop of the context.
*
* This function can only be called from the thread where this belongs.
*
* Example:
*
* ```
* auto ctx = sdk.context();
* auto client = sdk.clientFromSecondaryRoot(sr);
* auto room = client.room("!room-id:domain.name");
* room.sendTextMessage("test")
* .then([r=room.toEventLoop(), ctx](auto &&st) {
* if (!st) {
* std::cerr << "Cannot send message" << std::endl;
* return ctx.createResolvedPromise(st);
* }
* return r.sendTextMessage("follow-up");
* });
* ```
*
* @sa Sdk::clientFromSecondaryRoot , Client::room
*/
Room toEventLoop() const;
/* lager::reader<MapT<KeyOfState, Event>> */
inline auto stateEvents() const {
return roomCursor()
[&RoomModel::stateEvents];
}
/* lager::reader<std::optional<Event>> */
inline auto stateOpt(KeyOfState k) const {
return stateEvents()
[std::move(k)];
}
/* lager::reader<Event> */
inline auto state(KeyOfState k) const {
return stateOpt(k)
[lager::lenses::or_default];
}
/* lager::reader<RangeT<Event>> */
inline auto timelineEvents() const {
return roomCursor()
.xform(zug::map([](auto r) {
auto messages = r.messages;
auto timeline = r.timeline;
return intoImmer(
immer::flex_vector<Event>{},
zug::map([=](auto eventId) {
return messages[eventId];
}),
timeline);
}));
}
/* lager::reader<std::string> */
inline auto name() const {
using namespace lager::lenses;
return stateEvents()
[KeyOfState{"m.room.name", ""}]
[or_default]
.xform(zug::map([](Event ev) {
auto content = ev.content().get();
return
content.contains("name")
? std::string(content["name"])
// TODO: use heroes to generate a name
: "<no name>";
}));
}
/* lager::reader<std::string> */
inline auto avatarMxcUri() const {
using namespace lager::lenses;
return stateEvents()
[KeyOfState{"m.room.avatar", ""}]
[or_default]
.xform(zug::map([](Event ev) {
auto content = ev.content().get();
return
content.contains("avatar")
? std::string(content["avatar"])
: "";
}));
}
/* lager::reader<RangeT<std::string>> */
inline auto members() const {
return roomCursor().xform(zug::map([=](auto room) {
return room.joinedMemberIds();
}));
}
inline auto memberEventByCursor(lager::reader<std::string> userId) const {
return lager::with(roomCursor()[&RoomModel::stateEvents], userId)
.xform(zug::map([](auto events, auto userId) {
auto k = KeyOfState{"m.room.member", userId};
return events[k];
}));
}
/* lager::reader<std::optional<Event>> */
inline auto memberEventFor(std::string userId) const {
return memberEventByCursor(lager::make_constant(userId));
}
/**
* Get whether this room is encrypted.
*
* The encryption status is changed to true if the client
* receives a state event that turns on encryption.
* If that state event is removed later, the status will
* not be changed.
*
* @return A lager::reader<bool> that contains
* whether this room is encrypted.
*/
lager::reader<bool> encrypted() const;
/*lager::reader<std::string>*/
KAZV_WRAP_ATTR(RoomModel, roomCursor(), roomId);
/*lager::reader<RoomMembership>*/
KAZV_WRAP_ATTR(RoomModel, roomCursor(), membership);
/*lager::reader<std::string>*/
KAZV_WRAP_ATTR(RoomModel, roomCursor(), localDraft);
/* lager::reader<bool> */
KAZV_WRAP_ATTR(RoomModel, roomCursor(), membersFullyLoaded);
+ /**
+ * Get the ids of the heroes of the room.
+ *
+ * @return a lager::reader of a RangeT<std::string> containing
+ * the ids of the heroes of the room.
+ */
+ auto heroIds() const -> lager::reader<immer::flex_vector<std::string>>;
+
/**
* Set local draft for this room.
*
* After the returned Promise is resolved,
* @c localDraft() will contain @c localDraft .
*
* @param localDraft The local draft to send.
* @return A Promise that resolves when the local draft
* has been set, or when there is an error.
*/
PromiseT setLocalDraft(std::string localDraft) const;
/**
* Send an event to this room.
*
* @param msg The message to send
* @return A Promise that resolves when the event has been sent,
* or when there is an error.
*/
PromiseT sendMessage(Event msg) const;
/**
* Send a text message to this room.
*
* @param text The text
* @return A Promise that resolves when the text message has
* been sent, or when there is an error.
*/
PromiseT sendTextMessage(std::string text) const;
/**
* 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];
}
/* lager::reader<RangeT<std::string>> */
inline auto typingUsers() const {
using namespace lager::lenses;
return ephemeral("m.typing")
.xform(eventContent
| jsonAtOr("user_ids",
immer::flex_vector<std::string>{}));
}
/**
* 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{}));
}
/**
* Leave this room.
*
* @return A Promise that resolves when the state event
* for the leaving has been sent, or when there is an error.
*/
PromiseT leave() const;
/**
* Forget this room.
*
* One can only forget a room when they have already left it.
*
* @return A Promise that resolves when the room has been
* forgot, or when there is an error.
*/
PromiseT forget() const;
/**
* Kick a user from this room.
*
* You must have enough power levels in this room to do so.
*
* @param userId The id of the user that will be kicked.
* @param reason The reason to explain this kick.
*
* @return A Promise that resolves when the kick is done,
* or when there is an error.
*/
PromiseT kick(std::string userId, std::optional<std::string> reason = std::nullopt) const;
/**
* Ban a user from this room.
*
* You must have enough power levels in this room to do so.
*
* @param userId The id of the user that will be banned.
* @param reason The reason to explain this ban.
*
* @return A Promise that resolves when the ban is done,
* or when there is an error.
*/
PromiseT ban(std::string userId, std::optional<std::string> reason = std::nullopt) const;
// TODO: v1.1 adds reason field
/**
* Unban a user from this room.
*
* You must have enough power levels in this room to do so.
*
* @param userId The id of the user that will be unbanned.
*
* @return A Promise that resolves when the unban is done,
* or when there is an error.
*/
PromiseT unban(std::string userId/*, std::optional<std::string> reason = std::nullopt*/) const;
/* lager::reader<JsonWrap> */
inline auto avatar() const {
return state(KeyOfState{"m.room.avatar", ""})
.xform(eventContent);
}
/* lager::reader<RangeT<std::string>> */
inline auto pinnedEvents() const {
return state(KeyOfState{"m.room.pinned_events", ""})
.xform(eventContent
| jsonAtOr("pinned", immer::flex_vector<std::string>{}));
}
/**
* Set pinned events of this room
*
* @param eventIds The event ids of the new pinned events
*
* @return A Promise that resolves when the state event
* for the pinned events change has been sent, or when there is an error.
*/
PromiseT setPinnedEvents(immer::flex_vector<std::string> eventIds) const;
/**
* Get the Gaps in the timeline for this room.
*
* Any key of the map in the returned reader can be send as
* an argument of paginateBackFromEvent() to try to fill the Gap
* at that event.
*
* @return A lager::reader that contains an evnetId-to-prevBatch map.
*/
lager::reader<immer::map<std::string /* eventId */, std::string /* prevBatch */>> timelineGaps() const;
/**
* Try to paginate back from @c eventId.
*
* @param eventId An event id that is in the key of `+timelineGaps()`.
*
* @return A Promise that resolves when the pagination is
* successful, or when there is an error. If it is successful,
* `+timelineGaps()` will no longer contain eventId as key, and
* `timeline()` will contain the events before eventId in the
* full event chain on the homeserver.
* If `eventId` is not in `+timelineGaps()`, it is considered
* to be failed.
*/
PromiseT paginateBackFromEvent(std::string eventId) const;
private:
const lager::reader<SdkModel> &sdkCursor() const;
lager::reader<RoomModel> roomCursor() const;
std::string currentRoomId() 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;
KAZV_DECLARE_THREAD_ID();
KAZV_DECLARE_EVENT_LOOP_THREAD_ID_KEEPER(m_deps.has_value() ? &lager::get<EventLoopThreadIdKeeper &>(m_deps.value()) : 0);
};
}
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 7b3bba4..2f8fea0 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -1,53 +1,54 @@
include(CTest)
set(KAZVTEST_RESPATH ${CMAKE_CURRENT_SOURCE_DIR}/resources)
configure_file(kazvtest-respath.hpp.in kazvtest-respath.hpp)
add_executable(kazvtest
testmain.cpp
basejobtest.cpp
event-test.cpp
cursorutiltest.cpp
base/serialization-test.cpp
base/types-test.cpp
client/client-test-util.cpp
client/discovery-test.cpp
client/sync-test.cpp
client/content-test.cpp
client/paginate-test.cpp
client/util-test.cpp
client/serialization-test.cpp
client/encrypted-file-test.cpp
client/sdk-test.cpp
client/thread-safety-test.cpp
client/room-test.cpp
client/random-generator-test.cpp
client/profile-test.cpp
client/kick-test.cpp
client/ban-test.cpp
+ client/room/room-actions-test.cpp
kazvjobtest.cpp
event-emitter-test.cpp
crypto-test.cpp
crypto/deterministic-test.cpp
promise-test.cpp
store-test.cpp
file-desc-test.cpp
)
target_include_directories(
kazvtest
PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(kazvtest
PRIVATE Catch2::Catch2
PRIVATE kazv
PRIVATE kazveventemitter
PRIVATE kazvjob
PRIVATE nlohmann_json::nlohmann_json
PRIVATE immer
PRIVATE lager
PRIVATE zug)
diff --git a/src/tests/client/room/room-actions-test.cpp b/src/tests/client/room/room-actions-test.cpp
new file mode 100644
index 0000000..23b7f40
--- /dev/null
+++ b/src/tests/client/room/room-actions-test.cpp
@@ -0,0 +1,33 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2021 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+
+#include <catch2/catch.hpp>
+
+#include <room/room-model.hpp>
+
+using namespace Kazv;
+
+TEST_CASE("SetHeroIdsAction", "[client][room]")
+{
+ RoomModel r;
+ r.heroIds = immer::flex_vector<std::string>{"@u:test.org"};
+
+ auto newHeroes = immer::flex_vector<std::string>{"@a:test.org", "@b:test.org"};
+
+ WHEN("We update the model") {
+ auto res = RoomModel::update(r, SetHeroIdsAction{newHeroes});
+
+ THEN("it should update hero ids") {
+ REQUIRE(res.heroIds == newHeroes);
+ }
+
+ THEN("it should compare different than the original") {
+ REQUIRE(!(res == r));
+ }
+ }
+}
diff --git a/src/tests/client/sync-test.cpp b/src/tests/client/sync-test.cpp
index c680a06..d64273f 100644
--- a/src/tests/client/sync-test.cpp
+++ b/src/tests/client/sync-test.cpp
@@ -1,337 +1,343 @@
/*
* 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 <catch2/catch.hpp>
#include <boost/asio.hpp>
#include <zug/into_vector.hpp>
#include <asio-promise-handler.hpp>
#include <cursorutil.hpp>
#include <sdk-model.hpp>
#include <client/client.hpp>
#include "client-test-util.hpp"
// The example response is adapted from https://matrix.org/docs/spec/client_server/latest
static json syncResponseJson = R"({
"next_batch": "s72595_4483_1934",
"presence": {
"events": [
{
"content": {
"avatar_url": "mxc://localhost:wefuiwegh8742w",
"last_active_ago": 2478593,
"presence": "online",
"currently_active": false,
"status_msg": "Making cupcakes"
},
"type": "m.presence",
"sender": "@example:localhost"
}
]
},
"account_data": {
"events": [
{
"type": "org.example.custom.config",
"content": {
"custom_config_key": "custom_config_value"
}
}
]
},
"rooms": {
"join": {
"!726s6s6q:example.com": {
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.joined_member_count": 2,
"m.invited_member_count": 0
},
"state": {
"events": [
{
"content": {
"membership": "join",
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid"
},
"type": "m.room.member",
"event_id": "$143273582443PhrSn:example.org",
"room_id": "!726s6s6q:example.com",
"sender": "@example:example.org",
"origin_server_ts": 1432735824653,
"unsigned": {
"age": 1234
},
"state_key": "@alice:example.org"
}
]
},
"timeline": {
"events": [
{
"content": {
"membership": "join",
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid"
},
"type": "m.room.member",
"event_id": "$143273582443PhrSn:example.org",
"room_id": "!726s6s6q:example.com",
"sender": "@example:example.org",
"origin_server_ts": 1432735824653,
"unsigned": {
"age": 1234
},
"state_key": "@alice:example.org"
},
{
"content": {
"body": "This is an example text message",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>"
},
"type": "m.room.message",
"event_id": "$anothermessageevent:example.org",
"room_id": "!726s6s6q:example.com",
"sender": "@example:example.org",
"origin_server_ts": 1432735824653,
"unsigned": {
"age": 1234
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"type": "m.typing",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
}
]
},
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"type": "org.example.custom.room.config",
"content": {
"custom_config_key": "custom_config_value"
}
},
{
"type": "m.fully_read",
"content": {
"event_id": "$anothermessageevent:example.org"
}
}
]
}
}
},
"invite": {
"!696r7674:example.com": {
"invite_state": {
"events": [
{
"sender": "@alice:example.com",
"type": "m.room.name",
"state_key": "",
"content": {
"name": "My Room Name"
}
},
{
"sender": "@alice:example.com",
"type": "m.room.member",
"state_key": "@bob:example.com",
"content": {
"membership": "invite"
}
}
]
}
}
},
"leave": {}
},
"to_device": {
"events": [
{
"sender": "@alice:example.com",
"type": "m.new_device",
"content": {
"device_id": "XYZABCDE",
"rooms": ["!726s6s6q:example.com"]
}
}
]
}
})"_json;
static json stateInTimelineResponseJson = R"({
"next_batch": "some-example-value",
"rooms": {
"join": {
"!exampleroomid:example.com": {
"timeline": {
"events": [
{
"content": { "example": "foo" },
"state_key": "",
"event_id": "$example:example.com",
"sender": "@example:example.org",
"origin_server_ts": 1432735824653,
"unsigned": { "age": 1234 },
"type": "moe.kazv.mxc.custom.state.type"
}
],
"limited": false
}
}
}
}
})"_json;
TEST_CASE("use sync response to update client model", "[client][sync]")
{
using namespace Kazv::CursorOp;
boost::asio::io_context io;
AsioPromiseHandler ph{io.get_executor()};
auto store = createTestClientStore(ph);
auto resp = createResponse("Sync", syncResponseJson, json{{"is", "initial"}});
auto client = Client(store.reader().map([](auto c) { return SdkModel{c}; }), store,
std::nullopt);
store.dispatch(ProcessResponseAction{resp});
io.run();
auto rooms = +client.rooms();
std::string roomId = "!726s6s6q:example.com";
SECTION("rooms should be added") {
REQUIRE(rooms.find(roomId));
}
auto r = client.room(roomId);
SECTION("room members should be updated") {
auto members = +r.members();
auto hasAlice = zug::into_vector(
zug::filter([](auto id) { return id == "@alice:example.org"; }),
members)
.size() > 0;
REQUIRE(hasAlice);
}
+ SECTION("heroes should be updated") {
+ auto heroIds = +r.heroIds();
+
+ REQUIRE(heroIds == immer::flex_vector<std::string>{"@alice:example.com", "@bob:example.com"});
+ }
+
SECTION("ephemeral events should be updated") {
auto users = +r.typingUsers();
REQUIRE((users == immer::flex_vector<std::string>{
"@alice:matrix.org",
"@bob:example.com"
}));
}
auto eventId = "$anothermessageevent:example.org"s;
SECTION("timeline should be updated") {
auto timeline = +r.timelineEvents();
auto filtered = zug::into_vector(
zug::filter([=](auto event) { return event.id() == eventId; }),
timeline);
auto hasEvent = filtered.size() > 0;
REQUIRE(hasEvent);
auto onlyOneEvent = filtered.size() == 1;
REQUIRE(onlyOneEvent);
auto ev = filtered[0];
auto eventHasRoomId = ev.originalJson().get().contains("room_id"s);
REQUIRE(eventHasRoomId);
auto gaps = +r.timelineGaps();
// first event in the batch, correspond to its prevBatch
REQUIRE(gaps.at("$143273582443PhrSn:example.org") == "t34-23535_0_0");
}
SECTION("fully read marker should be updated") {
auto readMarker = +r.readMarker();
REQUIRE(readMarker == eventId);
}
SECTION("toDevice should be updated") {
auto toDevice = +client.toDevice();
REQUIRE(toDevice.size() == 1);
REQUIRE(toDevice[0].sender() == "@alice:example.com");
}
}
TEST_CASE("Sync should record state events in timeline", "[client][sync]")
{
using namespace Kazv::CursorOp;
boost::asio::io_context io;
AsioPromiseHandler ph{io.get_executor()};
auto store = createTestClientStore(ph);
auto resp = createResponse("Sync", stateInTimelineResponseJson, json{{"is", "initial"}});
auto client = Client(store.reader().map([](auto c) { return SdkModel{c}; }), store,
std::nullopt);
store.dispatch(ProcessResponseAction{resp});
io.run();
auto r = client.room("!exampleroomid:example.com");
auto stateOpt = +r.stateOpt(KeyOfState{"moe.kazv.mxc.custom.state.type", ""});
REQUIRE(stateOpt.has_value());
REQUIRE(stateOpt.value().content().get().at("example") == "foo");
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 19, 1:56 PM (18 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55213
Default Alt Text
(73 KB)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline
Log In to Comment