Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85711540
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
63 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/main.cpp b/src/main.cpp
index e100753..45c61b4 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,95 +1,94 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2021-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <immer/config.hpp> // https://github.com/arximboldi/immer/issues/168
#include <QApplication>
#include <QByteArray>
#include <QQmlApplicationEngine>
#include <QtQml>
#include <QUrl>
#include <QIcon>
#include <QCommandLineParser>
#include <QQuickStyle>
#include <KAboutData>
#if KAZV_LINK_BREEZE_ICONS
#include <BreezeIcons>
#endif
#include "meta-types.hpp"
#include "kazv-platform.hpp"
#include "kazv-path-config.hpp"
#include "kazv-version.hpp"
#include "kazv-log.hpp"
using namespace Qt::Literals::StringLiterals;
Q_DECL_EXPORT int main(int argc, char *argv[])
{
- QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QApplication app(argc, argv);
QCoreApplication::setOrganizationName(u"project-kazv"_s);
QCoreApplication::setOrganizationDomain(u"mxc.kazv.moe"_s);
QCoreApplication::setApplicationName(u"kazv"_s);
QGuiApplication::setDesktopFileName(u"moe.kazv.mxc.kazv.desktop"_s);
KAboutData aboutData;
aboutData
.setComponentName(u"kazv"_s)
.setVersion(QByteArray::fromStdString(kazvVersionString()))
.setOrganizationDomain(QByteArray("mxc.kazv.moe"))
.setDesktopFileName(u"moe.kazv.mxc.kazv"_s)
.setBugAddress(QByteArray("https://lily-is.land/kazv/kazv/-/issues"))
.setHomepage(u"https://kazv.chat"_s);
KAboutData::setApplicationData(aboutData);
#if KAZV_IS_WINDOWS
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
QQuickStyle::setStyle(QStringLiteral("org.kde.desktop"));
}
#endif
QQmlApplicationEngine engine;
#if KAZV_LINK_BREEZE_ICONS
BreezeIcons::initIcons();
#endif
QString iconThemeToSet;
#if KAZV_IS_WINDOWS
iconThemeToSet = QStringLiteral("breeze");
#endif
QCommandLineParser parser;
QCommandLineOption iconThemeOption(u"i"_s, u"Icon theme"_s, u"theme"_s);
parser.addOption(iconThemeOption);
parser.process(app);
if (!parser.value(iconThemeOption).isEmpty()) {
iconThemeToSet = parser.value(iconThemeOption);
}
KazvMetaTypeRegistration registration;
Q_UNUSED(registration);
#if KAZV_IS_WINDOWS
// On Windows the default search path is only in qrc
QStringList iconThemePaths;
iconThemePaths << (appDir() + QStringLiteral("/data/icons"));
QIcon::setThemeSearchPaths(iconThemePaths);
#endif
if (!iconThemeToSet.isEmpty()) {
QIcon::setThemeName(iconThemeToSet);
}
engine.loadFromModule(u"moe.kazv.mxc.kazvqml"_s, u"Main"_s);
if (engine.rootObjects().isEmpty()) {
return -1;
}
return app.exec();
}
diff --git a/src/matrix-sdk.cpp b/src/matrix-sdk.cpp
index f1350c6..7a69e48 100644
--- a/src/matrix-sdk.cpp
+++ b/src/matrix-sdk.cpp
@@ -1,210 +1,210 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <QtConcurrent>
#include <client/notification-handler.hpp>
#include <crypto/base64.hpp>
#include <zug/util.hpp>
#include <lager/event_loop/qt.hpp>
#include "matrix-sdk.hpp"
#include "matrix-promise.hpp"
#include "matrix-event.hpp"
#include "matrix-session-types.hpp"
#include "helper.hpp"
#include "kazv-path-config.hpp"
#include "kazv-version.hpp"
#include "qt-json.hpp"
#include "qt-rand-adapter.hpp"
#include "qt-job-handler.hpp"
#include "kazv-log.hpp"
#include "matrix-utils.hpp"
#include "matrix-session-controller.hpp"
#include "kazv-session-lock-guard.hpp"
#include "db-store.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
struct MatrixSdkPrivate
{
MatrixSdkPrivate(MatrixSdk *q)
: q(q)
, userDataDir(kazvUserDataDir().toStdString())
, thread(new QThread())
, controller(new MatrixSessionController(thread, userDataDir))
{
}
~MatrixSdkPrivate()
{
if (controller) {
controller->prepareDestroy();
controller->save();
}
thread->quit();
thread->wait();
thread->deleteLater();
}
MatrixSdk *q;
std::string userDataDir;
QThread *thread;
UniquePtrDL<MatrixSessionController> controller;
QTimer saveTimer;
void emplaceController()
{
if (controller) {
controller->prepareDestroy();
controller->save();
}
controller.reset(new MatrixSessionController(thread, userDataDir));
Q_EMIT q->sessionChanged();
QObject::connect(controller.get(), &MatrixSessionController::loadSessionFinished, q, &MatrixSdk::loadSessionFinished);
QObject::connect(controller.get(), &MatrixSessionController::sessionReady, q, &MatrixSdk::sessionChanged);
}
void runIoContext() {
thread->start();
}
void stopIoContext() {
thread->quit();
}
void maybeSerialize()
{
controller->save();
}
};
MatrixSdk::MatrixSdk(std::unique_ptr<MatrixSdkPrivate> d, QObject *parent)
: QObject(parent)
, m_d(std::move(d))
{
init();
}
void MatrixSdk::init()
{
connect(&m_d->saveTimer, &QTimer::timeout, &m_d->saveTimer, [m_d=m_d.get()]() {
m_d->maybeSerialize();
});
const int saveIntervalMs = 1000 * 60 * 5;
m_d->saveTimer.start(std::chrono::milliseconds{saveIntervalMs});
connect(this, &MatrixSdk::loadSessionFinished, this, &MatrixSdk::handleLoadSessionResult);
}
MatrixSdk::MatrixSdk(QObject *parent)
: MatrixSdk(std::make_unique<MatrixSdkPrivate>(
this
), parent)
{
}
MatrixSdk::~MatrixSdk()
{
}
QStringList MatrixSdk::allSessions() const
{
using StdPath = std::filesystem::path;
auto userDataDir = StdPath(m_d->userDataDir);
auto allSessionsDir = userDataDir / "sessions";
QStringList sessionNames;
try {
for (const auto &p : std::filesystem::directory_iterator(allSessionsDir)) {
if (p.is_directory()) {
auto maybeEncodedUserId = p.path().filename().string();
auto userId = decodeBase64(maybeEncodedUserId, Base64Opts::urlSafe);
if (userId.empty() || userId[0] != '@') {
continue;
}
for (const auto &q : std::filesystem::directory_iterator(p.path())) {
auto path = q.path();
auto deviceId = path.filename().string();
std::error_code err;
if (std::filesystem::exists(path / "store", err)) {
sessionNames.append(QString::fromStdString(userId + "/" + deviceId));
}
}
}
}
} catch (const std::filesystem::filesystem_error &) {
qDebug() << "sessionDir not available, ignoring";
}
return sessionNames;
}
void MatrixSdk::serializeToFile() const
{
m_d->maybeSerialize();
}
void MatrixSdk::loadSession(QString sessionName)
{
m_d->emplaceController();
m_d->controller->load(sessionName);
}
bool MatrixSdk::deleteSession(QString sessionName) {
using StdPath = std::filesystem::path;
qDebug() << "in deleteSession(), sessionName=" << sessionName;
auto userDataDir = StdPath(m_d->userDataDir);
auto parts = sessionName.split(u'/');
if (parts.size() == 2) {
auto userId = parts[0].toStdString();
auto deviceId = parts[1].toStdString();
auto sessionDir = sessionDirForUserAndDeviceId(userDataDir, userId, deviceId);
if (std::filesystem::exists(sessionDir)) {
qCDebug(kazvLog) << "new path works";
return std::filesystem::remove_all(sessionDir);
}
qCDebug(kazvLog) << "trying legacy path";
auto legacySessionDir = userDataDir / "sessions" / userId / deviceId;
if (std::filesystem::exists(legacySessionDir)) {
qCDebug(kazvLog) << "legacy path works";
return std::filesystem::remove_all(legacySessionDir);
}
}
qDebug(kazvLog) << "no session found for" << sessionName;
return false;
}
bool MatrixSdk::startNewSession()
{
m_d->emplaceController();
m_d->controller->create();
return true;
}
void MatrixSdk::startThread()
{
m_d->runIoContext();
}
-void MatrixSdk::handleLoadSessionResult(QString sessionName, Constants::LoadSessionResult result)
+void MatrixSdk::handleLoadSessionResult([[maybe_unused]] QString sessionName, Constants::LoadSessionResult result)
{
if (result == Constants::SessionLoadSuccess) {
m_d->controller->startSyncing();
}
}
void MatrixSdk::setUserDataDir(const std::string &userDataDir)
{
m_d->userDataDir = userDataDir;
}
MatrixSession *MatrixSdk::session() const
{
return m_d->controller->session();
}
diff --git a/src/matrix-session.cpp b/src/matrix-session.cpp
index 526a465..ec6f859 100644
--- a/src/matrix-session.cpp
+++ b/src/matrix-session.cpp
@@ -1,597 +1,599 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "matrix-session.hpp"
#include "matrix-utils.hpp"
#include "matrix-room-list.hpp"
#include "matrix-promise.hpp"
#include "matrix-event.hpp"
#include "device-mgmt/matrix-device-list.hpp"
#include "matrix-sticker-pack-list.hpp"
#include "matrix-sticker-pack-list-p.hpp"
#include "matrix-user-given-attrs-map.hpp"
#include "matrix-verification-list.hpp"
#include "sso-login-process.hpp"
#include "db-store.hpp"
#include "helper.hpp"
#include "kazv-log.hpp"
#include <csapi/login.hpp>
#include <csapi/directory.hpp>
#include <client/alias.hpp>
#include <QFile>
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
static const std::string clientName = "kazv";
MatrixSession::MatrixSession(
MatrixSessionContextT context,
Client client,
LagerStoreEventEmitter::Watchable watchable,
lager::reader<VerificationTrackerModel> verificationTrackerState,
std::function<DbStore &()> dbStoreGetter,
QObject *parent
)
: QObject(parent)
, m_context(std::move(context))
, m_clientOnSecondaryRoot(std::move(client))
, m_watchable(std::move(watchable))
, m_notificationHandler(m_clientOnSecondaryRoot.notificationHandler())
, m_verificationTrackerState(std::move(verificationTrackerState))
, m_ssoLoginProcess()
, m_dbStoreGetter(std::move(dbStoreGetter))
, LAGER_QT(serverUrl)(m_clientOnSecondaryRoot.serverUrl().xform(strToQt))
, LAGER_QT(userId)(m_clientOnSecondaryRoot.userId().xform(strToQt))
, LAGER_QT(token)(m_clientOnSecondaryRoot.token().xform(strToQt))
, LAGER_QT(deviceId)(m_clientOnSecondaryRoot.deviceId().xform(strToQt))
, LAGER_QT(specVersions)(m_clientOnSecondaryRoot.supportVersions())
{
m_watchable.afterAll(
- [this](KazvEvent e) {
+ [this](KazvTrigger e) {
Q_EMIT this->trigger(e);
});
m_watchable.after<LoginSuccessful>(
[this](LoginSuccessful e) {
Q_EMIT this->loginSuccessful(e);
});
m_watchable.after<LoginFailed>(
[this](LoginFailed e) {
Q_EMIT this->loginFailed(
QString::fromStdString(e.errorCode),
QString::fromStdString(e.error)
);
});
m_watchable.after<ReceivingRoomTimelineEvent>(
[this](ReceivingRoomTimelineEvent e) {
Q_EMIT this->receivedMessage(
QString::fromStdString(e.roomId),
QString::fromStdString(e.event.id())
);
});
}
MatrixSession::~MatrixSession() = default;
QString MatrixSession::mxcUriToHttp(QString mxcUri) const
{
return QString::fromStdString(m_clientOnSecondaryRoot.mxcUriToHttp(mxcUri.toStdString()));
}
QString MatrixSession::mxcUriToHttpAuthenticatedV1(QString mxcUri) const
{
return QString::fromStdString(m_clientOnSecondaryRoot.mxcUriToHttpV1(mxcUri.toStdString()));
}
MatrixDeviceList *MatrixSession::devicesOfUser(QString userId) const
{
return new MatrixDeviceList(m_clientOnSecondaryRoot.devicesOfUser(userId.toStdString()));
}
bool isIllFormatSpecVersion(const QString &version)
{
const bool isLegacy = version.startsWith(u"r"_s);
if (isLegacy && version.split(u'.').size() == 3) {
return false;
}
if (!isLegacy && version.split(u'.').size() == 2) {
return false;
}
return true;
}
// Return true if v1 is at least as new as v2, false if v1 is older than v2
// Return false if v1 or v2 is ill-format
bool compareSpecVersion(QString v1, QString v2)
{
// Check parameters format
if (isIllFormatSpecVersion(v1) || isIllFormatSpecVersion(v2)) {
return false;
}
const bool v1IsLegacy = v1.startsWith(u"r"_s);
const bool v2IsLegacy = v2.startsWith(u"r"_s);
if (v1IsLegacy != v2IsLegacy) {
return v2IsLegacy;
}
v1.remove(0, 1);
v2.remove(0, 1);
auto v1VersionNumbers = v1.split(u'.');
auto v2VersionNumbers = v2.split(u'.');
for (int i = 0; i < v1VersionNumbers.size(); i++) {
auto v1VerNum = v1VersionNumbers[i].toInt();
auto v2VerNum = v2VersionNumbers[i].toInt();
if (v1VerNum != v2VerNum) {
return v1VerNum > v2VerNum;
}
}
// v1 is equal to v2
return true;
}
// Return true if version in the range [minVer, maxVer]
// Return false if any parameter is ill-format
bool compareSpecVersionRange(const QString &version,
const QString &minVer, const QString &maxVer)
{
if (isIllFormatSpecVersion(version)
|| isIllFormatSpecVersion(minVer)
|| isIllFormatSpecVersion(maxVer)) {
return false;
}
if (compareSpecVersion(version, minVer) && compareSpecVersion(maxVer, version)) {
return true;
}
return false;
}
bool MatrixSession::checkSpecVersion(QString version) const
{
return std::find_if(specVersions().begin(), specVersions().end(),
[&version](auto v) {
return compareSpecVersion(QString::fromStdString(v), version);
}) != specVersions().end();
}
bool MatrixSession::checkSpecVersionRange(QString minVer, QString maxVer) const
{
return std::find_if(specVersions().begin(), specVersions().end(),
[&minVer, maxVer](auto v) {
return compareSpecVersionRange(QString::fromStdString(v), minVer, maxVer);
}) != specVersions().end();
}
QStringList MatrixSession::directRoomIds(QString userId) const
{
auto content = m_clientOnSecondaryRoot.accountData().get()["m.direct"].content().get();
auto roomIds = QStringList{};
for (auto i : content[userId.toStdString()]) {
roomIds.push_back(QString::fromStdString(i.get<std::string>()));
}
return roomIds;
}
MatrixVerificationList *MatrixSession::verificationList() const
{
return new MatrixVerificationList(m_clientOnSecondaryRoot, m_verificationTrackerState);
}
std::string MatrixSession::validateHomeserverUrl(const QString &url)
{
if (url.isEmpty()) {
return std::string();
}
auto u = QUrl::fromUserInput(url);
if (!u.isValid()) {
return std::string();
}
if (u.scheme() == u"http"_s) {
qCInfo(kazvLog) << "url" << u << "is http. Force switching to https.";
u.setScheme(u"https"_s);
} else if (u.scheme() != u"https"_s) {
qCWarning(kazvLog) << "url" << u << "is not http/https.";
return std::string();
}
return u.toString().toStdString();
}
void MatrixSession::login(const QString &userId, const QString &password, const QString &homeserverUrl)
{
auto loginFunc = [userId, password](const Client &client, const std::string &serverUrl) {
client.passwordLogin(
serverUrl,
userId.toStdString(),
password.toStdString(),
- clientName
+ clientName,
+ /* startSyncingOnSuccess = */ true
);
};
auto validated = validateHomeserverUrl(homeserverUrl);
if (!validated.empty()) {
loginFunc(m_clientOnSecondaryRoot, validated);
} else {
m_clientOnSecondaryRoot
.autoDiscover(userId.toStdString()) // autoDiscover() will dispatch GetVersionAction to get supported versions of the server
.then([
this,
client=m_clientOnSecondaryRoot.toEventLoop(),
userId,
password,
loginFunc
](auto res) {
if (!res.success()) {
// FIXME use real error codes and msgs when available in libkazv
Q_EMIT this->discoverFailed(u""_s, u""_s);
return res;
}
auto serverUrl = res.dataStr("homeserverUrl");
loginFunc(client, serverUrl);
return res;
});
}
}
void MatrixSession::discoverAndGetLoginFlows(const QString &userId, const QString &homeserverUrl)
{
auto validated = validateHomeserverUrl(homeserverUrl);
if (!validated.empty()) {
getLoginFlows(homeserverUrl);
} else {
m_clientOnSecondaryRoot
.autoDiscover(userId.toStdString())
.then([
this
](const EffectStatus &res) {
if (!res.success()) {
Q_EMIT discoverFailed(
QString::fromStdString(res.dataStr("errorCode")),
QString::fromStdString(res.dataStr("error"))
);
return;
}
auto serverUrl = QString::fromStdString(res.dataStr("homeserverUrl"));
Q_EMIT discoverSuccessful(serverUrl);
getLoginFlows(serverUrl);
});
}
}
void MatrixSession::getLoginFlows(const QString &serverUrl)
{
lager::get<JobInterface &>(m_context).submit(
Api::GetLoginFlowsJob(serverUrl.toStdString()),
[this](Api::GetLoginFlowsResponse r) {
if (!r.success()) {
Q_EMIT getLoginFlowsFailed(
QString::fromStdString(r.errorCode()),
QString::fromStdString(r.errorMessage())
);
return;
}
auto v = std::move(r).jsonBody().get().at("flows").template get<QJsonValue>();
Q_EMIT getLoginFlowsSuccessful(v);
}
);
}
QUrl MatrixSession::ssoLoginStart(const QString &homeserverUrl)
{
if (m_ssoLoginProcess) {
m_ssoLoginProcess.reset();
}
m_ssoLoginProcess.reset(new SsoLoginProcess());
if (!m_ssoLoginProcess->startServer()) {
return QUrl();
}
auto link = m_ssoLoginProcess->getSsoLink(homeserverUrl);
connect(m_ssoLoginProcess.get(), &SsoLoginProcess::loginTokenAvailable,
this, [this, homeserverUrl](const QString &loginToken) {
Q_EMIT ssoLoginTokenAvailable();
qCInfo(kazvLog) << "Got SSO login token";
m_ssoLoginProcess.reset();
m_clientOnSecondaryRoot.mLoginTokenLogin(
homeserverUrl.toStdString(),
loginToken.toStdString(),
- clientName
+ clientName,
+ /* startSyncingOnSuccess = */ true
);
});
return link;
}
void MatrixSession::logout()
{
m_clientOnSecondaryRoot.logout()
.then([&] (EffectStatus stat) {
if (stat.success()) {
Q_EMIT this->logoutSuccessful();
} else {
Q_EMIT this->logoutFailed(QString::fromStdString(stat.dataStr("errorCode")), QString::fromStdString(stat.dataStr("error")));
}
});
}
MatrixRoomList *MatrixSession::roomList() const
{
return new MatrixRoomList(m_clientOnSecondaryRoot);
}
static std::optional<std::string> optMaybe(QString s)
{
if (s.isEmpty()) {
return std::nullopt;
} else {
return s.toStdString();
}
}
MatrixPromise *MatrixSession::createRoom(
bool isPrivate,
const QString &name,
const QString &alias,
const QStringList &invite,
bool isDirect,
bool allowFederate,
const QString &topic,
const QJsonValue &powerLevelContentOverride,
Constants::CreateRoomPreset preset,
bool encrypted
)
{
immer::array<Event> initialState;
if (encrypted) {
initialState = {Event{json{
{"type", "m.room.encryption"},
{"state_key", ""},
{"content", {
{"algorithm", "m.megolm.v1.aes-sha2"},
}},
}}};
}
return new MatrixPromise(m_clientOnSecondaryRoot.createRoom(
isPrivate ? Kazv::RoomVisibility::Private : Kazv::RoomVisibility::Public,
optMaybe(name),
optMaybe(alias),
qStringListToStdF(invite),
isDirect,
allowFederate,
optMaybe(topic),
nlohmann::json(powerLevelContentOverride),
static_cast<Kazv::CreateRoomPreset>(preset),
initialState
));
}
MatrixPromise *MatrixSession::joinRoom(const QString &idOrAlias, const QStringList &servers)
{
return new MatrixPromise(m_clientOnSecondaryRoot.joinRoom(
idOrAlias.toStdString(),
qStringListToStdF(servers)
));
}
MatrixPromise *MatrixSession::setDeviceTrustLevel(QString userId, QString deviceId, QString trustLevel)
{
return new MatrixPromise(
m_clientOnSecondaryRoot.setDeviceTrustLevel(
userId.toStdString(),
deviceId.toStdString(),
qStringToTrustLevelFunc(trustLevel)
)
);
}
MatrixPromise *MatrixSession::getSelfProfile()
{
return new MatrixPromise(
m_clientOnSecondaryRoot.getProfile(userId().toStdString())
);
}
MatrixPromise *MatrixSession::setDisplayName(QString displayName)
{
return new MatrixPromise(
m_clientOnSecondaryRoot.setDisplayName(
displayName.isEmpty() ? std::nullopt : std::optional<std::string>(displayName.toStdString())
)
);
}
MatrixPromise *MatrixSession::setAvatarUrl(QString avatarUrl)
{
return new MatrixPromise(
m_clientOnSecondaryRoot.setAvatarUrl(
avatarUrl.isEmpty() ? std::nullopt : std::optional<std::string>(avatarUrl.toStdString())
)
);
}
bool MatrixSession::shouldNotify(MatrixEvent *event) const
{
// Do not notify own event
if (event->sender() == userId()) {
return false;
}
return m_notificationHandler.handleNotification(event->underlyingEvent()).shouldNotify;
}
bool MatrixSession::shouldPlaySound(MatrixEvent *event) const
{
return m_notificationHandler.handleNotification(event->underlyingEvent()).sound.has_value();
}
MatrixStickerPackList *MatrixSession::stickerPackList() const
{
return new MatrixStickerPackList(m_clientOnSecondaryRoot);
}
MatrixEvent *MatrixSession::stickerRoomsEvent() const
{
return new MatrixEvent(m_clientOnSecondaryRoot.accountData()[imagePackRoomsEventType][lager::lenses::or_default]);
}
MatrixPromise *MatrixSession::updateStickerPack(MatrixStickerPackSource source)
{
if (source.source == MatrixStickerPackSource::AccountData) {
auto eventJson = std::move(source.event).raw().get();
eventJson["type"] = source.eventType;
return sendAccountDataImpl(Event(std::move(eventJson)));
} else if (source.source == MatrixStickerPackSource::RoomState) {
auto eventJson = std::move(source.event).raw().get();
eventJson["type"] = source.eventType;
eventJson["state_key"] = source.stateKey;
return new MatrixPromise(
m_clientOnSecondaryRoot
.room(source.roomId)
.sendStateEvent(Event(std::move(eventJson))));
} else {
return 0;
}
}
MatrixUserGivenAttrsMap *MatrixSession::userGivenNicknameMap() const
{
return new MatrixUserGivenAttrsMap(
userGivenNicknameMapFor(m_clientOnSecondaryRoot),
[client=m_clientOnSecondaryRoot](json content) {
return client.setAccountData(json{
{"type", USER_GIVEN_NICKNAME_EVENT_TYPES[0]},
{"content", std::move(content)},
});
}
);
}
MatrixPromise *MatrixSession::sendAccountData(const QString &type, const QJsonObject &content)
{
Event e = json{
{"type", type.toStdString()},
{"content", content},
};
return sendAccountDataImpl(std::move(e));
}
MatrixPromise *MatrixSession::sendAccountDataImpl(Event event)
{
return new MatrixPromise(m_clientOnSecondaryRoot.setAccountData(event));
}
MatrixPromise *MatrixSession::getSpecVersions()
{
return new MatrixPromise(m_clientOnSecondaryRoot.getVersions(LAGER_QT(serverUrl).get().toStdString()));
}
MatrixPromise *MatrixSession::addDirectRoom(const QString &userId, const QString &roomId)
{
return new MatrixPromise(m_clientOnSecondaryRoot.addDirectRoom(userId.toStdString(), roomId.toStdString()));
}
MatrixPromise *MatrixSession::getRoomIdByAlias(const QString &roomAlias)
{
auto job = m_clientOnSecondaryRoot
.getRoomIdByAliasJob(roomAlias.toStdString());
return new MatrixPromise(
m_context.createPromise([ctx=m_context, job](auto resolve) {
lager::get<JobInterface &>(ctx).submit(job, [resolve](GetRoomIdByAliasResponse r) {
resolve(parseGetRoomIdByAliasResponse(r));
});
}));
}
inline constexpr std::size_t purgeEventsKeepNumber = 5;
MatrixPromise *MatrixSession::purgeEventsExceptRooms(const QStringList &roomIds)
{
return new MatrixPromise(
m_context.createResolvedPromise({})
.then([client=m_clientOnSecondaryRoot.toEventLoop(), roomIds=qStringListToStdF(roomIds)
]([[maybe_unused]] auto &&stat) {
auto map = intoImmer(immer::map<std::string, std::size_t>{},
zug::filter([&roomIds](const std::string &roomId) {
return std::find(roomIds.begin(), roomIds.end(), roomId) == roomIds.end();
})
| zug::map([](const std::string &roomId) {
return std::make_pair(roomId, purgeEventsKeepNumber);
}),
client.roomIds().make().get()
);
return client.purgeRoomEvents(map);
}).then([](const auto &stat) {
qCDebug(kazvLog) << "Purge room events stat:" << !!stat;
return stat;
})
);
}
MatrixPromise *MatrixSession::backfillRoomFromEvent(const QString &roomId, const QString &eventId)
{
return new MatrixPromise(
m_context.createPromise([
dbStoreGetter=m_dbStoreGetter,
client=m_clientOnSecondaryRoot.toEventLoop(),
roomId,
eventId
](auto resolve) {
dbStoreGetter().getEventsBefore(roomId, eventId).then([client, resolve, roomId](auto &&res) {
auto [timelineEvents, relatedEvents] = std::move(res);
auto loadedCount = timelineEvents[roomId.toStdString()].size();
client.loadEventsFromStorage(timelineEvents, relatedEvents)
.then([resolve, loadedCount](auto &&) {
resolve(EffectStatus(
!!loadedCount,
json{{"loadedCount", loadedCount}}
));
});
});
})
);
}
MatrixPromise *MatrixSession::importFromKeyBackupFile(QUrl fileUrl, QString password)
{
if (!fileUrl.isLocalFile()) {
return new MatrixPromise(m_context.createResolvedPromise({
/* succ = */ false,
json{{"error", "Not a local file"}, {"errorCode", "NOT_LOCAL_FILE"}},
}));
}
auto filename = fileUrl.toLocalFile();
auto f = QFile(filename);
if (!f.open(QFile::ReadOnly)) {
return new MatrixPromise(m_context.createResolvedPromise({
/* succ = */ false,
json{{"error", "File open failed"}, {"errorCode", "FILE_OPEN_FAILED"}},
}));
}
auto ba = f.readAll();
auto content = std::string(ba.begin(), ba.end());
auto passwordStd = std::move(password).toStdString();
return new MatrixPromise(m_clientOnSecondaryRoot.importFromKeyBackupFile(
std::move(content), std::move(passwordStd)
));
}
MatrixPromise *MatrixSession::requestVerifyDevice(QString userId, QString deviceId)
{
return new MatrixPromise(m_clientOnSecondaryRoot.requestOutgoingToDeviceVerification(
std::move(userId).toStdString(),
std::move(deviceId).toStdString()
));
}
diff --git a/src/matrix-session.hpp b/src/matrix-session.hpp
index 21c0c89..69fb4fb 100644
--- a/src/matrix-session.hpp
+++ b/src/matrix-session.hpp
@@ -1,301 +1,301 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include "helper.hpp"
#include "meta-types.hpp"
#include "matrix-session-types.hpp"
#include "constants.hpp"
#include <client.hpp>
#include <notification-handler.hpp>
#include <lagerstoreeventemitter.hpp>
#include <lager/extra/qt.hpp>
#include <QObject>
#include <QQmlEngine>
Q_MOC_INCLUDE("matrix-room-list.hpp")
Q_MOC_INCLUDE("matrix-device-list.hpp")
Q_MOC_INCLUDE("matrix-promise.hpp")
Q_MOC_INCLUDE("matrix-event.hpp")
Q_MOC_INCLUDE("matrix-sticker-pack-list.hpp")
Q_MOC_INCLUDE("matrix-user-given-attrs-map.hpp")
Q_MOC_INCLUDE("matrix-verification-list.hpp")
class MatrixRoomList;
class MatrixDeviceList;
class MatrixPromise;
class MatrixEvent;
class MatrixStickerPackList;
class MatrixUserGivenAttrsMap;
class MatrixVerificationList;
class SsoLoginProcess;
class DbStore;
/**
* Represent all operations that can be taken within a Matrix session.
*/
class MatrixSession : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
MatrixSessionContextT m_context;
Kazv::Client m_clientOnSecondaryRoot;
Kazv::LagerStoreEventEmitter::Watchable m_watchable;
Kazv::NotificationHandler m_notificationHandler;
lager::reader<Kazv::VerificationTrackerModel> m_verificationTrackerState;
UniquePtrDL<SsoLoginProcess> m_ssoLoginProcess;
std::function<DbStore &()> m_dbStoreGetter;
public:
MatrixSession(
MatrixSessionContextT context,
Kazv::Client client,
Kazv::LagerStoreEventEmitter::Watchable watchable,
lager::reader<Kazv::VerificationTrackerModel> verificationTrackerState,
std::function<DbStore &()> dbStoreGetter,
QObject *parent = nullptr
);
~MatrixSession() override;
static std::string validateHomeserverUrl(const QString &url);
LAGER_QT_READER(QString, serverUrl);
LAGER_QT_READER(QString, userId);
LAGER_QT_READER(QString, token);
LAGER_QT_READER(QString, deviceId);
LAGER_QT_READER(immer::array<std::string>, specVersions); // The versions of the Matrix Spec supported by the server.
Q_INVOKABLE MatrixRoomList *roomList() const;
Q_INVOKABLE QString mxcUriToHttp(QString mxcUri) const;
Q_INVOKABLE QString mxcUriToHttpAuthenticatedV1(QString mxcUri) const;
Q_INVOKABLE MatrixDeviceList *devicesOfUser(QString userId) const;
// Return true if version is at least as new as the spec version of server
Q_INVOKABLE bool checkSpecVersion(QString version) const;
// Return true if the spec version of server in the range [minVer, maxVer]
Q_INVOKABLE bool checkSpecVersionRange(QString minVer, QString maxVer) const;
Q_INVOKABLE QStringList directRoomIds(QString userId) const;
Q_INVOKABLE MatrixVerificationList *verificationList() const;
Q_SIGNALS:
- void trigger(Kazv::KazvEvent e);
+ void trigger(Kazv::KazvTrigger e);
- void loginSuccessful(Kazv::KazvEvent e);
+ void loginSuccessful(Kazv::KazvTrigger e);
void loginFailed(QString errorCode, QString errorMsg);
void discoverFailed(QString errorCode, QString errorMsg);
void discoverSuccessful(QString serverUrl);
void getLoginFlowsFailed(QString errorCode, QString errorMsg);
void getLoginFlowsSuccessful(QJsonValue flows);
void ssoLoginTokenAvailable();
void logoutSuccessful();
void logoutFailed(QString errorCode, QString errorMsg);
void receivedMessage(QString roomId, QString eventId);
public Q_SLOTS:
void login(const QString &userId, const QString &password, const QString &homeserverUrl);
/**
* Auto-discover the server url and then get the login flows from the server.
*
* If homeserverUrl is not provided, try to get it from auto-discovery.
* If discovery is successful, or it is already provided, get the login flows
* by calling getLoginFlows.
*/
void discoverAndGetLoginFlows(const QString &userId, const QString &homeserverUrl);
void getLoginFlows(const QString &serverUrl);
/**
* Start SSO login flow.
*
* It will start an http server on a local port and pass it as
* the redirect url to the SSO login link.
*
* When the user completes the SSO login flow, we get the login token
* for us to login via the token flow.
*
* @return The SSO login link for the user to open.
*/
QUrl ssoLoginStart(const QString &homeserverUrl);
void logout();
/**
* Create a new room.
*
* @param isPrivate Whether the room is private.
* @param name The room's name.
* @param alias The alias of the room.
* @param invite List of matrix ids of users to invite.
* @param isDirect Whether it is a direct message room.
* @param allowFederate Whether to allow users on other servers to join.
* @param topic The topic of the room.
* @param powerLevelContentOverride The content to override m.room.power_levels event.
* @param preset The preset to create the room with.
* @param encrypted Whether to enable encryption for this room.
*/
MatrixPromise *createRoom(
bool isPrivate,
const QString &name,
const QString &alias,
const QStringList &invite,
bool isDirect,
bool allowFederate,
const QString &topic,
const QJsonValue &powerLevelContentOverride,
Constants::CreateRoomPreset preset,
bool encrypted
);
/**
* Join a room.
* @param idOrAlias The id or alias of the room to join.
* @param servers The servers to use when joining the room.
*/
MatrixPromise *joinRoom(
const QString &idOrAlias,
const QStringList &servers
);
/**
* Change the trust level of a device.
*
* @param userId The user id that owns the device.
* @param deviceId The device id to set the trust level.
* @param trustLevel The trust level.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *setDeviceTrustLevel(QString userId, QString deviceId, QString trustLevel);
/**
* Get the profile of the current user.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *getSelfProfile();
/**
* Set the display name of the current user.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *setDisplayName(QString displayName);
/**
* Set the avatar url of the current user.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *setAvatarUrl(QString avatarUrl);
/**
* Check if an event should be notified.
*
* @param event The event to check.
* @return Whether `event` should be notified.
*/
bool shouldNotify(MatrixEvent *event) const;
/**
* Check if an event should be notified with sound.
*
* You should only call this method when `shouldNotify(event)`
* returns true.
*
* @param event The event to check.
* @return Whether `event` should be notified with sound.
*/
bool shouldPlaySound(MatrixEvent *event) const;
/**
* Get the sticker pack list for the current account.
*
* @return A list of sticker packs associated with the current account.
*/
MatrixStickerPackList *stickerPackList() const;
/**
* Get the sticker rooms account data event for the current account.
*
* @return A MatrixEvent representing the sticker rooms account data event.
*/
MatrixEvent *stickerRoomsEvent() const;
/**
* Update the sticker pack from source.
*
* @param source The source of the sticker pack to update.
* @return A promise that resolves when the sticker pack is updated,
* or when there is an error.
*/
MatrixPromise *updateStickerPack(MatrixStickerPackSource source);
MatrixUserGivenAttrsMap *userGivenNicknameMap() const;
MatrixPromise *sendAccountData(const QString &type, const QJsonObject &content);
/**
* Get all Matrix Spec versions supported by the server.
* Use MatrixSdk::supportSpecVersion() to check if a version is supported.
*/
MatrixPromise *getSpecVersions();
MatrixPromise *addDirectRoom(const QString &userId, const QString &roomId);
MatrixPromise *getRoomIdByAlias(const QString &roomAlias);
/**
* Purge events in all rooms except those specified in roomIds
*
* @param roomIds The ids for rooms to NOT be purged. This allows
* keeping the rooms that are already open intact.
* @return A MatrixPromise that resolves when the events are purged.
*/
MatrixPromise *purgeEventsExceptRooms(const QStringList &roomIds);
/**
* Backfill from the event storage from an existing event in a room.
*
* @param roomId The id of the room to operate on.
* @param eventId The id of the event to backfill from.
* @return A MatrixPromise that resolves when the events are backfilled.
* The Promise is considered successful if and only if at least one event
* is loaded.
*/
MatrixPromise *backfillRoomFromEvent(const QString &roomId, const QString &eventId);
/**
* Import from key backup file.
*
* @param fileUrl The url of the key backup file. Will be passed to
* QUrl::toLocalFile(), and must be a local file.
* @param password The password to decrypt the key backup file.
* @return A MatrixPromise that resolves when the keys are imported.
* If successful, `data["imported"]` contains the number of imported keys.
* If unsuccessful, `data` contains a standard error structure.
*/
MatrixPromise *importFromKeyBackupFile(QUrl fileUrl, QString password);
/**
* Send an outbound device verification request.
*
* @param userId The user id of the device to verify.
* @param deviceId The device id of the device to verify.
*/
MatrixPromise *requestVerifyDevice(QString userId, QString deviceId);
private:
MatrixPromise *sendAccountDataImpl(Kazv::Event event);
};
diff --git a/src/meta-types.cpp b/src/meta-types.cpp
index 5dbd0a3..79e30d2 100644
--- a/src/meta-types.cpp
+++ b/src/meta-types.cpp
@@ -1,16 +1,16 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "meta-types.hpp"
#include "kazv-platform.hpp"
KazvMetaTypeRegistration::KazvMetaTypeRegistration()
- : m_kazvEvent(qRegisterMetaType<Kazv::KazvEvent>())
+ : m_kazvTrigger(qRegisterMetaType<Kazv::KazvTrigger>())
, m_matrixStickerPackSource(qRegisterMetaType<MatrixStickerPackSource>())
{
}
diff --git a/src/meta-types.hpp b/src/meta-types.hpp
index adf40d2..9e33426 100644
--- a/src/meta-types.hpp
+++ b/src/meta-types.hpp
@@ -1,26 +1,26 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include <QMetaType>
-#include <base/kazvevents.hpp>
+#include <base/kazv-triggers.hpp>
#include "matrix-sticker-pack-source.hpp"
#include "kazv-platform.hpp"
-Q_DECLARE_METATYPE(Kazv::KazvEvent)
+Q_DECLARE_METATYPE(Kazv::KazvTrigger)
Q_DECLARE_METATYPE(MatrixStickerPackSource)
class KazvMetaTypeRegistration
{
public:
KazvMetaTypeRegistration();
- int m_kazvEvent;
+ int m_kazvTrigger;
int m_matrixStickerPackSource;
};
diff --git a/src/tests/kazv-file-test.cpp b/src/tests/kazv-file-test.cpp
index d4174f0..06f7a4c 100644
--- a/src/tests/kazv-file-test.cpp
+++ b/src/tests/kazv-file-test.cpp
@@ -1,59 +1,63 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "kazv-file.hpp"
#include "qt-rand-adapter.hpp"
#include <client/random-generator.hpp>
#include <crypto/aes-256-ctr.hpp>
#include <QObject>
#include <QtTest>
#include <QTemporaryDir>
class KazvFileTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testKazvFile();
};
using namespace Kazv;
void KazvFileTest::testKazvFile()
{
QTemporaryDir dir{};
dir.isValid();
auto rawContent = QByteArrayLiteral("Some test content");
QSaveFile rawFile{dir.filePath(QStringLiteral("rawFile"))};
- rawFile.open(QIODevice::WriteOnly);
+ auto openResult = rawFile.open(QIODevice::WriteOnly);
+ QVERIFY(openResult);
rawFile.write(rawContent.data(), rawContent.size());
rawFile.commit();
Kazv::RandomInterface randomGenerator = QtRandAdapter{};
auto aes = AES256CTRDesc::fromRandom(
randomGenerator.generateRange<Kazv::RandomData>(
Kazv::AES256CTRDesc::randomSize));
KazvFile kazvFile{rawFile.fileName(), aes};
- kazvFile.open(QIODevice::ReadOnly);
+ openResult = kazvFile.open(QIODevice::ReadOnly);
+ QVERIFY(openResult);
QByteArray encryptedContent = kazvFile.readAll();
kazvFile.close();
KazvSaveFile kazvSaveFile{
dir.filePath(QStringLiteral("decryptedFile")), aes};
- kazvSaveFile.open(QIODevice::WriteOnly);
+ openResult = kazvSaveFile.open(QIODevice::WriteOnly);
+ QVERIFY(openResult);
kazvSaveFile.write(encryptedContent.data(),
encryptedContent.size());
kazvSaveFile.commit();
QFile file{kazvSaveFile.fileName()};
- file.open(QIODevice::ReadOnly);
+ openResult = file.open(QIODevice::ReadOnly);
+ QVERIFY(openResult);
auto decryptedContent = file.readAll();
QCOMPARE(decryptedContent, rawContent);
}
QTEST_MAIN(KazvFileTest)
#include "kazv-file-test.moc"
diff --git a/src/tests/kazv-io-job-test.cpp b/src/tests/kazv-io-job-test.cpp
index 40c891c..1f077c5 100644
--- a/src/tests/kazv-io-job-test.cpp
+++ b/src/tests/kazv-io-job-test.cpp
@@ -1,418 +1,441 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2025 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <kazv-io-job.hpp>
#include <QObject>
#include <QtTest>
#include <QHttpServer>
#include <QTcpServer>
#include <QThread>
#include <QTemporaryFile>
#include <QCryptographicHash>
#include <QString>
#include <QHttpServerResponse>
#include <QHttpServerRequest>
#include <QSignalSpy>
#include <QJsonObject>
#include <QtGlobal>
using namespace Qt::Literals::StringLiterals;
class KazvIOJobTest : public QObject
{
Q_OBJECT
private:
QHttpServer httpServer;
QTcpServer tcpServer; // Required by QHttpServer
QThread serverThread;
quint16 port;
QTemporaryFile downloadFile;
QTemporaryFile uploadFile;
QCryptographicHash downloadFileHash{QCryptographicHash::Sha256};
QString hashStr;
const QString downloadEndpoint =
u"/_matrix/client/v1/media/download/serverName/download"_s;
const QString downloadAuthEndpoint =
u"/_matrix/client/v1/media/download/serverName/auth"_s;
const QString downloadPauseEndpoint =
u"/_matrix/client/v1/media/download/serverName/pause"_s;
const QString downloadCancelEndpoint =
u"/_matrix/client/v1/media/download/serverName/cancel"_s;
const QString uploadEndpoint = u"/_matrix/media/v3/upload"_s;
const QString token = u"token"_s;
const char *downloadFileContent = "download";
const char *uploadFileContent = "upload";
const char *responseErrorContent = "ResponseError";
QString serverUrl;
bool hasAuth{false};
private Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void testDownload();
void testDownloadAuth();
void testUpload();
void testDownloadPause();
void testUploadPause();
void testDownloadCancel();
void testUploadCancel();
void testDownloadHashError();
void testDownloadFileName();
void testUploadFileName();
void testDownloadOpenFileError();
void testUploadOpenFileError();
void testDownloadKIOError();
void testUploadKIOError();
void testResponseError();
Q_SIGNALS:
void readyPause();
void readyResume();
void readyCancel();
void canceled();
};
void KazvIOJobTest::initTestCase()
{
- downloadFile.open();
+ auto openResult = downloadFile.open();
+ QVERIFY(openResult);
downloadFile.write(downloadFileContent);
downloadFile.close();
downloadFileHash.addData(&downloadFile);
hashStr = QString::fromUtf8(downloadFileHash.result().toBase64(
QByteArray::Base64Encoding | QByteArray::OmitTrailingEquals));
// QHttpServer::route() requires QHttpServerResponder must be passed by universal reference before Qt6.8
// https://doc.qt.io/qt-6.5/qhttpserver.html#route
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
using QHttpServerResponderRef = QHttpServerResponder &&;
#else
using QHttpServerResponderRef = QHttpServerResponder &;
#endif
httpServer.route(downloadEndpoint, [this](QHttpServerResponderRef res) {
- downloadFile.open();
+ auto openResult = downloadFile.open();
+ QVERIFY(openResult);
res.write(downloadFile.readAll(), "application/octet-stream"_ba);
downloadFile.close();
});
httpServer.route(downloadAuthEndpoint, [this](const QHttpServerRequest &req) {
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
hasAuth = std::find_if(req.headers().begin(), req.headers().end(), [&req](auto header) {
return header.first == "Authoriation"_ba;
}) != req.headers().end();
#else
hasAuth = !req.headers().value(u"Authorization"_s).isNull();
#endif
return QHttpServerResponse(QHttpServerResponder::StatusCode::Ok);
});
httpServer.route(downloadPauseEndpoint, [this](QHttpServerResponderRef res) {
- downloadFile.open();
+ auto openResult = downloadFile.open();
+ QVERIFY(openResult);
QSignalSpy qs{this, &KazvIOJobTest::readyResume};
Q_EMIT readyPause();
QVERIFY(qs.wait());
res.write(downloadFile.readAll(), "application/octet-stream"_ba);
downloadFile.close();
});
httpServer.route(downloadCancelEndpoint, [this](QHttpServerResponderRef /* res */) {
- downloadFile.open();
+ auto openResult = downloadFile.open();
+ QVERIFY(openResult);
QSignalSpy qs{this, &KazvIOJobTest::canceled};
Q_EMIT readyCancel();
QVERIFY(qs.wait());
return;
});
httpServer.route(uploadEndpoint, [this](const QHttpServerRequest &req) {
if (req.body() == uploadFileContent) {
- uploadFile.open();
+ auto openResult = uploadFile.open();
+ if (!openResult) {
+ throw std::runtime_error("Cannot open upload file");
+ }
uploadFile.write(req.body());
uploadFile.close();
auto resJson = QJsonObject{{u"content_uri"_s, u"mxc://uri"_s}};
return QHttpServerResponse{
resJson, QHttpServerResponse::StatusCode::Ok};
} else if (req.body() == responseErrorContent) {
return QHttpServerResponse{QHttpServerResponder::StatusCode::Ok};
}
return QHttpServerResponse(QHttpServerResponder::StatusCode::Ok);
});
QVERIFY(tcpServer.listen());
httpServer.bind(&tcpServer);
port = tcpServer.serverPort();
serverUrl = u"http://localhost:"_s + QString::number(port);
httpServer.moveToThread(&serverThread);
serverThread.start();
}
void KazvIOJobTest::cleanupTestCase()
{
serverThread.quit();
serverThread.wait();
}
void KazvIOJobTest::testDownload()
{
// QTemporaryFile cannot be written by QSaveFile, use QTemporaryDir instead.
QTemporaryDir dir{};
auto fileName = dir.filePath(u"savedFile"_s);
auto url = QUrl{serverUrl + downloadEndpoint};
KazvIODownloadJob job{fileName, url, false, hashStr};
QTRY_VERIFY(job.isResulted());
QCOMPARE(job.error(), KazvIOBaseJob::NoError);
QFile savedFile{fileName};
- savedFile.open(QIODevice::ReadOnly);
- downloadFile.open();
+ auto openResult = savedFile.open(QIODevice::ReadOnly);
+ QVERIFY(openResult);
+ openResult = downloadFile.open();
+ QVERIFY(openResult);
QCOMPARE(downloadFile.readAll(), savedFile.readAll());
downloadFile.close();
savedFile.close();
}
void KazvIOJobTest::testDownloadAuth()
{
// QTemporaryFile cannot be written by QSaveFile, use QTemporaryDir instead.
QTemporaryDir dir{};
auto fileName = dir.filePath(u"savedFile"_s);
auto url = QUrl{serverUrl + downloadAuthEndpoint};
KazvIODownloadJob job{fileName, url, false, hashStr, token};
QTRY_VERIFY(job.isResulted());
QVERIFY(hasAuth);
}
void KazvIOJobTest::testUpload()
{
QTemporaryFile file;
- file.open();
+ auto openResult = file.open();
+ QVERIFY(openResult);
file.write(uploadFileContent);
file.close();
auto url = QUrl{serverUrl};
KazvIOUploadJob job{
file.fileName(), url, false, nullptr, u""_s, u"token"_s};
QTRY_VERIFY(job.isResulted());
QCOMPARE(job.error(), KazvIOBaseJob::NoError);
- file.open();
- uploadFile.open();
+ openResult = file.open();
+ QVERIFY(openResult);
+ openResult = uploadFile.open();
+ QVERIFY(openResult);
QCOMPARE(uploadFile.readAll(), file.readAll());
uploadFile.close();
file.close();
}
void KazvIOJobTest::testDownloadPause()
{
// QTemporaryFile cannot be written by QSaveFile, use QTemporaryDir instead.
QTemporaryDir dir{};
auto fileName = dir.filePath(u"savedFile"_s);
auto url = QUrl{serverUrl + downloadPauseEndpoint};
KazvIODownloadJob job{fileName, url, false, hashStr};
QSignalSpy qs{this, &KazvIOJobTest::readyPause};
QVERIFY(qs.wait());
job.suspend();
QVERIFY(job.isSuspended());
job.resume();
Q_EMIT readyResume();
QTRY_VERIFY(job.isResulted());
QCOMPARE(job.error(), KazvIOBaseJob::NoError);
QFile savedFile{fileName};
- savedFile.open(QIODevice::ReadOnly);
- downloadFile.open();
+ auto openResult = savedFile.open(QIODevice::ReadOnly);
+ QVERIFY(openResult);
+ openResult = downloadFile.open();
+ QVERIFY(openResult);
QCOMPARE(downloadFile.readAll(), savedFile.readAll());
downloadFile.close();
savedFile.close();
}
void KazvIOJobTest::testUploadPause()
{
QTemporaryFile file;
- file.open();
+ auto openResult = file.open();
+ QVERIFY(openResult);
file.write(uploadFileContent);
file.close();
auto url = QUrl{serverUrl};
KazvIOUploadJob job{file.fileName(), url, false, nullptr,
u""_s, u"token"_s, std::nullopt, u""_s, u""_s, true};
job.suspend();
QVERIFY(job.isSuspended());
job.resume();
job.testResume();
QTRY_VERIFY(job.isResulted());
QCOMPARE(job.error(), KazvIOBaseJob::NoError);
- file.open();
- uploadFile.open();
+ openResult = file.open();
+ QVERIFY(openResult);
+ openResult = uploadFile.open();
+ QVERIFY(openResult);
QCOMPARE(uploadFile.readAll(), file.readAll());
uploadFile.close();
file.close();
}
void KazvIOJobTest::testDownloadCancel()
{
// QTemporaryFile cannot be written by QSaveFile, use QTemporaryDir instead.
QTemporaryDir dir{};
auto fileName = dir.filePath(u"savedFile"_s);
auto url = QUrl{serverUrl + downloadCancelEndpoint};
KazvIODownloadJob job{fileName, url, false, hashStr};
QSignalSpy qs{this, &KazvIOJobTest::readyCancel};
QVERIFY(qs.wait());
job.cancel();
Q_EMIT canceled();
QTRY_VERIFY(job.isResulted());
QCOMPARE(job.error(), KazvIOBaseJob::UserCancel);
QFile savedFile{fileName};
QVERIFY(!savedFile.exists());
}
void KazvIOJobTest::testUploadCancel()
{
QTemporaryFile file;
- file.open();
+ auto openResult = file.open();
+ QVERIFY(openResult);
file.write(uploadFileContent);
file.close();
auto url = QUrl{serverUrl};
KazvIOUploadJob job{file.fileName(), url, false, nullptr,
u""_s, u"token"_s, std::nullopt, u""_s, u""_s, true};
job.cancel();
QTRY_VERIFY(job.isResulted());
QCOMPARE(job.error(), KazvIOBaseJob::UserCancel);
}
void KazvIOJobTest::testDownloadHashError()
{
// QTemporaryFile cannot be written by QSaveFile, use QTemporaryDir instead.
QTemporaryDir dir{};
auto fileName = dir.filePath(u"savedFile"_s);
auto url = QUrl{serverUrl + downloadEndpoint};
KazvIODownloadJob job{fileName, url, false, u"WrongHash"_s};
QTRY_VERIFY(job.isResulted());
QCOMPARE(job.error(), KazvIOBaseJob::HashError);
QFile savedFile{fileName};
QVERIFY(!savedFile.exists());
}
void KazvIOJobTest::testDownloadFileName()
{
// QTemporaryFile cannot be written by QSaveFile, use QTemporaryDir instead.
QTemporaryDir dir{};
auto fileName = dir.filePath(u"savedFile"_s);
auto url = QUrl{serverUrl + downloadEndpoint};
KazvIODownloadJob job{fileName, url, false, hashStr};
QCOMPARE(job.fileName(), fileName);
}
void KazvIOJobTest::testUploadFileName()
{
QTemporaryFile file;
- file.open();
+ auto openResult = file.open();
+ QVERIFY(openResult);
file.write(uploadFileContent);
file.close();
auto url = QUrl{serverUrl};
KazvIOUploadJob job{
file.fileName(), url, false, nullptr, u""_s, u"token"_s};
QCOMPARE(job.fileName(), file.fileName());
}
void KazvIOJobTest::testDownloadOpenFileError()
{
// QTemporaryFile cannot be written by QSaveFile, use QTemporaryDir instead.
QTemporaryDir dir{};
auto fileName = dir.filePath(u"savedFile"_s);
auto url = QUrl{serverUrl + downloadEndpoint};
QFile savedFile{fileName};
- savedFile.open(QIODevice::ReadWrite);
+ auto openResult = savedFile.open(QIODevice::ReadWrite);
+ QVERIFY(openResult);
savedFile.close();
// Remove all permissions so that Qt cannot open this file
QVERIFY(savedFile.setPermissions({}));
KazvIODownloadJob job{fileName, url, false, hashStr};
QTRY_VERIFY(job.isResulted());
QCOMPARE(job.error(), KazvIOBaseJob::OpenFileError);
}
void KazvIOJobTest::testUploadOpenFileError()
{
QTemporaryFile file;
- file.open();
+ auto openResult = file.open();
+ QVERIFY(openResult);
file.write(uploadFileContent);
file.close();
auto url = QUrl{serverUrl};
// Remove all permissions so that Qt cannot open this file
QVERIFY(file.setPermissions({}));
KazvIOUploadJob job{
file.fileName(), url, false, nullptr, u""_s, u"token"_s};
QTRY_VERIFY(job.isResulted());
QCOMPARE(job.error(), KazvIOBaseJob::OpenFileError);
}
void KazvIOJobTest::testDownloadKIOError()
{
// QTemporaryFile cannot be written by QSaveFile, use QTemporaryDir instead.
QTemporaryDir dir{};
auto fileName = dir.filePath(u"savedFile"_s);
auto wrongUrl = QUrl{};
KazvIODownloadJob job{fileName, wrongUrl, false, hashStr};
QTRY_VERIFY(job.isResulted());
QCOMPARE(job.error(), KazvIOBaseJob::KIOError);
}
void KazvIOJobTest::testUploadKIOError()
{
QTemporaryFile file;
- file.open();
+ auto openResult = file.open();
+ QVERIFY(openResult);
file.write(uploadFileContent);
file.close();
auto wrongUrl = QUrl{};
KazvIOUploadJob job{
file.fileName(), wrongUrl, false, nullptr, u""_s, u"token"_s};
QTRY_VERIFY(job.isResulted());
QCOMPARE(job.error(), KazvIOBaseJob::KIOError);
}
void KazvIOJobTest::testResponseError()
{
QTemporaryFile file;
- file.open();
+ auto openResult = file.open();
+ QVERIFY(openResult);
file.write(responseErrorContent);
file.close();
auto url = QUrl{serverUrl};
KazvIOUploadJob job{
file.fileName(), url, false, nullptr, u""_s, u"token"_s};
QTRY_VERIFY(job.isResulted());
QCOMPARE(job.error(), KazvIOBaseJob::ResponseError);
}
QTEST_MAIN(KazvIOJobTest)
#include "kazv-io-job-test.moc"
diff --git a/src/tests/kazv-io-manager-test.cpp b/src/tests/kazv-io-manager-test.cpp
index e89ac02..198c0f2 100644
--- a/src/tests/kazv-io-manager-test.cpp
+++ b/src/tests/kazv-io-manager-test.cpp
@@ -1,190 +1,192 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2025 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "kazv-io-manager.hpp"
#include "test-model.hpp"
#include "test-utils.hpp"
#include <QObject>
#include <QtTest>
#include <QHttpServer>
#include <QTcpServer>
#include <QThread>
#include <QTemporaryFile>
#include <QString>
#include <QHttpServerResponder>
using namespace Qt::Literals::StringLiterals;
static const QString downloadEndpoint =
u"/_matrix/client/v1/media/download/serverName/download"_s;
static const char *downloadFileContent = "download";
class KazvIOManagerTest : public QObject
{
Q_OBJECT
private:
QHttpServer httpServer;
QTcpServer tcpServer; // Required by QHttpServer
QThread serverThread;
quint16 port;
QTemporaryFile downloadFile;
QString serverUrl;
private Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void testCache();
void testDownload();
void testUpload();
void testRoomlessUpload();
void testClearJobs();
Q_SIGNALS:
void readyResume();
void readyPause();
};
void KazvIOManagerTest::initTestCase()
{
- downloadFile.open();
+ auto openResult = downloadFile.open();
+ QVERIFY(openResult);
downloadFile.write(downloadFileContent);
downloadFile.close();
// QHttpServer::route() requires QHttpServerResponder must be passed by universal reference before Qt6.8
// https://doc.qt.io/qt-6.5/qhttpserver.html#route
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
using QHttpServerResponderRef = QHttpServerResponder &&;
#else
using QHttpServerResponderRef = QHttpServerResponder &;
#endif
httpServer.route(downloadEndpoint, [this](QHttpServerResponderRef res) {
QSignalSpy qs{this, &KazvIOManagerTest::readyResume};
Q_EMIT readyPause();
QVERIFY(qs.wait());
- downloadFile.open();
+ auto openResult = downloadFile.open();
+ QVERIFY(openResult);
res.write(downloadFile.readAll(), "application/octet-stream"_ba);
downloadFile.close();
});
QVERIFY(tcpServer.listen());
httpServer.bind(&tcpServer);
port = tcpServer.serverPort();
serverUrl = u"http://localhost:"_s + QString::number(port);
httpServer.moveToThread(&serverThread);
serverThread.start();
}
void KazvIOManagerTest::cleanupTestCase()
{
serverThread.quit();
serverThread.wait();
}
void KazvIOManagerTest::testCache()
{
KazvIOManager manager;
auto url = QUrl{serverUrl + downloadEndpoint};
auto id = u"id"_s;
QSignalSpy qsPause{this, &KazvIOManagerTest::readyPause};
manager.cacheFile(url, id);
QVERIFY(qsPause.wait());
auto job = manager.getCacheJob(id);
QVERIFY(job != nullptr);
// Multiple calls do not repeat downloads
manager.cacheFile(QUrl{u"url"_s}, id);
QCOMPARE(manager.getCacheJob(id), job);
QSignalSpy qs{job, &KazvIOBaseJob::result};
Q_EMIT readyResume();
// Cache job is automatically destroyed
QVERIFY(qs.wait());
QCOMPARE(manager.getCacheJob(id), nullptr);
}
void KazvIOManagerTest::testDownload()
{
KazvIOManager manager;
auto id = u"id"_s;
manager.startNewDownloadJob(
QUrl{u"serverUrl"_s}, QUrl{u"localFileUrl"_s}, id, u"hash"_s);
QVERIFY(manager.getDownloadJob(id) != nullptr);
manager.deleteDownloadJob(id);
QCOMPARE(manager.getDownloadJob(id), nullptr);
}
void KazvIOManagerTest::testUpload()
{
auto model = makeTestModel();
Kazv::RoomModel room;
room.roomId = "!test:tusooa.xyz";
model.client.roomList.rooms =
model.client.roomList.rooms.set(room.roomId, room);
SessionSetup s(model);
auto roomList = toUniquePtr(s.session.roomList());
KazvIOManager manager;
auto job = manager.startNewUploadJob(QUrl{u"serverUrl"_s}, QUrl{u"localFileUrl"_s},
u"token"_s, QString::fromStdString(room.roomId), roomList.get(), false,
u"relType"_s, u"relatedTo"_s);
auto roomId = QString::fromStdString(room.roomId);
auto uploadModel = QPointer{manager.getUploadJobs(roomId)};
QCOMPARE(uploadModel->rowCount(), 1);
manager.deleteUploadJob(roomId, job);
QCOMPARE(uploadModel->rowCount(), 0);
manager.deleteModelIfEmpty(roomId);
QVERIFY(uploadModel.isNull());
}
void KazvIOManagerTest::testRoomlessUpload()
{
const auto ROOMLESS = u"not-a-room"_s;
KazvIOManager manager;
auto job = manager.startNewRoomlessUploadJob(QUrl{u"serverUrl"_s},
QUrl{u"localFileUrl"_s}, u"token"_s);
auto uploadModel = QPointer{manager.getUploadJobs(ROOMLESS)};
QCOMPARE(uploadModel->rowCount(), 1);
manager.deleteRoomlessUploadJob(job);
QCOMPARE(uploadModel->rowCount(), 0);
manager.deleteModelIfEmpty(ROOMLESS);
QVERIFY(uploadModel.isNull());
}
void KazvIOManagerTest::testClearJobs()
{
KazvIOManager manager;
auto id = u"id"_s;
manager.startNewDownloadJob(
QUrl{u"serverUrl"_s}, QUrl{u"localFileUrl"_s}, id, u"hash"_s);
QVERIFY(manager.getDownloadJob(id) != nullptr);
auto model = makeTestModel();
Kazv::RoomModel room;
room.roomId = "!test:tusooa.xyz";
model.client.roomList.rooms =
model.client.roomList.rooms.set(room.roomId, room);
SessionSetup s(model);
auto roomList = toUniquePtr(s.session.roomList());
manager.startNewUploadJob(QUrl{u"serverUrl"_s}, QUrl{u"localFileUrl"_s},
u"token"_s, QString::fromStdString(room.roomId), roomList.get(), false,
u"relType"_s, u"relatedTo"_s);
auto roomId = QString::fromStdString(room.roomId);
QCOMPARE(manager.getUploadJobs(roomId)->rowCount(), 1);
manager.clearJobs();
QCOMPARE(manager.getDownloadJob(id), nullptr);
QCOMPARE(manager.getUploadJobs(roomId)->rowCount(), 0);
}
QTEST_MAIN(KazvIOManagerTest)
#include "kazv-io-manager-test.moc"
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 19, 6:14 AM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768988
Default Alt Text
(63 KB)
Attached To
Mode
rK kazv
Attached
Detach File
Event Timeline
Log In to Comment