at://Press
1import type { APIRoute } from "astro";
2import { checkOrigin, checkAuth, parseJsonBody, createPdsSession } from "../../lib/api";
3import { isValidRkey, invalidateEntry, invalidateCache } from "../../lib/pds";
4import { getDraft, saveDraft, deleteDraft } from "../../lib/drafts";
5import { PDS_URL, DID, BLOG_COLLECTION, MAX_TITLE_LENGTH, MAX_CONTENT_LENGTH } from "../../lib/constants";
6const VALID_VISIBILITY = ["public", "author"];
7
8export const POST: APIRoute = async ({ request, cookies }) => {
9 const originErr = checkOrigin(request);
10 if (originErr) return originErr;
11
12 const authErr = checkAuth(cookies);
13 if (authErr) return authErr;
14
15 const [body, parseErr] = await parseJsonBody(request);
16 if (parseErr) return parseErr;
17
18 const rkey = typeof body.rkey === "string" ? body.rkey : "";
19 const title = typeof body.title === "string" ? body.title.trim() : "";
20 const content = typeof body.content === "string" ? body.content.trim() : "";
21 const visibility = typeof body.visibility === "string" ? body.visibility : "";
22 const rawCreatedAt = typeof body.createdAt === "string" ? body.createdAt : "";
23 const createdAt = rawCreatedAt && !isNaN(Date.parse(rawCreatedAt))
24 ? new Date(rawCreatedAt).toISOString()
25 : new Date().toISOString();
26
27 if (!rkey || !isValidRkey(rkey)) {
28 return new Response(JSON.stringify({ error: "Invalid rkey" }), { status: 400 });
29 }
30
31 if (!title || title.length > MAX_TITLE_LENGTH) {
32 return new Response(
33 JSON.stringify({ error: `Title is required and must be under ${MAX_TITLE_LENGTH} characters` }),
34 { status: 400 }
35 );
36 }
37
38 if (!content || content.length > MAX_CONTENT_LENGTH) {
39 return new Response(
40 JSON.stringify({ error: `Content is required and must be under ${MAX_CONTENT_LENGTH} characters` }),
41 { status: 400 }
42 );
43 }
44
45 if (!VALID_VISIBILITY.includes(visibility)) {
46 return new Response(
47 JSON.stringify({ error: "Visibility must be 'public' or 'author'" }),
48 { status: 400 }
49 );
50 }
51
52 const blobs = Array.isArray(body.blobs) ? body.blobs : [];
53 const existingDraft = getDraft(rkey);
54
55 if (existingDraft) {
56 // This rkey is a local SQLite draft
57 if (visibility === "author") {
58 // Draft → Draft: update in SQLite only
59 saveDraft({
60 rkey,
61 title,
62 content,
63 createdAt,
64 blobs: blobs.length > 0 ? blobs : undefined,
65 });
66 return new Response(JSON.stringify({ success: true, rkey }));
67 }
68
69 // Draft → Publish: move from SQLite to PDS
70 const [accessJwt, sessionErr] = await createPdsSession();
71 if (sessionErr) return sessionErr;
72
73 const putRes = await fetch(
74 `${PDS_URL}/xrpc/com.atproto.repo.putRecord`,
75 {
76 method: "POST",
77 headers: {
78 "Content-Type": "application/json",
79 Authorization: `Bearer ${accessJwt}`,
80 },
81 body: JSON.stringify({
82 repo: DID,
83 collection: BLOG_COLLECTION,
84 rkey,
85 record: {
86 $type: BLOG_COLLECTION,
87 title,
88 content,
89 createdAt,
90 visibility: "public",
91 ...(blobs.length > 0 && { blobs }),
92 },
93 }),
94 }
95 );
96
97 if (!putRes.ok) {
98 const err = await putRes.text();
99 console.error("PDS putRecord (publish draft) failed:", err);
100 return new Response(
101 JSON.stringify({ error: "Failed to publish" }),
102 { status: 500 }
103 );
104 }
105
106 deleteDraft(rkey);
107 invalidateCache();
108 return new Response(JSON.stringify({ success: true, rkey }));
109 }
110
111 // This rkey is a PDS record
112 if (visibility === "author") {
113 // Published → Unpublish: move from PDS to SQLite
114 saveDraft({
115 rkey,
116 title,
117 content,
118 createdAt,
119 blobs: blobs.length > 0 ? blobs : undefined,
120 });
121
122 const [accessJwt, sessionErr] = await createPdsSession();
123 if (sessionErr) return sessionErr;
124
125 const deleteRes = await fetch(
126 `${PDS_URL}/xrpc/com.atproto.repo.deleteRecord`,
127 {
128 method: "POST",
129 headers: {
130 "Content-Type": "application/json",
131 Authorization: `Bearer ${accessJwt}`,
132 },
133 body: JSON.stringify({
134 repo: DID,
135 collection: BLOG_COLLECTION,
136 rkey,
137 }),
138 }
139 );
140
141 if (!deleteRes.ok) {
142 console.warn("PDS deleteRecord (unpublish) failed:", deleteRes.status);
143 }
144
145 invalidateEntry(rkey);
146 return new Response(JSON.stringify({ success: true, rkey }));
147 }
148
149 // Published → Published: update on PDS (existing behavior)
150 const [accessJwt, sessionErr] = await createPdsSession();
151 if (sessionErr) return sessionErr;
152
153 const putRes = await fetch(
154 `${PDS_URL}/xrpc/com.atproto.repo.putRecord`,
155 {
156 method: "POST",
157 headers: {
158 "Content-Type": "application/json",
159 Authorization: `Bearer ${accessJwt}`,
160 },
161 body: JSON.stringify({
162 repo: DID,
163 collection: BLOG_COLLECTION,
164 rkey,
165 record: {
166 $type: BLOG_COLLECTION,
167 title,
168 content,
169 createdAt,
170 visibility,
171 ...(blobs.length > 0 && { blobs }),
172 },
173 }),
174 }
175 );
176
177 if (!putRes.ok) {
178 const err = await putRes.text();
179 console.error("PDS putRecord failed:", err);
180 return new Response(
181 JSON.stringify({ error: "Failed to update entry" }),
182 { status: 500 }
183 );
184 }
185
186 invalidateEntry(rkey);
187
188 return new Response(JSON.stringify({ success: true, rkey }));
189};