Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
1import type { MarginSession, TextSelector } from './types';
2import { apiUrlItem } from './storage';
3
4async function getApiUrl(): Promise<string> {
5 return await apiUrlItem.getValue();
6}
7
8async function getSessionCookie(): Promise<string | null> {
9 try {
10 const apiUrl = await getApiUrl();
11 const cookie = await browser.cookies.get({
12 url: apiUrl,
13 name: 'margin_session',
14 });
15 return cookie?.value || null;
16 } catch (error) {
17 console.error('Get cookie error:', error);
18 return null;
19 }
20}
21
22export async function checkSession(): Promise<MarginSession> {
23 try {
24 const apiUrl = await getApiUrl();
25 const cookie = await getSessionCookie();
26
27 if (!cookie) {
28 return { authenticated: false };
29 }
30
31 const res = await fetch(`${apiUrl}/auth/session`, {
32 headers: {
33 'X-Session-Token': cookie,
34 },
35 });
36
37 if (!res.ok) {
38 return { authenticated: false };
39 }
40
41 const sessionData = await res.json();
42
43 if (!sessionData.did || !sessionData.handle) {
44 return { authenticated: false };
45 }
46
47 return {
48 authenticated: true,
49 did: sessionData.did,
50 handle: sessionData.handle,
51 accessJwt: sessionData.accessJwt,
52 refreshJwt: sessionData.refreshJwt,
53 };
54 } catch (error) {
55 console.error('Session check error:', error);
56 return { authenticated: false };
57 }
58}
59
60async function apiRequest(path: string, options: RequestInit = {}): Promise<Response> {
61 const apiUrl = await getApiUrl();
62 const cookie = await getSessionCookie();
63
64 const headers: Record<string, string> = {
65 'Content-Type': 'application/json',
66 ...(options.headers as Record<string, string>),
67 };
68
69 if (cookie) {
70 headers['X-Session-Token'] = cookie;
71 }
72
73 const apiPath = path.startsWith('/api') ? path : `/api${path}`;
74
75 const response = await fetch(`${apiUrl}${apiPath}`, {
76 ...options,
77 headers,
78 credentials: 'include',
79 });
80
81 return response;
82}
83
84export async function getAnnotations(
85 url: string,
86 citedUrls: string[] = [],
87 cacheBust: boolean = false
88) {
89 try {
90 const apiUrl = await getApiUrl();
91 const uniqueUrls = [...new Set([url, ...citedUrls])];
92
93 const fetchPromises = uniqueUrls.map(async (u) => {
94 try {
95 let requestUrl = `${apiUrl}/api/targets?source=${encodeURIComponent(u)}`;
96 if (cacheBust) {
97 requestUrl += `&t=${Date.now()}`;
98 }
99 const res = await fetch(requestUrl);
100 if (!res.ok) return { annotations: [], highlights: [], bookmarks: [] };
101 return await res.json();
102 } catch {
103 return { annotations: [], highlights: [], bookmarks: [] };
104 }
105 });
106
107 const results = await Promise.all(fetchPromises);
108 const allItems: any[] = [];
109 const seenIds = new Set<string>();
110
111 results.forEach((data) => {
112 const items = [
113 ...(data.annotations || []),
114 ...(data.highlights || []),
115 ...(data.bookmarks || []),
116 ];
117 items.forEach((item: any) => {
118 const id = item.uri || item.id;
119 if (id && !seenIds.has(id)) {
120 seenIds.add(id);
121 allItems.push(item);
122 }
123 });
124 });
125
126 return allItems;
127 } catch (error) {
128 console.error('Get annotations error:', error);
129 return [];
130 }
131}
132
133export async function createAnnotation(data: {
134 url: string;
135 text: string;
136 title?: string;
137 selector?: TextSelector;
138 tags?: string[];
139}) {
140 try {
141 const res = await apiRequest('/annotations', {
142 method: 'POST',
143 body: JSON.stringify({
144 url: data.url,
145 text: data.text,
146 title: data.title,
147 selector: data.selector,
148 tags: data.tags,
149 }),
150 });
151
152 if (!res.ok) {
153 const error = await res.text();
154 return { success: false, error };
155 }
156
157 return { success: true, data: await res.json() };
158 } catch (error) {
159 return { success: false, error: String(error) };
160 }
161}
162
163export async function createBookmark(data: { url: string; title?: string; tags?: string[] }) {
164 try {
165 const res = await apiRequest('/bookmarks', {
166 method: 'POST',
167 body: JSON.stringify({ url: data.url, title: data.title, tags: data.tags }),
168 });
169
170 if (!res.ok) {
171 const error = await res.text();
172 return { success: false, error };
173 }
174
175 return { success: true, data: await res.json() };
176 } catch (error) {
177 return { success: false, error: String(error) };
178 }
179}
180
181export async function createHighlight(data: {
182 url: string;
183 title?: string;
184 selector: TextSelector;
185 color?: string;
186 tags?: string[];
187}) {
188 try {
189 const res = await apiRequest('/highlights', {
190 method: 'POST',
191 body: JSON.stringify({
192 url: data.url,
193 title: data.title,
194 selector: data.selector,
195 color: data.color,
196 tags: data.tags,
197 }),
198 });
199
200 if (!res.ok) {
201 const error = await res.text();
202 return { success: false, error };
203 }
204
205 return { success: true, data: await res.json() };
206 } catch (error) {
207 return { success: false, error: String(error) };
208 }
209}
210
211export async function getUserBookmarks(did: string) {
212 try {
213 const res = await apiRequest(`/users/${did}/bookmarks`);
214 if (!res.ok) return [];
215 const data = await res.json();
216 return data.items || data || [];
217 } catch (error) {
218 console.error('Get bookmarks error:', error);
219 return [];
220 }
221}
222
223export async function getUserHighlights(did: string) {
224 try {
225 const res = await apiRequest(`/users/${did}/highlights`);
226 if (!res.ok) return [];
227 const data = await res.json();
228 return data.items || data || [];
229 } catch (error) {
230 console.error('Get highlights error:', error);
231 return [];
232 }
233}
234
235export async function getUserCollections(did: string) {
236 try {
237 const res = await apiRequest(`/collections?author=${encodeURIComponent(did)}`);
238 if (!res.ok) return [];
239 const data = await res.json();
240 return data.items || data || [];
241 } catch (error) {
242 console.error('Get collections error:', error);
243 return [];
244 }
245}
246
247export async function addToCollection(collectionUri: string, annotationUri: string) {
248 try {
249 const res = await apiRequest(`/collections/${encodeURIComponent(collectionUri)}/items`, {
250 method: 'POST',
251 body: JSON.stringify({ annotationUri, position: 0 }),
252 });
253
254 if (!res.ok) {
255 const error = await res.text();
256 return { success: false, error };
257 }
258
259 return { success: true };
260 } catch (error) {
261 return { success: false, error: String(error) };
262 }
263}
264
265export async function getItemCollections(annotationUri: string): Promise<string[]> {
266 try {
267 const res = await apiRequest(
268 `/collections/containing?uri=${encodeURIComponent(annotationUri)}`
269 );
270 if (!res.ok) return [];
271 const data = await res.json();
272 return Array.isArray(data) ? data : [];
273 } catch (error) {
274 console.error('Get item collections error:', error);
275 return [];
276 }
277}
278
279export async function deleteHighlight(uri: string) {
280 try {
281 const rkey = (uri || '').split('/').pop();
282 if (!rkey) return { success: false, error: 'Invalid URI' };
283
284 const res = await apiRequest(`/highlights?rkey=${rkey}`, {
285 method: 'DELETE',
286 });
287
288 if (!res.ok) {
289 const error = await res.text();
290 return { success: false, error };
291 }
292
293 return { success: true };
294 } catch (error) {
295 return { success: false, error: String(error) };
296 }
297}
298
299export async function getUserTags(did: string) {
300 try {
301 const res = await apiRequest(`/users/${did}/tags?limit=50`);
302 if (!res.ok) return [];
303 const data = await res.json();
304 return (data || []).map((t: { tag: string }) => t.tag);
305 } catch (error) {
306 console.error('Get user tags error:', error);
307 return [];
308 }
309}
310
311export async function getTrendingTags() {
312 try {
313 const res = await apiRequest('/trending-tags?limit=50');
314 if (!res.ok) return [];
315 const data = await res.json();
316 return (data || []).map((t: { tag: string }) => t.tag);
317 } catch (error) {
318 console.error('Get trending tags error:', error);
319 return [];
320 }
321}
322
323export async function getReplies(uri: string) {
324 try {
325 const res = await apiRequest(`/annotations/${encodeURIComponent(uri)}/replies`);
326 if (!res.ok) return [];
327 const data = await res.json();
328 return data.items || data || [];
329 } catch (error) {
330 console.error('Get replies error:', error);
331 return [];
332 }
333}
334
335export async function createReply(data: {
336 parentUri: string;
337 parentCid: string;
338 rootUri: string;
339 rootCid: string;
340 text: string;
341}) {
342 try {
343 const res = await apiRequest('/replies', {
344 method: 'POST',
345 body: JSON.stringify(data),
346 });
347
348 if (!res.ok) {
349 const error = await res.text();
350 return { success: false, error };
351 }
352
353 return { success: true };
354 } catch (error) {
355 return { success: false, error: String(error) };
356 }
357}