a tool for shared writing and social publishing
at update/delete-blocks 162 lines 5.0 kB view raw
1"use server"; 2import { TID } from "@atproto/common"; 3import { 4 AtpBaseClient, 5 PubLeafletPublication, 6 SiteStandardPublication, 7} from "lexicons/api"; 8import { 9 restoreOAuthSession, 10 OAuthSessionError, 11} from "src/atproto-oauth"; 12import { getIdentityData } from "actions/getIdentityData"; 13import { supabaseServerClient } from "supabase/serverClient"; 14import { Json } from "supabase/database.types"; 15import { Vercel } from "@vercel/sdk"; 16import { isProductionDomain } from "src/utils/isProductionDeployment"; 17import { string } from "zod"; 18import { getPublicationType } from "src/utils/collectionHelpers"; 19import { PubThemeDefaultsRGB } from "components/ThemeManager/themeDefaults"; 20 21const VERCEL_TOKEN = process.env.VERCEL_TOKEN; 22const vercel = new Vercel({ 23 bearerToken: VERCEL_TOKEN, 24}); 25let subdomainValidator = string() 26 .min(3) 27 .max(63) 28 .regex(/^[a-z0-9-]+$/); 29type CreatePublicationResult = 30 | { success: true; publication: any } 31 | { success: false; error?: OAuthSessionError }; 32 33export async function createPublication({ 34 name, 35 description, 36 iconFile, 37 subdomain, 38 preferences, 39}: { 40 name: string; 41 description: string; 42 iconFile: File | null; 43 subdomain: string; 44 preferences: Omit<PubLeafletPublication.Preferences, "$type">; 45}): Promise<CreatePublicationResult> { 46 let isSubdomainValid = subdomainValidator.safeParse(subdomain); 47 if (!isSubdomainValid.success) { 48 return { success: false }; 49 } 50 let identity = await getIdentityData(); 51 if (!identity || !identity.atp_did) { 52 return { 53 success: false, 54 error: { 55 type: "oauth_session_expired", 56 message: "Not authenticated", 57 did: "", 58 }, 59 }; 60 } 61 62 let domain = `${subdomain}.leaflet.pub`; 63 64 const sessionResult = await restoreOAuthSession(identity.atp_did); 65 if (!sessionResult.ok) { 66 return { success: false, error: sessionResult.error }; 67 } 68 let credentialSession = sessionResult.value; 69 let agent = new AtpBaseClient( 70 credentialSession.fetchHandler.bind(credentialSession), 71 ); 72 73 // Use site.standard.publication for new publications 74 const publicationType = getPublicationType(); 75 const url = `https://${domain}`; 76 77 // Build record based on publication type 78 let record: SiteStandardPublication.Record | PubLeafletPublication.Record; 79 let iconBlob: Awaited<ReturnType<typeof agent.com.atproto.repo.uploadBlob>>["data"]["blob"] | undefined; 80 81 // Upload the icon if provided 82 if (iconFile && iconFile.size > 0) { 83 const buffer = await iconFile.arrayBuffer(); 84 const uploadResult = await agent.com.atproto.repo.uploadBlob( 85 new Uint8Array(buffer), 86 { encoding: iconFile.type }, 87 ); 88 iconBlob = uploadResult.data.blob; 89 } 90 91 if (publicationType === "site.standard.publication") { 92 record = { 93 $type: "site.standard.publication", 94 name, 95 url, 96 ...(description && { description }), 97 ...(iconBlob && { icon: iconBlob }), 98 basicTheme: { 99 $type: "site.standard.theme.basic", 100 background: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.background }, 101 foreground: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.foreground }, 102 accent: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.accent }, 103 accentForeground: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.accentForeground }, 104 }, 105 preferences: { 106 showInDiscover: preferences.showInDiscover, 107 showComments: preferences.showComments, 108 showMentions: preferences.showMentions, 109 showPrevNext: preferences.showPrevNext, 110 }, 111 } satisfies SiteStandardPublication.Record; 112 } else { 113 record = { 114 $type: "pub.leaflet.publication", 115 name, 116 base_path: domain, 117 ...(description && { description }), 118 ...(iconBlob && { icon: iconBlob }), 119 preferences, 120 } satisfies PubLeafletPublication.Record; 121 } 122 123 let { data: result } = await agent.com.atproto.repo.putRecord({ 124 repo: credentialSession.did!, 125 rkey: TID.nextStr(), 126 collection: publicationType, 127 record, 128 validate: false, 129 }); 130 131 //optimistically write to our db! 132 let { data: publication } = await supabaseServerClient 133 .from("publications") 134 .upsert({ 135 uri: result.uri, 136 identity_did: credentialSession.did!, 137 name, 138 record: record as unknown as Json, 139 }) 140 .select() 141 .single(); 142 143 // Create the custom domain 144 if (isProductionDomain()) { 145 await vercel.projects.addProjectDomain({ 146 idOrName: "prj_9jX4tmYCISnm176frFxk07fF74kG", 147 teamId: "team_42xaJiZMTw9Sr7i0DcLTae9d", 148 requestBody: { 149 name: domain, 150 }, 151 }); 152 } 153 await supabaseServerClient 154 .from("custom_domains") 155 .insert({ domain, confirmed: true, identity: null }); 156 157 await supabaseServerClient 158 .from("publication_domains") 159 .insert({ domain, publication: result.uri, identity: identity.atp_did }); 160 161 return { success: true, publication }; 162}