Website of atproto.fr
at main 371 lines 11 kB view raw
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}