a tool for shared writing and social publishing
1"use server";
2
3import { AtpBaseClient } from "lexicons/api";
4import { AppBskyActorDefs, Agent as BskyAgent } from "@atproto/api";
5import { getIdentityData } from "actions/getIdentityData";
6import { createOauthClient } from "src/atproto-oauth";
7import { TID } from "@atproto/common";
8import { supabaseServerClient } from "supabase/serverClient";
9import { revalidatePath } from "next/cache";
10import { AtUri } from "@atproto/syntax";
11import { redirect } from "next/navigation";
12import { encodeActionToSearchParam } from "app/api/oauth/[route]/afterSignInActions";
13import { Json } from "supabase/database.types";
14import { IdResolver } from "@atproto/identity";
15
16let leafletFeedURI =
17 "at://did:plc:btxrwcaeyodrap5mnjw2fvmz/app.bsky.feed.generator/subscribedPublications";
18let idResolver = new IdResolver();
19export async function subscribeToPublication(
20 publication: string,
21 redirectRoute?: string,
22) {
23 const oauthClient = await createOauthClient();
24 let identity = await getIdentityData();
25 if (!identity || !identity.atp_did) {
26 return redirect(
27 `/api/oauth/login?redirect_url=${redirectRoute}&action=${encodeActionToSearchParam({ action: "subscribe", publication })}`,
28 );
29 }
30
31 let credentialSession = await oauthClient.restore(identity.atp_did);
32 let agent = new AtpBaseClient(
33 credentialSession.fetchHandler.bind(credentialSession),
34 );
35 let record = await agent.pub.leaflet.graph.subscription.create(
36 { repo: credentialSession.did!, rkey: TID.nextStr() },
37 {
38 publication,
39 },
40 );
41 let { error } = await supabaseServerClient
42 .from("publication_subscriptions")
43 .insert({
44 uri: record.uri,
45 record,
46 publication,
47 identity: credentialSession.did!,
48 });
49 let bsky = new BskyAgent(credentialSession);
50 let [prefs, profile, resolveDid] = await Promise.all([
51 bsky.app.bsky.actor.getPreferences(),
52 bsky.app.bsky.actor.profile
53 .get({
54 repo: credentialSession.did!,
55 rkey: "self",
56 })
57 .catch(),
58 idResolver.did.resolve(credentialSession.did!),
59 ]);
60 if (!identity.bsky_profiles && profile.value) {
61 await supabaseServerClient.from("bsky_profiles").insert({
62 did: identity.atp_did,
63 record: profile.value as Json,
64 handle: resolveDid?.alsoKnownAs?.[0]?.slice(5),
65 });
66 }
67 let savedFeeds = prefs.data.preferences.find(
68 (pref) => pref.$type === "app.bsky.actor.defs#savedFeedsPrefV2",
69 ) as AppBskyActorDefs.SavedFeedsPrefV2;
70 revalidatePath("/lish/[did]/[publication]", "layout");
71 return {
72 hasFeed: !!savedFeeds.items.find((feed) => feed.value === leafletFeedURI),
73 };
74}
75
76export async function unsubscribeToPublication(publication: string) {
77 const oauthClient = await createOauthClient();
78 let identity = await getIdentityData();
79 if (!identity || !identity.atp_did) return;
80
81 let credentialSession = await oauthClient.restore(identity.atp_did);
82 let agent = new AtpBaseClient(
83 credentialSession.fetchHandler.bind(credentialSession),
84 );
85 let { data: existingSubscription } = await supabaseServerClient
86 .from("publication_subscriptions")
87 .select("*")
88 .eq("identity", identity.atp_did)
89 .eq("publication", publication)
90 .single();
91 if (!existingSubscription) return;
92 await agent.pub.leaflet.graph.subscription.delete({
93 repo: credentialSession.did!,
94 rkey: new AtUri(existingSubscription.uri).rkey,
95 });
96 await supabaseServerClient
97 .from("publication_subscriptions")
98 .delete()
99 .eq("identity", identity.atp_did)
100 .eq("publication", publication);
101 revalidatePath("/lish/[did]/[publication]", "layout");
102}