Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85827091
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
60 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/client/client.cpp b/src/client/client.cpp
index 139dfd3..a9cb6fe 100644
--- a/src/client/client.cpp
+++ b/src/client/client.cpp
@@ -1,813 +1,818 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <filesystem>
#include <algorithm>
#include <chrono>
#include <lager/constant.hpp>
#include "client.hpp"
#include "client-model.hpp"
#include "alias.hpp"
#include "immer-utils.hpp"
namespace Kazv
{
static Timestamp tsNow()
{
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
).count();
}
static Client::PromiseT sendMultiVerificationEvents(Client::ContextT ctx, VerificationTracker::PendingEvents pendingEvents)
{
- std::vector<Client::PromiseT> ps;
+ auto p = ctx.createResolvedPromise({});
for (auto ed : pendingEvents) {
- ps.push_back(ctx.dispatch(SendToDeviceMessageAction{
- ed.event,
- {{ed.toUserId, {ed.toDeviceId}}}
- }));
+ p = p.then([ctx, ed](auto &&stat) {
+ return ctx.dispatch(SendToDeviceMessageAction{
+ ed.event,
+ {{ed.toUserId, {ed.toDeviceId}}}
+ }).then([stat](auto &&cur) {
+ using PromiseCombination::dataCombine;
+ return dataCombine(stat, cur);
+ });
+ });
}
- return ctx.promiseInterface().all(ps);
+ return p;
}
static Client::PromiseT maybeMarkDevicesVerified(Client::ContextT ctx, VerificationTracker::PendingEvents pendingEvents)
{
immer::map<std::string, immer::map<std::string, DeviceTrustLevel>> trustLevelMap;
for (auto ed : pendingEvents) {
if (VerificationUtils::typeOf(ed.event) == VerificationEventTypes::tDone) {
// a process asking us to send an outbound done event means that we have verified them
trustLevelMap = setIn(trustLevelMap, DeviceTrustLevel::Verified, ed.toUserId, ed.toDeviceId);
kzo.client.dbg() << "maybeMarkDevicesVerified: verified " << ed.toUserId << "/" << ed.toDeviceId << std::endl;
}
}
if (!trustLevelMap.empty()) {
return ctx.dispatch(SetDevicesTrustLevelsAction{trustLevelMap});
}
return ctx.createResolvedPromise({});
}
static Client::PromiseT verificationChangePostProcess(Client::ContextT ctx, VerificationTracker::PendingEvents es)
{
return ctx.dispatch(NotifyVerificationTrackerModelAction{})
.then([ctx, es](auto &&) {
return maybeMarkDevicesVerified(ctx, es);
})
.then([ctx, es](auto &&) {
return sendMultiVerificationEvents(ctx, es);
});
}
Client::Client(lager::reader<SdkModel> sdk,
ContextT ctx, std::nullopt_t)
: m_sdk(sdk)
, m_client(sdk.map(&SdkModel::c))
, m_ctx(std::move(ctx))
{
}
Client::Client(lager::reader<SdkModel> sdk,
ContextWithDepsT ctx)
: m_sdk(sdk)
, m_client(sdk.map(&SdkModel::c))
, m_ctx(ctx)
, m_deps(std::move(ctx))
{
}
Client::Client(InEventLoopTag,
ContextWithDepsT ctx)
: m_sdk(std::nullopt)
, m_client(std::nullopt)
, m_ctx(ctx)
, m_deps(std::move(ctx))
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, KAZV_ON_EVENT_LOOP_VAR(true)
#endif
{
}
Client::Client(InEventLoopTag,
ContextT ctx, DepsT deps)
: m_sdk(std::nullopt)
, m_client(std::nullopt)
, m_ctx(std::move(ctx))
, m_deps(std::move(deps))
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, KAZV_ON_EVENT_LOOP_VAR(true)
#endif
{
}
Client Client::toEventLoop() const
{
return Client(InEventLoopTag{}, m_ctx, m_deps.value());
}
Room Client::room(std::string id) const
{
if (m_deps.has_value()) {
return Room(sdkCursor(), lager::make_constant(id), m_ctx, m_deps.value());
} else {
return Room(sdkCursor(), lager::make_constant(id), m_ctx);
}
}
Room Client::roomByCursor(lager::reader<std::string> id) const
{
if (m_deps.has_value()) {
return Room(sdkCursor(), id, m_ctx, m_deps.value());
} else {
return Room(sdkCursor(), id, m_ctx);
}
}
auto Client::passwordLogin(std::string homeserver, std::string username,
std::string password, std::string deviceName, bool startSyncingOnSuccess) const
-> PromiseT
{
auto p1 = m_ctx.dispatch(LoginAction{
homeserver, username, password, deviceName});
if (startSyncingOnSuccess) {
p1.then([that=toEventLoop()](auto stat) {
if (! stat.success()) {
return;
}
// It is meaningless to wait for it in a Promise
// that is never exposed to the user.
that.startSyncing();
});
}
return p1;
}
auto Client::passwordLogin(std::string homeserver, std::string username,
std::string password, std::string deviceName) const
-> PromiseT
{
return passwordLogin(std::move(homeserver), std::move(username),
std::move(password), std::move(deviceName), /* startSyncingOnSuccess = */ true);
}
auto Client::mLoginTokenLogin(
std::string homeserver,
std::string loginToken,
std::optional<std::string> deviceName,
bool startSyncingOnSuccess
) const -> PromiseT
{
auto p1 = m_ctx.dispatch(MLoginTokenLoginAction{
homeserver, loginToken, deviceName});
if (startSyncingOnSuccess) {
p1.then([that=toEventLoop()](auto stat) {
if (! stat.success()) {
return;
}
that.startSyncing();
});
}
return p1;
}
auto Client::mLoginTokenLogin(
std::string homeserver,
std::string loginToken,
std::optional<std::string> deviceName
) const -> PromiseT
{
return mLoginTokenLogin(std::move(homeserver), std::move(loginToken),
std::move(deviceName), /* startSyncingOnSuccess = */ true);
}
auto Client::tokenLogin(std::string homeserver, std::string username,
std::string token, std::string deviceId, bool startSyncingOnSuccess) const
-> PromiseT
{
auto p1 = m_ctx.dispatch(TokenLoginAction{
homeserver, username, token, deviceId});
if (startSyncingOnSuccess) {
p1.then([that=toEventLoop()](auto stat) {
if (! stat.success()) {
return;
}
that.startSyncing();
});
}
return p1;
}
auto Client::tokenLogin(std::string homeserver, std::string username,
std::string token, std::string deviceId) const
-> PromiseT
{
return tokenLogin(std::move(homeserver), std::move(username),
std::move(token), std::move(deviceId), /* startSyncingOnSuccess = */ true);
}
auto Client::shouldSync() const -> lager::reader<bool> {
return this->clientCursor()[&ClientModel::shouldSync];
}
auto Client::logout() const
-> PromiseT
{
return stopSyncing().then([ctx=m_ctx] ([[maybe_unused]] auto stat) {
return ctx.dispatch(HardLogoutAction{});
});
}
auto Client::autoDiscover(std::string userId) const
-> PromiseT
{
return m_ctx.dispatch(GetWellknownAction{userId})
.then([that=toEventLoop()](auto stat) {
if (!stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
return that.m_ctx.dispatch(GetVersionsAction{stat.dataStr("homeserverUrl")})
.then([that, stat](auto stat2) {
if (!stat2.success()) {
return stat2;
} else {
return stat;
}
});
});
}
auto Client::createRoom(
RoomVisibility v,
std::optional<std::string> name,
std::optional<std::string> alias,
immer::array<std::string> invite,
std::optional<bool> isDirect,
bool allowFederate,
std::optional<std::string> topic,
JsonWrap powerLevelContentOverride,
std::optional<CreateRoomPreset> preset,
immer::array<Event> initialState
) const
-> PromiseT
{
CreateRoomAction a;
a.visibility = v;
a.name = name;
a.roomAliasName = alias;
a.invite = invite;
a.isDirect = isDirect;
a.topic = topic;
a.powerLevelContentOverride = powerLevelContentOverride;
// Synapse won't buy it if we do not provide
// a creationContent object.
a.creationContent = json{
{"m.federate", allowFederate}
};
a.preset = preset;
a.initialState = initialState;
return m_ctx.dispatch(std::move(a));
}
auto Client::joinRoomById(std::string roomId) const -> PromiseT
{
return m_ctx.dispatch(JoinRoomByIdAction{roomId});
}
auto Client::joinRoom(std::string roomId, immer::array<std::string> serverName) const
-> PromiseT
{
return m_ctx.dispatch(JoinRoomAction{roomId, serverName});
}
auto Client::uploadContent(immer::box<Bytes> content,
std::string uploadId,
std::optional<std::string> filename,
std::optional<std::string> contentType) const
-> PromiseT
{
return m_ctx.dispatch(UploadContentAction{
FileDesc(FileContent{content.get().begin(), content.get().end()}),
filename, contentType, uploadId});
}
auto Client::uploadContent(FileDesc file) const
-> PromiseT
{
auto basename = file.name()
? std::optional(std::filesystem::path(file.name().value()).filename().string())
: std::nullopt;
return m_ctx.dispatch(UploadContentAction{
file,
// use only basename to prevent path info being leaked
basename,
file.contentType(),
// uploadId unused
std::string{}});
}
std::string Client::mxcUriToHttpV1(std::string mxcUri) const {
using namespace CursorOp;
auto [serverName, mediaId] = mxcUriToMediaDesc(mxcUri);
return (+clientCursor())
.template job<GetContentJobV1>()
.make(serverName, mediaId).url();
}
auto Client::downloadContent(std::string mxcUri, std::optional<FileDesc> downloadTo) const
-> PromiseT
{
return m_ctx.dispatch(DownloadContentAction{mxcUri, downloadTo});
}
auto Client::downloadThumbnail(
std::string mxcUri,
int width,
int height,
std::optional<ThumbnailResizingMethod> method,
std::optional<FileDesc> downloadTo) const
-> PromiseT
{
return m_ctx.dispatch(DownloadThumbnailAction{mxcUri, width, height, method, std::nullopt, downloadTo});
}
auto Client::startSyncing() const -> PromiseT
{
KAZV_VERIFY_THREAD_ID();
using namespace Kazv::CursorOp;
if (+syncing()) {
return m_ctx.createResolvedPromise(true);
}
auto p1 = ensureReadyForSyncing();
p1
.then([m_ctx=m_ctx](auto stat) {
m_ctx.dispatch(SetShouldSyncAction{true});
return stat;
})
.then([that=toEventLoop()](auto stat) {
if (stat.success()) {
that.syncForever();
}
});
return p1;
}
auto Client::processVerificationEventsFromSync(EventList toDeviceEvents) const -> PromiseT
{
return ensureInitVerificationTracker()
.then([that=toEventLoop(), ves=toDeviceEvents](auto &&stat) {
if (!stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
auto now = tsNow();
auto es = vt.processIncoming(now, ves);
auto p = maybeMarkDevicesVerified(that.m_ctx, es);
for (auto ed : es) {
// We don't actually need to wait for these events to
// be sent before we proceed into next sync cycle.
that.m_ctx.dispatch(SendToDeviceMessageAction{
ed.event,
{{ed.toUserId, {ed.toDeviceId}}}
});
}
return p.then([ctx=that.m_ctx](const auto &) {
return ctx.dispatch(NotifyVerificationTrackerModelAction{});
});
});
}
auto Client::requestOutgoingToDeviceVerification(std::string userId, std::string deviceId) const -> PromiseT
{
return ensureInitVerificationTracker()
.then([that=toEventLoop(), userId, deviceId](auto &&stat) {
if (!stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
return that.m_ctx.dispatch(EnsureKeysFromDevicesAction{
{{userId, {deviceId}}},
});
})
.then([that=toEventLoop(), userId, deviceId](auto &&stat) {
if (!stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
auto deviceOpt = that.clientCursor().map([userId, deviceId](const ClientModel &client) {
return client.deviceLists.get(userId, deviceId);
}).make().get();
if (!deviceOpt) {
return that.m_ctx.createResolvedPromise({ /* succ = */ false, json::object({
{"errorCode", "MOE.KAZV.MXC.NO_DEVICE_KEYS"},
{"error", "Cannot obtain device keys"},
})});
}
auto device = deviceOpt.value();
auto es = vt.requestOutgoingToDevice(VerificationUtils::DeviceIdentity{
userId,
deviceId,
device.ed25519Key,
}, tsNow());
return verificationChangePostProcess(that.m_ctx, es);
});
}
auto Client::readyForVerification(std::string userId, std::string deviceId) const -> PromiseT
{
return ensureInitVerificationTracker()
.then([that=toEventLoop(), userId, deviceId](auto &&stat) {
if (!stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
return that.m_ctx.dispatch(EnsureKeysFromDevicesAction{
{{userId, {deviceId}}},
});
})
.then([that=toEventLoop(), userId, deviceId](auto &&stat) {
if (!stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
auto deviceOpt = that.clientCursor().map([userId, deviceId](const ClientModel &client) {
return client.deviceLists.get(userId, deviceId);
}).make().get();
if (!deviceOpt) {
return that.m_ctx.createResolvedPromise({ /* succ = */ false, json::object({
{"errorCode", "MOE.KAZV.MXC.NO_DEVICE_KEYS"},
{"error", "Cannot obtain device keys"},
})});
}
auto device = deviceOpt.value();
vt.setTheirIdentity(VerificationUtils::DeviceIdentity{
userId,
deviceId,
device.ed25519Key,
});
auto es = vt.userReady(userId, deviceId);
return verificationChangePostProcess(that.m_ctx, es);
});
}
auto Client::cancelVerification(std::string userId, std::string deviceId) const -> PromiseT
{
return ensureInitVerificationTracker()
.then([that=toEventLoop(), userId, deviceId](auto &&) {
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
auto es = vt.userCancel(userId, deviceId);
return verificationChangePostProcess(that.m_ctx, es);
});
}
auto Client::confirmVerificationSasMatch(std::string userId, std::string deviceId) const -> PromiseT
{
return ensureInitVerificationTracker()
.then([that=toEventLoop(), userId, deviceId](auto &&) {
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
auto es = vt.userConfirmMatch(userId, deviceId);
return verificationChangePostProcess(that.m_ctx, es);
});
}
auto Client::denyVerificationSasMatch(std::string userId, std::string deviceId) const -> PromiseT
{
return ensureInitVerificationTracker()
.then([that=toEventLoop(), userId, deviceId](auto &&) {
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
auto es = vt.userDenyMatch(userId, deviceId);
return verificationChangePostProcess(that.m_ctx, es);
});
}
auto Client::ensureInitVerificationTracker() const -> PromiseT
{
if (!m_deps) {
return m_ctx.createResolvedPromise(false);
}
bool hasCrypto{clientCursor().map([](const auto &c) {
return c.crypto.has_value();
}).make().get()};
if (!hasCrypto) {
return m_ctx.createResolvedPromise(false);
}
return m_ctx.createResolvedPromise({})
.then([that=toEventLoop()](auto) {
auto &vt = lager::get<VerificationTracker>(that.m_deps.value());
if (vt.identity.userId.empty()) {
auto client = that.clientCursor().get();
const auto &crypto = client.constCrypto();
vt.identity = {
client.userId,
client.deviceId,
crypto.ed25519IdentityKey(),
};
}
return EffectStatus{/* succ = */ true};
});
}
auto Client::requestShareRoomSessionKey(
std::string roomId,
Event event,
immer::map<std::string, immer::flex_vector<std::string>> devices
) const -> PromiseT
{
return m_ctx.dispatch(RequestShareRoomSessionKeyAction{
std::move(roomId),
std::move(event),
std::move(devices)
});
}
auto Client::syncForever(std::optional<int> retryTime) const -> void
{
KAZV_VERIFY_THREAD_ID();
// assert (m_deps);
using namespace CursorOp;
bool shouldSync = +clientCursor()[&ClientModel::shouldSync];
if (! shouldSync) {
return;
}
doOneSyncCycle()
.then([that=toEventLoop(), retryTime](auto stat) {
if (stat.success()) {
that.syncForever(); // reset retry time
} else {
auto firstRetryTime = +that.clientCursor()[&ClientModel::firstRetryMs];
auto retryTimeFactor = +that.clientCursor()[&ClientModel::retryTimeFactor];
auto maxRetryTime = +that.clientCursor()[&ClientModel::maxRetryMs];
auto curRetryTime = retryTime ? retryTime.value() : firstRetryTime;
if (curRetryTime > maxRetryTime) { curRetryTime = maxRetryTime; }
auto nextRetryTime = curRetryTime * retryTimeFactor;
kzo.client.warn() << "Sync failed, retrying in " << curRetryTime << "ms" << std::endl;
auto &jh = getJobHandler(that.m_deps.value());
jh.setTimeout([that=that.toEventLoop(), nextRetryTime]() { that.syncForever(nextRetryTime); },
curRetryTime);
}
});
}
auto Client::stopSyncing() const -> PromiseT
{
return m_ctx.dispatch(SetShouldSyncAction{false});
}
auto Client::ensureReadyForSyncing() const -> PromiseT
{
KAZV_VERIFY_THREAD_ID();
using namespace Kazv::CursorOp;
return m_ctx.createResolvedPromise(true)
.then([that=toEventLoop()](auto) {
// post filters, if filters are incomplete
if ((+that.clientCursor()[&ClientModel::initialSyncFilterId]).empty()
|| (+that.clientCursor()[&ClientModel::incrementalSyncFilterId]).empty()) {
return that.m_ctx.dispatch(PostInitialFiltersAction{});
}
return that.m_ctx.createResolvedPromise(true);
})
.then([that=toEventLoop()](auto stat) {
if (! stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
// Upload identity keys if we need to
if (+that.clientCursor()[&ClientModel::crypto]
&& ! +that.clientCursor()[&ClientModel::identityKeysUploaded]) {
return that.m_ctx.dispatch(UploadIdentityKeysAction{});
} else {
return that.m_ctx.createResolvedPromise(true);
}
});
}
auto Client::doOneSyncCycle() const -> PromiseT
{
KAZV_VERIFY_THREAD_ID();
using namespace CursorOp;
bool isInitialSync = !(+clientCursor()[&ClientModel::syncToken]).has_value();
auto syncRes = m_ctx.dispatch(SyncAction{});
auto uploadOneTimeKeysRes = syncRes
.then([that=toEventLoop()](auto stat) {
if (! stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
auto &rg = lager::get<RandomInterface &>(that.m_deps.value());
bool hasCrypto{+that.clientCursor()[&ClientModel::crypto]};
if (! hasCrypto) {
return that.m_ctx.createResolvedPromise(true);
}
auto numKeysToGenerate = (+that.clientCursor()).numOneTimeKeysNeeded();
return that.m_ctx.dispatch(GenerateAndUploadOneTimeKeysAction{
numKeysToGenerate,
rg.generateRange<RandomData>(GenerateAndUploadOneTimeKeysAction::randomSize(numKeysToGenerate))
});
});
auto queryKeysRes = syncRes
.then([that=toEventLoop(), isInitialSync](auto stat) {
if (! stat.success()) {
return that.m_ctx.createResolvedPromise(stat);
}
bool hasCrypto{+that.clientCursor()[&ClientModel::crypto]};
return hasCrypto
? that.m_ctx.dispatch(QueryKeysAction{isInitialSync})
: that.m_ctx.createResolvedPromise(true);
});
auto processVerificationEventsRes = syncRes
.then([that=toEventLoop()](EffectStatus stat) {
if (!stat.success() || !that.m_deps ||
!that.clientCursor().map([](const auto &c) {
return c.crypto.has_value();
}).make().get()) {
return that.m_ctx.createResolvedPromise(stat);
}
kzo.client.dbg() << "processVerificationEvents: " << stat.data().get().dump() << std::endl;
EventList ves = stat.data().get().at("verificationEvents").at("toDevice").template get<EventList>();
return that.processVerificationEventsFromSync(ves);
});
return m_ctx.promiseInterface()
.all(std::vector<PromiseT>{uploadOneTimeKeysRes, queryKeysRes, processVerificationEventsRes});
}
lager::reader<ClientModel> Client::clientCursor() const
{
KAZV_VERIFY_THREAD_ID();
if (m_client.has_value()) {
return m_client.value();
} else {
assert(m_deps.has_value());
return lager::get<SdkModelCursorKey>(m_deps.value())->map(&SdkModel::c);
}
}
const lager::reader<SdkModel> &Client::sdkCursor() const
{
KAZV_VERIFY_THREAD_ID();
if (m_sdk.has_value()) {
return m_sdk.value();
} else {
assert(m_deps.has_value());
return *(lager::get<SdkModelCursorKey>(m_deps.value()));
}
}
auto Client::getProfile(std::string userId) const -> PromiseT
{
return m_ctx.dispatch(GetUserProfileAction{userId});
}
auto Client::setAvatarUrl(std::optional<std::string> avatarUrl) const -> PromiseT
{
return m_ctx.dispatch(SetAvatarUrlAction{avatarUrl});
}
auto Client::setDisplayName(std::optional<std::string> displayName) const -> PromiseT
{
return m_ctx.dispatch(SetDisplayNameAction{displayName});
}
auto Client::devicesOfUser(std::string userId) const -> lager::reader<immer::flex_vector<DeviceKeyInfo>>
{
return clientCursor()
[&ClientModel::deviceLists]
[&DeviceListTracker::deviceLists]
[userId]
[lager::lenses::or_default]
.xform(containerMap(immer::flex_vector<DeviceKeyInfo>{}, zug::map([](const auto &pair) {
const auto &[deviceId, info] = pair;
(void)deviceId;
return info;
})));
}
auto Client::setDeviceTrustLevel(std::string userId, std::string deviceId, DeviceTrustLevel trustLevel) const -> PromiseT
{
return m_ctx.dispatch(SetDeviceTrustLevelAction{userId, deviceId, trustLevel});
}
auto Client::trustLevelNeededToSendKeys() const -> lager::reader<DeviceTrustLevel>
{
return clientCursor()[&ClientModel::trustLevelNeededToSendKeys];
}
auto Client::setTrustLevelNeededToSendKeys(DeviceTrustLevel trustLevel) const -> PromiseT
{
return m_ctx.dispatch(SetTrustLevelNeededToSendKeysAction{trustLevel});
}
auto Client::directRoomMap() const -> lager::reader<immer::map<std::string, std::string>>
{
return clientCursor().map(&ClientModel::directRoomMap);
}
auto Client::roomIdsUnderTag(std::string tagId) const -> lager::reader<immer::map<std::string, double>>
{
return clientCursor().map([tagId](const auto &c) {
return c.roomIdsUnderTag(tagId);
});
}
auto Client::roomIdsByTagId() const -> lager::reader<immer::map<std::string, immer::map<std::string, double>>>
{
return clientCursor().map(&ClientModel::roomIdsByTagId);
}
auto Client::accountData() const -> lager::reader<immer::map<std::string, Event>>
{
return clientCursor()[&ClientModel::accountData];
}
auto Client::setAccountData(Event accountDataEvent) const -> PromiseT
{
return m_ctx.dispatch(SetAccountDataAction{accountDataEvent});
}
NotificationHandler Client::notificationHandler() const
{
return NotificationHandler(clientCursor());
}
auto Client::getVersions(std::string homeserver) const -> PromiseT
{
return m_ctx.dispatch(GetVersionsAction{homeserver});
}
auto Client::supportVersions() const -> lager::reader<immer::array<std::string>>
{
return clientCursor()[&ClientModel::versions];
}
auto Client::addDirectRoom(std::string userId, std::string roomId) const -> PromiseT
{
auto content = this->accountData().get()["m.direct"].content().get();
if (content.contains(userId)) {
auto& rooms = content[userId];
if (rooms.is_array()) {
if (std::find(rooms.begin(), rooms.end(), roomId) != rooms.end()) {
// The roomId is already in the m.direct, do nothing
return m_ctx.createResolvedPromise(true);
}
} else {
rooms = json::array({});
}
} else {
content.emplace(userId, json::array({}));
}
content[userId].push_back(roomId);
return Client::setAccountData(json{
{"type", "m.direct"},
{"content", std::move(content)}
});
}
auto Client::getRoomIdByAliasJob(std::string roomAlias) const -> BaseJob
{
return Kazv::getRoomIdByAliasJob(clientCursor().get(), roomAlias);
}
auto Client::purgeRoomEvents(immer::map<std::string, std::size_t> roomIdToMaxToKeepMap) const -> PromiseT
{
return m_ctx.dispatch(PurgeRoomTimelineAction{roomIdToMaxToKeepMap});
}
auto Client::loadEventsFromStorage(immer::map<std::string, EventList> timelineEvents, immer::map<std::string, EventList> relatedEvents) const -> PromiseT
{
return m_ctx.dispatch(LoadEventsFromStorageAction{
std::move(timelineEvents),
std::move(relatedEvents),
});
}
auto Client::importFromKeyBackupFile(std::string fileContent, std::string password) const -> PromiseT
{
return m_ctx.dispatch(ImportFromKeyBackupFileAction{
std::move(fileContent),
std::move(password),
});
}
}
diff --git a/src/tests/client/verification-processing-test.cpp b/src/tests/client/verification-processing-test.cpp
index a206ae5..b67beb3 100644
--- a/src/tests/client/verification-processing-test.cpp
+++ b/src/tests/client/verification-processing-test.cpp
@@ -1,590 +1,701 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <catch2/catch_test_macros.hpp>
#include <boost/asio.hpp>
#include <client/client.hpp>
#include <client/client-model.hpp>
#include <client/actions/encryption.hpp>
#include <crypto/verification-tracker.hpp>
#include <crypto/verification-utils.hpp>
#include <crypto.hpp>
#include "client-test-util.hpp"
#include "action-mock-utils.hpp"
#include "factory.hpp"
using namespace Kazv;
using namespace Kazv::Factory;
namespace VCC = Kazv::VerificationCancelCodes;
namespace VPS = Kazv::VerificationProcessStates;
using namespace Kazv::VerificationEventTypes;
using VTM = VerificationTrackerModel;
namespace
{
static Event withSender(Event e, std::string userId)
{
auto j = e.originalJson().get();
j["sender"] = userId;
return Event(j);
}
static ComposedModifier<ClientModel> withClientInfo(std::string userId, std::string deviceId)
{
return [userId, deviceId](ClientModel &m) {
m.userId = userId;
m.deviceId = deviceId;
};
}
static json makeDeviceInfo(const ClientModel &client)
{
auto [next, _] = updateClient(client, UploadIdentityKeysAction{});
return json::parse(std::get<Bytes>(next.nextJobs[0].requestBody()))["device_keys"];
}
struct VerificationTestSetup
{
std::string userIdA;
std::string userIdB;
std::string deviceIdA;
std::string deviceIdB;
MockSdkUtil utilA;
MockSdkUtil utilB;
json deviceInfoA;
json deviceInfoB;
VerificationTestSetup(boost::asio::io_context::executor_type ex)
: userIdA("@alice:example.com")
, userIdB("@bob:example.com")
, deviceIdA("AliceDevice")
, deviceIdB("BobDevice")
, utilA(ex, makeClient(withCrypto(makeCrypto()) | withClientInfo(userIdA, deviceIdA)))
, utilB(ex, makeClient(withCrypto(makeCrypto()) | withClientInfo(userIdB, deviceIdB)))
, deviceInfoA(makeDeviceInfo(lager::get<SdkModelCursorKey>(utilA.sdk.context())->get().client))
, deviceInfoB(makeDeviceInfo(lager::get<SdkModelCursorKey>(utilB.sdk.context())->get().client))
{
}
template<class PH, class Ctx>
auto handleKeyRequest([[maybe_unused]] PH &ph, Ctx &ctx, EnsureKeysFromDevicesAction a) -> typename MockSdkUtil::ContextT::PromiseT
{
if (!a.userIdToDeviceIdsMap.size()) {
return ctx.createResolvedPromise({});
}
auto userId = a.userIdToDeviceIdsMap.begin()->first;
auto deviceId = userId == userIdA ? deviceIdA : deviceIdB;
auto deviceInfo = userId == userIdA ? deviceInfoA : deviceInfoB;
auto body = json::object({
{"device_keys", {{userId, {
{deviceId, deviceInfo},
}}}},
});
auto resp = makeResponse(
"QueryKeys",
withResponseJsonBody(body)
| withResponseDataKV("deviceKeys", json(a.userIdToDeviceIdsMap)));
return ctx.dispatch(ProcessResponseAction{resp});
}
};
}
TEST_CASE("Client verification processing - complete verification flow", "[client][verification]")
{
boost::asio::io_context io;
VerificationTestSetup setup(io.get_executor());
auto dispatcherA = setup.utilA.getMockDispatcher(
makeHandler<EnsureKeysFromDevicesAction>([&setup](auto &ph, auto &ctx, EnsureKeysFromDevicesAction action) mutable {
return setup.handleKeyRequest(ph, ctx, action);
}),
returnEmpty<SendToDeviceMessageAction>(),
passDown<NotifyVerificationTrackerModelAction>(),
passDown<SetDevicesTrustLevelsAction>()
);
auto dispatcherB = setup.utilB.getMockDispatcher(
makeHandler<EnsureKeysFromDevicesAction>([&setup](auto &ph, auto &ctx, EnsureKeysFromDevicesAction action) mutable {
return setup.handleKeyRequest(ph, ctx, action);
}),
returnEmpty<SendToDeviceMessageAction>(),
passDown<NotifyVerificationTrackerModelAction>(),
passDown<SetDevicesTrustLevelsAction>()
);
auto clientA = setup.utilA.getClient(dispatcherA);
auto clientB = setup.utilB.getClient(dispatcherB);
SECTION("Alice initiates, Bob accepts, both confirm")
{
// Alice initiates verification
clientA.requestOutgoingToDeviceVerification(setup.userIdB, setup.deviceIdB)
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherA.calledTimes<SendToDeviceMessageAction>() == 1);
REQUIRE(dispatcherA.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
auto requestEvent = withSender(sentEvents.at(0).event, setup.userIdA);
dispatcherA.clear();
// After Alice initiates the verification, she should
// have queried Bob's keys and the device should be the default
// Unseen trust level.
auto deviceB = clientA.devicesOfUser(setup.userIdB).make().get().at(0);
REQUIRE(deviceB.deviceId == setup.deviceIdB);
REQUIRE(deviceB.trustLevel == DeviceTrustLevel::Unseen);
// Bob receives the request
return clientB.processVerificationEventsFromSync({requestEvent});
})
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherB.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
dispatcherB.clear();
// Bob signals ready
return clientB.readyForVerification(setup.userIdA, setup.deviceIdA);
})
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherB.calledTimes<SendToDeviceMessageAction>() == 1);
REQUIRE(dispatcherB.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
auto readyEvent = withSender(sentEvents.at(0).event, setup.userIdB);
dispatcherB.clear();
// By the time Bob signals ready for verification, he
// should have queried Alice's keys and marked it
// as the default Unseen trust level
auto deviceA = clientB.devicesOfUser(setup.userIdA).make().get().at(0);
REQUIRE(deviceA.deviceId == setup.deviceIdA);
REQUIRE(deviceA.trustLevel == DeviceTrustLevel::Unseen);
// Alice receives the ready event
return clientA.processVerificationEventsFromSync({readyEvent});
})
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherA.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
// Alice should have sent a start event
auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
REQUIRE(sentEvents.size() == 1);
Event startEvent = withSender(sentEvents.at(0).event, setup.userIdA);
REQUIRE(startEvent.type() == tStart);
dispatcherA.clear();
// Bob receives the start event
return clientB.processVerificationEventsFromSync({startEvent});
})
.then([&](auto stat) {
REQUIRE(stat.success());
// Bob should have sent an accept event
auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
REQUIRE(sentEvents.size() == 1);
Event acceptEvent = withSender(sentEvents.at(0).event, setup.userIdB);
REQUIRE(acceptEvent.type() == tAccept);
dispatcherB.clear();
// Alice receives accept event
return clientA.processVerificationEventsFromSync({acceptEvent});
})
.then([&](auto stat) {
REQUIRE(stat.success());
// Alice should have sent a key event
auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
REQUIRE(sentEvents.size() == 1);
// Find key event from Alice
Event keyEventA = withSender(sentEvents.at(0).event, setup.userIdA);
REQUIRE(keyEventA.type() == tKey);
dispatcherA.clear();
// Bob receives Alice's key event
return clientB.processVerificationEventsFromSync({keyEventA});
})
.then([&](auto stat) {
REQUIRE(stat.success());
// Bob should have sent a key event
auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
REQUIRE(sentEvents.size() == 1);
// Find key event from Bob
Event keyEventB = withSender(sentEvents.at(0).event, setup.userIdB);
REQUIRE(keyEventB.type() == tKey);
dispatcherB.clear();
// Alice receives Bob's key event
return clientA.processVerificationEventsFromSync({keyEventB});
})
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherA.calledTimes<SendToDeviceMessageAction>() == 0);
dispatcherA.clear();
auto stateA = lager::get<VerificationTracker>(setup.utilA.sdk.context()).model.processes.at(0).state;
auto stateB = lager::get<VerificationTracker>(setup.utilB.sdk.context()).model.processes.at(0).state;
REQUIRE(std::holds_alternative<VTM::ProcessCodeDisplayed>(stateA));
REQUIRE(std::holds_alternative<VTM::ProcessCodeDisplayed>(stateB));
REQUIRE(std::get<VTM::ProcessCodeDisplayed>(stateA).emojiIndices == std::get<VTM::ProcessCodeDisplayed>(stateB).emojiIndices);
// Alice confirms the SAS matches
return clientA.confirmVerificationSasMatch(setup.userIdB, setup.deviceIdB);
}).then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherA.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
auto macEventA = withSender(sentEvents.at(0).event, setup.userIdA);
REQUIRE(macEventA.type() == tMac);
dispatcherA.clear();
// Bob receives Alice's MAC
return clientB.processVerificationEventsFromSync({macEventA});
})
.then([&](auto stat) {
REQUIRE(stat.success());
dispatcherB.clear();
// Bob confirms the SAS match
return clientB.confirmVerificationSasMatch(setup.userIdA, setup.deviceIdA);
})
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherB.calledTimes<SendToDeviceMessageAction>() == 2);
auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
// Find MAC and done events from Bob
Event macEventB;
Event doneEventB;
for (auto se : sentEvents) {
if (se.event.type() == tMac) {
macEventB = withSender(se.event, setup.userIdB);
}
if (se.event.type() == tDone) {
doneEventB = withSender(se.event, setup.userIdB);
}
}
REQUIRE(macEventB.type() == tMac);
REQUIRE(doneEventB.type() == tDone);
dispatcherB.clear();
// By the time Bob wants to send a done event, he should
// have marked Alice's device as verified
auto deviceA = clientB.devicesOfUser(setup.userIdA).make().get().at(0);
REQUIRE(deviceA.deviceId == setup.deviceIdA);
REQUIRE(deviceA.trustLevel == DeviceTrustLevel::Verified);
// Alice receives Bob's MAC and done events
return clientA.processVerificationEventsFromSync({macEventB, doneEventB});
})
.then([&](auto stat) {
REQUIRE(stat.success());
// Alice should send a done event
auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
REQUIRE(sentEvents.size() == 1);
Event doneEventA = withSender(sentEvents.at(0).event, setup.userIdA);
REQUIRE(doneEventA.type() == tDone);
dispatcherA.clear();
// By the time Alice wants to send a done event, she should
// have marked Bob's device as verified
auto deviceB = clientA.devicesOfUser(setup.userIdB).make().get().at(0);
REQUIRE(deviceB.deviceId == setup.deviceIdB);
REQUIRE(deviceB.trustLevel == DeviceTrustLevel::Verified);
// Bob receives Alice's done event
return clientB.processVerificationEventsFromSync({doneEventA});
})
.then([&](auto stat) {
REQUIRE(stat.success());
io.stop();
});
io.run();
}
SECTION("Alice initiates, Bob cancels")
{
// Alice initiates verification
clientA.requestOutgoingToDeviceVerification(setup.userIdB, setup.deviceIdB)
.then([&](auto stat) {
REQUIRE(stat.success());
auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
auto requestEvent = withSender(sentEvents.at(0).event, setup.userIdA);
dispatcherA.clear();
// Bob receives the request
return clientB.processVerificationEventsFromSync({requestEvent});
})
.then([&](auto stat) {
REQUIRE(stat.success());
dispatcherB.clear();
// Bob cancels
return clientB.cancelVerification(setup.userIdA, setup.deviceIdA);
})
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherB.calledTimes<SendToDeviceMessageAction>() == 1);
REQUIRE(dispatcherB.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
auto cancelEvent = withSender(sentEvents.at(0).event, setup.userIdB);
REQUIRE(cancelEvent.type() == tCancel);
REQUIRE(cancelEvent.content().get().at("code") == VCC::userCancel);
dispatcherB.clear();
// Alice receives the cancel event
return clientA.processVerificationEventsFromSync({cancelEvent});
})
.then([&](auto stat) {
REQUIRE(stat.success());
// After receiving a cancel, Alice should not send another cancel
REQUIRE(dispatcherA.calledTimes<SendToDeviceMessageAction>() == 0);
io.stop();
});
io.run();
}
SECTION("Alice initiates, Bob accepts, both deny")
{
// Alice initiates verification
clientA.requestOutgoingToDeviceVerification(setup.userIdB, setup.deviceIdB)
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherA.calledTimes<SendToDeviceMessageAction>() == 1);
auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
auto requestEvent = withSender(sentEvents.at(0).event, setup.userIdA);
dispatcherA.clear();
// Bob receives the request
return clientB.processVerificationEventsFromSync({requestEvent});
})
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherB.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
dispatcherB.clear();
// Bob signals ready
return clientB.readyForVerification(setup.userIdA, setup.deviceIdA);
})
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherB.calledTimes<SendToDeviceMessageAction>() == 1);
auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
auto readyEvent = withSender(sentEvents.at(0).event, setup.userIdB);
dispatcherB.clear();
// Alice receives the ready event
return clientA.processVerificationEventsFromSync({readyEvent});
})
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherA.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
// Alice should have sent a start event
auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
REQUIRE(sentEvents.size() == 1);
Event startEvent = withSender(sentEvents.at(0).event, setup.userIdA);
REQUIRE(startEvent.type() == tStart);
dispatcherA.clear();
// Bob receives the start event
return clientB.processVerificationEventsFromSync({startEvent});
})
.then([&](auto stat) {
REQUIRE(stat.success());
// Bob should have sent an accept event
auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
REQUIRE(sentEvents.size() == 1);
Event acceptEvent = withSender(sentEvents.at(0).event, setup.userIdB);
REQUIRE(acceptEvent.type() == tAccept);
dispatcherB.clear();
// Alice receives accept event
return clientA.processVerificationEventsFromSync({acceptEvent});
})
.then([&](auto stat) {
REQUIRE(stat.success());
// Alice should have sent a key event
auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
REQUIRE(sentEvents.size() == 1);
// Find key event from Alice
Event keyEventA = withSender(sentEvents.at(0).event, setup.userIdA);
REQUIRE(keyEventA.type() == tKey);
dispatcherA.clear();
// Bob receives Alice's key event
return clientB.processVerificationEventsFromSync({keyEventA});
})
.then([&](auto stat) {
REQUIRE(stat.success());
// Bob should have sent a key event
auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
REQUIRE(sentEvents.size() == 1);
// Find key event from Bob
Event keyEventB = withSender(sentEvents.at(0).event, setup.userIdB);
REQUIRE(keyEventB.type() == tKey);
dispatcherB.clear();
// Alice receives Bob's key event
return clientA.processVerificationEventsFromSync({keyEventB});
})
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherA.calledTimes<SendToDeviceMessageAction>() == 0);
dispatcherA.clear();
// Alice denies the SAS matches
return clientA.denyVerificationSasMatch(setup.userIdB, setup.deviceIdB);
}).then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherA.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
auto sentEvents = dispatcherA.of<SendToDeviceMessageAction>();
auto cancelEventA = withSender(sentEvents.at(0).event, setup.userIdA);
REQUIRE(cancelEventA.type() == tCancel);
dispatcherA.clear();
})
.then([&](auto stat) {
REQUIRE(stat.success());
dispatcherB.clear();
// Bob denies the SAS match
return clientB.denyVerificationSasMatch(setup.userIdA, setup.deviceIdA);
})
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherB.calledTimes<SendToDeviceMessageAction>() >= 1);
auto sentEvents = dispatcherB.of<SendToDeviceMessageAction>();
auto cancelEventB = withSender(sentEvents.at(0).event, setup.userIdA);
REQUIRE(cancelEventB.type() == tCancel);
dispatcherB.clear();
io.stop();
});
io.run();
}
}
+TEST_CASE("Client verification processing - mac sent before done", "[client][verification]")
+{
+ boost::asio::io_context io;
+
+ VerificationTestSetup setup(io.get_executor());
+
+ bool macResolved = false;
+
+ auto dispatcherA = setup.utilA.getMockDispatcher(
+ makeHandler<EnsureKeysFromDevicesAction>([&setup](auto &ph, auto &ctx, EnsureKeysFromDevicesAction action) mutable {
+ return setup.handleKeyRequest(ph, ctx, action);
+ }),
+ returnEmpty<SendToDeviceMessageAction>(),
+ passDown<NotifyVerificationTrackerModelAction>(),
+ passDown<SetDevicesTrustLevelsAction>()
+ );
+ auto dispatcherB = setup.utilB.getMockDispatcher(
+ makeHandler<EnsureKeysFromDevicesAction>([&setup](auto &ph, auto &ctx, EnsureKeysFromDevicesAction action) mutable {
+ return setup.handleKeyRequest(ph, ctx, action);
+ }),
+ makeHandler<SendToDeviceMessageAction>([&io, &macResolved](auto &ph, auto &, SendToDeviceMessageAction a) {
+ if (a.event.type() == tMac) {
+ return ph.createResolveToPromise([&io, &macResolved, ph](auto resolve) {
+ auto timer = std::make_shared<boost::asio::steady_timer>(io);
+ timer->expires_after(std::chrono::milliseconds(100));
+ timer->async_wait([&macResolved, ph, resolve, timer](const boost::system::error_code &) {
+ macResolved = true;
+ resolve(ph.createResolved({}));
+ });
+ });
+ }
+ if (a.event.type() == tDone) {
+ REQUIRE(macResolved);
+ }
+ return ph.createResolved({});
+ }),
+ passDown<NotifyVerificationTrackerModelAction>(),
+ passDown<SetDevicesTrustLevelsAction>()
+ );
+
+ auto clientA = setup.utilA.getClient(dispatcherA);
+ auto clientB = setup.utilB.getClient(dispatcherB);
+
+ clientA.requestOutgoingToDeviceVerification(setup.userIdB, setup.deviceIdB)
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ auto requestEvent = withSender(dispatcherA.of<SendToDeviceMessageAction>().at(0).event, setup.userIdA);
+ dispatcherA.clear();
+ return clientB.processVerificationEventsFromSync({requestEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ dispatcherB.clear();
+ return clientB.readyForVerification(setup.userIdA, setup.deviceIdA);
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ auto readyEvent = withSender(dispatcherB.of<SendToDeviceMessageAction>().at(0).event, setup.userIdB);
+ dispatcherB.clear();
+ return clientA.processVerificationEventsFromSync({readyEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ auto startEvent = withSender(dispatcherA.of<SendToDeviceMessageAction>().at(0).event, setup.userIdA);
+ dispatcherA.clear();
+ return clientB.processVerificationEventsFromSync({startEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ auto acceptEvent = withSender(dispatcherB.of<SendToDeviceMessageAction>().at(0).event, setup.userIdB);
+ dispatcherB.clear();
+ return clientA.processVerificationEventsFromSync({acceptEvent});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ auto keyEventA = withSender(dispatcherA.of<SendToDeviceMessageAction>().at(0).event, setup.userIdA);
+ dispatcherA.clear();
+ return clientB.processVerificationEventsFromSync({keyEventA});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ auto keyEventB = withSender(dispatcherB.of<SendToDeviceMessageAction>().at(0).event, setup.userIdB);
+ dispatcherB.clear();
+ return clientA.processVerificationEventsFromSync({keyEventB});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ return clientA.confirmVerificationSasMatch(setup.userIdB, setup.deviceIdB);
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ auto macEventA = withSender(dispatcherA.of<SendToDeviceMessageAction>().at(0).event, setup.userIdA);
+ dispatcherA.clear();
+ return clientB.processVerificationEventsFromSync({macEventA});
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ dispatcherB.clear();
+ return clientB.confirmVerificationSasMatch(setup.userIdA, setup.deviceIdA);
+ })
+ .then([&](auto stat) {
+ REQUIRE(stat.success());
+ auto sent = dispatcherB.of<SendToDeviceMessageAction>();
+ REQUIRE(sent.size() == 2);
+ REQUIRE(sent.at(0).event.type() == tMac);
+ REQUIRE(sent.at(1).event.type() == tDone);
+ io.stop();
+ });
+ io.run();
+}
+
TEST_CASE("Client verification processing - tracker model updates", "[client][verification]")
{
boost::asio::io_context io;
VerificationTestSetup setup(io.get_executor());
auto dispatcherA = setup.utilA.getMockDispatcher(
returnEmpty<EnsureKeysFromDevicesAction>(),
returnEmpty<SendToDeviceMessageAction>(),
returnEmpty<NotifyVerificationTrackerModelAction>()
);
auto clientA = setup.utilA.getClient(dispatcherA);
// Process a verification request
Event requestEvent = R"({
"content": {
"from_device": "BobDevice",
"methods": ["m.sas.v1"],
"timestamp": 0,
"transaction_id": "testTxnId"
},
"type": "m.key.verification.request",
"sender": "@bob:example.com"
})"_json;
clientA.processVerificationEventsFromSync({requestEvent})
.then([&](auto stat) {
REQUIRE(stat.success());
REQUIRE(dispatcherA.calledTimes<NotifyVerificationTrackerModelAction>() == 1);
io.stop();
});
io.run();
}
TEST_CASE("Client verification processing - edge cases", "[client][verification]")
{
boost::asio::io_context io;
VerificationTestSetup setup(io.get_executor());
SECTION("Multiple verification events processed together")
{
auto dispatcherA = setup.utilA.getMockDispatcher(
returnEmpty<SendToDeviceMessageAction>(),
returnEmpty<NotifyVerificationTrackerModelAction>()
);
auto clientA = setup.utilA.getClient(dispatcherA);
Event requestEvent1 = R"({
"content": {
"from_device": "BobDevice",
"methods": ["m.sas.v1"],
"timestamp": 0,
"transaction_id": "txn1"
},
"type": "m.key.verification.request",
"sender": "@bob:example.com"
})"_json;
Event requestEvent2 = R"({
"content": {
"from_device": "BobDevice2",
"methods": ["m.sas.v1"],
"timestamp": 0,
"transaction_id": "txn1"
},
"type": "m.key.verification.request",
"sender": "@bob:example.com"
})"_json;
clientA.processVerificationEventsFromSync({requestEvent1, requestEvent2})
.then([&](auto stat) {
REQUIRE(stat.success());
io.stop();
});
io.run();
}
SECTION("Cancel non-existent verification process")
{
auto dispatcherA = setup.utilA.getMockDispatcher(
returnEmpty<SendToDeviceMessageAction>(),
passDown<NotifyVerificationTrackerModelAction>()
);
auto clientA = setup.utilA.getClient(dispatcherA);
// Try to cancel a verification that doesn't exist
clientA.cancelVerification(setup.userIdB, setup.deviceIdB)
.then([&](auto stat) {
REQUIRE(stat.success());
// Should still succeed, but no events sent
REQUIRE(dispatcherA.calledTimes<SendToDeviceMessageAction>() == 0);
io.stop();
});
io.run();
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Oct 30, 2:30 PM (17 h, 53 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1800211
Default Alt Text
(60 KB)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline