feat(snapshot): post-json-builder mit fallback-summary
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7e38b73785
commit
4b2c157938
|
|
@ -0,0 +1,108 @@
|
|||
import { nip19 } from 'nostr-tools'
|
||||
import type { SignedEvent } from './types.ts'
|
||||
|
||||
export interface CoverImage {
|
||||
url: string
|
||||
width?: number
|
||||
height?: number
|
||||
alt?: string
|
||||
mime?: string
|
||||
}
|
||||
|
||||
export interface TranslationRef {
|
||||
lang: string
|
||||
slug: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface PostJson {
|
||||
slug: string
|
||||
event_id: string
|
||||
created_at: number
|
||||
published_at: number
|
||||
title: string
|
||||
summary: string
|
||||
lang: string
|
||||
cover_image: CoverImage | null
|
||||
content_markdown: string
|
||||
tags: string[]
|
||||
naddr: string
|
||||
habla_url: string
|
||||
translations: TranslationRef[]
|
||||
}
|
||||
|
||||
const SUMMARY_MAX = 200
|
||||
|
||||
function tagValue(ev: SignedEvent, name: string): string | undefined {
|
||||
return ev.tags.find((t) => t[0] === name)?.[1]
|
||||
}
|
||||
|
||||
function tagsAll(ev: SignedEvent, name: string): string[] {
|
||||
return ev.tags.filter((t) => t[0] === name).map((t) => t[1])
|
||||
}
|
||||
|
||||
function deriveSummary(content: string): string {
|
||||
const flat = content.replace(/\s+/g, ' ').trim()
|
||||
if (flat.length <= SUMMARY_MAX) return flat
|
||||
const cut = flat.slice(0, SUMMARY_MAX)
|
||||
const lastSpace = cut.lastIndexOf(' ')
|
||||
const trimmed = lastSpace > SUMMARY_MAX * 0.5 ? cut.slice(0, lastSpace) : cut
|
||||
return trimmed + '…'
|
||||
}
|
||||
|
||||
export function buildPostJson(
|
||||
ev: SignedEvent,
|
||||
titleByDtag: Map<string, string>,
|
||||
): PostJson {
|
||||
const slug = tagValue(ev, 'd') ?? ''
|
||||
const title = tagValue(ev, 'title') ?? ''
|
||||
const summaryTag = tagValue(ev, 'summary')
|
||||
const summary = summaryTag && summaryTag.length > 0 ? summaryTag : deriveSummary(ev.content)
|
||||
const image = tagValue(ev, 'image')
|
||||
const publishedAtRaw = tagValue(ev, 'published_at')
|
||||
const publishedAt = publishedAtRaw ? parseInt(publishedAtRaw, 10) : ev.created_at
|
||||
const lang = ev.tags.find((t) => t[0] === 'l' && t[2] === 'ISO-639-1')?.[1] ?? 'de'
|
||||
|
||||
const cover_image: CoverImage | null = image
|
||||
? { url: image, alt: title || undefined }
|
||||
: null
|
||||
|
||||
const naddr = nip19.naddrEncode({
|
||||
kind: ev.kind,
|
||||
pubkey: ev.pubkey,
|
||||
identifier: slug,
|
||||
})
|
||||
|
||||
const translations: TranslationRef[] = []
|
||||
for (const tag of ev.tags) {
|
||||
if (tag[0] !== 'a') continue
|
||||
if (tag[3] !== 'translation') continue
|
||||
const coord = tag[1]
|
||||
if (!coord) continue
|
||||
const parts = coord.split(':')
|
||||
if (parts.length !== 3) continue
|
||||
const otherSlug = parts[2]
|
||||
const otherTitle = titleByDtag.get(otherSlug) ?? otherSlug
|
||||
translations.push({
|
||||
lang: lang === 'de' ? 'en' : 'de',
|
||||
slug: otherSlug,
|
||||
title: otherTitle,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
slug,
|
||||
event_id: ev.id,
|
||||
created_at: ev.created_at,
|
||||
published_at: publishedAt,
|
||||
title,
|
||||
summary,
|
||||
lang,
|
||||
cover_image,
|
||||
content_markdown: ev.content,
|
||||
tags: tagsAll(ev, 't'),
|
||||
naddr,
|
||||
habla_url: `https://habla.news/a/${naddr}`,
|
||||
translations,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { assertEquals } from '@std/assert'
|
||||
import { buildPostJson } from '../src/core/post-json.ts'
|
||||
import type { SignedEvent } from '../src/core/types.ts'
|
||||
|
||||
const PUBKEY = '4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41'
|
||||
|
||||
function buildEvent(opts: {
|
||||
d: string
|
||||
title: string
|
||||
summary?: string
|
||||
image?: string
|
||||
publishedAt?: number
|
||||
lang?: string
|
||||
tags?: string[]
|
||||
translationCoords?: string[]
|
||||
content: string
|
||||
}): SignedEvent {
|
||||
const tags: string[][] = [['d', opts.d], ['title', opts.title]]
|
||||
if (opts.summary) tags.push(['summary', opts.summary])
|
||||
if (opts.image) tags.push(['image', opts.image])
|
||||
if (opts.publishedAt) tags.push(['published_at', String(opts.publishedAt)])
|
||||
if (opts.lang) {
|
||||
tags.push(['L', 'ISO-639-1'])
|
||||
tags.push(['l', opts.lang, 'ISO-639-1'])
|
||||
}
|
||||
for (const t of opts.tags ?? []) tags.push(['t', t])
|
||||
for (const c of opts.translationCoords ?? []) tags.push(['a', c, '', 'translation'])
|
||||
return {
|
||||
id: 'event-' + opts.d, pubkey: PUBKEY, created_at: 1700000000, kind: 30023,
|
||||
sig: 'sig', content: opts.content, tags,
|
||||
}
|
||||
}
|
||||
|
||||
Deno.test('buildPostJson: vollstaendiges event', () => {
|
||||
const ev = buildEvent({
|
||||
d: 'bibel-selfies', title: 'Bibel-Selfies', summary: 'Kurz',
|
||||
image: 'https://blossom.edufeed.org/abc.jpg',
|
||||
publishedAt: 1699000000, lang: 'de', tags: ['Bibel'],
|
||||
translationCoords: [`30023:${PUBKEY}:bible-selfies`],
|
||||
content: '# body',
|
||||
})
|
||||
const titleByDtag = new Map([['bible-selfies', 'Bible-Selfies']])
|
||||
const json = buildPostJson(ev, titleByDtag)
|
||||
assertEquals(json.slug, 'bibel-selfies')
|
||||
assertEquals(json.title, 'Bibel-Selfies')
|
||||
assertEquals(json.summary, 'Kurz')
|
||||
assertEquals(json.lang, 'de')
|
||||
assertEquals(json.tags, ['Bibel'])
|
||||
assertEquals(json.published_at, 1699000000)
|
||||
assertEquals(json.cover_image?.url, 'https://blossom.edufeed.org/abc.jpg')
|
||||
assertEquals(json.translations, [
|
||||
{ lang: 'en', slug: 'bible-selfies', title: 'Bible-Selfies' },
|
||||
])
|
||||
assertEquals(json.content_markdown, '# body')
|
||||
})
|
||||
|
||||
Deno.test('buildPostJson: fallback summary aus content', () => {
|
||||
const ev = buildEvent({
|
||||
d: 'no-summary', title: 'X', content: 'Lorem ipsum dolor sit amet.'.repeat(20),
|
||||
})
|
||||
const json = buildPostJson(ev, new Map())
|
||||
if (!json.summary) throw new Error('summary fehlt')
|
||||
if (json.summary.length > 220) throw new Error('summary zu lang')
|
||||
if (!json.summary.endsWith('…')) throw new Error('summary ohne ellipsis')
|
||||
})
|
||||
|
||||
Deno.test('buildPostJson: fehlt published_at -> created_at', () => {
|
||||
const ev = buildEvent({ d: 'no-pub', title: 'X', content: 'x' })
|
||||
const json = buildPostJson(ev, new Map())
|
||||
assertEquals(json.published_at, 1700000000)
|
||||
})
|
||||
|
||||
Deno.test('buildPostJson: fehlt image -> cover_image null', () => {
|
||||
const ev = buildEvent({ d: 'no-img', title: 'X', content: 'x' })
|
||||
const json = buildPostJson(ev, new Map())
|
||||
assertEquals(json.cover_image, null)
|
||||
})
|
||||
|
||||
Deno.test('buildPostJson: lang default de wenn keine l-tags', () => {
|
||||
const ev = buildEvent({ d: 'no-lang', title: 'X', content: 'x' })
|
||||
const json = buildPostJson(ev, new Map())
|
||||
assertEquals(json.lang, 'de')
|
||||
})
|
||||
Loading…
Reference in New Issue