From 866fee2ea33502d42fe470e2412340d233487a79 Mon Sep 17 00:00:00 2001 From: James Graham Date: Sun, 13 Apr 2025 11:23:17 +0000 Subject: [PATCH] Move login and registration to login To make this work `AccountManager` is split off from controller taking all the code around `AccountRegister` and is added to LibNeoChat as it makes sense to have this functionality there. --- src/CMakeLists.txt | 8 +- src/controller.cpp | 302 ++++++++------------------- src/controller.h | 39 ++-- src/foreigntypes.h | 4 +- src/libneochat/CMakeLists.txt | 1 + src/libneochat/accountmanager.cpp | 270 ++++++++++++++++++++++++ src/libneochat/accountmanager.h | 103 +++++++++ src/libneochat/neochatconnection.cpp | 2 +- src/libneochat/neochatconnection.h | 2 +- src/login/CMakeLists.txt | 12 +- src/{ => login}/login.cpp | 24 ++- src/{ => login}/login.h | 6 + src/{ => login}/registration.cpp | 17 +- src/{ => login}/registration.h | 7 + src/main.cpp | 9 +- src/notificationsmanager.cpp | 2 +- src/qml/Main.qml | 7 + 17 files changed, 544 insertions(+), 271 deletions(-) create mode 100644 src/libneochat/accountmanager.cpp create mode 100644 src/libneochat/accountmanager.h rename src/{ => login}/login.cpp (88%) rename src/{ => login}/login.h (96%) rename src/{ => login}/registration.cpp (96%) rename src/{ => login}/registration.h (96%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 57996b81d..423fc5c14 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT NEOCHAT_FLATPAK AND NOT NEOC endif() add_subdirectory(libneochat) +add_subdirectory(login) add_subdirectory(timeline) add_library(neochat STATIC @@ -41,8 +42,6 @@ add_library(neochat STATIC models/roomtreemodel.h chatdocumenthandler.cpp chatdocumenthandler.h - login.cpp - login.h models/webshortcutmodel.cpp models/webshortcutmodel.h blurhash.cpp @@ -67,7 +66,6 @@ add_library(neochat STATIC models/livelocationsmodel.h models/locationsmodel.cpp models/locationsmodel.h - registration.cpp jobs/neochatgetcommonroomsjob.cpp jobs/neochatgetcommonroomsjob.h models/notificationsmodel.cpp @@ -214,7 +212,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE add_subdirectory(settings) add_subdirectory(devtools) -add_subdirectory(login) add_subdirectory(chatbar) if(NOT ANDROID AND NOT WIN32) @@ -290,7 +287,7 @@ else() endif() target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_CURRENT_SOURCE_DIR}/enums) -target_link_libraries(neochat PRIVATE Settingsplugin Timelineplugin devtoolsplugin loginplugin chatbarplugin) +target_link_libraries(neochat PRIVATE Settingsplugin Timelineplugin devtoolsplugin Loginplugin chatbarplugin) target_link_libraries(neochat PUBLIC LibNeoChat Timeline @@ -312,6 +309,7 @@ target_link_libraries(neochat PUBLIC KF6::IconThemes KF6::ItemModels QuotientQt6 + Login ) if (TARGET KF6::Crash) diff --git a/src/controller.cpp b/src/controller.cpp index ebbeb197b..4cf46a26c 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -20,6 +20,7 @@ #include #include "mediasizehelper.h" +#include "accountmanager.h" #include "models/actionsmodel.h" #include "models/messagemodel.h" #include "models/pushrulemodel.h" @@ -47,8 +48,6 @@ #include #endif -bool testMode = false; - using namespace Quotient; Controller::Controller(QObject *parent) @@ -108,19 +107,6 @@ Controller::Controller(QObject *parent) connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed); #endif - if (!testMode) { - QTimer::singleShot(0, this, [this] { - invokeLogin(); - }); - } else { - auto c = new NeoChatConnection(this); - c->assumeIdentity(u"@user:localhost:1234"_s, u"device_1234"_s, u"token_1234"_s); - connect(c, &Connection::connected, this, [c, this]() { - m_accountRegistry.add(c); - c->syncLoop(); - }); - } - QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] { delete m_trayIcon; NeoChatConfig::self()->save(); @@ -149,29 +135,15 @@ Controller::Controller(QObject *parent) } #endif - static int oldAccountCount = 0; - connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() { - if (m_accountRegistry.size() > oldAccountCount) { - auto connection = dynamic_cast(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]); - connect( - connection, - &NeoChatConnection::syncDone, - this, - [this, connection] { - if (!m_endpoint.isEmpty()) { - connection->setupPushNotifications(m_endpoint); - } - }, - Qt::SingleShotConnection); - } - oldAccountCount = m_accountRegistry.size(); - }); - #ifdef HAVE_KUNIFIEDPUSH auto connector = new KUnifiedPush::Connector(u"org.kde.neochat"_s); connect(connector, &KUnifiedPush::Connector::endpointChanged, this, [this](const QString &endpoint) { + if (!m_accountManager) { + return; + } + m_endpoint = endpoint; - for (auto "ientConnection : m_accountRegistry) { + for (auto "ientConnection : m_accountManager->accounts()->accounts()) { auto connection = dynamic_cast(quotientConnection); connection->setupPushNotifications(endpoint); } @@ -181,7 +153,6 @@ Controller::Controller(QObject *parent) i18nc("The reason for using push notifications, as in: '[Push notifications are used for] Receiving notifications for new messages'", "Receiving notifications for new messages")); - m_endpoint = connector->endpoint(); #endif } @@ -191,146 +162,72 @@ Controller &Controller::instance() return _instance; } -void Controller::addConnection(NeoChatConnection *c) +void Controller::setAccountManager(AccountManager *manager) { - Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection"); + if (manager == m_accountManager) { + return; + } - m_accountRegistry.add(c); + if (m_accountManager) { + m_accountManager->disconnect(this); + } - c->setLazyLoading(true); + m_accountManager = manager; - connect(c, &NeoChatConnection::syncDone, this, [c] { - c->sync(30000); - c->saveState(); - }); - connect(c, &NeoChatConnection::loggedOut, this, [this, c] { - if (accounts().count() > 1) { - // Only set the connection if the account being logged out is currently active - if (c == activeConnection()) { - setActiveConnection(dynamic_cast(accounts().accounts()[0])); - } - } else { - setActiveConnection(nullptr); - } - - dropConnection(c); - }); - connect(c, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount); - connect(c, &NeoChatConnection::syncDone, this, [this, c]() { - m_notificationsManager.handleNotifications(c); - }); - connect(this, &Controller::globalUrlPreviewDefaultChanged, c, &NeoChatConnection::globalUrlPreviewEnabledChanged); - - c->sync(); - - Q_EMIT connectionAdded(c); -} - -void Controller::dropConnection(NeoChatConnection *c) -{ - Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection"); - - c->disconnect(this); - c->disconnect(&m_notificationsManager); - m_accountRegistry.drop(c); - Q_EMIT connectionDropped(c); -} - -void Controller::invokeLogin() -{ - const auto accounts = SettingsGroup("Accounts"_L1).childGroups(); - for (const auto &accountId : accounts) { - AccountSettings account{accountId}; - m_accountsLoading += accountId; - Q_EMIT accountsLoadingChanged(); - if (!account.homeserver().isEmpty()) { - auto accessTokenLoadingJob = loadAccessTokenFromKeyChain(account.userId()); - connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, this, accessTokenLoadingJob](QKeychain::Job *) { - AccountSettings account{accountId}; - QString accessToken; - if (accessTokenLoadingJob->error() == QKeychain::Error::NoError) { - accessToken = QString::fromLatin1(accessTokenLoadingJob->binaryData()); - } else { - return; - } - - 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()) { - addConnection(connection); - m_accountsLoading.removeAll(connection->userId()); - m_connectionsLoading.remove(accountId); - Q_EMIT accountsLoadingChanged(); - } else { - connect( - connection->allRooms()[0], - &Room::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(), accessToken); - }); - } + if (m_accountManager) { + connect(m_accountManager, &AccountManager::errorOccured, this, &Controller::errorOccured); + connect(m_accountManager, &AccountManager::accountsLoadingChanged, this, &Controller::accountsLoadingChanged); + connect(m_accountManager, &AccountManager::connectionAdded, this, &Controller::initConnection); + connect(m_accountManager, &AccountManager::connectionDropped, this, &Controller::teardownConnection); + connect(m_accountManager, &AccountManager::activeConnectionChanged, this, &Controller::initActiveConnection); } } -QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const QString &userId) +void Controller::initConnection(NeoChatConnection *connection) { - qDebug() << "Reading access token from the keychain for" << userId; - auto job = new QKeychain::ReadPasswordJob(qAppName(), this); - job->setKey(userId); + if (!connection) { + return; + } - // 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; - } + connect( + connection, + &NeoChatConnection::syncDone, + this, + [this, connection] { + if (!m_endpoint.isEmpty()) { + connection->setupPushNotifications(m_endpoint); + } + }, + Qt::SingleShotConnection); + connect(connection, &NeoChatConnection::syncDone, this, [this, connection]() { + m_notificationsManager.handleNotifications(connection); }); - job->start(); - - return job; + connect(this, &Controller::globalUrlPreviewDefaultChanged, connection, &NeoChatConnection::globalUrlPreviewEnabledChanged); + Q_EMIT connectionAdded(connection); } -void Controller::saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken) +void Controller::teardownConnection(NeoChatConnection *connection) { - qDebug() << "Save the access token to the keychain for " << userId; - auto job = new QKeychain::WritePasswordJob(qAppName()); - job->setAutoDelete(true); - job->setKey(userId); - job->setBinaryData(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(); + if (!connection) { + return; + } + + connection->disconnect(this); + Q_EMIT connectionDropped(connection); +} + +void Controller::initActiveConnection(NeoChatConnection *oldConnection, NeoChatConnection *newConnection) +{ + if (oldConnection) { + oldConnection->disconnect(this); + } + + if (newConnection) { + connect(newConnection, &NeoChatConnection::errorOccured, this, &Controller::errorOccured); + connect(newConnection, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount); + newConnection->refreshBadgeNotificationCount(); + } + Q_EMIT activeConnectionChanged(newConnection); } bool Controller::supportSystemTray() const @@ -360,33 +257,26 @@ void Controller::setQuitOnLastWindowClosed() NeoChatConnection *Controller::activeConnection() const { - if (m_connection.isNull()) { + if (!m_accountManager) { return nullptr; } - return m_connection; + return m_accountManager->activeConnection(); } void Controller::setActiveConnection(NeoChatConnection *connection) { - if (connection == m_connection) { + if (!m_accountManager) { return; } + m_accountManager->setActiveConnection(connection); +} - if (m_connection != nullptr) { - m_connection->disconnect(this); - m_connection->disconnect(&m_notificationsManager); +QStringList Controller::accountsLoading() const +{ + if (!m_accountManager) { + return {}; } - - m_connection = connection; - - if (m_connection != nullptr) { - m_connection->refreshBadgeNotificationCount(); - updateBadgeNotificationCount(m_connection, m_connection->badgeNotificationCount()); - - connect(m_connection, &NeoChatConnection::errorOccured, this, &Controller::errorOccured); - } - - Q_EMIT activeConnectionChanged(m_connection); + return m_accountManager->accountsLoading(); } void Controller::listenForNotifications() @@ -415,34 +305,32 @@ void Controller::clearInvitationNotification(const QString &roomId) m_notificationsManager.clearInvitationNotification(roomId); } -void Controller::updateBadgeNotificationCount(NeoChatConnection *connection, int count) +void Controller::updateBadgeNotificationCount(int count) { - if (connection == m_connection) { #if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) #ifndef Q_OS_ANDROID - // copied from Telegram desktop - const auto launcherUrl = "application://org.kde.neochat.desktop"_L1; - // Gnome requires that count is a 64bit integer - const qint64 counterSlice = std::min(count, 9999); - QVariantMap dbusUnityProperties; + // copied from Telegram desktop + const auto launcherUrl = "application://org.kde.neochat.desktop"_L1; + // Gnome requires that count is a 64bit integer + const qint64 counterSlice = std::min(count, 9999); + QVariantMap dbusUnityProperties; - if (counterSlice > 0) { - dbusUnityProperties["count"_L1] = counterSlice; - dbusUnityProperties["count-visible"_L1] = true; - } else { - dbusUnityProperties["count-visible"_L1] = false; - } + if (counterSlice > 0) { + dbusUnityProperties["count"_L1] = counterSlice; + dbusUnityProperties["count-visible"_L1] = true; + } else { + dbusUnityProperties["count-visible"_L1] = false; + } - auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat"_L1, "com.canonical.Unity.LauncherEntry"_L1, "Update"_L1); + auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat"_L1, "com.canonical.Unity.LauncherEntry"_L1, "Update"_L1); - signal.setArguments({launcherUrl, dbusUnityProperties}); + signal.setArguments({launcherUrl, dbusUnityProperties}); - QDBusConnection::sessionBus().send(signal); + QDBusConnection::sessionBus().send(signal); #endif // Q_OS_ANDROID #else - qGuiApp->setBadgeNumber(count); + qGuiApp->setBadgeNumber(count); #endif // QT_VERSION_CHECK(6, 6, 0) - } } bool Controller::isFlatpak() const @@ -454,9 +342,9 @@ bool Controller::isFlatpak() const #endif } -AccountRegistry &Controller::accounts() +AccountRegistry *Controller::accounts() { - return m_accountRegistry; + return m_accountManager->accounts(); } QString Controller::loadFileContent(const QString &path) const @@ -467,23 +355,9 @@ QString Controller::loadFileContent(const QString &path) const return QString::fromLatin1(file.readAll()); } -void Controller::setTestMode(bool test) -{ - testMode = test; -} - void Controller::removeConnection(const QString &userId) { - // When loadAccessTokenFromKeyChain() fails m_connectionsLoading won't have an - // entry for it so we need to check both separately. - if (m_accountsLoading.contains(userId)) { - m_accountsLoading.removeAll(userId); - Q_EMIT accountsLoadingChanged(); - } - if (m_connectionsLoading.contains(userId) && m_connectionsLoading[userId]) { - auto connection = m_connectionsLoading[userId]; - SettingsGroup("Accounts"_L1).remove(userId); - } + m_accountManager->dropConnection(userId); } void Controller::revertToDefaultConfig() diff --git a/src/controller.h b/src/controller.h index 88d69dd58..744cbd7e2 100644 --- a/src/controller.h +++ b/src/controller.h @@ -6,6 +6,7 @@ #include #include +#include "accountmanager.h" #include "neochatconnection.h" #include "notificationsmanager.h" #include @@ -48,7 +49,7 @@ class Controller : public QObject */ Q_PROPERTY(bool isFlatpak READ isFlatpak CONSTANT) - Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged) + Q_PROPERTY(QStringList accountsLoading READ accountsLoading NOTIFY accountsLoadingChanged) public: static Controller &instance(); @@ -58,23 +59,12 @@ public: return &instance(); } - void setActiveConnection(NeoChatConnection *connection); + void setAccountManager(AccountManager *manager); + [[nodiscard]] NeoChatConnection *activeConnection() const; + void setActiveConnection(NeoChatConnection *connection); - /** - * @brief Add a new connection to the account registry. - */ - void addConnection(NeoChatConnection *c); - - /** - * @brief Drop a connection from the account registry. - */ - void dropConnection(NeoChatConnection *c); - - /** - * @brief Save an access token to the keychain for the given account. - */ - void saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken); + QStringList accountsLoading() const; [[nodiscard]] bool supportSystemTray() const; @@ -95,9 +85,7 @@ public: Q_INVOKABLE QString loadFileContent(const QString &path) const; - Quotient::AccountRegistry &accounts(); - - static void setTestMode(bool testMode); + Quotient::AccountRegistry *accounts(); Q_INVOKABLE void removeConnection(const QString &userId); @@ -115,23 +103,22 @@ public: private: explicit Controller(QObject *parent = nullptr); + QPointer m_accountManager; + void initConnection(NeoChatConnection *connection); + void teardownConnection(NeoChatConnection *connection); + void initActiveConnection(NeoChatConnection *oldConnection, NeoChatConnection *newConnection); + QPointer m_connection; TrayIcon *m_trayIcon = nullptr; - QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const QString &account); - - Quotient::AccountRegistry m_accountRegistry; - QStringList m_accountsLoading; - QMap> m_connectionsLoading; QString m_endpoint; QStringList m_shownImages; NotificationsManager m_notificationsManager; private Q_SLOTS: - void invokeLogin(); void setQuitOnLastWindowClosed(); - void updateBadgeNotificationCount(NeoChatConnection *connection, int count); + void updateBadgeNotificationCount(int count); Q_SIGNALS: /** diff --git a/src/foreigntypes.h b/src/foreigntypes.h index ecc625795..d90474051 100644 --- a/src/foreigntypes.h +++ b/src/foreigntypes.h @@ -22,8 +22,8 @@ struct ForeignAccountRegistry { public: static Quotient::AccountRegistry *create(QQmlEngine *, QJSEngine *) { - QQmlEngine::setObjectOwnership(&Controller::instance().accounts(), QQmlEngine::CppOwnership); - return &Controller::instance().accounts(); + QQmlEngine::setObjectOwnership(Controller::instance().accounts(), QQmlEngine::CppOwnership); + return Controller::instance().accounts(); } }; diff --git a/src/libneochat/CMakeLists.txt b/src/libneochat/CMakeLists.txt index d70e3b32f..eac4ec94e 100644 --- a/src/libneochat/CMakeLists.txt +++ b/src/libneochat/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(LibNeoChat PRIVATE neochatconnection.cpp neochatroom.cpp neochatroommember.cpp + accountmanager.cpp chatbarcache.cpp clipboard.cpp delegatesizehelper.cpp diff --git a/src/libneochat/accountmanager.cpp b/src/libneochat/accountmanager.cpp new file mode 100644 index 000000000..97db00586 --- /dev/null +++ b/src/libneochat/accountmanager.cpp @@ -0,0 +1,270 @@ +// SPDX-FileCopyrightText: 2023 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "accountmanager.h" + +#include +#include + +#include + +#include + +#include "neochatroom.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 c = new NeoChatConnection(this); + c->assumeIdentity(u"@user:localhost:1234"_s, u"device_1234"_s, u"token_1234"_s); + connect(c, &NeoChatConnection::connected, this, [c, this]() { + m_accountRegistry->add(c); + c->syncLoop(); + }); + } +} + +Quotient::AccountRegistry *AccountManager::accounts() +{ + return m_accountRegistry; +} + +void AccountManager::loadAccountsFromCache() +{ + const auto accounts = Quotient::SettingsGroup("Accounts"_L1).childGroups(); + for (const auto &accountId : accounts) { + Quotient::AccountSettings account{accountId}; + m_accountsLoading += accountId; + Q_EMIT accountsLoadingChanged(); + if (!account.homeserver().isEmpty()) { + auto accessTokenLoadingJob = loadAccessTokenFromKeyChain(account.userId()); + connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, this, accessTokenLoadingJob](QKeychain::Job *) { + Quotient::AccountSettings account{accountId}; + QString accessToken; + if (accessTokenLoadingJob->error() == QKeychain::Error::NoError) { + accessToken = QString::fromLatin1(accessTokenLoadingJob->binaryData()); + } else { + return; + } + + 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()) { + 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(), accessToken); + }); + } + } +} + +QStringList AccountManager::accountsLoading() const +{ + return m_accountsLoading; +} + +void AccountManager::saveAccessTokenToKeyChain(NeoChatConnection *connection) +{ + if (!connection) { + return; + } + const auto userId = connection->userId(); + + qDebug() << "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) +{ + qDebug() << "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(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; + } + const auto connection = dynamic_cast(m_accountRegistry->get(userId)); + if (connection) { + 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; +} diff --git a/src/libneochat/accountmanager.h b/src/libneochat/accountmanager.h new file mode 100644 index 000000000..fadf72141 --- /dev/null +++ b/src/libneochat/accountmanager.h @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2023 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include + +#include + +#include "neochatconnection.h" + +class AccountManager : public QObject +{ + Q_OBJECT + +public: + explicit AccountManager(bool testMode = false, QObject *parent = nullptr); + + Quotient::AccountRegistry *accounts(); + + /** + * @brief Load the accounts saved in the app cache. + * + * This should be called on app startup to retrieve accounts logged in, in a + * previous sessions. + */ + void loadAccountsFromCache(); + + /** + * @brief The accounts currently being loaded from cache. + */ + QStringList accountsLoading() const; + + /** + * @brief Get the primary connection being displayed in the rest of the app. + */ + NeoChatConnection *activeConnection() const; + + /** + * @brief Set the primary connection being displayed in the rest of the app. + */ + void setActiveConnection(NeoChatConnection *connection); + + /** + * @brief Add a new connection to the account registry. + */ + void addConnection(NeoChatConnection *connection); + + /** + * @brief Drop a connection from the account registry. + */ + Q_INVOKABLE void dropConnection(const QString &userId); + + /** + * @brief Drop a connection from the account registry. + */ + void dropConnection(NeoChatConnection *connection); + +Q_SIGNALS: + /** + * @brief Request a error message be shown to the user. + */ + void errorOccured(const QString &error); + + /** + * @brief The list of accounts loading the access token from keychain has changed. + */ + void accountsLoadingChanged(); + + /** + * @brief The list of connection loading has changed. + */ + void connectionLoadingChanged(); + + /** + * @brief The given connection has been added. + */ + void connectionAdded(NeoChatConnection *connection); + + /** + * @brief The given connection has been dropped. + */ + void connectionDropped(NeoChatConnection *connection); + + /** + * @brief The primary connection being displayed in the rest of the app has changed. + */ + void activeConnectionChanged(NeoChatConnection *oldConnection, NeoChatConnection *newConnection); + +private: + QPointer m_accountRegistry; + QStringList m_accountsLoading; + QMap> m_connectionsLoading; + + void saveAccessTokenToKeyChain(NeoChatConnection *connection); + QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const QString &userId); + + bool dropAccountLoading(const QString &userId); + bool dropConnectionLoading(NeoChatConnection *connection); + bool dropRegistry(NeoChatConnection *connection); + + QPointer m_activeConnection; +}; diff --git a/src/libneochat/neochatconnection.cpp b/src/libneochat/neochatconnection.cpp index 871853fbd..3a8eb830b 100644 --- a/src/libneochat/neochatconnection.cpp +++ b/src/libneochat/neochatconnection.cpp @@ -165,7 +165,7 @@ void NeoChatConnection::refreshBadgeNotificationCount() if (count != m_badgeNotificationCount) { m_badgeNotificationCount = count; - Q_EMIT badgeNotificationCountChanged(this, m_badgeNotificationCount); + Q_EMIT badgeNotificationCountChanged(m_badgeNotificationCount); } } diff --git a/src/libneochat/neochatconnection.h b/src/libneochat/neochatconnection.h index 1daa5ae5c..b9dcbc4ef 100644 --- a/src/libneochat/neochatconnection.h +++ b/src/libneochat/neochatconnection.h @@ -217,7 +217,7 @@ Q_SIGNALS: void isOnlineChanged(); void passwordStatus(NeoChatConnection::PasswordStatus status); void userConsentRequired(QUrl url); - void badgeNotificationCountChanged(NeoChatConnection *connection, int count); + void badgeNotificationCountChanged(int count); void canCheckMutualRoomsChanged(); void canEraseDataChanged(); void enablePushNotificationsChanged(); diff --git a/src/login/CMakeLists.txt b/src/login/CMakeLists.txt index 9738e40a6..a21b19a54 100644 --- a/src/login/CMakeLists.txt +++ b/src/login/CMakeLists.txt @@ -1,8 +1,8 @@ # SPDX-FileCopyrightText: 2024 James Graham # SPDX-License-Identifier: BSD-2-Clause -qt_add_library(login STATIC) -ecm_add_qml_module(login GENERATE_PLUGIN_SOURCE +qt_add_library(Login STATIC) +ecm_add_qml_module(Login GENERATE_PLUGIN_SOURCE URI org.kde.neochat.login OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/login QML_FILES @@ -20,4 +20,12 @@ ecm_add_qml_module(login GENERATE_PLUGIN_SOURCE Sso.qml Terms.qml Username.qml + SOURCES + login.cpp + registration.cpp +) + +target_link_libraries(Login PRIVATE + QuotientQt6 + LibNeoChat ) diff --git a/src/login.cpp b/src/login/login.cpp similarity index 88% rename from src/login.cpp rename to src/login/login.cpp index e608cdef0..9ad864f54 100644 --- a/src/login.cpp +++ b/src/login/login.cpp @@ -7,10 +7,10 @@ #include #include -#include "controller.h" - #include +#include "neochatconnection.h" + using namespace Quotient; LoginHelper::LoginHelper(QObject *parent) @@ -19,6 +19,11 @@ LoginHelper::LoginHelper(QObject *parent) init(); } +void LoginHelper::setAccountManager(AccountManager *manager) +{ + m_accountManager = manager; +} + void LoginHelper::init() { m_homeserverReachable = false; @@ -41,7 +46,7 @@ void LoginHelper::init() return; } - m_isLoggedIn = Controller::instance().accounts().isLoggedIn(m_matrixId); + m_isLoggedIn = m_accountManager->accounts()->isLoggedIn(m_matrixId); Q_EMIT isLoggedInChanged(); if (m_isLoggedIn) { return; @@ -77,18 +82,17 @@ void LoginHelper::init() account.setHomeserver(m_connection->homeserver()); account.setDeviceId(m_connection->deviceId()); account.setDeviceName(m_deviceName); - Controller::instance().saveAccessTokenToKeyChain(account.userId(), m_connection->accessToken()); account.sync(); - Controller::instance().addConnection(m_connection); - Controller::instance().setActiveConnection(m_connection); + m_accountManager->addConnection(m_connection); + m_accountManager->setActiveConnection(m_connection); m_connection = nullptr; }); - connect(m_connection, &Connection::networkError, this, [this](QString error, const QString &, int, int) { + connect(m_connection, &NeoChatConnection::networkError, this, [this](QString error, const QString &, int, int) { Q_EMIT m_connection->errorOccured(i18n("Network Error: %1", std::move(error))); m_isLoggingIn = false; Q_EMIT isLoggingInChanged(); }); - connect(m_connection, &Connection::loginError, this, [this](QString error, const QString &) { + connect(m_connection, &NeoChatConnection::loginError, this, [this](QString error, const QString &) { if (error == u"Invalid username or password"_s) { setInvalidPassword(true); } else { @@ -98,13 +102,13 @@ void LoginHelper::init() Q_EMIT isLoggingInChanged(); }); - connect(m_connection, &Connection::resolveError, this, [this](QString error) { + connect(m_connection, &NeoChatConnection::resolveError, this, [this](QString error) { Q_EMIT m_connection->errorOccured(i18n("Network Error: %1", std::move(error))); }); connect( m_connection.get(), - &Connection::syncDone, + &NeoChatConnection::syncDone, this, [this]() { Q_EMIT loaded(); diff --git a/src/login.h b/src/login/login.h similarity index 96% rename from src/login.h rename to src/login/login.h index f374867a3..8287c26c9 100644 --- a/src/login.h +++ b/src/login/login.h @@ -7,6 +7,8 @@ #include #include +#include "accountmanager.h" + class NeoChatConnection; /** @@ -91,6 +93,8 @@ public: return &instance(); } + void setAccountManager(AccountManager *manager); + Q_INVOKABLE void init(); bool homeserverReachable() const; @@ -138,6 +142,8 @@ Q_SIGNALS: void loaded(); private: + QPointer m_accountManager; + void setHomeserverReachable(bool reachable); bool m_homeserverReachable; diff --git a/src/registration.cpp b/src/login/registration.cpp similarity index 96% rename from src/registration.cpp rename to src/login/registration.cpp index 401b96229..5c0168466 100644 --- a/src/registration.cpp +++ b/src/login/registration.cpp @@ -10,9 +10,6 @@ #include #include -#include "controller.h" -#include "login.h" - #include using namespace Quotient; @@ -36,6 +33,11 @@ Registration::Registration() connect(this, &Registration::usernameChanged, this, &Registration::testUsername); } +void Registration::setAccountManager(AccountManager *manager) +{ + m_accountManager = manager; +} + void Registration::setRecaptchaResponse(const QString &recaptchaResponse) { m_recaptchaResponse = recaptchaResponse; @@ -102,16 +104,15 @@ void Registration::registerAccount() account.setHomeserver(connection->homeserver()); account.setDeviceId(connection->deviceId()); account.setDeviceName(displayName); - Controller::instance().saveAccessTokenToKeyChain(account.userId(), connection->accessToken()); account.sync(); - Controller::instance().addConnection(connection); - Controller::instance().setActiveConnection(connection); + m_accountManager->addConnection(connection); + m_accountManager->setActiveConnection(connection); connect( connection, &Connection::syncDone, this, - []() { - Q_EMIT LoginHelper::instance().loaded(); + [this]() { + Q_EMIT loaded(); }, Qt::SingleShotConnection); m_connection = nullptr; diff --git a/src/registration.h b/src/login/registration.h similarity index 96% rename from src/registration.h rename to src/login/registration.h index f55dc1a6a..1cf3a7163 100644 --- a/src/registration.h +++ b/src/login/registration.h @@ -16,6 +16,8 @@ #include #include +#include "accountmanager.h" + using namespace Qt::StringLiterals; namespace Quotient @@ -98,6 +100,8 @@ public: return &instance(); } + void setAccountManager(AccountManager *manager); + Q_INVOKABLE void registerAccount(); Q_INVOKABLE void registerEmail(); @@ -142,8 +146,11 @@ Q_SIGNALS: void emailChanged(); void nextStepChanged(); void statusChanged(); + void loaded(); private: + QPointer m_accountManager; + QString m_recaptchaSiteKey; QString m_recaptchaResponse; QString m_homeserver; diff --git a/src/main.cpp b/src/main.cpp index 35e9de1b0..4f6b241aa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -45,10 +45,13 @@ #include +#include "accountmanager.h" #include "blurhashimageprovider.h" #include "colorschemer.h" #include "controller.h" #include "logger.h" +#include "login.h" +#include "registration.h" #include "roommanager.h" #include "sharehandler.h" #include "windowcontroller.h" @@ -208,7 +211,6 @@ int main(int argc, char *argv[]) about.setupCommandLine(&parser); parser.process(app); about.processCommandLine(&parser); - Controller::setTestMode(parser.isSet("test"_L1)); #ifdef HAVE_KUNIFIEDPUSH if (parser.isSet(dbusActivatedOption)) { @@ -232,6 +234,11 @@ int main(int argc, char *argv[]) KDBusService service(KDBusService::Unique); #endif + const auto accountManager = std::make_unique(parser.isSet("test"_L1)); + Controller::instance().setAccountManager(accountManager.get()); + LoginHelper::instance().setAccountManager(accountManager.get()); + Registration::instance().setAccountManager(accountManager.get()); + Q_IMPORT_QML_PLUGIN(org_kde_neochat_settingsPlugin) Q_IMPORT_QML_PLUGIN(org_kde_neochat_timelinePlugin) Q_IMPORT_QML_PLUGIN(org_kde_neochat_devtoolsPlugin) diff --git a/src/notificationsmanager.cpp b/src/notificationsmanager.cpp index 458f92be2..cb293748e 100644 --- a/src/notificationsmanager.cpp +++ b/src/notificationsmanager.cpp @@ -235,7 +235,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room, if (!room) { return; } - auto connection = dynamic_cast(Controller::instance().accounts().get(room->localMember().id())); + auto connection = dynamic_cast(Controller::instance().accounts()->get(room->localMember().id())); Controller::instance().setActiveConnection(connection); RoomManager::instance().setConnection(connection); RoomManager::instance().resolveResource(room->id()); diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 2635e82b5..219327253 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -64,6 +64,13 @@ Kirigami.ApplicationWindow { } } + Connections { + target: Registration + function onLoaded() { + root.load(); + } + } + Connections { target: root.quitAction function onTriggered() {