Page MenuHomePhorge

No OneTemporary

Size
63 KB
Referenced Files
None
Subscribers
None
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

Mime Type
text/x-diff
Expires
Sat, Sep 19, 6:08 AM (1 d, 11 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768988
Default Alt Text
(63 KB)

Event Timeline