diff options
| -rw-r--r-- | index.ts | 3 | ||||
| -rw-r--r-- | lib/mls.ts | 126 | ||||
| -rw-r--r-- | routes/nostr.ts | 9 |
3 files changed, 138 insertions, 0 deletions
@@ -23,6 +23,7 @@ import { sendHeartbeat, websocket } from "$lib/sockets"; import { initTigerBeetle } from "$lib/tb"; import { startZmq } from "$lib/zmq"; import { listenForDMs } from "$lib/dmNotifications"; +import { initMlsIndex } from "$lib/mls"; import ecash from "$routes/ecash"; import email from "$routes/email"; @@ -59,6 +60,7 @@ try { setTimeout(listenForLightning, 2000); listenForDMs(); +initMlsIndex(); setInterval(sendHeartbeat, 2000); app.get("/balances", info.balances); @@ -81,6 +83,7 @@ app.post("/sign", auth, invoices.sign); app.get("/assetlinks.json", (_c) => { return new Response(Bun.file("assetlinks.json")); }); +app.get("/mls/users", nostr.mlsUsers); app.get("/nostr.json", nostr.identities); app.get("/profile/:profile", nostr.profile); app.get("/:pubkey/count", nostr.count); diff --git a/lib/mls.ts b/lib/mls.ts new file mode 100644 index 00000000..8467b1ba --- /dev/null +++ b/lib/mls.ts @@ -0,0 +1,126 @@ +import config from "$config"; +import { db } from "$lib/db"; +import { l, warn } from "$lib/logging"; +import { getProfile } from "$lib/nostr"; +import { Relay } from "nostr-tools/relay"; +import type { Event } from "nostr-tools"; + +const MLS_KEY = "mls:users"; +const RELAYS = config.relays.filter((r) => r.startsWith("wss://")); + +interface MlsUser { + pubkey: string; + name?: string; + picture?: string; + nip05?: string; +} + +const knownPubkeys = new Set<string>(); + +async function resolveProfile(pubkey: string): Promise<MlsUser> { + try { + const profile = await getProfile(pubkey); + return { + pubkey, + name: profile.display_name || profile.name || undefined, + picture: profile.picture || undefined, + nip05: profile.nip05 || undefined, + }; + } catch { + return { pubkey }; + } +} + +async function addUser(pubkey: string): Promise<void> { + if (knownPubkeys.has(pubkey)) return; + knownPubkeys.add(pubkey); + const user = await resolveProfile(pubkey); + await db.hSet(MLS_KEY, pubkey, JSON.stringify(user)); +} + +async function backfill(): Promise<void> { + // Paginate back 3 weeks + const threeWeeksAgo = Math.floor(Date.now() / 1000) - 21 * 24 * 60 * 60; + let until: number | undefined; + + for (let pass = 0; pass < 50; pass++) { + const filter: any = { kinds: [443], limit: 500 }; + if (until) filter.until = until; + + let events: Event[] = []; + try { + events = ( + await Promise.all( + RELAYS.map( + (url) => + new Promise<Event[]>((resolve) => { + Relay.connect(url) + .then((r) => { + const found: Event[] = []; + r.subscribe([filter], { + onevent(e) { found.push(e); }, + oneose() { r.close(); resolve(found); }, + }); + }) + .catch(() => resolve([])); + }), + ), + ) + ).flat(); + } catch { + break; + } + + if (events.length === 0) break; + + const newPubkeys = [...new Set(events.map((e) => e.pubkey))].filter( + (pk) => !knownPubkeys.has(pk), + ); + + for (const pk of newPubkeys) { + await addUser(pk); + } + + const oldest = Math.min(...events.map((e) => e.created_at)); + if (oldest <= threeWeeksAgo) break; + until = oldest; + + l(`mls backfill pass ${pass}: ${knownPubkeys.size} users, oldest ${new Date(oldest * 1000).toISOString()}`); + } + + l(`mls backfill complete: ${knownPubkeys.size} users`); +} + +function subscribe(): void { + for (const url of RELAYS) { + Relay.connect(url) + .then((r) => { + r.subscribe([{ kinds: [443], since: Math.floor(Date.now() / 1000) }], { + onevent(e) { + addUser(e.pubkey).catch(() => {}); + }, + }); + }) + .catch((e) => warn(`mls subscribe failed: ${url}`, e.message)); + } +} + +export async function initMlsIndex(): Promise<void> { + // Load existing index into memory + try { + const existing = await db.hGetAll(MLS_KEY); + for (const pk of Object.keys(existing)) knownPubkeys.add(pk); + l(`mls index loaded: ${knownPubkeys.size} users`); + } catch {} + + // Subscribe for new key packages + subscribe(); + + // Backfill in background + backfill().catch((e) => warn("mls backfill error", e.message)); +} + +export async function getMlsUsers(): Promise<MlsUser[]> { + const data = await db.hGetAll(MLS_KEY); + return Object.values(data).map((v) => JSON.parse(v)); +} diff --git a/routes/nostr.ts b/routes/nostr.ts index 8e189f3d..995fd7f9 100644 --- a/routes/nostr.ts +++ b/routes/nostr.ts @@ -1,5 +1,6 @@ import { db, g } from "$lib/db"; import ln from "$lib/ln"; +import { getMlsUsers } from "$lib/mls"; import { EX, get, @@ -20,6 +21,14 @@ import { decode } from "nostr-tools/nip19"; import { getZapEndpoint, makeZapRequest } from "nostr-tools/nip57"; export default { + async mlsUsers(c) { + try { + return c.json(await getMlsUsers()); + } catch (e) { + return bail(c, e.message); + } + }, + async event(c) { try { let id = c.req.param("id"); |
