Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
60 KB
Referenced Files
None
Subscribers
None
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

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)

Event Timeline