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";
15import {
16 Notification,
17 pingIdentityToUpdateNotification,
18} from "src/notifications";
19import { v7 } from "uuid";
20
21let leafletFeedURI =
22 "at://did:plc:btxrwcaeyodrap5mnjw2fvmz/app.bsky.feed.generator/subscribedPublications";
23let idResolver = new IdResolver();
24export async function subscribeToPublication(
25 publication: string,
26 redirectRoute?: string,
27) {
28 const oauthClient = await createOauthClient();
29 let identity = await getIdentityData();
30 if (!identity || !identity.atp_did) {
31 return redirect(
32 `/api/oauth/login?redirect_url=${redirectRoute}&action=${encodeActionToSearchParam({ action: "subscribe", publication })}`,
33 );
34 }
35
36 let credentialSession = await oauthClient.restore(identity.atp_did);
37 let agent = new AtpBaseClient(
38 credentialSession.fetchHandler.bind(credentialSession),
39 );
40 let record = await agent.pub.leaflet.graph.subscription.create(
41 { repo: credentialSession.did!, rkey: TID.nextStr() },
42 {
43 publication,
44 },
45 );
46 let { error } = await supabaseServerClient
47 .from("publication_subscriptions")
48 .insert({
49 uri: record.uri,
50 record,
51 publication,
52 identity: credentialSession.did!,
53 });
54
55 // Create notification for the publication owner
56 let publicationOwner = new AtUri(publication).host;
57 if (publicationOwner !== credentialSession.did) {
58 let notification: Notification = {
59 id: v7(),
60 recipient: publicationOwner,
61 data: {
62 type: "subscribe",
63 subscription_uri: record.uri,
64 },
65 };
66 await supabaseServerClient.from("notifications").insert(notification);
67 await pingIdentityToUpdateNotification(publicationOwner);
68 }
69
70 let bsky = new BskyAgent(credentialSession);
71 let [prefs, profile, resolveDid] = await Promise.all([
72 bsky.app.bsky.actor.getPreferences(),
73 bsky.app.bsky.actor.profile
74 .get({
75 repo: credentialSession.did!,
76 rkey: "self",
77 })
78 .catch(),
79 idResolver.did.resolve(credentialSession.did!),
80 ]);
81 if (!identity.bsky_profiles && profile.value) {
82 await supabaseServerClient.from("bsky_profiles").insert({
83 did: identity.atp_did,
84 record: profile.value as Json,
85 handle: resolveDid?.alsoKnownAs?.[0]?.slice(5),
86 });
87 }
88 let savedFeeds = prefs.data.preferences.find(
89 (pref) => pref.$type === "app.bsky.actor.defs#savedFeedsPrefV2",
90 ) as AppBskyActorDefs.SavedFeedsPrefV2;
91 revalidatePath("/lish/[did]/[publication]", "layout");
92 return {
93 hasFeed: !!savedFeeds.items.find((feed) => feed.value === leafletFeedURI),
94 };
95}
96
97export async function unsubscribeToPublication(publication: string) {
98 const oauthClient = await createOauthClient();
99 let identity = await getIdentityData();
100 if (!identity || !identity.atp_did) return;
101
102 let credentialSession = await oauthClient.restore(identity.atp_did);
103 let agent = new AtpBaseClient(
104 credentialSession.fetchHandler.bind(credentialSession),
105 );
106 let { data: existingSubscription } = await supabaseServerClient
107 .from("publication_subscriptions")
108 .select("*")
109 .eq("identity", identity.atp_did)
110 .eq("publication", publication)
111 .single();
112 if (!existingSubscription) return;
113 await agent.pub.leaflet.graph.subscription.delete({
114 repo: credentialSession.did!,
115 rkey: new AtUri(existingSubscription.uri).rkey,
116 });
117 await supabaseServerClient
118 .from("publication_subscriptions")
119 .delete()
120 .eq("identity", identity.atp_did)
121 .eq("publication", publication);
122 revalidatePath("/lish/[did]/[publication]", "layout");
123}