import { prisma } from '../db/prisma'; import { nostrService } from './nostr'; export interface ImportPostInput { nostrEventId: string; naddr?: string | null; title: string; excerpt?: string | null; authorPubkey: string; publishedAt?: number | string | null; tags?: string[]; visible?: boolean; } // Upserts a blog Post from a Nostr longform event. Shared by the manual import // endpoint and the submission-approval flow so both produce identical results // (auto slug, category links from `t` tags, visible by default). export async function importPostFromNostr(input: ImportPostInput) { const { nostrEventId, naddr, title, excerpt, authorPubkey, publishedAt, tags, visible } = input; const slugBase = title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''); const slug = `${slugBase}-${nostrEventId.slice(0, 8)}`; const post = await prisma.post.upsert({ where: { nostrEventId }, update: { title, excerpt: excerpt || undefined, naddr: naddr || undefined, }, create: { nostrEventId, naddr: naddr || null, title, slug, excerpt: excerpt || null, authorPubkey, visible: visible ?? true, publishedAt: publishedAt ? new Date(typeof publishedAt === 'number' ? publishedAt * 1000 : publishedAt) : new Date(), }, }); if (Array.isArray(tags) && tags.length > 0) { const categoryIds: string[] = []; for (const tag of tags) { const catSlug = tag.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); if (!catSlug) continue; const category = await prisma.category.upsert({ where: { slug: catSlug }, update: {}, create: { name: tag.charAt(0).toUpperCase() + tag.slice(1), slug: catSlug, }, }); categoryIds.push(category.id); } await prisma.postCategory.deleteMany({ where: { postId: post.id } }); if (categoryIds.length > 0) { await prisma.postCategory.createMany({ data: categoryIds.map((categoryId) => ({ postId: post.id, categoryId, })), }); } } return prisma.post.findUnique({ where: { id: post.id }, include: { categories: { include: { category: true } } }, }); } export interface SubmissionRef { eventId: string | null; naddr: string | null; title: string; authorPubkey: string; } // Resolves the Nostr longform event referenced by a submission into a blog // import payload. Returns null when the event cannot be resolved into something // publishable (e.g. naddr that no relay can serve and no fallback event id). export async function resolveSubmissionImport( submission: SubmissionRef ): Promise { const event = submission.naddr ? await nostrService.fetchLongformEvent(submission.naddr) : submission.eventId ? await nostrService.fetchEvent(submission.eventId) : null; const nostrEventId: string | undefined = event?.id || submission.eventId || undefined; if (!nostrEventId) return null; const eventTags: string[][] = Array.isArray(event?.tags) ? (event!.tags as string[][]) : []; const titleTag = eventTags.find((t) => t[0] === 'title')?.[1]; const topicTags = eventTags .filter((t) => t[0] === 't' && t[1]) .map((t) => (t[1] as string).toLowerCase()); const excerpt = ((event?.content as string) || '') .slice(0, 200) .replace(/[#*_\n]/g, '') .trim(); return { nostrEventId, naddr: submission.naddr || undefined, title: submission.title || titleTag || 'Untitled', excerpt: excerpt || undefined, authorPubkey: event?.pubkey || submission.authorPubkey, publishedAt: event?.created_at ?? null, tags: topicTags.length > 0 ? topicTags : undefined, visible: true, }; }