Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at v0.1.13 12 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 getUserAnnotations(did, limit = 50, offset = 0) { 61 return request( 62 `${API_BASE}/users/${encodeURIComponent(did)}/annotations?limit=${limit}&offset=${offset}`, 63 ); 64} 65 66export async function getUserHighlights(did, limit = 50, offset = 0) { 67 return request( 68 `${API_BASE}/users/${encodeURIComponent(did)}/highlights?limit=${limit}&offset=${offset}`, 69 ); 70} 71 72export async function getUserBookmarks(did, limit = 50, offset = 0) { 73 return request( 74 `${API_BASE}/users/${encodeURIComponent(did)}/bookmarks?limit=${limit}&offset=${offset}`, 75 ); 76} 77 78export async function getHighlights(creatorDid, limit = 50, offset = 0) { 79 return request( 80 `${API_BASE}/highlights?creator=${encodeURIComponent(creatorDid)}&limit=${limit}&offset=${offset}`, 81 ); 82} 83 84export async function getBookmarks(creatorDid, limit = 50, offset = 0) { 85 return request( 86 `${API_BASE}/bookmarks?creator=${encodeURIComponent(creatorDid)}&limit=${limit}&offset=${offset}`, 87 ); 88} 89 90export async function getReplies(annotationUri) { 91 return request( 92 `${API_BASE}/replies?uri=${encodeURIComponent(annotationUri)}`, 93 ); 94} 95 96export async function updateAnnotation(uri, text, tags) { 97 return request(`${API_BASE}/annotations?uri=${encodeURIComponent(uri)}`, { 98 method: "PUT", 99 body: JSON.stringify({ text, tags }), 100 }); 101} 102 103export async function updateHighlight(uri, color, tags) { 104 return request(`${API_BASE}/highlights?uri=${encodeURIComponent(uri)}`, { 105 method: "PUT", 106 body: JSON.stringify({ color, tags }), 107 }); 108} 109 110export async function createBookmark(url, title, description) { 111 return request(`${API_BASE}/bookmarks`, { 112 method: "POST", 113 body: JSON.stringify({ url, title, description }), 114 }); 115} 116 117export async function updateBookmark(uri, title, description, tags) { 118 return request(`${API_BASE}/bookmarks?uri=${encodeURIComponent(uri)}`, { 119 method: "PUT", 120 body: JSON.stringify({ title, description, tags }), 121 }); 122} 123 124export async function getCollections(did) { 125 let url = `${API_BASE}/collections`; 126 if (did) url += `?author=${encodeURIComponent(did)}`; 127 return request(url); 128} 129 130export async function getCollectionsContaining(annotationUri) { 131 return request( 132 `${API_BASE}/collections/containing?uri=${encodeURIComponent(annotationUri)}`, 133 ); 134} 135 136export async function getEditHistory(uri) { 137 return request( 138 `${API_BASE}/annotations/history?uri=${encodeURIComponent(uri)}`, 139 ); 140} 141 142export async function getNotifications(limit = 50, offset = 0) { 143 return request(`${API_BASE}/notifications?limit=${limit}&offset=${offset}`); 144} 145 146export async function getUnreadNotificationCount() { 147 return request(`${API_BASE}/notifications/count`); 148} 149 150export async function markNotificationsRead() { 151 return request(`${API_BASE}/notifications/read`, { method: "POST" }); 152} 153 154export async function updateCollection(uri, name, description, icon) { 155 return request(`${API_BASE}/collections?uri=${encodeURIComponent(uri)}`, { 156 method: "PUT", 157 body: JSON.stringify({ name, description, icon }), 158 }); 159} 160 161export async function createCollection(name, description, icon) { 162 return request(`${API_BASE}/collections`, { 163 method: "POST", 164 body: JSON.stringify({ name, description, icon }), 165 }); 166} 167 168export async function deleteCollection(uri) { 169 return request(`${API_BASE}/collections?uri=${encodeURIComponent(uri)}`, { 170 method: "DELETE", 171 }); 172} 173 174export async function getCollectionItems(collectionUri) { 175 return request( 176 `${API_BASE}/collections/${encodeURIComponent(collectionUri)}/items`, 177 ); 178} 179 180export async function addItemToCollection( 181 collectionUri, 182 annotationUri, 183 position = 0, 184) { 185 return request( 186 `${API_BASE}/collections/${encodeURIComponent(collectionUri)}/items`, 187 { 188 method: "POST", 189 body: JSON.stringify({ annotationUri, position }), 190 }, 191 ); 192} 193 194export async function removeItemFromCollection(itemUri) { 195 return request( 196 `${API_BASE}/collections/items?uri=${encodeURIComponent(itemUri)}`, 197 { 198 method: "DELETE", 199 }, 200 ); 201} 202 203export async function getLikeCount(annotationUri) { 204 return request(`${API_BASE}/likes?uri=${encodeURIComponent(annotationUri)}`); 205} 206 207export async function deleteHighlight(rkey) { 208 return request(`${API_BASE}/highlights?rkey=${encodeURIComponent(rkey)}`, { 209 method: "DELETE", 210 }); 211} 212 213export async function deleteBookmark(rkey) { 214 return request(`${API_BASE}/bookmarks?rkey=${encodeURIComponent(rkey)}`, { 215 method: "DELETE", 216 }); 217} 218 219export async function createHighlight({ url, title, selector, color, tags }) { 220 return request(`${API_BASE}/highlights`, { 221 method: "POST", 222 body: JSON.stringify({ url, title, selector, color, tags }), 223 }); 224} 225 226export async function createAnnotation({ 227 url, 228 text, 229 quote, 230 title, 231 selector, 232 tags, 233}) { 234 return request(`${API_BASE}/annotations`, { 235 method: "POST", 236 body: JSON.stringify({ url, text, quote, title, selector, tags }), 237 }); 238} 239 240export async function deleteAnnotation(rkey, type = "annotation") { 241 return request( 242 `${API_BASE}/annotations?rkey=${encodeURIComponent(rkey)}&type=${encodeURIComponent(type)}`, 243 { 244 method: "DELETE", 245 }, 246 ); 247} 248 249export async function likeAnnotation(subjectUri, subjectCid) { 250 return request(`${API_BASE}/annotations/like`, { 251 method: "POST", 252 headers: { 253 "Content-Type": "application/json", 254 }, 255 body: JSON.stringify({ 256 subjectUri, 257 subjectCid, 258 }), 259 }); 260} 261 262export async function unlikeAnnotation(subjectUri) { 263 return request( 264 `${API_BASE}/annotations/like?uri=${encodeURIComponent(subjectUri)}`, 265 { 266 method: "DELETE", 267 }, 268 ); 269} 270 271export async function createReply({ 272 parentUri, 273 parentCid, 274 rootUri, 275 rootCid, 276 text, 277}) { 278 return request(`${API_BASE}/annotations/reply`, { 279 method: "POST", 280 body: JSON.stringify({ parentUri, parentCid, rootUri, rootCid, text }), 281 }); 282} 283 284export async function deleteReply(uri) { 285 return request( 286 `${API_BASE}/annotations/reply?uri=${encodeURIComponent(uri)}`, 287 { 288 method: "DELETE", 289 }, 290 ); 291} 292 293export async function getSession() { 294 return request(`${AUTH_BASE}/session`); 295} 296 297export async function logout() { 298 return request(`${AUTH_BASE}/logout`, { method: "POST" }); 299} 300 301export function normalizeAnnotation(item) { 302 if (!item) return {}; 303 304 if (item.type === "Annotation") { 305 return { 306 type: item.type, 307 uri: item.uri || item.id, 308 author: item.author || item.creator, 309 url: item.url || item.target?.source, 310 title: item.title || item.target?.title, 311 text: item.text || item.body?.value, 312 selector: item.selector || item.target?.selector, 313 motivation: item.motivation, 314 tags: item.tags || [], 315 createdAt: item.createdAt || item.created, 316 cid: item.cid || item.CID, 317 likeCount: item.likeCount || 0, 318 replyCount: item.replyCount || 0, 319 viewerHasLiked: item.viewerHasLiked || false, 320 }; 321 } 322 323 if (item.type === "Bookmark") { 324 return { 325 type: item.type, 326 uri: item.uri || item.id, 327 author: item.author || item.creator, 328 url: item.url || item.source, 329 title: item.title, 330 description: item.description, 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 === "Highlight") { 341 return { 342 type: item.type, 343 uri: item.uri || item.id, 344 author: item.author || item.creator, 345 url: item.url || item.target?.source, 346 title: item.title || item.target?.title, 347 selector: item.selector || item.target?.selector, 348 color: item.color, 349 tags: item.tags || [], 350 createdAt: item.createdAt || item.created, 351 cid: item.cid || item.CID, 352 likeCount: item.likeCount || 0, 353 replyCount: item.replyCount || 0, 354 viewerHasLiked: item.viewerHasLiked || false, 355 }; 356 } 357 358 return { 359 uri: item.uri || item.id, 360 author: item.author || item.creator, 361 url: item.url || item.source || item.target?.source, 362 title: item.title || item.target?.title, 363 text: item.text || item.body?.value, 364 description: item.description, 365 selector: item.selector || item.target?.selector, 366 color: item.color, 367 tags: item.tags || [], 368 createdAt: item.createdAt || item.created, 369 cid: item.cid || item.CID, 370 likeCount: item.likeCount || 0, 371 replyCount: item.replyCount || 0, 372 viewerHasLiked: item.viewerHasLiked || false, 373 }; 374} 375 376export function normalizeHighlight(highlight) { 377 return { 378 uri: highlight.uri || highlight.id, 379 author: highlight.author || highlight.creator, 380 url: highlight.url || highlight.target?.source, 381 title: highlight.title || highlight.target?.title, 382 selector: highlight.selector || highlight.target?.selector, 383 color: highlight.color, 384 tags: highlight.tags || [], 385 createdAt: highlight.createdAt || highlight.created, 386 likeCount: highlight.likeCount || 0, 387 replyCount: highlight.replyCount || 0, 388 viewerHasLiked: highlight.viewerHasLiked || false, 389 }; 390} 391 392export function normalizeBookmark(bookmark) { 393 return { 394 uri: bookmark.uri || bookmark.id, 395 author: bookmark.author || bookmark.creator, 396 url: bookmark.url || bookmark.source, 397 title: bookmark.title, 398 description: bookmark.description, 399 tags: bookmark.tags || [], 400 createdAt: bookmark.createdAt || bookmark.created, 401 likeCount: bookmark.likeCount || 0, 402 replyCount: bookmark.replyCount || 0, 403 viewerHasLiked: bookmark.viewerHasLiked || false, 404 }; 405} 406 407export async function searchActors(query) { 408 const res = await fetch( 409 `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=5`, 410 ); 411 if (!res.ok) throw new Error("Search failed"); 412 return res.json(); 413} 414 415export async function resolveHandle(handle) { 416 const res = await fetch( 417 `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`, 418 ); 419 if (!res.ok) throw new Error("Failed to resolve handle"); 420 const data = await res.json(); 421 return data.did; 422} 423 424export async function startLogin(handle, inviteCode) { 425 return request(`${AUTH_BASE}/start`, { 426 method: "POST", 427 body: JSON.stringify({ handle, invite_code: inviteCode }), 428 }); 429} 430export async function getTrendingTags(limit = 10) { 431 return request(`${API_BASE}/tags/trending?limit=${limit}`); 432}