Page MenuHomePhorge

No OneTemporary

Size
50 KB
Referenced Files
None
Subscribers
None
diff --git a/src/client/actions/send.cpp b/src/client/actions/send.cpp
index b7c6dd9..d749eb2 100644
--- a/src/client/actions/send.cpp
+++ b/src/client/actions/send.cpp
@@ -1,151 +1,173 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <debug.hpp>
#include <types.hpp>
#include "send.hpp"
#include "status-utils.hpp"
namespace Kazv
{
static std::string increaseTxnId(std::string cur)
{
return std::to_string(std::stoull(cur) + 1);
}
+ static auto getTxnId(Event event, ClientModel &m)
+ {
+ auto origJson = event.originalJson().get();
+
+ auto txnId = std::to_string(std::hash<json>{}(origJson))
+ + std::to_string(std::chrono::system_clock::now()
+ .time_since_epoch().count())
+ + m.nextTxnId;
+
+ m.nextTxnId = increaseTxnId(m.nextTxnId);
+
+ return txnId;
+ }
+
ClientResult updateClient(ClientModel m, SendMessageAction a)
{
auto event = std::move(a.event);
auto roomId = a.roomId;
auto origJson = event.originalJson().get();
if (!origJson.contains("type") || !origJson.contains("content")) {
m.addTrigger(InvalidMessageFormat{});
return { std::move(m), lager::noop };
}
// We do not use event.type() etc. because we want
// encrypted events stay encrypted.
auto type = origJson["type"];
auto content = origJson["content"];
kzo.client.dbg() << "Sending message of type " << type
<< " with content " << content.dump()
<< " to " << a.roomId
<< " as #" << m.nextTxnId << std::endl;
// We combine the hash of json, the timestamp,
// and a numeric count in the client to avoid collision.
auto txnId = std::to_string(std::hash<json>{}(origJson))
+ std::to_string(std::chrono::system_clock::now()
.time_since_epoch().count())
+ m.nextTxnId;
m.roomList = RoomListModel::update(std::move(m.roomList),
UpdateRoomAction{
roomId,
AddLocalEchoAction{{txnId, event}},
}
);
m.nextTxnId = increaseTxnId(m.nextTxnId);
auto job = m.job<SendMessageJob>()
.make(a.roomId, type, txnId, content)
.withData(json{
{"roomId", a.roomId},
{"txnId", txnId},
});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, SendMessageResponse r)
{
auto roomId = r.dataStr("roomId");
if (! r.success()) {
auto txnId = r.dataStr("txnId");
kzo.client.dbg() << "Send message failed" << std::endl;
m.roomList.rooms = std::move(m.roomList.rooms).update(roomId, [txnId](auto room) {
auto maybeLocalEcho = room.getLocalEchoByTxnId(txnId);
if (!maybeLocalEcho.has_value()) {
kzo.client.warn() << "We do not have local echo with txnId " << txnId << " . Have we time-travelled?" << std::endl;
return room;
}
auto localEcho = std::move(maybeLocalEcho).value();
localEcho.status = LocalEchoDesc::Failed;
return RoomModel::update(std::move(room), AddLocalEchoAction{localEcho});
});
m.addTrigger(SendMessageFailed{roomId, r.errorCode(), r.errorMessage()});
return { std::move(m), failWithResponse(r) };
}
m.addTrigger(SendMessageSuccessful{roomId, r.eventId()});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, SendToDeviceMessageAction a)
{
auto origJson = a.event.originalJson().get();
if (!origJson.contains("type") || !origJson.contains("content")) {
return { std::move(m), simpleFail };
}
// We do not use event.type() etc. because we want
// encrypted events stay encrypted.
auto type = origJson["type"];
auto content = origJson["content"];
- auto txnId = std::to_string(std::hash<json>{}(origJson))
- + std::to_string(std::chrono::system_clock::now()
- .time_since_epoch().count())
- + m.nextTxnId;
-
- m.nextTxnId = increaseTxnId(m.nextTxnId);
+ auto txnId = getTxnId(a.event, m);
auto messages =
immer::map<std::string, immer::map<std::string, JsonWrap>>{};
for (auto [userId, devices] : a.devicesToSend) {
auto deviceIdToContentMap = immer::map<std::string, JsonWrap>{};
for (auto deviceId : devices) {
deviceIdToContentMap = std::move(deviceIdToContentMap).set(deviceId, content);
}
messages = std::move(messages).set(userId, deviceIdToContentMap);
}
auto job = m.job<SendToDeviceJob>()
.make(type, txnId, messages)
.withData(json{{"devicesToSend", a.devicesToSend},
{"txnId", txnId}});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, SendToDeviceResponse r)
{
auto devicesToSend = r.dataJson("devicesToSend");
auto txnId = r.dataStr("txnId");
if (! r.success()) {
m.addTrigger(SendToDeviceMessageFailed{devicesToSend, txnId, r.errorCode(), r.errorMessage()});
return { std::move(m), failWithResponse(r) };
}
m.addTrigger(SendToDeviceMessageSuccessful{devicesToSend, txnId});
return { std::move(m), lager::noop };
}
+ ClientResult updateClient(ClientModel m, SaveLocalEchoAction a)
+ {
+ auto txnId = a.txnId.has_value() ? a.txnId.value() : getTxnId(a.event, m);
+
+ m.roomList = RoomListModel::update(m.roomList, UpdateRoomAction{
+ a.roomId,
+ AddLocalEchoAction{{txnId, a.event}},
+ });
+
+ return { std::move(m), [txnId](auto) {
+ return EffectStatus(true, json::object({{"txnId", txnId}}));
+ }};
+ }
}
diff --git a/src/client/actions/send.hpp b/src/client/actions/send.hpp
index 67d0eab..2ff7ec0 100644
--- a/src/client/actions/send.hpp
+++ b/src/client/actions/send.hpp
@@ -1,22 +1,24 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <csapi/room_send.hpp>
#include <csapi/to_device.hpp>
#include "client-model.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, SendMessageAction a);
ClientResult processResponse(ClientModel m, SendMessageResponse r);
ClientResult updateClient(ClientModel m, SendToDeviceMessageAction a);
ClientResult processResponse(ClientModel m, SendToDeviceResponse r);
+
+ ClientResult updateClient(ClientModel m, SaveLocalEchoAction a);
}
diff --git a/src/client/client-model.hpp b/src/client/client-model.hpp
index f0bb71a..bbff3f0 100644
--- a/src/client/client-model.hpp
+++ b/src/client/client-model.hpp
@@ -1,515 +1,533 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <tuple>
#include <variant>
#include <string>
#include <optional>
#include <lager/context.hpp>
#include <boost/hana.hpp>
#include <serialization/std-optional.hpp>
#include <csapi/sync.hpp>
#include <file-desc.hpp>
#include <crypto.hpp>
#include <serialization/immer-flex-vector.hpp>
#include <serialization/immer-box.hpp>
#include <serialization/immer-map.hpp>
#include <serialization/immer-array.hpp>
#include "clientfwd.hpp"
#include "device-list-tracker.hpp"
#include "room/room-model.hpp"
namespace Kazv
{
inline const std::string DEFTXNID{"0"};
enum RoomVisibility
{
Private,
Public,
};
enum CreateRoomPreset
{
PrivateChat,
PublicChat,
TrustedPrivateChat,
};
enum ThumbnailResizingMethod
{
Crop,
Scale,
};
struct ClientModel
{
std::string serverUrl;
std::string userId;
std::string token;
std::string deviceId;
bool loggedIn{false};
bool syncing{false};
bool shouldSync{true};
int firstRetryMs{1000};
int retryTimeFactor{2};
int maxRetryMs{30 * 1000};
int syncTimeoutMs{20000};
std::string initialSyncFilterId;
std::string incrementalSyncFilterId;
std::optional<std::string> syncToken;
RoomListModel roomList;
immer::map<std::string /* sender */, Event> presence;
immer::map<std::string /* type */, Event> accountData;
std::string nextTxnId{DEFTXNID};
immer::flex_vector<BaseJob> nextJobs;
immer::flex_vector<KazvEvent> nextTriggers;
EventList toDevice;
std::optional<Crypto> crypto;
bool identityKeysUploaded{false};
DeviceListTracker deviceLists;
DeviceTrustLevel trustLevelNeededToSendKeys{DeviceTrustLevel::Unseen};
immer::flex_vector<std::string /* deviceId */> devicesToSendKeys(std::string userId) const;
/// rotate sessions for a room if there is a user in the room with
/// devicesToSendKeys changes
void maybeRotateSessions(ClientModel oldClient);
std::pair<Event, std::optional<std::string> /* sessionKey */>
megOlmEncrypt(Event e, std::string roomId, Timestamp timeMs, RandomData random);
/// precondition: the one-time keys for those devices must already be claimed
Event olmEncrypt(Event e, immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap, RandomData random);
/// @return number of one-time keys we need to generate
std::size_t numOneTimeKeysNeeded() const;
// helpers
template<class Job>
struct MakeJobT
{
template<class ...Args>
constexpr auto make(Args &&...args) const {
if constexpr (Job::needsAuth()) {
return Job(
serverUrl,
token,
std::forward<Args>(args)...);
} else {
return Job(
serverUrl,
std::forward<Args>(args)...);
}
}
std::string serverUrl;
std::string token;
};
template<class Job>
constexpr auto job() const {
return MakeJobT<Job>{serverUrl, token};
}
inline void addJob(BaseJob j) {
nextJobs = std::move(nextJobs).push_back(std::move(j));
}
inline auto popAllJobs() {
auto jobs = std::move(nextJobs);
nextJobs = DEFVAL;
return jobs;
};
inline void addTrigger(KazvEvent t) {
addTriggers({t});
}
inline void addTriggers(immer::flex_vector<KazvEvent> c) {
nextTriggers = std::move(nextTriggers) + c;
}
inline auto popAllTriggers() {
auto triggers = std::move(nextTriggers);
nextTriggers = DEFVAL;
return triggers;
}
using Action = ClientAction;
using Effect = ClientEffect;
using Result = ClientResult;
static Result update(ClientModel m, Action a);
};
// actions:
struct LoginAction {
std::string serverUrl;
std::string username;
std::string password;
std::optional<std::string> deviceName;
};
struct TokenLoginAction
{
std::string serverUrl;
std::string username;
std::string token;
std::string deviceId;
};
struct LogoutAction {};
struct GetWellknownAction
{
std::string userId;
};
struct GetVersionsAction
{
std::string serverUrl;
};
struct SyncAction {};
struct SetShouldSyncAction
{
bool shouldSync;
};
struct PaginateTimelineAction
{
std::string roomId;
/// Must be where the Gap is
std::string fromEventId;
std::optional<int> limit;
};
struct SendMessageAction
{
std::string roomId;
Event event;
+ std::optional<std::string> txnId{std::nullopt};
};
struct SendStateEventAction
{
std::string roomId;
Event event;
};
+ /**
+ * Saves an local echo.
+ *
+ * After dispatching this action, the result should be such that
+ * `result.dataStr("txnId")` contains the transaction id to be used
+ * in SendMessageAction.
+ */
+ struct SaveLocalEchoAction
+ {
+ /// The room id
+ std::string roomId;
+ /// The event to send
+ Event event;
+ /// The chosen txnId for this event. If not specified, generate from the current ClientModel.
+ std::optional<std::string> txnId{std::nullopt};
+ };
+
struct CreateRoomAction
{
using Visibility = RoomVisibility;
using Preset = CreateRoomPreset;
Visibility visibility;
std::optional<std::string> roomAliasName;
std::optional<std::string> name;
std::optional<std::string> topic;
immer::array<std::string> invite;
//immer::array<Invite3pid> invite3pid;
std::optional<std::string> roomVersion;
JsonWrap creationContent;
immer::array<Event> initialState;
std::optional<Preset> preset;
std::optional<bool> isDirect;
JsonWrap powerLevelContentOverride;
};
struct GetRoomStatesAction
{
std::string roomId;
};
struct GetStateEventAction
{
std::string roomId;
std::string type;
std::string stateKey;
};
struct InviteToRoomAction
{
std::string roomId;
std::string userId;
};
struct JoinRoomByIdAction
{
std::string roomId;
};
struct JoinRoomAction
{
std::string roomIdOrAlias;
immer::array<std::string> serverName;
};
struct LeaveRoomAction
{
std::string roomId;
};
struct ForgetRoomAction
{
std::string roomId;
};
struct KickAction
{
std::string roomId;
std::string userId;
std::optional<std::string> reason;
};
struct BanAction
{
std::string roomId;
std::string userId;
std::optional<std::string> reason;
};
struct UnbanAction
{
std::string roomId;
std::string userId;
};
struct SetTypingAction
{
std::string roomId;
bool typing;
std::optional<int> timeoutMs;
};
struct PostReceiptAction
{
std::string roomId;
std::string eventId;
};
struct SetReadMarkerAction
{
std::string roomId;
std::string eventId;
};
struct UploadContentAction
{
FileDesc content;
std::optional<std::string> filename;
std::optional<std::string> contentType;
std::string uploadId; // to be used by library users
};
struct DownloadContentAction
{
std::string mxcUri;
std::optional<FileDesc> downloadTo;
};
struct DownloadThumbnailAction
{
std::string mxcUri;
int width;
int height;
std::optional<ThumbnailResizingMethod> method;
std::optional<bool> allowRemote;
std::optional<FileDesc> downloadTo;
};
struct ResubmitJobAction
{
BaseJob job;
};
struct ProcessResponseAction
{
Response response;
};
struct PostInitialFiltersAction
{
};
struct SendToDeviceMessageAction
{
Event event;
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
};
struct UploadIdentityKeysAction
{
};
/**
* The action to generate one-time keys.
*
* `random.size()` must be at least `randomSize(numToGen)`.
*
* This action will not generate keys exceeding the local limit of olm.
*/
struct GenerateAndUploadOneTimeKeysAction
{
/// @return The size of random needed to generate
/// `numToGen` one-time keys
static std::size_t randomSize(std::size_t numToGen);
/// The number of keys to generate
std::size_t numToGen;
/// The random data used to generate keys
RandomData random;
};
struct QueryKeysAction
{
bool isInitialSync;
};
struct ClaimKeysAction
{
static std::size_t randomSize(immer::map<std::string, immer::flex_vector<std::string>> devicesToSend);
std::string roomId;
std::string sessionId;
std::string sessionKey;
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
RandomData random;
};
/**
* The action to encrypt an megolm event for a room.
*
* If the action is successful, the result `r` will
* be such that `r.dataJson("encrypted")` contains the encrypted event *json*.
*
* If the megolm session is rotated, `r.dataStr("key")` will contain the key
* of the megolm session. Otherwise, `r.data().contains("key")` will be false.
*
* The Action may fail due to insufficient random data,
* when the megolm session needs to be rotated.
* In this case, the reducer for the Action will fail,
* and its result `r` will be such that
* `r.dataStr("reason") == "NotEnoughRandom"`.
* The user needs to provide random data of
* at least size `maxRandomSize()`.
*
*/
struct EncryptMegOlmEventAction
{
static std::size_t maxRandomSize();
static std::size_t minRandomSize();
/// The id of the room to encrypt for.
std::string roomId;
/// The event to encrypt.
Event e;
/// The timestamp, to determine whether the session should expire.
Timestamp timeMs;
/// Random data for the operation. Must be of at least size
/// `minRandomSize()`. If this is a retry of the previous operation
/// due to NotEnoughRandom, it must be of at least size `maxRandomSize()`.
RandomData random;
};
/**
* The action to encrypt events with olm for multiple devices.
*
* If the action is successful,
* The result `r` will be such that `r.dataJson("encrypted")`
* contains the json of the encrypted event.
*/
struct EncryptOlmEventAction
{
using UserIdToDeviceIdMap = immer::map<std::string, immer::flex_vector<std::string>>;
static std::size_t randomSize(UserIdToDeviceIdMap devices);
/// Devices to encrypt for.
UserIdToDeviceIdMap devices;
/// The event to encrypt.
Event e;
/// The random data for the encryption. Must be of at least
/// size `randomSize(devices)`.
RandomData random;
};
struct SetDeviceTrustLevelAction
{
std::string userId;
std::string deviceId;
DeviceTrustLevel trustLevel;
};
struct SetTrustLevelNeededToSendKeysAction
{
DeviceTrustLevel trustLevel;
};
struct GetUserProfileAction
{
std::string userId;
};
struct SetAvatarUrlAction
{
std::optional<std::string> avatarUrl;
};
struct SetDisplayNameAction
{
std::optional<std::string> displayName;
};
template<class Archive>
void serialize(Archive &ar, ClientModel &m, std::uint32_t const version)
{
bool dummySyncing{false};
ar
& m.serverUrl
& m.userId
& m.token
& m.deviceId
& m.loggedIn
& dummySyncing
& m.firstRetryMs
& m.retryTimeFactor
& m.maxRetryMs
& m.syncTimeoutMs
& m.initialSyncFilterId
& m.incrementalSyncFilterId
& m.syncToken
& m.roomList
& m.presence
& m.accountData
& m.nextTxnId
& m.toDevice
& m.crypto
& m.identityKeysUploaded
& m.deviceLists
;
if (version >= 1) { ar & m.trustLevelNeededToSendKeys; }
}
}
BOOST_CLASS_VERSION(Kazv::ClientModel, 1)
diff --git a/src/client/clientfwd.hpp b/src/client/clientfwd.hpp
index 12436de..5152adc 100644
--- a/src/client/clientfwd.hpp
+++ b/src/client/clientfwd.hpp
@@ -1,136 +1,138 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <tuple>
#include <variant>
#include <lager/context.hpp>
#include <context.hpp>
#include "room/room-model.hpp"
namespace Kazv
{
using namespace Api;
class JobInterface;
class EventInterface;
struct LoginAction;
struct TokenLoginAction;
struct LogoutAction;
struct GetWellknownAction;
struct GetVersionsAction;
struct SyncAction;
struct SetShouldSyncAction;
struct PostInitialFiltersAction;
struct PaginateTimelineAction;
struct SendMessageAction;
struct SendStateEventAction;
+ struct SaveLocalEchoAction;
struct CreateRoomAction;
struct GetRoomStatesAction;
struct GetStateEventAction;
struct InviteToRoomAction;
struct JoinRoomByIdAction;
struct EmitKazvEventsAction;
struct JoinRoomAction;
struct LeaveRoomAction;
struct ForgetRoomAction;
struct KickAction;
struct BanAction;
struct UnbanAction;
struct ProcessResponseAction;
struct SetTypingAction;
struct PostReceiptAction;
struct SetReadMarkerAction;
struct UploadContentAction;
struct DownloadContentAction;
struct DownloadThumbnailAction;
struct SendToDeviceMessageAction;
struct UploadIdentityKeysAction;
struct GenerateAndUploadOneTimeKeysAction;
struct QueryKeysAction;
struct ClaimKeysAction;
struct EncryptMegOlmEventAction;
struct EncryptOlmEventAction;
struct SetDeviceTrustLevelAction;
struct SetTrustLevelNeededToSendKeysAction;
struct GetUserProfileAction;
struct SetAvatarUrlAction;
struct SetDisplayNameAction;
struct ResubmitJobAction;
struct ClientModel;
using ClientAction = std::variant<
RoomListAction,
LoginAction,
TokenLoginAction,
LogoutAction,
GetWellknownAction,
GetVersionsAction,
SyncAction,
SetShouldSyncAction,
PostInitialFiltersAction,
PaginateTimelineAction,
SendMessageAction,
SendStateEventAction,
+ SaveLocalEchoAction,
CreateRoomAction,
GetRoomStatesAction,
GetStateEventAction,
InviteToRoomAction,
JoinRoomByIdAction,
JoinRoomAction,
LeaveRoomAction,
ForgetRoomAction,
KickAction,
BanAction,
UnbanAction,
ProcessResponseAction,
SetTypingAction,
PostReceiptAction,
SetReadMarkerAction,
UploadContentAction,
DownloadContentAction,
DownloadThumbnailAction,
SendToDeviceMessageAction,
UploadIdentityKeysAction,
GenerateAndUploadOneTimeKeysAction,
QueryKeysAction,
ClaimKeysAction,
EncryptMegOlmEventAction,
EncryptOlmEventAction,
SetDeviceTrustLevelAction,
SetTrustLevelNeededToSendKeysAction,
GetUserProfileAction,
SetAvatarUrlAction,
SetDisplayNameAction,
ResubmitJobAction
>;
using ClientEffect = Effect<ClientAction, lager::deps<>>;
using ClientResult = std::pair<ClientModel, ClientEffect>;
}
diff --git a/src/client/room/room.cpp b/src/client/room/room.cpp
index 03bb1f3..aff4dae 100644
--- a/src/client/room/room.cpp
+++ b/src/client/room/room.cpp
@@ -1,454 +1,482 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <immer/algorithm.hpp>
#include <debug.hpp>
#include "room.hpp"
namespace Kazv
{
Room::Room(lager::reader<SdkModel> sdk,
lager::reader<std::string> roomId,
Context<ClientAction> ctx,
DepsT deps)
: m_sdk(sdk)
, m_roomId(roomId)
, m_ctx(ctx)
, m_deps(deps)
{
}
Room::Room(lager::reader<SdkModel> sdk,
lager::reader<std::string> roomId,
Context<ClientAction> ctx)
: m_sdk(sdk)
, m_roomId(roomId)
, m_ctx(ctx)
, m_deps(std::nullopt)
{
}
Room::Room(InEventLoopTag, std::string roomId, ContextT ctx, DepsT deps)
: m_sdk(std::nullopt)
, m_roomId(roomId)
, m_ctx(ctx)
, m_deps(deps)
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, KAZV_ON_EVENT_LOOP_VAR(true)
#endif
{
}
Room Room::toEventLoop() const
{
assert(m_deps.has_value());
return Room(InEventLoopTag{}, currentRoomId(), m_ctx, m_deps.value());
}
const lager::reader<SdkModel> &Room::sdkCursor() const
{
if (m_sdk.has_value()) { return m_sdk.value(); }
assert(m_deps.has_value());
return *lager::get<SdkModelCursorKey>(m_deps.value());
}
lager::reader<RoomModel> Room::roomCursor() const
{
return lager::match(m_roomId)(
[&](const lager::reader<std::string> &roomId) -> lager::reader<RoomModel> {
return lager::with(sdkCursor().map(&SdkModel::c)[&ClientModel::roomList], roomId)
.map([](auto rooms, auto id) {
return rooms[id];
}).make();
},
[&](const std::string &roomId) -> lager::reader<RoomModel> {
return sdkCursor().map(&SdkModel::c)[&ClientModel::roomList].map([roomId](auto rooms) { return rooms[roomId]; }).make();
});
}
std::string Room::currentRoomId() const
{
return lager::match(m_roomId)(
[&](const lager::reader<std::string> &roomId) {
return roomId.get();
},
[&](const std::string &roomId) {
return roomId;
});
}
auto Room::heroMemberEvents() const -> lager::reader<immer::flex_vector<Event>>
{
using namespace lager::lenses;
auto idsCursor = heroIds();
lager::reader<immer::map<KeyOfState, Event>> statesCursor = stateEvents();
return lager::with(idsCursor, statesCursor)
.xform(zug::map([](const auto &ids, const auto &states) {
return intoImmer(
immer::flex_vector<Event>{},
zug::map([states](const auto &id) {
return states[{"m.room.member", id}];
}),
ids);
}));
}
auto Room::heroDisplayNames() const
-> lager::reader<immer::flex_vector<std::string>>
{
return heroMemberEvents()
.xform(zug::map([](const auto &events) {
return intoImmer(
immer::flex_vector<std::string>{},
zug::map([](const auto &event) {
auto content = event.content();
return content.get().contains("displayname")
? content.get()["displayname"].template get<std::string>()
: std::string();
}),
events);
}));
}
auto Room::nameOpt() const -> lager::reader<std::optional<std::string>>
{
using namespace lager::lenses;
return stateEvents()
[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;
}));
}
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::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) {
+ .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 {
+ 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, msg, 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([ctx, deps, devicesToSend](auto status) {
if (! status) { return ctx.createResolvedPromise({}); }
kzo.client.dbg() << "olm-encrypting key event" << std::endl;
auto keyEv = status.dataJson("keyEvent");
auto &rg = lager::get<RandomInterface &>(deps);
return ctx.dispatch(EncryptOlmEventAction{
devicesToSend, keyEv,
rg.generateRange<RandomData>(EncryptOlmEventAction::randomSize(devicesToSend))
});
})
.then([ctx, devicesToSend](auto status) {
if (! status) { return ctx.createResolvedPromise({}); }
kzo.client.dbg() << "sending key event as to-device message" << std::endl;
auto event = Event(status.dataJson("encrypted"));
return ctx.dispatch(SendToDeviceMessageAction{event, devicesToSend});
});
});
}
return ret
.then([ctx, prevStatus=status](auto status) {
if (! status) { return status; }
return prevStatus;
});
};
- auto sendEncryptedEvent = [ctx, rid](auto status) {
+ auto sendEncryptedEvent = [ctx, rid, saveLocalEchoPromise](auto status) mutable {
if (! status) { return ctx.createResolvedPromise(status); }
- kzo.client.dbg() << "sending encrypted message" << std::endl;
-
- auto ev = Event(status.dataJson("encrypted"));
-
- return ctx.dispatch(SendMessageAction{rid, ev});
+ return saveLocalEchoPromise.then([ctx, rid, status](auto st) {
+ kzo.client.dbg() << "sending encrypted message" << std::endl;
+ kzo.client.dbg() << "data: " << st.data().get().dump() << std::endl;
+ auto txnId = st.dataStr("txnId");
+ kzo.client.dbg() << "sending encrypted message with txn id " << txnId << std::endl;
+ auto ev = Event(status.dataJson("encrypted"));
+
+ return ctx.dispatch(SendMessageAction{rid, ev, txnId});
+ });
};
return promise
.then(encryptEvent)
+ .then(saveEncryptedLocalEcho)
.then(maybeSendKeys)
.then(sendEncryptedEvent);
}
auto Room::sendTextMessage(std::string text) const
-> PromiseT
{
json j{
{"type", "m.room.message"},
{"content", {
{"msgtype", "m.text"},
{"body", text}
}
}
};
Event e{j};
return sendMessage(e);
}
auto Room::refreshRoomState() const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(GetRoomStatesAction{+roomId()});
}
auto Room::getStateEvent(std::string type, std::string stateKey) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(GetStateEventAction{+roomId(), type, stateKey});
}
auto Room::sendStateEvent(Event state) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(SendStateEventAction{+roomId(), state});
}
auto Room::setName(std::string name) const
-> PromiseT
{
json j{
{"type", "m.room.name"},
{"content", {
{"name", name}
}
}
};
Event e{j};
return sendStateEvent(e);
}
auto Room::setTopic(std::string topic) const
-> PromiseT
{
json j{
{"type", "m.room.topic"},
{"content", {
{"topic", topic}
}
}
};
Event e{j};
return sendStateEvent(e);
}
auto Room::invite(std::string userId) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(InviteToRoomAction{+roomId(), userId});
}
auto Room::setTyping(bool typing, std::optional<int> timeoutMs) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(SetTypingAction{+roomId(), typing, timeoutMs});
}
auto Room::leave() const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(LeaveRoomAction{+roomId()});
}
auto Room::forget() const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(ForgetRoomAction{+roomId()});
}
auto Room::kick(std::string userId, std::optional<std::string> reason) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(KickAction{+roomId(), userId, reason});
}
auto Room::ban(std::string userId, std::optional<std::string> reason) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(BanAction{+roomId(), userId, reason});
}
auto Room::unban(std::string userId/*, std::optional<std::string> reason*/) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(UnbanAction{+roomId(), userId});
}
auto Room::setPinnedEvents(immer::flex_vector<std::string> eventIds) const
-> PromiseT
{
json j{
{"type", "m.room.pinned_events"},
{"content", {
{"pinned", eventIds}
}
}
};
Event e{j};
return sendStateEvent(e);
}
auto Room::timelineGaps() const
-> lager::reader<immer::map<std::string /* eventId */,
std::string /* prevBatch */>>
{
return roomCursor()[&RoomModel::timelineGaps];
}
auto Room::paginateBackFromEvent(std::string eventId) const
-> PromiseT
{
using namespace CursorOp;
return m_ctx.dispatch(PaginateTimelineAction{
+roomId(), eventId, std::nullopt});
}
auto Room::localEchoes() const -> lager::reader<immer::flex_vector<LocalEchoDesc>>
{
return roomCursor()[&RoomModel::localEchoes];
}
}
diff --git a/src/tests/client/client-test-util.hpp b/src/tests/client/client-test-util.hpp
index a51c16a..0a1a236 100644
--- a/src/tests/client/client-test-util.hpp
+++ b/src/tests/client/client-test-util.hpp
@@ -1,70 +1,72 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <lager/store.hpp>
#include <lager/event_loop/manual.hpp>
#include <store.hpp>
#include <sdk.hpp>
#include <client/client-model.hpp>
#include <base/basejob.hpp>
using namespace Kazv;
ClientModel createTestClientModel();
template<class DataT>
inline Response createResponse(std::string jobId, DataT body, JsonWrap data = {})
{
Response r;
r.statusCode = 200;
r.body = body;
json origData = data.get();
origData["-job-id"] = jobId;
r.extraData = origData;
return r;
}
template<>
inline Response createResponse(std::string jobId, json j, JsonWrap data)
{
return createResponse(jobId, JsonWrap(j), data);
}
inline auto createTestClientStore(SingleTypePromiseInterface<DefaultRetType> ph)
{
return makeStore<ClientAction>(
createTestClientModel(),
&ClientModel::update,
std::move(ph));
}
inline auto createTestClientStoreFrom(ClientModel m, SingleTypePromiseInterface<DefaultRetType> ph)
{
return makeStore<ClientAction>(
std::move(m),
&ClientModel::update,
std::move(ph));
}
+using TestClientStoreT = decltype(createTestClientStoreFrom(std::declval<ClientModel>(), std::declval<SingleTypePromiseInterface<DefaultRetType>>()));
+
bool hasAccessToken(const BaseJob &job);
template<class Model>
void assert1Job(Model &&model)
{
REQUIRE(std::forward<Model>(model).nextJobs.size() == 1);
}
template<class Model, class Pred>
void for1stJob(Model &&model, Pred &&pred)
{
std::forward<Pred>(pred)(std::forward<Model>(model).nextJobs[0]);
}
Context<SdkAction> dumbContext();
diff --git a/src/tests/client/room/local-echo-test.cpp b/src/tests/client/room/local-echo-test.cpp
index befca69..9cd630d 100644
--- a/src/tests/client/room/local-echo-test.cpp
+++ b/src/tests/client/room/local-echo-test.cpp
@@ -1,109 +1,189 @@
/*
* 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"
using namespace Kazv;
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) {
+ kzo.client.dbg() << "dispatched: index " << action.index() << std::endl;
+ func(action);
+ if (std::holds_alternative<GetRoomStatesAction>(action)
+ || std::holds_alternative<QueryKeysAction>(action)
+ || std::holds_alternative<SendToDeviceMessageAction>(action)
+ || std::holds_alternative<SendMessageAction>(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<EncryptOlmEventAction>(action)) {
+ return ph.createResolved(EffectStatus{true, json::object({{"encrypted", json::object({{"type", ""}})}})});
+ } else if (std::holds_alternative<SaveLocalEchoAction>(action)
+ || std::holds_alternative<EncryptMegOlmEventAction>(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 = createResponse("SendMessage", json::object({}), json{
{"roomId", roomId},
{"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 mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled](const auto &a) {
+ if (std::holds_alternative<SaveLocalEchoAction>(a)) {
+ ++saveLocalEchoCalled;
+ }
+ });
+
+ 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 st) {
+ REQUIRE(st.success());
+ kzo.client.dbg() << "ended" << std::endl;
+ io.stop();
+ });
+
+ io.run();
+
+ REQUIRE(saveLocalEchoCalled == 2);
+}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Sep 18, 11:29 PM (4 h, 48 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768477
Default Alt Text
(50 KB)

Event Timeline