File indexing completed on 2024-09-15 04:28:26
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/qt_connection_util.h> 0027 0028 #include "neochatconfig.h" 0029 #include "neochatroom.h" 0030 #include "notificationsmanager.h" 0031 #include "proxycontroller.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 ProxyController::instance().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 "ientConnection : 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 m_connection = connection; 0289 Q_EMIT activeConnectionChanged(); 0290 } 0291 0292 void Controller::listenForNotifications() 0293 { 0294 #ifdef HAVE_KUNIFIEDPUSH 0295 auto connector = new KUnifiedPush::Connector(QStringLiteral("org.kde.neochat")); 0296 0297 auto timer = new QTimer(); 0298 connect(timer, &QTimer::timeout, qGuiApp, &QGuiApplication::quit); 0299 0300 connect(connector, &KUnifiedPush::Connector::messageReceived, [timer](const QByteArray &data) { 0301 NotificationsManager::instance().postPushNotification(data); 0302 timer->stop(); 0303 }); 0304 0305 // Wait five seconds to see if we received any messages or this happened to be an erroneous activation. 0306 // Otherwise, messageReceived is never activated, and this daemon could stick around forever. 0307 timer->start(5000); 0308 0309 connector->registerClient(i18n("Receiving push notifications")); 0310 #endif 0311 } 0312 0313 bool Controller::isFlatpak() const 0314 { 0315 #ifdef NEOCHAT_FLATPAK 0316 return true; 0317 #else 0318 return false; 0319 #endif 0320 } 0321 0322 AccountRegistry &Controller::accounts() 0323 { 0324 return m_accountRegistry; 0325 } 0326 0327 #include "moc_controller.cpp" 0328 0329 void Controller::setTestMode(bool test) 0330 { 0331 testMode = test; 0332 }