A Chrome extension to quickly capture URLs into Semble Collections at https://semble.so semble.so
at-proto semble chrome-extension
at feature/custom-pds-support 198 lines 4.9 kB view raw
1/** 2 * Background Service Worker 3 * Handles session management and Semble API communication 4 */ 5 6// Import Semble API client functions 7importScripts('../lib/atproto.js'); 8 9// Session state 10let session = null; 11 12/** 13 * Initialize session on extension startup 14 */ 15chrome.runtime.onStartup.addListener(async () => { 16 await loadSession(); 17}); 18 19/** 20 * Load session from storage 21 */ 22async function loadSession() { 23 try { 24 const result = await chrome.storage.local.get(['session']); 25 if (result.session) { 26 session = result.session; 27 console.log('Session loaded from storage'); 28 } 29 } catch (error) { 30 console.error('Failed to load session:', error); 31 } 32} 33 34/** 35 * Save session to storage 36 */ 37async function saveSession(sessionData) { 38 try { 39 session = sessionData; 40 await chrome.storage.local.set({ session: sessionData }); 41 console.log('Session saved to storage'); 42 } catch (error) { 43 console.error('Failed to save session:', error); 44 throw error; 45 } 46} 47 48/** 49 * Clear session from storage 50 */ 51async function clearSession() { 52 try { 53 session = null; 54 await chrome.storage.local.remove('session'); 55 console.log('Session cleared'); 56 } catch (error) { 57 console.error('Failed to clear session:', error); 58 } 59} 60 61/** 62 * Authenticate with Semble using Bluesky credentials 63 * @param {string} identifier - User handle 64 * @param {string} password - App password 65 * @param {string} [service] - Optional PDS service URL 66 */ 67async function authenticate(identifier, password, service) { 68 try { 69 console.log('Authenticating with:', { 70 identifier, 71 hasPassword: !!password, 72 service: service || 'default (bsky.social)' 73 }); 74 75 const sessionData = await createSession(identifier, password, service); 76 await saveSession(sessionData); 77 78 console.log('Authentication successful'); 79 return { success: true, session: sessionData }; 80 } catch (error) { 81 console.error('Authentication failed:', error); 82 return { success: false, error: error.message }; 83 } 84} 85 86/** 87 * Ensure we have a valid session 88 * Refreshes token if expired 89 */ 90async function ensureValidSession() { 91 if (!session) { 92 await loadSession(); 93 } 94 95 if (!session) { 96 throw new Error('Not authenticated. Please log in.'); 97 } 98 99 // TODO: Add token expiration check and refresh logic using refreshSession() 100 // For now, we'll assume the token is valid 101 102 return session; 103} 104 105/** 106 * Get user's collections from Semble 107 */ 108async function getCollections() { 109 try { 110 const session = await ensureValidSession(); 111 const collections = await listCollections(session.accessToken); 112 return { success: true, collections }; 113 } catch (error) { 114 console.error('Failed to fetch collections:', error); 115 return { success: false, error: error.message }; 116 } 117} 118 119/** 120 * Save URL card to Semble collection 121 */ 122async function saveCard(url, metadata, note, collectionId) { 123 try { 124 const session = await ensureValidSession(); 125 126 // Semble API handles everything in one call: 127 // - Fetches URL metadata 128 // - Creates URL card 129 // - Creates note card (if note provided) 130 // - Adds to collection(s) 131 // - Publishes to ATproto 132 const collectionIds = collectionId ? [collectionId] : []; 133 134 const result = await addUrlToLibrary( 135 session.accessToken, 136 url, 137 note, 138 collectionIds 139 ); 140 141 console.log('Card saved to Semble:', result); 142 143 return { 144 success: true, 145 urlCardId: result.urlCardId, 146 noteCardId: result.noteCardId, 147 }; 148 } catch (error) { 149 console.error('Failed to save card:', error); 150 return { success: false, error: error.message }; 151 } 152} 153 154/** 155 * Handle messages from popup and content scripts 156 */ 157chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 158 console.log('Background received message:', request); 159 160 switch (request.action) { 161 case 'authenticate': 162 authenticate(request.identifier, request.password, request.service) 163 .then(sendResponse); 164 return true; // Keep channel open for async response 165 166 case 'getSession': 167 ensureValidSession() 168 .then(session => sendResponse({ success: true, session })) 169 .catch(error => sendResponse({ success: false, error: error.message })); 170 return true; 171 172 case 'clearSession': 173 clearSession() 174 .then(() => sendResponse({ success: true })) 175 .catch(error => sendResponse({ success: false, error: error.message })); 176 return true; 177 178 case 'getCollections': 179 getCollections().then(sendResponse); 180 return true; 181 182 case 'saveCard': 183 saveCard( 184 request.url, 185 request.metadata, 186 request.note, 187 request.collectionId 188 ).then(sendResponse); 189 return true; 190 191 default: 192 console.warn('Unknown action:', request.action); 193 sendResponse({ success: false, error: 'Unknown action' }); 194 } 195}); 196 197// Load session on script initialization 198loadSession();