service/NtfyPushReceiver.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <QPointer> | ||
| 4 | #include <QStringList> | ||
| 5 | #include <QTimer> | ||
| 6 | #include <functional> | ||
| 7 | |||
| 8 | #include "PushReceiver.h" | ||
| 9 | |||
| 10 | class QNetworkAccessManager; | ||
| 11 | class QNetworkReply; | ||
| 12 | |||
| 13 | // Sprint 96 S96.2.1 / ADR-96-1: ntfy receive channel over the long-lived | ||
| 14 | // JSON stream GET <server>/<channel>/json (NDJSON). Nextcloud publishes to | ||
| 15 | // <server>/<channel>?up=1, so binary Web Push bodies arrive base64-encoded. | ||
| 16 | // | ||
| 17 | // Contract: | ||
| 18 | // - Bounded framing: at most kMaxFrameBytes per line and kMaxBufferBytes | ||
| 19 | // buffered; an overflow drops the stream (gap → owner catches up). | ||
| 20 | // - "open"/"keepalive" update the receive state only; "message" carries a | ||
| 21 | // payload; "poll_request" and unknown events are ignored. | ||
| 22 | // - No message within the heartbeat deadline (ntfy sends keepalives every | ||
| 23 | // 45 s by default) renews the stream; the stream has no total timeout. | ||
| 24 | // - Reconnect backoff 2, 4, 8 … s up to 5 min with earlier-only jitter, | ||
| 25 | // reset after a stable connection; 401/403 and TLS failures wait for the | ||
| 26 | // long backoff instead of retrying aggressively; 429 honours Retry-After. | ||
| 27 | // - TLS is always verified; the DAV credentials never reach the provider. | ||
| 28 | class NtfyPushReceiver final : public PushReceiver { | ||
| 29 | Q_OBJECT | ||
| 30 | public: | ||
| 31 | static constexpr int kMaxFrameBytes = 64 * 1024; | ||
| 32 | static constexpr int kMaxBufferBytes = 256 * 1024; | ||
| 33 | |||
| 34 | struct Timing { | ||
| 35 | int connectTimeoutMs = 30000; | ||
| 36 | int heartbeatTimeoutMs = 90000; | ||
| 37 | int stableAfterMs = 60000; // a connection this old resets the backoff | ||
| 38 | int backoffBaseMs = 2000; | ||
| 39 | int backoffMaxMs = 300000; | ||
| 40 | int failureBackoffMs = 300000; // auth/TLS: no aggressive retries | ||
| 41 | }; | ||
| 42 | |||
| 43 | explicit NtfyPushReceiver(QObject *parent = nullptr); | ||
| 44 | ~NtfyPushReceiver() override; | ||
| 45 | |||
| 46 | void start(const Config &config) override; | ||
| 47 | void stop() override; | ||
| 48 | QString endpoint() const override; | ||
| 49 | |||
| 50 | // Test seams: injected network access manager (ownership taken), timing | ||
| 51 | // and a jitter source returning a fraction in [0, 1). | ||
| 52 | void setNetworkAccessManager(QNetworkAccessManager *nam); | ||
| 53 | 11 | void setTiming(const Timing &timing) { m_timing = timing; } | |
| 54 | 12 | void setJitter(std::function<double()> jitter) { m_jitter = std::move(jitter); } | |
| 55 | int attempt() const { return m_attempt; } | ||
| 56 | |||
| 57 | private: | ||
| 58 | void connectStream(); | ||
| 59 | void onReadyRead(); | ||
| 60 | void onFinished(); | ||
| 61 | void processLine(const QByteArray &line); | ||
| 62 | void scheduleReconnect(Failure failure, const QString &detail, int minimumDelayMs = 0, | ||
| 63 | bool needsUser = false); | ||
| 64 | void dropStream(); | ||
| 65 | int backoffDelayMs() const; | ||
| 66 | |||
| 67 | QNetworkAccessManager *m_nam = nullptr; | ||
| 68 | QPointer<QNetworkReply> m_reply; | ||
| 69 | Config m_config; | ||
| 70 | Timing m_timing; | ||
| 71 | std::function<double()> m_jitter; | ||
| 72 | QByteArray m_buffer; | ||
| 73 | QTimer m_reconnectTimer; | ||
| 74 | QTimer m_heartbeatTimer; | ||
| 75 | QTimer m_connectTimer; | ||
| 76 | int m_attempt = 0; | ||
| 77 | bool m_running = false; | ||
| 78 | bool m_opened = false; | ||
| 79 | bool m_hadConnection = false; | ||
| 80 | QStringList m_recentIds; // bounded duplicate filter | ||
| 81 | }; | ||
| 82 |