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}
433
434export async function getAPIKeys() {
435 return request(`${API_BASE}/keys`);
436}
437
438export async function createAPIKey(name) {
439 return request(`${API_BASE}/keys`, {
440 method: "POST",
441 body: JSON.stringify({ name }),
442 });
443}
444
445export async function deleteAPIKey(id) {
446 return request(`${API_BASE}/keys/${id}`, { method: "DELETE" });
447}