Files
neochat/src/libneochat/accountmanager.cpp
2025-08-29 17:08:15 +02:00

267 lines
8.5 KiB
C++

// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "accountmanager.h"
#include <QCoreApplication>
#include <QTimer>
#include <KLocalizedString>
#include <Quotient/settings.h>
#include "neochatroom.h"
#include "general_logging.h"
using namespace Qt::StringLiterals;
AccountManager::AccountManager(bool testMode, QObject *parent)
: QObject(parent)
, m_accountRegistry(new Quotient::AccountRegistry(this))
{
if (!testMode) {
QTimer::singleShot(0, this, [this] {
loadAccountsFromCache();
});
} else {
auto connection = new NeoChatConnection(QUrl(u"https://localhost:1234"_s), this);
connection->assumeIdentity(u"@user:localhost:1234"_s, u"device_1234"_s, u"token_1234"_s);
m_accountRegistry->add(connection);
connection->syncLoop();
}
}
Quotient::AccountRegistry *AccountManager::accounts()
{
return m_accountRegistry;
}
void AccountManager::loadAccountsFromCache()
{
for (const auto &accountId : Quotient::SettingsGroup("Accounts"_L1).childGroups()) {
Quotient::AccountSettings account{accountId};
m_accountsLoading += accountId;
Q_EMIT accountsLoadingChanged();
if (account.homeserver().isEmpty()) {
continue;
}
auto accessTokenLoadingJob = loadAccessTokenFromKeyChain(account.userId());
connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, this, accessTokenLoadingJob](QKeychain::Job *) {
if (accessTokenLoadingJob->error() != QKeychain::Error::NoError) {
return;
}
Quotient::AccountSettings account{accountId};
auto connection = new NeoChatConnection(account.homeserver());
m_connectionsLoading[accountId] = connection;
connect(connection, &NeoChatConnection::connected, this, [this, connection, accountId] {
connection->loadState();
if (connection->allRooms().size() == 0 || connection->allRooms()[0]->currentState().get<Quotient::RoomCreateEvent>()) {
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
} else {
connect(
connection->allRooms()[0],
&NeoChatRoom::baseStateLoaded,
this,
[this, connection, accountId]() {
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
},
Qt::SingleShotConnection);
}
});
connection->assumeIdentity(account.userId(), account.deviceId(), QString::fromLatin1(accessTokenLoadingJob->binaryData()));
});
}
}
QStringList AccountManager::accountsLoading() const
{
return m_accountsLoading;
}
void AccountManager::saveAccessTokenToKeyChain(NeoChatConnection *connection)
{
if (!connection) {
return;
}
const auto userId = connection->userId();
qCDebug(GENERAL) << "Save the access token to the keychain for " << userId;
auto job = new QKeychain::WritePasswordJob(qAppName());
job->setAutoDelete(true);
job->setKey(userId);
job->setBinaryData(connection->accessToken());
connect(job, &QKeychain::WritePasswordJob::finished, this, [job]() {
if (job->error()) {
qWarning() << "Could not save access token to the keychain: " << qPrintable(job->errorString());
}
});
job->start();
}
QKeychain::ReadPasswordJob *AccountManager::loadAccessTokenFromKeyChain(const QString &userId)
{
qCDebug(GENERAL) << "Reading access token from the keychain for" << userId;
auto job = new QKeychain::ReadPasswordJob(qAppName(), this);
job->setKey(userId);
// Handling of errors
connect(job, &QKeychain::Job::finished, this, [this, job]() {
if (job->error() == QKeychain::Error::NoError) {
return;
}
switch (job->error()) {
case QKeychain::EntryNotFound:
Q_EMIT errorOccured(i18n("Access token wasn't found: Maybe it was deleted?"));
break;
case QKeychain::AccessDeniedByUser:
case QKeychain::AccessDenied:
Q_EMIT errorOccured(i18n("Access to keychain was denied: Please allow NeoChat to read the access token"));
break;
case QKeychain::NoBackendAvailable:
Q_EMIT errorOccured(i18n("No keychain available: Please install a keychain, e.g. KWallet or GNOME keyring on Linux"));
break;
case QKeychain::OtherError:
Q_EMIT errorOccured(i18n("Unable to read access token: %1", job->errorString()));
break;
default:
break;
}
});
job->start();
return job;
}
NeoChatConnection *AccountManager::activeConnection() const
{
return m_activeConnection;
}
void AccountManager::setActiveConnection(NeoChatConnection *connection)
{
if (connection == m_activeConnection) {
return;
}
const auto oldConnection = m_activeConnection;
m_activeConnection = connection;
Q_EMIT activeConnectionChanged(oldConnection, m_activeConnection);
}
void AccountManager::addConnection(NeoChatConnection *connection)
{
Q_ASSERT_X(connection, __FUNCTION__, "Attempt to add a null connection");
saveAccessTokenToKeyChain(connection);
m_accountRegistry->add(connection);
connection->setLazyLoading(true);
connect(connection, &NeoChatConnection::syncDone, this, [connection] {
connection->sync(30000);
connection->saveState();
});
connect(connection, &NeoChatConnection::loggedOut, this, [this, connection] {
// Only set the connection if the account being logged out is currently active
if (m_accountRegistry->accounts().count() == 1 && connection == activeConnection()) {
setActiveConnection(dynamic_cast<NeoChatConnection *>(m_accountRegistry->accounts()[0]));
} else {
setActiveConnection(nullptr);
}
dropConnection(connection);
});
connection->sync();
Q_EMIT connectionAdded(connection);
}
void AccountManager::dropConnection(const QString &userId)
{
if (userId.isEmpty()) {
return;
}
// There are 3 possible states:
// - in m_accountsLoading trying to loadAccessTokenFromKeyChain()
// - in m_connectionsLoading
// - in the AccountRegistry
// Check all locations.
if (dropAccountLoading(userId)) {
return;
}
if (dropConnectionLoading(m_connectionsLoading.value(userId, nullptr))) {
return;
}
if (const auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry->get(userId))) {
dropRegistry(connection);
}
}
void AccountManager::dropConnection(NeoChatConnection *connection)
{
if (!connection) {
return;
}
// There are 3 possible states:
// - in m_accountsLoading trying to loadAccessTokenFromKeyChain()
// - in m_connectionsLoading
// - in the AccountRegistry
// Check all locations.
if (dropAccountLoading(connection->userId())) {
return;
}
if (dropConnectionLoading(connection)) {
return;
}
dropRegistry(connection);
}
bool AccountManager::dropAccountLoading(const QString &userId)
{
if (!m_accountsLoading.contains(userId)) {
return false;
}
m_accountsLoading.removeAll(userId);
Q_EMIT accountsLoadingChanged();
return true;
}
bool AccountManager::dropConnectionLoading(NeoChatConnection *connection)
{
if (!connection || (m_connectionsLoading.contains(connection->userId()) && m_connectionsLoading.value(connection->userId(), nullptr) == connection)) {
return false;
}
m_connectionsLoading.remove(connection->userId());
Quotient::SettingsGroup("Accounts"_L1).remove(connection->userId());
Q_EMIT connectionLoadingChanged();
return true;
}
bool AccountManager::dropRegistry(NeoChatConnection *connection)
{
if (!m_accountRegistry->isLoggedIn(connection->userId())) {
return false;
}
connection->disconnect(this);
m_accountRegistry->drop(connection);
Q_EMIT connectionDropped(connection);
return true;
}