From ddb58e4ed753d214f34e7bed9ecc98768f3345f9 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 21 Feb 2026 20:35:00 +0100 Subject: [PATCH] Sync from gitrepublic-web monorepo - 2026-02-21 20:35:00 --- README.md | 3 + scripts/relay/profile-fetcher.js | 134 ++++++++++++++++++++++++------- 2 files changed, 110 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 146b150..09fbb6b 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,9 @@ gitrep-uninstall --keep-env - **Credential Helper**: Automatic NIP-98 authentication - **Commit Signing**: Automatically sign commits for GitRepublic repos - **API Access**: Full command-line access to all GitRepublic APIs +- **Profile Fetching**: Fetch user profiles with payment targets (NIP-A3 kind 10133) + - Automatically merges lightning addresses from NIP-01 (lud16) and kind 10133 + - Returns payment targets in `payto://` format ## Requirements diff --git a/scripts/relay/profile-fetcher.js b/scripts/relay/profile-fetcher.js index 4f45ed9..9d8902b 100644 --- a/scripts/relay/profile-fetcher.js +++ b/scripts/relay/profile-fetcher.js @@ -1,12 +1,12 @@ /** - * Fetch user profile (kind 0) from Nostr relays + * 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 from relays + * Fetch kind 0 profile event and kind 10133 payment targets from relays */ export async function fetchProfileFromRelays(pubkey, relays = null) { try { @@ -18,44 +18,124 @@ export async function fetchProfileFromRelays(pubkey, relays = null) { ? pubkey.toLowerCase() : pubkey; - const events = await pool.querySync(relayList, [ - { - kinds: [0], // Kind 0 = profile metadata - authors: [pubkeyHex], - limit: 1 - } + // 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); - if (events.length === 0) { - return null; - } - - const event = events[0]; const profile = {}; + let profileEvent = null; + let paymentTargets = []; - // Try to parse JSON content - try { - const content = JSON.parse(event.content); - profile.displayName = content.display_name || content.displayName; - profile.name = content.name; - profile.nip05 = content.nip05; - } catch { - // Invalid JSON, try tags - } - - // Check tags for nip05 (newer format) - if (!profile.nip05) { - const nip05Tag = event.tags.find((tag) => + // 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; } } - return profile; + // Initialize lightning addresses set for collecting from multiple sources + const lightningAddresses = new Set(); + + // 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;