You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
143 lines
4.7 KiB
143 lines
4.7 KiB
/** |
|
* Fetch user profile (kind 0) and payment targets (kind 10133) from Nostr relays |
|
*/ |
|
|
|
import { SimplePool } from 'nostr-tools'; |
|
import { DEFAULT_NOSTR_RELAYS } from '../config.js'; |
|
|
|
/** |
|
* Fetch kind 0 profile event and kind 10133 payment targets from relays |
|
*/ |
|
export async function fetchProfileFromRelays(pubkey, relays = null) { |
|
try { |
|
const pool = new SimplePool(); |
|
const relayList = relays || DEFAULT_NOSTR_RELAYS; |
|
|
|
// Ensure pubkey is in hex format (getPublicKey returns hex string) |
|
const pubkeyHex = typeof pubkey === 'string' && pubkey.length === 64 |
|
? pubkey.toLowerCase() |
|
: pubkey; |
|
|
|
// Fetch kind 0 (profile) and kind 10133 (payment targets) in parallel |
|
const [profileEvents, paymentEvents] = await Promise.all([ |
|
pool.querySync(relayList, [ |
|
{ |
|
kinds: [0], // Kind 0 = profile metadata |
|
authors: [pubkeyHex], |
|
limit: 1 |
|
} |
|
]), |
|
pool.querySync(relayList, [ |
|
{ |
|
kinds: [10133], // Kind 10133 = payment targets (NIP-A3) |
|
authors: [pubkeyHex], |
|
limit: 1 |
|
} |
|
]) |
|
]); |
|
|
|
pool.close(relayList); |
|
|
|
const profile = {}; |
|
let profileEvent = null; |
|
let paymentTargets = []; |
|
|
|
// Process profile event (kind 0) |
|
if (profileEvents.length > 0) { |
|
profileEvent = profileEvents[0]; |
|
|
|
// Try to parse JSON content (old format) |
|
let profileData = {}; |
|
try { |
|
profileData = JSON.parse(profileEvent.content); |
|
} catch { |
|
// Invalid JSON, will use tags |
|
} |
|
|
|
// Extract from tags (new format) - prefer tags over JSON |
|
const nameTag = profileEvent.tags.find(t => t[0] === 'name' || t[0] === 'display_name')?.[1]; |
|
const aboutTag = profileEvent.tags.find(t => t[0] === 'about')?.[1]; |
|
const pictureTag = profileEvent.tags.find(t => t[0] === 'picture' || t[0] === 'avatar')?.[1]; |
|
|
|
profile.displayName = nameTag || profileData.display_name || profileData.name; |
|
profile.name = profileData.name; |
|
profile.about = aboutTag || profileData.about; |
|
profile.picture = pictureTag || profileData.picture; |
|
|
|
// Check tags for nip05 (newer format) |
|
const nip05Tag = profileEvent.tags.find((tag) => |
|
(tag[0] === 'nip05' || tag[0] === 'l') && tag[1] |
|
); |
|
if (nip05Tag && nip05Tag[1]) { |
|
profile.nip05 = nip05Tag[1]; |
|
} else if (profileData.nip05) { |
|
profile.nip05 = profileData.nip05; |
|
} |
|
} |
|
|
|
// Initialize lightning addresses set for collecting from multiple sources |
|
const lightningAddresses = new Set<string>(); |
|
|
|
// Extract lightning addresses from NIP-01 (lud16 tag or JSON) |
|
if (profileEvent) { |
|
// From tags (lud16) |
|
const lud16Tags = profileEvent.tags.filter(t => t[0] === 'lud16').map(t => t[1]).filter(Boolean); |
|
lud16Tags.forEach(addr => lightningAddresses.add(addr.toLowerCase())); |
|
|
|
// From JSON (lud16 field) |
|
try { |
|
const profileData = JSON.parse(profileEvent.content); |
|
if (profileData.lud16 && typeof profileData.lud16 === 'string') { |
|
lightningAddresses.add(profileData.lud16.toLowerCase()); |
|
} |
|
} catch { |
|
// Invalid JSON, ignore |
|
} |
|
} |
|
|
|
// Extract lightning addresses from kind 10133 |
|
if (paymentEvents.length > 0) { |
|
const paytoTags = paymentEvents[0].tags.filter(t => t[0] === 'payto' && t[1] === 'lightning' && t[2]); |
|
paytoTags.forEach(tag => { |
|
if (tag[2]) { |
|
lightningAddresses.add(tag[2].toLowerCase()); |
|
} |
|
}); |
|
} |
|
|
|
// Build payment targets array - start with lightning addresses |
|
paymentTargets = Array.from(lightningAddresses).map(authority => ({ |
|
type: 'lightning', |
|
authority, |
|
payto: `payto://lightning/${authority}` |
|
})); |
|
|
|
// Also include other payment types from kind 10133 |
|
if (paymentEvents.length > 0) { |
|
const otherPaytoTags = paymentEvents[0].tags.filter(t => t[0] === 'payto' && t[1] && t[1] !== 'lightning' && t[2]); |
|
otherPaytoTags.forEach(tag => { |
|
const type = tag[1]?.toLowerCase() || ''; |
|
const authority = tag[2] || ''; |
|
if (type && authority) { |
|
// Check if we already have this (for deduplication) |
|
const existing = paymentTargets.find(p => p.type === type && p.authority.toLowerCase() === authority.toLowerCase()); |
|
if (!existing) { |
|
paymentTargets.push({ |
|
type, |
|
authority, |
|
payto: `payto://${type}/${authority}` |
|
}); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
return { |
|
...profile, |
|
paymentTargets |
|
}; |
|
} catch (error) { |
|
console.warn('Failed to fetch profile from relays:', error); |
|
return null; |
|
} |
|
}
|
|
|