diff options
Diffstat (limited to 'lib/notifications.ts')
| -rw-r--r-- | lib/notifications.ts | 74 |
1 files changed, 57 insertions, 17 deletions
diff --git a/lib/notifications.ts b/lib/notifications.ts index d5b4f73f..fe245c61 100644 --- a/lib/notifications.ts +++ b/lib/notifications.ts @@ -4,11 +4,11 @@ import ln from "$lib/ln"; import { err, l, warn } from "$lib/logging"; import { mail, templates } from "$lib/mail"; import mqtt from "$lib/mqtt"; -import { publish, serverSecret } from "$lib/nostr"; +import { encryptPayload, publish, serverSecret2 } from "$lib/nostr"; import { emit } from "$lib/sockets"; import { f, fiat, fmt, getUser, link, nada, t } from "$lib/utils"; import { hexToBytes } from "@noble/hashes/utils"; -import { finalizeEvent, nip04 } from "nostr-tools"; +import { finalizeEvent } from "nostr-tools"; import webpush from "web-push"; if (config.vapid) { @@ -19,6 +19,22 @@ if (config.vapid) { ); } + +// LNURL-pay invoices carry the raw metadata array as their memo when the +// payer left no comment, e.g. [["text/plain","Paying [email protected]"],...]. +// Receipts should show just the human-readable text. +const printerMemo = (memo) => { + if (typeof memo !== "string" || !memo.startsWith("[")) return memo; + try { + const meta = JSON.parse(memo); + if (Array.isArray(meta)) { + const plain = meta.find((m) => Array.isArray(m) && m[0] === "text/plain"); + if (plain) return plain[1]; + } + } catch (e) {} + return memo; +}; + export const notify = async (p, user, withdrawal) => { emit(user.id, "payment", p); let { username } = user; @@ -74,7 +90,8 @@ export const notify = async (p, user, withdrawal) => { if (!mqtt.connected) await mqtt.reconnect(); mqtt.publish( username, - `pay:${p.amount}:${p.tip}:${p.rate}:${p.created}:${p.id}:${p.memo}:${p.items}`, + `pay:${p.amount}:${p.tip}:${p.rate}:${p.created}:${p.id}:${printerMemo(p.memo)}:${p.items}`, + { qos: 1 }, ); } }; @@ -84,11 +101,23 @@ export const nwcNotify = async (p) => { const user = await getUser(p.uid); const pubkeys = await db.sMembers(`${user.id}:apps`); if (pubkeys.length) { - let payment_hash = ""; - if (p.type === "lightning") ({ payment_hash } = await ln.decode(p.hash)); + let payment_hash = p.payment_hash || ""; + if (!payment_hash && (p.type === "lightning" || p.type === "bolt12")) { + try { + const d = await ln.decode(p.hash); + // bolt11 decodes expose payment_hash, bolt12 invoices + // invoice_payment_hash + payment_hash = d.payment_hash || d.invoice_payment_hash || ""; + } catch (e) {} + } for (const pubkey of pubkeys) { - const { notify } = await g(`app:${pubkey}`); - if (!notify) continue; + // The app record may be missing/not-yet-written: a pubkey lands in + // `${uid}:apps` before its `app:<pubkey>` is persisted (creation race when + // adding an NWC connection), or a stale entry outlived its record. Guard + // the lookup — destructuring `notify` off null threw and, since the catch + // wraps the whole loop, aborted notifications for ALL the user's apps. + const app = await g(`app:${pubkey}`); + if (!app?.notify) continue; l("notifying", pubkey, p.type, p.amount); const notification = { @@ -108,22 +137,33 @@ export const nwcNotify = async (p) => { notification, }); - const content = await nip04.encrypt(serverSecret, pubkey, payload); + // NIP-47: a wallet supporting both schemes publishes each notification + // twice — kind 23196 nip04-encrypted, kind 23197 nip44-encrypted + for (const [scheme, kind] of [ + ["nip04", 23196], + ["nip44_v2", 23197], + ] as const) { + const content = await encryptPayload( + scheme, + serverSecret2, + pubkey, + payload, + ); - const unsigned = { - content, - tags: [["p", pubkey]], - kind: 23196, - created_at: Math.floor(Date.now() / 1000), - }; + const unsigned = { + content, + tags: [["p", pubkey]], + kind, + created_at: Math.floor(Date.now() / 1000), + }; - const event = finalizeEvent(unsigned, hexToBytes(serverSecret)); + const event = finalizeEvent(unsigned, hexToBytes(serverSecret2)); - publish(event).catch(nada); + publish(event).catch(nada); + } } } } catch (e) { - console.log(e); warn("nwc notification failed", e.message); } }; |
