a tool for shared writing and social publishing
at feature/backdate 129 lines 3.5 kB view raw
1"use server"; 2import { TID } from "@atproto/common"; 3import { AtpBaseClient, PubLeafletPublication } from "lexicons/api"; 4import { 5 restoreOAuthSession, 6 OAuthSessionError, 7} from "src/atproto-oauth"; 8import { getIdentityData } from "actions/getIdentityData"; 9import { supabaseServerClient } from "supabase/serverClient"; 10import { Un$Typed } from "@atproto/api"; 11import { Json } from "supabase/database.types"; 12import { Vercel } from "@vercel/sdk"; 13import { isProductionDomain } from "src/utils/isProductionDeployment"; 14import { string } from "zod"; 15 16const VERCEL_TOKEN = process.env.VERCEL_TOKEN; 17const vercel = new Vercel({ 18 bearerToken: VERCEL_TOKEN, 19}); 20let subdomainValidator = string() 21 .min(3) 22 .max(63) 23 .regex(/^[a-z0-9-]+$/); 24type CreatePublicationResult = 25 | { success: true; publication: any } 26 | { success: false; error?: OAuthSessionError }; 27 28export async function createPublication({ 29 name, 30 description, 31 iconFile, 32 subdomain, 33 preferences, 34}: { 35 name: string; 36 description: string; 37 iconFile: File | null; 38 subdomain: string; 39 preferences: Omit<PubLeafletPublication.Preferences, "$type">; 40}): Promise<CreatePublicationResult> { 41 let isSubdomainValid = subdomainValidator.safeParse(subdomain); 42 if (!isSubdomainValid.success) { 43 return { success: false }; 44 } 45 let identity = await getIdentityData(); 46 if (!identity || !identity.atp_did) { 47 return { 48 success: false, 49 error: { 50 type: "oauth_session_expired", 51 message: "Not authenticated", 52 did: "", 53 }, 54 }; 55 } 56 57 let domain = `${subdomain}.leaflet.pub`; 58 59 const sessionResult = await restoreOAuthSession(identity.atp_did); 60 if (!sessionResult.ok) { 61 return { success: false, error: sessionResult.error }; 62 } 63 let credentialSession = sessionResult.value; 64 let agent = new AtpBaseClient( 65 credentialSession.fetchHandler.bind(credentialSession), 66 ); 67 let record: Un$Typed<PubLeafletPublication.Record> = { 68 name, 69 base_path: domain, 70 preferences, 71 }; 72 73 if (description) { 74 record.description = description; 75 } 76 77 // Upload the icon if provided 78 if (iconFile && iconFile.size > 0) { 79 const buffer = await iconFile.arrayBuffer(); 80 const uploadResult = await agent.com.atproto.repo.uploadBlob( 81 new Uint8Array(buffer), 82 { encoding: iconFile.type }, 83 ); 84 85 if (uploadResult.data.blob) { 86 record.icon = uploadResult.data.blob; 87 } 88 } 89 90 let result = await agent.pub.leaflet.publication.create( 91 { repo: credentialSession.did!, rkey: TID.nextStr(), validate: false }, 92 record, 93 ); 94 95 //optimistically write to our db! 96 let { data: publication } = await supabaseServerClient 97 .from("publications") 98 .upsert({ 99 uri: result.uri, 100 identity_did: credentialSession.did!, 101 name: record.name, 102 record: { 103 ...record, 104 $type: "pub.leaflet.publication", 105 } as unknown as Json, 106 }) 107 .select() 108 .single(); 109 110 // Create the custom domain 111 if (isProductionDomain()) { 112 await vercel.projects.addProjectDomain({ 113 idOrName: "prj_9jX4tmYCISnm176frFxk07fF74kG", 114 teamId: "team_42xaJiZMTw9Sr7i0DcLTae9d", 115 requestBody: { 116 name: domain, 117 }, 118 }); 119 } 120 await supabaseServerClient 121 .from("custom_domains") 122 .insert({ domain, confirmed: true, identity: null }); 123 124 await supabaseServerClient 125 .from("publication_domains") 126 .insert({ domain, publication: result.uri, identity: identity.atp_did }); 127 128 return { success: true, publication }; 129}