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.
105 lines
3.6 KiB
105 lines
3.6 KiB
import { splitNip05Identifier } from '@/lib/nip05' |
|
import { decodeProfileSearchQueryToPubkeyHex } from '@/lib/profile-search-query' |
|
import type { Event } from 'nostr-tools' |
|
import { kinds } from 'nostr-tools' |
|
|
|
/** |
|
* Trim, lowercase, and collapse whitespace for case-insensitive profile / NIP-05 matching. |
|
*/ |
|
export function normalizeProfileSearchQueryForMatch(raw: string): string { |
|
return raw.trim().toLowerCase().replace(/\s+/g, ' ') |
|
} |
|
|
|
/** Every `nip05` tag value plus JSON `nip05` (string or string[]). */ |
|
export function collectNip05ValuesFromKind0(ev: Event): string[] { |
|
const set = new Set<string>() |
|
for (const t of ev.tags ?? []) { |
|
if (!Array.isArray(t) || t.length < 2) continue |
|
if (String(t[0]).toLowerCase() !== 'nip05') continue |
|
for (let i = 1; i < t.length; i++) { |
|
const v = String(t[i] ?? '').trim() |
|
if (v) set.add(v) |
|
} |
|
} |
|
try { |
|
const o = JSON.parse(ev.content || '{}') as Record<string, unknown> |
|
const n = o.nip05 |
|
if (typeof n === 'string' && n.trim()) set.add(n.trim()) |
|
else if (Array.isArray(n)) { |
|
for (const x of n) { |
|
if (typeof x === 'string' && x.trim()) set.add(x.trim()) |
|
} |
|
} |
|
} catch { |
|
/* ignore */ |
|
} |
|
return [...set] |
|
} |
|
|
|
function haystackMatchesNeedle(haystack: string, needle: string, needleNoAt: string): boolean { |
|
const h = haystack.toLowerCase() |
|
if (needle && h.includes(needle)) return true |
|
if (needleNoAt && needleNoAt.length > 0 && h.includes(needleNoAt)) return true |
|
return false |
|
} |
|
|
|
/** |
|
* True when kind-0 `ev` matches a profile search string: pubkey / npub decode, raw JSON `content`, |
|
* JSON name / display_name / about, every `nip05` tag, and JSON `nip05` (including multiple values). |
|
*/ |
|
export function profileKind0MatchesSearchQuery(ev: Event, rawQuery: string): boolean { |
|
if (ev.kind !== kinds.Metadata) return false |
|
const trimmed = rawQuery.trim() |
|
if (!trimmed) return false |
|
|
|
const decodedPk = decodeProfileSearchQueryToPubkeyHex(trimmed) |
|
if (decodedPk && ev.pubkey.toLowerCase() === decodedPk) return true |
|
|
|
const needle = normalizeProfileSearchQueryForMatch(trimmed) |
|
if (!needle) return false |
|
const needleNoAt = needle.startsWith('@') ? needle.slice(1).trim() : needle |
|
|
|
const pkLower = ev.pubkey.toLowerCase() |
|
if (pkLower.includes(needle) || (needleNoAt && pkLower.includes(needleNoAt))) return true |
|
|
|
const contentRaw = ev.content ?? '' |
|
if (haystackMatchesNeedle(contentRaw, needle, needleNoAt)) return true |
|
|
|
for (const nip of collectNip05ValuesFromKind0(ev)) { |
|
const nl = nip.toLowerCase() |
|
if (nl === needle || (needleNoAt && nl === needleNoAt)) return true |
|
if (haystackMatchesNeedle(nip, needle, needleNoAt)) return true |
|
const sp = splitNip05Identifier(nip) |
|
if (sp) { |
|
const compact = `${sp.name}@${sp.domain}`.toLowerCase() |
|
const spaced = `${sp.name} ${sp.domain}`.toLowerCase() |
|
if ( |
|
compact.includes(needle) || |
|
needle.includes(compact) || |
|
spaced.includes(needle) || |
|
needle.includes(spaced) |
|
) { |
|
return true |
|
} |
|
if (needleNoAt) { |
|
if (compact.includes(needleNoAt) || spaced.includes(needleNoAt)) return true |
|
} |
|
} |
|
} |
|
|
|
try { |
|
const profileObj = JSON.parse(ev.content || '{}') as Record<string, unknown> |
|
const blobs = [ |
|
typeof profileObj.display_name === 'string' ? profileObj.display_name : '', |
|
typeof profileObj.name === 'string' ? profileObj.name : '', |
|
typeof profileObj.about === 'string' ? profileObj.about : '' |
|
] |
|
for (const p of blobs) { |
|
if (haystackMatchesNeedle(p, needle, needleNoAt)) return true |
|
} |
|
} catch { |
|
return false |
|
} |
|
|
|
return false |
|
}
|
|
|