From e4518fbf699e04dc460915e2fa11ed948f09c683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Lohrer?= Date: Sat, 18 Apr 2026 05:25:10 +0200 Subject: [PATCH] publish(task 6): kind:30023 event-builder mit tag-mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildKind30023(args) baut unsigniertes kind:30023-event aus frontmatter + rewritten-body + cover-url. erzeugt pflicht-tags (d, title, published_at) und bedingt optionale (summary aus description, image aus coverUrl, t-tags aus tags[], client aus clientTag). plus additionalTags-parameter für spätere task 15: license-tag und imeta-tags (mit blossom-sha256) werden dort nach dem upload angehängt. 4 tests grün. Co-Authored-By: Claude Opus 4.6 (1M context) --- publish/src/core/event.ts | 43 +++++++++++++++++ publish/tests/event_test.ts | 94 +++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 publish/src/core/event.ts create mode 100644 publish/tests/event_test.ts diff --git a/publish/src/core/event.ts b/publish/src/core/event.ts new file mode 100644 index 0000000..6bdc595 --- /dev/null +++ b/publish/src/core/event.ts @@ -0,0 +1,43 @@ +import type { Frontmatter } from './frontmatter.ts' + +export interface UnsignedEvent { + kind: number + pubkey: string + created_at: number + tags: string[][] + content: string +} + +export interface BuildArgs { + fm: Frontmatter + rewrittenBody: string + coverUrl: string | undefined + pubkeyHex: string + clientTag: string + nowSeconds: number + additionalTags?: string[][] +} + +export function buildKind30023(args: BuildArgs): UnsignedEvent { + const { fm, rewrittenBody, coverUrl, pubkeyHex, clientTag, nowSeconds, additionalTags } = args + const publishedAt = Math.floor(fm.date.getTime() / 1000) + const tags: string[][] = [ + ['d', fm.slug], + ['title', fm.title], + ['published_at', String(publishedAt)], + ] + if (fm.description) tags.push(['summary', fm.description]) + if (coverUrl) tags.push(['image', coverUrl]) + if (Array.isArray(fm.tags)) { + for (const t of fm.tags) tags.push(['t', String(t)]) + } + if (clientTag) tags.push(['client', clientTag]) + if (additionalTags) tags.push(...additionalTags) + return { + kind: 30023, + pubkey: pubkeyHex, + created_at: nowSeconds, + tags, + content: rewrittenBody, + } +} diff --git a/publish/tests/event_test.ts b/publish/tests/event_test.ts new file mode 100644 index 0000000..3c2e637 --- /dev/null +++ b/publish/tests/event_test.ts @@ -0,0 +1,94 @@ +import { assertEquals } from '@std/assert' +import { buildKind30023 } from '../src/core/event.ts' +import type { Frontmatter } from '../src/core/frontmatter.ts' + +const PUBKEY = '4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41' + +Deno.test('buildKind30023: minimaler Post liefert alle Pflicht-Tags', () => { + const fm: Frontmatter = { + title: 'Hello', + slug: 'hello', + date: new Date('2024-01-15T00:00:00Z'), + } + const ev = buildKind30023({ + fm, + rewrittenBody: 'body text', + coverUrl: undefined, + pubkeyHex: PUBKEY, + clientTag: 'test-client', + nowSeconds: 1_700_000_000, + }) + assertEquals(ev.kind, 30023) + assertEquals(ev.pubkey, PUBKEY) + assertEquals(ev.created_at, 1_700_000_000) + assertEquals(ev.content, 'body text') + const tags = ev.tags + assertEquals(tags.find((t) => t[0] === 'd'), ['d', 'hello']) + assertEquals(tags.find((t) => t[0] === 'title'), ['title', 'Hello']) + assertEquals( + tags.find((t) => t[0] === 'published_at')?.[1], + String(Math.floor(Date.UTC(2024, 0, 15) / 1000)), + ) + assertEquals(tags.find((t) => t[0] === 'client'), ['client', 'test-client']) +}) + +Deno.test('buildKind30023: mapping summary / image / tags', () => { + const fm: Frontmatter = { + title: 'T', + slug: 's', + date: new Date('2024-01-01'), + description: 'Summary text', + tags: ['Foo', 'Bar Baz'], + } + const ev = buildKind30023({ + fm, + rewrittenBody: 'b', + coverUrl: 'https://bl.example/cover-hash.png', + pubkeyHex: PUBKEY, + clientTag: 'x', + nowSeconds: 1, + }) + assertEquals(ev.tags.find((t) => t[0] === 'summary'), ['summary', 'Summary text']) + assertEquals( + ev.tags.find((t) => t[0] === 'image'), + ['image', 'https://bl.example/cover-hash.png'], + ) + assertEquals( + ev.tags.filter((t) => t[0] === 't'), + [['t', 'Foo'], ['t', 'Bar Baz']], + ) +}) + +Deno.test('buildKind30023: ohne coverUrl kein image-tag', () => { + const fm: Frontmatter = { + title: 'T', + slug: 's', + date: new Date('2024-01-01'), + } + const ev = buildKind30023({ + fm, + rewrittenBody: 'b', + coverUrl: undefined, + pubkeyHex: PUBKEY, + clientTag: 'x', + nowSeconds: 1, + }) + assertEquals(ev.tags.some((t) => t[0] === 'image'), false) +}) + +Deno.test('buildKind30023: leerer clientTag wird weggelassen', () => { + const fm: Frontmatter = { + title: 'T', + slug: 's', + date: new Date('2024-01-01'), + } + const ev = buildKind30023({ + fm, + rewrittenBody: 'b', + coverUrl: undefined, + pubkeyHex: PUBKEY, + clientTag: '', + nowSeconds: 1, + }) + assertEquals(ev.tags.some((t) => t[0] === 'client'), false) +})