Highly ambitious ATProtocol AppView service and sdks
1/// <reference lib="deno.ns" />
2
3const API_URL = Deno.env.get("API_URL");
4const SLICE_URI = Deno.env.get("VITE_SLICE_URI");
5
6interface TokenInfo {
7 accessToken: string;
8 tokenType: string;
9}
10
11/**
12 * Initialize user profile by:
13 * 1. Checking if profile already exists
14 * 2. Syncing user collections (Bluesky data)
15 * 3. Fetching Bluesky profile
16 * 4. Creating network.slices.actor.profile with Bluesky data
17 */
18export async function initializeUserProfile(
19 userDid: string,
20 userHandle: string,
21 tokens: TokenInfo,
22): Promise<void> {
23 if (!API_URL || !SLICE_URI) {
24 console.error("Missing API_URL or VITE_SLICE_URI environment variables");
25 return;
26 }
27
28 try {
29 const graphqlUrl = `${API_URL}/graphql?slice=${
30 encodeURIComponent(SLICE_URI)
31 }`;
32 const authHeader = `${tokens.tokenType} ${tokens.accessToken}`;
33
34 // 1. Check if profile already exists
35 const checkQuery = `
36 query CheckProfile($did: String!) {
37 networkSlicesActorProfiles(where: { did: { eq: $did } }, first: 1) {
38 edges {
39 node {
40 id
41 }
42 }
43 }
44 }
45 `;
46
47 const checkResponse = await fetch(graphqlUrl, {
48 method: "POST",
49 headers: {
50 "Content-Type": "application/json",
51 "Authorization": authHeader,
52 },
53 body: JSON.stringify({
54 query: checkQuery,
55 variables: { did: userDid },
56 }),
57 });
58
59 if (!checkResponse.ok) {
60 throw new Error(`Profile check failed: ${checkResponse.statusText}`);
61 }
62
63 const checkData = await checkResponse.json();
64 const hasProfile =
65 (checkData?.data?.networkSlicesActorProfiles?.edges?.length ?? 0) > 0;
66
67 if (hasProfile) {
68 // User already has a profile, nothing to do
69 console.log("Profile already exists for", userDid);
70 return;
71 }
72
73 // 2. Sync user collections (to get Bluesky data)
74 const syncMutation = `
75 mutation SyncUserCollections($did: String!) {
76 syncUserCollections(did: $did) {
77 success
78 message
79 }
80 }
81 `;
82
83 const syncResponse = await fetch(graphqlUrl, {
84 method: "POST",
85 headers: {
86 "Content-Type": "application/json",
87 "Authorization": authHeader,
88 },
89 body: JSON.stringify({
90 query: syncMutation,
91 variables: { did: userDid },
92 }),
93 });
94
95 if (!syncResponse.ok) {
96 throw new Error(`Sync collections failed: ${syncResponse.statusText}`);
97 }
98
99 const syncResult = await syncResponse.json();
100 if (syncResult.errors) {
101 console.error("Sync collections errors:", syncResult.errors);
102 throw new Error("Failed to sync user collections");
103 }
104
105 // 3. Fetch Bluesky profile data
106 const bskyQuery = `
107 query GetBskyProfile($did: String!) {
108 appBskyActorProfiles(where: { did: { eq: $did } }, first: 1) {
109 edges {
110 node {
111 displayName
112 description
113 avatar {
114 ref
115 mimeType
116 size
117 }
118 }
119 }
120 }
121 }
122 `;
123
124 const bskyResponse = await fetch(graphqlUrl, {
125 method: "POST",
126 headers: {
127 "Content-Type": "application/json",
128 "Authorization": authHeader,
129 },
130 body: JSON.stringify({
131 query: bskyQuery,
132 variables: { did: userDid },
133 }),
134 });
135
136 if (!bskyResponse.ok) {
137 throw new Error(
138 `Fetch Bluesky profile failed: ${bskyResponse.statusText}`,
139 );
140 }
141
142 const bskyData = await bskyResponse.json();
143 const bskyProfile = bskyData?.data?.appBskyActorProfiles?.edges?.[0]?.node;
144
145 // 4. Create network.slices.actor.profile with Bluesky data
146 const profileInput: {
147 displayName: string;
148 description?: string;
149 avatar?: unknown;
150 createdAt: string;
151 } = {
152 displayName: bskyProfile?.displayName || userHandle || "User",
153 createdAt: new Date().toISOString(),
154 };
155
156 if (bskyProfile?.description) {
157 profileInput.description = bskyProfile.description;
158 }
159
160 if (
161 bskyProfile?.avatar?.ref &&
162 bskyProfile?.avatar?.mimeType &&
163 bskyProfile?.avatar?.size
164 ) {
165 // Reconstruct blob format for AT Protocol
166 profileInput.avatar = {
167 ref: bskyProfile.avatar.ref,
168 mimeType: bskyProfile.avatar.mimeType,
169 size: bskyProfile.avatar.size,
170 };
171 }
172
173 const createMutation = `
174 mutation CreateProfile(
175 $input: NetworkSlicesActorProfileInput!
176 $rkey: String
177 ) {
178 createNetworkSlicesActorProfile(input: $input, rkey: $rkey) {
179 id
180 }
181 }
182 `;
183
184 const createResponse = await fetch(graphqlUrl, {
185 method: "POST",
186 headers: {
187 "Content-Type": "application/json",
188 "Authorization": authHeader,
189 },
190 body: JSON.stringify({
191 query: createMutation,
192 variables: {
193 input: profileInput,
194 rkey: "self",
195 },
196 }),
197 });
198
199 if (!createResponse.ok) {
200 throw new Error(`Create profile failed: ${createResponse.statusText}`);
201 }
202
203 const createResult = await createResponse.json();
204 if (createResult.errors) {
205 console.error("Create profile errors:", createResult.errors);
206 throw new Error("Failed to create profile");
207 }
208
209 console.log("Successfully initialized profile for", userDid);
210 } catch (error) {
211 // Silent failure - user can set up profile manually later
212 console.error("Profile initialization failed:", error);
213 }
214}