diff --git a/snapshot/src/core/relays.ts b/snapshot/src/core/relays.ts new file mode 100644 index 0000000..348010d --- /dev/null +++ b/snapshot/src/core/relays.ts @@ -0,0 +1,99 @@ +import { Relay } from 'applesauce-relay' +import { firstValueFrom, timeout } from 'rxjs' +import type { SignedEvent } from './types.ts' + +export type RelayListLoader = ( + bootstrapRelay: string, + authorPubkey: string, +) => Promise + +export const FALLBACK_READ_RELAYS = [ + 'wss://relay.damus.io', + 'wss://nos.lol', + 'wss://relay.primal.net', + 'wss://relay.tchncs.de', + 'wss://relay.edufeed.org', +] + +export function extractReadRelays(kind10002: SignedEvent): string[] { + const out: string[] = [] + for (const tag of kind10002.tags) { + if (tag[0] !== 'r' || !tag[1]) continue + const marker = tag[2] + if (marker === 'write') continue + out.push(tag[1]) + } + return out +} + +export const defaultRelayListLoader: RelayListLoader = async (bootstrap, pubkey) => { + try { + const relay = new Relay(bootstrap) + const ev = await firstValueFrom( + relay.request({ kinds: [10002], authors: [pubkey], limit: 1 }) + .pipe(timeout({ first: 5_000 })), + ) + return ev as SignedEvent + } catch { + return undefined + } +} + +export async function loadReadRelays( + bootstrapRelay: string, + authorPubkey: string, + loader: RelayListLoader = defaultRelayListLoader, + fallback: string[] = FALLBACK_READ_RELAYS, +): Promise { + const ev = await loader(bootstrapRelay, authorPubkey) + if (!ev) return fallback + const list = extractReadRelays(ev) + return list.length > 0 ? list : fallback +} + +export interface FetchEventsResult { + events: SignedEvent[] + responded: string[] + queried: string[] +} + +export type EventFetcher = (relay: string, pubkey: string) => Promise + +export const defaultEventFetcher: EventFetcher = async (relay, pubkey) => { + const out: SignedEvent[] = [] + const r = new Relay(relay) + return await new Promise((resolve) => { + const sub = r.request({ kinds: [30023, 5], authors: [pubkey] }) + .pipe(timeout({ first: 10_000 })) + .subscribe({ + next: (ev) => out.push(ev as SignedEvent), + error: () => resolve(out), + complete: () => resolve(out), + }) + setTimeout(() => sub.unsubscribe(), 11_000) + }) +} + +export async function fetchEvents( + relays: string[], + authorPubkey: string, + fetcher: EventFetcher = defaultEventFetcher, +): Promise { + const results = await Promise.all( + relays.map(async (url) => { + try { + const events = await fetcher(url, authorPubkey) + return { url, ok: true as const, events } + } catch { + return { url, ok: false as const, events: [] as SignedEvent[] } + } + }), + ) + const events: SignedEvent[] = [] + for (const r of results) events.push(...r.events) + return { + events, + responded: results.filter((r) => r.ok).map((r) => r.url), + queried: relays, + } +} diff --git a/snapshot/tests/relays.test.ts b/snapshot/tests/relays.test.ts new file mode 100644 index 0000000..771ef33 --- /dev/null +++ b/snapshot/tests/relays.test.ts @@ -0,0 +1,33 @@ +import { assertEquals } from '@std/assert' +import { extractReadRelays, type RelayListLoader, loadReadRelays } from '../src/core/relays.ts' +import type { SignedEvent } from '../src/core/types.ts' + +const KIND_10002: SignedEvent = { + id: 'r', pubkey: 'P', created_at: 1, kind: 10002, sig: 's', content: '', + tags: [ + ['r', 'wss://relay.damus.io'], + ['r', 'wss://nos.lol', 'read'], + ['r', 'wss://relay.write-only.example', 'write'], + ], +} + +Deno.test('extractReadRelays: ohne marker = read+write, "read" = read, "write" = nicht', () => { + assertEquals(extractReadRelays(KIND_10002), [ + 'wss://relay.damus.io', + 'wss://nos.lol', + ]) +}) + +Deno.test('loadReadRelays: nutzt fallback wenn kein kind:10002', async () => { + const loader: RelayListLoader = async () => undefined + const relays = await loadReadRelays('wss://bootstrap', 'P', loader, [ + 'wss://fallback1', 'wss://fallback2', + ]) + assertEquals(relays, ['wss://fallback1', 'wss://fallback2']) +}) + +Deno.test('loadReadRelays: nutzt kind:10002 wenn vorhanden', async () => { + const loader: RelayListLoader = async () => KIND_10002 + const relays = await loadReadRelays('wss://bootstrap', 'P', loader, ['wss://fallback']) + assertEquals(relays, ['wss://relay.damus.io', 'wss://nos.lol']) +})