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