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 main 152 lines 4.0 kB view raw
1import type { AtpAgent } from "@atproto/api"; 2import type { Database } from "@atbb/db"; 3import { roles, rolePermissions } from "@atbb/db"; 4import { eq } from "drizzle-orm"; 5 6interface DefaultRole { 7 name: string; 8 description: string; 9 permissions: string[]; 10 priority: number; 11} 12 13export const DEFAULT_ROLES: DefaultRole[] = [ 14 { 15 name: "Owner", 16 description: "Forum owner with full control", 17 permissions: ["*"], 18 priority: 0, 19 }, 20 { 21 name: "Admin", 22 description: "Can manage forum structure and users", 23 permissions: [ 24 "space.atbb.permission.manageCategories", 25 "space.atbb.permission.manageRoles", 26 "space.atbb.permission.manageMembers", 27 "space.atbb.permission.moderatePosts", 28 "space.atbb.permission.banUsers", 29 "space.atbb.permission.pinTopics", 30 "space.atbb.permission.lockTopics", 31 "space.atbb.permission.createTopics", 32 "space.atbb.permission.createPosts", 33 ], 34 priority: 10, 35 }, 36 { 37 name: "Moderator", 38 description: "Can moderate content and users", 39 permissions: [ 40 "space.atbb.permission.moderatePosts", 41 "space.atbb.permission.banUsers", 42 "space.atbb.permission.pinTopics", 43 "space.atbb.permission.lockTopics", 44 "space.atbb.permission.createTopics", 45 "space.atbb.permission.createPosts", 46 ], 47 priority: 20, 48 }, 49 { 50 name: "Member", 51 description: "Regular forum member", 52 permissions: [ 53 "space.atbb.permission.createTopics", 54 "space.atbb.permission.createPosts", 55 ], 56 priority: 30, 57 }, 58]; 59 60export interface SeededRole { 61 name: string; 62 uri: string; 63 cid: string; 64} 65 66interface SeedRolesResult { 67 created: number; 68 skipped: number; 69 roles: SeededRole[]; 70} 71 72/** 73 * Seed default roles to Forum DID's PDS and database. 74 * Idempotent: checks for existing roles by name before creating. 75 * Returns role data (URI + CID) for downstream steps. 76 */ 77export async function seedDefaultRoles( 78 db: Database, 79 agent: AtpAgent, 80 forumDid: string 81): Promise<SeedRolesResult> { 82 let created = 0; 83 let skipped = 0; 84 const seededRoles: SeededRole[] = []; 85 86 for (const defaultRole of DEFAULT_ROLES) { 87 // Check if role already exists by name 88 const [existingRole] = await db 89 .select() 90 .from(roles) 91 .where(eq(roles.name, defaultRole.name)) 92 .limit(1); 93 94 if (existingRole) { 95 skipped++; 96 seededRoles.push({ 97 name: existingRole.name, 98 uri: `at://${existingRole.did}/space.atbb.forum.role/${existingRole.rkey}`, 99 cid: existingRole.cid, 100 }); 101 continue; 102 } 103 104 // Create role record on Forum DID's PDS 105 const response = await agent.com.atproto.repo.createRecord({ 106 repo: forumDid, 107 collection: "space.atbb.forum.role", 108 record: { 109 $type: "space.atbb.forum.role", 110 name: defaultRole.name, 111 description: defaultRole.description, 112 permissions: defaultRole.permissions, 113 priority: defaultRole.priority, 114 createdAt: new Date().toISOString(), 115 }, 116 }); 117 118 // Extract rkey from the returned URI (at://did/collection/rkey) 119 const rkey = response.data.uri.split("/").pop()!; 120 121 // Insert into database so downstream steps can query it 122 const [insertedRole] = await db.insert(roles).values({ 123 did: forumDid, 124 rkey, 125 cid: response.data.cid, 126 name: defaultRole.name, 127 description: defaultRole.description, 128 priority: defaultRole.priority, 129 createdAt: new Date(), 130 indexedAt: new Date(), 131 }).returning({ id: roles.id }); 132 133 if (defaultRole.permissions.length > 0) { 134 await db.insert(rolePermissions).values( 135 defaultRole.permissions.map((permission) => ({ 136 roleId: insertedRole.id, 137 permission, 138 })) 139 ); 140 } 141 142 seededRoles.push({ 143 name: defaultRole.name, 144 uri: response.data.uri, 145 cid: response.data.cid, 146 }); 147 148 created++; 149 } 150 151 return { created, skipped, roles: seededRoles }; 152}