Write on the margins of the internet. Powered by the AT Protocol.
at main 494 lines 14 kB view raw
1const API_BASE = "/api"; 2const AUTH_BASE = "/auth"; 3 4async function request(endpoint, options = {}) { 5 const response = await fetch(endpoint, { 6 credentials: "include", 7 headers: { 8 "Content-Type": "application/json", 9 ...options.headers, 10 }, 11 ...options, 12 }); 13 14 if (!response.ok) { 15 const error = await response.text(); 16 throw new Error(error || `HTTP ${response.status}`); 17 } 18 19 return response.json(); 20} 21 22export async function getURLMetadata(url) { 23 return request(`${API_BASE}/url-metadata?url=${encodeURIComponent(url)}`); 24} 25 26export async function getAnnotationFeed( 27 limit = 50, 28 offset = 0, 29 tag = "", 30 creator = "", 31) { 32 let url = `${API_BASE}/annotations/feed?limit=${limit}&offset=${offset}`; 33 if (tag) url += `&tag=${encodeURIComponent(tag)}`; 34 if (creator) url += `&creator=${encodeURIComponent(creator)}`; 35 return request(url); 36} 37 38export async function getAnnotations({ 39 source, 40 motivation, 41 limit = 50, 42 offset = 0, 43} = {}) { 44 let url = `${API_BASE}/annotations?limit=${limit}&offset=${offset}`; 45 if (source) url += `&source=${encodeURIComponent(source)}`; 46 if (motivation) url += `&motivation=${motivation}`; 47 return request(url); 48} 49 50export async function getByTarget(source, limit = 50, offset = 0) { 51 return request( 52 `${API_BASE}/targets?source=${encodeURIComponent(source)}&limit=${limit}&offset=${offset}`, 53 ); 54} 55 56export async function getAnnotation(uri) { 57 return request(`${API_BASE}/annotation?uri=${encodeURIComponent(uri)}`); 58} 59 60export async function getProfile(did) { 61 return request(`${API_BASE}/profile/${encodeURIComponent(did)}`); 62} 63 64export async function getUserAnnotations(did, limit = 50, offset = 0) { 65 return request( 66 `${API_BASE}/users/${encodeURIComponent(did)}/annotations?limit=${limit}&offset=${offset}`, 67 ); 68} 69 70export async function getUserHighlights(did, limit = 50, offset = 0) { 71 return request( 72 `${API_BASE}/users/${encodeURIComponent(did)}/highlights?limit=${limit}&offset=${offset}`, 73 ); 74} 75 76export async function getUserBookmarks(did, limit = 50, offset = 0) { 77 return request( 78 `${API_BASE}/users/${encodeURIComponent(did)}/bookmarks?limit=${limit}&offset=${offset}`, 79 ); 80} 81 82export async function getUserTargetItems(did, url, limit = 50, offset = 0) { 83 return request( 84 `${API_BASE}/users/${encodeURIComponent(did)}/targets?source=${encodeURIComponent(url)}&limit=${limit}&offset=${offset}`, 85 ); 86} 87 88export async function getHighlights(creatorDid, limit = 50, offset = 0) { 89 return request( 90 `${API_BASE}/highlights?creator=${encodeURIComponent(creatorDid)}&limit=${limit}&offset=${offset}`, 91 ); 92} 93 94export async function getBookmarks(creatorDid, limit = 50, offset = 0) { 95 return request( 96 `${API_BASE}/bookmarks?creator=${encodeURIComponent(creatorDid)}&limit=${limit}&offset=${offset}`, 97 ); 98} 99 100export async function getReplies(annotationUri) { 101 return request( 102 `${API_BASE}/replies?uri=${encodeURIComponent(annotationUri)}`, 103 ); 104} 105 106export async function updateAnnotation(uri, text, tags) { 107 return request(`${API_BASE}/annotations?uri=${encodeURIComponent(uri)}`, { 108 method: "PUT", 109 body: JSON.stringify({ text, tags }), 110 }); 111} 112 113export async function updateHighlight(uri, color, tags) { 114 return request(`${API_BASE}/highlights?uri=${encodeURIComponent(uri)}`, { 115 method: "PUT", 116 body: JSON.stringify({ color, tags }), 117 }); 118} 119 120export async function createBookmark(url, title, description) { 121 return request(`${API_BASE}/bookmarks`, { 122 method: "POST", 123 body: JSON.stringify({ url, title, description }), 124 }); 125} 126 127export async function updateBookmark(uri, title, description, tags) { 128 return request(`${API_BASE}/bookmarks?uri=${encodeURIComponent(uri)}`, { 129 method: "PUT", 130 body: JSON.stringify({ title, description, tags }), 131 }); 132} 133 134export async function getCollections(did) { 135 let url = `${API_BASE}/collections`; 136 if (did) url += `?author=${encodeURIComponent(did)}`; 137 return request(url); 138} 139 140export async function getCollectionsContaining(annotationUri) { 141 return request( 142 `${API_BASE}/collections/containing?uri=${encodeURIComponent(annotationUri)}`, 143 ); 144} 145 146export async function getEditHistory(uri) { 147 return request( 148 `${API_BASE}/annotations/history?uri=${encodeURIComponent(uri)}`, 149 ); 150} 151 152export async function getNotifications(limit = 50, offset = 0) { 153 return request(`${API_BASE}/notifications?limit=${limit}&offset=${offset}`); 154} 155 156export async function getUnreadNotificationCount() { 157 return request(`${API_BASE}/notifications/count`); 158} 159 160export async function markNotificationsRead() { 161 return request(`${API_BASE}/notifications/read`, { method: "POST" }); 162} 163 164export async function updateCollection(uri, name, description, icon) { 165 return request(`${API_BASE}/collections?uri=${encodeURIComponent(uri)}`, { 166 method: "PUT", 167 body: JSON.stringify({ name, description, icon }), 168 }); 169} 170 171export async function updateProfile({ bio, website, links }) { 172 return request(`${API_BASE}/profile`, { 173 method: "PUT", 174 body: JSON.stringify({ bio, website, links }), 175 }); 176} 177 178export async function createCollection(name, description, icon) { 179 return request(`${API_BASE}/collections`, { 180 method: "POST", 181 body: JSON.stringify({ name, description, icon }), 182 }); 183} 184 185export async function deleteCollection(uri) { 186 return request(`${API_BASE}/collections?uri=${encodeURIComponent(uri)}`, { 187 method: "DELETE", 188 }); 189} 190 191export async function getCollectionItems(collectionUri) { 192 return request( 193 `${API_BASE}/collections/${encodeURIComponent(collectionUri)}/items`, 194 ); 195} 196 197export async function addItemToCollection( 198 collectionUri, 199 annotationUri, 200 position = 0, 201) { 202 return request( 203 `${API_BASE}/collections/${encodeURIComponent(collectionUri)}/items`, 204 { 205 method: "POST", 206 body: JSON.stringify({ annotationUri, position }), 207 }, 208 ); 209} 210 211export async function removeItemFromCollection(itemUri) { 212 return request( 213 `${API_BASE}/collections/items?uri=${encodeURIComponent(itemUri)}`, 214 { 215 method: "DELETE", 216 }, 217 ); 218} 219 220export async function getLikeCount(annotationUri) { 221 return request(`${API_BASE}/likes?uri=${encodeURIComponent(annotationUri)}`); 222} 223 224export async function deleteHighlight(rkey) { 225 return request(`${API_BASE}/highlights?rkey=${encodeURIComponent(rkey)}`, { 226 method: "DELETE", 227 }); 228} 229 230export async function deleteBookmark(rkey) { 231 return request(`${API_BASE}/bookmarks?rkey=${encodeURIComponent(rkey)}`, { 232 method: "DELETE", 233 }); 234} 235 236export async function createHighlight({ url, title, selector, color, tags }) { 237 return request(`${API_BASE}/highlights`, { 238 method: "POST", 239 body: JSON.stringify({ url, title, selector, color, tags }), 240 }); 241} 242 243export async function createAnnotation({ 244 url, 245 text, 246 quote, 247 title, 248 selector, 249 tags, 250}) { 251 return request(`${API_BASE}/annotations`, { 252 method: "POST", 253 body: JSON.stringify({ url, text, quote, title, selector, tags }), 254 }); 255} 256 257export async function deleteAnnotation(rkey, type = "annotation") { 258 return request( 259 `${API_BASE}/annotations?rkey=${encodeURIComponent(rkey)}&type=${encodeURIComponent(type)}`, 260 { 261 method: "DELETE", 262 }, 263 ); 264} 265 266export async function likeAnnotation(subjectUri, subjectCid) { 267 return request(`${API_BASE}/annotations/like`, { 268 method: "POST", 269 headers: { 270 "Content-Type": "application/json", 271 }, 272 body: JSON.stringify({ 273 subjectUri, 274 subjectCid, 275 }), 276 }); 277} 278 279export async function unlikeAnnotation(subjectUri) { 280 return request( 281 `${API_BASE}/annotations/like?uri=${encodeURIComponent(subjectUri)}`, 282 { 283 method: "DELETE", 284 }, 285 ); 286} 287 288export async function createReply({ 289 parentUri, 290 parentCid, 291 rootUri, 292 rootCid, 293 text, 294}) { 295 return request(`${API_BASE}/annotations/reply`, { 296 method: "POST", 297 body: JSON.stringify({ parentUri, parentCid, rootUri, rootCid, text }), 298 }); 299} 300 301export async function deleteReply(uri) { 302 return request( 303 `${API_BASE}/annotations/reply?uri=${encodeURIComponent(uri)}`, 304 { 305 method: "DELETE", 306 }, 307 ); 308} 309 310export async function getSession() { 311 return request(`${AUTH_BASE}/session`); 312} 313 314export async function logout() { 315 return request(`${AUTH_BASE}/logout`, { method: "POST" }); 316} 317 318export function normalizeAnnotation(item) { 319 if (!item) return {}; 320 321 if (item.type === "Annotation") { 322 return { 323 type: item.type, 324 uri: item.uri || item.id, 325 author: item.author || item.creator, 326 url: item.url || item.target?.source, 327 title: item.title || item.target?.title, 328 text: item.text || item.body?.value, 329 selector: item.selector || item.target?.selector, 330 motivation: item.motivation, 331 tags: item.tags || [], 332 createdAt: item.createdAt || item.created, 333 cid: item.cid || item.CID, 334 likeCount: item.likeCount || 0, 335 replyCount: item.replyCount || 0, 336 viewerHasLiked: item.viewerHasLiked || false, 337 }; 338 } 339 340 if (item.type === "Bookmark") { 341 return { 342 type: item.type, 343 uri: item.uri || item.id, 344 author: item.author || item.creator, 345 url: item.url || item.source, 346 title: item.title, 347 description: item.description, 348 tags: item.tags || [], 349 createdAt: item.createdAt || item.created, 350 cid: item.cid || item.CID, 351 likeCount: item.likeCount || 0, 352 replyCount: item.replyCount || 0, 353 viewerHasLiked: item.viewerHasLiked || false, 354 }; 355 } 356 357 if (item.type === "Highlight") { 358 return { 359 type: item.type, 360 uri: item.uri || item.id, 361 author: item.author || item.creator, 362 url: item.url || item.target?.source, 363 title: item.title || item.target?.title, 364 selector: item.selector || item.target?.selector, 365 color: item.color, 366 tags: item.tags || [], 367 createdAt: item.createdAt || item.created, 368 cid: item.cid || item.CID, 369 likeCount: item.likeCount || 0, 370 replyCount: item.replyCount || 0, 371 viewerHasLiked: item.viewerHasLiked || false, 372 }; 373 } 374 375 return { 376 uri: item.uri || item.id, 377 author: item.author || item.creator, 378 url: item.url || item.source || item.target?.source, 379 title: item.title || item.target?.title, 380 text: item.text || item.body?.value, 381 description: item.description, 382 selector: item.selector || item.target?.selector, 383 color: item.color, 384 tags: item.tags || [], 385 createdAt: item.createdAt || item.created, 386 cid: item.cid || item.CID, 387 likeCount: item.likeCount || 0, 388 replyCount: item.replyCount || 0, 389 viewerHasLiked: item.viewerHasLiked || false, 390 }; 391} 392 393export function normalizeHighlight(highlight) { 394 return { 395 uri: highlight.uri || highlight.id, 396 author: highlight.author || highlight.creator, 397 url: highlight.url || highlight.target?.source, 398 title: highlight.title || highlight.target?.title, 399 selector: highlight.selector || highlight.target?.selector, 400 color: highlight.color, 401 tags: highlight.tags || [], 402 createdAt: highlight.createdAt || highlight.created, 403 likeCount: highlight.likeCount || 0, 404 replyCount: highlight.replyCount || 0, 405 viewerHasLiked: highlight.viewerHasLiked || false, 406 }; 407} 408 409export function normalizeBookmark(bookmark) { 410 return { 411 uri: bookmark.uri || bookmark.id, 412 author: bookmark.author || bookmark.creator, 413 url: bookmark.url || bookmark.source, 414 title: bookmark.title, 415 description: bookmark.description, 416 tags: bookmark.tags || [], 417 createdAt: bookmark.createdAt || bookmark.created, 418 likeCount: bookmark.likeCount || 0, 419 replyCount: bookmark.replyCount || 0, 420 viewerHasLiked: bookmark.viewerHasLiked || false, 421 }; 422} 423 424export async function searchActors(query) { 425 const res = await fetch( 426 `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=5`, 427 ); 428 if (!res.ok) throw new Error("Search failed"); 429 return res.json(); 430} 431 432export async function resolveHandle(handle) { 433 const res = await fetch( 434 `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`, 435 ); 436 if (!res.ok) throw new Error("Failed to resolve handle"); 437 const data = await res.json(); 438 return data.did; 439} 440 441export async function startLogin(handle, inviteCode) { 442 return request(`${AUTH_BASE}/start`, { 443 method: "POST", 444 body: JSON.stringify({ handle, invite_code: inviteCode }), 445 }); 446} 447export async function getTrendingTags(limit = 10) { 448 return request(`${API_BASE}/tags/trending?limit=${limit}`); 449} 450 451export async function getAPIKeys() { 452 return request(`${API_BASE}/keys`); 453} 454 455export async function createAPIKey(name) { 456 return request(`${API_BASE}/keys`, { 457 method: "POST", 458 body: JSON.stringify({ name }), 459 }); 460} 461 462export async function deleteAPIKey(id) { 463 return request(`${API_BASE}/keys/${id}`, { method: "DELETE" }); 464} 465 466export async function describeServer(service) { 467 const res = await fetch(`${service}/xrpc/com.atproto.server.describeServer`); 468 if (!res.ok) throw new Error("Failed to describe server"); 469 return res.json(); 470} 471 472export async function createAccount( 473 service, 474 { handle, email, password, inviteCode }, 475) { 476 const res = await fetch(`${service}/xrpc/com.atproto.server.createAccount`, { 477 method: "POST", 478 headers: { 479 "Content-Type": "application/json", 480 }, 481 body: JSON.stringify({ 482 handle, 483 email, 484 password, 485 inviteCode, 486 }), 487 }); 488 489 const data = await res.json(); 490 if (!res.ok) { 491 throw new Error(data.message || data.error || "Failed to create account"); 492 } 493 return data; 494}