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(limit = 50, offset = 0) {
27 return request(
28 `${API_BASE}/annotations/feed?limit=${limit}&offset=${offset}`,
29 );
30}
31
32export async function getAnnotations({
33 source,
34 motivation,
35 limit = 50,
36 offset = 0,
37} = {}) {
38 let url = `${API_BASE}/annotations?limit=${limit}&offset=${offset}`;
39 if (source) url += `&source=${encodeURIComponent(source)}`;
40 if (motivation) url += `&motivation=${motivation}`;
41 return request(url);
42}
43
44export async function getByTarget(source, limit = 50, offset = 0) {
45 return request(
46 `${API_BASE}/targets?source=${encodeURIComponent(source)}&limit=${limit}&offset=${offset}`,
47 );
48}
49
50export async function getAnnotation(uri) {
51 return request(`${API_BASE}/annotation?uri=${encodeURIComponent(uri)}`);
52}
53
54export async function getUserAnnotations(did, limit = 50, offset = 0) {
55 return request(
56 `${API_BASE}/users/${encodeURIComponent(did)}/annotations?limit=${limit}&offset=${offset}`,
57 );
58}
59
60export async function getUserHighlights(did, limit = 50, offset = 0) {
61 return request(
62 `${API_BASE}/users/${encodeURIComponent(did)}/highlights?limit=${limit}&offset=${offset}`,
63 );
64}
65
66export async function getUserBookmarks(did, limit = 50, offset = 0) {
67 return request(
68 `${API_BASE}/users/${encodeURIComponent(did)}/bookmarks?limit=${limit}&offset=${offset}`,
69 );
70}
71
72export async function getHighlights(creatorDid, limit = 50, offset = 0) {
73 return request(
74 `${API_BASE}/highlights?creator=${encodeURIComponent(creatorDid)}&limit=${limit}&offset=${offset}`,
75 );
76}
77
78export async function getBookmarks(creatorDid, limit = 50, offset = 0) {
79 return request(
80 `${API_BASE}/bookmarks?creator=${encodeURIComponent(creatorDid)}&limit=${limit}&offset=${offset}`,
81 );
82}
83
84export async function getReplies(annotationUri) {
85 return request(
86 `${API_BASE}/replies?uri=${encodeURIComponent(annotationUri)}`,
87 );
88}
89
90export async function updateAnnotation(uri, text, tags) {
91 return request(`${API_BASE}/annotations?uri=${encodeURIComponent(uri)}`, {
92 method: "PUT",
93 body: JSON.stringify({ text, tags }),
94 });
95}
96
97export async function updateHighlight(uri, color, tags) {
98 return request(`${API_BASE}/highlights?uri=${encodeURIComponent(uri)}`, {
99 method: "PUT",
100 body: JSON.stringify({ color, tags }),
101 });
102}
103
104export async function createBookmark(url, title, description) {
105 return request(`${API_BASE}/bookmarks`, {
106 method: "POST",
107 body: JSON.stringify({ url, title, description }),
108 });
109}
110
111export async function updateBookmark(uri, title, description, tags) {
112 return request(`${API_BASE}/bookmarks?uri=${encodeURIComponent(uri)}`, {
113 method: "PUT",
114 body: JSON.stringify({ title, description, tags }),
115 });
116}
117
118export async function getCollections(did) {
119 let url = `${API_BASE}/collections`;
120 if (did) url += `?author=${encodeURIComponent(did)}`;
121 return request(url);
122}
123
124export async function getCollectionsContaining(annotationUri) {
125 return request(
126 `${API_BASE}/collections/containing?uri=${encodeURIComponent(annotationUri)}`,
127 );
128}
129
130export async function getEditHistory(uri) {
131 return request(
132 `${API_BASE}/annotations/history?uri=${encodeURIComponent(uri)}`,
133 );
134}
135
136export async function getNotifications(limit = 50, offset = 0) {
137 return request(`${API_BASE}/notifications?limit=${limit}&offset=${offset}`);
138}
139
140export async function getUnreadNotificationCount() {
141 return request(`${API_BASE}/notifications/count`);
142}
143
144export async function markNotificationsRead() {
145 return request(`${API_BASE}/notifications/read`, { method: "POST" });
146}
147
148export async function updateCollection(uri, name, description, icon) {
149 return request(`${API_BASE}/collections?uri=${encodeURIComponent(uri)}`, {
150 method: "PUT",
151 body: JSON.stringify({ name, description, icon }),
152 });
153}
154
155export async function createCollection(name, description, icon) {
156 return request(`${API_BASE}/collections`, {
157 method: "POST",
158 body: JSON.stringify({ name, description, icon }),
159 });
160}
161
162export async function deleteCollection(uri) {
163 return request(`${API_BASE}/collections?uri=${encodeURIComponent(uri)}`, {
164 method: "DELETE",
165 });
166}
167
168export async function getCollectionItems(collectionUri) {
169 return request(
170 `${API_BASE}/collections/${encodeURIComponent(collectionUri)}/items`,
171 );
172}
173
174export async function addItemToCollection(
175 collectionUri,
176 annotationUri,
177 position = 0,
178) {
179 return request(
180 `${API_BASE}/collections/${encodeURIComponent(collectionUri)}/items`,
181 {
182 method: "POST",
183 body: JSON.stringify({ annotationUri, position }),
184 },
185 );
186}
187
188export async function removeItemFromCollection(itemUri) {
189 return request(
190 `${API_BASE}/collections/items?uri=${encodeURIComponent(itemUri)}`,
191 {
192 method: "DELETE",
193 },
194 );
195}
196
197export async function getLikeCount(annotationUri) {
198 return request(`${API_BASE}/likes?uri=${encodeURIComponent(annotationUri)}`);
199}
200
201export async function deleteHighlight(rkey) {
202 return request(`${API_BASE}/highlights?rkey=${encodeURIComponent(rkey)}`, {
203 method: "DELETE",
204 });
205}
206
207export async function deleteBookmark(rkey) {
208 return request(`${API_BASE}/bookmarks?rkey=${encodeURIComponent(rkey)}`, {
209 method: "DELETE",
210 });
211}
212
213export async function createAnnotation({ url, text, quote, title, selector }) {
214 return request(`${API_BASE}/annotations`, {
215 method: "POST",
216 body: JSON.stringify({ url, text, quote, title, selector }),
217 });
218}
219
220export async function deleteAnnotation(rkey, type = "annotation") {
221 return request(
222 `${API_BASE}/annotations?rkey=${encodeURIComponent(rkey)}&type=${encodeURIComponent(type)}`,
223 {
224 method: "DELETE",
225 },
226 );
227}
228
229export async function likeAnnotation(subjectUri, subjectCid) {
230 return request(`${API_BASE}/annotations/like`, {
231 method: "POST",
232 headers: {
233 "Content-Type": "application/json",
234 },
235 body: JSON.stringify({
236 subjectUri,
237 subjectCid,
238 }),
239 });
240}
241
242export async function unlikeAnnotation(subjectUri) {
243 return request(
244 `${API_BASE}/annotations/like?uri=${encodeURIComponent(subjectUri)}`,
245 {
246 method: "DELETE",
247 },
248 );
249}
250
251export async function createReply({
252 parentUri,
253 parentCid,
254 rootUri,
255 rootCid,
256 text,
257}) {
258 return request(`${API_BASE}/annotations/reply`, {
259 method: "POST",
260 body: JSON.stringify({ parentUri, parentCid, rootUri, rootCid, text }),
261 });
262}
263
264export async function deleteReply(uri) {
265 return request(
266 `${API_BASE}/annotations/reply?uri=${encodeURIComponent(uri)}`,
267 {
268 method: "DELETE",
269 },
270 );
271}
272
273export async function getSession() {
274 return request(`${AUTH_BASE}/session`);
275}
276
277export async function logout() {
278 return request(`${AUTH_BASE}/logout`, { method: "POST" });
279}
280
281export function normalizeAnnotation(item) {
282 if (!item) return {};
283
284 if (item.type === "Annotation") {
285 return {
286 uri: item.id,
287 author: item.creator,
288 url: item.target?.source,
289 title: item.target?.title,
290 text: item.body?.value,
291 selector: item.target?.selector,
292 motivation: item.motivation,
293 tags: item.tags || [],
294 createdAt: item.created,
295 cid: item.cid || item.CID,
296 };
297 }
298
299 if (item.type === "Bookmark") {
300 return {
301 uri: item.id,
302 author: item.creator,
303 url: item.source,
304 title: item.title,
305 description: item.description,
306 tags: item.tags || [],
307 createdAt: item.created,
308 cid: item.cid || item.CID,
309 };
310 }
311
312 if (item.type === "Highlight") {
313 return {
314 uri: item.id,
315 author: item.creator,
316 url: item.target?.source,
317 title: item.target?.title,
318 selector: item.target?.selector,
319 color: item.color,
320 tags: item.tags || [],
321 createdAt: item.created,
322 cid: item.cid || item.CID,
323 };
324 }
325
326 return {
327 uri: item.uri || item.id,
328 author: item.author || item.creator,
329 url: item.url || item.source || item.target?.source,
330 title: item.title || item.target?.title,
331 text: item.text || item.body?.value,
332 description: item.description,
333 selector: item.selector || item.target?.selector,
334 color: item.color,
335 tags: item.tags || [],
336 createdAt: item.createdAt || item.created,
337 cid: item.cid || item.CID,
338 };
339}
340
341export function normalizeHighlight(highlight) {
342 return {
343 uri: highlight.id,
344 author: highlight.creator,
345 url: highlight.target?.source,
346 title: highlight.target?.title,
347 selector: highlight.target?.selector,
348 color: highlight.color,
349 tags: highlight.tags || [],
350 createdAt: highlight.created,
351 };
352}
353
354export function normalizeBookmark(bookmark) {
355 return {
356 uri: bookmark.id,
357 author: bookmark.creator,
358 url: bookmark.source,
359 title: bookmark.title,
360 description: bookmark.description,
361 tags: bookmark.tags || [],
362 createdAt: bookmark.created,
363 };
364}
365
366export async function searchActors(query) {
367 const res = await fetch(
368 `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=5`,
369 );
370 if (!res.ok) throw new Error("Search failed");
371 return res.json();
372}
373
374export async function startLogin(handle, inviteCode) {
375 return request(`${AUTH_BASE}/start`, {
376 method: "POST",
377 body: JSON.stringify({ handle, invite_code: inviteCode }),
378 });
379}