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