serve a static website from your pds

Create class for API requests

Changed files
+122 -79
src
+97
src/lib/atproto.ts
··· 1 + import { isXRPCErrorPayload, type Client } from "@atcute/client"; 2 + import type {} from "@atcute/atproto"; 3 + import type {} from "@atcute/bluesky"; 4 + import type { Did } from "@atcute/lexicons"; 5 + 6 + export interface Bundle { 7 + description?: string; 8 + assets: Record<string, { $type: "blob"; ref: { $link: string }; mimeType: string; size: number }>; 9 + createdAt: string; 10 + } 11 + 12 + export default class ATProto { 13 + #did: Did; 14 + #client: Client; 15 + 16 + constructor(did: Did, client: Client) { 17 + this.#did = did; 18 + this.#client = client; 19 + } 20 + 21 + async getProfile(did: Did) { 22 + const { data } = await this.#client.get("app.bsky.actor.getProfile", { 23 + params: { actor: did }, 24 + }); 25 + 26 + if (isXRPCErrorPayload(data)) throw new Error(data.error); 27 + return data; 28 + } 29 + 30 + async listBundles() { 31 + const { data } = await this.#client.get("com.atproto.repo.listRecords", { 32 + params: { repo: this.#did, collection: "com.jakelazaroff.test" }, 33 + }); 34 + 35 + if (isXRPCErrorPayload(data)) throw new Error("couldn't load records"); 36 + return data; 37 + } 38 + 39 + async createBundle(rkey: string) { 40 + const record = { 41 + assets: {}, 42 + createdAt: new Date().toISOString(), 43 + }; 44 + 45 + const { data } = await this.#client.post("com.atproto.repo.createRecord", { 46 + input: { repo: this.#did, collection: "com.jakelazaroff.test", rkey, record }, 47 + }); 48 + 49 + if (isXRPCErrorPayload(data)) throw new Error(data.error); 50 + } 51 + 52 + async getBundle(rkey: string) { 53 + const { data } = await this.#client.get("com.atproto.repo.getRecord", { 54 + params: { repo: this.#did, collection: "com.jakelazaroff.test", rkey }, 55 + }); 56 + 57 + if (isXRPCErrorPayload(data)) throw new Error("couldn't load records"); 58 + return data; 59 + } 60 + 61 + async updateBundle(rkey: string, description: string, files: File[]) { 62 + const bundle: Bundle = { description, assets: {}, createdAt: new Date().toISOString() }; 63 + 64 + for (const file of files) { 65 + if (!(file instanceof File)) continue; 66 + 67 + const { data } = await this.#client.post("com.atproto.repo.uploadBlob", { input: file }); 68 + if (isXRPCErrorPayload(data)) throw new Error("couldn't upload file"); 69 + 70 + const filepath = file.webkitRelativePath?.replace(/^.+\//, "") ?? file.name; 71 + bundle.assets[filepath] = { 72 + $type: "blob", 73 + ref: data.blob.ref, 74 + mimeType: file.type, 75 + size: file.size, 76 + }; 77 + } 78 + 79 + const { data } = await this.#client.post("com.atproto.repo.putRecord", { 80 + input: { 81 + repo: this.#did, 82 + collection: "com.jakelazaroff.test", 83 + rkey, 84 + record: bundle as any, 85 + }, 86 + }); 87 + if (isXRPCErrorPayload(data)) throw new Error("couldn't deploy"); 88 + } 89 + 90 + async deleteBundle(rkey: string) { 91 + const { data } = await this.#client.post("com.atproto.repo.deleteRecord", { 92 + input: { repo: this.#did, collection: "com.jakelazaroff.test", rkey }, 93 + }); 94 + 95 + if (isXRPCErrorPayload(data)) throw new Error(data.error); 96 + } 97 + }
+5 -1
src/lib/oauth.ts
··· 3 3 import { Client } from "@atcute/client"; 4 4 import { configureOAuth, OAuthUserAgent, type Session } from "@atcute/oauth-browser-client"; 5 5 6 + import ATProto from "./atproto"; 7 + 6 8 export const CLIENT_ID = `${PUBLIC_HOST}/client-metadata.json`; 7 9 export const REDIRECT_URI = `${PUBLIC_REDIRECT_HOST || PUBLIC_HOST}/oauth/callback`; 8 10 ··· 12 14 13 15 export function client(session: Session) { 14 16 const handler = new OAuthUserAgent(session); 15 - return new Client({ handler }); 17 + const client = new Client({ handler }); 18 + 19 + return new ATProto(session.info.sub, client); 16 20 }
+5 -12
src/routes/~/+layout.ts
··· 1 - import type {} from "@atcute/bluesky"; 2 - import { Client, isXRPCErrorPayload } from "@atcute/client"; 3 - import { getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; 1 + import { getSession } from "@atcute/oauth-browser-client"; 4 2 5 - import { configure } from "~/lib/oauth"; 3 + import { client, configure } from "~/lib/oauth"; 6 4 7 5 import type { LayoutLoad } from "./$types"; 8 6 import { redirect } from "@sveltejs/kit"; ··· 14 12 const did = localStorage.getItem("did") as any; 15 13 const session = await getSession(did); 16 14 17 - const handler = new OAuthUserAgent(session); 18 - const rpc = new Client({ handler }); 15 + const atp = client(session); 19 16 20 - const { data } = await rpc.get("app.bsky.actor.getProfile", { 21 - params: { actor: did }, 22 - }); 23 - 24 - if (isXRPCErrorPayload(data)) throw new Error("couldn't load profile"); 17 + const { displayName } = await atp.getProfile(did); 25 18 26 - return { session, pds: session.info.aud, did, name: data.displayName }; 19 + return { session, pds: session.info.aud, did, name: displayName }; 27 20 } catch (e) { 28 21 console.error(e); 29 22 redirect(303, "/");
+3 -13
src/routes/~/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { invalidate } from "$app/navigation"; 3 3 4 - import type {} from "@atcute/atproto"; 5 - import { Client } from "@atcute/client"; 6 - import { OAuthUserAgent } from "@atcute/oauth-browser-client"; 4 + import { client } from "~/lib/oauth"; 7 5 8 6 let { data } = $props(); 9 7 10 - const handler = new OAuthUserAgent(data.session); 11 - const rpc = new Client({ handler }); 8 + const atp = client(data.session); 12 9 13 10 async function createWebsite(e: SubmitEvent & { currentTarget: HTMLFormElement }) { 14 11 e.preventDefault(); ··· 18 15 const rkey = formdata.get("rkey"); 19 16 if (typeof rkey !== "string") throw new Error("invalid rkey"); 20 17 21 - const record = { 22 - assets: [], 23 - createdAt: new Date().toISOString(), 24 - }; 25 - 26 - await rpc.post("com.atproto.repo.createRecord", { 27 - input: { repo: data.did, collection: "com.jakelazaroff.test", rkey, record }, 28 - }); 18 + await atp.createBundle(rkey); 29 19 await invalidate("collection:com.jakelazaroff.test"); 30 20 form.reset(); 31 21 }
+4 -10
src/routes/~/+page.ts
··· 2 2 import { Client, isXRPCErrorPayload } from "@atcute/client"; 3 3 import { OAuthUserAgent } from "@atcute/oauth-browser-client"; 4 4 5 - import { configure } from "~/lib/oauth"; 5 + import { client, configure } from "~/lib/oauth"; 6 6 7 7 import type { PageLoad } from "./$types"; 8 8 import { redirect } from "@sveltejs/kit"; ··· 15 15 try { 16 16 const { did, session } = await parent(); 17 17 18 - const handler = new OAuthUserAgent(session); 19 - const rpc = new Client({ handler }); 20 - 21 - const { data } = await rpc.get("com.atproto.repo.listRecords", { 22 - params: { repo: did, collection: "com.jakelazaroff.test" }, 23 - }); 24 - 25 - if (isXRPCErrorPayload(data)) throw new Error("couldn't load records"); 18 + const atp = client(session); 19 + const { records } = await atp.listBundles(); 26 20 27 - return { records: data.records }; 21 + return { records }; 28 22 } catch (e) { 29 23 console.error(e); 30 24 redirect(303, "/");
+4 -34
src/routes/~/sites/[name]/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { goto, invalidate } from "$app/navigation"; 3 3 4 - import type {} from "@atcute/atproto"; 5 - import { isXRPCErrorPayload } from "@atcute/client"; 6 - 7 4 import { client } from "~/lib/oauth"; 8 5 9 6 let { params, data } = $props(); 10 7 11 - const rpc = client(data.session); 8 + const atp = client(data.session); 12 9 13 10 async function deleteBundle(e: SubmitEvent & { currentTarget: HTMLFormElement }) { 14 11 e.preventDefault(); ··· 18 15 const rkey = formdata.get("rkey"); 19 16 if (typeof rkey !== "string") throw new Error("invalid rkey"); 20 17 21 - await rpc.post("com.atproto.repo.deleteRecord", { 22 - input: { repo: data.did, collection: "com.jakelazaroff.test", rkey }, 23 - }); 18 + await atp.deleteBundle(rkey); 24 19 goto("/~/"); 25 20 } 26 21 ··· 44 39 let description = formdata.get("description"); 45 40 if (typeof description !== "string" || !description) description = "Uploaded on website"; 46 41 47 - const files = formdata.getAll("files"); 48 - const bundle: Bundle = { description, assets: {}, createdAt: new Date().toISOString() }; 49 - for (const file of files) { 50 - if (!(file instanceof File)) continue; 51 - 52 - const { data } = await rpc.post("com.atproto.repo.uploadBlob", { input: file }); 53 - if (isXRPCErrorPayload(data)) throw new Error("couldn't upload file"); 54 - 55 - const filepath = file.webkitRelativePath?.replace(/^.+\//, "") ?? file.name; 56 - bundle.assets[filepath] = { 57 - $type: "blob", 58 - ref: data.blob.ref, 59 - mimeType: file.type, 60 - size: file.size, 61 - }; 62 - } 63 - 64 - await rpc.post("com.atproto.repo.putRecord", { 65 - input: { 66 - repo: data.did, 67 - collection: "com.jakelazaroff.test", 68 - rkey, 69 - record: bundle as any, 70 - }, 71 - }); 72 - if (isXRPCErrorPayload(data)) throw new Error("couldn't deploy"); 73 - 42 + const files = formdata.getAll("files").filter(entry => entry instanceof File); 43 + await atp.updateBundle(rkey, description, files); 74 44 invalidate(`rkey:${rkey}`); 75 45 form.reset(); 76 46 }
+4 -9
src/routes/~/sites/[name]/+page.ts
··· 12 12 depends(`rkey:${params.name}`); 13 13 14 14 try { 15 - const { did, session } = await parent(); 15 + const { session } = await parent(); 16 16 17 - const rpc = client(session); 17 + const atp = client(session); 18 + const record = await atp.getBundle(params.name); 18 19 19 - const { data } = await rpc.get("com.atproto.repo.getRecord", { 20 - params: { repo: did, collection: "com.jakelazaroff.test", rkey: params.name }, 21 - }); 22 - 23 - if (isXRPCErrorPayload(data)) throw new Error("couldn't load records"); 24 - 25 - return { record: data }; 20 + return { record }; 26 21 } catch (e) { 27 22 console.error(e); 28 23 redirect(303, "/");