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>
125 lines
3.7 KiB
TypeScript
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,
|
|
};
|
|
}
|