app/core/DavPushAccount.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <QElapsedTimer> | ||
| 4 | #include <QHash> | ||
| 5 | #include <QObject> | ||
| 6 | #include <QPointer> | ||
| 7 | #include <QSet> | ||
| 8 | #include <QTimer> | ||
| 9 | #include <functional> | ||
| 10 | #include <optional> | ||
| 11 | |||
| 12 | #include "CalendarAccountSync.h" | ||
| 13 | #include "DavPushSecrets.h" | ||
| 14 | #include "data/DavPushSettings.h" | ||
| 15 | #include "data/DavPushStore.h" | ||
| 16 | #include "service/DavPushClient.h" | ||
| 17 | #include "service/PushReceiver.h" | ||
| 18 | |||
| 19 | class DavPushStatistics; | ||
| 20 | |||
| 21 | // Sprint 96 S96.2.4: DAV-Push of one DAV account — its receiver (one push | ||
| 22 | // channel per account and installation), its key material and the | ||
| 23 | // registrations of its collections. The coordinator computes the desired | ||
| 24 | // state; apply() converges towards it. | ||
| 25 | // | ||
| 26 | // Lifecycle contract (SPRINT-96.md §3.3): | ||
| 27 | // - Endpoint and keys are prepared durably (keyring) before any POST; a | ||
| 28 | // lost local commit after a confirmed POST re-registers with the same | ||
| 29 | // endpoint (the server updates instead of duplicating). | ||
| 30 | // - After a successful registration a catch-up sync of that collection is | ||
| 31 | // requested (closes the gap since the last sync). | ||
| 32 | // - Renewal at 75 % of the confirmed lifetime with earlier-only jitter; | ||
| 33 | // failures back off (1 min doubling to 1 h; Retry-After honoured). | ||
| 34 | // - A replaced receiver (server change, lost secret) registers again and | ||
| 35 | // then removes the old registration; foreign registrations are never | ||
| 36 | // touched (only own Location URLs are ever deleted). | ||
| 37 | // - Every async callback is bound to the generation it was started in; | ||
| 38 | // stale answers overwrite neither state nor secrets. | ||
| 39 | class DavPushAccount final : public QObject { | ||
| 40 | 1 | Q_OBJECT | |
| 41 | public: | ||
| 42 | struct Context { | ||
| 43 | DavPushStore *store = nullptr; | ||
| 44 | DavPushStatistics *stats = nullptr; | ||
| 45 | DavPushSecretStore *secrets = nullptr; | ||
| 46 | QString installationId; | ||
| 47 | // provider: DavPushSettings::Receiver::provider ("ntfy", "unifiedpush"). | ||
| 48 | std::function<PushReceiver *(const QString &provider, QObject *parent)> makeReceiver; | ||
| 49 | // Ends the provider-side registration of a replaced receiver (UnifiedPush | ||
| 50 | // distributors keep it otherwise); no-op for ntfy. | ||
| 51 | std::function<void(const QString &provider, const QString &server, const QString &channel)> releaseReceiver; | ||
| 52 | std::function<DavPushClient *(const QString &serverUrl, const QString &username, const QString &password)> makeClient; | ||
| 53 | CalendarAccountSync::CredentialResolver resolveCredentials; | ||
| 54 | std::function<qint64()> now; // epoch seconds | ||
| 55 | // Read requests (account-wide when paths are empty). | ||
| 56 | std::function<void(const QString &accountId, const QStringList &paths, unsigned reasons)> requestSync; | ||
| 57 | // Committed RFC 6578 token of a collection (token no-op check). | ||
| 58 | std::function<QString(const QString &accountId, const QString &path)> storedToken; | ||
| 59 | // Known push failure (true) / healthy again (false) for the fallback. | ||
| 60 | std::function<void(const QString &accountId, bool failing)> pushHealth; | ||
| 61 | }; | ||
| 62 | |||
| 63 | struct Target { | ||
| 64 | CalendarSyncAccount account; | ||
| 65 | DavPushSettings::Receiver receiver; | ||
| 66 | QSet<QString> knownPaths; // selected collections of every kind (topic mapping) | ||
| 67 | QSet<QString> desiredPaths; // supported + automatic + selected | ||
| 68 | bool active = false; // push allowed now (online, enabled, …) | ||
| 69 | }; | ||
| 70 | |||
| 71 | DavPushAccount(const QString &accountId, Context context, QObject *parent = nullptr); | ||
| 72 | ~DavPushAccount() override; | ||
| 73 | |||
| 74 | void apply(const Target &target); | ||
| 75 | // Pause (offline, suspend, quit): stop the receiver, keep registrations. | ||
| 76 | void pause(); | ||
| 77 | // Account deleted: forget receiver, secrets and local registrations. | ||
| 78 | void forget(); | ||
| 79 | // "Reconnect push for this account": new stream, renew registrations. | ||
| 80 | void reconnect(); | ||
| 81 | // The provider token changed: reconnect the stream with the new token. | ||
| 82 | void reloadToken(); | ||
| 83 | |||
| 84 | QString accountId() const { return m_accountId; } | ||
| 85 | 2679 | const PushReceiver *receiver() const { return m_receiver; } | |
| 86 | 463 | bool registering(const QString &path) const { return m_inFlight.contains(path); } | |
| 87 | 419 | bool unregistering(const QString &path) const { return m_unregistering.contains(path); } | |
| 88 | 57 | QString receiverProblem() const { return m_receiverProblem; } // keyring/crypto | |
| 89 | qint64 nextRenewalAt() const { return m_nextRenewalAt; } | ||
| 90 | // Monotonic ms of the last accepted push per collection (latency). | ||
| 91 | std::optional<qint64> pendingPushSince(const QString &path) const; | ||
| 92 | 1 | void clearPendingPush(const QString &path) { m_pushSince.remove(path); } | |
| 93 | |||
| 94 | signals: | ||
| 95 | void changed(); | ||
| 96 | |||
| 97 | private: | ||
| 98 | void ensureReceiver(); | ||
| 99 | void createReceiver(); | ||
| 100 | void startReceiver(); | ||
| 101 | void stopReceiver(); | ||
| 102 | void converge(); | ||
| 103 | void withClient(std::function<void(DavPushClient *client)> work); | ||
| 104 | void registerPath(DavPushClient *client, const QString &path); | ||
| 105 | void onRegistered(const DavPushClient::Result &result, quint64 generation); | ||
| 106 | void unregisterUrl(DavPushClient *client, const QString &path, const QString &url, qint64 cleanupId); | ||
| 107 | void onMessage(const QByteArray &payload, const QString &messageId); | ||
| 108 | void onReceiverState(); | ||
| 109 | void scheduleRenewal(); | ||
| 110 | bool needsRegistration(const DavPushCollectionRecord &record) const; | ||
| 111 | // Hash of the receiver's current push endpoint ("" while unknown). | ||
| 112 | QString endpointHash() const; | ||
| 113 | void releaseCurrentReceiver(); | ||
| 114 | void recordError(DavPushCollectionRecord record, const QString &error, int retrySeconds); | ||
| 115 | QString collectionScope(const QString &path) const; | ||
| 116 | |||
| 117 | QString m_accountId; | ||
| 118 | Context m_ctx; | ||
| 119 | Target m_target; | ||
| 120 | quint64 m_generation = 0; | ||
| 121 | std::optional<DavPushReceiverRecord> m_receiverRecord; | ||
| 122 | std::optional<DavPushReceiverSecret> m_secret; | ||
| 123 | QByteArray m_providerToken; | ||
| 124 | bool m_streamSeen = false; // S106-082: the first stream of this process was seen | ||
| 125 | QSet<QString> m_caughtUpThisProcess; // registered (and caught up) in this process | ||
| 126 | bool m_secretLoading = false; | ||
| 127 | bool m_tokenLoaded = false; | ||
| 128 | QString m_receiverProblem; | ||
| 129 | QPointer<PushReceiver> m_receiver; | ||
| 130 | QSet<QString> m_inFlight; | ||
| 131 | QSet<QString> m_unregistering; | ||
| 132 | QSet<qint64> m_cleanupInFlight; | ||
| 133 | QHash<QString, QString> m_replaced; // path -> old registration URL to delete after success | ||
| 134 | QHash<QString, QString> m_registeringEndpoint; // path -> endpoint hash of the POST in flight | ||
| 135 | QTimer m_renewal; | ||
| 136 | qint64 m_nextRenewalAt = 0; | ||
| 137 | bool m_reportedFailure = false; | ||
| 138 | bool m_clientPending = false; | ||
| 139 | QList<std::function<void(DavPushClient *)>> m_clientWork; | ||
| 140 | QHash<QString, qint64> m_pushSince; | ||
| 141 | QElapsedTimer m_monotonic; | ||
| 142 | }; | ||
| 143 |