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.
 
 
 
 
 

266 lines
6.8 KiB

/**
* Unified user data fetcher
* Consolidates profile, status, and relay list fetching
*/
import { nostrClient } from './nostr/nostr-client.js';
import { relayManager } from './nostr/relay-manager.js';
import { cacheProfile, getProfile, getProfiles } from './cache/profile-cache.js';
import { config } from './nostr/config.js';
import type { NostrEvent } from '../types/nostr.js';
// Re-export profile types and functions
export interface ProfileData {
name?: string;
about?: string;
picture?: string;
website?: string[];
nip05?: string[];
lud16?: string[];
}
/**
* Parse profile from kind 0 event
*/
export function parseProfile(event: NostrEvent): ProfileData {
const profile: ProfileData = {};
// Try to parse from tags first (preferred)
const nameTag = event.tags.find((t) => t[0] === 'name');
if (nameTag && nameTag[1]) profile.name = nameTag[1];
const aboutTag = event.tags.find((t) => t[0] === 'about');
if (aboutTag && aboutTag[1]) profile.about = aboutTag[1];
const pictureTag = event.tags.find((t) => t[0] === 'picture');
if (pictureTag && pictureTag[1]) profile.picture = pictureTag[1];
// Multiple tags for website, nip05, lud16
profile.website = event.tags.filter((t) => t[0] === 'website').map((t) => t[1]).filter(Boolean);
profile.nip05 = event.tags.filter((t) => t[0] === 'nip05').map((t) => t[1]).filter(Boolean);
profile.lud16 = event.tags.filter((t) => t[0] === 'lud16').map((t) => t[1]).filter(Boolean);
// Fallback to JSON content if tags not found
if (!profile.name || !profile.about) {
try {
const json = JSON.parse(event.content);
if (json.name && !profile.name) profile.name = json.name;
if (json.about && !profile.about) profile.about = json.about;
if (json.picture && !profile.picture) profile.picture = json.picture;
if (json.website && profile.website.length === 0) {
profile.website = Array.isArray(json.website) ? json.website : [json.website];
}
if (json.nip05 && profile.nip05.length === 0) {
profile.nip05 = Array.isArray(json.nip05) ? json.nip05 : [json.nip05];
}
if (json.lud16 && profile.lud16.length === 0) {
profile.lud16 = Array.isArray(json.lud16) ? json.lud16 : [json.lud16];
}
} catch {
// Invalid JSON, ignore
}
}
return profile;
}
/**
* Fetch profile for a pubkey
*/
export async function fetchProfile(
pubkey: string,
relays?: string[]
): Promise<ProfileData | null> {
// Try cache first
const cached = await getProfile(pubkey);
if (cached) {
return parseProfile(cached.event);
}
// Fetch from relays
const relayList = relays || [
...config.defaultRelays,
...config.profileRelays
];
const events = await nostrClient.fetchEvents(
[{ kinds: [0], authors: [pubkey], limit: 1 }],
relayList,
{ useCache: true, cacheResults: true }
);
if (events.length === 0) return null;
const event = events[0];
await cacheProfile(event);
return parseProfile(event);
}
/**
* Fetch multiple profiles
*/
export async function fetchProfiles(
pubkeys: string[],
relays?: string[]
): Promise<Map<string, ProfileData>> {
const profiles = new Map<string, ProfileData>();
// Check cache first
const cached = await getProfiles(pubkeys);
for (const [pubkey, cachedProfile] of cached.entries()) {
profiles.set(pubkey, parseProfile(cachedProfile.event));
}
// Fetch missing profiles
const missing = pubkeys.filter((p) => !profiles.has(p));
if (missing.length === 0) return profiles;
const relayList = relays || [
...config.defaultRelays,
...config.profileRelays
];
const events = await nostrClient.fetchEvents(
[{ kinds: [0], authors: missing, limit: 1 }],
relayList,
{ useCache: true, cacheResults: true }
);
for (const event of events) {
await cacheProfile(event);
profiles.set(event.pubkey, parseProfile(event));
}
return profiles;
}
/**
* Parse user status from kind 30315 event
*/
export function parseUserStatus(event: NostrEvent): string | null {
if (event.kind !== 30315) return null;
// Check for d tag with value "general"
const dTag = event.tags.find((t) => t[0] === 'd' && t[1] === 'general');
if (!dTag) return null;
return event.content || null;
}
/**
* Fetch user status for a pubkey
*/
export async function fetchUserStatus(
pubkey: string,
relays?: string[]
): Promise<string | null> {
const relayList = relays || [
...config.defaultRelays,
...config.profileRelays
];
const events = await nostrClient.fetchEvents(
[
{
kinds: [30315],
authors: [pubkey],
'#d': ['general'],
limit: 1
} as any // NIP-38 uses #d tag for parameterized replaceable events
],
relayList,
{ useCache: true, cacheResults: true }
);
if (events.length === 0) return null;
return parseUserStatus(events[0]);
}
export interface RelayInfo {
url: string;
read: boolean;
write: boolean;
}
/**
* Parse relay list from event
*/
export function parseRelayList(event: NostrEvent): RelayInfo[] {
const relays: RelayInfo[] = [];
for (const tag of event.tags) {
if (tag[0] === 'r' && tag[1]) {
const url = tag[1];
const markers = tag.slice(2);
// If no markers, relay is both read and write
if (markers.length === 0) {
relays.push({ url, read: true, write: true });
continue;
}
// Check for explicit markers
const hasRead = markers.includes('read');
const hasWrite = markers.includes('write');
// If only 'read' marker: read=true, write=false
// If only 'write' marker: read=false, write=true
// If both or neither explicitly: both true (default behavior)
const read = hasRead || (!hasRead && !hasWrite);
const write = hasWrite || (!hasRead && !hasWrite);
relays.push({ url, read, write });
}
}
return relays;
}
/**
* Fetch relay lists for a pubkey (kind 10002 and 10432)
*/
export async function fetchRelayLists(
pubkey: string,
relays?: string[]
): Promise<{
inbox: string[];
outbox: string[];
}> {
const relayList = relays || [
...config.defaultRelays,
...config.profileRelays
];
// Fetch both kind 10002 and 10432
const events = await nostrClient.fetchEvents(
[
{ kinds: [10002], authors: [pubkey], limit: 1 },
{ kinds: [10432], authors: [pubkey], limit: 1 }
],
relayList,
{ useCache: true, cacheResults: true }
);
const inbox: string[] = [];
const outbox: string[] = [];
for (const event of events) {
const relayInfos = parseRelayList(event);
for (const info of relayInfos) {
if (info.read && !inbox.includes(info.url)) {
inbox.push(info.url);
}
if (info.write && !outbox.includes(info.url)) {
outbox.push(info.url);
}
}
}
// Deduplicate
return {
inbox: [...new Set(inbox)],
outbox: [...new Set(outbox)]
};
}