aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-01 12:35:15 -0700
committerAdam Soltys <[email protected]>2026-10-01 12:35:15 -0700
commitff6786509c639c33babc979b8cf28b6338af00ba (patch)
tree81193180c3066444b078037311655faf064d5f1d
parentc1b737f8246816d04db540d275aa489ecde7e13e (diff)
downloadcoinosv3-server-ff6786509c639c33babc979b8cf28b6338af00ba.tar.gz
coinosv3-server-ff6786509c639c33babc979b8cf28b6338af00ba.zip
Zaps and reactions get their own notification switches.
The notifier now watches kind-7 reactions p-tagged at the user (opt-in by an explicit flag, so older apps that can't word them stay quiet) and sends zap receipts as 'zap' to apps that report the category, 'payment' to older ones. Coinos zap receipts carry their amount into the push. The service worker names the reactor when it knows them and stays quiet for a muted one. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VP5Ljqk7EYcm5QQAbzDwog
-rw-r--r--nwcpush/server.js30
1 files changed, 24 insertions, 6 deletions
diff --git a/nwcpush/server.js b/nwcpush/server.js
index d4eb6a5..4110f91 100644
--- a/nwcpush/server.js
+++ b/nwcpush/server.js
@@ -138,8 +138,8 @@ function resubscribeNotify() {
if (!ptags.length && !authors.length) return;
const filters = [];
// DMs / direct invites / zap receipts land p-tagged at the user
- // ...as do replies and mentions (kind 1 naming the user)
- if (ptags.length) filters.push({ kinds: [1059, 9735, 9737, 1], '#p': ptags, since: Math.floor(Date.now() / 1000) });
+ // ...as do replies and mentions (kind 1 naming the user) and reactions (7)
+ if (ptags.length) filters.push({ kinds: [1059, 9735, 9737, 1, 7], '#p': ptags, since: Math.floor(Date.now() / 1000) });
// community chat: wraps authored by a channel's derived stream key
if (authors.length) filters.push({ kinds: [1059], authors, since: Math.floor(Date.now() / 1000) });
// one sub per filter — the pool API takes a single filter object
@@ -152,7 +152,7 @@ function resubscribeNotify() {
// DMs are throttled barely at all now: the device drops the ones it decides
// are noise (a stranger, or our own sent-copy), and a server-side cooldown
// would let a discarded push swallow the friend's message that followed it.
-const COOLDOWN = { payment: 10_000, dm: 1_000, chat: 90_000, mention: 5_000 };
+const COOLDOWN = { payment: 10_000, zap: 10_000, dm: 1_000, chat: 90_000, mention: 5_000, reaction: 30_000 };
const lastPush = new Map(); // endpointId:reason -> ts
const seenNotifyEvents = new Map(); // event id -> ts
function onNotifyEvent(ev) {
@@ -177,7 +177,20 @@ function onNotifyEvent(ev) {
}
return;
}
- const reason = ev.kind === 9735 || ev.kind === 9737 ? 'payment'
+ // A like or emoji on the user's post. Opt-in by an explicit flag: an app
+ // too old to send one has no words for it and would call it chat.
+ if (ev.kind === 7) {
+ const ps = (ev.tags || []).filter((t) => t[0] === 'p' && t[1]).map((t) => t[1]);
+ const extra = { from: ev.pubkey, emoji: String(ev.content || '+').slice(0, 16) };
+ for (const [id, r] of Object.entries(notifyRegs)) {
+ const mine = r.ptags || [];
+ if (r.reasons?.reaction !== true || mine.includes(ev.pubkey)) continue;
+ if (ps.some((x) => mine.includes(x))) pushNotify(id, r, 'reaction', extra).catch(() => {});
+ }
+ return;
+ }
+ const zap = ev.kind === 9735 || ev.kind === 9737;
+ const reason = zap ? 'payment'
: p && notifyPtags().includes(p) ? 'dm' : 'chat';
// A wrapped DM hides its sender from us by design, so we can't decide here
// whether it's from a friend or a stranger — we hand the whole wrap to the
@@ -185,11 +198,16 @@ function onNotifyEvent(ev) {
// around 4KB after encryption; anything larger goes as a bare nudge and the
// device shows its generic notification.
const extra = reason === 'dm' && JSON.stringify(ev).length < 3500 ? { wrap: ev } : {};
+ // a coinos zap receipt says how much in the clear
+ const amt = ev.kind === 9737 ? parseInt(ev.tags?.find((t) => t[0] === 'amount')?.[1], 10) : 0;
+ if (amt > 0) extra.amountSat = amt;
for (const [id, r] of Object.entries(notifyRegs)) {
const hit = reason === 'chat'
? (r.authors || []).includes(ev.pubkey)
: p && (r.ptags || []).includes(p);
- if (hit) pushNotify(id, r, reason, extra).catch(() => {});
+ // a zap is its own category for apps that know one; older apps keep
+ // getting it as a payment
+ if (hit) pushNotify(id, r, zap && typeof r.reasons?.zap === 'boolean' ? 'zap' : reason, extra).catch(() => {});
}
}
@@ -438,7 +456,7 @@ const server = Bun.serve({
const id = Bun.hash(sub_.endpoint).toString(36);
// per-category opt-outs; absent means everything on (older clients)
const reasons = {};
- for (const k of ['payment', 'dm', 'chat', 'mention']) {
+ for (const k of ['payment', 'zap', 'dm', 'chat', 'mention', 'reaction']) {
if (typeof body.notify.reasons?.[k] === 'boolean') reasons[k] = body.notify.reasons[k];
}
notifyRegs[id] = {