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.
 
 
 
 

87 lines
3.1 KiB

import { ExtendedKind } from '@/constants'
import { cleanUrl } from '@/lib/url'
/** NIP-22: `K` / `k` value for http(s) URL comment scopes (web pages, articles). */
export const NIP22_URL_SCOPE_KIND = 'web'
import { bytesToHex } from '@noble/hashes/utils'
import { sha256 } from '@noble/hashes/sha256'
import type { Event } from 'nostr-tools'
/** Encode article URL for a single path segment (UTF-8 → base64url, no padding). */
export function encodeRssArticlePathSegment(articleUrl: string): string {
const bytes = new TextEncoder().encode(articleUrl)
let binary = ''
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!)
const b64 = btoa(binary)
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}
export function decodeRssArticlePathSegment(segment: string): string {
const b64 = segment.replace(/-/g, '+').replace(/_/g, '/')
const pad = b64.length % 4 === 0 ? '' : '='.repeat(4 - (b64.length % 4))
const binary = atob(b64 + pad)
const out = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i)
return new TextDecoder().decode(out)
}
/** Stable fake event id for caching / stats keys (not a published note id). */
export function rssArticleStableEventId(articleUrl: string): string {
return bytesToHex(sha256(new TextEncoder().encode(`rss-thread-root:${articleUrl}`)))
}
/** Strip tracking params from http(s) article URLs; leave other values unchanged. */
export function canonicalizeRssArticleUrl(url: string): string {
const t = url.trim()
if (!t.startsWith('http://') && !t.startsWith('https://')) return t
return cleanUrl(t) || t
}
/** Normalize user input to an http(s) URL for manual article threads; returns null if invalid. */
export function normalizeHttpArticleUrl(raw: string): string | null {
let s = raw.trim()
if (!s) return null
if (!/^https?:\/\//i.test(s)) {
s = `https://${s}`
}
try {
const u = new URL(s)
if (u.protocol !== 'http:' && u.protocol !== 'https:') return null
return canonicalizeRssArticleUrl(u.href)
} catch {
return null
}
}
/**
* Synthetic parent event for kind 1111 comments on an RSS article.
* Thread is keyed by the article URL in both `i` and `I` tags (no e/a root).
*/
export function createRssThreadRootEvent(articleUrl: string): Event {
const canonical = canonicalizeRssArticleUrl(articleUrl)
return {
id: rssArticleStableEventId(canonical),
pubkey: '0'.repeat(64),
created_at: 0,
kind: ExtendedKind.RSS_THREAD_ROOT,
tags: [
['I', canonical],
['i', canonical],
['K', NIP22_URL_SCOPE_KIND],
['k', NIP22_URL_SCOPE_KIND]
],
content: '',
sig: ''
}
}
export function getArticleUrlFromCommentITags(event: Event): string | undefined {
const upper = event.tags.find((t) => t[0] === 'I')?.[1]
if (upper) return upper
return event.tags.find((t) => t[0] === 'i')?.[1]
}
/** Client-only RSS thread parent (non-standard kind); not a real relay event. */
export function isRssThreadSyntheticParentEvent(event: Pick<Event, 'kind'>): boolean {
return event.kind === ExtendedKind.RSS_THREAD_ROOT
}