feat(iios): notification engine — presence-gated Web Push (engine phase)
Generic notification engine: reacts to message.sent and runs three gates before delivering — policy (DM always; group only on @mention or reply-to-you), presence (skip if the recipient is focused on that thread), mute (per-thread). Delivery via a swappable NotificationPort (Web Push/VAPID adapter); a 'gone' (404/410) prunes the sub. - PresenceService + gateway `focus_thread` signal + disconnect cleanup (room membership != viewing, since the sidebar joins every thread room). - IiosNotificationSubscription table + `muted` on IiosThreadParticipant (migration). - Endpoints: GET vapid-public-key, POST/DELETE subscribe, POST threads/:id/mute|unmute; listThreads returns the caller's `muted`. - DM-vs-group read from the opaque `membership` attribute in the notification POLICY only — kernel stays generic (grep-verified). Tests: 13 new (3 gates + reply-to-you + prune + web-push adapter states); full suite 205 green. Verified live: module boots, subscribe stored, mute/unmute round-trips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import webpush from 'web-push';
|
||||
import type { NotificationPayload, NotificationPort, PushSub } from './notification.port';
|
||||
|
||||
type SendFn = typeof webpush.sendNotification;
|
||||
export interface Vapid {
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
subject: string;
|
||||
}
|
||||
|
||||
/** Web Push (VAPID) delivery. A dead subscription (404/410) → 'gone' so the caller prunes it. */
|
||||
@Injectable()
|
||||
export class WebPushDelivery implements NotificationPort {
|
||||
private readonly vapid?: Vapid;
|
||||
|
||||
constructor(
|
||||
vapid?: Vapid,
|
||||
private readonly send: SendFn = webpush.sendNotification,
|
||||
) {
|
||||
// Store VAPID; pass it per-send (not global setVapidDetails) so construction never
|
||||
// validates keys — keeps the adapter unit-testable with a mocked transport.
|
||||
this.vapid = vapid?.publicKey ? vapid : undefined;
|
||||
}
|
||||
|
||||
async deliver(sub: PushSub, payload: NotificationPayload): Promise<'sent' | 'gone' | 'failed'> {
|
||||
if (!this.vapid) return 'failed'; // push disabled (no VAPID keys)
|
||||
try {
|
||||
await this.send(
|
||||
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
|
||||
JSON.stringify(payload),
|
||||
{ vapidDetails: this.vapid },
|
||||
);
|
||||
return 'sent';
|
||||
} catch (e) {
|
||||
const code = (e as { statusCode?: number }).statusCode;
|
||||
return code === 404 || code === 410 ? 'gone' : 'failed';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user