feat: Nostr profile enrichment with caching
Fetch full kind:0 metadata (display_name, about, banner, nip05) from relays with a 5-min TTL cache. Show Nostr banner, display name, and NIP-05 on bot profile pages. Expose ownerPubkey in stats API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
7761f713d7
commit
d1fe4d6e83
@@ -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<string, { data: NostrProfile; ts: number }>()
|
||||
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<string | null> {
|
||||
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<NostrProfile | null> {
|
||||
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<string | null> {
|
||||
return null
|
||||
}
|
||||
|
||||
function queryRelay(url: string, pk: string): Promise<string | null> {
|
||||
function queryRelayProfile(url: string, pk: string): Promise<NostrProfile | null> {
|
||||
return new Promise((resolve) => {
|
||||
let timedOut = false
|
||||
const timeout = setTimeout(() => {
|
||||
@@ -433,7 +458,6 @@ function queryRelay(url: string, pk: string): Promise<string | null> {
|
||||
|
||||
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<string | null> {
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user