A Chrome extension to quickly capture URLs into Semble Collections at https://semble.so semble.so
at-proto semble chrome-extension
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

at feature/custom-pds-support 182 lines 5.2 kB view raw
1/** 2 * Semble API Client Library 3 * Handles authentication and communication with Semble backend 4 */ 5 6const SEMBLE_API_URL = 'https://api.semble.so'; 7// For local development: 'http://127.0.0.1:3000' 8 9/** 10 * Login with Bluesky app password to get Semble tokens 11 * @param {string} identifier - User handle (e.g., user.bsky.social) 12 * @param {string} password - Bluesky app password 13 * @param {string} [service] - Optional PDS service URL (e.g., https://bsky.social) 14 * @returns {Promise<{accessToken: string, refreshToken: string}>} 15 */ 16async function createSession(identifier, password, service) { 17 const payload = { 18 identifier, 19 appPassword: password, 20 }; 21 22 // Include service parameter if provided (for custom PDS servers) 23 if (service) { 24 payload.service = service; 25 } 26 27 const response = await fetch(`${SEMBLE_API_URL}/api/users/login/app-password`, { 28 method: 'POST', 29 headers: { 30 'Content-Type': 'application/json', 31 }, 32 body: JSON.stringify(payload), 33 }); 34 35 if (!response.ok) { 36 const error = await response.json().catch(() => ({})); 37 38 // Enhanced error handling 39 let errorMessage = error.message || response.statusText; 40 41 // Provide more specific error messages 42 if (response.status === 401) { 43 errorMessage = 'Invalid identifier or password'; 44 } else if (response.status === 400) { 45 errorMessage = error.message || 'Invalid request. Please check your inputs.'; 46 } else if (response.status === 500) { 47 errorMessage = 'Server error. Please try again later.'; 48 } else if (response.status === 503) { 49 errorMessage = 'Service temporarily unavailable. Please try again later.'; 50 } 51 52 throw new Error(`Authentication failed: ${errorMessage}`); 53 } 54 55 return await response.json(); 56} 57 58/** 59 * Refresh Semble access token 60 * @param {string} refreshToken - Refresh token 61 * @returns {Promise<{accessToken: string, refreshToken: string}>} 62 */ 63async function refreshSession(refreshToken) { 64 const response = await fetch(`${SEMBLE_API_URL}/api/users/oauth/refresh`, { 65 method: 'POST', 66 headers: { 67 'Content-Type': 'application/json', 68 }, 69 body: JSON.stringify({ 70 refreshToken, 71 }), 72 }); 73 74 if (!response.ok) { 75 throw new Error('Token refresh failed'); 76 } 77 78 return await response.json(); 79} 80 81/** 82 * Get user's Semble collections 83 * @param {string} accessToken - Semble access token 84 * @returns {Promise<Array>} 85 */ 86async function listCollections(accessToken) { 87 const response = await fetch(`${SEMBLE_API_URL}/api/collections`, { 88 headers: { 89 'Authorization': `Bearer ${accessToken}`, 90 }, 91 }); 92 93 if (!response.ok) { 94 const error = await response.json().catch(() => ({})); 95 throw new Error(`Failed to fetch collections: ${error.message || response.statusText}`); 96 } 97 98 const data = await response.json(); 99 return data.collections || []; 100} 101 102/** 103 * Add URL to library via Semble API (creates card, fetches metadata, adds to collections) 104 * @param {string} accessToken - Semble access token 105 * @param {string} url - URL to add 106 * @param {string} [note] - Optional note 107 * @param {string[]} [collectionIds] - Optional collection IDs to add card to 108 * @returns {Promise<{urlCardId: string, noteCardId?: string}>} 109 */ 110async function addUrlToLibrary(accessToken, url, note, collectionIds) { 111 const payload = { 112 url, 113 }; 114 115 if (note && note.trim()) { 116 payload.note = note.trim(); 117 } 118 119 if (collectionIds && collectionIds.length > 0) { 120 payload.collectionIds = collectionIds; 121 } 122 123 console.log('Adding URL to Semble library:', { 124 url, 125 hasNote: !!payload.note, 126 collectionCount: collectionIds?.length || 0, 127 }); 128 129 const response = await fetch(`${SEMBLE_API_URL}/api/cards/library/urls`, { 130 method: 'POST', 131 headers: { 132 'Authorization': `Bearer ${accessToken}`, 133 'Content-Type': 'application/json', 134 }, 135 body: JSON.stringify(payload), 136 }); 137 138 if (!response.ok) { 139 const error = await response.json().catch(() => ({})); 140 console.error('Failed to add URL to library:', { 141 status: response.status, 142 statusText: response.statusText, 143 error: error, 144 }); 145 throw new Error(`Failed to add URL: ${error.message || response.statusText}`); 146 } 147 148 const result = await response.json(); 149 console.log('URL added to Semble successfully:', result); 150 return result; 151} 152 153// Legacy function names for compatibility - no longer used 154async function createRecord() { 155 throw new Error('createRecord is deprecated - use addUrlToLibrary instead'); 156} 157 158async function createUrlCard() { 159 throw new Error('createUrlCard is deprecated - use addUrlToLibrary instead'); 160} 161 162async function createNoteCard() { 163 throw new Error('createNoteCard is deprecated - note is now part of addUrlToLibrary'); 164} 165 166async function createCollectionLink() { 167 throw new Error('createCollectionLink is deprecated - collectionIds is now part of addUrlToLibrary'); 168} 169 170// Export functions for use in other scripts 171if (typeof module !== 'undefined' && module.exports) { 172 module.exports = { 173 createSession, 174 refreshSession, 175 listCollections, 176 addUrlToLibrary, 177 createRecord, 178 createUrlCard, 179 createNoteCard, 180 createCollectionLink, 181 }; 182}