File indexing completed on 2024-04-28 08:51:49

0001 // SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
0002 // SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
0003 // SPDX-License-Identifier: GPL-3.0-only
0004 
0005 #include "controller.h"
0006 
0007 #include <qt6keychain/keychain.h>
0008 
0009 #include <KLocalizedString>
0010 
0011 #include <QGuiApplication>
0012 #include <QNetworkProxy>
0013 #include <QQuickTextDocument>
0014 #include <QQuickWindow>
0015 #include <QStandardPaths>
0016 #include <QStringBuilder>
0017 #include <QTimer>
0018 
0019 #include <signal.h>
0020 
0021 #include <Quotient/accountregistry.h>
0022 #include <Quotient/connection.h>
0023 #include <Quotient/csapi/logout.h>
0024 #include <Quotient/csapi/notifications.h>
0025 #include <Quotient/eventstats.h>
0026 #include <Quotient/jobs/downloadfilejob.h>
0027 #include <Quotient/qt_connection_util.h>
0028 
0029 #include "neochatconfig.h"
0030 #include "neochatroom.h"
0031 #include "notificationsmanager.h"
0032 #include "roommanager.h"
0033 
0034 #if defined(Q_OS_WIN) || defined(Q_OS_MAC)
0035 #include "trayicon.h"
0036 #elif !defined(Q_OS_ANDROID)
0037 #include "trayicon_sni.h"
0038 #endif
0039 
0040 bool testMode = false;
0041 
0042 using namespace Quotient;
0043 
0044 Controller::Controller(QObject *parent)
0045     : QObject(parent)
0046 {
0047     Connection::setRoomType<NeoChatRoom>();
0048 
0049     setApplicationProxy();
0050 
0051 #ifndef Q_OS_ANDROID
0052     setQuitOnLastWindowClosed();
0053     connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed);
0054 #endif
0055 
0056     if (!testMode) {
0057         QTimer::singleShot(0, this, [this] {
0058             invokeLogin();
0059         });
0060     } else {
0061         auto c = new NeoChatConnection(this);
0062         c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("token_1234"));
0063         connect(c, &Connection::connected, this, [c, this]() {
0064             m_accountRegistry.add(c);
0065             c->syncLoop();
0066         });
0067     }
0068 
0069     QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
0070         delete m_trayIcon;
0071         NeoChatConfig::self()->save();
0072     });
0073 
0074 #ifndef Q_OS_WINDOWS
0075     const auto unixExitHandler = [](int) -> void {
0076         QCoreApplication::quit();
0077     };
0078 
0079     const int quitSignals[] = {SIGQUIT, SIGINT, SIGTERM, SIGHUP};
0080 
0081     sigset_t blockingMask;
0082     sigemptyset(&blockingMask);
0083     for (const auto sig : quitSignals) {
0084         sigaddset(&blockingMask, sig);
0085     }
0086 
0087     struct sigaction sa;
0088     sa.sa_handler = unixExitHandler;
0089     sa.sa_mask = blockingMask;
0090     sa.sa_flags = 0;
0091 
0092     for (auto sig : quitSignals) {
0093         sigaction(sig, &sa, nullptr);
0094     }
0095 #endif
0096 
0097     static int oldAccountCount = 0;
0098     connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
0099         if (m_accountRegistry.size() > oldAccountCount) {
0100             auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
0101             connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
0102                 NotificationsManager::instance().handleNotifications(connection);
0103             });
0104             connectSingleShot(connection, &NeoChatConnection::syncDone, this, [this, connection] {
0105                 connection->setupPushNotifications(m_endpoint);
0106             });
0107         }
0108         oldAccountCount = m_accountRegistry.size();
0109     });
0110 
0111 #ifdef HAVE_KUNIFIEDPUSH
0112     auto connector = new KUnifiedPush::Connector(QStringLiteral("org.kde.neochat"));
0113     connect(connector, &KUnifiedPush::Connector::endpointChanged, this, [this](const QString &endpoint) {
0114         m_endpoint = endpoint;
0115         for (auto &quotientConnection : m_accountRegistry) {
0116             auto connection = dynamic_cast<NeoChatConnection *>(quotientConnection);
0117             connection->setupPushNotifications(endpoint);
0118         }
0119     });
0120 
0121     connector->registerClient(i18n("Receiving push notifications"));
0122 
0123     m_endpoint = connector->endpoint();
0124 #endif
0125 }
0126 
0127 Controller &Controller::instance()
0128 {
0129     static Controller _instance;
0130     return _instance;
0131 }
0132 
0133 void Controller::addConnection(NeoChatConnection *c)
0134 {
0135     Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
0136 
0137     m_accountRegistry.add(c);
0138 
0139     c->setLazyLoading(true);
0140 
0141     connect(c, &NeoChatConnection::syncDone, this, [c] {
0142         c->sync(30000);
0143         c->saveState();
0144     });
0145     connect(c, &NeoChatConnection::loggedOut, this, [this, c] {
0146         dropConnection(c);
0147     });
0148 
0149     c->sync();
0150 
0151     Q_EMIT connectionAdded(c);
0152 }
0153 
0154 void Controller::dropConnection(NeoChatConnection *c)
0155 {
0156     Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
0157 
0158     m_accountRegistry.drop(c);
0159     Q_EMIT connectionDropped(c);
0160 }
0161 
0162 void Controller::invokeLogin()
0163 {
0164     const auto accounts = SettingsGroup("Accounts"_ls).childGroups();
0165     for (const auto &accountId : accounts) {
0166         AccountSettings account{accountId};
0167         m_accountsLoading += accountId;
0168         Q_EMIT accountsLoadingChanged();
0169         if (!account.homeserver().isEmpty()) {
0170             auto accessTokenLoadingJob = loadAccessTokenFromKeyChain(account);
0171             connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, this, accessTokenLoadingJob](QKeychain::Job *) {
0172                 AccountSettings account{accountId};
0173                 QString accessToken;
0174                 if (accessTokenLoadingJob->error() == QKeychain::Error::NoError) {
0175                     accessToken = QString::fromLatin1(accessTokenLoadingJob->binaryData());
0176                 } else {
0177                     return;
0178                 }
0179 
0180                 auto connection = new NeoChatConnection(account.homeserver());
0181                 connect(connection, &NeoChatConnection::connected, this, [this, connection] {
0182                     connection->loadState();
0183                     addConnection(connection);
0184                     m_accountsLoading.removeAll(connection->userId());
0185                     Q_EMIT accountsLoadingChanged();
0186                 });
0187                 connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
0188                     Q_EMIT errorOccured(i18n("Network Error: %1", error), {});
0189                 });
0190                 connection->assumeIdentity(account.userId(), accessToken);
0191             });
0192         }
0193     }
0194 }
0195 
0196 QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const AccountSettings &account)
0197 {
0198     qDebug() << "Reading access token from the keychain for" << account.userId();
0199     auto job = new QKeychain::ReadPasswordJob(qAppName(), this);
0200     job->setKey(account.userId());
0201 
0202     // Handling of errors
0203     connect(job, &QKeychain::Job::finished, this, [this, job]() {
0204         if (job->error() == QKeychain::Error::NoError) {
0205             return;
0206         }
0207 
0208         switch (job->error()) {
0209         case QKeychain::EntryNotFound:
0210             Q_EMIT errorOccured(i18n("Access token wasn't found"), i18n("Maybe it was deleted?"));
0211             break;
0212         case QKeychain::AccessDeniedByUser:
0213         case QKeychain::AccessDenied:
0214             Q_EMIT errorOccured(i18n("Access to keychain was denied."), i18n("Please allow NeoChat to read the access token"));
0215             break;
0216         case QKeychain::NoBackendAvailable:
0217             Q_EMIT errorOccured(i18n("No keychain available."), i18n("Please install a keychain, e.g. KWallet or GNOME keyring on Linux"));
0218             break;
0219         case QKeychain::OtherError:
0220             Q_EMIT errorOccured(i18n("Unable to read access token"), job->errorString());
0221             break;
0222         default:
0223             break;
0224         }
0225     });
0226     job->start();
0227 
0228     return job;
0229 }
0230 
0231 bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken)
0232 {
0233     qDebug() << "Save the access token to the keychain for " << account.userId();
0234     QKeychain::WritePasswordJob job(qAppName());
0235     job.setAutoDelete(false);
0236     job.setKey(account.userId());
0237     job.setBinaryData(accessToken);
0238     QEventLoop loop;
0239     QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
0240     job.start();
0241     loop.exec();
0242 
0243     if (job.error()) {
0244         qWarning() << "Could not save access token to the keychain: " << qPrintable(job.errorString());
0245         return false;
0246     }
0247     return true;
0248 }
0249 
0250 bool Controller::supportSystemTray() const
0251 {
0252 #ifdef Q_OS_ANDROID
0253     return false;
0254 #else
0255     auto de = QString::fromLatin1(qgetenv("XDG_CURRENT_DESKTOP"));
0256     return de != QStringLiteral("GNOME") && de != QStringLiteral("Pantheon");
0257 #endif
0258 }
0259 
0260 void Controller::setQuitOnLastWindowClosed()
0261 {
0262 #ifndef Q_OS_ANDROID
0263     if (NeoChatConfig::self()->systemTray()) {
0264         m_trayIcon = new TrayIcon(this);
0265         m_trayIcon->show();
0266     } else {
0267         if (m_trayIcon) {
0268             delete m_trayIcon;
0269             m_trayIcon = nullptr;
0270         }
0271     }
0272 #endif
0273 }
0274 
0275 NeoChatConnection *Controller::activeConnection() const
0276 {
0277     if (m_connection.isNull()) {
0278         return nullptr;
0279     }
0280     return m_connection;
0281 }
0282 
0283 void Controller::setActiveConnection(NeoChatConnection *connection)
0284 {
0285     if (connection == m_connection) {
0286         return;
0287     }
0288     if (m_connection != nullptr) {
0289         disconnect(m_connection, &NeoChatConnection::syncError, this, nullptr);
0290         disconnect(m_connection, &NeoChatConnection::accountDataChanged, this, nullptr);
0291     }
0292     m_connection = connection;
0293     if (connection != nullptr) {
0294         connect(connection, &NeoChatConnection::requestFailed, this, [](BaseJob *job) {
0295             if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"_ls].toString() == "M_TOO_LARGE"_ls) {
0296                 RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
0297             }
0298         });
0299     }
0300     NeoChatConfig::self()->save();
0301     Q_EMIT activeConnectionChanged();
0302 }
0303 
0304 void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
0305 {
0306     // HACK: Workaround bug QTBUG 93281
0307     connect(textDocument->textDocument(), SIGNAL(imagesLoaded()), item, SLOT(updateWholeDocument()));
0308 }
0309 
0310 void Controller::listenForNotifications()
0311 {
0312 #ifdef HAVE_KUNIFIEDPUSH
0313     auto connector = new KUnifiedPush::Connector(QStringLiteral("org.kde.neochat"));
0314 
0315     auto timer = new QTimer();
0316     connect(timer, &QTimer::timeout, qGuiApp, &QGuiApplication::quit);
0317 
0318     connect(connector, &KUnifiedPush::Connector::messageReceived, [timer](const QByteArray &data) {
0319         NotificationsManager::instance().postPushNotification(data);
0320         timer->stop();
0321     });
0322 
0323     // Wait five seconds to see if we received any messages or this happened to be an erroneous activation.
0324     // Otherwise, messageReceived is never activated, and this daemon could stick around forever.
0325     timer->start(5000);
0326 
0327     connector->registerClient(i18n("Receiving push notifications"));
0328 #endif
0329 }
0330 
0331 void Controller::setApplicationProxy()
0332 {
0333     NeoChatConfig *cfg = NeoChatConfig::self();
0334     QNetworkProxy proxy;
0335 
0336     // type match to ProxyType from neochatconfig.kcfg
0337     switch (cfg->proxyType()) {
0338     case 1: // HTTP
0339         proxy.setType(QNetworkProxy::HttpProxy);
0340         proxy.setHostName(cfg->proxyHost());
0341         proxy.setPort(cfg->proxyPort());
0342         proxy.setUser(cfg->proxyUser());
0343         proxy.setPassword(cfg->proxyPassword());
0344         break;
0345     case 2: // SOCKS 5
0346         proxy.setType(QNetworkProxy::Socks5Proxy);
0347         proxy.setHostName(cfg->proxyHost());
0348         proxy.setPort(cfg->proxyPort());
0349         proxy.setUser(cfg->proxyUser());
0350         proxy.setPassword(cfg->proxyPassword());
0351         break;
0352     case 0: // System Default
0353     default:
0354         // do nothing
0355         break;
0356     }
0357 
0358     QNetworkProxy::setApplicationProxy(proxy);
0359 }
0360 
0361 bool Controller::isFlatpak() const
0362 {
0363 #ifdef NEOCHAT_FLATPAK
0364     return true;
0365 #else
0366     return false;
0367 #endif
0368 }
0369 
0370 AccountRegistry &Controller::accounts()
0371 {
0372     return m_accountRegistry;
0373 }
0374 
0375 #include "moc_controller.cpp"
0376 
0377 void Controller::setTestMode(bool test)
0378 {
0379     testMode = test;
0380 }