diff --git a/frontend/src/composables/useNostr.ts b/frontend/src/composables/useNostr.ts index 2c6bce7..878dcd0 100644 --- a/frontend/src/composables/useNostr.ts +++ b/frontend/src/composables/useNostr.ts @@ -397,21 +397,46 @@ export function useNostr() { updateWebhook, getStoredNsec, logout, + fetchNostrProfile, } } +export interface NostrProfile { + displayName: string | null + about: string | null + picture: string | null + banner: string | null + nip05: string | null +} + +// Profile cache with 5-min TTL +const profileCache = new Map() +const PROFILE_TTL = 5 * 60 * 1000 + +const RELAYS = [ + 'wss://relay.damus.io', + 'wss://relay.nostr.band', + 'wss://nos.lol', +] + // Fetch profile picture from a Nostr relay async function fetchNostrProfilePic(pk: string): Promise { - const relays = [ - 'wss://relay.damus.io', - 'wss://relay.nostr.band', - 'wss://nos.lol', - ] + const profile = await fetchNostrProfile(pk) + return profile?.picture || null +} - for (const relay of relays) { +/** Fetch full Nostr profile (kind:0 metadata) with caching */ +async function fetchNostrProfile(pk: string): Promise { + const cached = profileCache.get(pk) + if (cached && Date.now() - cached.ts < PROFILE_TTL) return cached.data + + for (const relay of RELAYS) { try { - const pic = await queryRelay(relay, pk) - if (pic) return pic + const profile = await queryRelayProfile(relay, pk) + if (profile) { + profileCache.set(pk, { data: profile, ts: Date.now() }) + return profile + } } catch { continue } @@ -419,7 +444,7 @@ async function fetchNostrProfilePic(pk: string): Promise { return null } -function queryRelay(url: string, pk: string): Promise { +function queryRelayProfile(url: string, pk: string): Promise { return new Promise((resolve) => { let timedOut = false const timeout = setTimeout(() => { @@ -433,7 +458,6 @@ function queryRelay(url: string, pk: string): Promise { ws.onopen = () => { if (timedOut) { ws.close(); return } - // Request kind 0 (metadata) for this pubkey ws.send(JSON.stringify(['REQ', subId, { kinds: [0], authors: [pk], limit: 1 }])) } @@ -444,7 +468,13 @@ function queryRelay(url: string, pk: string): Promise { const meta = JSON.parse(data[2].content) clearTimeout(timeout) ws.close() - resolve(meta.picture || null) + resolve({ + displayName: meta.display_name || meta.name || null, + about: meta.about || null, + picture: meta.picture || null, + banner: meta.banner || null, + nip05: meta.nip05 || null, + }) } else if (data[0] === 'EOSE') { clearTimeout(timeout) ws.close() diff --git a/frontend/src/pages/BotProfilePage.vue b/frontend/src/pages/BotProfilePage.vue index 6a3848f..5d34168 100644 --- a/frontend/src/pages/BotProfilePage.vue +++ b/frontend/src/pages/BotProfilePage.vue @@ -1,7 +1,7 @@