Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at main 357 lines 8.9 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( 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}