KUnifiedPush

ntfypushprovider.cpp
1/*
2 SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5
6#include "ntfypushprovider.h"
7#include "client.h"
8#include "logging.h"
9#include "message.h"
10
11#include <QJsonDocument>
12#include <QJsonObject>
13#include <QNetworkReply>
14#include <QSettings>
15#include <QUrlQuery>
16#include <QUuid>
17
18using namespace Qt::Literals;
19using namespace KUnifiedPush;
20
21NtfyPushProvider::NtfyPushProvider(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;
26 if (sse.event.isEmpty()) {
27 QJsonObject msgObj = QJsonDocument::fromJson(sse.data).object();
28 Message msg;
29 msg.clientRemoteId = msgObj.value("topic"_L1).toString();
30 msg.content = msgObj.value("message"_L1).toString().toUtf8();
31 if (msgObj.value("encoding"_L1).toString() == "base64"_L1) {
32 msg.content = QByteArray::fromBase64(msg.content);
33 }
34
35 const auto msgId = msgObj.value("id"_L1).toString();
36
37 // encrypted messages come in as attachments apparently, so resolve those here
38 if (const auto attachment = msgObj.value("attachment"_L1).toObject(); !attachment.isEmpty()) {
39 const auto attachmentUrl = attachment.value("url"_L1).toString();
40 if (!attachmentUrl.isEmpty()) {
41 auto reply = nam()->get(QNetworkRequest(QUrl(attachmentUrl)));
42 connect(reply, &QNetworkReply::finished, this, [this, reply, msgId, msg]() {
43 reply->deleteLater();
44 if (reply->error() != QNetworkReply::NoError) {
45 qCWarning(Log) << reply->errorString() << reply->readAll();
46 return;
47 }
48 Message m(msg);
49 m.content = reply->readAll();
50 m_lastMessageId = msgId;
51 Q_EMIT messageReceived(m);
52 storeState();
53 });
54 return;
55 }
56 }
57 m_lastMessageId = msgId;
58 Q_EMIT messageReceived(msg);
59 storeState();
60 } else if (sse.event == "open") {
61 Q_EMIT connected();
62 Q_EMIT urgencyChanged();
63 }
64 });
65}
66
67NtfyPushProvider::~NtfyPushProvider() = default;
68
70{
71 m_url = settings.value(QStringLiteral("Url"), QUrl()).toUrl();
72
73 QSettings internal;
74 internal.beginGroup(providerId() + "-internal"_L1);
75 m_topics = internal.value(QStringLiteral("Topics"), QStringList()).toStringList();
76 m_lastMessageId = internal.value(QStringLiteral("LastMessageId"), QString()).toString();
77
78 return m_url.isValid();
79}
80
81void NtfyPushProvider::resetSettings([[maybe_unused]] QSettings &settings)
82{
83 QSettings internal;
84 internal.remove(providerId() + "-internal"_L1);
85}
86
88{
89 doConnectToProvider(urgency);
90}
91
93{
94 if (m_sseReply) {
95 m_sseReply->abort();
96 }
98}
99
101{
102 // hardcoded constraints in ntfy:
103 // must start with "up" AND must be exactly 14 characters long (incl. the up prefix)
104 // if we violate this visitor rate limiting will not work and we get an HTTP 507 error
106 auto newClient = client;
107 newClient.remoteId = topic;
108
109 QUrl endpoint = m_url;
110 auto path = endpoint.path();
111 if (!path.endsWith(QLatin1Char('/'))) {
112 path += QLatin1Char('/');
113 }
114 path += topic;
115 endpoint.setPath(path);
116 newClient.endpoint = endpoint.toString();
117
118 m_topics.push_back(topic);
119 storeState();
120 doConnectToProvider(urgency());
121 Q_EMIT clientRegistered(newClient);
122}
123
125{
126 m_topics.removeAll(client.remoteId);
127 storeState();
128 doConnectToProvider(urgency());
130}
131
132#if 0
133// TODO see doConnectToProvider
134void NtfyPushProvider::doChangeUrgency(Urgency urgency)
135{
136 doConnectToProvider(urgency);
137}
138#endif
139
140void NtfyPushProvider::doConnectToProvider(Urgency urgency)
141{
142 if (m_sseReply) {
143 m_sseReply->abort();
144 }
145
146 if (m_topics.empty()) {
149 return;
150 }
151
152 QUrl url = m_url;
153 QString path = url.path();
154 path += QLatin1Char('/') + m_topics.join(QLatin1Char(',')) + QLatin1String("/sse");
155 url.setPath(path);
156 QUrlQuery query;
157 query.addQueryItem(QStringLiteral("up"), QStringLiteral("1"));
158 query.addQueryItem(QStringLiteral("since"), m_lastMessageId.isEmpty() ? QStringLiteral("all") : m_lastMessageId);
159 // TODO urgency filter
160 // ntfy's "priority" comes close to this, but first would need RFC 8030 compliant urgency support for incoming messages
161 // before we can add a corresponding filter here, otherwise Web Push message will not get correct urgency levels assigned
162 // and we'd miss high priority ones
163 url.setQuery(query);
164 qCDebug(Log) << url << qToUnderlying(urgency);
165
166 auto reply = nam()->get(QNetworkRequest(url));
167 connect(reply, &QNetworkReply::finished, this, [reply, this]() {
168 reply->deleteLater();
169 if (reply->error() == QNetworkReply::OperationCanceledError) {
170 return; // we triggered this ourselves
171 }
172
173 qCDebug(Log) << reply->error() << reply->errorString() << m_sseStream.buffer();
174 const auto obj = QJsonDocument::fromJson(m_sseStream.buffer()).object();
175 if (const auto errMsg = obj.value("error"_L1).toString(); !errMsg.isEmpty()) {
177 return;
178 }
179
180 Q_EMIT disconnected(TransientNetworkError, reply->errorString());
181 });
182
183 m_sseReply = reply;
184 m_sseStream.read(reply);
185 setUrgency(urgency);
186}
187
188void NtfyPushProvider::storeState()
189{
190 QSettings settings;
191 settings.beginGroup(providerId() + "-internal"_L1);
192 settings.setValue(QStringLiteral("Topics"), m_topics);
193 settings.setValue(QStringLiteral("LastMessageId"), m_lastMessageId);
194}
195
196#include "moc_ntfypushprovider.cpp"
Base class for push provider protocol implementations.
virtual void doChangeUrgency(Urgency urgency)
Re-implement if urgency leve changes are done as a separate command.
void connected()
Emitted after the connection to the push provider has been established successfully.
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 urgencyChanged()
Emitted when the urgency level change request has been executed.
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
QLatin1StringView providerId() const
Provider id used e.g.
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 resetSettings(QSettings &settings) override
Reset any internal state for a fresh setup connecting to a different push server instance.
bool loadSettings(const QSettings &settings) override
Load connection settings.
void unregisterClient(const Client &client) override
Unregister a client from the provider.
void disconnectFromProvider() override
Disconnect and existing connection to the push provider.
std::optional< QSqlQuery > query(const QString &queryStatement)
QString path(const QString &relativePath)
Client-side integration with UnifiedPush.
Definition connector.h:14
int64_t Id
QByteArray fromBase64(const QByteArray &base64, Base64Options options)
bool isEmpty() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
bool isEmpty() const const
QJsonValue value(QLatin1StringView key) const const
QJsonObject toObject() const const
QString toString() const const
QLatin1StringView left(qsizetype length) const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void beginGroup(QAnyStringView prefix)
void remove(QAnyStringView key)
void setValue(QAnyStringView key, const QVariant &value)
QVariant value(QAnyStringView key) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
void push_back(QChar ch)
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)
void setQuery(const QString &query, ParsingMode mode)
QString toString(FormattingOptions options) const const
QUuid createUuid()
QString toString() const const
QStringList toStringList() const const
QUrl toUrl() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 18 2025 12:16:55 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.