235ce394f90d | atagen | 2026-10-26 06:54 UTC | 1 | import { PUSH_CHANGED_MESSAGE, PUSH_CHANNEL, parsePush, parseResolved, shouldSilence, subscriptionKeys, type PushDisplay } from './lib/push-model'; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 2 | |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 3 | const RESOLVE_TIMEOUT_MS = 3000; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 4 | |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 5 | declare const __MALICE_DEVICE__: boolean; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 6 | |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 7 | interface BadgeNavigator { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 8 | setAppBadge?: (n: number) => Promise<void>; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 9 | clearAppBadge?: () => Promise<void>; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 10 | } |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 11 | |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 12 | let channel: BroadcastChannel | undefined; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 13 | let nextRequestId = 0; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 14 | const pending = new Map<string, (reply: { json?: unknown; error?: unknown }) => void>(); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 15 | |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 16 | function resolveChannel(): BroadcastChannel { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 17 | if (!channel) { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 18 | channel = new BroadcastChannel(PUSH_CHANNEL); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 19 | channel.onmessage = (ev: MessageEvent) => { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 20 | const msg = ev.data as { id?: unknown; json?: unknown; error?: unknown } | null; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 21 | if (typeof msg?.id !== 'string') return; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 22 | const done = pending.get(msg.id); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 23 | if (!done) return; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 24 | pending.delete(msg.id); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 25 | done(msg); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 26 | }; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 27 | } |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 28 | return channel; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 29 | } |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 30 | |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 31 | function requestResolve(roomId: string, eventId: string): Promise<unknown | null> { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 32 | const id = `${Date.now()}-${nextRequestId++}`; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 33 | return new Promise((resolve) => { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 34 | const timer = setTimeout(() => { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 35 | pending.delete(id); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 36 | resolve(null); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 37 | }, RESOLVE_TIMEOUT_MS); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 38 | pending.set(id, (reply) => { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 39 | clearTimeout(timer); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 40 | resolve(reply.error ? null : (reply.json ?? null)); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 41 | }); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 42 | resolveChannel().postMessage({ id, kind: 'resolve', room_id: roomId, event_id: eventId }); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 43 | }); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 44 | } |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 45 | |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 46 | function show(sw: ServiceWorkerGlobalScope, display: PushDisplay, title: string, body: string, silent: boolean): Promise<void> { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 47 | const options: NotificationOptions & { renotify?: boolean } = { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 48 | body, |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 49 | tag: display.tag, |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 50 | silent, |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 51 | renotify: display.mention && !silent, |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 52 | data: { roomId: display.roomId, navigate: display.navigate, eventId: display.eventId } |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 53 | }; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 54 | return sw.registration.showNotification(title, options); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 55 | } |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 56 | |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 57 | function setBadge(sw: ServiceWorkerGlobalScope, count: number | null): void { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 58 | if (count === null) return; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 59 | const nav = sw.navigator as unknown as BadgeNavigator; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 60 | const call = count > 0 ? nav.setAppBadge?.(count) : nav.clearAppBadge?.(); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 61 | void call?.catch(() => undefined); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 62 | } |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 63 | |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 64 | async function handle(sw: ServiceWorkerGlobalScope, event: PushEvent): Promise<void> { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 65 | const display = parsePush(event.data?.text() ?? null, sw.location.origin); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 66 | const windows = await sw.clients.matchAll({ type: 'window', includeUncontrolled: true }); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 67 | const focused = windows.some((client) => client.focused && client.visibilityState === 'visible'); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 68 | await show(sw, display, display.title, display.body, shouldSilence(display, focused)); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 69 | setBadge(sw, display.badge); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 70 | if (!__MALICE_DEVICE__ || windows.length === 0 || !display.roomId || !display.eventId) return; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 71 | const resolved = parseResolved(await requestResolve(display.roomId, display.eventId), display); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 72 | if (resolved) await show(sw, display, resolved.title, resolved.body, true); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 73 | } |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 74 | |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 75 | /** Browsers revoke the subscription when a push shows nothing, so every failure still shows a generic notification. */ |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 76 | export function onPush(sw: ServiceWorkerGlobalScope, event: PushEvent): void { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 77 | event.waitUntil( |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 78 | handle(sw, event).catch(() => |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 79 | sw.registration.showNotification('New message', { tag: 'malice', data: {} }).catch(() => undefined) |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 80 | ) |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 81 | ); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 82 | } |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 83 | |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 84 | export function onSubscriptionChange(sw: ServiceWorkerGlobalScope, event: Event): void { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 85 | const change = event as Event & { oldSubscription?: PushSubscription | null; newSubscription?: PushSubscription | null }; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 86 | (event as ExtendableEvent).waitUntil( |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 87 | (async () => { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 88 | let sub = change.newSubscription ?? (await sw.registration.pushManager.getSubscription()); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 89 | const key = change.oldSubscription?.options.applicationServerKey; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 90 | if (!sub && key) sub = await sw.registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key }); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 91 | const keys = sub ? subscriptionKeys(sub) : null; |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 92 | if (keys && !__MALICE_DEVICE__) { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 93 | await fetch('/api/push/subscribe', { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 94 | method: 'POST', |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 95 | credentials: 'same-origin', |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 96 | headers: { 'content-type': 'application/json' }, |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 97 | body: JSON.stringify({ endpoint: keys.endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth } }) |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 98 | }).catch(() => undefined); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 99 | } |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 100 | for (const client of await sw.clients.matchAll({ type: 'window', includeUncontrolled: true })) { |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 101 | client.postMessage({ type: PUSH_CHANGED_MESSAGE }); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 102 | } |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 103 | })().catch(() => undefined) |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 104 | ); |
235ce394f90d | atagen | 2026-10-26 06:54 UTC | 105 | } |