Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85805482
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
108 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Oct 11, 12:58 PM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785787
Default Alt Text
(108 KB)
Attached To
Mode
rK kazv
Attached
Detach File
Event Timeline
Log In to Comment