Page MenuHomePhorge

No OneTemporary

Size
124 KB
Referenced Files
None
Subscribers
None
diff --git a/src/client/actions/encryption.cpp b/src/client/actions/encryption.cpp
index 29174e9..5a1112f 100644
--- a/src/client/actions/encryption.cpp
+++ b/src/client/actions/encryption.cpp
@@ -1,634 +1,634 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <zug/transducer/filter.hpp>
#include <zug/transducer/cat.hpp>
#include "encryption.hpp"
#include <immer-utils.hpp>
#include <debug.hpp>
#include "cursorutil.hpp"
#include "status-utils.hpp"
namespace Kazv
{
using namespace CryptoConstants;
static json convertSignature(const ClientModel &m, std::string signature)
{
auto j = json::object();
j[m.userId] = json::object();
j[m.userId][ed25519 + ":" + m.deviceId] = signature;
return j;
}
ClientResult updateClient(ClientModel m, UploadIdentityKeysAction)
{
if (! m.crypto) {
kzo.client.warn() << "Client::crypto is invalid, ignoring it." << std::endl;
return { std::move(m), lager::noop };
}
auto keys =
immer::map<std::string, std::string>{}
.set(ed25519 + ":" + m.deviceId, m.constCrypto().ed25519IdentityKey())
.set(curve25519 + ":" + m.deviceId, m.constCrypto().curve25519IdentityKey());
DeviceKeys k {
m.userId,
m.deviceId,
{olmAlgo, megOlmAlgo},
keys,
{} // signatures to be added soon
};
auto j = json(k);
auto sig = m.withCrypto([&](auto &crypto) { return crypto.sign(j); });
k.signatures = convertSignature(m, sig);
auto job = m.job<UploadKeysJob>()
.make(k)
.withData(json{{"is", "identityKeys"}});
kzo.client.dbg() << "Uploading identity keys" << std::endl;
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, GenerateAndUploadOneTimeKeysAction a)
{
if (! m.crypto) {
kzo.client.warn() << "Client::crypto is invalid, ignoring it." << std::endl;
return { std::move(m), simpleFail };
}
kzo.client.dbg() << "Generating one-time keys..." << std::endl;
auto maxNumKeys = m.constCrypto().maxNumberOfOneTimeKeys();
auto numLocalKeys = m.constCrypto().numUnpublishedOneTimeKeys();
auto numStoredKeys = m.constCrypto().uploadedOneTimeKeysCount(signedCurve25519) + numLocalKeys;
auto numKeysToGenerate = a.numToGen;
auto genKeysLimit = maxNumKeys - numStoredKeys;
if (numKeysToGenerate > genKeysLimit) {
numKeysToGenerate = genKeysLimit;
}
if (numLocalKeys <= 0 && numKeysToGenerate <= 0) { // we have enough already
kzo.client.dbg() << "We have enough one-time keys. Ignoring this." << std::endl;
return { std::move(m), lager::noop };
}
if (numKeysToGenerate > 0) {
m.withCrypto([&](auto &c) { c.genOneTimeKeysWithRandom(a.random, numKeysToGenerate); });
}
kzo.client.dbg() << "Generating done." << std::endl;
auto keys = m.constCrypto().unpublishedOneTimeKeys();
auto cv25519Keys = keys.at(curve25519);
json oneTimeKeys = json::object();
for (auto [id, keyStr] : cv25519Keys.items()) {
json keyObject = json::object();
keyObject["key"] = keyStr;
keyObject["signatures"] = convertSignature(m, m.withCrypto([&](auto &c) { return c.sign(keyObject); }));
oneTimeKeys[signedCurve25519 + ":" + id] = keyObject;
}
auto job = m.job<UploadKeysJob>()
.make(
std::nullopt, // deviceKeys
oneTimeKeys)
.withData(json{{"is", "oneTimeKeys"}});
kzo.client.dbg() << "Uploading one time keys" << std::endl;
m.addJob(std::move(job));
return { std::move(m), lager::noop };
};
ClientResult processResponse(ClientModel m, UploadKeysResponse r)
{
if (! m.crypto) {
kzo.client.warn() << "Client::crypto is invalid, ignoring it." << std::endl;
return { std::move(m), lager::noop };
}
auto is = r.dataStr("is");
if (is == "identityKeys") {
if (! r.success()) {
kzo.client.dbg() << "Uploading identity keys failed" << std::endl;
m.addTrigger(UploadIdentityKeysFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), failWithResponse(r) };
}
kzo.client.dbg() << "Uploading identity keys successful" << std::endl;
m.addTrigger(UploadIdentityKeysSuccessful{});
m.identityKeysUploaded = true;
} else {
if (! r.success()) {
kzo.client.dbg() << "Uploading one-time keys failed" << std::endl;
m.addTrigger(UploadOneTimeKeysFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), failWithResponse(r) };
}
kzo.client.dbg() << "Uploading one-time keys successful" << std::endl;
m.addTrigger(UploadOneTimeKeysSuccessful{});
m.withCrypto([&](auto &c) { c.markOneTimeKeysAsPublished(); });
}
m.withCrypto([&](auto &c) { c.setUploadedOneTimeKeysCount(r.oneTimeKeyCounts()); });
return { std::move(m), lager::noop };
}
static JsonWrap cannotDecryptEvent(std::string reason)
{
return json{
{"type", "m.room.message"},
{"content", {
{"msgtype","moe.kazv.mxc.cannot.decrypt"},
{"body", "**This message cannot be decrypted due to " + reason + ".**"}}}};
}
static bool verifyEvent(ClientModel &m, Event e, const json &plainJson)
{
bool valid = true;
try {
std::string senderCurve25519Key = e.originalJson().get()
.at("content").at("sender_key");
auto deviceInfoOpt = m.deviceLists.findByCurve25519Key(e.sender(), senderCurve25519Key);
if (! deviceInfoOpt) {
kzo.client.dbg() << "Device key " << senderCurve25519Key
<< " unknown, thus invalid" << std::endl;
valid = false;
}
auto deviceInfo = deviceInfoOpt.value();
std::string algo = e.originalJson().get().at("content").at("algorithm");
if (algo == olmAlgo) {
if (! (plainJson.at("sender") == e.sender())) {
kzo.client.dbg() << "Sender does not match, thus invalid" << std::endl;
valid = false;
}
if (! (plainJson.at("recipient") == m.userId)) {
kzo.client.dbg() << "Recipient does not match, thus invalid" << std::endl;
valid = false;
}
if (! (plainJson.at("recipient_keys").at(ed25519) == m.constCrypto().ed25519IdentityKey())) {
kzo.client.dbg() << "Recipient key does not match, thus invalid" << std::endl;
valid = false;
}
auto thisEd25519Key = plainJson.at("keys").at(ed25519).get<std::string>();
if (thisEd25519Key != deviceInfo.ed25519Key) {
kzo.client.dbg() << "Sender ed25519 key does not match, thus invalid" << std::endl;
valid = false;
}
} else if (algo == megOlmAlgo) {
if (! (plainJson.at("room_id").get<std::string>() ==
e.originalJson().get().at("room_id").get<std::string>())) {
kzo.client.dbg() << "Room id does not match, thus invalid" << std::endl;
valid = false;
}
if (e.originalJson().get().at("content").at("device_id").get<std::string>()
!= deviceInfo.deviceId) {
kzo.client.dbg() << "Device id does not match, thus invalid" << std::endl;
valid = false;
}
auto actualEd25519Key = m.constCrypto().getInboundGroupSessionEd25519KeyFromEvent(e.originalJson().get());
if ((! actualEd25519Key)
|| deviceInfo.ed25519Key != actualEd25519Key.value()) {
kzo.client.dbg() << "sender ed25519 key does not match, thus invalid" << std::endl;
kzo.client.dbg() << "From group session: "
<< (actualEd25519Key ? actualEd25519Key.value() : "<none>") << std::endl;
kzo.client.dbg() << "From device info: " << deviceInfo.ed25519Key << std::endl;
valid = false;
}
} else {
kzo.client.dbg() << "Unknown algorithm, thus invalid" << std::endl;
valid = false;
}
} catch (const std::exception &) {
kzo.client.dbg() << "json format is not correct, thus invalid" << std::endl;
valid = false;
}
return valid;
}
static Event decryptEvent(ClientModel &m, Event e)
{
// no need for decryption
if (e.decrypted() || (! e.encrypted())) {
return e;
}
kzo.client.dbg() << "About to decrypt event: "
<< e.id() << std::endl;
auto maybePlainText = m.withCrypto([&](Crypto &c) {
return c.decrypt(e.originalJson().get());
});
if (! maybePlainText) {
kzo.client.dbg() << "Cannot decrypt: " << maybePlainText.reason() << std::endl;
return e.setDecryptedJson(cannotDecryptEvent(maybePlainText.reason()), Event::NotDecrypted);
} else {
auto plainJson = json::parse(maybePlainText.value());
auto valid = verifyEvent(m, e, plainJson);
if (valid) {
kzo.client.dbg() << "The decrypted event is valid." << std::endl;
}
return valid
? e.setDecryptedJson(plainJson, Event::Decrypted)
: e.setDecryptedJson(cannotDecryptEvent("invalid event"), Event::NotDecrypted);
}
}
ClientModel tryDecryptEvents(ClientModel m)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring decryption request" << std::endl;
return m;
}
kzo.client.dbg() << "Trying to decrypt events..." << std::endl;
auto decryptFunc = [&](auto e) { return decryptEvent(m, e); };
auto takeOutRoomKeyEvents =
[&](auto e) {
if (e.type() != "m.room_key") {
// Leave it as it is
return true;
}
try {
auto content = e.content();
std::string roomId = content.get().at("room_id");
std::string sessionId = content.get().at("session_id");
std::string sessionKey = content.get().at("session_key");
std::string senderKey = e.originalJson().get().at("content").at("sender_key");
auto k = KeyOfGroupSession{roomId, senderKey, sessionId};
std::string ed25519Key = e.decryptedJson().get().at("keys").at(ed25519);
if (m.withCrypto([&](auto &c) { return c.createInboundGroupSession(k, sessionKey, ed25519Key); })) {
return false; // such that this event is removed
}
} catch (...) {
kzo.client.dbg() << "cannot create group session";
return false;
}
return true;
};
m.toDevice = intoImmer(
EventList{},
zug::map(decryptFunc)
| zug::filter(takeOutRoomKeyEvents),
std::move(m.toDevice));
auto decryptEventInRoom =
[&](auto id, auto room) {
if (! room.encrypted) {
return;
} else {
auto messages = room.messages;
room.messages = merge(
room.messages,
intoImmer(
EventList{},
zug::filter([](auto n) {
auto e = n.second;
return e.encrypted();
})
| zug::map([=](auto n) {
auto event = n.second;
return decryptFunc(event);
}),
std::move(messages)),
keyOfTimeline);
m.roomList.rooms = std::move(m.roomList.rooms).set(id, room);
}
};
auto rooms = m.roomList.rooms;
for (auto [id, room]: rooms) {
decryptEventInRoom(id, room);
}
return m;
}
std::optional<BaseJob> clientPerform(ClientModel m, QueryKeysAction a)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return std::nullopt;
}
immer::map<std::string, immer::array<std::string>> deviceKeys;
auto encryptedUsers = m.deviceLists.outdatedUsers();
if (encryptedUsers.empty()) {
kzo.client.dbg() << "Keys are up-to-date." << std::endl;
return std::nullopt;
}
kzo.client.dbg() << "We need to query keys for: " << std::endl;
for (auto userId: encryptedUsers) {
kzo.client.dbg() << userId << std::endl;
deviceKeys = std::move(deviceKeys).set(userId, {});
}
kzo.client.dbg() << "^" << std::endl;
auto job = m.job<QueryKeysJob>()
.make(std::move(deviceKeys),
std::nullopt, // timeout
a.isInitialSync ? std::nullopt : m.syncToken
);
return job;
}
ClientResult updateClient(ClientModel m, QueryKeysAction a)
{
auto jobOpt = clientPerform(m, a);
if (jobOpt) {
m.addJob(jobOpt.value());
}
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, QueryKeysResponse r)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return { std::move(m), simpleFail };
}
if (! r.success()) {
kzo.client.dbg() << "query keys failed: " << r.errorCode() << r.errorMessage() << std::endl;
return { std::move(m), failWithResponse(r) };
}
kzo.client.dbg() << "Received a query key response" << std::endl;
auto usersMap = r.deviceKeys();
for (auto [userId, deviceMap] : usersMap) {
for (auto [deviceId, deviceInfo] : deviceMap) {
kzo.client.dbg() << "Key for " << userId
<< "/" << deviceId
<< ": " << json(deviceInfo).dump()
<< std::endl;
m.withCrypto([&](Crypto &c) {
m.deviceLists.addDevice(userId, deviceId, deviceInfo, c);
});
}
m.deviceLists.markUpToDate(userId);
}
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, ClaimKeysAction a)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return { std::move(m), lager::noop };
}
kzo.client.dbg() << "claim keys for: " << json(a.devicesToSend).dump() << std::endl;
auto keyMap = immer::map<std::string, immer::map<std::string /* deviceId */,
std::string /* curve25519IdentityKey */>>{};
for (auto [userId, devices] : a.devicesToSend) {
kzo.client.dbg() << "Iterating through user " << userId << std::endl;
auto deviceToKey = immer::map<std::string, std::string>{};
for (auto deviceId : devices) {
kzo.client.dbg() << "Device: " << deviceId << std::endl;
auto infoOpt = m.deviceLists.get(userId, deviceId);
if (infoOpt) {
kzo.client.dbg() << "Got device info, curve25519 key is: " << infoOpt.value().curve25519Key << std::endl;
deviceToKey = std::move(deviceToKey)
.set(deviceId, infoOpt.value().curve25519Key);
} else {
kzo.client.dbg() << "Did not get device info" << std::endl;
}
}
keyMap = std::move(keyMap).set(userId, deviceToKey);
}
auto devicesToClaimKeys = m.withCrypto([&](auto &c) { return c.devicesMissingOutboundSessionKey(keyMap); });
kzo.client.dbg() << "Really claim keys for: " << json(devicesToClaimKeys).dump() << std::endl;
auto oneTimeKeys = immer::map<std::string, immer::map<std::string, std::string>>{};
for (auto [userId, devices] : devicesToClaimKeys) {
auto devKeys = immer::map<std::string, std::string>{};
for (auto deviceId: devices) {
devKeys = std::move(devKeys).set(deviceId, signedCurve25519);
}
oneTimeKeys = std::move(oneTimeKeys).set(userId, devKeys);
}
auto job = m.job<ClaimKeysJob>()
.make(std::move(oneTimeKeys))
.withData(json{
{"roomId", a.roomId},
{"sessionId", a.sessionId},
{"sessionKey", a.sessionKey},
{"devicesToSend", a.devicesToSend},
{"random", a.random}
});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, ClaimKeysResponse r)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return { std::move(m), simpleFail };
}
if (! r.success()) {
kzo.client.dbg() << "claim keys failed" << std::endl;
m.addTrigger(ClaimKeysFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), failWithResponse(r) };
}
kzo.client.dbg() << "claim keys successful" << std::endl;
kzo.client.dbg() << "Json body: " << r.jsonBody().get().dump() << std::endl;
auto roomId = r.dataStr("roomId");
auto sessionKey = r.dataStr("sessionKey");
auto sessionId = r.dataStr("sessionId");
auto devicesToSend =
immer::map<std::string, immer::flex_vector<std::string>>(r.dataJson("devicesToSend"));
auto random = r.dataJson("random").template get<RandomData>();
// create outbound sessions for those devices
auto oneTimeKeys = r.oneTimeKeys();
for (auto [userId, deviceMap] : oneTimeKeys) {
for (auto [deviceId, keyVar] : deviceMap) {
auto keys = keyVar.get();
for (auto [keyId, key] : keys.items()) {
auto deviceInfoOpt = m.deviceLists.get(userId, deviceId);
if (deviceInfoOpt) {
auto deviceInfo = deviceInfoOpt.value();
kzo.client.dbg() << "Verifying key for " << userId
<< "/" << deviceId
<< key.dump()
<< " with ed25519 key "
<< deviceInfo.ed25519Key << std::endl;
auto verified = m.withCrypto([&](auto &c) { return c.verify(key, userId, deviceId, deviceInfo.ed25519Key); });
kzo.client.dbg() << (verified ? "passed" : "did not pass") << std::endl;
if (verified && key.contains("key")) {
auto theirOneTimeKey = key.at("key");
kzo.client.dbg() << "creating outbound session for it" << std::endl;
m.withCrypto([&](auto &c) { c.createOutboundSessionWithRandom(random, deviceInfo.curve25519Key, theirOneTimeKey); });
random.erase(0, Crypto::createOutboundSessionRandomSize());
kzo.client.dbg() << "done" << std::endl;
}
}
}
}
}
auto eventJson = json{
{"content", {{"algorithm", megOlmAlgo},
{"room_id", roomId},
{"session_id", sessionId},
{"session_key", sessionKey}}},
{"type", "m.room_key"}
};
auto event = Event(JsonWrap(eventJson));
return {
std::move(m),
[event](auto &&) { return EffectStatus{ /* success = */ true, json{{ "keyEvent", event.originalJson() }} }; }
};
}
ClientResult updateClient(ClientModel m, EncryptMegOlmEventAction a)
{
auto [encryptedEvent, maybeKey] = m.megOlmEncrypt(a.e, a.roomId, a.timeMs, a.random);
return {
std::move(m),
[=](auto && /* ctx */) {
auto retJson = json::object({
{"encrypted", encryptedEvent.originalJson()},
});
if (maybeKey.has_value()) {
retJson["key"] = maybeKey.value();
}
return EffectStatus(/* succ = */ true, retJson);
}
};
}
ClientResult updateClient(ClientModel m, EncryptOlmEventAction a)
{
auto encryptedEvent = m.olmEncrypt(a.e, a.devices, a.random);
return {
std::move(m),
[=](auto && /* ctx */) {
auto retJson = json::object({
{"encrypted", encryptedEvent.originalJson()},
});
return EffectStatus(/* succ = */ true, retJson);
}
};
}
ClientResult updateClient(ClientModel m, SetDeviceTrustLevelAction a)
{
auto maybeOldInfo = m.deviceLists.get(a.userId, a.deviceId);
if (!maybeOldInfo) {
return {
std::move(m),
[=](auto && /* ctx */) {
auto retJson = json::object({
{"error", "No such device"},
{"errorCode", "MOE_KAZV_MXC_KAZV_NO_SUCH_DEVICE"},
});
return EffectStatus(/* succ = */ false, retJson);
}
};
}
m.deviceLists.deviceLists = updateIn(
std::move(m.deviceLists.deviceLists),
[a](auto device) {
device.trustLevel = a.trustLevel;
return device;
},
a.userId,
a.deviceId
);
return { m, lager::noop };
}
ClientResult updateClient(ClientModel m, SetTrustLevelNeededToSendKeysAction a)
{
m.trustLevelNeededToSendKeys = a.trustLevel;
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, PrepareForSharingRoomKeyAction a)
{
- auto encryptedEvent = m.olmEncrypt(a.e, a.devices, a.random);
+ auto messages = m.olmEncryptSplit(a.e, a.devices, a.random);
- auto txnId = getTxnId(a.e, m);
+ auto txnId = getTxnId(Event(), m);
m.roomList = RoomListModel::update(
std::move(m.roomList),
UpdateRoomAction{
a.roomId,
AddPendingRoomKeyAction{
- PendingRoomKeyEvent{txnId, encryptedEvent, a.devices}
+ PendingRoomKeyEvent{txnId, messages}
}
}
);
return { std::move(m), [txnId](auto &&) {
return EffectStatus(/* succ = */ true, json::object({{"txnId", txnId}}));
} };
}
}
diff --git a/src/client/room/room-model.cpp b/src/client/room/room-model.cpp
index 179aa7f..3412bf8 100644
--- a/src/client/room/room-model.cpp
+++ b/src/client/room/room-model.cpp
@@ -1,559 +1,575 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa
* 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"};
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);
}
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);
};
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);
// 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;
// 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) {
const auto &json = e.originalJson().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);
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;
}
);
}
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));
}
}
diff --git a/src/client/room/room-model.hpp b/src/client/room/room-model.hpp
index c99d6fa..fc6dc34 100644
--- a/src/client/room/room-model.hpp
+++ b/src/client/room/room-model.hpp
@@ -1,419 +1,430 @@
/*
* 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 "local-echo.hpp"
#include "clientutil.hpp"
namespace Kazv
{
struct PendingRoomKeyEvent
{
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;
};
+ 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;
};
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 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;
};
inline bool operator==(const PendingRoomKeyEvent &a, const PendingRoomKeyEvent &b)
{
- return a.txnId == b.txnId && a.event == b.event && a.devices == b.devices;
+ 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)
{
- ar & e.txnId & e.event & e.devices;
+ 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;
+ }
}
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.
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;
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();
using Action = std::variant<
AddStateEventsAction,
AppendTimelineAction,
PrependTimelineAction,
AddToTimelineAction,
AddAccountDataAction,
ChangeMembershipAction,
ChangeInviteStateAction,
AddEphemeralAction,
SetLocalDraftAction,
SetRoomEncryptionAction,
MarkMembersFullyLoadedAction,
SetHeroIdsAction,
AddLocalEchoAction,
RemoveLocalEchoAction,
AddPendingRoomKeyAction,
RemovePendingRoomKeyAction,
UpdateJoinedMemberCountAction,
UpdateInvitedMemberCountAction
>;
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
;
}
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;
}
}
template<class Archive>
void serialize(Archive &ar, RoomListModel &l, std::uint32_t const /*version*/)
{
ar & l.rooms;
}
}
-BOOST_CLASS_VERSION(Kazv::PendingRoomKeyEvent, 0)
+BOOST_CLASS_VERSION(Kazv::PendingRoomKeyEvent, 1)
BOOST_CLASS_VERSION(Kazv::ReadReceipt, 0)
BOOST_CLASS_VERSION(Kazv::RoomModel, 6)
BOOST_CLASS_VERSION(Kazv::RoomListModel, 0)
diff --git a/src/client/room/room.cpp b/src/client/room/room.cpp
index a99208b..29f451c 100644
--- a/src/client/room/room.cpp
+++ b/src/client/room/room.cpp
@@ -1,883 +1,882 @@
/*
* 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::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::heroIds() const
-> lager::reader<immer::flex_vector<std::string>>
{
return roomCursor().map([](const auto &room) {
return room.heroIds;
});
}
auto Room::joinedMemberCount() const -> lager::reader<std::size_t>
{
return roomCursor()[&RoomModel::joinedMemberCount];
}
auto Room::invitedMemberCount() const -> lager::reader<std::size_t>
{
return roomCursor()[&RoomModel::invitedMemberCount];
}
auto Room::avatarMxcUri() const -> lager::reader<std::string>
{
using namespace lager::lenses;
return inviteStateOrState()
[KeyOfState{"m.room.avatar", ""}]
[or_default]
.map([](Event ev) {
auto content = ev.content().get();
return content.contains("url")
? std::string(content["url"])
: "";
});
}
auto Room::setLocalDraft(std::string localDraft) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(UpdateRoomAction{+roomId(), SetLocalDraftAction{localDraft}});
}
auto Room::sendMessage(Event msg) const
-> PromiseT
{
using namespace CursorOp;
auto hasCrypto = ~sdkCursor().map([](const auto &sdk) -> bool {
return sdk.c().crypto.has_value();
});
auto roomEncrypted = ~roomCursor()[&RoomModel::encrypted];
auto noFullMembers = ~roomCursor()[&RoomModel::membersFullyLoaded]
.map([](auto b) { return !b; });
auto rid = +roomId();
// Don't use m_ctx directly in the callbacks
// as `this` may have been destroyed when
// the callbacks are called.
auto ctx = m_ctx;
auto promise = ctx.createResolvedPromise(true);
// If we do not need encryption just send it as-is
if (! +allCursors(hasCrypto, roomEncrypted)) {
return promise
.then([ctx, rid, msg](auto succ) {
if (! succ) {
return ctx.createResolvedPromise(false);
}
return ctx.dispatch(SendMessageAction{rid, msg});
});
}
if (! m_deps) {
return ctx.createResolvedPromise({false, json{{"error", "missing-deps"}}});
}
auto deps = m_deps.value();
promise = promise.then([ctx, rid, msg](auto) {
return ctx.dispatch(SaveLocalEchoAction{rid, msg});
});
auto saveLocalEchoPromise = promise;
// If the room member list is not complete, load it fully first.
if (+allCursors(hasCrypto, roomEncrypted, noFullMembers)) {
kzo.client.dbg() << "The members of " << rid
<< " are not fully loaded." << std::endl;
promise = promise
.then([ctx, rid](auto) { // SaveLocalEchoAction can't fail
return ctx.dispatch(GetRoomStatesAction{rid});
})
.then([ctx, rid](auto succ) {
if (! succ) {
kzo.client.warn() << "Loading members of " << rid
<< " failed." << std::endl;
return ctx.createResolvedPromise(false);
} else {
// XXX remove the hard-coded initialSync parameter
return ctx.dispatch(QueryKeysAction{true})
.then([](auto succ) {
if (! succ) {
kzo.client.warn() << "Query keys failed" << std::endl;
}
return succ;
});
}
});
}
auto encryptEvent = [ctx, rid, msg, deps](auto &&status) {
if (! status) { return ctx.createResolvedPromise(status); }
kzo.client.dbg() << "encrypting megolm" << std::endl;
auto &rg = lager::get<RandomInterface &>(deps);
return ctx.dispatch(EncryptMegOlmEventAction{
rid,
msg,
currentTimeMs(),
rg.generateRange<RandomData>(EncryptMegOlmEventAction::maxRandomSize())
});
};
auto saveEncryptedLocalEcho = [saveLocalEchoPromise, msg, rid, ctx](auto &&status) mutable {
if (! status) { return ctx.createResolvedPromise(status); }
return saveLocalEchoPromise
.then([status, msg, rid, ctx](auto &&st) {
auto txnId = st.dataStr("txnId");
kzo.client.dbg() << "saving encrypted local echo with txn id " << txnId << std::endl;
auto encrypted = status.dataJson("encrypted");
auto decrypted = msg.originalJson();
auto event = Event(encrypted).setDecryptedJson(decrypted, Event::Decrypted);
return ctx.dispatch(SaveLocalEchoAction{rid, event, txnId});
})
.then([status](auto &&) {
return status;
});
};
auto maybeSendKeys = [ctx, rid, r=toEventLoop(), deps](auto status) {
if (! status) { return ctx.createResolvedPromise(status); }
auto encryptedEvent = status.dataJson("encrypted");
auto content = encryptedEvent.at("content");
auto ret = ctx.createResolvedPromise({});
if (status.data().get().contains("key")) {
kzo.client.dbg() << "megolm session rotated, sending session key" << std::endl;
auto key = status.dataStr("key");
ret = ret
.then([rid, r, key, ctx, deps, sessionId=content.at("session_id")](auto &&) {
auto members = (+r.roomCursor()).joinedMemberIds();
auto client = (+r.sdkCursor()).c();
using DeviceMapT = immer::map<std::string, immer::flex_vector<std::string>>;
auto devicesToSend = accumulate(
members, DeviceMapT{}, [client](auto map, auto uid) {
return std::move(map)
.set(uid, client.devicesToSendKeys(uid));
});
auto &rg = lager::get<RandomInterface &>(deps);
return ctx.dispatch(ClaimKeysAction{
rid, sessionId, key, devicesToSend,
rg.generateRange<RandomData>(ClaimKeysAction::randomSize(devicesToSend))
})
.then([rid, ctx, deps, devicesToSend](auto status) {
if (! status) { return ctx.createResolvedPromise({}); }
kzo.client.dbg() << "olm-encrypting key event" << std::endl;
auto keyEv = status.dataJson("keyEvent");
auto &rg = lager::get<RandomInterface &>(deps);
return ctx.dispatch(PrepareForSharingRoomKeyAction{
rid,
devicesToSend, keyEv,
rg.generateRange<RandomData>(EncryptOlmEventAction::randomSize(devicesToSend))
});
})
.then([ctx, r, devicesToSend, rid](auto status) {
if (! status) { return ctx.createResolvedPromise({}); }
auto txnId = status.dataStr("txnId");
return r.sendPendingKeyEvent(txnId);
});
});
}
return ret
.then([ctx, prevStatus=status](auto status) {
if (! status) { return status; }
return prevStatus;
});
};
auto sendEncryptedEvent = [ctx, rid, saveLocalEchoPromise, msg](auto status) mutable {
if (! status) { return ctx.createResolvedPromise(status); }
return saveLocalEchoPromise.then([ctx, rid, status, msg](auto st) {
auto txnId = st.dataStr("txnId");
kzo.client.dbg() << "sending encrypted message with txn id " << txnId << std::endl;
auto encrypted = status.dataJson("encrypted");
auto decrypted = msg.originalJson();
auto event = Event(encrypted).setDecryptedJson(decrypted, Event::Decrypted);
return ctx.dispatch(SendMessageAction{rid, event, txnId});
});
};
auto maybeSetStatusToFailed = [ctx, saveLocalEchoPromise, rid](const auto &st) mutable {
if (st.success()) {
return ctx.createResolvedPromise(st);
}
return saveLocalEchoPromise
.then([rid, ctx](const auto &saveLocalEchoStatus) {
auto txnId = saveLocalEchoStatus.dataStr("txnId");
return ctx.dispatch(UpdateLocalEchoStatusAction{rid, txnId, LocalEchoDesc::Failed});
})
.then([st](const auto &) { // can't fail
return st;
});
};
return promise
.then(encryptEvent)
.then(saveEncryptedLocalEcho)
.then(maybeSendKeys)
.then(sendEncryptedEvent)
.then(maybeSetStatusToFailed);
}
auto Room::sendTextMessage(std::string text) const
-> PromiseT
{
json j{
{"type", "m.room.message"},
{"content", {
{"msgtype", "m.text"},
{"body", text}
}
}
};
Event e{j};
return sendMessage(e);
}
auto Room::resendMessage(std::string txnId) const
-> PromiseT
{
return m_ctx.createResolvedPromise({})
.then([that=toEventLoop()](const auto &) {
kzo.client.dbg() << "resending all pending key events" << std::endl;
return that.sendAllPendingKeyEvents();
})
.then([that=toEventLoop(), txnId](const auto &sendKeyStatus) {
if (!sendKeyStatus.success()) {
kzo.client.warn() << "resending all pending key events failed" << std::endl;
return that.m_ctx.createResolvedPromise(sendKeyStatus);
}
auto maybeLocalEcho = that.roomCursor().map([txnId](const auto &room) {
return room.getLocalEchoByTxnId(txnId);
}).make();
auto roomEncrypted = that.roomCursor()[&RoomModel::encrypted].make();
if (!maybeLocalEcho.get()) {
return that.m_ctx.createResolvedPromise({false, json{
{"errorCode", "MOE_KAZV_MXC_NO_SUCH_TXNID"},
{"error", "No such txn id"}
}});
}
auto localEcho = maybeLocalEcho.get().value();
auto event = localEcho.event;
auto rid = that.roomId().make().get();
if ((roomEncrypted.get() && event.encrypted()) || !roomEncrypted.get()) {
kzo.client.info() << "resending message with txn id " << localEcho.txnId << std::endl;
return that.m_ctx.dispatch(SendMessageAction{rid, event, localEcho.txnId});
} else {
kzo.client.info() << "room encrypted but event isn't, just resend everything" << std::endl;
return that.m_ctx.dispatch(RoomListAction{UpdateRoomAction{rid, RemoveLocalEchoAction{localEcho.txnId}}})
.then([localEcho, that](auto) { // Can't fail
return that.sendMessage(localEcho.event);
});
}
});
}
auto Room::redactEvent(std::string eventId, std::optional<std::string> reason) const -> PromiseT
{
return m_ctx.dispatch(RedactEventAction{
roomId().make().get(),
eventId,
reason
});
}
auto Room::sendPendingKeyEvent(std::string txnId) const -> PromiseT
{
return m_ctx.createResolvedPromise({})
.then([txnId, r=toEventLoop()](auto &&) {
auto ctx = r.m_ctx;
auto rid = r.roomId().make().get();
kzo.client.dbg() << "sending key event as to-device message" << std::endl;
kzo.client.dbg() << "txnId of key event: " << txnId << std::endl;
auto maybePending = r.roomCursor().make().get().getPendingRoomKeyEventByTxnId(txnId);
if (!maybePending.has_value()) {
kzo.client.warn() << "No such pending room key event";
return ctx.createResolvedPromise(EffectStatus(/* succ = */ false, json::object(
{{"errorCode", "MOE_KAZV_MXC_NO_SUCH_PENDING_ROOM_KEY_EVENT"},
{"error", "No such pending room key event"}}
)));
}
auto pending = maybePending.value();
- auto event = pending.event;
- return ctx.dispatch(SendToDeviceMessageAction{event, pending.devices})
+ 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::setPinnedEvents(immer::flex_vector<std::string> eventIds) const
-> PromiseT
{
json j{
{"type", "m.room.pinned_events"},
{"content", {
{"pinned", eventIds}
}
}
};
Event e{j};
return sendStateEvent(e);
}
auto Room::timelineEventIds() const -> lager::reader<immer::flex_vector<std::string>>
{
return roomCursor()[&RoomModel::timeline];
}
auto Room::messagesMap() const -> lager::reader<immer::map<std::string, Event>>
{
return roomCursor()[&RoomModel::messages];
}
auto Room::timelineEvents() const -> lager::reader<immer::flex_vector<Event>>
{
return roomCursor()
.xform(zug::map([](auto r) {
auto messages = r.messages;
auto timeline = r.timeline;
return intoImmer(
immer::flex_vector<Event>{},
zug::map([=](auto eventId) {
return messages[eventId];
}),
timeline);
}));
}
auto Room::timelineGaps() const
-> lager::reader<immer::map<std::string /* eventId */,
std::string /* prevBatch */>>
{
return roomCursor()[&RoomModel::timelineGaps];
}
auto Room::paginateBackFromEvent(std::string eventId) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(PaginateTimelineAction{
+roomId(), eventId, std::nullopt});
}
auto Room::localEchoes() const -> lager::reader<immer::flex_vector<LocalEchoDesc>>
{
return roomCursor()[&RoomModel::localEchoes];
}
auto Room::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});
}
}
diff --git a/src/tests/client/encryption-test.cpp b/src/tests/client/encryption-test.cpp
index e4a3fd8..bf32da3 100644
--- a/src/tests/client/encryption-test.cpp
+++ b/src/tests/client/encryption-test.cpp
@@ -1,142 +1,141 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <catch2/catch_all.hpp>
#include <client/actions/encryption.hpp>
#include <client-model.hpp>
#include "client-test-util.hpp"
#include "factory.hpp"
using namespace Kazv::Factory;
TEST_CASE("PrepareForSharingRoomKeyAction: adds the encrypted event to pending events", "[client][encryption]")
{
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
auto event = Event{json{
{"type", "m.room_key"},
{"content", {{"some", "thing"}}},
}};
auto [next, dontCareEffect] = ClientModel::update(m, PrepareForSharingRoomKeyAction{"!exampleroomid:example.com", {}, event, {}});
auto nextRoom = next.roomList.rooms.at("!exampleroomid:example.com");
REQUIRE(nextRoom.pendingRoomKeyEvents.size() == 1);
- REQUIRE(nextRoom.pendingRoomKeyEvents[0].event.encrypted());
}
TEST_CASE("encrypted event will keep a copy of m.relates_to in plaintext", "[client][encryption]")
{
auto room = makeRoom(withRoomEncrypted(true));
auto client = makeClient(
withCrypto(makeCrypto())
| withRoom(room)
);
auto eventToEncrypt = makeEvent(
withEventType("m.room.message")
| withEventRelationship("moe.kazv.mxc.custom-rel-type", "$some-event-id")
);
auto [encryptedEvent, maybeKey] = client.megOlmEncrypt(
eventToEncrypt,
room.roomId,
0,
genRandomData(EncryptMegOlmEventAction::maxRandomSize())
);
// because we do not have session key yet, it should always be rotated
REQUIRE(maybeKey.has_value());
// check we can still access relationship
REQUIRE(encryptedEvent.relationship() == std::pair<std::string, std::string>{"moe.kazv.mxc.custom-rel-type", "$some-event-id"});
// check that the relationship is also in plaintext
REQUIRE(encryptedEvent.originalJson().get()["content"]["m.relates_to"] == json{
{"rel_type", "moe.kazv.mxc.custom-rel-type"},
{"event_id", "$some-event-id"},
});
}
TEST_CASE("encrypting event without relationship should not put m.relates_to key in plaintext", "[client][encryption]")
{
auto room = makeRoom(withRoomEncrypted(true));
auto client = makeClient(
withCrypto(makeCrypto())
| withRoom(room)
);
auto eventToEncrypt = makeEvent(
withEventType("m.room.message")
);
auto [encryptedEvent, maybeKey] = client.megOlmEncrypt(
eventToEncrypt,
room.roomId,
0,
genRandomData(EncryptMegOlmEventAction::maxRandomSize())
);
REQUIRE(maybeKey.has_value());
REQUIRE(!encryptedEvent.originalJson().get()["content"].contains("m.relates_to"));
}
TEST_CASE("ClientModel::olmEncryptSplit()", "[client][encryption]")
{
auto makeDeviceInfo = [](const Crypto &crypto, std::string userId, std::string deviceId) {
auto client = makeClient(withCrypto(crypto));
client.userId = userId;
client.deviceId = deviceId;
auto [next, _] = updateClient(client, UploadIdentityKeysAction{});
return json::parse(std::get<Bytes>(next.nextJobs[0].requestBody()))["device_keys"];
};
auto receiver1 = makeCrypto();
receiver1.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
auto receiver2 = makeCrypto();
receiver2.genOneTimeKeysWithRandom(genRandomData(Crypto::genOneTimeKeysRandomSize(1)), 1);
auto oneTimeKeys1 = receiver1.unpublishedOneTimeKeys();
auto cv25519Key1 = oneTimeKeys1["curve25519"].items().begin().value().template get<std::string>();
auto oneTimeKeys2 = receiver2.unpublishedOneTimeKeys();
auto cv25519Key2 = oneTimeKeys2["curve25519"].items().begin().value().template get<std::string>();
auto queryKeysRespJson = json{
{"device_keys", {{"@receiver:example.com", {
{"device1", makeDeviceInfo(receiver1, "@receiver:example.com", "device1")},
{"device2", makeDeviceInfo(receiver2, "@receiver:example.com", "device2")},
}}}},
};
// Query keys
auto client = makeClient(withCrypto(makeCrypto()));
std::tie(client, std::ignore) = processResponse(client, QueryKeysResponse(
makeResponse("QueryKeys", withResponseJsonBody(queryKeysRespJson))
));
// Claim keys
client.withCrypto([&](auto &c) { c.createOutboundSessionWithRandom(genRandomData(Crypto::createOutboundSessionRandomSize()), receiver1.curve25519IdentityKey(), cv25519Key1); });
client.withCrypto([&](auto &c) { c.createOutboundSessionWithRandom(genRandomData(Crypto::createOutboundSessionRandomSize()), receiver2.curve25519IdentityKey(), cv25519Key2); });
// encrypt
auto res = client.olmEncryptSplit(Event(json::object()),
{{"@receiver:example.com", {"device1", "device2"}}},
genRandomData(Crypto::encryptOlmMaxRandomSize() * 2));
REQUIRE(res["@receiver:example.com"]["device1"].originalJson().get().at("content").at("ciphertext").size() == 1);
REQUIRE(res["@receiver:example.com"]["device1"].originalJson().get().at("content").at("ciphertext").contains(receiver1.curve25519IdentityKey()));
REQUIRE(res["@receiver:example.com"]["device2"].originalJson().get().at("content").at("ciphertext").size() == 1);
REQUIRE(res["@receiver:example.com"]["device2"].originalJson().get().at("content").at("ciphertext").contains(receiver2.curve25519IdentityKey()));
}
diff --git a/src/tests/client/room/local-echo-test.cpp b/src/tests/client/room/local-echo-test.cpp
index f500a2b..b5e42c8 100644
--- a/src/tests/client/room/local-echo-test.cpp
+++ b/src/tests/client/room/local-echo-test.cpp
@@ -1,662 +1,682 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <tuple>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_predicate.hpp>
#include <lager/event_loop/boost_asio.hpp>
#include <asio-promise-handler.hpp>
#include <cursorutil.hpp>
#include <sdk-model.hpp>
#include <client/client.hpp>
#include <crypto-util.hpp>
#include <cprjobhandler.hpp>
#include <lagerstoreeventemitter.hpp>
#include <debug.hpp>
#include <sdk.hpp>
#include "client-test-util.hpp"
#include "factory.hpp"
using namespace Kazv;
using namespace Kazv::Factory;
using Catch::Matchers::Predicate;
inline auto eventJson = json{
{"content", {
{"foo", "bar"},
}},
{"type", "m.room.message"},
};
template<class Store, class Func>
static auto getMockContext(SingleTypePromiseInterface<EffectStatus> &ph, Store &store, Func func)
{
return typename Client::ContextT([&ph, &store, func](const auto &action) {
static unsigned long long nextTxnId;
kzo.client.dbg() << "dispatched: index " << action.index() << std::endl;
auto [cont, res] = func(action);
if (!cont) {
return ph.createResolved(res);
}
if (std::holds_alternative<GetRoomStatesAction>(action)
|| std::holds_alternative<QueryKeysAction>(action)
|| std::holds_alternative<SendMessageAction>(action)) {
return ph.createResolved(EffectStatus(true, json::object()));
- } else if (std::holds_alternative<SendToDeviceMessageAction>(action)) {
- auto a = std::get<SendToDeviceMessageAction>(action);
- kzo.client.dbg() << "to device encrypted? " << a.event.encrypted() << std::endl;
+ } else if (std::holds_alternative<SendMultipleToDeviceMessagesAction>(action)) {
+ auto a = std::get<SendMultipleToDeviceMessagesAction>(action);
return ph.createResolved(EffectStatus(true, json::object()));
} else if (std::holds_alternative<ClaimKeysAction>(action)) {
return ph.createResolved(EffectStatus(true, json::object({{"keyEvent", json::object({})}})));
} else if (std::holds_alternative<PrepareForSharingRoomKeyAction>(action)) {
auto a = std::get<PrepareForSharingRoomKeyAction>(action);
auto txnId = std::to_string(nextTxnId++);
return store.dispatch(UpdateRoomAction{
a.roomId,
AddPendingRoomKeyAction{
- PendingRoomKeyEvent{txnId, Event(json{{"type", "m.room.encrypted"}, {"content", {{"whatever", "ok"}}}}), a.devices}
+ makePendingRoomKeyEventV0(
+ txnId,
+ Event(json{{"type", "m.room.encrypted"}, {"content", {{"whatever", "ok"}}}}),
+ // Mocking the device list
+ {{"@foo:example.com", {"device1", "device2"}}}
+ )
}
}).then([txnId](auto &&) {
return EffectStatus{true, json::object({{"txnId", txnId}})};
});
} else if (std::holds_alternative<SaveLocalEchoAction>(action)
|| std::holds_alternative<EncryptMegOlmEventAction>(action)
|| std::holds_alternative<RoomListAction>(action)
|| std::holds_alternative<UpdateLocalEchoStatusAction>(action)) {
return store.dispatch(action);
} else {
kzo.client.err() << "Unhandled action: index " << action.index();
throw std::runtime_error{"unhandled action"};
}
}, ph, lager::deps<>{});
}
TEST_CASE("Local echo", "[client][room]")
{
RoomModel r;
auto next = RoomModel::update(r, AddLocalEchoAction{{"txnId1", Event(eventJson)}});
REQUIRE(!(next == r));
next = RoomModel::update(next, AddLocalEchoAction{{"txnId2", Event(eventJson)}});
REQUIRE(!(next == r));
REQUIRE(next.localEchoes.size() == 2);
REQUIRE_THAT(next.localEchoes[0], Predicate<LocalEchoDesc>([](const auto &desc) {
return desc.txnId == "txnId1";
}));
REQUIRE_THAT(next.localEchoes[1], Predicate<LocalEchoDesc>([](const auto &desc) {
return desc.txnId == "txnId2";
}));
}
TEST_CASE("Remove local echo", "[client][room]")
{
RoomModel r;
r.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"txnId1", Event(eventJson)},
{"txnId2", Event(eventJson)},
};
auto next = RoomModel::update(r, RemoveLocalEchoAction{"txnId1"});
REQUIRE(next.localEchoes.size() == 1);
REQUIRE_THAT(next.localEchoes[0], Predicate<LocalEchoDesc>([](const auto &desc) {
return desc.txnId == "txnId2";
}));
}
TEST_CASE("getLocalEchoByTxnId()", "[client][room]")
{
RoomModel r;
r.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"txnId1", Event(eventJson)},
{"txnId2", Event(eventJson)},
};
REQUIRE(r.getLocalEchoByTxnId("txnId1").value() == r.localEchoes[0]);
REQUIRE(!r.getLocalEchoByTxnId("txnId3").has_value());
}
TEST_CASE("Sending a message leaves a local echo", "[client][room]")
{
ClientModel m;
const auto roomId = "!foo:tusooa.xyz"s;
m.roomList.rooms = m.roomList.rooms.set(roomId, RoomModel{});
auto [next, dontCareEffect] = ClientModel::update(m, SendMessageAction{roomId, Event(eventJson)});
auto localEchoes = next.roomList.rooms[roomId].localEchoes;
REQUIRE(localEchoes.size() == 1);
assert1Job(next);
for1stJob(next, [localEchoes](const auto &job) {
REQUIRE(job.dataStr("txnId") == localEchoes[0].txnId);
});
}
TEST_CASE("Failed send changes the status of the local echo", "[client][room]")
{
ClientModel m;
const auto roomId = "!foo:tusooa.xyz"s;
m.roomList.rooms = m.roomList.rooms.set(roomId, RoomModel{});
auto [next, dontCareEffect] = ClientModel::update(m, SendMessageAction{roomId, Event(eventJson)});
auto resp = makeResponse(
"SendMessage",
withResponseDataKV("roomId", roomId)
| withResponseDataKV("txnId", next.roomList.rooms[roomId].localEchoes[0].txnId)
);
resp.statusCode = 500;
std::tie(next, dontCareEffect) = ClientModel::update(next, ProcessResponseAction{resp});
auto localEchoes = next.roomList.rooms[roomId].localEchoes;
REQUIRE(localEchoes.size() == 1);
REQUIRE(localEchoes[0].status == LocalEchoDesc::Failed);
}
TEST_CASE("Local echo with encrypted event", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
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 saveLocalEchoCalled = 0;
auto sendMessageCalled = 0;
auto txnId = std::string();
auto sendMessageEvent = Event();
auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &txnId, &sendMessageEvent](const auto &a) {
if (std::holds_alternative<SaveLocalEchoAction>(a)) {
++saveLocalEchoCalled;
} else if (std::holds_alternative<SendMessageAction>(a)) {
++sendMessageCalled;
sendMessageEvent = std::get<SendMessageAction>(a).event;
REQUIRE(std::get<SendMessageAction>(a).txnId.has_value());
txnId = std::get<SendMessageAction>(a).txnId.value();
}
return std::make_pair(/* cont = */ true, EffectStatus());
});
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.sendTextMessage("test")
.then([&io](auto) {
kzo.client.dbg() << "ended" << std::endl;
io.stop();
});
io.run();
REQUIRE(saveLocalEchoCalled == 2);
REQUIRE(sendMessageCalled == 1);
REQUIRE(sendMessageEvent.encrypted());
REQUIRE(sendMessageEvent.decrypted());
REQUIRE(r.localEchoes().make().get().size() == 1);
REQUIRE(r.localEchoes().make().get()[0].txnId == txnId);
}
TEST_CASE("Local echo with encrypted event, loading room member failed", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
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 saveLocalEchoCalled = 0;
auto sendMessageCalled = 0;
auto megOlmEncryptCalled = 0;
auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &megOlmEncryptCalled](const auto &a) {
if (std::holds_alternative<GetRoomStatesAction>(a)) {
auto err = json::object({{"errorCode", "400"}, {"error", "Cannot get room states"}});
return std::make_pair(/* cont = */ false, EffectStatus(false, err));
} else if (std::holds_alternative<SaveLocalEchoAction>(a)) {
++saveLocalEchoCalled;
} else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
++megOlmEncryptCalled;
} else if (std::holds_alternative<SendMessageAction>(a)) {
++sendMessageCalled;
}
return std::make_pair(/* cont = */ true, EffectStatus());
});
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.sendTextMessage("test")
.then([&io](const auto &status) {
kzo.client.dbg() << "ended" << std::endl;
REQUIRE(!status.success());
io.stop();
});
io.run();
REQUIRE(saveLocalEchoCalled == 1);
REQUIRE(sendMessageCalled == 0);
REQUIRE(megOlmEncryptCalled == 0);
REQUIRE(r.localEchoes().make().get().size() == 1);
REQUIRE(r.localEchoes().make().get()[0].status == LocalEchoDesc::Failed);
}
TEST_CASE("Local echo with encrypted event, querying keys failed", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
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 saveLocalEchoCalled = 0;
auto sendMessageCalled = 0;
auto megOlmEncryptCalled = 0;
auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &megOlmEncryptCalled](const auto &a) {
if (std::holds_alternative<QueryKeysAction>(a)) {
auto err = json::object({{"errorCode", "400"}, {"error", "Cannot get room states"}});
return std::make_pair(/* cont = */ false, EffectStatus(false, err));
} else if (std::holds_alternative<SaveLocalEchoAction>(a)) {
++saveLocalEchoCalled;
} else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
++megOlmEncryptCalled;
} else if (std::holds_alternative<SendMessageAction>(a)) {
++sendMessageCalled;
}
return std::make_pair(/* cont = */ true, EffectStatus());
});
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.sendTextMessage("test")
.then([&io](const auto &status) {
kzo.client.dbg() << "ended" << std::endl;
REQUIRE(!status.success());
io.stop();
});
io.run();
REQUIRE(saveLocalEchoCalled == 1);
REQUIRE(sendMessageCalled == 0);
REQUIRE(megOlmEncryptCalled == 0);
REQUIRE(r.localEchoes().make().get().size() == 1);
REQUIRE(r.localEchoes().make().get()[0].status == LocalEchoDesc::Failed);
}
TEST_CASE("Encrypted room: Resend encrypted local echo", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
auto encryptedEvent = Event{json{
{"content", {{"foo", "bar"}}},
{"type", "m.room.encrypted"},
}};
auto decryptedJson = json{
{"content", {{"dec-foo", "dec-bar"}}},
{"type", "m.room.message"},
};
auto event = encryptedEvent.setDecryptedJson(decryptedJson, Event::Decrypted);
room.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"some-txn-id", event, LocalEchoDesc::Failed},
};
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
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 saveLocalEchoCalled = 0;
auto sendMessageCalled = 0;
auto megOlmEncryptCalled = 0;
auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &megOlmEncryptCalled](const auto &a) {
if (std::holds_alternative<SaveLocalEchoAction>(a)) {
++saveLocalEchoCalled;
} else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
++megOlmEncryptCalled;
} else if (std::holds_alternative<SendMessageAction>(a)) {
++sendMessageCalled;
REQUIRE(std::get<SendMessageAction>(a).txnId.has_value());
REQUIRE(std::get<SendMessageAction>(a).txnId.value() == "some-txn-id");
}
return std::make_pair(/* cont = */ true, EffectStatus());
});
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.resendMessage("some-txn-id")
.then([&io](auto) {
kzo.client.dbg() << "ended" << std::endl;
io.stop();
});
io.run();
REQUIRE(saveLocalEchoCalled == 0);
REQUIRE(megOlmEncryptCalled == 0);
REQUIRE(sendMessageCalled == 1);
REQUIRE(r.localEchoes().make().get().size() == 1);
REQUIRE(r.localEchoes().make().get()[0].txnId == "some-txn-id");
}
TEST_CASE("Encrypted room: Resend encrypted local echo AND failed key share event", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
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 saveLocalEchoCalled = 0;
auto sendMessageCalled = 0;
auto megOlmEncryptCalled = 0;
auto sendToDeviceCalled = 0;
auto olmEncryptCalled = 0;
auto resending = false;
auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled,
&sendMessageCalled, &megOlmEncryptCalled, &sendToDeviceCalled, &olmEncryptCalled, &resending](const auto &a) {
- if (std::holds_alternative<SendToDeviceMessageAction>(a)) {
+ if (std::holds_alternative<SendMultipleToDeviceMessagesAction>(a)) {
auto sendKeyData = json{
{"error", "Bad request"},
{"errorCode", "400"},
};
++sendToDeviceCalled;
if (!resending) {
return std::make_pair(/* cont = */ false, EffectStatus(/* succ = */ false, sendKeyData));
}
} else if (std::holds_alternative<SaveLocalEchoAction>(a)) {
++saveLocalEchoCalled;
} else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
++megOlmEncryptCalled;
} else if (std::holds_alternative<SendMessageAction>(a)) {
++sendMessageCalled;
} else if (std::holds_alternative<PrepareForSharingRoomKeyAction>(a)) {
++olmEncryptCalled;
}
return std::make_pair(/* cont = */ true, EffectStatus());
});
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.sendTextMessage("test")
.then([&io](auto) {
kzo.client.dbg() << "ended" << std::endl;
io.stop();
});
io.run();
REQUIRE(saveLocalEchoCalled == 2);
REQUIRE(megOlmEncryptCalled == 1);
REQUIRE(sendToDeviceCalled == 1);
REQUIRE(olmEncryptCalled == 1);
REQUIRE(sendMessageCalled == 0);
REQUIRE(r.localEchoes().make().get().size() == 1);
auto savedEvent = r.localEchoes().make().get()[0];
auto txnId = savedEvent.txnId;
REQUIRE(savedEvent.event.encrypted());
saveLocalEchoCalled = 0;
megOlmEncryptCalled = 0;
sendMessageCalled = 0;
sendToDeviceCalled = 0;
olmEncryptCalled = 0;
resending = true;
r.resendMessage(txnId)
.then([&io](auto) {
kzo.client.dbg() << "resent" << std::endl;
io.stop();
});
io.restart();
io.run();
REQUIRE(saveLocalEchoCalled == 0);
REQUIRE(megOlmEncryptCalled == 0);
REQUIRE(sendMessageCalled == 1);
REQUIRE(olmEncryptCalled == 0);
REQUIRE(sendToDeviceCalled == 1);
REQUIRE(r.pendingRoomKeyEvents().make().get().size() == 0);
}
TEST_CASE("Encrypted room: Resend unencrypted local echo", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
auto event = Event{json{
{"content", {{"dec-foo", "dec-bar"}}},
{"type", "m.room.message"},
}};
room.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"some-txn-id", event, LocalEchoDesc::Failed},
{"some-other-txn-id", event, LocalEchoDesc::Failed},
};
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
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 saveLocalEchoCalled = 0;
auto sendMessageCalled = 0;
auto megOlmEncryptCalled = 0;
auto roomListActionCalled = 0;
auto txnId = std::string();
auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &megOlmEncryptCalled, &roomListActionCalled, &txnId](const auto &a) {
if (std::holds_alternative<SaveLocalEchoAction>(a)) {
++saveLocalEchoCalled;
} else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
++megOlmEncryptCalled;
} else if (std::holds_alternative<RoomListAction>(a)) {
++roomListActionCalled;
} else if (std::holds_alternative<SendMessageAction>(a)) {
++sendMessageCalled;
REQUIRE(std::get<SendMessageAction>(a).txnId.has_value());
txnId = std::get<SendMessageAction>(a).txnId.value();
}
return std::make_pair(/* cont = */ true, EffectStatus());
});
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.resendMessage("some-txn-id")
.then([&io](auto st) {
REQUIRE(st.success());
kzo.client.dbg() << "ended" << std::endl;
io.stop();
});
io.run();
REQUIRE(saveLocalEchoCalled == 2);
REQUIRE(roomListActionCalled == 2); // once for removing the existing local echo, once for removing pending key event
REQUIRE(megOlmEncryptCalled == 1);
REQUIRE(sendMessageCalled == 1);
REQUIRE(r.localEchoes().make().get().size() == 2);
REQUIRE(r.localEchoes().make().get()[0].txnId == "some-other-txn-id");
REQUIRE(r.localEchoes().make().get()[1].txnId == txnId);
}
TEST_CASE("Unencrypted room: Resend unencrypted local echo", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = false;
room.roomId = "!exampleroomid:example.com";
auto event = Event{json{
{"content", {{"dec-foo", "dec-bar"}}},
{"type", "m.room.message"},
}};
room.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"some-txn-id", event, LocalEchoDesc::Failed},
};
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
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 saveLocalEchoCalled = 0;
auto sendMessageCalled = 0;
auto megOlmEncryptCalled = 0;
auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &megOlmEncryptCalled](const auto &a) {
if (std::holds_alternative<SaveLocalEchoAction>(a)) {
++saveLocalEchoCalled;
} else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
++megOlmEncryptCalled;
} else if (std::holds_alternative<SendMessageAction>(a)) {
++sendMessageCalled;
REQUIRE(std::get<SendMessageAction>(a).txnId.has_value());
REQUIRE(std::get<SendMessageAction>(a).txnId.value() == "some-txn-id");
}
return std::make_pair(/* cont = */ true, EffectStatus());
});
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(!r.encrypted().make().get());
r.resendMessage("some-txn-id")
.then([&io](auto) {
kzo.client.dbg() << "ended" << std::endl;
io.stop();
});
io.run();
REQUIRE(saveLocalEchoCalled == 0);
REQUIRE(megOlmEncryptCalled == 0);
REQUIRE(sendMessageCalled == 1);
REQUIRE(r.localEchoes().make().get().size() == 1);
REQUIRE(r.localEchoes().make().get()[0].txnId == "some-txn-id");
}
+
+TEST_CASE("makePendingRoomKeyEventV0()", "[client][room][local-echo]")
+{
+ auto devices = immer::map<std::string, immer::flex_vector<std::string>>{
+ {"@foo:example.com", {"device1"}},
+ {"@bar:example.com", {"device2", "device3"}},
+ };
+ std::string txnId = "xxx";
+ Event event = json{{"type", "m.room.encrypted"}, {"content", {{"whatever", "ok"}}}};
+ auto e = makePendingRoomKeyEventV0(txnId, event, devices);
+ auto expected = immer::map<std::string, immer::map<std::string, Event>>{
+ {"@foo:example.com", {{"device1", event}}},
+ {"@bar:example.com", {{"device2", event}, {"device3", event}}},
+ };
+ REQUIRE(e.messages == expected);
+}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 11:23 PM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55502
Default Alt Text
(124 KB)

Event Timeline