Page MenuHomePhorge

No OneTemporary

Size
43 KB
Referenced Files
None
Subscribers
None
diff --git a/changelogs/66.add b/changelogs/66.add
new file mode 100644
index 0000000..c42ba58
--- /dev/null
+++ b/changelogs/66.add
@@ -0,0 +1 @@
+Add preset param to Client::createRoom()
diff --git a/src/client/client.cpp b/src/client/client.cpp
index db6343e..0d521a1 100644
--- a/src/client/client.cpp
+++ b/src/client/client.cpp
@@ -1,434 +1,438 @@
/*
* 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 <lager/constant.hpp>
#include "client.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::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::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) const
+ 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
+ ) 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;
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().native())
: std::nullopt;
return m_ctx.dispatch(UploadContentAction{
file,
// use only basename to prevent path info being leaked
basename,
file.contentType(),
// uploadId unused
std::string{}});
}
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());
}
}
diff --git a/src/client/client.hpp b/src/client/client.hpp
index 3d9e4d5..17d6faa 100644
--- a/src/client/client.hpp
+++ b/src/client/client.hpp
@@ -1,549 +1,553 @@
/*
* 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 "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;
/**
* 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()) const;
+ 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
+ ) 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();
}
/**
* 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();
}
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/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index db65b1f..94831b6 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -1,78 +1,79 @@
include(CTest)
set(KAZVTEST_RESPATH ${CMAKE_CURRENT_SOURCE_DIR}/resources)
configure_file(kazvtest-respath.hpp.in kazvtest-respath.hpp)
add_executable(kazvtest
testmain.cpp
basejobtest.cpp
event-test.cpp
cursorutiltest.cpp
base/serialization-test.cpp
base/types-test.cpp
base/immer-utils-test.cpp
base/json-utils-test.cpp
client/client-test-util.cpp
client/discovery-test.cpp
client/sync-test.cpp
client/content-test.cpp
client/paginate-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/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
kazvjobtest.cpp
event-emitter-test.cpp
crypto-test.cpp
crypto/deterministic-test.cpp
promise-test.cpp
store-test.cpp
file-desc-test.cpp
)
target_include_directories(
kazvtest
PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/client)
target_compile_definitions(kazvtest PRIVATE CATCH_CONFIG_ENABLE_ALL_STRINGMAKERS)
target_link_libraries(kazvtest
PRIVATE Catch2::Catch2WithMain
PRIVATE kazv
PRIVATE kazveventemitter
PRIVATE kazvjob
PRIVATE nlohmann_json::nlohmann_json
PRIVATE immer
PRIVATE lager
PRIVATE zug
PRIVATE kazvtestfixtures
)
add_test(NAME kazvtest COMMAND kazvtest "~[needs-internet]")
diff --git a/src/tests/client/create-room-test.cpp b/src/tests/client/create-room-test.cpp
new file mode 100644
index 0000000..43a0bd8
--- /dev/null
+++ b/src/tests/client/create-room-test.cpp
@@ -0,0 +1,97 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2021-2023 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+
+#include <lager/event_loop/boost_asio.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+#include <boost/asio.hpp>
+#include <asio-promise-handler.hpp>
+#include <cprjobhandler.hpp>
+#include <lagerstoreeventemitter.hpp>
+#include <cursorutil.hpp>
+#include <sdk-model.hpp>
+#include <client/client.hpp>
+
+#include "client-test-util.hpp"
+#include "factory.hpp"
+
+using namespace Kazv;
+using namespace Kazv::Factory;
+
+template<class Store, class Func>
+static auto getMockContext(SingleTypePromiseInterface<EffectStatus> &ph, Store &store, Func func)
+{
+ return typename Client::ContextT([&ph, &store, func](const auto &action) {
+ auto [cont, res] = func(action);
+ if (!cont) {
+ return ph.createResolved(res);
+ }
+ throw std::runtime_error{"unhandled action"};
+ }, ph, lager::deps<>{});
+}
+
+TEST_CASE("Client::createRoom()", "[client][create-room]")
+{
+ ClientModel m = makeClient();
+
+ boost::asio::io_context io;
+ SingleTypePromiseInterface<EffectStatus> ph{AsioPromiseHandler{io.get_executor()}};
+ auto jh = Kazv::CprJobHandler{io.get_executor()};
+ auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
+
+ auto sdk = Kazv::makeSdk(
+ SdkModel{m},
+ jh,
+ ee,
+ Kazv::AsioPromiseHandler{io.get_executor()},
+ zug::identity
+ );
+
+ auto ctx = sdk.context();
+ auto createRoomCalled = 0;
+ std::optional<CreateRoomAction> action = std::nullopt;
+
+ auto mockContext = getMockContext(ph, ctx, [&createRoomCalled, &action](const auto &a) {
+ if (std::holds_alternative<CreateRoomAction>(a)) {
+ ++createRoomCalled;
+ action = std::get<CreateRoomAction>(a);
+ return std::make_pair(/* cont = */ false, EffectStatus());
+ }
+ return std::make_pair(/* cont = */ true, EffectStatus());
+ });
+
+ auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
+
+ client.createRoom(
+ /* visibility = */ RoomVisibility::Private,
+ /* name = */ "some name",
+ /* alias = */ "alias",
+ /* invite = */ immer::array<std::string>{"@invited:example.com"},
+ /* isDirect = */ true,
+ /* allowFederate = */ true,
+ /* topic = */ "some topic",
+ /* powerLevelContentOverride = */ json::object({{"messages", 1}}),
+ /* preset = */ CreateRoomPreset::TrustedPrivateChat
+ ).then([&io](auto) {
+ io.stop();
+ });
+
+ io.run();
+
+ REQUIRE(createRoomCalled == 1);
+ REQUIRE(action->visibility == RoomVisibility::Private);
+ REQUIRE(action->name == "some name");
+ REQUIRE(action->roomAliasName == "alias");
+ REQUIRE(action->invite == immer::array<std::string>{"@invited:example.com"});
+ REQUIRE(action->isDirect == true);
+ REQUIRE(action->creationContent.get() == json::object({
+ {"m.federate", true},
+ }));
+ REQUIRE(action->topic == "some topic");
+ REQUIRE(action->preset == CreateRoomPreset::TrustedPrivateChat);
+}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Oct 8, 5:45 PM (1 h, 25 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784159
Default Alt Text
(43 KB)

Event Timeline