Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at frontend-rewrite 301 lines 7.5 kB view raw
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(url: string, citedUrls: string[] = []) { 85 try { 86 const apiUrl = await getApiUrl(); 87 const uniqueUrls = [...new Set([url, ...citedUrls])]; 88 89 const fetchPromises = uniqueUrls.map(async (u) => { 90 try { 91 const res = await fetch(`${apiUrl}/api/targets?source=${encodeURIComponent(u)}`); 92 if (!res.ok) return { annotations: [], highlights: [], bookmarks: [] }; 93 return await res.json(); 94 } catch { 95 return { annotations: [], highlights: [], bookmarks: [] }; 96 } 97 }); 98 99 const results = await Promise.all(fetchPromises); 100 const allItems: any[] = []; 101 const seenIds = new Set<string>(); 102 103 results.forEach((data) => { 104 const items = [ 105 ...(data.annotations || []), 106 ...(data.highlights || []), 107 ...(data.bookmarks || []), 108 ]; 109 items.forEach((item: any) => { 110 const id = item.uri || item.id; 111 if (id && !seenIds.has(id)) { 112 seenIds.add(id); 113 allItems.push(item); 114 } 115 }); 116 }); 117 118 return allItems; 119 } catch (error) { 120 console.error('Get annotations error:', error); 121 return []; 122 } 123} 124 125export async function createAnnotation(data: { 126 url: string; 127 text: string; 128 title?: string; 129 selector?: TextSelector; 130}) { 131 try { 132 const res = await apiRequest('/annotations', { 133 method: 'POST', 134 body: JSON.stringify({ 135 url: data.url, 136 text: data.text, 137 title: data.title, 138 selector: data.selector, 139 }), 140 }); 141 142 if (!res.ok) { 143 const error = await res.text(); 144 return { success: false, error }; 145 } 146 147 return { success: true, data: await res.json() }; 148 } catch (error) { 149 return { success: false, error: String(error) }; 150 } 151} 152 153export async function createBookmark(data: { url: string; title?: string }) { 154 try { 155 const res = await apiRequest('/bookmarks', { 156 method: 'POST', 157 body: JSON.stringify({ url: data.url, title: data.title }), 158 }); 159 160 if (!res.ok) { 161 const error = await res.text(); 162 return { success: false, error }; 163 } 164 165 return { success: true, data: await res.json() }; 166 } catch (error) { 167 return { success: false, error: String(error) }; 168 } 169} 170 171export async function createHighlight(data: { 172 url: string; 173 title?: string; 174 selector: TextSelector; 175 color?: string; 176}) { 177 try { 178 const res = await apiRequest('/highlights', { 179 method: 'POST', 180 body: JSON.stringify({ 181 url: data.url, 182 title: data.title, 183 selector: data.selector, 184 color: data.color, 185 }), 186 }); 187 188 if (!res.ok) { 189 const error = await res.text(); 190 return { success: false, error }; 191 } 192 193 return { success: true, data: await res.json() }; 194 } catch (error) { 195 return { success: false, error: String(error) }; 196 } 197} 198 199export async function getUserBookmarks(did: string) { 200 try { 201 const res = await apiRequest(`/users/${did}/bookmarks`); 202 if (!res.ok) return []; 203 const data = await res.json(); 204 return data.items || data || []; 205 } catch (error) { 206 console.error('Get bookmarks error:', error); 207 return []; 208 } 209} 210 211export async function getUserHighlights(did: string) { 212 try { 213 const res = await apiRequest(`/users/${did}/highlights`); 214 if (!res.ok) return []; 215 const data = await res.json(); 216 return data.items || data || []; 217 } catch (error) { 218 console.error('Get highlights error:', error); 219 return []; 220 } 221} 222 223export async function getUserCollections(did: string) { 224 try { 225 const res = await apiRequest(`/collections?author=${encodeURIComponent(did)}`); 226 if (!res.ok) return []; 227 const data = await res.json(); 228 return data.items || data || []; 229 } catch (error) { 230 console.error('Get collections error:', error); 231 return []; 232 } 233} 234 235export async function addToCollection(collectionUri: string, annotationUri: string) { 236 try { 237 const res = await apiRequest(`/collections/${encodeURIComponent(collectionUri)}/items`, { 238 method: 'POST', 239 body: JSON.stringify({ annotationUri, position: 0 }), 240 }); 241 242 if (!res.ok) { 243 const error = await res.text(); 244 return { success: false, error }; 245 } 246 247 return { success: true }; 248 } catch (error) { 249 return { success: false, error: String(error) }; 250 } 251} 252 253export async function getItemCollections(annotationUri: string): Promise<string[]> { 254 try { 255 const res = await apiRequest( 256 `/collections/containing?uri=${encodeURIComponent(annotationUri)}` 257 ); 258 if (!res.ok) return []; 259 const data = await res.json(); 260 return Array.isArray(data) ? data : []; 261 } catch (error) { 262 console.error('Get item collections error:', error); 263 return []; 264 } 265} 266 267export async function getReplies(uri: string) { 268 try { 269 const res = await apiRequest(`/annotations/${encodeURIComponent(uri)}/replies`); 270 if (!res.ok) return []; 271 const data = await res.json(); 272 return data.items || data || []; 273 } catch (error) { 274 console.error('Get replies error:', error); 275 return []; 276 } 277} 278 279export async function createReply(data: { 280 parentUri: string; 281 parentCid: string; 282 rootUri: string; 283 rootCid: string; 284 text: string; 285}) { 286 try { 287 const res = await apiRequest('/replies', { 288 method: 'POST', 289 body: JSON.stringify(data), 290 }); 291 292 if (!res.ok) { 293 const error = await res.text(); 294 return { success: false, error }; 295 } 296 297 return { success: true }; 298 } catch (error) { 299 return { success: false, error: String(error) }; 300 } 301}