Page MenuHomePhorge

No OneTemporary

Size
91 KB
Referenced Files
None
Subscribers
None
diff --git a/README.md b/README.md
index 0f1dcde..5483721 100644
--- a/README.md
+++ b/README.md
@@ -1,225 +1,224 @@
# libkazv {#mainpage}
[![pipeline status](https://lily-is.land/kazv/libkazv/badges/servant/pipeline.svg)](https://lily-is.land/kazv/libkazv/-/commits/servant)
[![coverage report](https://lily-is.land/kazv/libkazv/badges/servant/coverage.svg)](https://lily-is.land/kazv/libkazv/-/commits/servant)
libkazv is a matrix client sdk built upon [lager](https://github.com/arximboldi/lager)
and the value-oriented design it enables.
# Functionalities
libkazv support the following functionalities:
- Logging in
- Receiving room states
- Receiving room messages
- Receiving and sending account data
- Receiving presence
- Sending messages
- Send room state events
- Create rooms
- Room invites
- Join rooms
- Typing notifications
- Receipts and fully-read markers
- Leaving and forgetting rooms
- Content repository
- Send-to-device messages
- E2EE (send and receive events only and attachments)
- Banning and kicking
- Direct messages
- Redactions
- Room tagging
- Mentions
These functionalities are currently not supported:
- Setting presence
- Device management
- Room history visibility
- Registering
- VoIP
- Searching
- Room previews
These functionalities may be implemented, but in a low priority:
- Push notifications
- Third-party invites
- Guest access
- Server administration
- Event context
- Ignoring users
- Reporting content
- Third party networks
- Server notices
- Moderation policy lists
libkazv is not planning to support these functionalities:
-- Single Sign On
- Spaces
- Stories
# Build and Use
## For Gentoo users
If you are using Gentoo, you can use [tusooa-overlay][tusooa-overlay]
to install libkazv. The dependency `olm` can be installed from
[src_prepare-overlay][src-prep].
[tusooa-overlay]: https://gitlab.com/tusooa/tusooa-overlay
[src-prep]: https://gitlab.com/src_prepare/src_prepare-overlay
## Dependencies
libkazv depends on [lager](https://github.com/arximboldi/lager),
[immer](https://github.com/arximboldi/immer),
[zug](https://github.com/arximboldi/zug),
[boost](https://boost.org),
[nlohmann_json](https://github.com/nlohmann/json),
[olm](https://gitlab.matrix.org/matrix-org/olm),
[libcrypto++](https://cryptopp.com/).
kazvjob also depends on [cpr](https://github.com/whoshuu/cpr).
Tests also depend on [Catch2](https://github.com/catchorg/Catch2).
Examples also depend on [libhttpserver](https://github.com/etr/libhttpserver).
## Process
You can build libkazv through the standard CMake process:
```
mkdir -pv build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/path/to/prefix
make install
```
libkazv offers the following CMake options:
- `libkazv_BUILD_TESTS`: boolean value to specify whether to build tests
- `libkazv_BUILD_EXAMPLES`: boolean value to specify whether to build examples
- `libkazv_OUTPUT_LEVEL`: integral value from 0 to 100 to determine what kinds
of logs are shown. Setting to 100 makes libkazv output the most debug
information.
- `libkazv_INSTALL_HEADERS`: boolean value to determine whether to install
libkazv's headers. This is by default set to OFF when libkazv is built
as a subproject.
libkazv can be incorporated into your project using CMake `FetchContent()`.
It can also be used via `find_package(libkazv)`.
It has a few libraries you can link to:
- `libkazv::kazvall` is the one that contains API call definitions
and client logic. It does not, however, define how the jobs are
fetched.
- `libkazv::kazvjob` is a tiny library that provides async
and network fetching functionalities. There is one class
`CprJobHandler` that implements `JobInterface` in `kazv`.
You can link your program to `kazvjob` or make up another
job handler using what you choose as async and network
libraries. To switch from one job handler to another,
you only need to change one or two lines in your program.
Note that you will need to add `COMPONENTS job` to the arguments
of `find_package()` to use this.
# Tutorials
Tutorial 0: [Getting started with libkazv][tut0]
[tut0]: https://lily-is.land/kazv/libkazv/-/blob/servant/tutorials/tutorial0.md
# APIs
The API documentation is available at <https://kazv.chat/libkazv/api/>.
You can also use `doxygen` at the root directory of the repository
to build docs locally. It will be generated in `doc/`.
You SHOULD use only the following APIs in your program:
1. `Kazv::makeSdk`, `Kazv::makeDefaultEncryptedSdk`, `Kazv::makeDefaultSdkWithCryptoRandom`.
2. Default constructor of `Kazv::SdkModel`.
3. Constructors of `Kazv::CprJobHandler`,
`Kazv::AsioPromiseHandler`.
4. `Kazv::Sdk`, `Kazv::Client`, `Kazv::Room`, but not their constructors.
5. `Kazv::LagerStoreEventEmitter`.
6. The classes that are required to interact with (e.g. return type or argument of)
the classes in 4 and 5 (e.g. `Kazv::Event`).
Anything in the `kazvapi` module (i.e. the namespace `Kazv::Api`) is not meant
to be used directly.
You should not rely on the return type of a function if it is declared as
`auto` (without trailing return type notation). If it returns `auto`, you
should also use `auto` to store that the return value.
If the documented return type is a `using` declaration in that class, you
should either use `auto` or the exact alias (not the aliased type) for that.
For example, in `Client`, a lot of functions return `Client::PromiseT`, so
you should use `auto` or `Client::PromiseT` to refer to the return type.
# Versioning
The following versioning strategies are used:
1. There are two versions in this library:
- Package version, specified by `libkazv_VERSION_STRING` in CMakeLists.txt ,
in the format of `X.Y.Z`. `X`, `Y`, `Z` are called Major, Minor, and Patch
Versions respectively.
- so version, specified by `libkazv_SOVERSION` in CMakeLists.txt .
2. When there is a new release, the versions are determined by the following rules:
1. If there are no changes to any of the public headers (headers containing public
APIs) existing in the last release,
keep Major and Minor Versions as the same, increase Patch Version by 1,
and keep so version as the same.
2. If 2.1 does not hold, and all possible source code invoking only public APIs in
the last release according to the guidelines (e.g. always use `auto` when specified)
that compiles under a certain GCC version will still compile under that GCC version,
keep Major Version
as the same, increase Minor Version by 1, let Patch Version be 0, and increase
so version by 1.
3. Otherwise, increase Major Version by 1, let Minor and Patch Versions be 0, and
increase so version by 1.
A tldr but inaccurate version of the rules: If the new release is binary-compatible
with the last one, bump Patch Version; if the new release is not binary-compatible but
source-compatible with the last one, bump Minor Version and so version,
reset Patch Version; otherwise, bump Major Version and so version, reset Minor and Patch
Versions.
# Acknowledgement
libkazv uses [gtad](https://github.com/KitsuneRal/gtad) to generate the API
definitions it needed. The source of the Matrix API is
<https://github.com/matrix-org/matrix-spec/tree/main/data>.
The gtad configuration files and json/query serializing used in libkazv are
adapted from the ones in [libQuotient](https://github.com/quotient-im/libQuotient).
libQuotient is released under GNU LGPL v2.1. The changes in said files
in libkazv compared to libQuotient's are:
- Get rid of the `avoidCopy` and `moveOnly` markers
- Use data types from `immer` and `std` instead of Qt
- Use `nlohmann::json` instead of Qt's JSON library
# License
Copyright (C) 2020-2024 the Kazv Project <https://kazv.chat>
libkazv is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
libkazv is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with libkazv. If not, see <https://www.gnu.org/licenses/>.
diff --git a/src/client/actions/auth.cpp b/src/client/actions/auth.cpp
index 15d2a44..7755612 100644
--- a/src/client/actions/auth.cpp
+++ b/src/client/actions/auth.cpp
@@ -1,176 +1,191 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2022 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <debug.hpp>
#include <jobinterface.hpp>
#include <eventinterface.hpp>
#include "auth.hpp"
#include "status-utils.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, LoginAction a)
{
m.addJob(LoginJob{a.serverUrl,
"m.login.password"s, // type
UserIdentifier{ "m.id.user"s, json{{"user", a.username}} }, // identifier
a.password,
{}, // token, not used
{}, // device id, not used
a.deviceName.value_or("libkazv")}
.withData(json{
{"serverUrl", a.serverUrl},
}));
return { m, lager::noop };
}
+ ClientResult updateClient(ClientModel m, MLoginTokenLoginAction a)
+ {
+ m.addJob(LoginJob{a.serverUrl,
+ "m.login.token"s, // type
+ std::nullopt, // identifier
+ std::nullopt, // password
+ a.loginToken, // token, not used
+ {}, // device id, not used
+ a.deviceName.value_or("libkazv")}
+ .withData(json{
+ {"serverUrl", a.serverUrl},
+ }));
+ return { m, lager::noop };
+ }
+
ClientResult processResponse(ClientModel m, LoginResponse r)
{
if (! r.success()) {
m.addTrigger(LoginFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), failWithResponse(r) };
}
kzo.client.dbg() << "Job success" << std::endl;
auto jw = r.jsonBody();
auto const &j = jw.get();
// TODO: replace this with r.wellKnown()
std::string serverUrl = j.contains("well_known")
? j.at("well_known").at("m.homeserver").at("base_url").get<std::string>()
: r.dataStr("serverUrl");
// Synapse will return the server url with trailing slash
// and not recognize double slashes in the middle
while (serverUrl.back() == '/') {
serverUrl.pop_back();
}
m.serverUrl = serverUrl;
m.userId = r.userId().value_or(DEFVAL);
m.token = r.accessToken().value_or(DEFVAL);
m.deviceId = r.deviceId().value_or(DEFVAL);
m.loggedIn = true;
m.addTrigger(LoginSuccessful{});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, TokenLoginAction a)
{
m.serverUrl = a.serverUrl;
m.userId = a.username;
m.token = a.token;
m.deviceId = a.deviceId;
m.loggedIn = true;
m.addTrigger(LoginSuccessful{});
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, LogoutAction)
{
// Note: this only performs a soft-logout.
m.serverUrl = "";
m.userId = "";
m.token = "";
m.deviceId = "";
m.loggedIn = false;
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, HardLogoutAction)
{
m.addJob(LogoutJob{m.serverUrl, m.token});
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, LogoutResponse r)
{
if (r.success()) {
m.token = "";
m.loggedIn = false;
return { std::move(m), lager::noop };
} else {
return { std::move(m), failWithResponse(std::move(r)) };
}
}
ClientResult updateClient(ClientModel m, GetWellknownAction a)
{
auto pos = a.userId.find(':');
if (pos == std::string::npos || pos == a.userId.size() - 1) {
return { std::move(m), simpleFail };
}
auto serverUrl = "https://" + a.userId.substr(pos + 1);
m.addJob(GetWellknownJob{serverUrl}
.withData(json{{"serverUrl", serverUrl}}));
return { m, lager::noop };
}
ClientResult processResponse(ClientModel m, GetWellknownResponse r)
{
auto success = r.success() || r.statusCode == 404;
auto error = std::string();
std::string serverUrl = r.dataStr("serverUrl");
if (r.success()) {
auto data = r.data();
if (data.homeserver.baseUrl.empty()) {
success = false;
error = "FAIL_PROMPT";
} else {
serverUrl = data.homeserver.baseUrl;
}
} else {
error = "FAIL_PROMPT";
}
return {
std::move(m),
[success, serverUrl, error, r](auto &&) {
auto data = json{
{"homeserverUrl", serverUrl},
{"error", error},
{"errorCode", r.errorCode()},
};
return EffectStatus(success, data);
}
};
}
ClientResult updateClient(ClientModel m, GetVersionsAction a)
{
m.addJob(GetVersionsJob{a.serverUrl});
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, GetVersionsResponse r)
{
m.versions = r.versions();
return {
std::move(m),
[r](auto &&ctx) {
if (r.success()) {
return EffectStatus(r.success(), json{
{"versions", r.versions()},
});
} else {
return failWithResponse(r)(std::forward<decltype(ctx)>(ctx));
}
}
};
}
}
diff --git a/src/client/actions/auth.hpp b/src/client/actions/auth.hpp
index 267989b..35a542f 100644
--- a/src/client/actions/auth.hpp
+++ b/src/client/actions/auth.hpp
@@ -1,32 +1,33 @@
/*
* This file is part of libkazv.
- * SPDX-FileCopyrightText: 2020-2022 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <csapi/login.hpp>
#include <csapi/wellknown.hpp>
#include <csapi/versions.hpp>
#include <csapi/logout.hpp>
#include "client-model.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, LoginAction a);
+ ClientResult updateClient(ClientModel m, MLoginTokenLoginAction a);
ClientResult updateClient(ClientModel m, TokenLoginAction a);
ClientResult processResponse(ClientModel m, LoginResponse r);
ClientResult updateClient(ClientModel m, LogoutAction a);
ClientResult updateClient(ClientModel m, HardLogoutAction a);
ClientResult processResponse(ClientModel m, LogoutResponse r);
ClientResult updateClient(ClientModel m, GetWellknownAction a);
ClientResult processResponse(ClientModel m, GetWellknownResponse r);
ClientResult updateClient(ClientModel m, GetVersionsAction a);
ClientResult processResponse(ClientModel m, GetVersionsResponse r);
}
diff --git a/src/client/client-model.hpp b/src/client/client-model.hpp
index d0d49a2..ebea6d6 100644
--- a/src/client/client-model.hpp
+++ b/src/client/client-model.hpp
@@ -1,675 +1,685 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <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<KazvTrigger> nextTriggers;
EventList toDevice;
std::optional<immer::box<Crypto>> crypto;
bool identityKeysUploaded{false};
DeviceListTracker deviceLists;
DeviceTrustLevel trustLevelNeededToSendKeys{DeviceTrustLevel::Unseen};
immer::array<std::string /* version */> versions;
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
/// @return A map from user id to device id to encrypted event for that device
immer::map<std::string, immer::map<std::string, Event>> olmEncryptSplit(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;
/// @return the mapping from room id to user id of direct rooms
auto directRoomMap() const -> immer::map<std::string, std::string>;
auto roomIdsUnderTag(std::string tagId) const -> immer::map<std::string, double>;
auto roomIdsByTagId() const -> immer::map<std::string, immer::map<std::string, double>>;
/// Get the const reference of crypto of this client.
///
/// `crypto.has_value()` must be true.
const Crypto &constCrypto() const;
/// Do func with crypto, returning its return value.
///
/// `crypto.has_value()` must be true.
template<class Func>
auto withCrypto(Func &&func) -> std::decay_t<std::invoke_result_t<Func &&, Crypto &>>
{
using ResT = std::decay_t<std::invoke_result_t<Func &&, Crypto &>>;
if constexpr (std::is_same_v<ResT, void>) {
crypto = std::move(crypto).value()
.update([f=std::forward<Func>(func)](Crypto c) mutable {
std::forward<Func>(f)(c);
return c;
});
} else {
std::optional<ResT> res;
crypto = std::move(crypto).value()
.update([f=std::forward<Func>(func), &res](Crypto c) mutable {
res = std::forward<Func>(f)(c);
return c;
});
return std::move(res).value();
}
}
// 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(KazvTrigger t) {
addTriggers({t});
}
inline void addTriggers(immer::flex_vector<KazvTrigger> c) {
nextTriggers = std::move(nextTriggers) + c;
}
inline auto popAllTriggers() {
auto triggers = std::move(nextTriggers);
nextTriggers = DEFVAL;
return triggers;
}
void maybeAddSaveEventsTrigger(const ClientModel &old);
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;
};
+ /**
+ * Login using the m.token.login flow.
+ */
+ struct MLoginTokenLoginAction
+ {
+ std::string serverUrl;
+ std::string loginToken;
+ std::optional<std::string> deviceName;
+ };
+
struct LogoutAction {};
struct HardLogoutAction {};
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};
};
/**
* Updates the status of an local echo.
*
* After dispatching this action, the local echo's status will be
* set to the one described in the action.
*/
struct UpdateLocalEchoStatusAction
{
/// The room id.
std::string roomId;
/// The chosen txnId for this event.
std::string txnId;
/// The updated status of this local echo.
LocalEchoDesc::Status status;
};
struct RedactEventAction
{
std::string roomId;
std::string eventId;
std::optional<std::string> reason;
};
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 SetAccountDataPerRoomAction
{
std::string roomId;
Event accountDataEvent;
};
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 SetAccountDataAction
{
Event accountDataEvent;
};
struct SendToDeviceMessageAction
{
Event event;
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
std::optional<std::string> txnId{std::nullopt};
};
/**
* Send multiple to device messages.
*
* Due to limitations of the spec, the type of the Events must be the same.
*/
struct SendMultipleToDeviceMessagesAction
{
/// A map from user id to device id to the event.
immer::map<std::string, immer::map<std::string, Event>> userToDeviceToEventMap;
/// An optional transaction id. Will be generated if not provided.
std::optional<std::string> txnId{std::nullopt};
};
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;
};
struct SetDeviceTrustLevelAction
{
std::string userId;
std::string deviceId;
DeviceTrustLevel trustLevel;
};
struct SetTrustLevelNeededToSendKeysAction
{
DeviceTrustLevel trustLevel;
};
/// Encrypt room key as olm and add it to the room's
/// pending keyshare slots.
/// This is to ensure atomicity and that we do not lose an olm-encrypted event.
struct PrepareForSharingRoomKeyAction
{
using UserIdToDeviceIdMap = immer::map<std::string, immer::flex_vector<std::string>>;
static std::size_t randomSize(UserIdToDeviceIdMap devices);
/// The room to share the key event in.
std::string roomId;
/// Devices to encrypt for.
UserIdToDeviceIdMap devices;
/// The key event to encrypt.
Event e;
/// The random data for the encryption. Must be of at least
/// size `randomSize(devices)`.
RandomData random;
};
/**
* Import keys from key backup file.
*
* On success, the reducer returns data with `imported` property
* being the number of keys imported. On failure, it returns data with
* `errorCode` and `error` properties set to the error in the process.
*/
struct ImportFromKeyBackupFileAction
{
/// The content of the key backup file.
std::string fileContent;
/// The password.
std::string password;
};
struct GetUserProfileAction
{
std::string userId;
};
struct SetAvatarUrlAction
{
std::optional<std::string> avatarUrl;
};
struct SetDisplayNameAction
{
std::optional<std::string> displayName;
};
/// Load events from the storage into the model
struct LoadEventsFromStorageAction
{
/// Map from room id to a list of
/// loaded events that should be put into the timeline. From oldest to latest.
immer::map<std::string, EventList> timelineEvents;
/// Map from room id to a list of
/// related events that should not be put into the timeline. From oldest to latest.
/// There might be events in the storage that is needed to display
/// existing events or room state (e.g. pinned events), but
/// the storage may not know its place in the timeline.
immer::map<std::string, EventList> relatedEvents;
};
/// Remove events from the model, keeping only the latest `maxToKeep` events.
/// For each room, this takes O(maxToKeep * log(maxToKeep)) time.
struct PurgeRoomTimelineAction
{
/// A map from roomId to maxToKeep
immer::map<std::string, std::size_t> roomIdToMaxToKeepMap;
};
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;
// version <= 1 uses std::optional<Crypto>
// while version >= 2 uses std::optional<immer::box<Crypto>>
if (version >= 2) {
ar & m.crypto;
} else {
if constexpr (typename Archive::is_loading()) {
std::optional<Crypto> crypto;
ar >> crypto;
if (crypto.has_value()) {
m.crypto = immer::box<Crypto>(std::move(crypto).value());
}
}
// otherwise is_saving, which will always use the latest version
// this is unreachable
}
ar
& m.identityKeysUploaded
& m.deviceLists
;
if (version >= 1) { ar & m.trustLevelNeededToSendKeys; }
}
}
BOOST_CLASS_VERSION(Kazv::ClientModel, 2)
diff --git a/src/client/client.cpp b/src/client/client.cpp
index 9a534ea..0a8beaa 100644
--- a/src/client/client.cpp
+++ b/src/client/client.cpp
@@ -1,524 +1,544 @@
/*
* 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 <lager/constant.hpp>
#include "client.hpp"
#include "client-model.hpp"
#include "alias.hpp"
namespace Kazv
{
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) const
-> PromiseT
{
auto p1 = m_ctx.dispatch(LoginAction{
homeserver, username, password, deviceName});
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::mLoginTokenLogin(
+ std::string homeserver,
+ std::string loginToken,
+ std::optional<std::string> deviceName
+ ) const -> PromiseT
+ {
+ auto p1 = m_ctx.dispatch(MLoginTokenLoginAction{
+ homeserver, loginToken, deviceName});
+ 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
{
auto p1 = m_ctx.dispatch(TokenLoginAction{
homeserver, username, token, deviceId});
p1
.then([that=toEventLoop()](auto stat) {
if (! stat.success()) {
return;
}
that.startSyncing();
});
return p1;
}
auto Client::shouldSync() const -> lager::reader<bool> {
return this->clientCursor()[&ClientModel::shouldSync];
}
auto Client::logout() const
-> PromiseT
{
return stopSyncing().then([ctx=m_ctx] (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 = 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);
}
});
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::syncForever(std::optional<int> retryTime) const -> void
{
KAZV_VERIFY_THREAD_ID();
// assert (m_deps);
using namespace CursorOp;
bool isInitialSync = ! (+clientCursor()[&ClientModel::syncToken]).has_value();
bool shouldSync = +clientCursor()[&ClientModel::shouldSync];
if (! shouldSync) {
return;
}
//
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);
});
m_ctx.promiseInterface()
.all(std::vector<PromiseT>{uploadOneTimeKeysRes, queryKeysRes})
.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});
}
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/client/client.hpp b/src/client/client.hpp
index 3aeeb3f..89d0c78 100644
--- a/src/client/client.hpp
+++ b/src/client/client.hpp
@@ -1,653 +1,674 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <lager/reader.hpp>
#include <immer/box.hpp>
#include <immer/map.hpp>
#include <immer/flex_vector.hpp>
#include <immer/flex_vector_transient.hpp>
#include "sdk-model.hpp"
#include "client/client-model.hpp"
#include "client/actions/content.hpp"
#include "sdk-model-cursor-tag.hpp"
#include "get-content-job-v1.hpp"
#include "room/room.hpp"
#include "notification-handler.hpp"
namespace Kazv
{
/**
* Represent a Matrix client.
*
* If the Client is constructed from a cursor originated from
* a root whose event loop is on thread A, then we say that
* the Client belongs to thread A. If the Client is not constructed
* from a cursor, then we say that the Client belongs to the thread
* where the event loop of the context runs.
*
* All methods in this class that take a cursor only take a cursor
* on the same thread as the Client. All methods in this class that
* return a cursor will return a cursor on the same thread as the Client.
*
* All methods in this class must be run on the same thread as the
* the Client. If the Client is not constructed from a cursor,
* copy-constructing another Client from this is safe from any thread.
* If the Client is constructed from a cursor, copy-constructing another
* Client is safe only from the same thread as this Client.
*
* ## Error handling
*
* A lot of functions in Client and Room are asynchronous actions.
* These actions return the result via a Promise.
* If an API request has failed, the Promise p will satisfy the following:
* - `!p.success()`
* - `p.dataStr("error")` will contain the error message from the response.
* - `p.dataStr("errorCode")` will contain the matrix error code, if available,
* or the HTTP status code otherwise.
*
* What information is resolved if the API request has succeeded is defined
* by individual functions.
*/
class Client
{
public:
using ActionT = ClientAction;
using DepsT = lager::deps<JobInterface &, EventInterface &, SdkModelCursorKey, RandomInterface &
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, EventLoopThreadIdKeeper &
#endif
>;
using ContextT = Context<ActionT>;
using ContextWithDepsT = Context<ActionT, DepsT>;
using PromiseT = SingleTypePromise<DefaultRetType>;
struct InEventLoopTag {};
/**
* Constructor.
*
* Construct the client. Without Deps support.
*
* @warning You should not use this directly. Use
* Sdk::client() instead.
*/
Client(lager::reader<SdkModel> sdk,
ContextT ctx, std::nullopt_t);
/**
* Constructor.
*
* Construct the client, with Deps support.
*
* This enables startSyncing() to work properly.
*
* @warning You should not use this directly. Use
* Sdk::client() instead.
*/
Client(lager::reader<SdkModel> sdk,
ContextWithDepsT ctx);
/**
* Constructor.
*
* Construct the client, with Deps support.
*
* The constructed Client belongs to the thread of event loop.
*
* @warning You should not use this directly. Use
* Sdk::client() instead.
*/
Client(InEventLoopTag,
ContextWithDepsT ctx);
/**
* Constructor.
*
* Construct the client, with Deps support.
*
* The constructed Client belongs to the thread of event loop.
*
* @warning You should not use this directly. Use
* Sdk::client() instead.
*/
Client(InEventLoopTag, ContextT ctx, DepsT deps);
/**
* Create a Client that is not constructed from a cursor.
*
* The returned Client belongs to the thread of event loop.
*
* This function is thread-safe if every thread calls it
* using different objects.
*
* @return A Client not constructed from a cursor.
*/
Client toEventLoop() const;
/* lager::reader<immer::map<std::string, Room>> */
inline auto rooms() const {
return clientCursor()
[&ClientModel::roomList]
[&RoomListModel::rooms];
}
/* lager::reader<RangeT<std::string>> */
inline auto roomIds() const {
return rooms().xform(
zug::map([](auto m) {
return intoImmer(
immer::flex_vector<std::string>{},
zug::map([](auto val) { return val.first; }),
m);
}));
}
auto roomIdsUnderTag(std::string tagId) const -> lager::reader<immer::map<std::string, double>>;
/**
* Get the room ids under all tags.
*
* @return A lager::reader containing the map from tag id to a map from room id to order.
* Rooms without a tag will be under the tag id of the empty string.
*/
auto roomIdsByTagId() const -> lager::reader<immer::map<std::string, immer::map<std::string, double>>>;
KAZV_WRAP_ATTR(ClientModel, clientCursor(), serverUrl)
KAZV_WRAP_ATTR(ClientModel, clientCursor(), loggedIn)
KAZV_WRAP_ATTR(ClientModel, clientCursor(), userId)
KAZV_WRAP_ATTR(ClientModel, clientCursor(), token)
KAZV_WRAP_ATTR(ClientModel, clientCursor(), deviceId)
KAZV_WRAP_ATTR(ClientModel, clientCursor(), toDevice)
/**
* Get the room with @c id .
*
* This is equivalent to `roomByCursor(lager::make_constant(id))`.
*
* @param id The room id.
* @return A Room representing the room with `id`.
*/
Room room(std::string id) const;
/**
* Get the room with `id`.
*
* The Room returned will change as the content in `id` changes.
*
* For example, you can have the Room that is always the first
* alphabetically in all rooms by:
*
* \code{.cpp}
* auto someProcessing =
* zug::map([=](auto ids) {
* std::sort(ids.begin(), ids.end(), [=](auto id1, auto id2) {
* using namespace Kazv::CursorOp;
* return (+client.room(id1).name()) < (+client.room(id2).name());
* });
* return ids;
* });
* auto room =
* client.roomByCursor(
* client.roomIds().xform(someProcessing)[0]);
* \endcode
*
* @param id A lager::reader<std::string> containing the room id.
* @return A Room representing the room with `id`.
*/
Room roomByCursor(lager::reader<std::string> id) const;
/**
* Login using the password.
*
* This will create a new session on the homeserver.
*
* If the returned Promise resolves successfully, this will
* call `startSyncing()`.
*
* @param homeserver The base url of the homeserver. E.g. `https://tusooa.xyz`.
* @param username The username. This can be the full user id or
* just the local part. E.g. `tusooa`, `@tusooa:tusooa.xyz`.
* @param password The password.
* @param deviceName Optionally, a custom device name. If empty, `libkazv`
* will be used.
* @return A Promise that resolves when logging in successfully, or
* when there is an error.
*/
PromiseT passwordLogin(std::string homeserver, std::string username,
std::string password, std::string deviceName) const;
/**
* Login using `token` and `deviceId`.
*
* This will not make a request. Library users should make sure
* the information is correct and the token and the device id are valid.
*
* If the returned Promise resolves successfully, this will
* call `startSyncing()`.
*
* @param homeserver The base url of the homeserver. E.g. `https://tusooa.xyz`.
* @param username The full user id. E.g. `@tusooa:tusooa.xyz`.
* @param token The access token.
* @param deviceId The device id that is paired with `token`.
* @return A Promise that resolves when the account information is filled in.
*/
PromiseT tokenLogin(std::string homeserver, std::string username,
std::string token, std::string deviceId) const;
+ /**
+ * Login using a login token.
+ *
+ * This will create a new session on the homeserver.
+ *
+ * If the returned Promise resolves successfully, this will
+ * call `startSyncing()`.
+ *
+ * @param homeserver The base url of the homeserver. E.g. `https://tusooa.xyz`.
+ * @param loginToken The login token.
+ * @param deviceName Optionally, a custom device name. If empty, `libkazv`
+ * will be used.
+ * @return A Promise that resolves when logging in successfully, or
+ * when there is an error.
+ */
+ PromiseT mLoginTokenLogin(
+ std::string homeserver,
+ std::string loginToken,
+ std::optional<std::string> deviceName
+ ) const;
+
/**
* Get the `shouldSync` field of current ClientModel.
*
* @return A lager::reader of bool of the `shouldSync` field of the ClientModel
*/
auto shouldSync() const -> lager::reader<bool>;
/**
* Stop syncing and then logout current session.
*
* Meanwhile, clear the current token and set loggedIn to false.
*
* @return A promise that resolves when the syncing is stopped.
*/
PromiseT logout() const;
/**
* Automatically discover the homeserver for `userId`.
*
* If the operation succeeds, `r.dataStr("homeserverUrl")` will contain
* the url suitable to pass to `tokenLogin()` and `passwordLogin()`.
*
* If there is no well-known file (i.e. server responds with 404),
* `r.dataStr("homeserverUrl")` will contain the domain part of the user
* id (`https://example.org` for `@foo:example.org`).
*
* @param userId The full user id. E.g. `@foo:example.org`.
* @return A Promise that resolves when the auto-discovery finishes.
*/
PromiseT autoDiscover(std::string userId) const;
/**
* Create a room.
*
* @param v The visibility of the room.
* @param name The name of the room.
* @param alias The alias of the room.
* @param invite User ids to invite to this room.
* @param isDirect Whether this room is a direct chat.
* @param allowFederate Whether to allow users from other homeservers
* to join this room.
* @param topic The topic of the room.
* @param powerLevelContentOverride The content of the m.room.power_levels
* state event to override the default.
* @param preset The preset to create the room with.
* @return A Promise that resolves when the room is created,
* or when there is an error.
*/
PromiseT createRoom(
RoomVisibility v,
std::optional<std::string> name = {},
std::optional<std::string> alias = {},
immer::array<std::string> invite = {},
std::optional<bool> isDirect = {},
bool allowFederate = true,
std::optional<std::string> topic = {},
JsonWrap powerLevelContentOverride = json::object(),
std::optional<CreateRoomPreset> preset = std::nullopt,
immer::array<Event> initialState = immer::array<Event>()
) const;
/**
* Join a room by its id.
*
* @param roomId The id of the room to join.
* @return A Promise that resolves when the room is joined,
* or when there is an error.
*/
PromiseT joinRoomById(std::string roomId) const;
/**
* Join a room by its id or alias.
*
* @param roomId The id *or alias* of the room to join.
* @param serverName A list of servers to use when joining the room.
* This corresponds to the `via` parameter in a matrix.to url.
* @return A Promise that resolves when the room is joined,
* or when there is an error.
*/
PromiseT joinRoom(std::string roomId, immer::array<std::string> serverName) const;
/**
* Upload content to the content repository.
*
* @param content The content to upload.
* @param uploadId
* @param filename The name of the file.
* @param contentType The content type of the file.
* @return A Promise that resolves when the upload is successful,
* or when there is an error. If it successfully resolves to `r`,
* `r.dataStr("mxcUri")` will be the MXC URI of the uploaded
* content.
*/
PromiseT uploadContent(immer::box<Bytes> content,
std::string uploadId,
std::optional<std::string> filename = std::nullopt,
std::optional<std::string> contentType = std::nullopt) const;
/**
* Upload content to the content repository.
*
* @param file The file to upload.
* @return A Promise that resolves when the upload is successful,
* or when there is an error. If it successfully resolves to `r`,
* `r.dataStr("mxcUri")` will be the MXC URI of the uploaded
* content.
*/
PromiseT uploadContent(FileDesc file) const;
/**
* Convert a MXC URI to an HTTP(s) URI.
*
* The converted URI will be using the homeserver of
* this Client.
*
* @param mxcUri The MXC URI to convert.
* @return The HTTP(s) URI that has the content indicated
* by `mxcUri`.
*/
inline std::string mxcUriToHttp(std::string mxcUri) const {
using namespace CursorOp;
auto [serverName, mediaId] = mxcUriToMediaDesc(mxcUri);
return (+clientCursor())
.template job<GetContentJob>()
.make(serverName, mediaId).url();
}
/**
* Convert a MXC URI to an HTTP(s) URI that needs Authorization.
*
* The converted URI will be using the homeserver of
* this Client.
*
* @param mxcUri The MXC URI to convert.
* @return The HTTP(s) URI that has the content indicated
* by `mxcUri`.
*/
std::string mxcUriToHttpV1(std::string mxcUri) const;
/**
* Download content from the content repository
*
* After the returned Promise resolves successfully,
* if @c downloadTo is provided, the content will be available
* in that file; if it is not provided, `r.dataStr("content")`
* will contain the content of the downloaded file.
*
* @param mxcUri The MXC URI of the content.
* @param downloadTo The file to write the content to. Must not be
* an in-memory file.
* @return A Promise that is resolved after the content
* is downloaded, or when there is an error.
*/
PromiseT downloadContent(std::string mxcUri,
std::optional<FileDesc> downloadTo = std::nullopt) const;
/**
* Download a thumbnail from the content repository
*
* After the returned Promise resolves successfully,
* if @c downloadTo is provided, the content will be available
* in that file; if it is not provided, `r.dataStr("content")`
* will contain the content of the downloaded file.
*
* @param mxcUri The MXC URI of the content.
* @param width,height The dimension wanted for the thumbnail
* @param method The method to generate the thumbnail. Either `Crop`
* or `Scale`.
* @param downloadTo The file to write the content to. Must not be
* an in-memory file.
* @return A Promise that is resolved after the thumbnail
* is downloaded, or when there is an error.
*/
PromiseT downloadThumbnail(std::string mxcUri,
int width,
int height,
std::optional<ThumbnailResizingMethod> method = std::nullopt,
std::optional<FileDesc> downloadTo = std::nullopt) const;
/**
* Fetch the profile of a user.
*
* @param userId The id of the user to fetch.
* @return A Promise that resolves when the fetch is completed.
* If successful, `r.dataStr("avatarUrl")` will contain the
* avatar url of that user, and `r.dataStr("displayName")` will
* contain the display name of that user.
*/
PromiseT getProfile(std::string userId) const;
/**
* Change the avatar url of the current user.
*
* @param avatarUrl The url of the new avatar. Should be an MXC URI.
* If it is std::nullopt, remove the user avatar.
* @return A Promise that resolves when the request is completed.
*/
PromiseT setAvatarUrl(std::optional<std::string> avatarUrl) const;
/**
* Change the display name of the current user.
*
* @param displayName The new display name. If it is std::nullopt,
* remove the user avatar.
* @return A Promise that resolves when the request is completed.
*/
PromiseT setDisplayName(std::optional<std::string> displayName) const;
// lager::reader<bool>
inline auto syncing() const {
return clientCursor()[&ClientModel::syncing];
}
/**
* Start syncing if the Client is not syncing.
*
* Syncing will continue indefinitely, if the preparation of
* the sync (posting filters and uploading identity keys,
* if needed) is successful, or until stopSyncing() is called.
*
* @return A Promise that resolves when the Client is syncing
* (more exactly, when syncing() contains true), or when there
* is an error in the preparation of the sync.
*/
PromiseT startSyncing() const;
/**
* Stop the indefinite syncing.
*
* After this, no more syncing actions will be dispatched.
*
* @return A Promise that resolves when syncing is stopped.
*/
PromiseT stopSyncing() const;
/**
* Get the info of all devices of user `userId` that supports encryption.
*
* @param userId The id of the user to get the devices of.
*
* @return a lager::reader of a RangeT of DeviceKeyInfo representing the devices of that user.
*/
auto devicesOfUser(std::string userId) const -> lager::reader<immer::flex_vector<DeviceKeyInfo>>;
/**
* Set the trust level of a device.
*
* @param userId The id of the user to whom the device belongs.
* @param deviceId The id of the device.
*
* @return a Promise that resolves when the setting is changed.
*/
PromiseT setDeviceTrustLevel(std::string userId, std::string deviceId, DeviceTrustLevel trustLevel) const;
/**
* Get the trust level needed to send keys to a device.
*
* @return a lager::reader of the trust level threshold.
*/
auto trustLevelNeededToSendKeys() const -> lager::reader<DeviceTrustLevel>;
/**
* Set the trust level needed to send keys to a device.
*
* @param trustLevel The trust level threshold.
*
* @return a Promise that resolves when the setting is changed.
*/
PromiseT setTrustLevelNeededToSendKeys(DeviceTrustLevel trustLevel) const;
/**
* Get the map from direct messaging room ids to user ids.
*
* @return a lager::reader of such mapping.
*/
auto directRoomMap() const -> lager::reader<immer::map<std::string, std::string>>;
/**
* Get the account data that is not associated with any room.
*
* @return A lager::reader of a map from the type to the account data event.
*/
auto accountData() const -> lager::reader<immer::map<std::string, Event>>;
/**
* Set the account data that is not associated with any room.
*
* @return A Promise that resolves when the account data
* has been set, or when there is an error.
*/
PromiseT setAccountData(Event accountDataEvent) const;
/**
* Get a notification handler that works on this Client.
*
* @return A notification handler that works on this Client.
*/
NotificationHandler notificationHandler() const;
/**
* Serialize the model to a Boost.Serialization archive.
*
* @param ar A Boost.Serialization output archive.
*
* This function can be used to save the model. For loading,
* you should use the makeSdk function. For example:
*
* ```c++
* client.serializeTo(outputAr);
*
* SdkModel m;
* inputAr >> m;
* auto newSdk = makeSdk(m, ...);
* ```
*/
template<class Archive>
void serializeTo(Archive &ar) const {
ar << sdkCursor().get();
}
/**
* Get all supported versions.
*
* @param homeserver The base url of the homeserver. E.g. `https://tusooa.xyz`.
* @return A Promise that resolves when the versions has been set,
* or when there is an error.
*/
PromiseT getVersions(std::string homeserver) const;
/**
* Get all supported versions.
*
* @return A lager::reader of a array contains all supported versions.
*
* See https://spec.matrix.org/v1.14/#specification-versions
*/
auto supportVersions() const -> lager::reader<immer::array<std::string>>;
/**
* Mark a room as a direct chat by send the m.direct account data.
*
* @param userId The user id that direct to.
* @param roomId The direct chat room id.
* @return A Promise that resolves when the account data
* has been set, or when there is an error.
*/
PromiseT addDirectRoom(std::string userId, std::string roomId) const;
/**
* Get a GetRoomIdByAliasJob.
* Use Kazv::parseGetRoomIdByAliasResponse to parse its response.
*
* @param roomAlias The room alias.
* @return A GetRoomIdByAliasJob.
*/
BaseJob getRoomIdByAliasJob(std::string roomAlias) const;
/**
* Purge events in room, keeping the latest `numToKeep` events.
*
* The events are removed from the lager store. The timeline will
* contain at most `numToKeep` events, but the `messages` property
* may contain more in order to maintain the room invariants.
* @sa RoomModel
*
* @param roomIdToMaxToKeepMap A map from "room id" to "max number of timeline events to keep."
*/
PromiseT purgeRoomEvents(immer::map<std::string, std::size_t> roomIdToMaxToKeepMap) const;
/**
* Load events from storage into the model.
*
* @param timelineEvents Map from room id to a list of message events that
* should be put into the timeline.
* @param relatedEvents Map from room id to a list of message events that should
* not be put into the timeline (for example, because the storage does not
* know or care where it should go in the timeline).
* @return A Promise that resolves when the events are loaded into the store.
*/
PromiseT loadEventsFromStorage(immer::map<std::string, EventList> timelineEvents, immer::map<std::string, EventList> relatedEvents) const;
/**
* Import keys from a key backup file.
*
* @param fileContent The raw content of the file.
* @param password The password to decrypt the file.
* @return A Promise that resolves when the keys are imported or when there is an error. Assume the Promise resolves to `r`, if it is successful, `r.dataJson("imported")` contains the number of keys imported. Otherwise, `r` contains the standard error structure.
*/
PromiseT importFromKeyBackupFile(std::string fileContent, std::string password) const;
private:
void syncForever(std::optional<int> retryTime = std::nullopt) const;
const lager::reader<SdkModel> &sdkCursor() const;
lager::reader<ClientModel> clientCursor() const;
std::optional<lager::reader<SdkModel>> m_sdk;
std::optional<lager::reader<ClientModel>> m_client;
ContextT m_ctx;
std::optional<DepsT> m_deps;
KAZV_DECLARE_THREAD_ID();
KAZV_DECLARE_EVENT_LOOP_THREAD_ID_KEEPER(m_deps.has_value() ? &lager::get<EventLoopThreadIdKeeper &>(m_deps.value()) : 0);
};
}
diff --git a/src/client/clientfwd.hpp b/src/client/clientfwd.hpp
index 064c16a..761da7f 100644
--- a/src/client/clientfwd.hpp
+++ b/src/client/clientfwd.hpp
@@ -1,157 +1,159 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <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 MLoginTokenLoginAction;
struct LogoutAction;
struct HardLogoutAction;
struct GetWellknownAction;
struct GetVersionsAction;
struct SyncAction;
struct SetShouldSyncAction;
struct PostInitialFiltersAction;
struct SetAccountDataAction;
struct PaginateTimelineAction;
struct SendMessageAction;
struct SendStateEventAction;
struct SaveLocalEchoAction;
struct UpdateLocalEchoStatusAction;
struct RedactEventAction;
struct CreateRoomAction;
struct GetRoomStatesAction;
struct GetStateEventAction;
struct InviteToRoomAction;
struct JoinRoomByIdAction;
struct JoinRoomAction;
struct LeaveRoomAction;
struct ForgetRoomAction;
struct KickAction;
struct BanAction;
struct UnbanAction;
struct SetAccountDataPerRoomAction;
struct ProcessResponseAction;
struct SetTypingAction;
struct PostReceiptAction;
struct SetReadMarkerAction;
struct UploadContentAction;
struct DownloadContentAction;
struct DownloadThumbnailAction;
struct SendToDeviceMessageAction;
struct SendMultipleToDeviceMessagesAction;
struct UploadIdentityKeysAction;
struct GenerateAndUploadOneTimeKeysAction;
struct QueryKeysAction;
struct ClaimKeysAction;
struct EncryptMegOlmEventAction;
struct SetDeviceTrustLevelAction;
struct SetTrustLevelNeededToSendKeysAction;
struct PrepareForSharingRoomKeyAction;
struct ImportFromKeyBackupFileAction;
struct GetUserProfileAction;
struct SetAvatarUrlAction;
struct SetDisplayNameAction;
struct ResubmitJobAction;
struct LoadEventsFromStorageAction;
struct PurgeRoomTimelineAction;
struct ClientModel;
using ClientAction = std::variant<
RoomListAction,
LoginAction,
TokenLoginAction,
+ MLoginTokenLoginAction,
LogoutAction,
HardLogoutAction,
GetWellknownAction,
GetVersionsAction,
SyncAction,
SetShouldSyncAction,
PostInitialFiltersAction,
SetAccountDataAction,
PaginateTimelineAction,
SendMessageAction,
SendStateEventAction,
SaveLocalEchoAction,
UpdateLocalEchoStatusAction,
RedactEventAction,
CreateRoomAction,
GetRoomStatesAction,
GetStateEventAction,
InviteToRoomAction,
JoinRoomByIdAction,
JoinRoomAction,
LeaveRoomAction,
ForgetRoomAction,
KickAction,
BanAction,
UnbanAction,
SetAccountDataPerRoomAction,
ProcessResponseAction,
SetTypingAction,
PostReceiptAction,
SetReadMarkerAction,
UploadContentAction,
DownloadContentAction,
DownloadThumbnailAction,
SendToDeviceMessageAction,
SendMultipleToDeviceMessagesAction,
UploadIdentityKeysAction,
GenerateAndUploadOneTimeKeysAction,
QueryKeysAction,
ClaimKeysAction,
EncryptMegOlmEventAction,
SetDeviceTrustLevelAction,
SetTrustLevelNeededToSendKeysAction,
PrepareForSharingRoomKeyAction,
ImportFromKeyBackupFileAction,
GetUserProfileAction,
SetAvatarUrlAction,
SetDisplayNameAction,
ResubmitJobAction,
LoadEventsFromStorageAction,
PurgeRoomTimelineAction
>;
using ClientEffect = Effect<ClientAction, lager::deps<>>;
using ClientResult = std::pair<ClientModel, ClientEffect>;
}
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 5222c53..93f9f20 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -1,128 +1,129 @@
include(CTest)
set(KAZVTEST_RESPATH ${CMAKE_CURRENT_SOURCE_DIR}/resources)
configure_file(kazvtest-respath.hpp.in kazvtest-respath.hpp)
function(libkazv_add_tests)
set(options "")
set(oneValueArgs "")
set(multiValueArgs EXTRA_LINK_LIBRARIES EXTRA_INCLUDE_DIRECTORIES)
cmake_parse_arguments(PARSE_ARGV 0 libkazv_add_tests "${options}" "${oneValueArgs}" "${multiValueArgs}")
foreach(test_source ${libkazv_add_tests_UNPARSED_ARGUMENTS})
string(REGEX REPLACE "\\.cpp$" "" test_executable "${test_source}")
string(REGEX REPLACE "/|\\\\" "--" test_executable "${test_executable}")
message(STATUS "Test ${test_executable} added")
add_executable("${test_executable}" "${test_source}")
target_link_libraries("${test_executable}"
PRIVATE Catch2::Catch2WithMain
Threads::Threads
${libkazv_add_tests_EXTRA_LINK_LIBRARIES}
)
target_include_directories(
"${test_executable}"
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
${libkazv_add_tests_EXTRA_INCLUDE_DIRECTORIES}
)
target_compile_definitions("${test_executable}" PRIVATE CATCH_CONFIG_ENABLE_ALL_STRINGMAKERS)
add_test(NAME "${test_executable}" COMMAND "${test_executable}" "--allow-running-no-tests" "~[needs-internet]")
endforeach()
endfunction()
libkazv_add_tests(
event-test.cpp
cursorutiltest.cpp
base/serialization-test.cpp
base/types-test.cpp
base/immer-utils-test.cpp
base/json-utils-test.cpp
EXTRA_LINK_LIBRARIES kazvbase
)
add_library(client-test-lib SHARED client/client-test-util.cpp)
target_link_libraries(client-test-lib PUBLIC kazvjob kazvclient)
libkazv_add_tests(
client/discovery-test.cpp
client/sync-test.cpp
client/content-test.cpp
client/paginate-test.cpp
client/storage-actions-test.cpp
client/util-test.cpp
client/serialization-test.cpp
client/encrypted-file-test.cpp
client/sdk-test.cpp
client/thread-safety-test.cpp
client/room-test.cpp
client/random-generator-test.cpp
client/profile-test.cpp
client/kick-test.cpp
client/ban-test.cpp
client/join-test.cpp
client/keys-test.cpp
client/device-ops-test.cpp
client/send-test.cpp
client/encryption-test.cpp
client/redact-test.cpp
client/tagging-test.cpp
client/account-data-test.cpp
client/room/room-actions-test.cpp
client/room/local-echo-test.cpp
client/room/event-relationships-test.cpp
client/room/member-membership-test.cpp
client/room/purge-test.cpp
client/push-rules-desc-test.cpp
client/notification-handler-test.cpp
client/validator-test.cpp
client/power-levels-desc-test.cpp
client/client-test.cpp
client/create-room-test.cpp
client/device-list-tracker-test.cpp
client/device-list-tracker-benchmark-test.cpp
client/room/read-receipt-test.cpp
client/room/undecrypted-events-test.cpp
client/encryption-benchmark-test.cpp
+ client/login-test.cpp
client/logout-test.cpp
client/room/pinned-events-test.cpp
client/get-versions-test.cpp
client/alias-test.cpp
client/encode-test.cpp
client/maybe-add-save-events-trigger-benchmark-test.cpp
EXTRA_LINK_LIBRARIES kazvclient kazveventemitter kazvjob client-test-lib kazvtestfixtures
EXTRA_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/client
)
libkazv_add_tests(
basejobtest.cpp
kazvjobtest.cpp
file-desc-test.cpp
EXTRA_LINK_LIBRARIES kazvbase kazvjob
)
libkazv_add_tests(
promise-test.cpp
EXTRA_LINK_LIBRARIES kazvbase kazvjob kazvstore
)
libkazv_add_tests(
event-emitter-test.cpp
EXTRA_LINK_LIBRARIES kazvbase kazveventemitter
)
libkazv_add_tests(
crypto-test.cpp
crypto/inbound-group-session-test.cpp
crypto/outbound-group-session-test.cpp
crypto/session-test.cpp
crypto/key-export-test.cpp
EXTRA_LINK_LIBRARIES kazvcrypto
)
libkazv_add_tests(
store-test.cpp
EXTRA_LINK_LIBRARIES kazvstore kazvjob
)
diff --git a/src/tests/client/client-test-util.hpp b/src/tests/client/client-test-util.hpp
index af74450..91b9ac1 100644
--- a/src/tests/client/client-test-util.hpp
+++ b/src/tests/client/client-test-util.hpp
@@ -1,54 +1,55 @@
/*
* 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>
+#include <catch2/catch_test_macros.hpp>
using namespace Kazv;
ClientModel createTestClientModel();
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/login-test.cpp b/src/tests/client/login-test.cpp
new file mode 100644
index 0000000..1156bcb
--- /dev/null
+++ b/src/tests/client/login-test.cpp
@@ -0,0 +1,29 @@
+/*
+ * 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 "client.hpp"
+#include "action-mock-utils.hpp"
+#include "client-test-util.hpp"
+#include "actions/auth.hpp"
+#include <catch2/catch_test_macros.hpp>
+
+using namespace Kazv;
+
+TEST_CASE("MLoginTokenLoginAction", "[client][login]")
+{
+ auto [next, _] = updateClient(ClientModel{}, MLoginTokenLoginAction{"https://m.example.com", "some-token", std::nullopt});
+ assert1Job(next);
+ for1stJob(next, [](const BaseJob &j) {
+ auto url = j.url();
+ REQUIRE(url.find("/_matrix/client/v3/login") != std::string::npos);
+ auto b = json::parse(std::get<BytesBody>(j.requestBody()));
+ REQUIRE(b.at("type").template get<std::string>() == "m.login.token");
+ REQUIRE(b.at("token").template get<std::string>() == "some-token");
+ REQUIRE(!b.contains("identifier"));
+ REQUIRE(!b.contains("password"));
+ });
+}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Aug 8, 9:55 AM (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1723073
Default Alt Text
(91 KB)

Event Timeline