Compare commits
No commits in common. "bab2895848ed9ca21fad8abc11a4040071449c66" and "ec9d361a13f71c9fdca194e564ad8470ac2faa9d" have entirely different histories.
bab2895848
...
ec9d361a13
|
|
@ -1,146 +0,0 @@
|
||||||
import { get } from 'svelte/store';
|
|
||||||
import { lastValueFrom, timeout, toArray, EMPTY, tap } from 'rxjs';
|
|
||||||
import { catchError } from 'rxjs/operators';
|
|
||||||
import type { NostrEvent } from 'applesauce-core/helpers/event';
|
|
||||||
import type { Filter as ApplesauceFilter } from 'applesauce-core/helpers/filter';
|
|
||||||
import { pool } from './pool';
|
|
||||||
import { readRelays } from '$lib/stores/readRelays';
|
|
||||||
import { AUTHOR_PUBKEY_HEX, RELAY_HARD_TIMEOUT_MS } from './config';
|
|
||||||
|
|
||||||
/** Re-export als sprechenden Alias */
|
|
||||||
export type { NostrEvent };
|
|
||||||
|
|
||||||
/** Profile-Content (kind:0) */
|
|
||||||
export interface Profile {
|
|
||||||
name?: string;
|
|
||||||
display_name?: string;
|
|
||||||
picture?: string;
|
|
||||||
banner?: string;
|
|
||||||
about?: string;
|
|
||||||
website?: string;
|
|
||||||
nip05?: string;
|
|
||||||
lud16?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Filter = ApplesauceFilter;
|
|
||||||
|
|
||||||
interface CollectOpts {
|
|
||||||
onEvent?: (ev: NostrEvent) => void;
|
|
||||||
hardTimeoutMs?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Startet eine Request-Subscription und sammelt alle gelieferten Events
|
|
||||||
* bis EOSE (pool.request completes nach EOSE) oder Hard-Timeout.
|
|
||||||
*/
|
|
||||||
async function collectEvents(
|
|
||||||
relays: string[],
|
|
||||||
filter: Filter,
|
|
||||||
opts: CollectOpts = {}
|
|
||||||
): Promise<NostrEvent[]> {
|
|
||||||
const events = await lastValueFrom(
|
|
||||||
pool.request(relays, filter).pipe(
|
|
||||||
tap((ev: NostrEvent) => opts.onEvent?.(ev)),
|
|
||||||
timeout(opts.hardTimeoutMs ?? RELAY_HARD_TIMEOUT_MS),
|
|
||||||
toArray(),
|
|
||||||
catchError(() => EMPTY)
|
|
||||||
),
|
|
||||||
{ defaultValue: [] as NostrEvent[] }
|
|
||||||
);
|
|
||||||
return events;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Dedup per d-Tag: neueste (created_at) wins */
|
|
||||||
function dedupByDtag(events: NostrEvent[]): NostrEvent[] {
|
|
||||||
const byDtag = new Map<string, NostrEvent>();
|
|
||||||
for (const ev of events) {
|
|
||||||
const d = ev.tags.find((t) => t[0] === 'd')?.[1];
|
|
||||||
if (!d) continue;
|
|
||||||
const existing = byDtag.get(d);
|
|
||||||
if (!existing || ev.created_at > existing.created_at) {
|
|
||||||
byDtag.set(d, ev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [...byDtag.values()];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Alle kind:30023-Posts des Autors, neueste zuerst */
|
|
||||||
export async function loadPostList(
|
|
||||||
onEvent?: (ev: NostrEvent) => void
|
|
||||||
): Promise<NostrEvent[]> {
|
|
||||||
const relays = get(readRelays);
|
|
||||||
const events = await collectEvents(
|
|
||||||
relays,
|
|
||||||
{ kinds: [30023], authors: [AUTHOR_PUBKEY_HEX], limit: 200 },
|
|
||||||
{ onEvent }
|
|
||||||
);
|
|
||||||
const deduped = dedupByDtag(events);
|
|
||||||
return deduped.sort((a, b) => {
|
|
||||||
const ap = parseInt(
|
|
||||||
a.tags.find((t) => t[0] === 'published_at')?.[1] ?? `${a.created_at}`,
|
|
||||||
10
|
|
||||||
);
|
|
||||||
const bp = parseInt(
|
|
||||||
b.tags.find((t) => t[0] === 'published_at')?.[1] ?? `${b.created_at}`,
|
|
||||||
10
|
|
||||||
);
|
|
||||||
return bp - ap;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Einzelpost per d-Tag */
|
|
||||||
export async function loadPost(dtag: string): Promise<NostrEvent | null> {
|
|
||||||
const relays = get(readRelays);
|
|
||||||
const events = await collectEvents(relays, {
|
|
||||||
kinds: [30023],
|
|
||||||
authors: [AUTHOR_PUBKEY_HEX],
|
|
||||||
'#d': [dtag],
|
|
||||||
limit: 1
|
|
||||||
});
|
|
||||||
if (events.length === 0) return null;
|
|
||||||
return events.reduce((best, cur) =>
|
|
||||||
cur.created_at > best.created_at ? cur : best
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Profil-Event kind:0 (neueste Version) */
|
|
||||||
export async function loadProfile(): Promise<Profile | null> {
|
|
||||||
const relays = get(readRelays);
|
|
||||||
const events = await collectEvents(relays, {
|
|
||||||
kinds: [0],
|
|
||||||
authors: [AUTHOR_PUBKEY_HEX],
|
|
||||||
limit: 1
|
|
||||||
});
|
|
||||||
if (events.length === 0) return null;
|
|
||||||
const latest = events.reduce((best, cur) =>
|
|
||||||
cur.created_at > best.created_at ? cur : best
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
return JSON.parse(latest.content) as Profile;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Post-Adresse im `a`-Tag-Format: "30023:<pubkey>:<dtag>" */
|
|
||||||
function eventAddress(pubkey: string, dtag: string): string {
|
|
||||||
return `30023:${pubkey}:${dtag}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alle kind:1-Replies auf einen Post, chronologisch aufsteigend (älteste zuerst).
|
|
||||||
* Streamt via onEvent, wenn angegeben.
|
|
||||||
*/
|
|
||||||
export async function loadReplies(
|
|
||||||
dtag: string,
|
|
||||||
onEvent?: (ev: NostrEvent) => void
|
|
||||||
): Promise<NostrEvent[]> {
|
|
||||||
const relays = get(readRelays);
|
|
||||||
const address = eventAddress(AUTHOR_PUBKEY_HEX, dtag);
|
|
||||||
const events = await collectEvents(
|
|
||||||
relays,
|
|
||||||
{ kinds: [1], '#a': [address], limit: 500 },
|
|
||||||
{ onEvent }
|
|
||||||
);
|
|
||||||
return events.sort((a, b) => a.created_at - b.created_at);
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { RelayPool } from 'applesauce-relay';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Singleton-Pool für alle Nostr-Requests der SPA.
|
|
||||||
* applesauce-relay verwaltet Reconnects, Subscriptions, deduping intern.
|
|
||||||
*/
|
|
||||||
export const pool = new RelayPool();
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
import { lastValueFrom, timeout, toArray, EMPTY } from 'rxjs';
|
|
||||||
import { catchError } from 'rxjs/operators';
|
|
||||||
import type { NostrEvent } from 'applesauce-core/helpers/event';
|
|
||||||
import { pool } from './pool';
|
|
||||||
import {
|
|
||||||
AUTHOR_PUBKEY_HEX,
|
|
||||||
BOOTSTRAP_RELAY,
|
|
||||||
FALLBACK_READ_RELAYS,
|
|
||||||
RELAY_TIMEOUT_MS
|
|
||||||
} from './config';
|
|
||||||
|
|
||||||
export interface OutboxRelay {
|
|
||||||
url: string;
|
|
||||||
/** true = zum Lesen zu nutzen (kein dritter Tag-Wert oder "read") */
|
|
||||||
read: boolean;
|
|
||||||
/** true = zum Schreiben zu nutzen (kein dritter Tag-Wert oder "write") */
|
|
||||||
write: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lädt die NIP-65-Relay-Liste (kind:10002) des Autors vom Bootstrap-Relay.
|
|
||||||
* Fallback auf FALLBACK_READ_RELAYS, wenn das Event nicht innerhalb von
|
|
||||||
* RELAY_TIMEOUT_MS gefunden wird.
|
|
||||||
*
|
|
||||||
* Interpretation des dritten Tag-Werts:
|
|
||||||
* - nicht gesetzt → read + write
|
|
||||||
* - "read" → nur read
|
|
||||||
* - "write" → nur write
|
|
||||||
*/
|
|
||||||
export async function loadOutboxRelays(): Promise<OutboxRelay[]> {
|
|
||||||
const event = await firstEvent();
|
|
||||||
|
|
||||||
if (!event) {
|
|
||||||
return FALLBACK_READ_RELAYS.map((url) => ({ url, read: true, write: true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const relays: OutboxRelay[] = [];
|
|
||||||
for (const tag of event.tags) {
|
|
||||||
if (tag[0] !== 'r' || !tag[1]) continue;
|
|
||||||
const mode = tag[2];
|
|
||||||
relays.push({
|
|
||||||
url: tag[1],
|
|
||||||
read: mode !== 'write',
|
|
||||||
write: mode !== 'read'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (relays.length === 0) {
|
|
||||||
return FALLBACK_READ_RELAYS.map((url) => ({ url, read: true, write: true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return relays;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Nur die Read-URLs aus OutboxRelay[] */
|
|
||||||
export function readUrls(relays: OutboxRelay[]): string[] {
|
|
||||||
return relays.filter((r) => r.read).map((r) => r.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Nur die Write-URLs aus OutboxRelay[] */
|
|
||||||
export function writeUrls(relays: OutboxRelay[]): string[] {
|
|
||||||
return relays.filter((r) => r.write).map((r) => r.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Internes --------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragt das neueste kind:10002-Event vom Bootstrap-Relay ab.
|
|
||||||
* Sammelt alle Events bis EOSE (`pool.request(...)` emittiert nur Events
|
|
||||||
* und completes bei EOSE), nimmt das neueste, oder null falls keines.
|
|
||||||
*/
|
|
||||||
async function firstEvent(): Promise<NostrEvent | null> {
|
|
||||||
try {
|
|
||||||
const events = await lastValueFrom(
|
|
||||||
pool
|
|
||||||
.request([BOOTSTRAP_RELAY], {
|
|
||||||
kinds: [10002],
|
|
||||||
authors: [AUTHOR_PUBKEY_HEX],
|
|
||||||
limit: 1
|
|
||||||
})
|
|
||||||
.pipe(
|
|
||||||
timeout(RELAY_TIMEOUT_MS),
|
|
||||||
toArray(),
|
|
||||||
catchError(() => EMPTY)
|
|
||||||
),
|
|
||||||
{ defaultValue: [] as NostrEvent[] }
|
|
||||||
);
|
|
||||||
if (events.length === 0) return null;
|
|
||||||
return events.reduce((best, cur) =>
|
|
||||||
cur.created_at > best.created_at ? cur : best
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { writable, type Readable } from 'svelte/store';
|
|
||||||
import { loadOutboxRelays, readUrls } from '$lib/nostr/relays';
|
|
||||||
import { FALLBACK_READ_RELAYS } from '$lib/nostr/config';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store mit der aktuellen Read-Relay-Liste.
|
|
||||||
* Initial = FALLBACK_READ_RELAYS, damit die SPA sofort abfragen kann;
|
|
||||||
* sobald loadOutboxRelays() fertig ist, wird der Store aktualisiert.
|
|
||||||
*
|
|
||||||
* Singleton-Initialisierung: bootstrapReadRelays() wird genau einmal beim ersten
|
|
||||||
* Import aufgerufen.
|
|
||||||
*/
|
|
||||||
const store = writable<string[]>([...FALLBACK_READ_RELAYS]);
|
|
||||||
let bootstrapped = false;
|
|
||||||
|
|
||||||
export function bootstrapReadRelays(): void {
|
|
||||||
if (bootstrapped) return;
|
|
||||||
bootstrapped = true;
|
|
||||||
loadOutboxRelays()
|
|
||||||
.then((relays) => {
|
|
||||||
const urls = readUrls(relays);
|
|
||||||
if (urls.length > 0) store.set(urls);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Store behält seinen initialen FALLBACK-Zustand
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const readRelays: Readable<string[]> = store;
|
|
||||||
Loading…
Reference in New Issue