import {hash, maybe} from "@welshman/lib" import type {Filter} from "@welshman/util" import {Address, DELETE, makeEvent, makeHttpAuth, makeHttpAuthHeader} from "@welshman/util" import {Relays, User} from "@welshman/app" import {buildUrl} from "@lib/util" import {app, thunks} from "@app/core" import {device} from "@app/device" import {EMAIL_NOTIFICATION_SERVER, PUSH_BRIDGE} from "@app/env" import {emailNotificationState, syncRelaySubscriptions} from "@app/push/adapters/common" export class MailNotifications { _controller = maybe() async register(email: string, frequency: string) { const $user = User.require(app.get()) const $signer = $user.signer if (!$signer) throw new Error("No signer available") const authEvent = await $signer.sign(await makeHttpAuth(EMAIL_NOTIFICATION_SERVER, "POST")) const authHeader = makeHttpAuthHeader(authEvent) const res = await fetch(buildUrl(EMAIL_NOTIFICATION_SERVER, "subscription", "email"), { method: "POST", body: JSON.stringify({email, frequency, pubkey: $user.pubkey}), headers: {Authorization: authHeader, "Content-Type": "application/json"}, }) if (!res.ok) throw new Error(`Registration failed: ${await res.text()}`) const {key, callback} = await res.json() emailNotificationState.set({key, callback, email, frequency}) } async unregister() { const {key} = emailNotificationState.get() if (!key) return const $signer = app.get().user?.signer if (!$signer) return try { const authEvent = await $signer.sign(await makeHttpAuth(EMAIL_NOTIFICATION_SERVER, "DELETE")) const authHeader = makeHttpAuthHeader(authEvent) await fetch(buildUrl(EMAIL_NOTIFICATION_SERVER, "subscription", key), { method: "DELETE", headers: {Authorization: authHeader}, }) } catch { // Best effort } emailNotificationState.set({}) } async enable() { if (this._controller) return const {email, frequency} = emailNotificationState.get() if (!EMAIL_NOTIFICATION_SERVER) { console.warn("Email notification server not configured") return } this._controller = new AbortController() // Register if we don't have a subscription yet if (!emailNotificationState.get().callback && email && frequency) { await this.register(email, frequency) } // Sync kind 30390 subscriptions using the same filters as push syncRelaySubscriptions( this._controller.signal, async (url: string, key: string, filters: Filter[], ignore: Filter[]) => { const {callback} = emailNotificationState.get() if (!callback) return if (filters.length > 0) { await this._syncRelay(url, key, filters, ignore) } else { await this._unsyncRelay(url, key) } }, ) } async disable() { this._controller?.abort() this._controller = undefined await this.unregister() } _getSubscriptionIdentifier = (relay: string, key: string) => String(hash(relay + key + device.get() + "email")) _getPushUrl = async (url: string) => { for (const candidate of [url, PUSH_BRIDGE]) { const relay = await app.get().use(Relays).load(candidate) if (relay?.hasNip("9a")) { return candidate } } } _syncRelay = async (relay: string, key: string, filters: Filter[], ignore: Filter[] = []) => { const {callback} = emailNotificationState.get() if (!callback) return const url = await this._getPushUrl(relay) if (!url) { console.warn(`Failed to subscribe ${relay} to email notifications: unsupported`) return } const identifier = this._getSubscriptionIdentifier(relay, key) const thunk = thunks.get().publish({ relays: [url], event: makeEvent(30390, { tags: [ ["d", identifier], ["relay", relay], ["callback", callback], ...ignore.map(f => ["ignore", JSON.stringify(f)]), ...filters.map(f => ["filter", JSON.stringify(f)]), ], }), }) const error = await thunk.waitForError() if (error) { console.warn(`Failed to subscribe ${relay} to email notifications:`, error) } } _unsyncRelay = async (relay: string, key: string) => { const url = await this._getPushUrl(relay) if (!url) return const $pubkey = User.require(app.get()).pubkey const identifier = this._getSubscriptionIdentifier(relay, key) const address = new Address(30390, $pubkey, identifier).toString() const event = makeEvent(DELETE, {tags: [["a", address]]}) const error = await thunks .get() .publish({relays: [url], event}) .waitForError() if (error) { console.warn(`Failed to unsubscribe ${relay} from email notifications:`, error) } } } export const Mail = new MailNotifications()