KUnifiedPush

distributor.cpp
1/*
2 SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5
6#include "distributor.h"
7#include "distributor1adaptor.h"
8#include "managementadaptor.h"
9
10#include "client.h"
11#include "connector1iface.h"
12#include "gotifypushprovider.h"
13#include "logging.h"
14#include "message.h"
15#include "mockpushprovider.h"
16#include "nextpushprovider.h"
17#include "ntfypushprovider.h"
18
19#include "../shared/unifiedpush-constants.h"
20
21#include <QDBusConnection>
22#include <QSettings>
23
24#include <QNetworkInformation>
25
26using namespace KUnifiedPush;
27
28Distributor::Distributor(QObject *parent)
29 : QObject(parent)
30{
31 qDBusRegisterMetaType<KUnifiedPush::ClientInfo>();
32 qDBusRegisterMetaType<QList<KUnifiedPush::ClientInfo>>();
33
34 // setup network status tracking
35 if (QNetworkInformation::loadBackendByFeatures(QNetworkInformation::Feature::Reachability)) {
36 connect(QNetworkInformation::instance(), &QNetworkInformation::reachabilityChanged, this, &Distributor::processNextCommand);
37 } else {
38 qCWarning(Log) << "No network state information available!" << QNetworkInformation::availableBackends();
39 }
40
41 // register at D-Bus
42 new Distributor1Adaptor(this);
43 QDBusConnection::sessionBus().registerObject(QLatin1String(UP_DISTRIBUTOR_PATH), this);
44
45 new ManagementAdaptor(this);
46 QDBusConnection::sessionBus().registerObject(QLatin1String(KDE_DISTRIBUTOR_MANAGEMENT_PATH), this);
47
48 // create and set up push provider
49 if (!setupPushProvider()) {
50 return;
51 }
52
53 // load previous clients
54 // TODO what happens to existing clients if the above failed?
55 QSettings settings;
56 const auto clientTokens = settings.value(QStringLiteral("Clients/Tokens"), QStringList()).toStringList();
57 m_clients.reserve(clientTokens.size());
58 for (const auto &token : clientTokens) {
59 auto client = Client::load(token, settings);
60 if (client.isValid()) {
61 m_clients.push_back(std::move(client));
62 }
63 }
64 qCDebug(Log) << m_clients.size() << "registered clients loaded";
65
66 // purge uninstalled apps
67 purgeUnavailableClients();
68
69 // connect to push provider if necessary
70 if (!m_clients.empty())
71 {
72 setStatus(DistributorStatus::NoNetwork);
73 Command cmd;
74 cmd.type = Command::Connect;
75 m_commandQueue.push_back(std::move(cmd));
76 } else {
77 setStatus(DistributorStatus::Idle);
78 }
79
80 processNextCommand();
81}
82
83Distributor::~Distributor() = default;
84
85QString Distributor::Register(const QString& serviceName, const QString& token, const QString &description, QString& registrationResultReason)
86{
87 qCDebug(Log) << serviceName << token;
88 const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&token](const auto &client) {
89 return client.token == token;
90 });
91 if (it == m_clients.end()) {
92 qCDebug(Log) << "Registering new client";
93
94 // if this is the first client, connect to the push provider first
95 // this can involve first-time device registration that is a prerequisite for registering clients
96 if (m_clients.empty()) {
97 Command cmd;
98 cmd.type = Command::Connect;
99 m_commandQueue.push_back(std::move(cmd));
100 }
101
102 Command cmd;
103 cmd.type = Command::Register;
104 cmd.client.token = token;
105 cmd.client.serviceName = serviceName;
106 cmd.client.description = description;
107 setDelayedReply(true);
108 cmd.reply = message().createReply();
109 m_commandQueue.push_back(std::move(cmd));
110
111 processNextCommand();
112 return {};
113 }
114
115 qCDebug(Log) << "Registering known client";
116 (*it).activate();
117 (*it).connector().NewEndpoint((*it).token, (*it).endpoint);
118 registrationResultReason.clear();
119 return QLatin1String(UP_REGISTER_RESULT_SUCCESS);
120}
121
122void Distributor::Unregister(const QString& token)
123{
124 qCDebug(Log) << token;
125 const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&token](const auto &client) {
126 return client.token == token;
127 });
128 if (it == m_clients.end()) {
129 qCWarning(Log) << "Unregistration request for unknown client.";
130 return;
131 }
132
133 Command cmd;
134 cmd.type = Command::Unregister;
135 cmd.client = (*it);
136 m_commandQueue.push_back(std::move(cmd));
137 processNextCommand();
138}
139
140void Distributor::messageReceived(const Message &msg) const
141{
142 qCDebug(Log) << msg.clientRemoteId << msg.content;
143 const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&msg](const auto &client) {
144 return client.remoteId == msg.clientRemoteId;
145 });
146 if (it == m_clients.end()) {
147 qCWarning(Log) << "Received message for unknown client";
148 return;
149 }
150
151 (*it).activate();
152 (*it).connector().Message((*it).token, msg.content, {});
153}
154
155void Distributor::clientRegistered(const Client &client, AbstractPushProvider::Error error, const QString &errorMsg)
156{
157 qCDebug(Log) << client.token << client.remoteId << client.serviceName << error << errorMsg;
158 switch (error) {
160 {
161 // TODO check whether we got an endpoint, otherwise report an error
162 m_clients.push_back(client);
163
164 QSettings settings;
165 client.store(settings);
166 settings.setValue(QStringLiteral("Clients/Tokens"), clientTokens());
167 Q_EMIT registeredClientsChanged();
168
169 client.connector().NewEndpoint(client.token, client.endpoint);
170
171 if (m_currentCommand.reply.type() != QDBusMessage::InvalidMessage) {
172 m_currentCommand.reply << QString::fromLatin1(UP_REGISTER_RESULT_SUCCESS) << QString();
173 QDBusConnection::sessionBus().send(m_currentCommand.reply);
174 }
175 break;
176 }
178 // retry
179 m_commandQueue.push_front(std::move(m_currentCommand));
180 break;
182 m_currentCommand.reply << QString::fromLatin1(UP_REGISTER_RESULT_FAILURE) << errorMsg;
183 QDBusConnection::sessionBus().send(m_currentCommand.reply);
184 break;
185 }
186
187 m_currentCommand = {};
188 processNextCommand();
189}
190
191void Distributor::clientUnregistered(const Client &client, AbstractPushProvider::Error error)
192{
193 qCDebug(Log) << client.token << client.remoteId << client.serviceName << error;
194 switch (error) {
196 client.connector().Unregistered(m_currentCommand.type == Command::Unregister ? QString() : client.token);
197 [[fallthrough]];
199 {
200 QSettings settings;
201 settings.remove(client.token);
202 const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&client](const auto &c) {
203 return c.token == client.token;
204 });
205 if (it != m_clients.end()) {
206 m_clients.erase(it);
207
208 // if this was the last client, also disconnect from the push provider
209 if (m_clients.empty()) {
210 Command cmd;
211 cmd.type = Command::Disconnect;
212 m_commandQueue.push_back(std::move(cmd));
213 }
214 }
215 settings.setValue(QStringLiteral("Clients/Tokens"), clientTokens());
216 Q_EMIT registeredClientsChanged();
217 break;
218 }
220 // retry
221 m_commandQueue.push_front(std::move(m_currentCommand));
222 break;
223 }
224
225 m_currentCommand = {};
226 processNextCommand();
227}
228
229void Distributor::providerConnected()
230{
231 qCDebug(Log);
232 setStatus(DistributorStatus::Connected);
233 m_currentCommand = {};
234 processNextCommand();
235}
236
237void Distributor::providerDisconnected(AbstractPushProvider::Error error, const QString &errorMsg)
238{
239 qCDebug(Log) << error << errorMsg;
240 if (m_currentCommand.type == Command::Disconnect) {
241 m_currentCommand = {};
242 setStatus(m_clients.empty() ? DistributorStatus::Idle : DistributorStatus::NoNetwork);
243 } else {
244 setStatus(DistributorStatus::NoNetwork);
245 }
246 processNextCommand();
247}
248
249QStringList Distributor::clientTokens() const
250{
251 QStringList l;
252 l.reserve(m_clients.size());
253 std::transform(m_clients.begin(), m_clients.end(), std::back_inserter(l), [](const auto &client) { return client.token; });
254 return l;
255}
256
257bool Distributor::setupPushProvider()
258{
259 // determine push provider
260 const auto pushProviderName = pushProviderId();
261 if (pushProviderName == QLatin1String(GotifyPushProvider::Id)) {
262 m_pushProvider.reset(new GotifyPushProvider);
263 } else if (pushProviderName == QLatin1String(NextPushProvider::Id)) {
264 m_pushProvider.reset(new NextPushProvider);
265 } else if (pushProviderName == QLatin1String(NtfyPushProvider::Id)) {
266 m_pushProvider.reset(new NtfyPushProvider);
267 } else if (pushProviderName == QLatin1String(MockPushProvider::Id)) {
268 m_pushProvider.reset(new MockPushProvider);
269 } else {
270 qCWarning(Log) << "Unknown push provider:" << pushProviderName;
271 m_pushProvider.reset();
272 setStatus(DistributorStatus::NoSetup);
273 return false;
274 }
275
276 QSettings settings;
277 settings.beginGroup(pushProviderName);
278 if (!m_pushProvider->loadSettings(settings)) {
279 qCWarning(Log) << "Invalid push provider settings!";
280 setStatus(DistributorStatus::NoSetup);
281 return false;
282 }
283 settings.endGroup();
284
285 connect(m_pushProvider.get(), &AbstractPushProvider::messageReceived, this, &Distributor::messageReceived);
286 connect(m_pushProvider.get(), &AbstractPushProvider::clientRegistered, this, &Distributor::clientRegistered);
287 connect(m_pushProvider.get(), &AbstractPushProvider::clientUnregistered, this, &Distributor::clientUnregistered);
288 connect(m_pushProvider.get(), &AbstractPushProvider::connected, this, &Distributor::providerConnected);
289 connect(m_pushProvider.get(), &AbstractPushProvider::disconnected, this, &Distributor::providerDisconnected);
290 return true;
291}
292
293void Distributor::purgeUnavailableClients()
294{
296 std::sort(activatableServiceNames.begin(), activatableServiceNames.end());
297
298 // collect clients to unregister first, so m_clients doesn't change underneath us
299 QStringList tokensToUnregister;
300 for (const auto &client : m_clients) {
301 if (!std::binary_search(activatableServiceNames.begin(), activatableServiceNames.end(), client.serviceName)) {
302 tokensToUnregister.push_back(client.token);
303 }
304 }
305
306 for (const auto &token : tokensToUnregister) {
307 Unregister(token);
308 }
309}
310
311bool Distributor::hasCurrentCommand() const
312{
313 return m_currentCommand.type != Command::NoCommand;
314}
315
316void Distributor::processNextCommand()
317{
318 if (hasCurrentCommand() || m_commandQueue.empty() || !isNetworkAvailable()) {
319 return;
320 }
321
322 m_currentCommand = m_commandQueue.front();
323 m_commandQueue.pop_front();
324 switch (m_currentCommand.type) {
325 case Command::NoCommand:
326 Q_ASSERT(false);
327 processNextCommand();
328 break;
329 case Command::Register:
330 m_pushProvider->registerClient(m_currentCommand.client);
331 break;
334 m_pushProvider->unregisterClient(m_currentCommand.client);
335 break;
336 case Command::Connect:
337 m_pushProvider->connectToProvider();
338 break;
339 case Command::Disconnect:
340 m_pushProvider->disconnectFromProvider();
341 break;
342 case Command::ChangePushProvider:
343 {
344 QSettings settings;
345 settings.setValue(QLatin1String("PushProvider/Type"), m_currentCommand.pushProvider);
346 m_currentCommand = {};
347 if (setupPushProvider()) {
348 processNextCommand();
349 }
350 break;
351 }
352 }
353}
354
355int Distributor::status() const
356{
357 return m_status;
358}
359
360void Distributor::setStatus(DistributorStatus::Status status)
361{
362 if (m_status == status) {
363 return;
364 }
365
366 m_status = status;
367 Q_EMIT statusChanged();
368}
369
370QString Distributor::pushProviderId() const
371{
372 QSettings settings;
373 return settings.value(QStringLiteral("PushProvider/Type"), QString()).toString();
374}
375
376QVariantMap Distributor::pushProviderConfiguration(const QString &pushProviderId) const
377{
378 if (pushProviderId.isEmpty()) {
379 return {};
380 }
381
382 QSettings settings;
383 settings.beginGroup(pushProviderId);
384 const auto keys = settings.allKeys();
385
386 QVariantMap config;
387 for (const auto &key : keys) {
388 const auto v = settings.value(key);
389 if (v.isValid()) {
390 config.insert(key, settings.value(key));
391 }
392 }
393
394 return config;
395}
396
397void Distributor::setPushProvider(const QString &pushProviderId, const QVariantMap &config)
398{
399 // store push provider config and check for changes
400 bool configChanged = false;
401 QSettings settings;
402 settings.beginGroup(pushProviderId);
403 for (auto it = config.begin(); it != config.end(); ++it) {
404 const auto oldValue = settings.value(it.key());
405 configChanged |= oldValue != it.value();
406 settings.setValue(it.key(), it.value());
407 }
408 settings.endGroup();
409 if (!configChanged && pushProviderId == this->pushProviderId()) {
410 return; // nothing changed
411 }
412
413 // if push provider or config changed: unregister all clients, create new push provider backend, re-register all clients
414 if (m_status != DistributorStatus::NoSetup) {
415 for (const auto &client : m_clients) {
416 forceUnregisterClient(client.token);
417 }
418 if (m_status == DistributorStatus::Connected) {
419 Command cmd;
420 cmd.type = Command::Disconnect;
421 m_commandQueue.push_back(std::move(cmd));
422 }
423 {
424 Command cmd;
425 cmd.type = Command::ChangePushProvider;
426 cmd.pushProvider = pushProviderId;
427 m_commandQueue.push_back(std::move(cmd));
428 }
429
430 // reconnect if there are clients
431 if (!m_clients.empty()) {
432 Command cmd;
433 cmd.type = Command::Connect;
434 m_commandQueue.push_back(std::move(cmd));
435 }
436
437 // re-register clients
438 for (const auto &client : m_clients) {
439 Command cmd;
440 cmd.type = Command::Register;
441 cmd.client = client;
442 m_commandQueue.push_back(std::move(cmd));
443 }
444 } else {
445 // recover from a previously failed attempt to change push providers
446
447 // reconnect if there are clients
448 if (!m_commandQueue.empty()) {
449 Command cmd;
450 cmd.type = Command::Connect;
451 m_commandQueue.push_front(std::move(cmd));
452 }
453
454 Command cmd;
455 cmd.type = Command::ChangePushProvider;
456 cmd.pushProvider = pushProviderId;
457 m_commandQueue.push_front(std::move(cmd));
458 }
459
460 processNextCommand();
461}
462
463QList<KUnifiedPush::ClientInfo> Distributor::registeredClients() const
464{
466 result.reserve(m_clients.size());
467
468 for (const auto &client : m_clients) {
469 ClientInfo info;
470 info.token = client.token;
471 info.serviceName = client.serviceName;
472 info.description = client.description;
473 result.push_back(std::move(info));
474 }
475
476 return result;
477}
478
479void Distributor::forceUnregisterClient(const QString &token)
480{
481 qCDebug(Log) << token;
482 const auto it = std::find_if(m_clients.begin(), m_clients.end(), [&token](const auto &client) {
483 return client.token == token;
484 });
485 if (it == m_clients.end()) {
486 qCWarning(Log) << "Unregistration request for unknown client.";
487 return;
488 }
489
490 Command cmd;
491 cmd.type = Command::ForceUnregister;
492 cmd.client = (*it);
493 m_commandQueue.push_back(std::move(cmd));
494 processNextCommand();
495}
496
497bool Distributor::isNetworkAvailable() const
498{
499 // if in doubt assume we have network and try to connect
501 const auto reachability = QNetworkInformation::instance()->reachability();
502 return reachability == QNetworkInformation::Reachability::Online || reachability == QNetworkInformation::Reachability::Unknown;
503 }
504 return true;
505}
506
507#include "moc_distributor.cpp"
void messageReceived(const KUnifiedPush::Message &msg)
Inform about a received push notification.
void connected()
Emitted after the connection to the push provider has been established successfully.
void clientUnregistered(const KUnifiedPush::Client &client, KUnifiedPush::AbstractPushProvider::Error error=NoError)
Emitted after successful client unregistration.
void disconnected(KUnifiedPush::AbstractPushProvider::Error error, const QString &errorMsg={})
Emitted after the connection to the push provider disconnected or failed to be established.
void clientRegistered(const KUnifiedPush::Client &client, KUnifiedPush::AbstractPushProvider::Error error=NoError, const QString &errorMsg={})
Emitted after successful client registration.
@ ProviderRejected
communication worked, but the provider refused to complete the operation
@ TransientNetworkError
temporary network error, try again
Information about a registered client.
Definition client.h:19
OrgUnifiedpushConnector1Interface connector() const
D-Bus UnifiedPush connector interface.
Definition client.cpp:52
Distributor command queue entries.
Definition command.h:17
@ Unregister
unregistration requested by client
Definition command.h:22
@ ForceUnregister
unregistration triggered by distributor
Definition command.h:23
Push provider protocol implementation for Gotify.
A received push notification message.
Definition message.h:15
NextPush push provider implementation.
Q_SCRIPTABLE CaptureState status()
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
Client-side integration with UnifiedPush.
Definition connector.h:16
NETWORKMANAGERQT_EXPORT NetworkManager::Status status()
QDBusConnectionInterface * interface() const const
bool registerObject(const QString &path, QObject *object, RegisterOptions options)
bool send(const QDBusMessage &message) const const
QDBusConnection sessionBus()
const QDBusMessage & message() const const
void setDelayedReply(bool enable) const const
QDBusMessage createReply(const QList< QVariant > &arguments) const const
MessageType type() const const
iterator begin()
iterator end()
void push_back(parameter_type value)
void reserve(qsizetype size)
QStringList availableBackends()
QNetworkInformation * instance()
bool loadBackendByFeatures(Features features)
void reachabilityChanged(Reachability newReachability)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QStringList allKeys() const const
void beginGroup(QAnyStringView prefix)
void endGroup()
void remove(QAnyStringView key)
void setValue(QAnyStringView key, const QVariant &value)
QVariant value(QAnyStringView key) const const
void clear()
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QString toString() const const
QStringList toStringList() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 24 2024 12:02:24 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.