KUnifiedPush

nextpushprovider.cpp
1/*
2 SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5
6#include "nextpushprovider.h"
7#include "client.h"
8#include "logging.h"
9#include "message.h"
10
11#include <QHostInfo>
12#include <QJsonDocument>
13#include <QJsonObject>
14#include <QNetworkReply>
15#include <QSettings>
16#include <QStandardPaths>
17
18using namespace Qt::Literals;
19using namespace KUnifiedPush;
20
21NextPushProvider::NextPushProvider(QObject *parent)
22 : AbstractPushProvider(Id, parent)
23{
24 connect(&m_sseStream, &ServerSentEventsStream::messageReceived, this, [this](const SSEMessage &sse) {
25 qCDebug(Log) << sse.event << sse.data << sse.metaData;
26 if (sse.event == "message") {
27 QJsonObject msgObj = QJsonDocument::fromJson(sse.data).object();
28 Message msg;
29 msg.messageId = QString::fromUtf8(sse.metaData.value("id"));
30 msg.clientRemoteId = msgObj.value("token"_L1).toString();
31 msg.content = QByteArray::fromBase64(msgObj.value("message"_L1).toString().toUtf8());
32 Q_EMIT messageReceived(msg);
33 }
34 if (sse.event == "start") {
35 Q_EMIT connected();
36 Q_EMIT urgencyChanged();
37 }
38 });
39}
40
41NextPushProvider::~NextPushProvider() = default;
42
44{
45 m_appPassword = settings.value(QStringLiteral("AppPassword"), QString()).toString();
46 m_url = QUrl(settings.value(QStringLiteral("Url"), QString()).toString());
47 m_userName = settings.value(QStringLiteral("Username"), QString()).toString();
48 m_deviceId = settings.value(QStringLiteral("DeviceId"), QString()).toString();
49 qCDebug(Log) << m_url << m_userName << m_deviceId;
50 return m_url.isValid() && !m_appPassword.isEmpty() && !m_userName.isEmpty();
51}
52
54{
55 settings.remove("DeviceId");
56}
57
59{
60 qCDebug(Log) << m_deviceId;
61
62 if (m_deviceId.isEmpty()) {
63 auto req = prepareRequest("device");
64 req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
65
66 QJsonObject content;
67 content.insert(QLatin1String("deviceName"), QHostInfo::localHostName());
68 auto reply = nam()->put(req, QJsonDocument(content).toJson(QJsonDocument::Compact));
69 connect(reply, &QNetworkReply::finished, this, [reply, urgency, this]() {
70 reply->deleteLater();
71 if (reply->error() != QNetworkReply::NoError) {
72 qCWarning(Log) << reply->errorString();
73 Q_EMIT disconnected(ProviderRejected, reply->errorString());
74 return;
75 }
76
77 const auto content = QJsonDocument::fromJson(reply->readAll()).object();
78 qCDebug(Log) << QJsonDocument(content).toJson(QJsonDocument::Compact);
79 // TODO check "success" field
80 m_deviceId = content.value(QLatin1String("deviceId")).toString();
81
82 QSettings settings;
83 settings.setValue(QStringLiteral("NextPush/DeviceId"), m_deviceId);
84 waitForMessage(urgency);
85 });
86 } else {
87 waitForMessage(urgency);
88 }
89}
90
92{
93 if (m_sseReply) {
94 m_sseReply->abort();
95 }
97}
98
100{
101 qCDebug(Log) << client.serviceName;
102 auto req = prepareRequest("app");
103 req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
104
105 QJsonObject content;
106 content.insert(QLatin1String("deviceId"), m_deviceId);
107 content.insert(QLatin1String("appName"), client.serviceName);
108 if (!client.vapidKey.isEmpty()) {
109 content.insert("vapid"_L1, client.vapidKey);
110 }
111 auto reply = nam()->put(req, QJsonDocument(content).toJson(QJsonDocument::Compact));
112 connect(reply, &QNetworkReply::finished, this, [reply, this, client]() {
113 reply->deleteLater();
114 if (reply->error() != QNetworkReply::NoError) {
115 Q_EMIT clientRegistered(client, TransientNetworkError, reply->errorString());
116 return;
117 }
118
119 const auto content = QJsonDocument::fromJson(reply->readAll()).object();
120 qCDebug(Log) << QJsonDocument(content).toJson(QJsonDocument::Compact);
121 if (!content.value(QLatin1String("success")).toBool()) {
122 Q_EMIT clientRegistered(client, ProviderRejected, QString()); // TODO do we get an error message in this case?
123 return;
124 }
125 auto newClient = client;
126 newClient.remoteId = content.value(QLatin1String("token")).toString();
127
128 QUrl endpointUrl = m_url;
129 auto path = endpointUrl.path();
130 path += QLatin1String("/index.php/apps/uppush/push/") + newClient.remoteId;
131 endpointUrl.setPath(path);
132 newClient.endpoint = endpointUrl.toString();
133
134 Q_EMIT clientRegistered(newClient);
135 });
136}
137
139{
140 qCDebug(Log) << client.serviceName << client.remoteId;
141 auto req = prepareRequest("app", client.remoteId);
142 auto reply = nam()->deleteResource(req);
143 connect(reply, &QNetworkReply::finished, this, [reply, this, client]() {
144 reply->deleteLater();
145 if (reply->error() != QNetworkReply::NoError) {
146 qCWarning(Log) << reply->errorString();
148 } else {
149 qCDebug(Log) << "application deleted";
151 }
152 });
153}
154
155void NextPushProvider::acknowledgeMessage(const Client &client, const QString &messageIdentifier)
156{
157 qCDebug(Log) << client.serviceName << messageIdentifier;
158 auto req = prepareRequest("message", messageIdentifier);
159 auto reply = nam()->deleteResource(req);
160 connect(reply, &QNetworkReply::finished, this, [reply, this, client, messageIdentifier]() {
161 reply->deleteLater();
162 if (reply->error() != QNetworkReply::NoError) {
163 qCWarning(Log) << reply->errorString() << reply->readAll();
164 };
165 Q_EMIT messageAcknowledged(client, messageIdentifier);
166 });
167}
168
170{
171 waitForMessage(urgency);
172}
173
174void NextPushProvider::waitForMessage(Urgency urgency)
175{
176 qCDebug(Log) << qToUnderlying(urgency);
177 if (m_sseReply) {
178 m_sseReply->abort();
179 }
180
181 // HACK For unknown reasons doing SSE requests on the same
182 // QNAM instance as the other HTTP operations here will
183 // prevent other requests from happening while an SSE connection is
184 // active and will also prevent an SSE connection from being
185 // re-established after a disconnect.
186 // This doesn't match the behavior in the Ntfy implementation nor
187 // the QNAM documentation. Using a new QNAM per SSE does avoid
188 // those problems.
189 auto nam = new QNetworkAccessManager(this);
193
194 auto req = prepareRequest("device", m_deviceId);
195 req.setRawHeader("urgency", urgencyValue(urgency));
196 auto reply = nam->get(req);
197 connect(reply, &QNetworkReply::finished, this, [reply, nam, this]() {
198 reply->deleteLater();
199 nam->deleteLater();
200 if (reply->error() == QNetworkReply::OperationCanceledError) {
201 return; // we triggered this ourselves
202 }
203
204 if (reply->error() != QNetworkReply::NoError) {
205 qCWarning(Log) << reply->errorString();
206 Q_EMIT disconnected(TransientNetworkError, reply->errorString());
207 } else {
208 qCDebug(Log) << "GET finished";
210 }
211 });
212 m_sseStream.read(reply);
213 m_sseReply = reply;
214 setUrgency(urgency);
215}
216
217QNetworkRequest NextPushProvider::prepareRequest(const char *restCmd, const QString &restArg) const
218{
219 QUrl url = m_url;
220 auto path = url.path();
221 path += QLatin1String("/index.php/apps/uppush/") + QLatin1String(restCmd) + QLatin1Char('/') + restArg;
222 url.setPath(path);
223
224 QNetworkRequest req(url);
225 req.setRawHeader("Authorization", "Basic " + QByteArray(m_userName.toUtf8() + ':' + m_appPassword.toUtf8()).toBase64());
226 return req;
227}
Base class for push provider protocol implementations.
Urgency urgency() const
The urgency level currently used by this provider.
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
void messageAcknowledged(const KUnifiedPush::Client &client, const QString &messageIdentifier)
Emitted after a message reception has been acknowledge to the push server.
Information about a registered client.
Definition client.h:20
A received push notification message.
Definition message.h:15
void connectToProvider(Urgency urgency) override
Attempt to establish a connection to the push provider.
void registerClient(const Client &client) override
Register a new client with the provider.
void disconnectFromProvider() override
Disconnect and existing connection to the push provider.
void doChangeUrgency(Urgency urgency) override
Re-implement if urgency leve changes are done as a separate command.
void unregisterClient(const Client &client) override
Unregister a client from the provider.
bool loadSettings(const QSettings &settings) override
Load connection settings.
void acknowledgeMessage(const Client &client, const QString &messageIdentifier) override
Acknowledge a message.
void resetSettings(QSettings &settings) override
Reset any internal state for a fresh setup connecting to a different push server instance.
QString path(const QString &relativePath)
Client-side integration with UnifiedPush.
Definition connector.h:14
int64_t Id
QByteArray fromBase64(const QByteArray &base64, Base64Options options)
T value(const Key &key) const const
QString localHostName()
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
QByteArray toJson(JsonFormat format) const const
iterator insert(QLatin1StringView key, const QJsonValue &value)
QJsonValue value(QLatin1StringView key) const const
QString toString() const const
void enableStrictTransportSecurityStore(bool enabled, const QString &storeDir)
QNetworkReply * get(const QNetworkRequest &request)
void setRedirectPolicy(QNetworkRequest::RedirectPolicy policy)
void setStrictTransportSecurityEnabled(bool enabled)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
void remove(QAnyStringView key)
void setValue(QAnyStringView key, const QVariant &value)
QVariant value(QAnyStringView key) const const
QString writableLocation(StandardLocation type)
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
QByteArray toUtf8() const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QString path(ComponentFormattingOptions options) const const
void setPath(const QString &path, ParsingMode mode)
QString toString(FormattingOptions options) const const
QString toString() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 25 2025 12:05:39 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.