Files
BelgianBitcoinEmbassy/backend/src/services/postImport.ts
T
bbeandCursor 99380ef6aa fix: auto-publish approved blog submissions on approval
Approved user submissions now import into the blog automatically, with a
backfill script for existing approvals and a WebSocket polyfill so backend
Nostr relay queries work on Node 20.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 23:38:48 +02:00

125 lines
3.7 KiB
TypeScript

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<ImportPostInput | null> {
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,
};
}