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.
225 lines
6.6 KiB
225 lines
6.6 KiB
import { ndkInstance } from "../ndk.ts"; |
|
import { fetchEventWithFallback } from "./nostrUtils.ts"; |
|
import { nip19 } from "nostr-tools"; |
|
import type { NDKFilter } from "@nostr-dev-kit/ndk"; |
|
import { NDKEvent } from "@nostr-dev-kit/ndk"; |
|
import { get } from "svelte/store"; |
|
import { wellKnownUrl, isValidNip05Address } from "./search_utils.ts"; |
|
import { TIMEOUTS, VALIDATION } from "./search_constants.ts"; |
|
|
|
/** |
|
* Search for a single event by ID or filter |
|
*/ |
|
export async function searchEvent(query: string): Promise<NDKEvent | null> { |
|
// Clean the query and normalize to lowercase |
|
const cleanedQuery = query.replace(/^nostr:/, "").toLowerCase(); |
|
let filterOrId: NDKFilter | string = cleanedQuery; |
|
|
|
// If it's a valid hex string, try as event id first, then as pubkey (profile) |
|
if ( |
|
new RegExp(`^[a-f0-9]{${VALIDATION.HEX_LENGTH}}$`, "i").test(cleanedQuery) |
|
) { |
|
// Try as event id |
|
filterOrId = cleanedQuery; |
|
const eventResult = await fetchEventWithFallback( |
|
get(ndkInstance), |
|
filterOrId, |
|
TIMEOUTS.EVENT_FETCH, |
|
); |
|
// Always try as pubkey (profile event) as well |
|
const profileFilter = { kinds: [0], authors: [cleanedQuery] }; |
|
const profileEvent = await fetchEventWithFallback( |
|
get(ndkInstance), |
|
profileFilter, |
|
TIMEOUTS.EVENT_FETCH, |
|
); |
|
// Prefer profile if found and pubkey matches query |
|
if ( |
|
profileEvent && |
|
profileEvent.pubkey.toLowerCase() === cleanedQuery.toLowerCase() |
|
) { |
|
return profileEvent; |
|
} else if (eventResult) { |
|
return eventResult; |
|
} |
|
} else if ( |
|
new RegExp( |
|
`^(nevent|note|naddr|npub|nprofile)[a-z0-9]{${VALIDATION.MIN_NOSTR_IDENTIFIER_LENGTH},}$`, |
|
"i", |
|
).test(cleanedQuery) |
|
) { |
|
try { |
|
const decoded = nip19.decode(cleanedQuery); |
|
if (!decoded) throw new Error("Invalid identifier"); |
|
switch (decoded.type) { |
|
case "nevent": |
|
filterOrId = decoded.data.id; |
|
break; |
|
case "note": |
|
filterOrId = decoded.data; |
|
break; |
|
case "naddr": |
|
filterOrId = { |
|
kinds: [decoded.data.kind], |
|
authors: [decoded.data.pubkey], |
|
"#d": [decoded.data.identifier], |
|
}; |
|
break; |
|
case "nprofile": |
|
filterOrId = { |
|
kinds: [0], |
|
authors: [decoded.data.pubkey], |
|
}; |
|
break; |
|
case "npub": |
|
filterOrId = { |
|
kinds: [0], |
|
authors: [decoded.data], |
|
}; |
|
break; |
|
default: |
|
filterOrId = cleanedQuery; |
|
} |
|
} catch (e) { |
|
console.error("[Search] Invalid Nostr identifier:", cleanedQuery, e); |
|
throw new Error("Invalid Nostr identifier."); |
|
} |
|
} |
|
|
|
try { |
|
const event = await fetchEventWithFallback( |
|
get(ndkInstance), |
|
filterOrId, |
|
TIMEOUTS.EVENT_FETCH, |
|
); |
|
|
|
if (!event) { |
|
console.warn("[Search] Event not found for filterOrId:", filterOrId); |
|
return null; |
|
} else { |
|
return event; |
|
} |
|
} catch (err) { |
|
console.error("[Search] Error fetching event:", err, "Query:", query); |
|
throw new Error("Error fetching event. Please check the ID and try again."); |
|
} |
|
} |
|
|
|
/** |
|
* Search for NIP-05 address |
|
*/ |
|
export async function searchNip05( |
|
nip05Address: string, |
|
): Promise<NDKEvent | null> { |
|
// NIP-05 address pattern: user@domain |
|
if (!isValidNip05Address(nip05Address)) { |
|
throw new Error("Invalid NIP-05 address format. Expected: user@domain"); |
|
} |
|
|
|
try { |
|
const [name, domain] = nip05Address.split("@"); |
|
|
|
const res = await fetch(wellKnownUrl(domain, name)); |
|
|
|
if (!res.ok) { |
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`); |
|
} |
|
|
|
const data = await res.json(); |
|
|
|
const pubkey = data.names?.[name]; |
|
if (pubkey) { |
|
const profileFilter = { kinds: [0], authors: [pubkey] }; |
|
const profileEvent = await fetchEventWithFallback( |
|
get(ndkInstance), |
|
profileFilter, |
|
TIMEOUTS.EVENT_FETCH, |
|
); |
|
if (profileEvent) { |
|
return profileEvent; |
|
} else { |
|
throw new Error( |
|
`No profile found for ${name}@${domain} (pubkey: ${pubkey})`, |
|
); |
|
} |
|
} else { |
|
throw new Error(`NIP-05 address not found: ${name}@${domain}`); |
|
} |
|
} catch (e) { |
|
console.error( |
|
`[Search] Error resolving NIP-05 address ${nip05Address}:`, |
|
e, |
|
); |
|
const errorMessage = e instanceof Error ? e.message : String(e); |
|
throw new Error(`Error resolving NIP-05 address: ${errorMessage}`); |
|
} |
|
} |
|
|
|
/** |
|
* Find containing 30040 index events for a given content event |
|
* @param contentEvent The content event to find containers for (30041, 30818, etc.) |
|
* @returns Array of containing 30040 index events |
|
*/ |
|
export async function findContainingIndexEvents( |
|
contentEvent: NDKEvent, |
|
): Promise<NDKEvent[]> { |
|
// Support all content event kinds that can be contained in indexes |
|
const contentEventKinds = [30041, 30818, 30040, 30023]; |
|
if (!contentEventKinds.includes(contentEvent.kind!)) { |
|
return []; |
|
} |
|
|
|
try { |
|
const ndk = get(ndkInstance); |
|
|
|
// Search for 30040 events that reference this content event |
|
// We need to search for events that have an 'a' tag or 'e' tag referencing this event |
|
const contentEventId = contentEvent.id; |
|
const contentEventAddress = contentEvent.tagAddress(); |
|
|
|
// Search for index events that reference this content event |
|
const indexEvents = await ndk.fetchEvents( |
|
{ |
|
kinds: [30040], |
|
"#a": [contentEventAddress], |
|
}, |
|
{ |
|
groupable: true, |
|
skipVerification: false, |
|
skipValidation: false, |
|
}, |
|
); |
|
|
|
// Also search for events with 'e' tags (legacy format) |
|
const indexEventsWithETags = await ndk.fetchEvents( |
|
{ |
|
kinds: [30040], |
|
"#e": [contentEventId], |
|
}, |
|
{ |
|
groupable: true, |
|
skipVerification: false, |
|
skipValidation: false, |
|
}, |
|
); |
|
|
|
// Combine and deduplicate results |
|
const allIndexEvents = new Set([...indexEvents, ...indexEventsWithETags]); |
|
|
|
// Filter to only include valid index events |
|
const validIndexEvents = Array.from(allIndexEvents).filter((event) => { |
|
// Check if it's a valid index event (has title, d tag, and either a or e tags) |
|
const hasTitle = event.getMatchingTags("title").length > 0; |
|
const hasDTag = event.getMatchingTags("d").length > 0; |
|
const hasATags = event.getMatchingTags("a").length > 0; |
|
const hasETags = event.getMatchingTags("e").length > 0; |
|
|
|
return hasTitle && hasDTag && (hasATags || hasETags); |
|
}); |
|
|
|
return validIndexEvents; |
|
} catch (error) { |
|
console.error("[Search] Error finding containing index events:", error); |
|
return []; |
|
} |
|
}
|
|
|