Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85802829
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
47 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/packaging/GNU-Linux/appimage/build-unprivileged.sh b/packaging/GNU-Linux/appimage/build-unprivileged.sh
new file mode 100755
index 0000000..b23d1ec
--- /dev/null
+++ b/packaging/GNU-Linux/appimage/build-unprivileged.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+export XDG_RUNTIME_DIR=/run/user/test
+
+cmake .. -DCMAKE_INSTALL_PREFIX="$KAZV_INSTALL_DIR" \
+ -DCMAKE_PREFIX_PATH="$LIBKAZV_INSTALL_DIR;$DEPS_INSTALL_DIR" -DCMAKE_BUILD_TYPE=$BUILD_TYPE -Dkazv_KF_QT_MAJOR_VERSION=$KF_VER -Dkazv_LINK_BREEZE_ICONS=ON && \
+ make -j$JOBS && \
+ make -j$JOBS DESTDIR=AppDir install && \
+ dbus-launch --exit-with-session -- bash "$thisDir"/test.sh
diff --git a/packaging/GNU-Linux/appimage/build.sh b/packaging/GNU-Linux/appimage/build.sh
index 9146919..b47babe 100755
--- a/packaging/GNU-Linux/appimage/build.sh
+++ b/packaging/GNU-Linux/appimage/build.sh
@@ -1,112 +1,111 @@
#!/bin/bash
set -x
set -e
+export thisDir="$(readlink -f "$(dirname "$(realpath "$0")")")"
+
# Collect deps
wget https://raw.githubusercontent.com/KDE/docker-neon/Neon/stable/public.key && \
apt-key add public.key && \
rm public.key
DEP_PACKAGES=(
extra-cmake-modules
wget
weston
dbus-x11
libcmark-dev
)
if [[ $KF_VER == 5 ]]; then
echo "Building on KF5 is no longer supported."
exit 1
else
DEP_PACKAGES+=(
qt6-base-dev
qt6-declarative-dev
qt6-multimedia-dev
qt6-imageformats
qml6-module-qtmultimedia
libqt6concurrent6
libqt6gui6
libqt6svg6-dev
libqt6quickcontrols2-6
qml6-module-qtquick-controls
qml6-module-qtquick-layouts
qml6-module-qt-labs-qmlmodels
kf6-kirigami2-dev
libkf6config-dev
qt6-gtk-platformtheme
plasma-integration
libkf6kio-dev
qml6-module-qttest
qml6-module-qt-labs-platform
libkf6notifications-dev
qml6-module-qtquick-dialogs
qml6-module-org-kde-notification
kf6-kirigami-addons-dev
kf6-kcoreaddons-dev
qml6-module-org-kde-qqc2desktopstyle
kf6-qqc2-desktop-style-dev
kf6-breeze-icon-theme-dev
cmark
)
export QMAKE=qmake6
cp -v packaging/GNU-Linux/appimage/kde-neon-jammy.list /etc/apt/sources.list.d/
fi
apt-get update && \
apt-get -y upgrade && \
DEBIAN_FRONTEND="noninteractive" \
apt-get -y install aptitude && \
aptitude -y install "${DEP_PACKAGES[@]}"
-# Actual build
-mkdir -pv /run/user/test
-export XDG_RUNTIME_DIR=/run/user/test
-
-thisDir="$(readlink -f "$(dirname "$(realpath "$0")")")"
-
-BUILD_TYPE="$1"
+export BUILD_TYPE="$1"
if [[ ! "$BUILD_TYPE" ]]; then
export BUILD_TYPE=Debug
fi
+
+useradd -U builder
+mkdir build
+chown builder:builder build
+cd build
+
export JOBS=${JOBS:-$(( $(nproc) / 2))} DEPS_INSTALL_DIR=/opt/libkazv-deps LIBKAZV_INSTALL_DIR=/opt/libkazv KAZV_INSTALL_DIR=/usr
export LD_LIBRARY_PATH="$LIBKAZV_INSTALL_DIR/lib:$LIBKAZV_INSTALL_DIR/lib/x86_64-linux-gnu:$DEPS_INSTALL_DIR/lib:$DEPS_INSTALL_DIR/lib/x86_64-linux-gnu"
-mkdir build && cd build && \
- cmake .. -DCMAKE_INSTALL_PREFIX="$KAZV_INSTALL_DIR" \
- -DCMAKE_PREFIX_PATH="$LIBKAZV_INSTALL_DIR;$DEPS_INSTALL_DIR" -DCMAKE_BUILD_TYPE=$BUILD_TYPE -Dkazv_KF_QT_MAJOR_VERSION=$KF_VER -Dkazv_LINK_BREEZE_ICONS=ON && \
- make -j$JOBS && \
- make -j$JOBS DESTDIR=AppDir install && \
- dbus-launch --exit-with-session -- bash "$thisDir"/test.sh
+mkdir -pv /run/user/test
+chown builder:builder /run/user/test
+su builder -c "$thisDir"/build-unprivileged.sh || exit 1
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
chmod +x linuxdeploy*.AppImage
export QML_SOURCES_PATHS=../src
./linuxdeploy-plugin-qt-x86_64.AppImage --appimage-extract
./linuxdeploy-x86_64.AppImage --appimage-extract
# https://github.com/linuxdeploy/linuxdeploy/issues/154
mv -v ./linuxdeploy-plugin-qt-x86_64.AppImage ./linuxdeploy-notplugin-qt-x86_64.AppImage
# Enable the appimage to use Plasma themes, linuxdeploy-plugin-qt will only copy the gtk3 plugin -((
mkdir -pv AppDir/usr/plugins/platformthemes/
cp /usr/lib/x86_64-linux-gnu/qt6/plugins/platformthemes/KDEPlasmaPlatformTheme6.so AppDir/usr/plugins/platformthemes/ || true
mkdir -pv AppDir/usr/qml/org/kde
cp -rv /usr/lib/x86_64-linux-gnu/qt6/qml/org/kde/desktop AppDir/usr/qml/org/kde
cp -rv /usr/lib/x86_64-linux-gnu/qt6/qml/org/kde/qqc2desktopstyle AppDir/usr/qml/org/kde
mkdir -pv AppDir/usr/plugins/kf6/kirigami/platform
cp -v /usr/lib/x86_64-linux-gnu/qt6/plugins/kf6/kirigami/platform/org.kde.desktop.so AppDir/usr/plugins/kf6/kirigami/platform/
# Workaround "cannot mix incompatible qt versions" problem
mkdir -pv AppDir/apprun-hooks/
cp -v "$thisDir"/hooks/*.sh AppDir/apprun-hooks/
./squashfs-root/AppRun --appdir AppDir -i ../icons/kazvlogo-256.png --plugin qt --output appimage # -l /lib/x86_64-linux-gnu/libstdc++.so.6
cp kazv*.AppImage ../kazv-"$BUILD_TYPE".AppImage
diff --git a/src/matrix-sdk.cpp b/src/matrix-sdk.cpp
index 6988382..f3160e0 100644
--- a/src/matrix-sdk.cpp
+++ b/src/matrix-sdk.cpp
@@ -1,757 +1,772 @@
/*
* 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;
// 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 encodedUserId = encodeBase64(userId, Base64Opts::urlSafe);
- auto sessionDir = userDataDir / "sessions"
- / encodedUserId / deviceId;
+ 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;
}
}
- {
- auto storeStream = std::ofstream(storeFile);
+ try {
+ auto storeStream = std::ofstream(storeFileNew);
if (! storeStream) {
- qDebug() << "Unable to open storeFile";
+ qCWarning(kazvLog) << "Unable to open storeFile";
return;
}
using OAr = boost::archive::text_oarchive;
auto archive = OAr{storeStream};
c.serializeTo(archive);
- qDebug() << "Serialization done";
+ } 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());
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()));
}
MatrixDeviceList *MatrixSdk::devicesOfUser(QString userId) const
{
return new MatrixDeviceList(m_d->clientOnSecondaryRoot.devicesOfUser(userId.toStdString()));
}
void MatrixSdk::login(const QString &userId, const QString &password)
{
m_d->clientOnSecondaryRoot
.autoDiscover(userId.toStdString())
.then([
this,
client=m_d->clientOnSecondaryRoot.toEventLoop(),
userId,
password
](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");
client.passwordLogin(
serverUrl,
userId.toStdString(),
password.toStdString(),
"kazv"
);
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 sessionId = parts[1].toStdString();
- auto encodedUserId = encodeBase64(userId, Base64Opts::urlSafe);
- auto sessionDir = userDataDir / "sessions" / encodedUserId / sessionId;
+ 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 sessionId = parts[1].toStdString();
- auto encodedUserId = encodeBase64(userId, Base64Opts::urlSafe);
- qCDebug(kazvLog) << "trying new path";
- auto sessionDir = userDataDir / "sessions" / encodedUserId / sessionId;
+ 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 / sessionId;
+ 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));
}
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 be74b02..666ba76 100644
--- a/src/matrix-sdk.hpp
+++ b/src/matrix-sdk.hpp
@@ -1,288 +1,291 @@
/*
* 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);
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);
Q_INVOKABLE MatrixRoomList *roomList() const;
Q_INVOKABLE QString mxcUriToHttp(QString mxcUri) const;
Q_INVOKABLE MatrixDeviceList *devicesOfUser(QString userId) const;
Kazv::RandomInterface &randomGenerator() const;
private:
// Replaces the store with another one
void emplace(std::optional<Kazv::SdkModel> model, std::unique_ptr<KazvSessionLockGuard> lockGuard);
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);
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);
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/matrix-sdk-sessions-test.cpp b/src/tests/matrix-sdk-sessions-test.cpp
index 422419d..4c19e3d 100644
--- a/src/tests/matrix-sdk-sessions-test.cpp
+++ b/src/tests/matrix-sdk-sessions-test.cpp
@@ -1,141 +1,195 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <kazv-platform.hpp>
#include <memory>
#include <filesystem>
#include <fstream>
#include <QObject>
#include <QtTest>
#include <QProcess>
#include <QCoreApplication>
#include <crypto/base64.hpp>
#include "test-temp.hpp"
#include "matrix-sdk-sessions-test.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
namespace Fs = std::filesystem;
using StdPath = Fs::path;
void MatrixSdkSessionsTest::initTestCase()
{
auto dir = StdPath(kazvTestTempDir()) / "sessions-test";
m_userDataDir = dir.string();
}
void MatrixSdkSessionsTest::clearDir()
{
QVERIFY(!m_userDataDir.empty());
std::error_code ec;
Fs::remove_all(StdPath(m_userDataDir), ec);
QVERIFY(!ec);
}
void MatrixSdkSessionsTest::init() { clearDir(); }
void MatrixSdkSessionsTest::cleanup() { clearDir(); }
void MatrixSdkSessionsTest::createSession(std::string userId, std::string deviceId)
{
auto sessionsDir = StdPath(m_userDataDir) / "sessions";
SdkModel model;
model.client.userId = userId;
model.client.deviceId = deviceId;
{
auto sdk = makeMatrixSdk(model, /* testing = */ false);
sdk->serializeToFile();
}
// Not ideal, but this is the only way to wait for
// MatrixSdkPravite to be destroyed.
QTest::qWait(100);
}
void MatrixSdkSessionsTest::testListSessions()
{
createSession("@mew:example.com", "device1");
createSession("@mew:example.com", "device2");
createSession("@mew2:example.com", "device3");
auto sdk = makeMatrixSdk();
QSet<QString> expected{
QStringLiteral("@mew:example.com/device1"),
QStringLiteral("@mew:example.com/device2"),
QStringLiteral("@mew2:example.com/device3"),
};
auto sessionList = sdk->allSessions();
auto sessionSet = QSet<QString>(sessionList.begin(), sessionList.end());
QCOMPARE(sessionSet, expected);
}
void MatrixSdkSessionsTest::testListLegacySessions()
{
// This test cannot be run on Windows, because the file name
// there cannot contain ':'.
#if KAZV_IS_WINDOWS
QSKIP("Skipping because there are no legacy sessions on Windows");
#endif
createSession("@mew:example.com", "device1");
auto legacySession = StdPath(m_userDataDir) / "sessions" / "@mew3:example.com" / "device4";
{
std::ofstream s{legacySession / "store"};
s << "";
}
auto sdk = makeMatrixSdk();
QStringList expected{
QStringLiteral("@mew:example.com/device1"),
};
QCOMPARE(sdk->allSessions(), expected);
}
void MatrixSdkSessionsTest::testLoadSession()
{
createSession("@mew:example.com", "device4");
auto sdk = makeMatrixSdk();
auto res = sdk->loadSession(u"@mew:example.com/device4"_s);
QCOMPARE(res, MatrixSdk::SessionLoadSuccess);
QCOMPARE(sdk->userId(), QStringLiteral("@mew:example.com"));
QCOMPARE(sdk->deviceId(), QStringLiteral("device4"));
}
void MatrixSdkSessionsTest::testSessionLock()
{
#if KAZV_IS_WINDOWS
QSKIP("Skipping because session lock is not yet supported on Windows");
#else
createSession("@mew:example.com", "device5");
auto program = QCoreApplication::applicationDirPath() + QStringLiteral("/matrix-sdk-session-loader");
QProcess proc1;
QStringList args{
QString::fromStdString(m_userDataDir),
QStringLiteral("@mew:example.com/device5"),
};
proc1.start(program, args);
proc1.waitForReadyRead();
auto line = proc1.readLine();
QCOMPARE(line, QByteArray("loaded session\n"));
QProcess proc2;
proc2.start(program, args);
auto res = proc2.waitForFinished();
QVERIFY(res);
QCOMPARE(proc2.exitStatus(), QProcess::NormalExit);
QCOMPARE(proc2.exitCode(), 1);
proc1.kill();
#endif
}
+void MatrixSdkSessionsTest::testSaveSessionFailure()
+{
+#if KAZV_IS_WINDOWS
+ QSKIP("Skipping because session the condition cannot be easily satisfied on Windows");
+#endif
+
+ auto userId = std::string("@mew:example.com");
+ auto deviceId = std::string("device1");
+ createSession(userId, deviceId);
+
+ auto sessionDir = sessionDirForUserAndDeviceId(Fs::path(m_userDataDir), userId, deviceId);
+ // there is store, metadata and lock under the sessionDir
+
+ std::error_code err;
+ // make the sessionDir readonly, so that new file cannot be created
+ Fs::permissions(
+ sessionDir,
+ Fs::perms::owner_read | Fs::perms::owner_exec,
+ Fs::perm_options::replace,
+ err);
+ QVERIFY(!err);
+ err.clear();
+
+ // a model that is slightly different from the original one
+ SdkModel model;
+ model.client.userId = userId;
+ model.client.deviceId = deviceId;
+ model.client.serverUrl = "https://example.com";
+
+ {
+ auto sdk = makeMatrixSdk(model, /* testing = */ false);
+ // this should fail
+ sdk->serializeToFile();
+ }
+ // Not ideal, but this is the only way to wait for
+ // MatrixSdkPravite to be destroyed.
+ QTest::qWait(100);
+
+ auto sdk = makeMatrixSdk();
+ auto res = sdk->loadSession(QString::fromStdString(userId + "/" + deviceId));
+ QCOMPARE(res, MatrixSdk::SessionLoadSuccess);
+ // verify that the original data is kept
+ QCOMPARE(sdk->serverUrl(), u""_s);
+
+ // before cleaning up, add write permissions back so the files can be removed
+ Fs::permissions(
+ sessionDir,
+ Fs::perms::owner_read | Fs::perms::owner_write | Fs::perms::owner_exec,
+ Fs::perm_options::replace,
+ err);
+ QVERIFY(!err);
+ err.clear();
+}
+
#ifndef MATRIX_SDK_SESSIONS_TEST_NO_MAIN
QTEST_MAIN(MatrixSdkSessionsTest)
#endif
diff --git a/src/tests/matrix-sdk-sessions-test.hpp b/src/tests/matrix-sdk-sessions-test.hpp
index db878c8..c282c6e 100644
--- a/src/tests/matrix-sdk-sessions-test.hpp
+++ b/src/tests/matrix-sdk-sessions-test.hpp
@@ -1,47 +1,48 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include <memory>
#include <QObject>
#include <matrix-sdk.hpp>
class MatrixSdkSessionsTest : public QObject
{
Q_OBJECT
private:
std::string m_userDataDir;
void clearDir();
void createSession(std::string userId, std::string deviceId);
public:
template<class ...Args>
static std::unique_ptr<MatrixSdk> makeMatrixSdkImpl(std::string userDataDir, Args &&...args)
{
std::unique_ptr<MatrixSdk> sdk(new MatrixSdk(std::forward<Args>(args)...));
sdk->setUserDataDir(userDataDir);
return sdk;
}
template<class ...Args>
std::unique_ptr<MatrixSdk> makeMatrixSdk(Args &&...args)
{
return makeMatrixSdkImpl(m_userDataDir, std::forward<Args>(args)...);
}
private Q_SLOTS:
void initTestCase();
void init();
void cleanup();
void testListSessions();
void testListLegacySessions();
void testLoadSession();
void testSessionLock();
+ void testSaveSessionFailure();
};
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Oct 8, 7:56 PM (6 h, 56 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784251
Default Alt Text
(47 KB)
Attached To
Mode
rK kazv
Attached
Detach File
Event Timeline
Log In to Comment