WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at atb-46-modlog-endpoint 104 lines 2.6 kB view raw
1import type { AtpAgent } from "@atproto/api"; 2import type { Database } from "@atbb/db"; 3import { boards } from "@atbb/db"; 4import { eq, and } from "drizzle-orm"; 5import { deriveSlug } from "../slug.js"; 6 7interface CreateBoardInput { 8 name: string; 9 description?: string; 10 slug?: string; 11 sortOrder?: number; 12 categoryUri: string; // AT URI: at://did/space.atbb.forum.category/rkey 13 categoryId: bigint; // DB FK 14 categoryCid: string; // CID for the category strongRef 15} 16 17interface CreateBoardResult { 18 created: boolean; 19 skipped: boolean; 20 uri?: string; 21 cid?: string; 22 existingName?: string; 23} 24 25/** 26 * Create a space.atbb.forum.board record on the Forum DID's PDS 27 * and insert it into the database. 28 * Idempotent: skips if a board with the same name in the same category exists. 29 */ 30export async function createBoard( 31 db: Database, 32 agent: AtpAgent, 33 forumDid: string, 34 input: CreateBoardInput 35): Promise<CreateBoardResult> { 36 // Check if board with this name already exists in the category 37 const [existing] = await db 38 .select() 39 .from(boards) 40 .where( 41 and( 42 eq(boards.did, forumDid), 43 eq(boards.name, input.name), 44 eq(boards.categoryUri, input.categoryUri) 45 ) 46 ) 47 .limit(1); 48 49 if (existing) { 50 return { 51 created: false, 52 skipped: true, 53 uri: `at://${existing.did}/space.atbb.forum.board/${existing.rkey}`, 54 cid: existing.cid, 55 existingName: existing.name, 56 }; 57 } 58 59 const slug = input.slug ?? deriveSlug(input.name); 60 const now = new Date(); 61 62 const response = await agent.com.atproto.repo.createRecord({ 63 repo: forumDid, 64 collection: "space.atbb.forum.board", 65 record: { 66 $type: "space.atbb.forum.board", 67 name: input.name, 68 ...(input.description && { description: input.description }), 69 slug, 70 ...(input.sortOrder !== undefined && { sortOrder: input.sortOrder }), 71 // categoryRef shape: { category: strongRef } 72 category: { 73 category: { 74 uri: input.categoryUri, 75 cid: input.categoryCid, 76 }, 77 }, 78 createdAt: now.toISOString(), 79 }, 80 }); 81 82 const rkey = response.data.uri.split("/").pop()!; 83 84 await db.insert(boards).values({ 85 did: forumDid, 86 rkey, 87 cid: response.data.cid, 88 name: input.name, 89 description: input.description ?? null, 90 slug, 91 sortOrder: input.sortOrder ?? null, 92 categoryId: input.categoryId, 93 categoryUri: input.categoryUri, 94 createdAt: now, 95 indexedAt: now, 96 }); 97 98 return { 99 created: true, 100 skipped: false, 101 uri: response.data.uri, 102 cid: response.data.cid, 103 }; 104}