Adds RETIRED_RELAY_HOSTS (goskatespots.nostr1.com, wotr.relatr.xyz) with isRetiredRelay/pruneRetiredRelays. Applied in resolveRelays (runtime), AppProvider (one-time cleanup on load) and NostrSync (when applying a fetched NIP-65), so existing users stop showing/using the retired relay without manual edits.
106 lines
3.7 KiB
TypeScript
106 lines
3.7 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useAppContext } from '@/hooks/useAppContext';
|
|
import type { RelayMetadata } from '@/contexts/AppContext';
|
|
|
|
/** Host (no scheme) for display, falling back to the raw value. */
|
|
export function relayHost(url: string): string {
|
|
try {
|
|
return new URL(url).host;
|
|
} catch {
|
|
return url;
|
|
}
|
|
}
|
|
|
|
/** Normalise user input into a `ws(s)://` relay URL, or null if invalid. */
|
|
export function normalizeRelayUrl(input: string): string | null {
|
|
const raw = input.trim();
|
|
if (!raw) return null;
|
|
try {
|
|
const url = new URL(/^wss?:\/\//i.test(raw) ? raw : `wss://${raw}`);
|
|
if (url.protocol === 'http:') url.protocol = 'ws:';
|
|
if (url.protocol === 'https:') url.protocol = 'wss:';
|
|
if (url.protocol !== 'ws:' && url.protocol !== 'wss:') return null;
|
|
if (!url.host) return null;
|
|
return url.toString().replace(/\/$/, '');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Hosts that are retired or dead and are pruned from every relay list. */
|
|
export const RETIRED_RELAY_HOSTS = new Set([
|
|
'goskatespots.nostr1.com', // legacy hosted relay — retired 2026-10-12
|
|
'wotr.relatr.xyz', // dead (does not resolve)
|
|
]);
|
|
|
|
/** True when `url` points at a retired/dead relay host. */
|
|
export function isRetiredRelay(url: string): boolean {
|
|
try {
|
|
return RETIRED_RELAY_HOSTS.has(new URL(url).host.toLowerCase());
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove retired/dead relays from a stored RelayMetadata. Returns a new object
|
|
* only when something actually changed (so callers can skip a no-op write), else
|
|
* null.
|
|
*/
|
|
export function pruneRetiredRelays(metadata: RelayMetadata): RelayMetadata | null {
|
|
const relays = metadata.relays.filter((r) => !isRetiredRelay(r.url));
|
|
const local = metadata.localRelays?.filter((u) => !isRetiredRelay(u));
|
|
const dm = metadata.dmRelays?.filter((u) => !isRetiredRelay(u));
|
|
const changed =
|
|
relays.length !== metadata.relays.length ||
|
|
(local?.length ?? 0) !== (metadata.localRelays?.length ?? 0) ||
|
|
(dm?.length ?? 0) !== (metadata.dmRelays?.length ?? 0);
|
|
if (!changed) return null;
|
|
return {
|
|
...metadata,
|
|
relays,
|
|
...(metadata.localRelays ? { localRelays: local } : {}),
|
|
...(metadata.dmRelays ? { dmRelays: dm } : {}),
|
|
};
|
|
}
|
|
|
|
export interface ResolvedRelays {
|
|
/** NIP-65 relays with blocked ones removed (read/write flags kept). */
|
|
configured: { url: string; read: boolean; write: boolean }[];
|
|
/** App-local relays (blocked ones removed). */
|
|
local: string[];
|
|
/** The blocklist, lower-cased. */
|
|
blocked: string[];
|
|
/** URLs to read from: local first, then read-enabled configured. */
|
|
read: string[];
|
|
/** URLs to write to: write-enabled configured, then local. */
|
|
write: string[];
|
|
}
|
|
|
|
/**
|
|
* Resolve the effective relay sets from config: blocked relays are removed
|
|
* everywhere, and local relays are added to both reads and writes.
|
|
*/
|
|
export function resolveRelays(metadata: RelayMetadata): ResolvedRelays {
|
|
const blocked = new Set((metadata.blockedRelays ?? []).map((u) => u.toLowerCase()));
|
|
const local = (metadata.localRelays ?? []).filter(
|
|
(u) => !blocked.has(u.toLowerCase()) && !isRetiredRelay(u),
|
|
);
|
|
const configured = metadata.relays.filter(
|
|
(r) => !blocked.has(r.url.toLowerCase()) && !isRetiredRelay(r.url),
|
|
);
|
|
|
|
return {
|
|
configured,
|
|
local,
|
|
blocked: [...blocked],
|
|
read: [...local, ...configured.filter((r) => r.read).map((r) => r.url)],
|
|
write: [...configured.filter((r) => r.write).map((r) => r.url), ...local],
|
|
};
|
|
}
|
|
|
|
/** React wrapper around {@link resolveRelays} for components/hooks. */
|
|
export function useRelays(): ResolvedRelays {
|
|
const { config } = useAppContext();
|
|
return useMemo(() => resolveRelays(config.relayMetadata), [config.relayMetadata]);
|
|
}
|