Changeset 96c714f in Klonkt
- Timestamp:
- 07/22/2026 07:12:11 AM (7 weeks ago)
- Branches:
- main
- Children:
- 9a00f28
- Parents:
- 8240e80
- git-author:
- Robin <roboburr@…> (07/22/2026 07:11:54 AM)
- git-committer:
- Robin <roboburr@…> (07/22/2026 07:12:11 AM)
- Files:
-
- 4 edited
-
.env.example (modified) (1 diff)
-
README.md (modified) (4 diffs)
-
src/services/PushService.js (modified) (2 diffs)
-
test/push.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
.env.example
r8240e80 r96c714f 15 15 # so a database dump alone stays useless). Or set your own: openssl rand -base64 32 16 16 PAID_SECRET= 17 # Web-push (notificaties) VAPID keys. Leave EMPTY to auto-generate on first use 18 # (saved to storage/.vapid). Do NOT rotate: new keys break every subscription. 19 VAPID_PUBLIC_KEY= 20 VAPID_PRIVATE_KEY= 21 VAPID_SUBJECT= 17 22 DATABASE_PATH=./storage/database.sqlite 18 23 MEDIA_PATH=./storage/media -
README.md
r8240e80 r96c714f 22 22 sites, Mastodon, PeerTube — any fediverse server) and their public posts appear in your 23 23 Circle. Decentralized, no central platform — built on ActivityPub. 24 - **Push notifications** — a browser/PWA notification for new followers, replies, 25 mentions, likes, boosts and private messages, even with the site closed. 26 Self-hosted Web Push (VAPID): payloads are encrypted end-to-end to your 27 browser, and private messages never carry their text in the push. 24 28 - **Themes & languages** — multiple palettes (light + dark), interface in EN/NL/DE. 25 29 - **Installable (PWA)**, **privacy-first** (self-hosted fonts, no tracking). … … 168 172 | `PUBLIC_BASE_URL` | ✅ | Canonical URL (e.g. `https://yourdomain.com`) | 169 173 | `PAID_SECRET` | auto | Encrypts the stored Patreon secrets for **paid posts**. Leave empty to auto-generate on first use (`storage/.paid-secret`), or set your own ≥16-char string. | 174 | `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` / `VAPID_SUBJECT` | auto | Identify this server to browser push services (**push notifications**). Leave empty to auto-generate on first use (`storage/.vapid`). | 170 175 | `SMTP_HOST` / `_PORT` / `_USER` / `_PASS` / `_FROM` | — | Email for password reset + newsletter | 171 176 | `KLONKT_DEFAULT_LANG` | — | Default language for visitors (`en`/`nl`/`de`) | … … 182 187 - `PAID_SECRET` → `storage/.paid-secret` (encrypts the stored Patreon secrets for 183 188 paid posts) 189 - `VAPID_*` → `storage/.vapid` (identifies this server to browser push services; 190 regenerating it would silently break every existing push subscription) 184 191 185 192 The paid-posts key lives **outside** the database on purpose: encrypting the … … 189 196 **Back up the whole `storage/` directory** (database, media *and* these key 190 197 files). Restoring the database without `storage/.paid-secret` leaves the stored 191 Patreon secrets unreadable — you'd have to reconnect Patreon. 198 Patreon secrets unreadable — you'd have to reconnect Patreon. Restoring without 199 `storage/.vapid` breaks push subscriptions — every device would have to re-enable 200 notifications. 192 201 193 202 ## Stack -
src/services/PushService.js
r8240e80 r96c714f 118 118 } 119 119 120 // Burst throttle: a wave of likes or a mass-follow must not become a wave of 121 // pushes. Per (user, type) at most one push per window; extras drop silently 122 // (the events themselves are still in Berichten — only the ping is deduped). 123 // In-memory is fine: one process, and a restart just means one extra ping. 124 const THROTTLE_SECONDS = { follow: 60, reply: 30, dm: 30, like: 300, boost: 300, test: 0 }; 125 const _lastPush = new Map(); 126 export function throttled(userId, type, nowSeconds = Math.floor(Date.now() / 1000)) { 127 const windowS = THROTTLE_SECONDS[type] ?? 60; 128 if (!windowS) return false; 129 const key = `${userId}:${type}`; 130 const prev = _lastPush.get(key) || 0; 131 if (nowSeconds - prev < windowS) return true; 132 _lastPush.set(key, nowSeconds); 133 return false; 134 } 135 120 136 // Notify one user on all their devices, honouring per-type preferences. 121 137 // type ∈ {follow, reply, like, boost, dm, test}. Fire-and-forget at call sites. 122 138 export async function notifyUser(userId, { type, title, body, url }) { 123 139 if (!(await pushReady())) return 0; 140 if (throttled(userId, type)) return 0; 124 141 const rows = db.prepare('SELECT * FROM push_subscriptions WHERE user_id = ?').all(userId); 125 142 let sent = 0; … … 143 160 144 161 export default { 145 publicKey, pushReady, DEFAULT_ALERTS, 162 publicKey, pushReady, DEFAULT_ALERTS, throttled, 146 163 saveSubscription, deleteSubscription, listSubscriptions, updateAlerts, 147 164 notifyUser, notifySite, -
test/push.test.js
r8240e80 r96c714f 65 65 }); 66 66 67 test('burst throttle: one ping per window per (user,type); test type never throttles', () => { 68 assert.equal(Push.throttled('tu1', 'like', 1000), false); // first passes 69 assert.equal(Push.throttled('tu1', 'like', 1100), true); // within 300s window 70 assert.equal(Push.throttled('tu1', 'like', 1301), false); // window elapsed 71 assert.equal(Push.throttled('tu1', 'boost', 1000), false); // other type independent 72 assert.equal(Push.throttled('tu2', 'like', 1000), false); // other user independent 73 assert.equal(Push.throttled('tu1', 'test', 1000), false); // test bypasses 74 assert.equal(Push.throttled('tu1', 'test', 1001), false); 75 }); 76 67 77 test('incomplete subscription payloads are refused', () => { 68 78 assert.equal(Push.saveSubscription({ endpoint: '', userId: 'u1', p256dh: 'x', auth: 'y' }), false);
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)