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.
 
 
 
 

194 lines
4.9 KiB

import { nip19 } from 'nostr-tools'
/** Find npub1 / nprofile1 / note1 / nevent1 / naddr1 tokens in text. */
const BECH32_NOSTR_RE = /(?:npub1|nprofile1|note1|nevent1|naddr1)[a-z0-9]+/gi
export type NostrUrlExtract = { kind: 'event' | 'profile'; id: string }
function classifyBech32(id: string): NostrUrlExtract | null {
try {
const { type } = nip19.decode(id)
if (type === 'npub' || type === 'nprofile') return { kind: 'profile', id }
if (type === 'note' || type === 'nevent' || type === 'naddr') return { kind: 'event', id }
} catch {
// ignore
}
return null
}
function firstNostrExtractInString(s: string): NostrUrlExtract | null {
const re = new RegExp(BECH32_NOSTR_RE.source, 'gi')
let m: RegExpExecArray | null
while ((m = re.exec(s)) !== null) {
const hit = classifyBech32(m[0])
if (hit) return hit
}
return null
}
function isValidEmbeddedNotePointer(id: string): boolean {
const s = id.trim()
if (/^[0-9a-f]{64}$/i.test(s)) return true
const hit = classifyBech32(s)
return hit?.kind === 'event'
}
function isProfilePointer(id: string): boolean {
const s = id.trim()
if (/^[0-9a-f]{64}$/i.test(s)) return true
const hit = classifyBech32(s)
return hit?.kind === 'profile'
}
function extractHex64(s: string): string | null {
const m = s.match(/\b[0-9a-f]{64}\b/i)
return m ? m[0].toLowerCase() : null
}
/**
* True if this hostname serves this web app: current tab origin and/or known production/dev hosts.
* Needed so `https://jumble.imwald.eu/.../notes/nevent…` embeds while the dev server runs on localhost.
*/
export function urlHostnameIsKnownImwaldWebHost(
urlHostname: string,
appOrigin: string | null
): boolean {
const h = urlHostname.toLowerCase()
if (h === 'jumble.imwald.eu') return true
if (h === 'localhost' || h === '127.0.0.1') return true
if (appOrigin) {
try {
if (h === new URL(appOrigin).hostname.toLowerCase()) return true
} catch {
// ignore
}
}
return false
}
/**
* In-app HTTP(S) links to our routes → embed like `nostr:…` (same tab origin or known Imwald/localhost host).
*/
export function parseSameOriginAppNostrUrl(urlStr: string, appOrigin: string | null): NostrUrlExtract | null {
let u: URL
try {
u = new URL(urlStr)
} catch {
return null
}
if (!urlHostnameIsKnownImwaldWebHost(u.hostname, appOrigin)) return null
let path = u.pathname
if (path.length > 1 && path.endsWith('/')) path = path.slice(0, -1)
if (!path) path = '/'
const usersMatch = path.match(/^\/users\/([^/?#]+)$/i)
if (usersMatch) {
const id = decodeURIComponent(usersMatch[1])
if (isProfilePointer(id)) {
return { kind: 'profile', id }
}
return null
}
const notesMatch = path.match(/\/notes\/([^/?#]+)$/i)
if (notesMatch) {
const id = decodeURIComponent(notesMatch[1])
if (isValidEmbeddedNotePointer(id)) {
return { kind: 'event', id }
}
return null
}
return null
}
const QUERY_KEYS_PRIORITY = [
'id',
'nevent',
'note',
'naddr',
'event',
'e',
'npub',
'nprofile',
'pubkey',
'user',
'p',
'author'
]
/**
* Third-party URLs: Nostr id in query or path — offer chevron-expand embed (not auto).
*/
export function extractExternalUrlNostrForExpandable(
urlStr: string,
appOrigin: string | null
): NostrUrlExtract | null {
if (parseSameOriginAppNostrUrl(urlStr, appOrigin)) return null
let u: URL
try {
u = new URL(urlStr)
} catch {
return null
}
const tryPiece = (raw: string): NostrUrlExtract | null => {
const s = raw.trim()
if (!s) return null
const hex = extractHex64(s)
if (hex) return { kind: 'event', id: hex }
const b = firstNostrExtractInString(s)
if (b) return b
return null
}
for (const key of QUERY_KEYS_PRIORITY) {
const v = u.searchParams.get(key)
if (!v) continue
let decoded = v
try {
decoded = decodeURIComponent(v)
} catch {
// use raw
}
const hit = tryPiece(decoded)
if (hit) return hit
}
for (const [, v] of u.searchParams.entries()) {
let decoded = v
try {
decoded = decodeURIComponent(v)
} catch {
// use raw
}
const hit = tryPiece(decoded)
if (hit) return hit
}
const pathHit = tryPiece(u.pathname)
if (pathHit) return pathHit
const hash = u.hash ? u.hash.slice(1) : ''
if (hash) {
const hashHit = tryPiece(hash)
if (hashHit) return hashHit
}
return firstNostrExtractInString(u.href) ?? null
}
export function getBrowserAppOrigin(): string | null {
if (typeof window === 'undefined') return null
return window.location.origin
}
/** Skip duplicate WebPreview at bottom of note when URL is handled as embed / expandable. */
export function httpUrlSkipsBottomWebPreview(urlStr: string, appOrigin: string | null): boolean {
return (
parseSameOriginAppNostrUrl(urlStr, appOrigin) != null ||
extractExternalUrlNostrForExpandable(urlStr, appOrigin) != null
)
}