Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85806202
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
92 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/base/immer-utils.hpp b/src/base/immer-utils.hpp
new file mode 100644
index 0000000..bc70d99
--- /dev/null
+++ b/src/base/immer-utils.hpp
@@ -0,0 +1,61 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include "libkazv-config.hpp"
+
+#include <tuple>
+
+namespace Kazv
+{
+ template<class T>
+ [[nodiscard]] auto getIn(T &&item)
+ {
+ return std::forward<T>(item);
+ }
+
+ template<class ImmerT, class K, class ...Keys>
+ [[nodiscard]] auto getIn(ImmerT &&container, K &&key, Keys &&...keys)
+ {
+ return getIn(std::forward<ImmerT>(container)[std::forward<K>(key)], std::forward<Keys>(keys)...);
+ }
+
+ template<class T>
+ [[nodiscard]] auto setIn(T, T newVal)
+ {
+ return newVal;
+ }
+
+ template<class ImmerT, class K>
+ [[nodiscard]] auto setIn(ImmerT container, std::decay_t<decltype(getIn(std::declval<ImmerT>(), std::declval<K>()))> newVal, K &&key) -> std::decay_t<ImmerT>
+ {
+ return std::move(container).set(std::forward<K>(key), std::move(newVal));
+ }
+
+ template<class ImmerT, class K, class ...Keys>
+ [[nodiscard]] auto setIn(ImmerT container, std::decay_t<decltype(getIn(std::declval<ImmerT>(), std::declval<K>(), std::declval<Keys>()...))> newVal, K &&key, Keys &&...keys) -> std::decay_t<ImmerT>
+ {
+ auto oldItem = getIn(container, key);
+ return std::move(container).set(
+ std::forward<K>(key),
+ setIn(oldItem, std::move(newVal), std::forward<Keys>(keys)...)
+ );
+ }
+
+ template<class T, class Func>
+ [[nodiscard]] auto updateIn(T oldVal, Func func) -> T
+ {
+ return std::move(func)(oldVal);
+ }
+
+ template<class ImmerT, class Func, class ...Keys>
+ [[nodiscard]] auto updateIn(ImmerT container, Func func, Keys &&...keys) -> ImmerT
+ {
+ auto oldVal = getIn(container, keys...);
+ auto newVal = std::move(func)(std::move(oldVal));
+ return setIn(container, std::move(newVal), std::forward<Keys>(keys)...);
+ }
+}
diff --git a/src/client/actions/encryption.cpp b/src/client/actions/encryption.cpp
index 20185fa..0c8f83f 100644
--- a/src/client/actions/encryption.cpp
+++ b/src/client/actions/encryption.cpp
@@ -1,586 +1,617 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <zug/transducer/filter.hpp>
#include <zug/transducer/cat.hpp>
#include "encryption.hpp"
+#include <immer-utils.hpp>
#include <debug.hpp>
#include "cursorutil.hpp"
#include "status-utils.hpp"
namespace Kazv
{
using namespace CryptoConstants;
static json convertSignature(const ClientModel &m, std::string signature)
{
auto j = json::object();
j[m.userId] = json::object();
j[m.userId][ed25519 + ":" + m.deviceId] = signature;
return j;
}
ClientResult updateClient(ClientModel m, UploadIdentityKeysAction)
{
if (! m.crypto) {
kzo.client.warn() << "Client::crypto is invalid, ignoring it." << std::endl;
return { std::move(m), lager::noop };
}
auto &crypto = m.crypto.value();
auto keys =
immer::map<std::string, std::string>{}
.set(ed25519 + ":" + m.deviceId, crypto.ed25519IdentityKey())
.set(curve25519 + ":" + m.deviceId, crypto.curve25519IdentityKey());
DeviceKeys k {
m.userId,
m.deviceId,
{olmAlgo, megOlmAlgo},
keys,
{} // signatures to be added soon
};
auto j = json(k);
auto sig = crypto.sign(j);
k.signatures = convertSignature(m, sig);
auto job = m.job<UploadKeysJob>()
.make(k)
.withData(json{{"is", "identityKeys"}});
kzo.client.dbg() << "Uploading identity keys" << std::endl;
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, GenerateAndUploadOneTimeKeysAction a)
{
if (! m.crypto) {
kzo.client.warn() << "Client::crypto is invalid, ignoring it." << std::endl;
return { std::move(m), simpleFail };
}
auto &crypto = m.crypto.value();
kzo.client.dbg() << "Generating one-time keys..." << std::endl;
auto maxNumKeys = crypto.maxNumberOfOneTimeKeys();
auto numLocalKeys = crypto.numUnpublishedOneTimeKeys();
auto numStoredKeys = crypto.uploadedOneTimeKeysCount(signedCurve25519) + numLocalKeys;
auto numKeysToGenerate = a.numToGen;
auto genKeysLimit = maxNumKeys - numStoredKeys;
if (numKeysToGenerate > genKeysLimit) {
numKeysToGenerate = genKeysLimit;
}
if (numLocalKeys <= 0 && numKeysToGenerate <= 0) { // we have enough already
kzo.client.dbg() << "We have enough one-time keys. Ignoring this." << std::endl;
return { std::move(m), lager::noop };
}
if (numKeysToGenerate > 0) {
crypto.genOneTimeKeysWithRandom(a.random, numKeysToGenerate);
}
kzo.client.dbg() << "Generating done." << std::endl;
auto keys = crypto.unpublishedOneTimeKeys();
auto cv25519Keys = keys.at(curve25519);
immer::map<std::string, Variant> oneTimeKeys;
for (auto [id, keyStr] : cv25519Keys.items()) {
json keyObject = json::object();
keyObject["key"] = keyStr;
keyObject["signatures"] = convertSignature(m, crypto.sign(keyObject));
oneTimeKeys = std::move(oneTimeKeys).set(signedCurve25519 + ":" + id, JsonWrap(keyObject));
}
auto job = m.job<UploadKeysJob>()
.make(
std::nullopt, // deviceKeys
oneTimeKeys)
.withData(json{{"is", "oneTimeKeys"}});
kzo.client.dbg() << "Uploading one time keys" << std::endl;
m.addJob(std::move(job));
return { std::move(m), lager::noop };
};
ClientResult processResponse(ClientModel m, UploadKeysResponse r)
{
if (! m.crypto) {
kzo.client.warn() << "Client::crypto is invalid, ignoring it." << std::endl;
return { std::move(m), lager::noop };
}
auto &crypto = m.crypto.value();
auto is = r.dataStr("is");
if (is == "identityKeys") {
if (! r.success()) {
kzo.client.dbg() << "Uploading identity keys failed" << std::endl;
m.addTrigger(UploadIdentityKeysFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), failWithResponse(r) };
}
kzo.client.dbg() << "Uploading identity keys successful" << std::endl;
m.addTrigger(UploadIdentityKeysSuccessful{});
m.identityKeysUploaded = true;
} else {
if (! r.success()) {
kzo.client.dbg() << "Uploading one-time keys failed" << std::endl;
m.addTrigger(UploadOneTimeKeysFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), failWithResponse(r) };
}
kzo.client.dbg() << "Uploading one-time keys successful" << std::endl;
m.addTrigger(UploadOneTimeKeysSuccessful{});
crypto.markOneTimeKeysAsPublished();
}
crypto.setUploadedOneTimeKeysCount(r.oneTimeKeyCounts());
return { std::move(m), lager::noop };
}
static JsonWrap cannotDecryptEvent(std::string reason)
{
return json{
{"type", "m.room.message"},
{"content", {
{"msgtype","moe.kazv.mxc.cannot.decrypt"},
{"body", "**This message cannot be decrypted due to " + reason + ".**"}}}};
}
static bool verifyEvent(ClientModel &m, Event e, const json &plainJson)
{
auto crypto = m.crypto.value();
bool valid = true;
try {
std::string senderCurve25519Key = e.originalJson().get()
.at("content").at("sender_key");
auto deviceInfoOpt = m.deviceLists.findByCurve25519Key(e.sender(), senderCurve25519Key);
if (! deviceInfoOpt) {
kzo.client.dbg() << "Device key " << senderCurve25519Key
<< " unknown, thus invalid" << std::endl;
valid = false;
}
auto deviceInfo = deviceInfoOpt.value();
std::string algo = e.originalJson().get().at("content").at("algorithm");
if (algo == olmAlgo) {
if (! (plainJson.at("sender") == e.sender())) {
kzo.client.dbg() << "Sender does not match, thus invalid" << std::endl;
valid = false;
}
if (! (plainJson.at("recipient") == m.userId)) {
kzo.client.dbg() << "Recipient does not match, thus invalid" << std::endl;
valid = false;
}
if (! (plainJson.at("recipient_keys").at(ed25519) == crypto.ed25519IdentityKey())) {
kzo.client.dbg() << "Recipient key does not match, thus invalid" << std::endl;
valid = false;
}
auto thisEd25519Key = plainJson.at("keys").at(ed25519).get<std::string>();
if (thisEd25519Key != deviceInfo.ed25519Key) {
kzo.client.dbg() << "Sender ed25519 key does not match, thus invalid" << std::endl;
valid = false;
}
} else if (algo == megOlmAlgo) {
if (! (plainJson.at("room_id").get<std::string>() ==
e.originalJson().get().at("room_id").get<std::string>())) {
kzo.client.dbg() << "Room id does not match, thus invalid" << std::endl;
valid = false;
}
if (e.originalJson().get().at("content").at("device_id").get<std::string>()
!= deviceInfo.deviceId) {
kzo.client.dbg() << "Device id does not match, thus invalid" << std::endl;
valid = false;
}
auto actualEd25519Key = crypto.getInboundGroupSessionEd25519KeyFromEvent(e.originalJson().get());
if ((! actualEd25519Key)
|| deviceInfo.ed25519Key != actualEd25519Key.value()) {
kzo.client.dbg() << "sender ed25519 key does not match, thus invalid" << std::endl;
kzo.client.dbg() << "From group session: "
<< (actualEd25519Key ? actualEd25519Key.value() : "<none>") << std::endl;
kzo.client.dbg() << "From device info: " << deviceInfo.ed25519Key << std::endl;
valid = false;
}
} else {
kzo.client.dbg() << "Unknown algorithm, thus invalid" << std::endl;
valid = false;
}
} catch (const std::exception &) {
kzo.client.dbg() << "json format is not correct, thus invalid" << std::endl;
valid = false;
}
return valid;
}
static Event decryptEvent(ClientModel &m, Event e)
{
// no need for decryption
if (e.decrypted() || (! e.encrypted())) {
return e;
}
auto &crypto = m.crypto.value();
kzo.client.dbg() << "About to decrypt event: "
<< e.originalJson().get().dump() << std::endl;
auto maybePlainText = crypto.decrypt(e.originalJson().get());
if (! maybePlainText) {
kzo.client.dbg() << "Cannot decrypt: " << maybePlainText.reason() << std::endl;
return e.setDecryptedJson(cannotDecryptEvent(maybePlainText.reason()), Event::NotDecrypted);
} else {
kzo.client.dbg() << "Decrypted message: " << maybePlainText.value() << std::endl;
auto plainJson = json::parse(maybePlainText.value());
auto valid = verifyEvent(m, e, plainJson);
if (valid) {
kzo.client.dbg() << "The decrypted event is valid." << std::endl;
}
return valid
? e.setDecryptedJson(plainJson, Event::Decrypted)
: e.setDecryptedJson(cannotDecryptEvent("invalid event"), Event::NotDecrypted);
}
}
ClientModel tryDecryptEvents(ClientModel m)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring decryption request" << std::endl;
return m;
}
auto &crypto = m.crypto.value();
kzo.client.dbg() << "Trying to decrypt events..." << std::endl;
auto decryptFunc = [&](auto e) { return decryptEvent(m, e); };
auto takeOutRoomKeyEvents =
[&](auto e) {
if (e.type() != "m.room_key") {
// Leave it as it is
return true;
}
auto content = e.content();
std::string roomId = content.get().at("room_id");
std::string sessionId = content.get().at("session_id");
std::string sessionKey = content.get().at("session_key");
std::string senderKey = e.originalJson().get().at("content").at("sender_key");
auto k = KeyOfGroupSession{roomId, senderKey, sessionId};
std::string ed25519Key = e.decryptedJson().get().at("keys").at(ed25519);
if (crypto.createInboundGroupSession(k, sessionKey, ed25519Key)) {
return false; // such that this event is removed
}
return true;
};
m.toDevice = intoImmer(
EventList{},
zug::map(decryptFunc)
| zug::filter(takeOutRoomKeyEvents),
std::move(m.toDevice));
auto decryptEventInRoom =
[&](auto id, auto room) {
if (! room.encrypted) {
return;
} else {
auto messages = room.messages;
room.messages = merge(
room.messages,
intoImmer(
EventList{},
zug::filter([](auto n) {
auto e = n.second;
return e.encrypted();
})
| zug::map([=](auto n) {
auto event = n.second;
return decryptFunc(event);
}),
std::move(messages)),
keyOfTimeline);
m.roomList.rooms = std::move(m.roomList.rooms).set(id, room);
}
};
auto rooms = m.roomList.rooms;
for (auto [id, room]: rooms) {
decryptEventInRoom(id, room);
}
return m;
}
std::optional<BaseJob> clientPerform(ClientModel m, QueryKeysAction a)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return std::nullopt;
}
immer::map<std::string, immer::array<std::string>> deviceKeys;
auto encryptedUsers = m.deviceLists.outdatedUsers();
if (encryptedUsers.empty()) {
kzo.client.dbg() << "Keys are up-to-date." << std::endl;
return std::nullopt;
}
kzo.client.dbg() << "We need to query keys for: " << std::endl;
for (auto userId: encryptedUsers) {
kzo.client.dbg() << userId << std::endl;
deviceKeys = std::move(deviceKeys).set(userId, {});
}
kzo.client.dbg() << "^" << std::endl;
auto job = m.job<QueryKeysJob>()
.make(std::move(deviceKeys),
std::nullopt, // timeout
a.isInitialSync ? std::nullopt : m.syncToken
);
return job;
}
ClientResult updateClient(ClientModel m, QueryKeysAction a)
{
auto jobOpt = clientPerform(m, a);
if (jobOpt) {
m.addJob(jobOpt.value());
}
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, QueryKeysResponse r)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return { std::move(m), simpleFail };
}
if (! r.success()) {
kzo.client.dbg() << "query keys failed: " << r.errorCode() << r.errorMessage() << std::endl;
return { std::move(m), failWithResponse(r) };
}
kzo.client.dbg() << "Received a query key response" << std::endl;
auto &crypto = m.crypto.value();
auto usersMap = r.deviceKeys();
for (auto [userId, deviceMap] : usersMap) {
for (auto [deviceId, deviceInfo] : deviceMap) {
kzo.client.dbg() << "Key for " << userId
<< "/" << deviceId
<< ": " << json(deviceInfo).dump()
<< std::endl;
m.deviceLists.addDevice(userId, deviceId, deviceInfo, crypto);
}
m.deviceLists.markUpToDate(userId);
}
return { std::move(m), lager::noop };
}
ClientResult updateClient(ClientModel m, ClaimKeysAction a)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return { std::move(m), lager::noop };
}
auto &c = m.crypto.value();
kzo.client.dbg() << "claim keys for: " << json(a.devicesToSend).dump() << std::endl;
auto keyMap = immer::map<std::string, immer::map<std::string /* deviceId */,
std::string /* curve25519IdentityKey */>>{};
for (auto [userId, devices] : a.devicesToSend) {
kzo.client.dbg() << "Iterating through user " << userId << std::endl;
auto deviceToKey = immer::map<std::string, std::string>{};
for (auto deviceId : devices) {
kzo.client.dbg() << "Device: " << deviceId << std::endl;
auto infoOpt = m.deviceLists.get(userId, deviceId);
if (infoOpt) {
kzo.client.dbg() << "Got device info, curve25519 key is: " << infoOpt.value().curve25519Key << std::endl;
deviceToKey = std::move(deviceToKey)
.set(deviceId, infoOpt.value().curve25519Key);
} else {
kzo.client.dbg() << "Did not get device info" << std::endl;
}
}
keyMap = std::move(keyMap).set(userId, deviceToKey);
}
auto devicesToClaimKeys = c.devicesMissingOutboundSessionKey(keyMap);
kzo.client.dbg() << "Really claim keys for: " << json(devicesToClaimKeys).dump() << std::endl;
auto oneTimeKeys = immer::map<std::string, immer::map<std::string, std::string>>{};
for (auto [userId, devices] : devicesToClaimKeys) {
auto devKeys = immer::map<std::string, std::string>{};
for (auto deviceId: devices) {
devKeys = std::move(devKeys).set(deviceId, signedCurve25519);
}
oneTimeKeys = std::move(oneTimeKeys).set(userId, devKeys);
}
auto job = m.job<ClaimKeysJob>()
.make(std::move(oneTimeKeys))
.withData(json{
{"roomId", a.roomId},
{"sessionId", a.sessionId},
{"sessionKey", a.sessionKey},
{"devicesToSend", a.devicesToSend},
{"random", a.random}
});
m.addJob(std::move(job));
return { std::move(m), lager::noop };
}
ClientResult processResponse(ClientModel m, ClaimKeysResponse r)
{
if (! m.crypto) {
kzo.client.dbg() << "We have no encryption enabled--ignoring this" << std::endl;
return { std::move(m), simpleFail };
}
if (! r.success()) {
kzo.client.dbg() << "claim keys failed" << std::endl;
m.addTrigger(ClaimKeysFailed{r.errorCode(), r.errorMessage()});
return { std::move(m), failWithResponse(r) };
}
kzo.client.dbg() << "claim keys successful" << std::endl;
kzo.client.dbg() << "Json body: " << r.jsonBody().get().dump() << std::endl;
auto &c = m.crypto.value();
auto roomId = r.dataStr("roomId");
auto sessionKey = r.dataStr("sessionKey");
auto sessionId = r.dataStr("sessionId");
auto devicesToSend =
immer::map<std::string, immer::flex_vector<std::string>>(r.dataJson("devicesToSend"));
auto random = r.dataJson("random").template get<RandomData>();
// create outbound sessions for those devices
auto oneTimeKeys = r.oneTimeKeys();
for (auto [userId, deviceMap] : oneTimeKeys) {
for (auto [deviceId, keyVar] : deviceMap) {
if (std::holds_alternative<JsonWrap>(keyVar)) {
auto keys = std::get<JsonWrap>(keyVar).get();
for (auto [keyId, key] : keys.items()) {
auto deviceInfoOpt = m.deviceLists.get(userId, deviceId);
if (deviceInfoOpt) {
auto deviceInfo = deviceInfoOpt.value();
kzo.client.dbg() << "Verifying key for " << userId
<< "/" << deviceId
<< key.dump()
<< " with ed25519 key "
<< deviceInfo.ed25519Key << std::endl;
auto verified = c.verify(key, userId, deviceId, deviceInfo.ed25519Key);
kzo.client.dbg() << (verified ? "passed" : "did not pass") << std::endl;
if (verified && key.contains("key")) {
auto theirOneTimeKey = key.at("key");
kzo.client.dbg() << "creating outbound session for it" << std::endl;
c.createOutboundSessionWithRandom(random, deviceInfo.curve25519Key, theirOneTimeKey);
random.erase(0, Crypto::createOutboundSessionRandomSize());
kzo.client.dbg() << "done" << std::endl;
}
}
}
}
}
}
auto eventJson = json{
{"content", {{"algorithm", megOlmAlgo},
{"room_id", roomId},
{"session_id", sessionId},
{"session_key", sessionKey}}},
{"type", "m.room_key"}
};
auto event = Event(JsonWrap(eventJson));
return {
std::move(m),
[event](auto &&) { return EffectStatus{ /* success = */ true, json{{ "keyEvent", event.originalJson() }} }; }
};
}
ClientResult updateClient(ClientModel m, EncryptMegOlmEventAction a)
{
auto [encryptedEvent, maybeKey] = m.megOlmEncrypt(a.e, a.roomId, a.timeMs, a.random);
return {
std::move(m),
[=](auto && /* ctx */) {
auto retJson = json::object({
{"encrypted", encryptedEvent.originalJson()},
});
if (maybeKey.has_value()) {
retJson["key"] = maybeKey.value();
}
return EffectStatus(/* succ = */ true, retJson);
}
};
}
ClientResult updateClient(ClientModel m, EncryptOlmEventAction a)
{
auto encryptedEvent = m.olmEncrypt(a.e, a.devices, a.random);
return {
std::move(m),
[=](auto && /* ctx */) {
auto retJson = json::object({
{"encrypted", encryptedEvent.originalJson()},
});
return EffectStatus(/* succ = */ true, retJson);
}
};
}
+
+ ClientResult updateClient(ClientModel m, SetDeviceTrustLevelAction a)
+ {
+ auto maybeOldInfo = m.deviceLists.get(a.userId, a.deviceId);
+ if (!maybeOldInfo) {
+ return {
+ std::move(m),
+ [=](auto && /* ctx */) {
+ auto retJson = json::object({
+ {"error", "No such device"},
+ {"errorCode", "MOE_KAZV_MXC_KAZV_NO_SUCH_DEVICE"},
+ });
+
+ return EffectStatus(/* succ = */ false, retJson);
+ }
+ };
+ }
+
+ m.deviceLists.deviceLists = updateIn(
+ std::move(m.deviceLists.deviceLists),
+ [a](auto device) {
+ device.trustLevel = a.trustLevel;
+ return device;
+ },
+ a.userId,
+ a.deviceId
+ );
+
+ return { m, lager::noop };
+ }
}
diff --git a/src/client/actions/encryption.hpp b/src/client/actions/encryption.hpp
index 6b40bce..cd4cf21 100644
--- a/src/client/actions/encryption.hpp
+++ b/src/client/actions/encryption.hpp
@@ -1,32 +1,34 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include "client-model.hpp"
#include "csapi/keys.hpp"
namespace Kazv
{
ClientResult updateClient(ClientModel m, UploadIdentityKeysAction a);
ClientResult updateClient(ClientModel m, GenerateAndUploadOneTimeKeysAction a);
ClientResult processResponse(ClientModel m, UploadKeysResponse r);
ClientModel tryDecryptEvents(ClientModel m);
std::optional<BaseJob> clientPerform(ClientModel m, QueryKeysAction a);
ClientResult updateClient(ClientModel m, QueryKeysAction a);
ClientResult processResponse(ClientModel m, QueryKeysResponse r);
ClientResult updateClient(ClientModel m, ClaimKeysAction a);
ClientResult processResponse(ClientModel m, ClaimKeysResponse r);
ClientResult updateClient(ClientModel m, EncryptMegOlmEventAction a);
ClientResult updateClient(ClientModel m, EncryptOlmEventAction a);
+
+ ClientResult updateClient(ClientModel m, SetDeviceTrustLevelAction a);
}
diff --git a/src/client/client-model.hpp b/src/client/client-model.hpp
index e3df068..1954dd7 100644
--- a/src/client/client-model.hpp
+++ b/src/client/client-model.hpp
@@ -1,496 +1,503 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <tuple>
#include <variant>
#include <string>
#include <optional>
#include <lager/context.hpp>
#include <boost/hana.hpp>
#include <serialization/std-optional.hpp>
#include <csapi/sync.hpp>
#include <file-desc.hpp>
#include <crypto.hpp>
#include <serialization/immer-flex-vector.hpp>
#include <serialization/immer-box.hpp>
#include <serialization/immer-map.hpp>
#include <serialization/immer-array.hpp>
#include "clientfwd.hpp"
#include "device-list-tracker.hpp"
#include "room/room-model.hpp"
namespace Kazv
{
inline const std::string DEFTXNID{"0"};
enum RoomVisibility
{
Private,
Public,
};
enum CreateRoomPreset
{
PrivateChat,
PublicChat,
TrustedPrivateChat,
};
enum ThumbnailResizingMethod
{
Crop,
Scale,
};
struct ClientModel
{
std::string serverUrl;
std::string userId;
std::string token;
std::string deviceId;
bool loggedIn{false};
bool syncing{false};
bool shouldSync{true};
int firstRetryMs{1000};
int retryTimeFactor{2};
int maxRetryMs{30 * 1000};
int syncTimeoutMs{20000};
std::string initialSyncFilterId;
std::string incrementalSyncFilterId;
std::optional<std::string> syncToken;
RoomListModel roomList;
immer::map<std::string /* sender */, Event> presence;
immer::map<std::string /* type */, Event> accountData;
std::string nextTxnId{DEFTXNID};
immer::flex_vector<BaseJob> nextJobs;
immer::flex_vector<KazvEvent> nextTriggers;
EventList toDevice;
std::optional<Crypto> crypto;
bool identityKeysUploaded{false};
DeviceListTracker deviceLists;
immer::flex_vector<std::string /* deviceId */> devicesToSendKeys(std::string userId) const;
std::pair<Event, std::optional<std::string> /* sessionKey */>
megOlmEncrypt(Event e, std::string roomId, Timestamp timeMs, RandomData random);
/// precondition: the one-time keys for those devices must already be claimed
Event olmEncrypt(Event e, immer::map<std::string, immer::flex_vector<std::string>> userIdToDeviceIdMap, RandomData random);
/// @return number of one-time keys we need to generate
std::size_t numOneTimeKeysNeeded() const;
// helpers
template<class Job>
struct MakeJobT
{
template<class ...Args>
constexpr auto make(Args &&...args) const {
if constexpr (Job::needsAuth()) {
return Job(
serverUrl,
token,
std::forward<Args>(args)...);
} else {
return Job(
serverUrl,
std::forward<Args>(args)...);
}
}
std::string serverUrl;
std::string token;
};
template<class Job>
constexpr auto job() const {
return MakeJobT<Job>{serverUrl, token};
}
inline void addJob(BaseJob j) {
nextJobs = std::move(nextJobs).push_back(std::move(j));
}
inline auto popAllJobs() {
auto jobs = std::move(nextJobs);
nextJobs = DEFVAL;
return jobs;
};
inline void addTrigger(KazvEvent t) {
addTriggers({t});
}
inline void addTriggers(immer::flex_vector<KazvEvent> c) {
nextTriggers = std::move(nextTriggers) + c;
}
inline auto popAllTriggers() {
auto triggers = std::move(nextTriggers);
nextTriggers = DEFVAL;
return triggers;
}
using Action = ClientAction;
using Effect = ClientEffect;
using Result = ClientResult;
static Result update(ClientModel m, Action a);
};
// actions:
struct LoginAction {
std::string serverUrl;
std::string username;
std::string password;
std::optional<std::string> deviceName;
};
struct TokenLoginAction
{
std::string serverUrl;
std::string username;
std::string token;
std::string deviceId;
};
struct LogoutAction {};
struct GetWellknownAction
{
std::string userId;
};
struct GetVersionsAction
{
std::string serverUrl;
};
struct SyncAction {};
struct SetShouldSyncAction
{
bool shouldSync;
};
struct PaginateTimelineAction
{
std::string roomId;
/// Must be where the Gap is
std::string fromEventId;
std::optional<int> limit;
};
struct SendMessageAction
{
std::string roomId;
Event event;
};
struct SendStateEventAction
{
std::string roomId;
Event event;
};
struct CreateRoomAction
{
using Visibility = RoomVisibility;
using Preset = CreateRoomPreset;
Visibility visibility;
std::optional<std::string> roomAliasName;
std::optional<std::string> name;
std::optional<std::string> topic;
immer::array<std::string> invite;
//immer::array<Invite3pid> invite3pid;
std::optional<std::string> roomVersion;
JsonWrap creationContent;
immer::array<Event> initialState;
std::optional<Preset> preset;
std::optional<bool> isDirect;
JsonWrap powerLevelContentOverride;
};
struct GetRoomStatesAction
{
std::string roomId;
};
struct GetStateEventAction
{
std::string roomId;
std::string type;
std::string stateKey;
};
struct InviteToRoomAction
{
std::string roomId;
std::string userId;
};
struct JoinRoomByIdAction
{
std::string roomId;
};
struct JoinRoomAction
{
std::string roomIdOrAlias;
immer::array<std::string> serverName;
};
struct LeaveRoomAction
{
std::string roomId;
};
struct ForgetRoomAction
{
std::string roomId;
};
struct KickAction
{
std::string roomId;
std::string userId;
std::optional<std::string> reason;
};
struct BanAction
{
std::string roomId;
std::string userId;
std::optional<std::string> reason;
};
struct UnbanAction
{
std::string roomId;
std::string userId;
};
struct SetTypingAction
{
std::string roomId;
bool typing;
std::optional<int> timeoutMs;
};
struct PostReceiptAction
{
std::string roomId;
std::string eventId;
};
struct SetReadMarkerAction
{
std::string roomId;
std::string eventId;
};
struct UploadContentAction
{
FileDesc content;
std::optional<std::string> filename;
std::optional<std::string> contentType;
std::string uploadId; // to be used by library users
};
struct DownloadContentAction
{
std::string mxcUri;
std::optional<FileDesc> downloadTo;
};
struct DownloadThumbnailAction
{
std::string mxcUri;
int width;
int height;
std::optional<ThumbnailResizingMethod> method;
std::optional<bool> allowRemote;
std::optional<FileDesc> downloadTo;
};
struct ResubmitJobAction
{
BaseJob job;
};
struct ProcessResponseAction
{
Response response;
};
struct PostInitialFiltersAction
{
};
struct SendToDeviceMessageAction
{
Event event;
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
};
struct UploadIdentityKeysAction
{
};
/**
* The action to generate one-time keys.
*
* `random.size()` must be at least `randomSize(numToGen)`.
*
* This action will not generate keys exceeding the local limit of olm.
*/
struct GenerateAndUploadOneTimeKeysAction
{
/// @return The size of random needed to generate
/// `numToGen` one-time keys
static std::size_t randomSize(std::size_t numToGen);
/// The number of keys to generate
std::size_t numToGen;
/// The random data used to generate keys
RandomData random;
};
struct QueryKeysAction
{
bool isInitialSync;
};
struct ClaimKeysAction
{
static std::size_t randomSize(immer::map<std::string, immer::flex_vector<std::string>> devicesToSend);
std::string roomId;
std::string sessionId;
std::string sessionKey;
immer::map<std::string, immer::flex_vector<std::string>> devicesToSend;
RandomData random;
};
/**
* The action to encrypt an megolm event for a room.
*
* If the action is successful, the result `r` will
* be such that `r.dataJson("encrypted")` contains the encrypted event *json*.
*
* If the megolm session is rotated, `r.dataStr("key")` will contain the key
* of the megolm session. Otherwise, `r.data().contains("key")` will be false.
*
* The Action may fail due to insufficient random data,
* when the megolm session needs to be rotated.
* In this case, the reducer for the Action will fail,
* and its result `r` will be such that
* `r.dataStr("reason") == "NotEnoughRandom"`.
* The user needs to provide random data of
* at least size `maxRandomSize()`.
*
*/
struct EncryptMegOlmEventAction
{
static std::size_t maxRandomSize();
static std::size_t minRandomSize();
/// The id of the room to encrypt for.
std::string roomId;
/// The event to encrypt.
Event e;
/// The timestamp, to determine whether the session should expire.
Timestamp timeMs;
/// Random data for the operation. Must be of at least size
/// `minRandomSize()`. If this is a retry of the previous operation
/// due to NotEnoughRandom, it must be of at least size `maxRandomSize()`.
RandomData random;
};
/**
* The action to encrypt events with olm for multiple devices.
*
* If the action is successful,
* The result `r` will be such that `r.dataJson("encrypted")`
* contains the json of the encrypted event.
*/
struct EncryptOlmEventAction
{
using UserIdToDeviceIdMap = immer::map<std::string, immer::flex_vector<std::string>>;
static std::size_t randomSize(UserIdToDeviceIdMap devices);
/// Devices to encrypt for.
UserIdToDeviceIdMap devices;
/// The event to encrypt.
Event e;
/// The random data for the encryption. Must be of at least
/// size `randomSize(devices)`.
RandomData random;
};
+ struct SetDeviceTrustLevelAction
+ {
+ std::string userId;
+ std::string deviceId;
+ DeviceTrustLevel trustLevel;
+ };
+
struct GetUserProfileAction
{
std::string userId;
};
struct SetAvatarUrlAction
{
std::optional<std::string> avatarUrl;
};
struct SetDisplayNameAction
{
std::optional<std::string> displayName;
};
template<class Archive>
void serialize(Archive &ar, ClientModel &m, std::uint32_t const /*version*/)
{
bool dummySyncing{false};
ar
& m.serverUrl
& m.userId
& m.token
& m.deviceId
& m.loggedIn
& dummySyncing
& m.firstRetryMs
& m.retryTimeFactor
& m.maxRetryMs
& m.syncTimeoutMs
& m.initialSyncFilterId
& m.incrementalSyncFilterId
& m.syncToken
& m.roomList
& m.presence
& m.accountData
& m.nextTxnId
& m.toDevice
& m.crypto
& m.identityKeysUploaded
& m.deviceLists
;
}
}
BOOST_CLASS_VERSION(Kazv::ClientModel, 0)
diff --git a/src/client/client.cpp b/src/client/client.cpp
index 8d35518..a681201 100644
--- a/src/client/client.cpp
+++ b/src/client/client.cpp
@@ -1,373 +1,392 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <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
-> 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}
};
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);
}
});
}
void Client::stopSyncing() const
{
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});
+ }
}
diff --git a/src/client/client.hpp b/src/client/client.hpp
index cecc308..73b0c2a 100644
--- a/src/client/client.hpp
+++ b/src/client/client.hpp
@@ -1,472 +1,491 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <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"
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);
}));
}
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.
* @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;
/**
* 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.
*/
void 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;
+
/**
* 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/client/clientfwd.hpp b/src/client/clientfwd.hpp
index cbceced..1a277d1 100644
--- a/src/client/clientfwd.hpp
+++ b/src/client/clientfwd.hpp
@@ -1,132 +1,134 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <tuple>
#include <variant>
#include <lager/context.hpp>
#include <context.hpp>
#include "room/room-model.hpp"
namespace Kazv
{
using namespace Api;
class JobInterface;
class EventInterface;
struct LoginAction;
struct TokenLoginAction;
struct LogoutAction;
struct GetWellknownAction;
struct GetVersionsAction;
struct SyncAction;
struct SetShouldSyncAction;
struct PostInitialFiltersAction;
struct PaginateTimelineAction;
struct SendMessageAction;
struct SendStateEventAction;
struct CreateRoomAction;
struct GetRoomStatesAction;
struct GetStateEventAction;
struct InviteToRoomAction;
struct JoinRoomByIdAction;
struct EmitKazvEventsAction;
struct JoinRoomAction;
struct LeaveRoomAction;
struct ForgetRoomAction;
struct KickAction;
struct BanAction;
struct UnbanAction;
struct ProcessResponseAction;
struct SetTypingAction;
struct PostReceiptAction;
struct SetReadMarkerAction;
struct UploadContentAction;
struct DownloadContentAction;
struct DownloadThumbnailAction;
struct SendToDeviceMessageAction;
struct UploadIdentityKeysAction;
struct GenerateAndUploadOneTimeKeysAction;
struct QueryKeysAction;
struct ClaimKeysAction;
struct EncryptMegOlmEventAction;
struct EncryptOlmEventAction;
+ struct SetDeviceTrustLevelAction;
struct GetUserProfileAction;
struct SetAvatarUrlAction;
struct SetDisplayNameAction;
struct ResubmitJobAction;
struct ClientModel;
using ClientAction = std::variant<
RoomListAction,
LoginAction,
TokenLoginAction,
LogoutAction,
GetWellknownAction,
GetVersionsAction,
SyncAction,
SetShouldSyncAction,
PostInitialFiltersAction,
PaginateTimelineAction,
SendMessageAction,
SendStateEventAction,
CreateRoomAction,
GetRoomStatesAction,
GetStateEventAction,
InviteToRoomAction,
JoinRoomByIdAction,
JoinRoomAction,
LeaveRoomAction,
ForgetRoomAction,
KickAction,
BanAction,
UnbanAction,
ProcessResponseAction,
SetTypingAction,
PostReceiptAction,
SetReadMarkerAction,
UploadContentAction,
DownloadContentAction,
DownloadThumbnailAction,
SendToDeviceMessageAction,
UploadIdentityKeysAction,
GenerateAndUploadOneTimeKeysAction,
QueryKeysAction,
ClaimKeysAction,
EncryptMegOlmEventAction,
EncryptOlmEventAction,
+ SetDeviceTrustLevelAction,
GetUserProfileAction,
SetAvatarUrlAction,
SetDisplayNameAction,
ResubmitJobAction
>;
using ClientEffect = Effect<ClientAction, lager::deps<>>;
using ClientResult = std::pair<ClientModel, ClientEffect>;
}
diff --git a/src/client/device-list-tracker.cpp b/src/client/device-list-tracker.cpp
index 6746512..46fd31c 100644
--- a/src/client/device-list-tracker.cpp
+++ b/src/client/device-list-tracker.cpp
@@ -1,151 +1,156 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include "device-list-tracker.hpp"
#include <algorithm>
#include <zug/transducer/filter.hpp>
#include <zug/sequence.hpp>
#include <zug/transducer/distinct.hpp>
#include <zug/transducer/chain.hpp>
#include <debug.hpp>
namespace Kazv
{
immer::flex_vector<std::string> DeviceListTracker::outdatedUsers() const
{
return intoImmer(
immer::flex_vector<std::string>{},
zug::filter([](auto n) {
auto [userId, outdated] = n;
return outdated;
})
| zug::map([](auto n) {
auto [userId, outdated] = n;
return userId;
}),
usersToTrackDeviceLists);
}
bool DeviceListTracker::addDevice(std::string userId, std::string deviceId, Api::QueryKeysJob::DeviceInformation deviceInfo, Crypto &crypto)
{
using namespace CryptoConstants;
if (userId != deviceInfo.userId
|| deviceId != deviceInfo.deviceId) {
return false;
}
// if the ed25519 key changed, reject
auto curEd25519Key = deviceInfo.keys[ed25519 + ":" + deviceId];
if (deviceLists[userId].find(deviceId)
&& curEd25519Key != deviceLists[userId][deviceId].ed25519Key) {
return false;
}
kzo.client.dbg() << "verifying device info" << std::endl;
if (crypto.verify(deviceInfo, userId, deviceId, curEd25519Key)) {
kzo.client.dbg() << "passed verification" << std::endl;
auto info = DeviceKeyInfo{
deviceId,
deviceInfo.keys[ed25519 + ":" + deviceId],
deviceInfo.keys[curve25519 + ":" + deviceId],
deviceInfo.unsignedData ? deviceInfo.unsignedData.value().deviceDisplayName : std::nullopt
};
deviceLists = std::move(deviceLists)
.update(userId, [=](auto deviceMap) {
return std::move(deviceMap).set(deviceId, info);
});
return true;
}
kzo.client.dbg() << "did not pass verification" << std::endl;
return false;
}
void DeviceListTracker::markUpToDate(std::string userId)
{
usersToTrackDeviceLists = std::move(usersToTrackDeviceLists).set(userId, false);
}
std::optional<DeviceKeyInfo> DeviceListTracker::get(std::string userId, std::string deviceId) const
{
try {
return deviceLists.at(userId).at(deviceId);
} catch (const std::exception &) {
return std::nullopt;
}
}
std::optional<DeviceKeyInfo> DeviceListTracker::findByEd25519Key(
std::string userId, std::string ed25519Key) const
{
auto devices = deviceLists.at(userId);
auto it = std::find_if(devices.begin(), devices.end(),
[=](auto n) {
auto [deviceId, info] = n;
return info.ed25519Key == ed25519Key;
});
if (it != devices.end()) {
return it->second;
} else {
return std::nullopt;
}
}
std::optional<DeviceKeyInfo> DeviceListTracker::findByCurve25519Key(
std::string userId, std::string curve25519Key) const
{
auto devices = deviceLists.at(userId);
auto it = std::find_if(devices.begin(), devices.end(),
[=](auto n) {
auto [deviceId, info] = n;
return info.curve25519Key == curve25519Key;
});
if (it != devices.end()) {
return it->second;
} else {
return std::nullopt;
}
}
immer::flex_vector<std::string> DeviceListTracker::diff(DeviceListTracker that) const
{
auto users = zug::sequence(zug::map([](auto n) { return n.first; }), deviceLists);
auto thatUsers = zug::sequence(zug::map([](auto n) { return n.first; }), that.deviceLists);
auto changedUsers = intoImmer(
immer::flex_vector<std::string>{},
zug::chain(std::move(thatUsers))
| zug::distinct
| zug::filter([=](auto userId) {
return deviceLists[userId] != that.deviceLists[userId];
}),
std::move(users));
return changedUsers;
}
auto DeviceListTracker::devicesFor(std::string userId) const -> DeviceMapT
{
return deviceLists[userId];
}
bool operator==(DeviceKeyInfo a, DeviceKeyInfo b)
{
return a.deviceId == b.deviceId
&& a.ed25519Key == b.ed25519Key
&& a.curve25519Key == b.curve25519Key
&& a.displayName == b.displayName
&& a.trustLevel == b.trustLevel;
}
+
+ bool operator!=(DeviceKeyInfo a, DeviceKeyInfo b)
+ {
+ return !(a == b);
+ }
}
diff --git a/src/client/device-list-tracker.hpp b/src/client/device-list-tracker.hpp
index a7e165c..8613e81 100644
--- a/src/client/device-list-tracker.hpp
+++ b/src/client/device-list-tracker.hpp
@@ -1,106 +1,107 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <string>
#include <immer/map.hpp>
#include <immer/flex_vector.hpp>
#include <boost/serialization/string.hpp>
#include <serialization/std-optional.hpp>
#include <serialization/immer-map.hpp>
#include <crypto.hpp>
#include <csapi/keys.hpp>
#include "cursorutil.hpp"
namespace Kazv
{
enum DeviceTrustLevel
{
Blocked,
Unseen,
Seen,
Verified,
};
struct DeviceKeyInfo
{
std::string deviceId;
std::string ed25519Key;
std::string curve25519Key;
std::optional<std::string> displayName;
DeviceTrustLevel trustLevel{Unseen};
};
bool operator==(DeviceKeyInfo a, DeviceKeyInfo b);
+ bool operator!=(DeviceKeyInfo a, DeviceKeyInfo b);
template<class Archive>
void serialize(Archive &ar, DeviceKeyInfo &i, std::uint32_t const /*version*/)
{
ar
& i.deviceId
& i.ed25519Key
& i.curve25519Key
& i.displayName
& i.trustLevel
;
}
struct DeviceListTracker
{
using DeviceMapT = immer::map<std::string /* deviceId */, DeviceKeyInfo>;
immer::map<std::string /* userId */, bool /* outdated */> usersToTrackDeviceLists;
immer::map<std::string /* userId */, DeviceMapT> deviceLists;
template<class RangeT>
void track(RangeT &&userIds) {
for (auto userId : std::forward<RangeT>(userIds)) {
usersToTrackDeviceLists = std::move(usersToTrackDeviceLists)
.set(userId, true);
}
}
template<class RangeT>
void untrack(RangeT &&userIds) {
for (auto userId : std::forward<RangeT>(userIds)) {
usersToTrackDeviceLists = std::move(usersToTrackDeviceLists).erase(userId);
}
}
immer::flex_vector<std::string> outdatedUsers() const;
bool addDevice(std::string userId, std::string deviceId, Api::QueryKeysJob::DeviceInformation deviceInfo, Crypto &crypto);
void markUpToDate(std::string userId);
DeviceMapT devicesFor(std::string userId) const;
std::optional<DeviceKeyInfo> get(std::string userId, std::string deviceId) const;
std::optional<DeviceKeyInfo> findByEd25519Key(std::string userId, std::string ed25519Key) const;
std::optional<DeviceKeyInfo> findByCurve25519Key(std::string userId, std::string curve25519Key) const;
/// returns a list of users whose device list has changed
immer::flex_vector<std::string> diff(DeviceListTracker that) const;
};
template<class Archive>
void serialize(Archive &ar, DeviceListTracker &t, std::uint32_t const /*version*/)
{
ar
& t.usersToTrackDeviceLists
& t.deviceLists
;
}
}
BOOST_CLASS_VERSION(Kazv::DeviceKeyInfo, 0)
BOOST_CLASS_VERSION(Kazv::DeviceListTracker, 0)
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 82922e4..5c0da22 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -1,56 +1,58 @@
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
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/room/room-actions-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})
target_link_libraries(kazvtest
PRIVATE Catch2::Catch2WithMain
PRIVATE kazv
PRIVATE kazveventemitter
PRIVATE kazvjob
PRIVATE nlohmann_json::nlohmann_json
PRIVATE immer
PRIVATE lager
PRIVATE zug)
diff --git a/src/tests/base/immer-utils-test.cpp b/src/tests/base/immer-utils-test.cpp
new file mode 100644
index 0000000..0c23dbf
--- /dev/null
+++ b/src/tests/base/immer-utils-test.cpp
@@ -0,0 +1,79 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_predicate.hpp>
+#include <catch2/matchers/catch_matchers_contains.hpp>
+
+#include <string>
+
+#include <immer/map.hpp>
+#include <immer/flex_vector.hpp>
+
+#include <immer-utils.hpp>
+
+using namespace Kazv;
+
+using ComplexMapT = immer::map<std::string, immer::map<std::string, immer::flex_vector<std::string>>>;
+
+TEST_CASE("getIn()", "[base][immer-utils]")
+{
+ ComplexMapT complexMap{
+ {"a", {{"a/b", {"a/b/0", "a/b/1"}}}},
+ {"c", {{"c/d", {"c/d/0", "c/d/1"}}}},
+ };
+
+ REQUIRE(getIn(complexMap) == complexMap);
+ REQUIRE(getIn(complexMap, "a") == complexMap["a"]);
+ REQUIRE(getIn(complexMap, "a", "a/b") == complexMap["a"]["a/b"]);
+ REQUIRE(getIn(complexMap, "a", "a/b", 0) == complexMap["a"]["a/b"][0]);
+}
+
+TEST_CASE("setIn()", "[base][immer-utils]")
+{
+ ComplexMapT complexMap{
+ {"a", {{"a/b", {"a/b/0", "a/b/1"}}}},
+ {"c", {{"c/d", {"c/d/0", "c/d/1"}}}},
+ };
+
+ REQUIRE(setIn(complexMap, {}) == ComplexMapT{});
+ REQUIRE(setIn(complexMap, {}, "a") == ComplexMapT{
+ {"a", {}},
+ {"c", {{"c/d", {"c/d/0", "c/d/1"}}}},
+ });
+ REQUIRE(setIn(complexMap, {}, "a", "a/b") == ComplexMapT{
+ {"a", {{"a/b", {}}}},
+ {"c", {{"c/d", {"c/d/0", "c/d/1"}}}},
+ });
+ REQUIRE(setIn(complexMap, "test", "a", "a/b", 0) == ComplexMapT{
+ {"a", {{"a/b", {"test", "a/b/1"}}}},
+ {"c", {{"c/d", {"c/d/0", "c/d/1"}}}},
+ });
+}
+
+TEST_CASE("updateIn()", "[base][immer-utils]")
+{
+ ComplexMapT complexMap{
+ {"a", {{"a/b", {"a/b/0", "a/b/1"}}}},
+ {"c", {{"c/d", {"c/d/0", "c/d/1"}}}},
+ };
+
+ REQUIRE(updateIn(complexMap, [](auto) { return ComplexMapT{}; }) == ComplexMapT{});
+ REQUIRE(updateIn(complexMap, [](auto a) { return a.set("a/b2", {"a/b2/0", "a/b2/1"}); }, "a") == ComplexMapT{
+ {"a", {{"a/b", {"a/b/0", "a/b/1"}}, {"a/b2", {"a/b2/0", "a/b2/1"}}}},
+ {"c", {{"c/d", {"c/d/0", "c/d/1"}}}},
+ });
+ REQUIRE(updateIn(complexMap, [](auto a) { return a.set(0, "new"); }, "a", "a/b") == ComplexMapT{
+ {"a", {{"a/b", {"new", "a/b/1"}}}},
+ {"c", {{"c/d", {"c/d/0", "c/d/1"}}}},
+ });
+ REQUIRE(updateIn(complexMap, [](auto a) { return a + "test"; }, "a", "a/b", 0) == ComplexMapT{
+ {"a", {{"a/b", {"a/b/0test", "a/b/1"}}}},
+ {"c", {{"c/d", {"c/d/0", "c/d/1"}}}},
+ });
+}
diff --git a/src/tests/client/device-ops-test.cpp b/src/tests/client/device-ops-test.cpp
new file mode 100644
index 0000000..8bde721
--- /dev/null
+++ b/src/tests/client/device-ops-test.cpp
@@ -0,0 +1,79 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_predicate.hpp>
+#include <catch2/matchers/catch_matchers_contains.hpp>
+
+#include <boost/asio.hpp>
+#include <asio-promise-handler.hpp>
+#include <cursorutil.hpp>
+#include <sdk-model.hpp>
+#include <client/client.hpp>
+
+#include "client-test-util.hpp"
+
+static auto addDeviceToModel(ClientModel &model, std::string userId, std::string deviceId, std::string displayName)
+{
+ DeviceKeyInfo info{
+ deviceId,
+ deviceId + "ed25519",
+ deviceId + "curve25519",
+ displayName,
+ };
+ model.deviceLists.deviceLists = std::move(model.deviceLists.deviceLists)
+ .update(userId, [=](auto deviceMap) {
+ return std::move(deviceMap).set(deviceId, info);
+ });
+ return model;
+}
+
+static auto getModel()
+{
+ auto model = createTestClientModel();
+ addDeviceToModel(model, "@test1:test1.org", "device1", "test1-device1");
+ addDeviceToModel(model, "@test1:test1.org", "device2", "test1-device2");
+ return model;
+}
+
+using namespace Kazv::CursorOp;
+using namespace Catch::Matchers;
+
+TEST_CASE("devicesOfUser()", "[client][device-list-tracker]")
+{
+ boost::asio::io_context io;
+ AsioPromiseHandler ph{io.get_executor()};
+ auto model = getModel();
+ auto store = createTestClientStoreFrom(model, ph);
+ auto client = Client(store.reader().map([](auto c) { return SdkModel{c}; }), store,
+ std::nullopt);
+
+ auto devices = +client.devicesOfUser("@test1:test1.org");
+ REQUIRE(devices.size() == 2);
+ REQUIRE_THAT(devices, Contains(Predicate<DeviceKeyInfo>([](auto d) { return d.deviceId == "device1"; })));
+ REQUIRE_THAT(devices, Contains(Predicate<DeviceKeyInfo>([](auto d) { return d.deviceId == "device2"; })));
+}
+
+TEST_CASE("setTrustLevel()", "[client][device-list-tracker]")
+{
+ boost::asio::io_context io;
+ AsioPromiseHandler ph{io.get_executor()};
+ auto model = getModel();
+ auto store = createTestClientStoreFrom(model, ph);
+ auto client = Client(store.reader().map([](auto c) { return SdkModel{c}; }), store,
+ std::nullopt);
+
+ client.setDeviceTrustLevel("@test1:test1.org", "device1", Seen)
+ .then([client](auto s) {
+ REQUIRE(s.success());
+ REQUIRE_THAT((+client.devicesOfUser("@test1:test1.org")),
+ Contains(Predicate<DeviceKeyInfo>([](auto d) { return d.deviceId == "device1" && d.trustLevel == Seen; })));
+ });
+
+ io.run();
+}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Oct 12, 3:55 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1786356
Default Alt Text
(92 KB)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline
Log In to Comment