A Chrome extension to quickly capture URLs into Semble Collections at https://semble.so
semble.so
at-proto
semble
chrome-extension
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 */
64async function authenticate(identifier, password) {
65 try {
66 const sessionData = await createSession(identifier, password);
67 await saveSession(sessionData);
68 return { success: true, session: sessionData };
69 } catch (error) {
70 console.error('Authentication failed:', error);
71 return { success: false, error: error.message };
72 }
73}
74
75/**
76 * Ensure we have a valid session
77 * Refreshes token if expired
78 */
79async function ensureValidSession() {
80 if (!session) {
81 await loadSession();
82 }
83
84 if (!session) {
85 throw new Error('Not authenticated. Please log in.');
86 }
87
88 // TODO: Add token expiration check and refresh logic using refreshSession()
89 // For now, we'll assume the token is valid
90
91 return session;
92}
93
94/**
95 * Get user's collections from Semble
96 */
97async function getCollections() {
98 try {
99 const session = await ensureValidSession();
100 const collections = await listCollections(session.accessToken);
101 return { success: true, collections };
102 } catch (error) {
103 console.error('Failed to fetch collections:', error);
104 return { success: false, error: error.message };
105 }
106}
107
108/**
109 * Save URL card to Semble collection
110 */
111async function saveCard(url, metadata, note, collectionId) {
112 try {
113 const session = await ensureValidSession();
114
115 // Semble API handles everything in one call:
116 // - Fetches URL metadata
117 // - Creates URL card
118 // - Creates note card (if note provided)
119 // - Adds to collection(s)
120 // - Publishes to ATproto
121 const collectionIds = collectionId ? [collectionId] : [];
122
123 const result = await addUrlToLibrary(
124 session.accessToken,
125 url,
126 note,
127 collectionIds
128 );
129
130 console.log('Card saved to Semble:', result);
131
132 return {
133 success: true,
134 urlCardId: result.urlCardId,
135 noteCardId: result.noteCardId,
136 };
137 } catch (error) {
138 console.error('Failed to save card:', error);
139 return { success: false, error: error.message };
140 }
141}
142
143/**
144 * Handle messages from popup and content scripts
145 */
146chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
147 console.log('Background received message:', request);
148
149 switch (request.action) {
150 case 'authenticate':
151 authenticate(request.identifier, request.password)
152 .then(sendResponse);
153 return true; // Keep channel open for async response
154
155 case 'getSession':
156 ensureValidSession()
157 .then(session => sendResponse({ success: true, session }))
158 .catch(error => sendResponse({ success: false, error: error.message }));
159 return true;
160
161 case 'clearSession':
162 clearSession()
163 .then(() => sendResponse({ success: true }))
164 .catch(error => sendResponse({ success: false, error: error.message }));
165 return true;
166
167 case 'getCollections':
168 getCollections().then(sendResponse);
169 return true;
170
171 case 'saveCard':
172 saveCard(
173 request.url,
174 request.metadata,
175 request.note,
176 request.collectionId
177 ).then(sendResponse);
178 return true;
179
180 default:
181 console.warn('Unknown action:', request.action);
182 sendResponse({ success: false, error: 'Unknown action' });
183 }
184});
185
186// Load session on script initialization
187loadSession();