+97
src/lib/atproto.ts
+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
+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
+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
+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
+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
+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
+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, "/");