forked from
atproto.fr/atproto.fr
Website of atproto.fr
1import "@atcute/atproto";
2import "@atcute/bluesky";
3import "./lexicons/fr/atproto/annuaire/etiquette";
4import "./lexicons/fr/atproto/annuaire/entree";
5import "./lexicons/fr/atproto/annuaire/suggestion";
6import "./lexicons/fr/atproto/annuaire/contributeur";
7import {
8 configureOAuth,
9 createAuthorizationUrl,
10 finalizeAuthorization,
11 getSession,
12 listStoredSessions,
13 deleteStoredSession,
14 OAuthUserAgent,
15} from "@atcute/oauth-browser-client";
16import {
17 CompositeDidDocumentResolver,
18 DohJsonHandleResolver,
19 LocalActorResolver,
20 PlcDidDocumentResolver,
21 WebDidDocumentResolver,
22} from "@atcute/identity-resolver";
23import { getPdsEndpoint } from "@atcute/identity";
24import { Client, simpleFetchHandler } from "@atcute/client";
25import type { Did, Handle } from "@atcute/lexicons/syntax";
26
27const publicClient = new Client({
28 handler: simpleFetchHandler({ service: "https://public.api.bsky.app" }),
29});
30
31const SCOPE = "atproto repo:fr.atproto.annuaire.suggestion repo:fr.atproto.annuaire.entree repo:fr.atproto.annuaire.etiquette repo:fr.atproto.annuaire.contributeur";
32const IS_LOCAL = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
33const REDIRECT_URI = IS_LOCAL
34 ? `http://127.0.0.1${window.location.port ? ":" + window.location.port : ""}/oauth/callback`
35 : `${window.location.origin}/oauth/callback`;
36const CLIENT_ID = IS_LOCAL
37 ? `http://localhost?redirect_uri=${encodeURIComponent(REDIRECT_URI)}&scope=${encodeURIComponent(SCOPE)}`
38 : `${window.location.origin}/client-metadata.json`;
39
40const didDocumentResolver = new CompositeDidDocumentResolver({
41 methods: {
42 plc: new PlcDidDocumentResolver(),
43 web: new WebDidDocumentResolver(),
44 },
45});
46
47const identityResolver = new LocalActorResolver({
48 handleResolver: new DohJsonHandleResolver({ dohUrl: "https://dns.google/resolve" }),
49 didDocumentResolver,
50});
51
52configureOAuth({
53 metadata: {
54 client_id: CLIENT_ID,
55 redirect_uri: REDIRECT_URI,
56 },
57 identityResolver,
58});
59
60export async function login(handle: string): Promise<void> {
61 // ATProto OAuth requires 127.0.0.1 for local dev; ensure we're on the
62 // same origin so sessionStorage (where the state is kept) matches.
63 if (window.location.hostname === "localhost") {
64 const url = new URL(window.location.href);
65 url.hostname = "127.0.0.1";
66 window.location.assign(url.toString());
67 return;
68 }
69
70 const authUrl = await createAuthorizationUrl({
71 target: { type: "account", identifier: handle as Handle },
72 scope: SCOPE,
73 });
74
75 window.location.assign(authUrl);
76}
77
78export async function handleCallback(): Promise<Did> {
79 // PDS may redirect with params in hash or query string
80 const hash = location.hash.slice(1);
81 const search = location.search.slice(1);
82 const params = new URLSearchParams(hash || search);
83 // scrub params from URL to prevent replay
84 history.replaceState(null, '', location.pathname);
85 const { session } = await finalizeAuthorization(params);
86 return session.info.sub;
87}
88
89export function getStoredDid(): Did | null {
90 const sessions = listStoredSessions();
91 return sessions.length > 0 ? sessions[0] : null;
92}
93
94export async function createClient(did: Did): Promise<Client> {
95 const session = await getSession(did);
96 const agent = new OAuthUserAgent(session);
97 return new Client({ handler: agent });
98}
99
100export function logout(did: Did): void {
101 deleteStoredSession(did);
102}
103
104export async function fetchProfile(_client: Client, did: Did) {
105 const res = await publicClient.get("app.bsky.actor.getProfile", {
106 params: { actor: did },
107 });
108 if (!res.ok) throw new Error("Failed to fetch profile");
109 return res.data;
110}
111
112export const ADMIN_DID: Did = "did:web:did.atproto.fr";
113
114export async function createEtiquette(client: Client, did: Did, nom: string, description: string) {
115 const res = await client.post("com.atproto.repo.createRecord", {
116 input: {
117 repo: did,
118 collection: "fr.atproto.annuaire.etiquette",
119 record: {
120 $type: "fr.atproto.annuaire.etiquette",
121 nom,
122 description,
123 },
124 },
125 });
126 if (!res.ok) throw new Error("Failed to create etiquette");
127 return res.data;
128}
129
130export async function listEtiquettes(client: Client, did: Did) {
131 const res = await client.get("com.atproto.repo.listRecords", {
132 params: {
133 repo: did,
134 collection: "fr.atproto.annuaire.etiquette",
135 },
136 });
137 if (!res.ok) throw new Error("Failed to list etiquettes");
138 return res.data;
139}
140
141export async function updateEtiquette(client: Client, did: Did, rkey: string, nom: string, description: string) {
142 const res = await client.post("com.atproto.repo.putRecord", {
143 input: {
144 repo: did,
145 collection: "fr.atproto.annuaire.etiquette",
146 rkey,
147 record: {
148 $type: "fr.atproto.annuaire.etiquette",
149 nom,
150 description,
151 },
152 },
153 });
154 if (!res.ok) throw new Error("Failed to update etiquette");
155 return res.data;
156}
157
158export async function createSuggestion(
159 client: Client,
160 repo: Did,
161 did: string,
162 etiquettes: { uri: string; cid: string }[],
163) {
164 const res = await client.post("com.atproto.repo.createRecord", {
165 input: {
166 repo,
167 collection: "fr.atproto.annuaire.suggestion",
168 record: {
169 $type: "fr.atproto.annuaire.suggestion",
170 did,
171 etiquettes,
172 },
173 },
174 });
175 if (!res.ok) throw new Error("Failed to create suggestion");
176 return res.data;
177}
178
179export async function listSuggestions(client: Client, did: Did) {
180 const res = await client.get("com.atproto.repo.listRecords", {
181 params: {
182 repo: did,
183 collection: "fr.atproto.annuaire.suggestion",
184 },
185 });
186 if (!res.ok) throw new Error("Failed to list suggestions");
187 return res.data;
188}
189
190export async function createEntree(
191 client: Client,
192 repo: Did,
193 did: string,
194 etiquettes: { uri: string; cid: string }[],
195) {
196 const res = await client.post("com.atproto.repo.createRecord", {
197 input: {
198 repo,
199 collection: "fr.atproto.annuaire.entree",
200 record: {
201 $type: "fr.atproto.annuaire.entree",
202 did,
203 etiquettes,
204 },
205 },
206 });
207 if (!res.ok) throw new Error("Failed to create entree");
208 return res.data;
209}
210
211export async function listEntrees(client: Client, did: Did) {
212 const res = await client.get("com.atproto.repo.listRecords", {
213 params: {
214 repo: did,
215 collection: "fr.atproto.annuaire.entree",
216 limit: 100,
217 },
218 });
219 if (!res.ok) throw new Error("Failed to list entrees");
220 return res.data;
221}
222
223export async function updateEntree(
224 client: Client,
225 repo: Did,
226 rkey: string,
227 did: string,
228 etiquettes: { uri: string; cid: string }[],
229) {
230 const res = await client.post("com.atproto.repo.putRecord", {
231 input: {
232 repo,
233 collection: "fr.atproto.annuaire.entree",
234 rkey,
235 record: {
236 $type: "fr.atproto.annuaire.entree",
237 did,
238 etiquettes,
239 },
240 },
241 });
242 if (!res.ok) throw new Error("Failed to update entree");
243 return res.data;
244}
245
246export async function deleteEntree(client: Client, repo: Did, rkey: string) {
247 const res = await client.post("com.atproto.repo.deleteRecord", {
248 input: {
249 repo,
250 collection: "fr.atproto.annuaire.entree",
251 rkey,
252 },
253 });
254 if (!res.ok) throw new Error("Failed to delete entree");
255 return res.data;
256}
257
258export async function deleteSuggestion(client: Client, repo: Did, rkey: string) {
259 const res = await client.post("com.atproto.repo.deleteRecord", {
260 input: {
261 repo,
262 collection: "fr.atproto.annuaire.suggestion",
263 rkey,
264 },
265 });
266 if (!res.ok) throw new Error("Failed to delete suggestion");
267 return res.data;
268}
269
270export async function resolveHandle(handle: string): Promise<Did> {
271 const res = await publicClient.get("com.atproto.identity.resolveHandle", {
272 params: { handle: handle as Handle },
273 });
274 if (!res.ok) throw new Error("Failed to resolve handle: " + handle);
275 return res.data.did;
276}
277
278export async function fetchPublicProfile(actor: string) {
279 const res = await publicClient.get("app.bsky.actor.getProfile", {
280 params: { actor: actor as Did },
281 });
282 if (!res.ok) throw new Error("Failed to fetch profile");
283 return res.data;
284}
285
286export async function fetchRecord(repo: string, collection: string, rkey: string) {
287 const res = await publicClient.get("com.atproto.repo.getRecord", {
288 params: {
289 repo: repo as Did,
290 collection: collection as `${string}.${string}.${string}`,
291 rkey,
292 },
293 });
294 if (!res.ok) throw new Error("Failed to fetch record");
295 return res.data;
296}
297
298export async function listContributeurs(client: Client, did: Did) {
299 const res = await client.get("com.atproto.repo.listRecords", {
300 params: {
301 repo: did,
302 collection: "fr.atproto.annuaire.contributeur",
303 limit: 100,
304 },
305 });
306 if (!res.ok) throw new Error("Failed to list contributeurs");
307 return res.data;
308}
309
310export async function createOrIncrementContributeur(client: Client, repo: Did, contributeurDid: string) {
311 // List existing contributeur records to find one matching this DID
312 const existing = await listContributeurs(client, repo);
313 const found = existing.records.find(
314 (r) => (r.value as { did: string; score: number }).did === contributeurDid,
315 );
316
317 if (found) {
318 // Increment score via putRecord
319 const val = found.value as { did: string; score: number };
320 const rkey = found.uri.split("/").pop()!;
321 const res = await client.post("com.atproto.repo.putRecord", {
322 input: {
323 repo,
324 collection: "fr.atproto.annuaire.contributeur",
325 rkey,
326 record: {
327 $type: "fr.atproto.annuaire.contributeur",
328 did: contributeurDid,
329 score: val.score + 1,
330 },
331 },
332 });
333 if (!res.ok) throw new Error("Failed to update contributeur");
334 return res.data;
335 } else {
336 // Create new record with score 1
337 const res = await client.post("com.atproto.repo.createRecord", {
338 input: {
339 repo,
340 collection: "fr.atproto.annuaire.contributeur",
341 record: {
342 $type: "fr.atproto.annuaire.contributeur",
343 did: contributeurDid,
344 score: 1,
345 },
346 },
347 });
348 if (!res.ok) throw new Error("Failed to create contributeur");
349 return res.data;
350 }
351}
352
353export async function fetchRecordFromPds(repo: string, collection: string, rkey: string) {
354 const didDoc = await didDocumentResolver.resolve(repo as `did:plc:${string}`);
355 const pdsUrl = getPdsEndpoint(didDoc);
356 if (!pdsUrl) throw new Error("No PDS endpoint found for " + repo);
357
358 const pdsClient = new Client({
359 handler: simpleFetchHandler({ service: pdsUrl }),
360 });
361
362 const res = await pdsClient.get("com.atproto.repo.getRecord", {
363 params: {
364 repo: repo as Did,
365 collection: collection as `${string}.${string}.${string}`,
366 rkey,
367 },
368 });
369 if (!res.ok) throw new Error("Failed to fetch record from PDS");
370 return res.data;
371}