Page MenuHomePhorge

No OneTemporary

Size
108 KB
Referenced Files
None
Subscribers
None
diff --git a/src/contents/ui/FileHandler.qml b/src/contents/ui/FileHandler.qml
index afe501d..c251b1e 100644
--- a/src/contents/ui/FileHandler.qml
+++ b/src/contents/ui/FileHandler.qml
@@ -1,156 +1,176 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import moe.kazv.mxc.kazv 0.0 as MK
import QtQml 2.15
import QtQuick 2.15
/**
* Used to handle files described in event, such as caching(if autoCache is true) and download.
* It is either used for caching or downloading. These two operations cannot be performed in the same FileHandler.
*/
QtObject {
id: fileHandler
required property var eventContent
/**
* If autoCache is true, the file is automatically cached(thumbnail is downloaded if exists).
*/
required property bool autoCache
required property var kazvIOManager
required property var matrixSdk
/**
* Used to download or caching.
* mxcUri, sha256, key, iv will be setted automatically
* according to whether the file is encrypted or not.
*/
property var mxcUri: ""
- property var url: mxcUri ? fileHandler.matrixSdk.mxcUriToHttp(fileHandler.mxcUri) : ""
+ property var httpVer: matrixSdk.checkSpecVersion("v1.11") ? MK.MatrixSdk.AuthenticatedMediaV1 : MK.MatrixSdk.UnauthenticatedMediaV3
+ property var url: fileHandler.mxcToHttp(httpVer, mxcUri)
+ property var saveFileUrl: ""
property var mediaId: mxcUri && fileHandler.getMediaId(fileHandler.mxcUri)
property var sha256: ""
+ property var token: httpVer === MK.MatrixSdk.AuthenticatedMediaV1 ? matrixSdk.token : ""
property var key: ""
property var iv: ""
property var kazvIOJob
readonly property url localFile: kazvIOJob ? '' : cachedFile
property real percent: fileHandler.kazvIOJob ? fileHandler.kazvIOJob.progress : 0.0 // from 0 to 1
property var result
property bool success: fileHandler.result == MK.KazvIOBaseJob.NoError
/**
* This attribute is only used after result is set (that is, after signal resultChanged is emitted),
* and only when success is true, this attribute has a correct and valid value.
*/
property url cachedFile
property var fileInfo: "info" in eventContent ? eventContent.info : {}
property var encryptedFile: "file" in eventContent ? eventContent.file : {}
property var encryptedFileMxcUri: "url" in encryptedFile ? encryptedFile.url : ""
property var encryptedFileSha256: "hashes" in encryptedFile && "sha256" in encryptedFile.hashes ? encryptedFile.hashes.sha256 : ""
// property var encryptedFileAlg: "key" in encryptedFile && "alg" in encryptedFile.key ? encryptedFile.key.alg : ""
property var encryptedFileKey: "key" in encryptedFile && "k" in encryptedFile.key ? encryptedFile.key.k : ""
property var encryptedFileIv: "iv" in encryptedFile ? encryptedFile.iv : ""
property var encryptedThumbnail: "thumbnail_file" in fileInfo ? fileInfo.thumbnail_file : {}
property var encryptedThumbnailMxcUri: "url" in encryptedThumbnail ? encryptedThumbnail.url : ""
property var encryptedThumbnailSha256: "hashes" in encryptedThumbnail && "sha256" in encryptedThumbnail.hashes ? encryptedThumbnail.hashes.sha256 : ""
// property var encryptedFileAlg: "key" in encryptedThumbnail && "alg" in encryptedThumbnail.key ? encryptedThumbnail.key.alg : ""
property var encryptedThumbnailKey: "key" in encryptedThumbnail && "k" in encryptedThumbnail.key ? encryptedThumbnail.key.k : ""
property var encryptedThumbnailIv: "iv" in encryptedThumbnail ? encryptedThumbnail.iv : ""
property var unencryptedFileMxcUri: "url" in eventContent ? eventContent.url : ""
property var unencryptedThumbnailMxcUri: "thumbnail_url" in fileInfo ? fileInfo.thumbnail_url : ""
/**
* Emit when start download, not emit when start cache.
* Can be used to indicate progress bar visible
*/
signal startDownload
function getMediaId(mxcUri) {
const temp = mxcUri.toString().split('/')
return temp[2] + '_' + temp[3]
}
function downloadFile(saveFileUrl) {
- fileHandler.kazvIOManager.startNewDownloadJob(fileHandler.url,saveFileUrl,
+ fileHandler.saveFileUrl = saveFileUrl
+ fileHandler.kazvIOManager.startNewDownloadJob(fileHandler.url,
+ fileHandler.saveFileUrl,
fileHandler.mediaId,
fileHandler.sha256,
+ fileHandler.token,
fileHandler.key,
fileHandler.iv)
fileHandler.updateKazvIOJob(fileHandler.mediaId)
fileHandler.startDownload()
}
function cacheFile() {
fileHandler.cachedFile = fileHandler.kazvIOManager.cacheFile(fileHandler.url,
fileHandler.mediaId,
fileHandler.sha256,
+ fileHandler.token,
fileHandler.key,
fileHandler.iv)
fileHandler.updateKazvIOJob(fileHandler.mediaId)
}
function updateKazvIOJob(mediaId) {
fileHandler.kazvIOJob = null
if (autoCache) {
fileHandler.kazvIOJob = fileHandler.kazvIOManager.getCacheJob(fileHandler.mediaId)
} else {
fileHandler.kazvIOJob = fileHandler.kazvIOManager.getDownloadJob(fileHandler.mediaId)
}
if (kazvIOJob) {
fileHandler.kazvIOJob.result.connect(function (ec) {
fileHandler.result = ec
})
}
}
+ function mxcToHttp(httpVer, mxcUri) {
+ if (!mxcUri) {
+ return "";
+ }
+ if (httpVer === MK.MatrixSdk.UnauthenticatedMediaV3) {
+ return matrixSdk.mxcUriToHttp(mxcUri);
+ } else if (httpVer === MK.MatrixSdk.AuthenticatedMediaV1) {
+ return matrixSdk.mxcUriToHttpAuthenticatedV1(mxcUri);
+ } else {
+ return "";
+ }
+ }
+
Component.onCompleted: {
// Check if there is an encryted file or unencrypted file
if (encryptedFileMxcUri &&
encryptedFileSha256 &&
encryptedFileKey &&
encryptedFileIv) {
fileHandler.mxcUri = encryptedFileMxcUri
fileHandler.sha256 = encryptedFileSha256
fileHandler.key = encryptedFileKey
fileHandler.iv = encryptedFileIv
} else if (unencryptedFileMxcUri) {
fileHandler.mxcUri = unencryptedFileMxcUri
} else {
// The event content is incorrect or the FileHandler.qml has a bug
return
}
/**
* Thumbnail will only be downloaded in the cache
**/
if (fileHandler.autoCache) {
// Check if there is an encrypted thumbnail or unencrypted thumbnail
if (encryptedThumbnailMxcUri &&
encryptedThumbnailSha256 &&
encryptedThumbnailKey &&
encryptedThumbnailIv) {
fileHandler.mxcUri = encryptedThumbnailMxcUri
fileHandler.sha256 = encryptedThumbnailSha256
fileHandler.key = encryptedThumbnailKey
fileHandler.iv = encryptedThumbnailIv
} else if (unencryptedThumbnailMxcUri) {
fileHandler.mxcUri = unencryptedThumbnailMxcUri
}
fileHandler.cacheFile()
}
fileHandler.updateKazvIOJob(fileHandler.mediaId)
fileHandler.onSuccessChanged.connect(function () {
fileHandler.kazvIOManager.deleteDownloadJob(mediaId)
})
}
}
diff --git a/src/contents/ui/Main.qml b/src/contents/ui/Main.qml
index 0265520..d9aeda9 100644
--- a/src/contents/ui/Main.qml
+++ b/src/contents/ui/Main.qml
@@ -1,364 +1,365 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import org.kde.kirigami as Kirigami
import QtQuick.Layouts
import QtQuick.Controls
import moe.kazv.mxc.kazv as MK
import '.' as Kazv
import 'shortcuts' as KazvShortcuts
import 'l10n.js' as L10n
Kirigami.ApplicationWindow {
id: root
property var kazvIOManager: MK.KazvIOManager {
cacheDirectory: kazvConfig.cacheDirectory
}
property var matrixSdk: MK.MatrixSdk {
onLoginSuccessful: {
switchToMainPage();
recordLastSession();
}
onLoginFailed: {
console.log("Login Failed");
showPassiveNotification(l10n.get('login-page-request-failed-prompt', { errorCode, errorMsg }));
}
onDiscoverFailed: {
console.log("Discover Failed");
showPassiveNotification(l10n.get('login-page-discover-failed-enter-prompt', { errorCode, errorMsg }));
}
onSessionChanged: {
console.log('session changed');
reloadSdkVariables();
}
onLogoutSuccessful: {
root.loggedIn = false;
root.mainPage = Qt.createComponent("MainPage.qml").createObject(root);
pageStack.clear();
pushLoginPage();
}
onLogoutFailed: {
console.warn('Logout failed');
showPassiveNotification(l10n.get('logout-failed-prompt', { errorCode, errorMsg }));
}
}
property var loggedIn: !!matrixSdk.token
property var sdkVars: QtObject {
property var roomList: matrixSdk.roomList()
property var userGivenNicknameMap: matrixSdk.userGivenNicknameMap()
property string currentRoomId: ''
}
property var l10nProvider: MK.L10nProvider {
}
property var kazvConfig: MK.KazvConfig {
}
function initializeL10n () {
const availableLocales = l10nProvider.availableLocaleCodes();
console.log('available locales:', availableLocales);
const desiredLanguages = [Qt.uiLanguage];
console.log('ui language is: ', desiredLanguages);
const defaultLocale = 'en';
const wantedLocales = L10n.negotiateLanguages(desiredLanguages, availableLocales, defaultLocale);
console.log('wanted locales:', wantedLocales);
const bundles = L10n.generateBundles(wantedLocales, l10nProvider.getFtlData(wantedLocales));
const provider = new L10n.FluentProvider(bundles);
return provider;
}
property var l10n: initializeL10n();
property var confirmLogoutPopup: Kirigami.OverlaySheet {
id: confirmLogoutPopup
title: l10n.get('confirm-logout-popup-title')
ColumnLayout {
Label {
Layout.fillWidth: true
text: l10n.get('confirm-logout-popup-prompt')
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignCenter
Button {
text: l10n.get('confirm-logout-popup-accept-button')
onClicked: {
hardLogout();
confirmLogoutPopup.close();
}
}
Button {
text: l10n.get('confirm-logout-popup-cancel-button')
onClicked: confirmLogoutPopup.close();
}
}
}
}
title: l10n.get('app-title')
globalDrawer: Kirigami.GlobalDrawer {
title: l10n.get('global-drawer-title')
titleIcon: "applications-graphics"
property var actionGroupName: 'global-drawer-actions'
actions: [
Kirigami.Action {
objectName: 'switch-account'
text: l10n.get('global-drawer-action-switch-account')
icon.name: 'system-switch-user'
onTriggered: switchAccount()
},
Kirigami.Action {
objectName: 'hard-logout'
text: l10n.get('global-drawer-action-hard-logout')
icon.name: 'application-exit'
onTriggered: confirmLogoutPopup.open()
},
Kirigami.Action {
objectName: 'save'
text: l10n.get('global-drawer-action-save-session')
icon.name: 'document-save'
onTriggered: {
recordLastSession()
matrixSdk.serializeToFile()
}
},
Kirigami.Action {
objectName: 'configure-shortcuts'
text: l10n.get('global-drawer-action-configure-shortcuts')
icon.name: 'configure-shortcuts'
onTriggered: {
pushActionSettingsPage();
}
},
Kirigami.Action {
objectName: 'settings'
text: l10n.get('global-drawer-action-settings')
icon.name: 'configure'
onTriggered: {
pushSettingsPage();
}
},
Kirigami.Action {
objectName: 'create-room'
enabled: root.loggedIn
icon.name: 'contact-new'
text: l10n.get('global-drawer-action-create-room')
onTriggered: {
pushCreateRoomPage();
}
},
Kirigami.Action {
objectName: 'join-room'
enabled: root.loggedIn
text: l10n.get('global-drawer-action-join-room')
icon.name: 'list-add-user'
onTriggered: {
pushJoinRoomPage();
}
},
Kirigami.Action {
objectName: 'aboutAction'
text: l10n.get('global-drawer-action-about')
icon.name: 'help-about'
onTriggered: pageStack.pushDialogLayer(Qt.resolvedUrl('About.qml'))
}
]
}
contextDrawer: Kirigami.ContextDrawer {
id: contextDrawer
}
property var actionCollection: KazvShortcuts.ActionCollection {
id: globalActionCollection
children: [
root.globalDrawer,
root.contextDrawer,
]
shortcutsConfig: kazvConfig
}
property var notifier: Kazv.Notifier {
}
pageStack.initialPage: Qt.resolvedUrl("LoginPage.qml")
property var mainPage: Kazv.MainPage {}
property var emptyPage: Kirigami.Page {
title: l10n.get('empty-room-page-title')
Label {
text: l10n.get('empty-room-page-description')
}
}
signal switchToRoomRequested(string roomId)
property var pageManager: Kazv.PageManager {
pageStack: root.pageStack
sdkVars: root.sdkVars
main: root
}
function activateRoomSettingsPage(room) {
pageStack.push(Qt.resolvedUrl("room-settings/RoomSettingsPage.qml"), {
room
});
}
function activateRoomPinnedEventsPage(room) {
pageStack.push(Qt.resolvedUrl('RoomPinnedEventsPage.qml'), {
room
});
}
function activateRoomInvitePage(room) {
pageStack.push(Qt.resolvedUrl("room-settings/RoomInvitePage.qml"), {
room
});
}
function activateRoomStickerPacksPage(room) {
pageStack.push(Qt.resolvedUrl("room-settings/RoomStickerPacksPage.qml"), {
room
});
}
function activateRoomStatePage(room) {
pageStack.push(Qt.resolvedUrl("room-settings/RoomStatePage.qml"), {
room
});
}
function activateUserPage(user, room, userId) {
pageStack.push(Qt.resolvedUrl("UserPage.qml"), {
userId: userId || user.userId,
user,
room
});
}
function switchToMainPage() {
pageStack.replace([root.mainPage])
pageStack.currentIndex = 0
}
function switchToLoginPage() {
pageStack.replace([Qt.resolvedUrl("LoginPage.qml")])
pageStack.currentIndex = 0
}
function pushLoginPage() {
pageStack.push(Qt.resolvedUrl("LoginPage.qml"), {
isSwitchingAccount: true,
});
}
function pushActionSettingsPage() {
pageStack.push(Qt.resolvedUrl("ActionSettingsPage.qml"), {
actionsToConfigure: globalActionCollection.allActions(),
});
}
function pushSettingsPage() {
pageStack.push(Qt.resolvedUrl("SettingsPage.qml"));
}
function pushCreateRoomPage() {
pageStack.push(Qt.resolvedUrl("CreateRoomPage.qml"), {});
}
function pushJoinRoomPage() {
pageStack.push(Qt.resolvedUrl("JoinRoomPage.qml"), {});
}
function sessionNameFor(userId, deviceId) {
return userId + '/' + deviceId;
}
function getSessionLoadError(res, sessionName) {
switch (res) {
case MK.MatrixSdk.SessionNotFound:
return l10n.get('session-load-failure-not-found', { sessionName });
case MK.MatrixSdk.SessionFormatUnknown:
return l10n.get('session-load-failure-format-unknown', { sessionName });
case MK.MatrixSdk.SessionCannotBackup:
return l10n.get('session-load-failure-cannot-backup', { sessionName });
case MK.MatrixSdk.SessionLockFailed:
return l10n.get('session-load-failure-lock-failed', { sessionName });
case MK.MatrixSdk.SessionCannotOpenFile:
return l10n.get('session-load-failure-cannot-open-file', { sessionName });
case MK.MatrixSdk.SessionDeserializeFailed:
return l10n.get('session-load-failure-deserialize-failed', { sessionName });
default:
console.error('loadSession: Encountered an unknown error code');
}
}
function loadSession(sessionName) {
const res = matrixSdk.loadSession(sessionName);
if (res === MK.MatrixSdk.SessionLoadSuccess) {
console.log('load session successful');
switchToMainPage();
recordLastSession();
+ matrixSdk.getSpecVersions();
} else {
showPassiveNotification(getSessionLoadError(res, sessionName));
}
return res;
}
function loadLastSession() {
console.log('last session is:', kazvConfig.lastSession);
return loadSession(kazvConfig.lastSession);
}
function recordLastSession() {
kazvConfig.lastSession = sessionNameFor(matrixSdk.userId, matrixSdk.deviceId);
}
function reloadSdkVariables() {
sdkVars.roomList = matrixSdk.roomList();
sdkVars.userGivenNicknameMap = matrixSdk.userGivenNicknameMap();
sdkVars.currentRoomId = '';
}
function switchAccount() {
pushLoginPage();
}
function hardLogout() {
matrixSdk.logout();
}
Component.onCompleted: {
actionCollection.setupShortcuts();
loadLastSession();
}
onClosing: {
kazvIOManager.clearJobs()
}
}
diff --git a/src/kazv-io-job.cpp b/src/kazv-io-job.cpp
index fb2ec98..bf77e8c 100644
--- a/src/kazv-io-job.cpp
+++ b/src/kazv-io-job.cpp
@@ -1,365 +1,386 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+#include <client.hpp>
#include <kazv-defs.hpp>
#include "kazv-io-job.hpp"
#include "matrix-room.hpp"
#include "kazv-log.hpp"
+#include "client/actions/content.hpp"
#include <QObject>
#include <QPointer>
#include <QSharedPointer>
#include <QSaveFile>
#include <QFile>
#include <QString>
#include <QMimeDatabase>
#include <QCryptographicHash>
#include <QLoggingCategory>
#include <KJob>
#include <KIO/TransferJob>
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
using namespace Qt::Literals::StringLiterals;
struct KazvIOBaseJobPrivate
{
QPointer<KIO::TransferJob> job;
std::optional<KazvIOBaseJob::ErrorCode> result = std::nullopt;
std::optional<Kazv::AES256CTRDesc> aes = std::nullopt;
};
KazvIOBaseJob::KazvIOBaseJob(std::optional<Kazv::AES256CTRDesc> aes, QObject *parent)
: QObject(parent)
, m_d(new KazvIOBaseJobPrivate)
{
/**
* For unknown reasons, if a new AES256CTRDesc is not constructed here,
* it will cause the program to crash.
*/
if (aes != std::nullopt) {
m_d->aes = Kazv::AES256CTRDesc{aes.value().key(), aes.value().iv()};
}
// m_d->aes = aes;
connect(this, &KazvIOBaseJob::jobChanged, this, &KazvIOBaseJob::connectJob);
}
KazvIOBaseJob::~KazvIOBaseJob() = default;
float KazvIOBaseJob::progress() {
if (m_d->job.isNull()) {
return 0;
}
return static_cast<float>(m_d->job->percent()) / 100;
}
void KazvIOBaseJob::suspend()
{
if (m_d->job.isNull()) {
emitResult(KazvError);
return;
}
m_d->job->suspend();
}
void KazvIOBaseJob::resume()
{
if (m_d->job.isNull()) {
emitResult(KazvError);
return;
}
m_d->job->resume();
}
void KazvIOBaseJob::cancel()
{
if (m_d->job.isNull()) {
emitResult(KazvError);
return;
}
emitResult(UserCancel);
m_d->job->kill();
}
bool KazvIOBaseJob::isSuspended()
{
if (m_d->job.isNull()) {
return false;
}
return m_d->job->isSuspended();
}
bool KazvIOBaseJob::isResulted()
{
return m_d->result.has_value();
}
KazvIOBaseJob::ErrorCode KazvIOBaseJob::error()
{
if (m_d->result.has_value()) {
return m_d->result.value();
}
// Shouldn't call this function before result are emited
return NoError;
}
void KazvIOBaseJob::connectJob()
{
connect(m_d->job, &KJob::result, this, [this](KJob *job) {
if (job->error()) {
emitResult(KIOError);
}
});
connect(m_d->job, &KJob::percentChanged, this,
[this](KJob * /* job */, unsigned long /* percent */) { Q_EMIT progressChanged(); });
}
QPointer<KIO::TransferJob> KazvIOBaseJob::job()
{
return m_d->job;
}
void KazvIOBaseJob::setJob(QPointer<KIO::TransferJob> job)
{
m_d->job = job;
Q_EMIT jobChanged();
}
void KazvIOBaseJob::emitResult(ErrorCode ec, QString data)
{
if (m_d->result.has_value()) {
return;
}
m_d->result = ec;
Q_EMIT result(m_d->result.value(), data);
}
std::optional<Kazv::AES256CTRDesc> KazvIOBaseJob::aes()
{
return m_d->aes;
}
void KazvIOBaseJob::setAes(Kazv::AES256CTRDesc aes)
{
m_d->aes = aes;
}
struct KazvIODownloadJobPrivate
{
QSharedPointer<KazvSaveFile> file;
QString hash;
};
KazvIODownloadJob::KazvIODownloadJob(const QString &hash, std::optional<Kazv::AES256CTRDesc> aes, QObject *parent)
: KazvIOBaseJob(aes, parent)
, m_d(new KazvIODownloadJobPrivate)
{
m_d->hash = hash;
}
-KazvIODownloadJob::KazvIODownloadJob(const QString &fileName, const QUrl &serverUrl, bool showProgressBar,
- const QString &hash, std::optional<Kazv::AES256CTRDesc> aes, QObject *parent)
+KazvIODownloadJob::KazvIODownloadJob(const QString &fileName,
+ const QUrl &serverUrl, bool showProgressBar, const QString &hash,
+ const QString &token, std::optional<Kazv::AES256CTRDesc> aes,
+ QObject *parent)
: KazvIOBaseJob(aes, parent)
, m_d(new KazvIODownloadJobPrivate)
{
- if (setFile(fileName)) {
- auto flag = showProgressBar ? KIO::DefaultFlags : KIO::HideProgressInfo;
- setJob(KIO::get(serverUrl, KIO::NoReload, flag));
+ if (!setFile(fileName)) {
+ return;
}
+ auto flag = showProgressBar ? KIO::DefaultFlags : KIO::HideProgressInfo;
+ auto kioJob = KIO::get(serverUrl, KIO::NoReload, flag);
+ if (!token.isEmpty()) {
+ auto [serverName, mediaId] =
+ [&serverUrl]() -> std::pair<std::string, std::string>{
+ auto splited = serverUrl.path().split(u"/"_s);
+ return {splited.last(2).first().toStdString(),
+ splited.last(2).last().toStdString()};
+ }();
+ auto getContentJobV1 = Kazv::GetContentJobV1{
+ serverUrl.toString().toStdString(),
+ token.toStdString(), serverName, mediaId};
+ kioJob->addMetaData(u"customHTTPHeader"_s,
+ QStringLiteral("Authorization: ") .append(QString::fromStdString(
+ getContentJobV1.requestHeader()->at("Authorization"))));
+ }
+
+ setJob(kioJob);
m_d->hash = hash;
}
KazvIODownloadJob::~KazvIODownloadJob() = default;
QString KazvIODownloadJob::fileName() {
if (m_d->file.isNull()) {
return QStringLiteral("");
}
return m_d->file->fileName();
}
void KazvIODownloadJob::connectJob()
{
KazvIOBaseJob::connectJob();
connect(this->job(), &KIO::TransferJob::data, this, &KazvIODownloadJob::writeFile);
connect(this->job(), &KJob::result, this, &KazvIODownloadJob::closeFile);
}
bool KazvIODownloadJob::setFile(QString fileName)
{
m_d->file.reset(new KazvSaveFile(fileName, this->aes()));
if (m_d->file->open(QIODevice::WriteOnly)) {
return true;
}
emitResult(KazvIOBaseJob::OpenFileError);
return false;
}
void KazvIODownloadJob::writeFile(KJob *job, const QByteArray &data)
{
if (m_d->file.isNull() || !m_d->file->isOpen()) {
emitResult(KazvError);
job->kill();
return;
}
auto len = m_d->file->write(data);
if (len == -1) {
KazvIOBaseJob::emitResult(WriteFileError);
job->kill();
}
}
void KazvIODownloadJob::closeFile(KJob *job)
{
if (m_d->file.isNull()) {
emitResult(KazvError);
return;
}
if (job->error()) {
m_d->file->cancelWriting();
return;
}
if (!m_d->hash.isEmpty() && QString::fromUtf8(m_d->file->hash()) != m_d->hash) {
emitResult(HashError);
return;
}
m_d->file->commit();
emitResult(NoError);
}
struct KazvIOUploadJobPrivate
{
QSharedPointer<KazvFile> file;
QString response;
QSharedPointer<MatrixRoom> room;
QString mimeType;
QString mxcUri;
QString relType;
QString relatedTo;
bool testing{false};
};
KazvIOUploadJob::KazvIOUploadJob(std::optional<Kazv::AES256CTRDesc> aes,
const QString &relType, const QString &relatedTo, QObject *parent)
: KazvIOBaseJob(aes, parent)
, m_d(new KazvIOUploadJobPrivate)
{
m_d->relType = relType;
m_d->relatedTo = relatedTo;
}
KazvIOUploadJob::KazvIOUploadJob(const QString fileName, const QUrl serverUrl,
const bool showProgressBar, MatrixRoomList *roomList, const QString &roomId,
const QString token, std::optional<Kazv::AES256CTRDesc> aes,
const QString &relType, const QString &relatedTo,
const bool testing, QObject *parent)
: KazvIOBaseJob(aes, parent)
, m_d(new KazvIOUploadJobPrivate)
{
m_d->relType = relType;
m_d->relatedTo = relatedTo;
m_d->testing = testing;
if (setFile(fileName)) {
auto kazvUploadJob = Kazv::Api::UploadContentJob(serverUrl.toString().toStdString(),
token.toStdString(), Kazv::FileDesc(std::string()));
auto jobFlag = showProgressBar ? KIO::DefaultFlags : KIO::HideProgressInfo;
auto job = KIO::http_post(
QUrl(QString::fromStdString(kazvUploadJob.url())),
m_d->file.data(), jobFlag);
job->addMetaData(u"customHTTPHeader"_s, QStringLiteral("Authorization: ")
.append(QString::fromStdString(kazvUploadJob.requestHeader()->at("Authorization"))));
job->addMetaData(u"PropagateHttpHeader"_s, u"true"_s);
setJob(job);
if (roomList) {
m_d->room.reset(roomList->room(roomId));
}
}
}
KazvIOUploadJob::~KazvIOUploadJob() = default;
QString KazvIOUploadJob::fileName()
{
if (m_d->file.isNull()) {
return QStringLiteral("");
}
return m_d->file->fileName();
}
void KazvIOUploadJob::connectJob()
{
KazvIOBaseJob::connectJob();
connect(this->job(), &KIO::TransferJob::data, this,
[this](KJob * /* job */, const QByteArray &data) { m_d->response.append(QString::fromUtf8(data)); });
connect(this->job(), &KJob::result, this, &KazvIOUploadJob::handleResult);
connect(this->job(), &KIO::TransferJob::mimeTypeFound, this,
[this](KJob * /* job */, const QString &mimeType) { m_d->mimeType = mimeType; });
}
void KazvIOUploadJob::testResume()
{
m_d->file->testResume();
}
void KazvIOUploadJob::handleResult(KJob *job)
{
if (m_d->file.isNull()) {
emitResult(KazvError);
return;
}
m_d->file->close();
if (job->error()) {
return;
}
nlohmann::json j;
try {
j = nlohmann::json::parse(m_d->response.toStdString());
} catch(...) {
emitResult(ResponseError);
qCWarning(kazvLog) << "Get a invaild response from server.";
return;
}
QString mxcUri{};
if (j.contains("content_uri") && j["content_uri"].is_string()) {
mxcUri = QString::fromStdString(j["content_uri"].template get<std::string>());
} else {
emitResult(ResponseError);
qCWarning(kazvLog) << "There is no \"content_uri\" field in response.";
return;
}
auto shouldSendMessage = !!m_d->room;
if (shouldSendMessage) {
auto aes = this->aes();
if (aes.has_value()) {
m_d->room->sendEncryptedFileMessage(QUrl::fromLocalFile(m_d->file->fileName()).fileName(),
QMimeDatabase().mimeTypeForFile(m_d->file.data()->fileName()).name(),
m_d->file->size(), mxcUri,
QString::fromStdString(aes.value().key()), QString::fromStdString(aes.value().iv()),
m_d->file->hash(), m_d->relType, m_d->relatedTo);
} else {
m_d->room->sendMediaFileMessage(QUrl::fromLocalFile(m_d->file->fileName()).fileName(),
QMimeDatabase().mimeTypeForData(m_d->file.data()).name(),
m_d->file->size(), mxcUri, m_d->relType, m_d->relatedTo);
}
}
KazvIOBaseJob::emitResult(NoError, mxcUri);
}
bool KazvIOUploadJob::setFile(const QString fileName)
{
m_d->file.reset(new KazvFile(fileName, this->aes(), m_d->testing));
if (m_d->file->open(QIODevice::ReadOnly)) {
return true;
}
emitResult(KazvIOBaseJob::OpenFileError);
return false;
}
diff --git a/src/kazv-io-job.hpp b/src/kazv-io-job.hpp
index 8563f10..e2c8a15 100644
--- a/src/kazv-io-job.hpp
+++ b/src/kazv-io-job.hpp
@@ -1,169 +1,174 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include "matrix-room.hpp"
#include "matrix-room-list.hpp"
#include "kazv-file.hpp"
#include <aes-256-ctr.hpp>
#include <QObject>
#include <QPointer>
#include <QString>
#include <QUrl>
#include <KIO/TransferJob>
#include <memory>
#include <optional>
+using namespace Qt::Literals::StringLiterals;
+
struct KazvIOBaseJobPrivate;
class KazvIOBaseJob : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
Q_PROPERTY(float progress READ progress NOTIFY progressChanged)
std::unique_ptr<KazvIOBaseJobPrivate> m_d;
public:
enum ErrorCode {
/**
* No Error or the signal result has not been emitted yet.
*/
NoError = 0,
/**
* The user actively canceled the job.
*/
UserCancel,
/**
* Open File Error.
*/
OpenFileError,
/**
* Write File Error
*/
WriteFileError,
/**
* Errors caused by KIO, such network error.
*/
KIOError,
/**
* Error caused by hash error
*/
HashError,
/**
* Error caused by invalid response.
* KIO reported no error, but got an invalid JSON response.
*/
ResponseError,
/**
* Errors caused by bad code, means the bug is somewhere.
*/
KazvError
};
Q_ENUM(ErrorCode)
KazvIOBaseJob(std::optional<Kazv::AES256CTRDesc> aes = std::nullopt, QObject *parent = 0);
~KazvIOBaseJob();
float progress();
Q_INVOKABLE void suspend();
Q_INVOKABLE void resume();
Q_INVOKABLE void cancel();
Q_INVOKABLE bool isSuspended();
Q_INVOKABLE bool isResulted();
/**
* Only use it after emitted the result signal
*/
Q_INVOKABLE ErrorCode error();
Q_SIGNALS:
void jobChanged();
void progressChanged();
/**
* @param data the mxcUri if this is an upload job.
*/
void result(ErrorCode ec, QString data);
protected Q_SLOTS:
virtual void connectJob();
protected:
QPointer<KIO::TransferJob> job();
void setJob(QPointer<KIO::TransferJob> job);
std::optional<Kazv::AES256CTRDesc> aes();
void setAes(Kazv::AES256CTRDesc);
void emitResult(ErrorCode ec, QString data = QString());
};
struct KazvIODownloadJobPrivate;
class KazvIODownloadJob : public KazvIOBaseJob
{
Q_OBJECT
std::unique_ptr<KazvIODownloadJobPrivate> m_d;
bool setFile(QString fileName);
public:
KazvIODownloadJob(const QString &hash, std::optional<Kazv::AES256CTRDesc> aes = std::nullopt, QObject *parent = 0);
- KazvIODownloadJob(const QString &fileName, const QUrl &serverUrl, const bool showProgressBar, const QString &hash,
- std::optional<Kazv::AES256CTRDesc> aes = std::nullopt, QObject *parent = 0);
+ KazvIODownloadJob(const QString &fileName, const QUrl &serverUrl,
+ const bool showProgressBar, const QString &hash,
+ const QString &token = u""_s,
+ std::optional<Kazv::AES256CTRDesc> aes = std::nullopt,
+ QObject *parent = 0);
~KazvIODownloadJob();
Q_INVOKABLE QString fileName();
private Q_SLOTS:
void writeFile(KJob *job, const QByteArray &data);
void closeFile(KJob *job);
protected Q_SLOTS:
void connectJob() override;
};
struct KazvIOUploadJobPrivate;
class KazvIOUploadJob : public KazvIOBaseJob
{
Q_OBJECT
std::unique_ptr<KazvIOUploadJobPrivate> m_d;
bool setFile(const QString fileName);
public:
KazvIOUploadJob(std::optional<Kazv::AES256CTRDesc> aes = std::nullopt,
const QString &relType = QStringLiteral(""),
const QString &relatedTo = QStringLiteral(""), QObject *parent = 0);
// Parameter testing is ONLY for unit tests, otherwise MUST be set to false
KazvIOUploadJob(const QString fileName, const QUrl serverUrl,
const bool showProgressBar, MatrixRoomList *roomList,
const QString &roomId, const QString token,
std::optional<Kazv::AES256CTRDesc> aes = std::nullopt,
const QString &relType = QStringLiteral(""),
const QString &relatedTo = QStringLiteral(""),
const bool testing = false, QObject *parent = 0);
~KazvIOUploadJob();
Q_INVOKABLE QString fileName();
void testResume(); // Just for unit test
private Q_SLOTS:
void handleResult(KJob *job);
protected Q_SLOTS:
void connectJob() override;
};
diff --git a/src/kazv-io-manager.cpp b/src/kazv-io-manager.cpp
index 0e6f5cc..96a2da1 100644
--- a/src/kazv-io-manager.cpp
+++ b/src/kazv-io-manager.cpp
@@ -1,229 +1,231 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "kazv-io-manager.hpp"
#include "kazv-io-job.hpp"
#include "matrix-room.hpp"
#include "upload-job-model.hpp"
#include "qt-rand-adapter.hpp"
#include <api/csapi/content-repo.hpp>
#include <file-desc.hpp>
#include <QObject>
#include <QIODevice>
#include <QFile>
#include <QSaveFile>
#include <QPointer>
#include <QSharedPointer>
#include <QUrl>
#include <QMap>
#include <QMultiMap>
#include <QTemporaryDir>
#include <string>
#include <algorithm>
using namespace Qt::Literals::StringLiterals;
static const QString ROOMLESS = QStringLiteral("not-a-room");
struct KazvIOManagerPrivate
{
/**
* Used to store various temporary files.
* If the user does not specify a cache directory,
* this directory is also used as a cache directory.
*/
QTemporaryDir tmpDir = QTemporaryDir{};
/**
* Stores files that need to be previewed (e.g. in a timeline)
* and are not downloaded by the user, such as images, audio, video covers, and videos.
*/
QDir cacheDir{tmpDir.path()};
Kazv::RandomInterface randomGenerator = QtRandAdapter{};
QMap<QString, QPointer<KazvIODownloadJob>> cacheJobs;
QMap<QString, QSharedPointer<KazvIODownloadJob>> downloadJobs;
QMap<QString, QSharedPointer<UploadJobModel>> uploadJobs;
};
KazvIOManager::KazvIOManager(QObject *parent)
: QObject(parent)
, m_d(new KazvIOManagerPrivate)
{
}
KazvIOManager::~KazvIOManager() = default;
QPointer<KazvIODownloadJob> KazvIOManager::makeDownloadJob(
const QUrl &serverUrl, const QUrl &localFileUrl,
- const bool showPrograssBar, const QString &hash,
+ const bool showPrograssBar, const QString &hash, const QString &token,
const QString &key, const QString &iv) const
{
QPointer<KazvIODownloadJob> job{};
if (key.isEmpty()) {
- job = new KazvIODownloadJob{localFileUrl.toLocalFile(),
- serverUrl, showPrograssBar, hash, std::nullopt};
+ job = new KazvIODownloadJob{localFileUrl.toLocalFile(), serverUrl,
+ showPrograssBar, hash, token, std::nullopt};
} else {
auto aes = Kazv::AES256CTRDesc{key.toStdString(), iv.toStdString()};
- job = new KazvIODownloadJob{localFileUrl.toLocalFile(),
- serverUrl, showPrograssBar, hash, aes};
+ job = new KazvIODownloadJob{localFileUrl.toLocalFile(), serverUrl,
+ showPrograssBar, hash, token, aes};
}
return job;
}
-QUrl KazvIOManager::cacheFile(const QUrl &fileUrl, const QString &id, const QString &hash,
- const QString &key, const QString &iv)
+QUrl KazvIOManager::cacheFile(const QUrl &fileUrl, const QString &id,
+ const QString &hash, const QString &token, const QString &key,
+ const QString &iv)
{
const QString filePath = m_d->cacheDir.filePath(id);
QDir dir(m_d->cacheDir.path());
if (!dir.exists(id) && this->getCacheJob(id) == nullptr) {
auto job = this->makeDownloadJob(fileUrl,
QUrl::fromLocalFile(filePath),
- /* showProgressBar = */ false, hash, key, iv);
+ /* showProgressBar = */ false, hash, token, key, iv);
m_d->cacheJobs[id] = job;
connect(job, &KazvIOBaseJob::result, this, [this, id](auto /* result */) {
m_d->cacheJobs[id]->deleteLater();
m_d->cacheJobs.remove(id);
});
}
return QUrl::fromLocalFile(filePath);
}
-KazvIOBaseJob *KazvIOManager::startNewDownloadJob(const QUrl &serverUrl, const QUrl &localFileUrl,
- const QString &jobId, const QString &hash, const QString &key, const QString &iv)
+KazvIOBaseJob *KazvIOManager::startNewDownloadJob(const QUrl &serverUrl,
+ const QUrl &localFileUrl, const QString &jobId, const QString &hash,
+ const QString &token, const QString &key, const QString &iv)
{
auto downloadJob = this->makeDownloadJob(serverUrl, localFileUrl,
- /* showProgressBar = */ true, hash, key, iv);
+ /* showProgressBar = */ true, hash, token, key, iv);
m_d->downloadJobs[jobId].reset(downloadJob);
QQmlEngine::setObjectOwnership(downloadJob.data(), QQmlEngine::CppOwnership);
return downloadJob.data();
}
KazvIOBaseJob *KazvIOManager::startNewUploadJob(
const QUrl &serverUrl, const QUrl &localFileUrl, const QString &token,
const QString &roomId, MatrixRoomList *roomList, const bool encrypted,
const QString &relType, const QString &relatedTo)
{
auto uploadJob = QPointer<KazvIOUploadJob>();
if (encrypted) {
auto aes = Kazv::AES256CTRDesc::fromRandom(
m_d->randomGenerator.generateRange<Kazv::RandomData>(Kazv::AES256CTRDesc::randomSize));
uploadJob = new KazvIOUploadJob(localFileUrl.toLocalFile(), serverUrl,
/* showProgressBar = */ true, roomList, roomId, token, aes, relType, relatedTo);
} else {
uploadJob = new KazvIOUploadJob(localFileUrl.toLocalFile(), serverUrl,
/* showProgressBar = */ true, roomList, roomId, token, std::nullopt, relType, relatedTo);
}
if (!m_d->uploadJobs.contains(roomId) || m_d->uploadJobs[roomId].isNull()) {
m_d->uploadJobs[roomId].reset(new UploadJobModel());
}
m_d->uploadJobs[roomId]->addJob(uploadJob);
QQmlEngine::setObjectOwnership(uploadJob.data(), QQmlEngine::CppOwnership);
return uploadJob.data();
}
KazvIOBaseJob *KazvIOManager::startNewRoomlessUploadJob(const QUrl &serverUrl, const QUrl &localFileUrl, const QString &token)
{
return startNewUploadJob(
serverUrl, localFileUrl, token, ROOMLESS, 0, false,
QStringLiteral(""), QStringLiteral(""));
}
KazvIOBaseJob *KazvIOManager::getCacheJob(const QString &jobId) const
{
auto targetJob = m_d->cacheJobs.find(jobId);
if (targetJob == m_d->cacheJobs.end()) {
return nullptr;
}
QQmlEngine::setObjectOwnership(targetJob.value().data(), QQmlEngine::CppOwnership);
return targetJob.value().data();
}
KazvIOBaseJob *KazvIOManager::getDownloadJob(const QString &jobId)
{
auto targetJob = m_d->downloadJobs.find(jobId);
if (targetJob == m_d->downloadJobs.end()) {
return nullptr;
}
QQmlEngine::setObjectOwnership(targetJob.value().data(), QQmlEngine::CppOwnership);
return targetJob.value().data();
}
void KazvIOManager::deleteDownloadJob(const QString &jobId)
{
m_d->downloadJobs.remove(jobId);
}
UploadJobModel *KazvIOManager::getUploadJobs(const QString &roomId)
{
if (!m_d->uploadJobs.contains(roomId) || m_d->uploadJobs[roomId].isNull()) {
m_d->uploadJobs[roomId].reset(new UploadJobModel());
}
QQmlEngine::setObjectOwnership(m_d->uploadJobs[roomId].data(), QQmlEngine::CppOwnership);
return m_d->uploadJobs[roomId].data();
}
void KazvIOManager::deleteUploadJob(const QString &roomId, KazvIOBaseJob *job)
{
m_d->uploadJobs[roomId]->removeJob(QPointer<KazvIOUploadJob>(qobject_cast<KazvIOUploadJob*>(job)));
}
void KazvIOManager::deleteRoomlessUploadJob(KazvIOBaseJob *job)
{
deleteUploadJob(ROOMLESS, job);
}
void KazvIOManager::deleteModelIfEmpty(const QString &roomId)
{
if (!m_d->uploadJobs.contains(roomId)) {
return;
}
if (m_d->uploadJobs[roomId]->rowCount() == 0) {
m_d->uploadJobs.remove(roomId);
}
}
void KazvIOManager::clearJobs()
{
/**
* KazvIOBaseJob::cancel() for cache jobs will remove itself from cacheJobs,
* so iterators and loops for m_d->cacheJobs cannot be used reliably.
*/
for (auto key : m_d->cacheJobs.keys()) {
m_d->cacheJobs[key]->cancel();
}
std::for_each(m_d->downloadJobs.begin(), m_d->downloadJobs.end(), [this](auto job){
job->cancel();
});
std::for_each(m_d->uploadJobs.begin(), m_d->uploadJobs.end(), [this](auto room) {
room->clearJobs();
});
m_d->downloadJobs.clear();
m_d->uploadJobs.clear();
}
QString KazvIOManager::cacheDirectory() const
{
return m_d->cacheDir.path();
}
void KazvIOManager::setCacheDirectory(QString cacheDirectory)
{
if (cacheDirectory.isEmpty()) {
m_d->cacheDir = QDir{m_d->tmpDir.path()};
} else {
auto cacheDir = QDir{QUrl{cacheDirectory}.toLocalFile()};
if (!cacheDir.mkpath(u"."_s)) {
return;
}
m_d->cacheDir = cacheDir;
}
m_d->downloadJobs.clear();
Q_EMIT cacheDirectoryChanged();
}
diff --git a/src/kazv-io-manager.hpp b/src/kazv-io-manager.hpp
index dead687..e0e6114 100644
--- a/src/kazv-io-manager.hpp
+++ b/src/kazv-io-manager.hpp
@@ -1,74 +1,78 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include "kazv-io-job.hpp"
#include "matrix-room.hpp"
#include "upload-job-model.hpp"
#include "matrix-room-list.hpp"
#include "matrix-sdk.hpp"
#include <QObject>
#include <QtQml>
#include <QUrl>
#include <QString>
#include <KIO/TransferJob>
#include <QPointer>
#include <memory>
+using namespace Qt::Literals::StringLiterals;
+
struct KazvIOManagerPrivate;
class KazvIOManager : public QObject
{
Q_OBJECT
QML_ELEMENT
std::unique_ptr<KazvIOManagerPrivate> m_d;
Q_PROPERTY(QString cacheDirectory READ cacheDirectory WRITE setCacheDirectory NOTIFY cacheDirectoryChanged)
QPointer<KazvIODownloadJob> makeDownloadJob(
const QUrl &serverUrl, const QUrl &localFileUrl,
const bool showPrograssBar, const QString &hash,
- const QString &key = QStringLiteral(""),
- const QString &iv = QStringLiteral("")) const;
+ const QString &token = u""_s, const QString &key = u""_s,
+ const QString &iv = u""_s) const;
public:
explicit KazvIOManager(QObject *parent = 0);
~KazvIOManager() override;
/**
* Cache the file specified by fileUrl into a temporary folder
* Multiple calls do not repeat downloads
* Returns the local url of the cached file.
*/
- Q_INVOKABLE QUrl cacheFile(const QUrl &fileUrl, const QString &id, const QString &hash = QStringLiteral(""),
- const QString &key = QStringLiteral(""), const QString &iv = QStringLiteral(""));
- Q_INVOKABLE KazvIOBaseJob *startNewDownloadJob(const QUrl &serverUrl, const QUrl &localFileName,
- const QString &jobId, const QString &hash,
- const QString &key = QStringLiteral(""), const QString &iv = QStringLiteral(""));
+ Q_INVOKABLE QUrl cacheFile(const QUrl &fileUrl, const QString &id,
+ const QString &token = u""_s, const QString &hash = u""_s,
+ const QString &key = u""_s, const QString &iv = u""_s);
+ Q_INVOKABLE KazvIOBaseJob *startNewDownloadJob(const QUrl &serverUrl,
+ const QUrl &localFileName, const QString &jobId, const QString &hash,
+ const QString &token = u""_s, const QString &key = u""_s,
+ const QString &iv = u""_s);
Q_INVOKABLE KazvIOBaseJob *startNewUploadJob(
const QUrl &serverUrl, const QUrl &localFileName, const QString &token,
const QString &roomId, MatrixRoomList *roomList, const bool encrypted,
const QString &relType, const QString &relatedTo);
Q_INVOKABLE KazvIOBaseJob *startNewRoomlessUploadJob(const QUrl &serverUrl, const QUrl &localFileName, const QString &token);
Q_INVOKABLE KazvIOBaseJob *getCacheJob(const QString &jobId) const;
Q_INVOKABLE KazvIOBaseJob *getDownloadJob(const QString &jobId);
Q_INVOKABLE void deleteDownloadJob(const QString &jobId);
Q_INVOKABLE UploadJobModel *getUploadJobs(const QString &roomId);
Q_INVOKABLE void deleteUploadJob(const QString &roomId, KazvIOBaseJob *job);
Q_INVOKABLE void deleteRoomlessUploadJob(KazvIOBaseJob *job);
Q_INVOKABLE void deleteModelIfEmpty(const QString &roomId);
Q_INVOKABLE void clearJobs();
QString cacheDirectory() const;
void setCacheDirectory(QString cacheDirectory);
Q_SIGNALS:
void cacheDirectoryChanged();
};
diff --git a/src/matrix-sdk.cpp b/src/matrix-sdk.cpp
index e6c23b8..33c7b2e 100644
--- a/src/matrix-sdk.cpp
+++ b/src/matrix-sdk.cpp
@@ -1,807 +1,895 @@
/*
* 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 <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <fstream>
#include <filesystem>
#include <chrono>
#include <QMutex>
#include <QMutexLocker>
#include <QtConcurrent>
#include <QThreadPool>
#include <KConfig>
#include <KConfigGroup>
#include <eventemitter/lagerstoreeventemitter.hpp>
#include <client/sdk.hpp>
#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-room-list.hpp"
#include "matrix-promise.hpp"
#include "matrix-event.hpp"
#include "helper.hpp"
#include "kazv-path-config.hpp"
#include "kazv-version.hpp"
#include "qt-json.hpp"
#include "qt-rand-adapter.hpp"
#include "qt-promise-handler.hpp"
#include "qt-job-handler.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 "kazv-log.hpp"
#include "matrix-utils.hpp"
#include "kazv-session-lock-guard.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
static const std::string clientName = "kazv";
// Sdk with qt event loop, identity transform and no enhancers
using SdkT =
decltype(makeSdk(
SdkModel{},
detail::declref<JobInterface>(),
detail::declref<EventInterface>(),
QtPromiseHandler(detail::declref<QObject>()),
zug::identity,
withRandomGenerator(detail::declref<RandomInterface>())));
struct QtEventLoop
{
QObject *m_obj;
template<class Fn>
void async(Fn &&) { throw std::runtime_error{"not implemented!"}; }
template<class Fn>
void post(Fn &&fn)
{
QMetaObject::invokeMethod(
m_obj, std::forward<Fn>(fn), Qt::QueuedConnection
);
}
void finish() {}
void pause() { throw std::runtime_error{"not implemented!"}; }
void resume() { throw std::runtime_error{"not implemented!"}; }
};
std::filesystem::path sessionDirForUserAndDeviceId(std::filesystem::path userDataDir, std::string userId, std::string deviceId)
{
auto encodedUserId = encodeBase64(userId, Base64Opts::urlSafe);
auto sessionDir = userDataDir / "sessions"
/ encodedUserId / deviceId;
return sessionDir;
}
struct MatrixSdkPrivate
{
MatrixSdkPrivate(MatrixSdk *q, bool testing, std::unique_ptr<KazvSessionLockGuard> lockGuard);
MatrixSdkPrivate(MatrixSdk *q, bool testing, SdkModel model, std::unique_ptr<KazvSessionLockGuard> lockGuard);
bool testing;
std::string userDataDir;
std::unique_ptr<KazvSessionLockGuard> lockGuard;
RandomInterface randomGenerator;
QThread *thread;
QObject *obj;
QtJobHandler *jobHandler;
LagerStoreEventEmitter ee;
LagerStoreEventEmitter::Watchable watchable;
SdkT sdk;
QTimer saveTimer;
using SecondaryRootT = decltype(sdk.createSecondaryRoot(std::declval<lager::with_qt_event_loop>()));
SecondaryRootT secondaryRoot;
Client clientOnSecondaryRoot;
NotificationHandler notificationHandler;
void runIoContext() {
thread->start();
}
void stopIoContext() {
thread->quit();
}
void maybeSerialize()
{
if (!testing) {
serializeClientToFile(clientOnSecondaryRoot);
}
}
void serializeClientToFile(Client c);
};
// Cleaning up notes:
// 0. Callback functions may store the context for an indefinite time
// 1. The QThread event loop can be stopped
// 2. QtJobHandler::submit() should only happen in the primary event loop thread
// 3. QtJobHandler lives in the primary event loop thread
// 4. QtJobs live in the primary event loop thread
// 5. Job callbacks are called in the primary event loop thread
// 6. QtPromise::then() callbacks are called in the primary event loop thread
// 7. When the QThread event loop stops, no more callbacks will be executed (there is nothing to post to)
// 8. The QThread should stop before obj is deleted
class CleanupHelper : public QObject
{
Q_OBJECT
public:
explicit CleanupHelper(std::unique_ptr<MatrixSdkPrivate> d)
: oldD(std::move(d))
{
}
std::unique_ptr<MatrixSdkPrivate> oldD;
void cleanup()
{
qCInfo(kazvLog) << "start to clean up everything";
oldD->clientOnSecondaryRoot.stopSyncing()
.then([obj=oldD->obj, thread=oldD->thread](auto &&) {
qCDebug(kazvLog) << "stopped syncing";
QMetaObject::invokeMethod(obj, [thread]() {
thread->quit();
});
});
oldD->thread->wait();
oldD->thread->deleteLater();
// After the thread's event loop is finished, we can delete the root object
oldD->obj->deleteLater();
this->deleteLater();
qCInfo(kazvLog) << "thread is done";
}
};
void MatrixSdkPrivate::serializeClientToFile(Client c)
{
using namespace Kazv::CursorOp;
auto userId = +c.userId();
auto deviceId = +c.deviceId();
if (userId.empty() || deviceId.empty()) {
qDebug() << "Not logged in, nothing to serialize";
return;
}
using StdPath = std::filesystem::path;
auto userDataDir = StdPath(this->userDataDir);
auto sessionDir = sessionDirForUserAndDeviceId(userDataDir, userId, deviceId);
auto storeFile = sessionDir / "store";
auto storeFileNew = sessionDir / "store.new";
auto metadataFile = sessionDir / "metadata";
qDebug() << "storeFile=" << QString::fromStdString(storeFile.string());
std::error_code err;
if ((! std::filesystem::create_directories(sessionDir, err))
&& err) {
qDebug() << "Unable to create sessionDir";
return;
}
if (!lockGuard) {
try {
lockGuard = std::make_unique<KazvSessionLockGuard>(sessionDir);
} catch (const std::runtime_error &e) {
qCWarning(kazvLog) << "Error locking session: " << e.what();
return;
}
}
try {
auto storeStream = std::ofstream(storeFileNew);
if (! storeStream) {
qCWarning(kazvLog) << "Unable to open storeFile";
return;
}
using OAr = boost::archive::text_oarchive;
auto archive = OAr{storeStream};
c.serializeTo(archive);
} catch (const std::exception &e) {
qCWarning(kazvLog) << "Cannot write to store file: " << e.what();
return;
}
err.clear();
std::filesystem::rename(storeFileNew, storeFile, err);
if (err) {
qCWarning(kazvLog) << "Cannot move storeFile into place: " << QString::fromStdString(err.message());
return;
}
qDebug() << "Serialization done";
// store metadata
{
KConfig metadata(QString::fromStdString(metadataFile.string()));
KConfigGroup mdGroup(&metadata, u"Metadata"_s);
mdGroup.writeEntry("kazvVersion", QString::fromStdString(kazvVersionString()));
mdGroup.writeEntry("archiveFormat", "text");
}
}
MatrixSdkPrivate::MatrixSdkPrivate(MatrixSdk *q, bool testing, std::unique_ptr<KazvSessionLockGuard> lockGuard)
: testing(testing)
, userDataDir{kazvUserDataDir().toStdString()}
, lockGuard(std::move(lockGuard))
, randomGenerator(QtRandAdapter{})
, thread(new QThread())
, obj(new QObject())
, jobHandler(new QtJobHandler(obj))
, ee{QtEventLoop{obj}}
, watchable(ee.watchable())
, sdk(makeDefaultSdkWithCryptoRandom(
randomGenerator.generateRange<std::string>(makeDefaultSdkWithCryptoRandomSize()),
static_cast<JobInterface &>(*jobHandler),
static_cast<EventInterface &>(ee),
QtPromiseHandler(*obj),
zug::identity,
withRandomGenerator(randomGenerator)))
, secondaryRoot(sdk.createSecondaryRoot(QtEventLoop{q}))
, clientOnSecondaryRoot(sdk.clientFromSecondaryRoot(secondaryRoot))
, notificationHandler(clientOnSecondaryRoot.notificationHandler())
{
obj->moveToThread(thread);
}
MatrixSdkPrivate::MatrixSdkPrivate(MatrixSdk *q, bool testing, SdkModel model, std::unique_ptr<KazvSessionLockGuard> lockGuard)
: testing(testing)
, userDataDir{kazvUserDataDir().toStdString()}
, lockGuard(std::move(lockGuard))
, randomGenerator(QtRandAdapter{})
, thread(new QThread())
, obj(new QObject())
, jobHandler(new QtJobHandler(obj))
, ee{QtEventLoop{obj}}
, watchable(ee.watchable())
, sdk(makeSdk(
model,
static_cast<JobInterface &>(*jobHandler),
static_cast<EventInterface &>(ee),
QtPromiseHandler(*obj),
zug::identity,
withRandomGenerator(randomGenerator)))
, secondaryRoot(sdk.createSecondaryRoot(QtEventLoop{q}, std::move(model)))
, clientOnSecondaryRoot(sdk.clientFromSecondaryRoot(secondaryRoot))
, notificationHandler(clientOnSecondaryRoot.notificationHandler())
{
obj->moveToThread(thread);
}
MatrixSdk::MatrixSdk(std::unique_ptr<MatrixSdkPrivate> d, QObject *parent)
: QObject(parent)
, m_userDataDir(kazvUserDataDir())
, m_d(std::move(d))
{
init();
connect(this, &MatrixSdk::trigger,
this, [](KazvEvent e) {
qDebug() << "receiving trigger:";
if (std::holds_alternative<LoginSuccessful>(e)) {
qDebug() << "Login successful";
}
});
}
void MatrixSdk::init()
{
LAGER_QT(serverUrl) = m_d->clientOnSecondaryRoot.serverUrl().xform(strToQt); Q_EMIT serverUrlChanged(serverUrl());
LAGER_QT(userId) = m_d->clientOnSecondaryRoot.userId().xform(strToQt); Q_EMIT userIdChanged(userId());
LAGER_QT(token) = m_d->clientOnSecondaryRoot.token().xform(strToQt); Q_EMIT tokenChanged(token());
LAGER_QT(deviceId) = m_d->clientOnSecondaryRoot.deviceId().xform(strToQt); Q_EMIT deviceIdChanged(deviceId());
+ LAGER_QT(specVersions) = m_d->clientOnSecondaryRoot.supportVersions(); Q_EMIT specVersionsChanged(specVersions());
m_d->watchable.afterAll(
[this](KazvEvent e) {
Q_EMIT this->trigger(e);
});
m_d->watchable.after<LoginSuccessful>(
[this](LoginSuccessful e) {
Q_EMIT this->loginSuccessful(e);
});
m_d->watchable.after<LoginFailed>(
[this](LoginFailed e) {
Q_EMIT this->loginFailed(
QString::fromStdString(e.errorCode),
QString::fromStdString(e.error)
);
});
m_d->watchable.after<ReceivingRoomTimelineEvent>(
[this](ReceivingRoomTimelineEvent e) {
Q_EMIT this->receivedMessage(
QString::fromStdString(e.roomId),
QString::fromStdString(e.event.id())
);
});
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});
}
MatrixSdk::MatrixSdk(QObject *parent)
: MatrixSdk(std::make_unique<MatrixSdkPrivate>(this, /* testing = */ false, std::unique_ptr<KazvSessionLockGuard>()), parent)
{
}
MatrixSdk::MatrixSdk(SdkModel model, bool testing, QObject *parent)
: MatrixSdk(std::make_unique<MatrixSdkPrivate>(this, testing, std::move(model), std::unique_ptr<KazvSessionLockGuard>()), parent)
{
}
static void cleanupDPointer(std::unique_ptr<MatrixSdkPrivate> oldD)
{
oldD->saveTimer.disconnect();
oldD->saveTimer.stop();
auto helper = new CleanupHelper(std::move(oldD));
helper->cleanup();
}
MatrixSdk::~MatrixSdk()
{
if (m_d) {
serializeToFile();
cleanupDPointer(std::move(m_d));
}
}
QString MatrixSdk::mxcUriToHttp(QString mxcUri) const
{
return QString::fromStdString(m_d->clientOnSecondaryRoot.mxcUriToHttp(mxcUri.toStdString()));
}
+QString MatrixSdk::mxcUriToHttpAuthenticatedV1(QString mxcUri) const
+{
+ return QString::fromStdString(m_d->clientOnSecondaryRoot.mxcUriToHttpV1(mxcUri.toStdString()));
+}
+
MatrixDeviceList *MatrixSdk::devicesOfUser(QString userId) const
{
return new MatrixDeviceList(m_d->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 MatrixSdk::checkSpecVersion(QString version) const
+{
+ return std::find_if(specVersions().begin(), specVersions().end(),
+ [&version](auto v) {
+ return compareSpecVersion(QString::fromStdString(v), version);
+ }) != specVersions().end();
+}
+
+bool MatrixSdk::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();
+}
+
std::string MatrixSdk::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 MatrixSdk::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
);
};
auto validated = validateHomeserverUrl(homeserverUrl);
if (!validated.empty()) {
loginFunc(m_d->clientOnSecondaryRoot, validated);
} else {
m_d->clientOnSecondaryRoot
- .autoDiscover(userId.toStdString())
+ .autoDiscover(userId.toStdString()) // autoDiscover() will dispatch GetVersionAction to get supported versions of the server
.then([
this,
client=m_d->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;
});
}
m_d->runIoContext();
}
void MatrixSdk::logout()
{
m_d->clientOnSecondaryRoot.logout()
.then([&] (EffectStatus stat) {
if (stat.success()) {
m_d->stopIoContext();
Q_EMIT this->logoutSuccessful();
} else {
Q_EMIT this->logoutFailed(QString::fromStdString(stat.dataStr("errorCode")), QString::fromStdString(stat.dataStr("error")));
}
});
}
MatrixRoomList *MatrixSdk::roomList() const
{
return new MatrixRoomList(m_d->clientOnSecondaryRoot);
}
void MatrixSdk::emplace(std::optional<SdkModel> model, std::unique_ptr<KazvSessionLockGuard> lockGuard)
{
auto testing = m_d->testing;
auto userDataDir = m_d->userDataDir;
if (m_d) {
cleanupDPointer(std::move(m_d));
}
m_d = (model.has_value()
? std::make_unique<MatrixSdkPrivate>(this, testing, std::move(model.value()), std::move(lockGuard))
: std::make_unique<MatrixSdkPrivate>(this, testing, std::move(lockGuard)));
m_d->userDataDir = userDataDir;
// Re-initialize lager-qt cursors and watchable connections
init();
m_d->runIoContext();
m_d->clientOnSecondaryRoot.startSyncing();
Q_EMIT sessionChanged();
}
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();
}
auto MatrixSdk::loadSession(QString sessionName) -> LoadSessionResult
{
using StdPath = std::filesystem::path;
std::unique_ptr<KazvSessionLockGuard> lockGuard;
auto loadFromSession = [this, sessionName, &lockGuard](StdPath sessionDir) {
auto storeFile = sessionDir / "store";
auto metadataFile = sessionDir / "metadata";
if (! std::filesystem::exists(storeFile)) {
qDebug() << "storeFile does not exist, skip loading session " << sessionName;
return SessionNotFound;
}
if (std::filesystem::exists(metadataFile)) {
KConfig metadata(QString::fromStdString(metadataFile.string()));
KConfigGroup mdGroup(&metadata, u"Metadata"_s);
auto format = mdGroup.readEntry(u"archiveFormat"_s);
if (format != QStringLiteral("text")) {
qDebug() << "Unknown archive format:" << format;
return SessionFormatUnknown;
}
auto version = mdGroup.readEntry(u"kazvVersion"_s);
auto curVersion = kazvVersionString();
if (version != QString::fromStdString(curVersion)) {
qDebug() << "A different version from the current one, making a backup";
std::error_code err;
auto now = std::chrono::system_clock::now();
auto backupName =
std::to_string(std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count());
auto backupDir = sessionDir / "backup" / backupName;
if (! std::filesystem::create_directories(backupDir, err)
&& err) {
qDebug() << "Cannot create backup directory";
return SessionCannotBackup;
}
std::filesystem::copy_file(storeFile, backupDir / "store");
std::filesystem::copy_file(metadataFile, backupDir / "metadata");
}
}
SdkModel model;
try {
auto storeStream = std::ifstream(storeFile);
if (! storeStream) {
qDebug() << "Unable to open storeFile";
return SessionCannotOpenFile;
}
using IAr = boost::archive::text_iarchive;
auto archive = IAr{storeStream};
archive >> model;
qDebug() << "Finished loading session";
} catch (const std::exception &e) {
qDebug() << "Error when loading session:" << QString::fromStdString(e.what());
return SessionDeserializeFailed;
}
emplace(std::move(model), std::move(lockGuard));
return SessionLoadSuccess;
};
qDebug() << "in loadSession(), 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);
try {
lockGuard = std::make_unique<KazvSessionLockGuard>(sessionDir);
} catch (const std::runtime_error &e) {
qCWarning(kazvLog) << "Error locking session: " << e.what();
return SessionLockFailed;
}
return loadFromSession(sessionDir);
}
qDebug(kazvLog) << "no session found for" << sessionName;
return SessionNotFound;
}
bool MatrixSdk::deleteSession(QString sessionName) {
using StdPath = std::filesystem::path;
qDebug() << "in deleteSession(), sessionName=" << sessionName;
auto userDataDir = StdPath(kazvUserDataDir().toStdString());
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()
{
emplace(std::nullopt, std::unique_ptr<KazvSessionLockGuard>());
return true;
}
static std::optional<std::string> optMaybe(QString s)
{
if (s.isEmpty()) {
return std::nullopt;
} else {
return s.toStdString();
}
}
MatrixPromise *MatrixSdk::createRoom(
bool isPrivate,
const QString &name,
const QString &alias,
const QStringList &invite,
bool isDirect,
bool allowFederate,
const QString &topic,
const QJsonValue &powerLevelContentOverride,
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_d->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 *MatrixSdk::joinRoom(const QString &idOrAlias, const QStringList &servers)
{
return new MatrixPromise(m_d->clientOnSecondaryRoot.joinRoom(
idOrAlias.toStdString(),
qStringListToStdF(servers)
));
}
MatrixPromise *MatrixSdk::setDeviceTrustLevel(QString userId, QString deviceId, QString trustLevel)
{
return new MatrixPromise(
m_d->clientOnSecondaryRoot.setDeviceTrustLevel(
userId.toStdString(),
deviceId.toStdString(),
qStringToTrustLevelFunc(trustLevel)
)
);
}
MatrixPromise *MatrixSdk::getSelfProfile()
{
return new MatrixPromise(
m_d->clientOnSecondaryRoot.getProfile(userId().toStdString())
);
}
MatrixPromise *MatrixSdk::setDisplayName(QString displayName)
{
return new MatrixPromise(
m_d->clientOnSecondaryRoot.setDisplayName(
displayName.isEmpty() ? std::nullopt : std::optional<std::string>(displayName.toStdString())
)
);
}
MatrixPromise *MatrixSdk::setAvatarUrl(QString avatarUrl)
{
return new MatrixPromise(
m_d->clientOnSecondaryRoot.setAvatarUrl(
avatarUrl.isEmpty() ? std::nullopt : std::optional<std::string>(avatarUrl.toStdString())
)
);
}
void MatrixSdk::startThread()
{
m_d->runIoContext();
}
RandomInterface &MatrixSdk::randomGenerator() const
{
return m_d->randomGenerator;
}
bool MatrixSdk::shouldNotify(MatrixEvent *event) const
{
// Do not notify own event
if (event->sender() == userId()) {
return false;
}
return m_d->notificationHandler.handleNotification(event->underlyingEvent()).shouldNotify;
}
bool MatrixSdk::shouldPlaySound(MatrixEvent *event) const
{
return m_d->notificationHandler.handleNotification(event->underlyingEvent()).sound.has_value();
}
MatrixStickerPackList *MatrixSdk::stickerPackList() const
{
return new MatrixStickerPackList(m_d->clientOnSecondaryRoot);
}
MatrixEvent *MatrixSdk::stickerRoomsEvent() const
{
return new MatrixEvent(m_d->clientOnSecondaryRoot.accountData()[imagePackRoomsEventType][lager::lenses::or_default]);
}
MatrixPromise *MatrixSdk::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_d->clientOnSecondaryRoot
.room(source.roomId)
.sendStateEvent(Event(std::move(eventJson))));
} else {
return 0;
}
}
MatrixUserGivenAttrsMap *MatrixSdk::userGivenNicknameMap() const
{
return new MatrixUserGivenAttrsMap(
userGivenNicknameMapFor(m_d->clientOnSecondaryRoot),
[client=m_d->clientOnSecondaryRoot](json content) {
return client.setAccountData(json{
{"type", USER_GIVEN_NICKNAME_EVENT_TYPES[0]},
{"content", std::move(content)},
});
}
);
}
MatrixPromise *MatrixSdk::sendAccountData(const QString &type, const QJsonObject &content)
{
Event e = json{
{"type", type.toStdString()},
{"content", content},
};
return sendAccountDataImpl(std::move(e));
}
MatrixPromise *MatrixSdk::sendAccountDataImpl(Event event)
{
return new MatrixPromise(m_d->clientOnSecondaryRoot.setAccountData(event));
}
+MatrixPromise *MatrixSdk::getSpecVersions()
+{
+ return new MatrixPromise(m_d->clientOnSecondaryRoot.getVersions(LAGER_QT(serverUrl).get().toStdString()));
+}
+
void MatrixSdk::setUserDataDir(const std::string &userDataDir)
{
m_d->userDataDir = userDataDir;
}
#include "matrix-sdk.moc"
diff --git a/src/matrix-sdk.hpp b/src/matrix-sdk.hpp
index 42e2a0d..2074ef8 100644
--- a/src/matrix-sdk.hpp
+++ b/src/matrix-sdk.hpp
@@ -1,293 +1,315 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include <QObject>
#include <QQmlEngine>
#include <QString>
#include <memory>
#include <filesystem>
#include <lager/extra/qt.hpp>
#include <sdk-model.hpp>
#include <random-generator.hpp>
#include "meta-types.hpp"
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")
class MatrixRoomList;
class MatrixDeviceList;
class MatrixPromise;
class MatrixSdkTest;
class MatrixSdkSessionsTest;
class MatrixEvent;
class MatrixStickerPackList;
class MatrixUserGivenAttrsMap;
class KazvSessionLockGuard;
struct MatrixSdkPrivate;
std::filesystem::path sessionDirForUserAndDeviceId(std::filesystem::path userDataDir, std::string userId, std::string deviceId);
class MatrixSdk : public QObject
{
Q_OBJECT
QML_ELEMENT
QString m_userDataDir;
std::unique_ptr<MatrixSdkPrivate> m_d;
/// @param d A dynamically allocated d-pointer, whose ownership
/// will be transferred to this MatrixSdk.
explicit MatrixSdk(std::unique_ptr<MatrixSdkPrivate> d, QObject *parent);
void init();
public:
enum CreateRoomPreset {
PrivateChat = Kazv::CreateRoomPreset::PrivateChat,
PublicChat = Kazv::CreateRoomPreset::PublicChat,
TrustedPrivateChat = Kazv::CreateRoomPreset::TrustedPrivateChat,
};
Q_ENUM(CreateRoomPreset);
enum LoadSessionResult {
/// Successfully loaded the session
SessionLoadSuccess,
/// There is no store file
SessionNotFound,
/// The format of the store file is not supported
SessionFormatUnknown,
/// The store file cannot be backed up
SessionCannotBackup,
/// Cannot grab the lock on the session file
SessionLockFailed,
/// Cannot open store file
SessionCannotOpenFile,
/// Cannot deserialize the store file
SessionDeserializeFailed,
};
Q_ENUM(LoadSessionResult);
+ enum MediaDownloadEndpointVersion {
+ UnauthenticatedMediaV3, // Classical endpoint series
+ AuthenticatedMediaV1, // Endpoints that needs authorization
+ };
+
+ Q_ENUM(MediaDownloadEndpointVersion);
+
explicit MatrixSdk(QObject *parent = 0);
~MatrixSdk() override;
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;
+
Kazv::RandomInterface &randomGenerator() const;
private:
// Replaces the store with another one
void emplace(std::optional<Kazv::SdkModel> model, std::unique_ptr<KazvSessionLockGuard> lockGuard);
static std::string validateHomeserverUrl(const QString &url);
Q_SIGNALS:
void trigger(Kazv::KazvEvent e);
void loginSuccessful(Kazv::KazvEvent e);
void loginFailed(QString errorCode, QString errorMsg);
void discoverFailed(QString errorCode, QString errorMsg);
void logoutSuccessful();
void logoutFailed(QString errorCode, QString errorMsg);
void receivedMessage(QString roomId, QString eventId);
void sessionChanged();
public Q_SLOTS:
void login(const QString &userId, const QString &password, const QString &homeserverUrl);
void logout();
/**
* Serialize data to <AppDataDir>/sessions/<userid>/<deviceid>/
*
* If not logged in, do nothing.
*/
void serializeToFile() const;
/**
* Load session at <AppDataDir>/sessions/<sessionName> .
*
* @param sessionName A string in the form of <userid>/<deviceid> .
*
* @return true if successful, false otherwise.
*/
LoadSessionResult loadSession(QString sessionName);
/**
* Delete session at <AppDataDir>/sessions/<sessionName> .
*
* @param sessionName A string in the form of <userid>/<deviceid> .
*
* @return true if successful, false otherwise.
*/
bool deleteSession(QString sessionName);
/**
* Start an empty session.
*
* The new session is not logged in, and need to call login().
*
* @return true if successful, false otherwise.
*/
bool startNewSession();
/**
* Get all saved sessions.
*
* @return A list of session names in the form of <userid>/<deviceid> .
*/
QStringList allSessions() const;
/**
* 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,
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();
+
private:
MatrixPromise *sendAccountDataImpl(Kazv::Event event);
private: // Testing
friend MatrixSdkTest;
friend MatrixSdkSessionsTest;
friend MatrixSdk *makeTestSdk(Kazv::SdkModel model);
void setUserDataDir(const std::string &userDataDir);
explicit MatrixSdk(Kazv::SdkModel model, bool testing = false, QObject *parent = 0);
void startThread();
};
diff --git a/src/tests/kazv-io-job-test.cpp b/src/tests/kazv-io-job-test.cpp
index ca2a8fc..40c891c 100644
--- a/src/tests/kazv-io-job-test.cpp
+++ b/src/tests/kazv-io-job-test.cpp
@@ -1,391 +1,418 @@
/*
* 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();
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();
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();
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();
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();
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();
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();
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();
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();
QCOMPARE(downloadFile.readAll(), savedFile.readAll());
downloadFile.close();
savedFile.close();
}
void KazvIOJobTest::testUploadPause()
{
QTemporaryFile file;
file.open();
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();
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();
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();
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);
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();
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();
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();
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/matrix-sdk-test.cpp b/src/tests/matrix-sdk-test.cpp
index 5a74b75..73336ae 100644
--- a/src/tests/matrix-sdk-test.cpp
+++ b/src/tests/matrix-sdk-test.cpp
@@ -1,87 +1,113 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <memory>
#include <QtTest>
#include <QSignalSpy>
#include <matrix-sdk.hpp>
#include <matrix-device-list.hpp>
#include <matrix-device.hpp>
#include <matrix-promise.hpp>
#include "test-model.hpp"
#include "test-utils.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
class MatrixSdkTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testDevicesOfUser();
void testSetDeviceTrustLevel();
void testValidateHomeserverUrl();
+ void testSpecVersion();
};
void MatrixSdkTest::testDevicesOfUser()
{
auto model = makeTestModel();
std::unique_ptr<MatrixSdk> sdk{new MatrixSdk(model, /* testing = */ true)};
auto devices = toUniquePtr(sdk->devicesOfUser(QStringLiteral("@test1:test1.org")));
QVERIFY(devices->count() == 2);
auto dev = toUniquePtr(devices->at(0));
QVERIFY(dev->deviceId() == u"device1"_s);
QVERIFY(dev->trustLevel() == u"unseen"_s);
}
void MatrixSdkTest::testSetDeviceTrustLevel()
{
auto model = makeTestModel();
std::unique_ptr<MatrixSdk> sdk{new MatrixSdk(model, /* testing = */ true)};
sdk->startThread();
auto promise = toUniquePtr(
sdk->setDeviceTrustLevel(
QStringLiteral("@test1:test1.org"),
QStringLiteral("device1"),
QStringLiteral("verified")
)
);
auto spy = QSignalSpy(promise.get(), &MatrixPromise::succeeded);
spy.wait();
auto devices = toUniquePtr(sdk->devicesOfUser(QStringLiteral("@test1:test1.org")));
QVERIFY(devices->count() == 2);
auto dev = toUniquePtr(devices->at(0));
QVERIFY(dev->deviceId() == u"device1"_s);
QTRY_VERIFY(dev->trustLevel() == u"verified"_s); // wait for the change to propagate to this thread
}
void MatrixSdkTest::testValidateHomeserverUrl()
{
auto v = [](const QString &u) {
return QString::fromStdString(MatrixSdk::validateHomeserverUrl(u));
};
QCOMPARE(v(u"example.com"_s), u"https://example.com"_s);
QCOMPARE(v(u"to"_s), u"https://to"_s);
QVERIFY(v(u""_s).isEmpty());
QVERIFY(v(u"/usr/libexec"_s).isEmpty());
QCOMPARE(v(u"ea.pl"_s), u"https://ea.pl"_s);
}
+void MatrixSdkTest::testSpecVersion()
+{
+ auto model = makeTestModel();
+ model.client.versions = immer::array{"v1.11"s, "v1.12"s};
+ std::unique_ptr<MatrixSdk> sdk{new MatrixSdk(model, /* testing = */ true)};
+ QVERIFY(sdk->checkSpecVersion(u"r0.5.0"_s));
+ QVERIFY(sdk->checkSpecVersion(u"v1.10"_s));
+ QVERIFY(sdk->checkSpecVersion(u"v1.11"_s));
+ QVERIFY(!sdk->checkSpecVersion(u"v1.13"_s));
+ QVERIFY(!sdk->checkSpecVersion(u"mew"_s));
+ QVERIFY(sdk->checkSpecVersionRange(u"r0.5.0"_s, u"v1.12"_s));
+ QVERIFY(sdk->checkSpecVersionRange(u"v1.10"_s, u"v1.12"_s));
+ QVERIFY(sdk->checkSpecVersionRange(u"v1.11"_s, u"v1.13"_s));
+ QVERIFY(!sdk->checkSpecVersionRange(u"r0.4.0"_s, u"v1.9"_s));
+ QVERIFY(!sdk->checkSpecVersionRange(u"v1.13"_s, u"v1.14"_s));
+ model.client.versions = immer::array{"r0.4.0"s};
+ sdk.reset(new MatrixSdk(model, /* testing = */ true));
+ QVERIFY(sdk->checkSpecVersion(u"r0.4.0"_s));
+ QVERIFY(!sdk->checkSpecVersion(u"v1.1"_s));
+ QVERIFY(sdk->checkSpecVersionRange(u"r0.3.5"_s, u"r0.4.0"_s));
+ QVERIFY(!sdk->checkSpecVersionRange(u"r0.3.0"_s, u"r0.3.5"_s));
+ QVERIFY(!sdk->checkSpecVersionRange(u"r0.4.1"_s, u"r0.4.5"_s));
+ QVERIFY(!sdk->checkSpecVersionRange(u"v1.1"_s, u"v1.2"_s));
+}
+
QTEST_MAIN(MatrixSdkTest)
#include "matrix-sdk-test.moc"
diff --git a/src/tests/quick-tests/test-helpers/KazvIOManagerMock.qml b/src/tests/quick-tests/test-helpers/KazvIOManagerMock.qml
index 03fd94b..d80d402 100644
--- a/src/tests/quick-tests/test-helpers/KazvIOManagerMock.qml
+++ b/src/tests/quick-tests/test-helpers/KazvIOManagerMock.qml
@@ -1,29 +1,31 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
QtObject {
id: managerMock
property var startNewUploadJob: mockHelper.noop([
'serverUrl',
'fileUrl',
'token',
'roomId',
'roomList',
'encrypted',
'draftRelType',
'draftRelatedTo'
])
property var deleteDownloadJob: mockHelper.noop()
property var deleteUploadJob: mockHelper.noop()
property var cacheFile: mockHelper.noop()
+ property var getCacheJob: mockHelper.noop()
+ property var startNewDownloadJob: mockHelper.noop()
function getDownloadJob (jobId) {
const component = Qt.createComponent("KazvIOJobMock.qml");
return component.createObject(managerMock);
}
}
diff --git a/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml b/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml
index 16e7730..70e6460 100644
--- a/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml
+++ b/src/tests/quick-tests/test-helpers/MatrixSdkMock.qml
@@ -1,44 +1,55 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
QtObject {
property var userId: ''
property var serverUrl: 'https://example.com'
property var token: 'token'
property var updateStickerPack: mockHelper.promise()
property var createRoom: mockHelper.promise([
'isPrivate',
'name',
'alias',
'invites',
'isDirect',
'allowFederate',
'topic',
'powerLevelContentOverride',
'preset',
'encrypted',
])
property var joinRoom: mockHelper.promise()
property var sendAccountData: mockHelper.promise()
property var login: mockHelper.noop()
property var sessions: []
+ property var specVersions: ["v1.11"]
+
function allSessions() {
return sessions;
}
function mxcUriToHttp (uri) {
console.log('mxcUriToHttp');
return uri || '';
}
+ function mxcUriToHttpAuthenticatedV1 (uri) {
+ console.log('mxcUriToHttpAuthenticatedV1');
+ return uri || '';
+ }
+
function devicesOfUser (userId) {
return [];
}
+
+ function checkSpecVersion (ver) {
+ return !!specVersions.includes(ver);
+ }
}
diff --git a/src/tests/quick-tests/tst_FileHandler.qml b/src/tests/quick-tests/tst_FileHandler.qml
new file mode 100644
index 0000000..5e9401f
--- /dev/null
+++ b/src/tests/quick-tests/tst_FileHandler.qml
@@ -0,0 +1,129 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2025 nannanko <nannanko@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+import QtTest
+
+import '../../contents/ui' as Kazv
+import 'test-helpers' as QmlHelpers
+import 'test-helpers.js' as JsHelpers
+
+QmlHelpers.TestItem {
+ id: item
+
+ property var unencryptedEventContent: ({
+ body: "fileName",
+ msgtype: "m.image",
+ url: "mxc://some",
+ })
+
+ property var encryptedEventContent: ({
+ body: "fileName",
+ msgtype: "m.image",
+ file: {
+ url: "mxc://some",
+ key: {
+ kty: "oct",
+ key_ops: ["encrypt","decrypt"],
+ alg: "A256CTR",
+ k: "somekey",
+ ext: true,
+ },
+ iv: "someiv",
+ hashes: {
+ "sha256": "somesha256hash"
+ },
+ v: "v2",
+ }
+ })
+
+ property var oldFileHandler: Kazv.FileHandler {
+ eventContent: unencryptedEventContent
+ autoCache: true
+ kazvIOManager: QmlHelpers.KazvIOManagerMock {}
+ matrixSdk: QmlHelpers.MatrixSdkMock {
+ specVersions: []
+ property var mxcUriToHttp: mockHelper.noop()
+ }
+ }
+
+ property var newFileHandler: Kazv.FileHandler {
+ eventContent: unencryptedEventContent
+ autoCache: true
+ kazvIOManager: QmlHelpers.KazvIOManagerMock {}
+ matrixSdk: QmlHelpers.MatrixSdkMock {
+ property var mxcUriToHttpAuthenticatedV1: mockHelper.noop()
+ }
+ }
+
+ property var unencryptedFileHandler: Kazv.FileHandler {
+ eventContent: unencryptedEventContent
+ autoCache: true
+ kazvIOManager: QmlHelpers.KazvIOManagerMock {}
+ matrixSdk: QmlHelpers.MatrixSdkMock {}
+ }
+
+ property var encryptedFileHandler: Kazv.FileHandler {
+ eventContent: encryptedEventContent
+ autoCache: true
+ kazvIOManager: QmlHelpers.KazvIOManagerMock {}
+ matrixSdk: QmlHelpers.MatrixSdkMock {}
+ }
+
+ TestCase {
+ id: fileHandlerTest
+ name: 'fileHandlerTest'
+ when: windowShown
+
+ function test_specVersion() {
+ compare(oldFileHandler.matrixSdk.mxcUriToHttp.calledTimes(), 1);
+ compare(newFileHandler.matrixSdk.mxcUriToHttpAuthenticatedV1.calledTimes(), 1);
+
+ compare(oldFileHandler.kazvIOManager.cacheFile.calledTimes(), 1);
+ compare(oldFileHandler.kazvIOManager.cacheFile.lastArgs()[3], "");
+ compare(newFileHandler.kazvIOManager.cacheFile.calledTimes(), 1);
+ compare(newFileHandler.kazvIOManager.cacheFile.lastArgs()[3], newFileHandler.matrixSdk.token);
+
+ oldFileHandler.downloadFile("fileUrl");
+ compare(oldFileHandler.kazvIOManager.startNewDownloadJob.calledTimes(), 1);
+ compare(oldFileHandler.kazvIOManager.startNewDownloadJob.lastArgs()[4], "");
+ newFileHandler.downloadFile("fileUrl");
+ compare(newFileHandler.kazvIOManager.startNewDownloadJob.calledTimes(), 1);
+ compare(newFileHandler.kazvIOManager.startNewDownloadJob.lastArgs()[4], newFileHandler.matrixSdk.token);
+ }
+
+ function test_autoCache() {
+ compare(unencryptedFileHandler.kazvIOManager.cacheFile.calledTimes(), 1);
+ compare(encryptedFileHandler.kazvIOManager.cacheFile.calledTimes(), 1);
+ }
+
+ function test_encrypted() {
+ let downloadFunc = encryptedFileHandler.kazvIOManager.startNewDownloadJob;
+ encryptedFileHandler.downloadFile("fileUrl");
+ compare(downloadFunc.calledTimes(), 1);
+ compare(downloadFunc.lastArgs()[0], encryptedFileHandler.matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://some"));
+ compare(downloadFunc.lastArgs()[1], "fileUrl");
+ compare(downloadFunc.lastArgs()[3], "somesha256hash");
+ compare(downloadFunc.lastArgs()[4], encryptedFileHandler.matrixSdk.token);
+ compare(downloadFunc.lastArgs()[5], "somekey");
+ compare(downloadFunc.lastArgs()[6], "someiv");
+ }
+
+ function test_unencrypted() {
+ let downloadFunc = unencryptedFileHandler.kazvIOManager.startNewDownloadJob;
+ unencryptedFileHandler.downloadFile("fileUrl");
+ compare(downloadFunc.calledTimes(), 1);
+
+ let args = downloadFunc.lastArgs();
+ compare(args[0], unencryptedFileHandler.matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://some"));
+ compare(args[1], "fileUrl");
+ compare(args[3], "");
+ compare(args[4], unencryptedFileHandler.matrixSdk.token);
+ compare(args[5], "");
+ compare(args[6], "");
+ }
+ }
+}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Oct 11, 12:58 PM (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785787
Default Alt Text
(108 KB)

Event Timeline