aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-18 23:32:31 -0700
committerAdam Soltys <[email protected]>2026-10-18 23:32:31 -0700
commita99d37ca24e6484d8c2edd2ec7ac7dcaf4f29809 (patch)
treee984d45a06a73fecfa2e70f5a6329e51e8ac94be
parent7367343cb45d08bcc0f5e3246c8c5462ea204281 (diff)
downloadcoinosv3-a99d37ca24e6484d8c2edd2ec7ac7dcaf4f29809.tar.gz
coinosv3-a99d37ca24e6484d8c2edd2ec7ac7dcaf4f29809.zip
DMs and calls go to relay.coinos.io and relay.primal.net (both take gift wraps and pass call messages on live), not nos.lol, which refused or ignored the household IP's publishes. A relay that turns a call message away is left out of the next ones on this device: a day for one that won't take the kind (nip17.com: only kind 1059), ten minutes for a rate limit or ban. publishOn tells its caller which relay refused and why (onrefused).
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
-rw-r--r--src/features/messages.js23
-rw-r--r--src/nostr.js9
2 files changed, 28 insertions, 4 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index bfdabd1..695cba2 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -63,7 +63,10 @@ import { COMMUNITY, EPOCH } from '../community.js';
import COMMUNITY_DIRECTORY from '../community-directory.json';
import { assetUrl } from '../asset-url.js';
import { mergeFeeds } from '../feed-sync.js';
-const DM_RELAYS = ['wss://relay.coinos.io', 'wss://nos.lol'];
+// coinos's own relay and primal: both take DM gift wraps and pass call
+// messages (ephemeral 21059) on live. nos.lol was here until 2026-10-19 — it
+// refused or ignored the household IP's publishes, call answers among them.
+const DM_RELAYS = ['wss://relay.coinos.io', 'wss://relay.primal.net'];
const NOTIFIER = 'https://nwcpush.coinos.io';
const APP_BASE = 'https://v3.coinos.io';
const CACHE_MAX = 50; // messages kept per channel / per DM thread in feature state
@@ -9257,6 +9260,21 @@ export function messagesFeature(ctx) {
};
let calls = null;
const callInbox = new Map(); // peer -> their inbox relays, for this session
+ // Relays that turned a call message away are left out of the next ones: a
+ // relay that won't take the kind (nip17.com: "only kind 1059") for a day,
+ // one rate-limiting or banning us for ten minutes. Kept on this device.
+ const CALL_SHUN_KEY = 'coinos-call-shun';
+ let callShun = (() => { try { return JSON.parse(localStorage.getItem(CALL_SHUN_KEY) || '{}'); } catch { return {}; } })(); // relay -> until
+ function callRefused(url, msg) {
+ const kindRule = /only kind|not (accepted|allowed)|blocked|unsupported|restricted|kind \d+/i.test(msg) && !/rate|too many|banned|slow down/i.test(msg);
+ callShun[url] = Date.now() + (kindRule ? 86400_000 : 600_000);
+ try { localStorage.setItem(CALL_SHUN_KEY, JSON.stringify(callShun)); } catch {}
+ }
+ function callRelaysOf(relays) {
+ const now = Date.now();
+ const ok = relays.filter((r) => !(callShun[r] > now));
+ return ok.length ? ok : relays; // all of them shunned: try them anyway
+ }
async function inboxFor(pk) {
if (callInbox.has(pk)) return callInbox.get(pk);
const p = (isMe(pk) ? Promise.resolve(ownInbox) : fetchInboxRelays(pk).catch(() => []))
@@ -9313,7 +9331,8 @@ export function messagesFeature(ctx) {
const tries = signed.kind === CALL.ICE ? 1 : 4;
for (let i = 0; i < tries; i++) {
if (i) await new Promise((r) => setTimeout(r, 1500 * 2 ** (i - 1)));
- if (await publishOn(await inboxFor(pk), wrapCall(signed, pk), { onauth: relayAuth })) return;
+ const relays = callRelaysOf(await inboxFor(pk));
+ if (await publishOn(relays, wrapCall(signed, pk), { onauth: relayAuth, onrefused: callRefused })) return;
}
throw new Error('not delivered');
},
diff --git a/src/nostr.js b/src/nostr.js
index 8dcab01..c77fc27 100644
--- a/src/nostr.js
+++ b/src/nostr.js
@@ -498,7 +498,7 @@ export async function queryStreamOn(relays, filter, onEvent, maxWait = 1500) {
});
} catch {} finally { await Promise.all(pending); }
}
-export async function publishOn(relays, evt, { onauth = null } = {}) {
+export async function publishOn(relays, evt, { onauth = null, onrefused = null } = {}) {
try {
const live = liveRelays(relays);
// A rejected publish is a relay refusing the event (policy, rate limit,
@@ -520,7 +520,12 @@ export async function publishOn(relays, evt, { onauth = null } = {}) {
// "relay connection errored/closed" means the socket never came up —
// that's a sick relay, not a rejected event.
if (/connect|closed|errored|timeout|network/i.test(msg)) markRelaySick(live[i], msg);
- else console.warn(`nostr: publish kind ${evt.kind} refused by ${live[i]}:`, msg);
+ else {
+ console.warn(`nostr: publish kind ${evt.kind} refused by ${live[i]}:`, msg);
+ // the relay said no to this event (policy, a rate limit, a ban): a
+ // caller can stop asking it
+ if (onrefused) try { onrefused(live[i], msg); } catch {}
+ }
return false;
}));
// The first acceptance IS delivery — one durable copy is enough, and