/* SPDX-FileCopyrightText: 2022 Volker Krause SPDX-License-Identifier: LGPL-2.0-or-later */ #include "distributor.h" #include "distributor1adaptor.h" #include "managementadaptor.h" #include "client.h" #include "connector1iface.h" #include "gotifypushprovider.h" #include "logging.h" #include "message.h" #include "mockpushprovider.h" #include "nextpushprovider.h" #include "ntfypushprovider.h" #include "../shared/unifiedpush-constants.h" #include #include #include using namespace KUnifiedPush; Distributor::Distributor(QObject *parent) : QObject(parent) { qDBusRegisterMetaType(); qDBusRegisterMetaType>(); // setup network status tracking if (QNetworkInformation::loadBackendByFeatures(QNetworkInformation::Feature::Reachability)) { connect(QNetworkInformation::instance(), &QNetworkInformation::reachabilityChanged, this, &Distributor::processNextCommand); } else { qCWarning(Log) << "No network state information available!" << QNetworkInformation::availableBackends(); } // register at D-Bus new Distributor1Adaptor(this); QDBusConnection::sessionBus().registerObject(QLatin1String(UP_DISTRIBUTOR_PATH), this); new ManagementAdaptor(this); QDBusConnection::sessionBus().registerObject(QLatin1String(KDE_DISTRIBUTOR_MANAGEMENT_PATH), this); // create and set up push provider if (!setupPushProvider()) { return; } // load previous clients // TODO what happens to existing clients if the above failed? QSettings settings; const auto clientTokens = settings.value(QStringLiteral("Clients/Tokens"), QStringList()).toStringList(); m_clients.reserve(clientTokens.size()); for (const auto &token : clientTokens) { auto client = Client::load(token, settings); if (client.isValid()) { m_clients.push_back(std::move(client)); } } qCDebug(Log) << m_clients.size() << "registered clients loaded"; // purge uninstalled apps purgeUnavailableClients(); // connect to push provider if necessary if (!m_clients.empty()) { setStatus(DistributorStatus::NoNetwork); Command cmd; cmd.type = Command::Connect; m_commandQueue.push_back(std::move(cmd)); } else { setStatus(DistributorStatus::Idle); } processNextCommand(); } Distributor::~Distributor() = default; QString Distributor::Register(const QString& serviceName, const QString& token, const QString &description, QString& registrationResultReason) { qCDebug(Log) << serviceName << token; const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&token](const auto &client) { return client.token == token; }); if (it == m_clients.end()) { qCDebug(Log) << "Registering new client"; // if this is the first client, connect to the push provider first // this can involve first-time device registration that is a prerequisite for registering clients if (m_clients.empty()) { Command cmd; cmd.type = Command::Connect; m_commandQueue.push_back(std::move(cmd)); } Command cmd; cmd.type = Command::Register; cmd.client.token = token; cmd.client.serviceName = serviceName; cmd.client.description = description; setDelayedReply(true); cmd.reply = message().createReply(); m_commandQueue.push_back(std::move(cmd)); processNextCommand(); return {}; } qCDebug(Log) << "Registering known client"; (*it).activate(); (*it).connector().NewEndpoint((*it).token, (*it).endpoint); registrationResultReason.clear(); return QLatin1String(UP_REGISTER_RESULT_SUCCESS); } void Distributor::Unregister(const QString& token) { qCDebug(Log) << token; const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&token](const auto &client) { return client.token == token; }); if (it == m_clients.end()) { qCWarning(Log) << "Unregistration request for unknown client."; return; } Command cmd; cmd.type = Command::Unregister; cmd.client = (*it); m_commandQueue.push_back(std::move(cmd)); processNextCommand(); } void Distributor::messageReceived(const Message &msg) const { qCDebug(Log) << msg.clientRemoteId << msg.content; const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&msg](const auto &client) { return client.remoteId == msg.clientRemoteId; }); if (it == m_clients.end()) { qCWarning(Log) << "Received message for unknown client"; return; } (*it).activate(); (*it).connector().Message((*it).token, msg.content, {}); } void Distributor::clientRegistered(const Client &client, AbstractPushProvider::Error error, const QString &errorMsg) { qCDebug(Log) << client.token << client.remoteId << client.serviceName << error << errorMsg; switch (error) { case AbstractPushProvider::NoError: { // TODO check whether we got an endpoint, otherwise report an error m_clients.push_back(client); QSettings settings; client.store(settings); settings.setValue(QStringLiteral("Clients/Tokens"), clientTokens()); Q_EMIT registeredClientsChanged(); client.connector().NewEndpoint(client.token, client.endpoint); if (m_currentCommand.reply.type() != QDBusMessage::InvalidMessage) { m_currentCommand.reply << QString::fromLatin1(UP_REGISTER_RESULT_SUCCESS) << QString(); QDBusConnection::sessionBus().send(m_currentCommand.reply); } break; } case AbstractPushProvider::TransientNetworkError: // retry m_commandQueue.push_front(std::move(m_currentCommand)); break; case AbstractPushProvider::ProviderRejected: m_currentCommand.reply << QString::fromLatin1(UP_REGISTER_RESULT_FAILURE) << errorMsg; QDBusConnection::sessionBus().send(m_currentCommand.reply); break; } m_currentCommand = {}; processNextCommand(); } void Distributor::clientUnregistered(const Client &client, AbstractPushProvider::Error error) { qCDebug(Log) << client.token << client.remoteId << client.serviceName << error; switch (error) { case AbstractPushProvider::NoError: client.connector().Unregistered(m_currentCommand.type == Command::Unregister ? QString() : client.token); [[fallthrough]]; case AbstractPushProvider::ProviderRejected: { QSettings settings; settings.remove(client.token); const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&client](const auto &c) { return c.token == client.token; }); if (it != m_clients.end()) { m_clients.erase(it); // if this was the last client, also disconnect from the push provider if (m_clients.empty()) { Command cmd; cmd.type = Command::Disconnect; m_commandQueue.push_back(std::move(cmd)); } } settings.setValue(QStringLiteral("Clients/Tokens"), clientTokens()); Q_EMIT registeredClientsChanged(); break; } case AbstractPushProvider::TransientNetworkError: // retry m_commandQueue.push_front(std::move(m_currentCommand)); break; } m_currentCommand = {}; processNextCommand(); } void Distributor::providerConnected() { qCDebug(Log); setStatus(DistributorStatus::Connected); m_currentCommand = {}; processNextCommand(); } void Distributor::providerDisconnected(AbstractPushProvider::Error error, const QString &errorMsg) { qCDebug(Log) << error << errorMsg; if (m_currentCommand.type == Command::Disconnect) { m_currentCommand = {}; setStatus(m_clients.empty() ? DistributorStatus::Idle : DistributorStatus::NoNetwork); } else { setStatus(DistributorStatus::NoNetwork); } processNextCommand(); } QStringList Distributor::clientTokens() const { QStringList l; l.reserve(m_clients.size()); std::transform(m_clients.begin(), m_clients.end(), std::back_inserter(l), [](const auto &client) { return client.token; }); return l; } bool Distributor::setupPushProvider() { // determine push provider const auto pushProviderName = pushProviderId(); if (pushProviderName == QLatin1String(GotifyPushProvider::Id)) { m_pushProvider.reset(new GotifyPushProvider); } else if (pushProviderName == QLatin1String(NextPushProvider::Id)) { m_pushProvider.reset(new NextPushProvider); } else if (pushProviderName == QLatin1String(NtfyPushProvider::Id)) { m_pushProvider.reset(new NtfyPushProvider); } else if (pushProviderName == QLatin1String(MockPushProvider::Id)) { m_pushProvider.reset(new MockPushProvider); } else { qCWarning(Log) << "Unknown push provider:" << pushProviderName; m_pushProvider.reset(); setStatus(DistributorStatus::NoSetup); return false; } QSettings settings; settings.beginGroup(pushProviderName); if (!m_pushProvider->loadSettings(settings)) { qCWarning(Log) << "Invalid push provider settings!"; setStatus(DistributorStatus::NoSetup); return false; } settings.endGroup(); connect(m_pushProvider.get(), &AbstractPushProvider::messageReceived, this, &Distributor::messageReceived); connect(m_pushProvider.get(), &AbstractPushProvider::clientRegistered, this, &Distributor::clientRegistered); connect(m_pushProvider.get(), &AbstractPushProvider::clientUnregistered, this, &Distributor::clientUnregistered); connect(m_pushProvider.get(), &AbstractPushProvider::connected, this, &Distributor::providerConnected); connect(m_pushProvider.get(), &AbstractPushProvider::disconnected, this, &Distributor::providerDisconnected); return true; } void Distributor::purgeUnavailableClients() { QStringList activatableServiceNames = QDBusConnection::sessionBus().interface()->activatableServiceNames(); std::sort(activatableServiceNames.begin(), activatableServiceNames.end()); // collect clients to unregister first, so m_clients doesn't change underneath us QStringList tokensToUnregister; for (const auto &client : m_clients) { if (!std::binary_search(activatableServiceNames.begin(), activatableServiceNames.end(), client.serviceName)) { tokensToUnregister.push_back(client.token); } } for (const auto &token : tokensToUnregister) { Unregister(token); } } bool Distributor::hasCurrentCommand() const { return m_currentCommand.type != Command::NoCommand; } void Distributor::processNextCommand() { if (hasCurrentCommand() || m_commandQueue.empty() || !isNetworkAvailable()) { return; } m_currentCommand = m_commandQueue.front(); m_commandQueue.pop_front(); switch (m_currentCommand.type) { case Command::NoCommand: Q_ASSERT(false); processNextCommand(); break; case Command::Register: m_pushProvider->registerClient(m_currentCommand.client); break; case Command::Unregister: case Command::ForceUnregister: m_pushProvider->unregisterClient(m_currentCommand.client); break; case Command::Connect: m_pushProvider->connectToProvider(); break; case Command::Disconnect: m_pushProvider->disconnectFromProvider(); break; case Command::ChangePushProvider: { QSettings settings; settings.setValue(QLatin1String("PushProvider/Type"), m_currentCommand.pushProvider); m_currentCommand = {}; if (setupPushProvider()) { processNextCommand(); } break; } } } int Distributor::status() const { return m_status; } void Distributor::setStatus(DistributorStatus::Status status) { if (m_status == status) { return; } m_status = status; Q_EMIT statusChanged(); } QString Distributor::pushProviderId() const { QSettings settings; return settings.value(QStringLiteral("PushProvider/Type"), QString()).toString(); } QVariantMap Distributor::pushProviderConfiguration(const QString &pushProviderId) const { if (pushProviderId.isEmpty()) { return {}; } QSettings settings; settings.beginGroup(pushProviderId); const auto keys = settings.allKeys(); QVariantMap config; for (const auto &key : keys) { const auto v = settings.value(key); if (v.isValid()) { config.insert(key, settings.value(key)); } } return config; } void Distributor::setPushProvider(const QString &pushProviderId, const QVariantMap &config) { // store push provider config and check for changes bool configChanged = false; QSettings settings; settings.beginGroup(pushProviderId); for (auto it = config.begin(); it != config.end(); ++it) { const auto oldValue = settings.value(it.key()); configChanged |= oldValue != it.value(); settings.setValue(it.key(), it.value()); } settings.endGroup(); if (!configChanged && pushProviderId == this->pushProviderId()) { return; // nothing changed } // if push provider or config changed: unregister all clients, create new push provider backend, re-register all clients if (m_status != DistributorStatus::NoSetup) { for (const auto &client : m_clients) { forceUnregisterClient(client.token); } if (m_status == DistributorStatus::Connected) { Command cmd; cmd.type = Command::Disconnect; m_commandQueue.push_back(std::move(cmd)); } { Command cmd; cmd.type = Command::ChangePushProvider; cmd.pushProvider = pushProviderId; m_commandQueue.push_back(std::move(cmd)); } // reconnect if there are clients if (!m_clients.empty()) { Command cmd; cmd.type = Command::Connect; m_commandQueue.push_back(std::move(cmd)); } // re-register clients for (const auto &client : m_clients) { Command cmd; cmd.type = Command::Register; cmd.client = client; m_commandQueue.push_back(std::move(cmd)); } } else { // recover from a previously failed attempt to change push providers // reconnect if there are clients if (!m_commandQueue.empty()) { Command cmd; cmd.type = Command::Connect; m_commandQueue.push_front(std::move(cmd)); } Command cmd; cmd.type = Command::ChangePushProvider; cmd.pushProvider = pushProviderId; m_commandQueue.push_front(std::move(cmd)); } processNextCommand(); } QList Distributor::registeredClients() const { QList result; result.reserve(m_clients.size()); for (const auto &client : m_clients) { ClientInfo info; info.token = client.token; info.serviceName = client.serviceName; info.description = client.description; result.push_back(std::move(info)); } return result; } void Distributor::forceUnregisterClient(const QString &token) { qCDebug(Log) << token; const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&token](const auto &client) { return client.token == token; }); if (it == m_clients.end()) { qCWarning(Log) << "Unregistration request for unknown client."; return; } Command cmd; cmd.type = Command::ForceUnregister; cmd.client = (*it); m_commandQueue.push_back(std::move(cmd)); processNextCommand(); } bool Distributor::isNetworkAvailable() const { // if in doubt assume we have network and try to connect if (QNetworkInformation::instance()) { const auto reachability = QNetworkInformation::instance()->reachability(); return reachability == QNetworkInformation::Reachability::Online || reachability == QNetworkInformation::Reachability::Unknown; } return true; } #include "moc_distributor.cpp"