this repo has no description
0
fork

Configure Feed

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

Add Weaver support and reorder services (#16)

authored by

Alice and committed by
GitHub
2c4d2d7d 159609fd

+247 -228
+2 -1
README.md
··· 6 6 7 7 ## Supported services 8 8 9 - - [deer.social](https://deer.social) 10 9 - [bsky.app](https://bsky.app) 11 10 - [atp.tools](https://atp.tools) 11 + - [alpha.weaver.sh](https://alpha.weaver.sh) 12 12 - [pdsls.dev](https://pdsls.dev) 13 + - [deer.social](https://deer.social) 13 14 - [repoview.edavis.dev](https://repoview.edavis.dev) 14 15 - [astrolabe.at](https://astrolabe.at) 15 16 - [clearsky.app](https://clearsky.app)
+11 -5
src/popup/popup.css
··· 21 21 body { 22 22 font-family: -apple-system, sans-serif; 23 23 margin: 0; 24 - padding: 10px 10px 10px 10px; 24 + padding: 8px; 25 25 width: 280px; 26 26 } 27 27 ul { ··· 30 30 margin: 0; 31 31 } 32 32 li { 33 - margin: 4px 0; 33 + margin: 3px 0; 34 34 } 35 35 button { 36 36 width: 100%; 37 37 text-align: left; 38 - padding: 6px 8px; 38 + padding: 5px 8px; 39 39 border: 1px solid #ccc; 40 40 border-radius: 6px; 41 41 background: #fafafa; ··· 49 49 display: block; 50 50 width: 100%; 51 51 text-align: left; 52 - padding: 6px 8px; 52 + padding: 5px 8px; 53 53 border: 1px solid #ccc; 54 54 border-radius: 6px; 55 55 background: #fafafa; ··· 64 64 text-decoration: none; 65 65 } 66 66 67 - hr { 67 + .divider { 68 68 border: 0; 69 69 height: 1px; 70 70 background-color: #ccc; 71 + margin: 6px 0; 71 72 } 72 73 73 74 #emptyCacheBtn { 74 75 width: auto; 75 76 text-align: center; 77 + font-size: 12px; 76 78 } 77 79 78 80 @media (prefers-color-scheme: dark) { ··· 138 140 color: #aaa; 139 141 } 140 142 } 143 + #controls { 144 + text-align: center; 145 + margin-top: 6px; 146 + }
+3 -3
src/popup/popup.html
··· 7 7 </head> 8 8 <body> 9 9 <ul id="dest"></ul> 10 - <hr style="margin-top: 10px; margin-bottom: 10px" /> 11 - <div style="text-align: center"> 12 - <button id="emptyCacheBtn" style="padding: 4px 8px; font-size: 12px">Empty Handle+DID Cache</button> 10 + <hr class="divider" /> 11 + <div id="controls"> 12 + <button id="emptyCacheBtn">Empty Handle+DID Cache</button> 13 13 </div> 14 14 <div id="debugInfo" class="debug-info" hidden></div> 15 15 <script type="module" src="./popup.ts"></script>
+226 -218
src/shared/services.ts
··· 4 4 emoji: string; 5 5 name: string; 6 6 contentSupport: 'only-profiles' | 'only-posts' | 'profiles-and-posts' | 'full'; 7 - 8 - // Input parsing configuration 9 7 parsing?: { 10 - hostname: string | string[]; // Support multiple hostnames 8 + hostname: string | string[]; 11 9 patterns?: { 12 - // For services accepting handle OR DID (e.g., bsky.app/profile/X) 13 10 profileIdentifier?: RegExp; 14 - 15 - // For handle-only services (e.g., cred.blue/alice) 16 11 profileHandle?: RegExp; 17 - 18 - // For DID-only services (e.g., clearsky.app/did:plc:xyz) 19 12 profileDid?: RegExp; 20 - 21 - // For query parameter extraction (e.g., ?q=did:plc:xyz) 22 13 queryParam?: string; 23 - 24 - // For complex cases (e.g., skythread's multi-param format) 25 14 customParser?: (url: URL) => string | null; 26 15 }; 27 16 }; 28 - 29 - // Output building configuration 30 17 buildUrl: (info: TransformInfo) => string | null; 31 18 requiredFields?: { 32 19 handle?: boolean; 33 20 rkey?: boolean; 34 - plcOnly?: boolean; // Only for did:plc, not did:web 21 + plcOnly?: boolean; 35 22 }; 36 23 } 37 24 38 - export const SERVICES: Record<string, ServiceConfig> = { 39 - DEER_SOCIAL: { 40 - emoji: '🦌', 41 - name: 'deer.social', 42 - contentSupport: 'full', 43 - parsing: { 44 - hostname: 'deer.social', 45 - patterns: { 46 - // Matches /profile/IDENTIFIER where IDENTIFIER can be handle or DID 47 - profileIdentifier: /^\/profile\/([^/]+)/, 25 + const SERVICE_LIST: [string, ServiceConfig][] = [ 26 + [ 27 + 'BSKY_APP', 28 + { 29 + emoji: '🦋', 30 + name: 'bsky.app', 31 + contentSupport: 'full', 32 + parsing: { 33 + hostname: 'bsky.app', 34 + patterns: { 35 + profileIdentifier: /^\/profile\/([^/]+)/, 36 + }, 48 37 }, 38 + buildUrl: (info) => `https://bsky.app${info.bskyAppPath}`, 49 39 }, 50 - buildUrl: (info) => `https://deer.social${info.bskyAppPath}`, 51 - }, 52 - 53 - BSKY_APP: { 54 - emoji: '🦋', 55 - name: 'bsky.app', 56 - contentSupport: 'full', 57 - parsing: { 58 - hostname: 'bsky.app', 59 - patterns: { 60 - // Matches /profile/IDENTIFIER where IDENTIFIER can be handle or DID 61 - profileIdentifier: /^\/profile\/([^/]+)/, 40 + ], 41 + [ 42 + 'DEER_SOCIAL', 43 + { 44 + emoji: '🦌', 45 + name: 'deer.social', 46 + contentSupport: 'full', 47 + parsing: { 48 + hostname: 'deer.social', 49 + patterns: { 50 + profileIdentifier: /^\/profile\/([^/]+)/, 51 + }, 62 52 }, 53 + buildUrl: (info) => `https://deer.social${info.bskyAppPath}`, 63 54 }, 64 - buildUrl: (info) => `https://bsky.app${info.bskyAppPath}`, 65 - }, 66 - 67 - ATP_TOOLS: { 68 - emoji: '🛠️', 69 - name: 'atp.tools', 70 - contentSupport: 'full', 71 - parsing: { 72 - hostname: 'atp.tools', 73 - patterns: { 74 - customParser: (url) => { 75 - // ATP Tools uses at:/ instead of at:// in URLs 76 - const atMatch = /at:\/[\w:.\-/]+/.exec(url.pathname); 77 - if (atMatch) { 78 - // Convert at:/ to at:// for canonicalization 79 - return atMatch[0].replace('at:/', 'at://'); 80 - } 81 - return null; 55 + ], 56 + [ 57 + 'WEAVER', 58 + { 59 + emoji: '🧵', 60 + name: 'alpha.weaver.sh', 61 + contentSupport: 'full', 62 + buildUrl: (info) => (info.atUri ? `https://alpha.weaver.sh/record/${info.atUri}` : null), 63 + }, 64 + ], 65 + [ 66 + 'ATP_TOOLS', 67 + { 68 + emoji: '🛠️', 69 + name: 'atp.tools', 70 + contentSupport: 'full', 71 + parsing: { 72 + hostname: 'atp.tools', 73 + patterns: { 74 + customParser: (url) => { 75 + const atMatch = /at:\/[\w:.\-/]+/.exec(url.pathname); 76 + return atMatch ? atMatch[0].replace('at:/', 'at://') : null; 77 + }, 82 78 }, 83 79 }, 80 + buildUrl: (info) => (info.atUri ? `https://atp.tools/${info.atUri.replace('at://', 'at:/')}` : ''), 84 81 }, 85 - buildUrl: (info) => (info.atUri ? `https://atp.tools/${info.atUri.replace('at://', 'at:/')}` : ''), 86 - }, 87 - 88 - PDSLS_DEV: { 89 - emoji: '⚙️', 90 - name: 'pdsls.dev', 91 - contentSupport: 'full', 92 - parsing: { 93 - hostname: 'pdsls.dev', 94 - patterns: { 95 - customParser: (url) => { 96 - // Extract AT URI from pathname: /at://did:plc:xyz/app.bsky.feed.post/abc 97 - const atMatch = /at:\/\/[\w:.\-/]+/.exec(url.pathname); 98 - return atMatch ? atMatch[0] : null; 82 + ], 83 + [ 84 + 'PDSLS_DEV', 85 + { 86 + emoji: '⚙️', 87 + name: 'pdsls.dev', 88 + contentSupport: 'full', 89 + parsing: { 90 + hostname: 'pdsls.dev', 91 + patterns: { 92 + customParser: (url) => { 93 + const atMatch = /at:\/\/[\w:.\-/]+/.exec(url.pathname); 94 + return atMatch ? atMatch[0] : null; 95 + }, 99 96 }, 100 97 }, 98 + buildUrl: (info) => `https://pdsls.dev/${info.atUri}`, 101 99 }, 102 - buildUrl: (info) => `https://pdsls.dev/${info.atUri}`, 103 - }, 104 - 105 - REPOVIEW: { 106 - emoji: '📁', 107 - name: 'repoview.edavis.dev', 108 - contentSupport: 'full', 109 - parsing: { 110 - hostname: 'repoview.edavis.dev', 111 - patterns: { 112 - customParser: (url) => { 113 - // Extract AT URI from pathname: /at://did:plc:xyz/app.bsky.feed.post/abc 114 - const atMatch = /at:\/\/[\w:.\-/]+/.exec(url.pathname); 115 - return atMatch ? atMatch[0] : null; 100 + ], 101 + [ 102 + 'REPOVIEW', 103 + { 104 + emoji: '📁', 105 + name: 'repoview.edavis.dev', 106 + contentSupport: 'full', 107 + parsing: { 108 + hostname: 'repoview.edavis.dev', 109 + patterns: { 110 + customParser: (url) => { 111 + const atMatch = /at:\/\/[\w:.\-/]+/.exec(url.pathname); 112 + return atMatch ? atMatch[0] : null; 113 + }, 116 114 }, 117 115 }, 116 + buildUrl: (info) => `https://repoview.edavis.dev/${info.atUri}`, 118 117 }, 119 - buildUrl: (info) => `https://repoview.edavis.dev/${info.atUri}`, 120 - }, 121 - 122 - ASTROLABE: { 123 - emoji: '🔭', 124 - name: 'astrolabe.at', 125 - contentSupport: 'full', 126 - parsing: { 127 - hostname: 'astrolabe.at', 128 - patterns: { 129 - customParser: (url) => { 130 - // astrolabe uses at/ instead of at:// in URLs 131 - const atMatch = /at\/[\w:.\-/]+/.exec(url.pathname); 132 - if (atMatch) { 133 - // Convert at/ to at:// for canonicalization 134 - return atMatch[0].replace('at/', 'at://'); 135 - } 136 - return null; 118 + ], 119 + [ 120 + 'ASTROLABE', 121 + { 122 + emoji: '🔭', 123 + name: 'astrolabe.at', 124 + contentSupport: 'full', 125 + parsing: { 126 + hostname: 'astrolabe.at', 127 + patterns: { 128 + customParser: (url) => { 129 + const atMatch = /at\/[\w:.\-/]+/.exec(url.pathname); 130 + return atMatch ? atMatch[0].replace('at/', 'at://') : null; 131 + }, 137 132 }, 138 133 }, 134 + buildUrl: (info) => (info.atUri ? `https://astrolabe.at/${info.atUri.replace('at://', 'at/')}` : ''), 139 135 }, 140 - buildUrl: (info) => (info.atUri ? `https://astrolabe.at/${info.atUri.replace('at://', 'at/')}` : ''), 141 - }, 142 - 143 - CLEARSKY: { 144 - emoji: '☀️', 145 - name: 'clearsky', 146 - contentSupport: 'only-profiles', 147 - parsing: { 148 - hostname: 'clearsky.app', 149 - patterns: { 150 - // Clearsky URLs contain DIDs: /did:plc:xyz/blocked-by 151 - profileDid: /^\/(did:[^/]+)/, 136 + ], 137 + [ 138 + 'CLEARSKY', 139 + { 140 + emoji: '☀️', 141 + name: 'clearsky', 142 + contentSupport: 'only-profiles', 143 + parsing: { 144 + hostname: 'clearsky.app', 145 + patterns: { 146 + profileDid: /^\/(did:[^/]+)/, 147 + }, 152 148 }, 149 + buildUrl: (info) => (info.handle ? `https://clearsky.app/${info.handle}/blocking/blocked-by` : null), 150 + requiredFields: { handle: true }, 153 151 }, 154 - buildUrl: (info) => `https://clearsky.app/${info.did}/blocked-by`, 155 - }, 156 - 157 - SKYTHREAD: { 158 - emoji: '☁️', 159 - name: 'skythread', 160 - contentSupport: 'only-posts', 161 - parsing: { 162 - hostname: 'blue.mackuba.eu', 163 - patterns: { 164 - customParser: (url) => { 165 - if (url.pathname.startsWith('/skythread')) { 166 - const author = url.searchParams.get('author'); 167 - const post = url.searchParams.get('post'); 168 - if (author?.startsWith('did:') && post) { 169 - // Return in a format that canonicalize can handle 170 - return `${author}/app.bsky.feed.post/${post}`; 152 + ], 153 + [ 154 + 'SKYTHREAD', 155 + { 156 + emoji: '☁️', 157 + name: 'skythread', 158 + contentSupport: 'only-posts', 159 + parsing: { 160 + hostname: 'blue.mackuba.eu', 161 + patterns: { 162 + customParser: (url) => { 163 + if (url.pathname.startsWith('/skythread')) { 164 + const author = url.searchParams.get('author'); 165 + const post = url.searchParams.get('post'); 166 + if (author?.startsWith('did:') && post) { 167 + return `${author}/app.bsky.feed.post/${post}`; 168 + } 171 169 } 172 - } 173 - return null; 170 + return null; 171 + }, 174 172 }, 175 173 }, 174 + buildUrl: (info) => 175 + info.rkey ? `https://blue.mackuba.eu/skythread/?author=${info.did}&post=${info.rkey}` : null, 176 + requiredFields: { rkey: true }, 176 177 }, 177 - buildUrl: (info) => (info.rkey ? `https://blue.mackuba.eu/skythread/?author=${info.did}&post=${info.rkey}` : null), 178 - requiredFields: { rkey: true }, 179 - }, 180 - 181 - CRED_BLUE: { 182 - emoji: '🍥', 183 - name: 'cred.blue', 184 - contentSupport: 'only-profiles', 185 - parsing: { 186 - hostname: 'cred.blue', 187 - patterns: { 188 - // cred.blue/handle (no @ prefix) 189 - profileHandle: /^\/([^/]+)$/, 178 + ], 179 + [ 180 + 'CRED_BLUE', 181 + { 182 + emoji: '🍥', 183 + name: 'cred.blue', 184 + contentSupport: 'only-profiles', 185 + parsing: { 186 + hostname: 'cred.blue', 187 + patterns: { 188 + profileHandle: /^\/([^/]+)$/, 189 + }, 190 190 }, 191 + buildUrl: (info) => (info.handle ? `https://cred.blue/${info.handle}` : null), 192 + requiredFields: { handle: true }, 191 193 }, 192 - buildUrl: (info) => (info.handle ? `https://cred.blue/${info.handle}` : null), 193 - requiredFields: { handle: true }, 194 - }, 195 - 196 - TANGLED_SH: { 197 - emoji: '🪢', 198 - name: 'tangled.sh', 199 - contentSupport: 'only-profiles', 200 - parsing: { 201 - hostname: 'tangled.sh', 202 - patterns: { 203 - // tangled.sh/handle or tangled.sh/@handle 204 - profileHandle: /^\/@?([^/]+)$/, 194 + ], 195 + [ 196 + 'TANGLED_SH', 197 + { 198 + emoji: '🪢', 199 + name: 'tangled.sh', 200 + contentSupport: 'only-profiles', 201 + parsing: { 202 + hostname: 'tangled.sh', 203 + patterns: { 204 + profileHandle: /^\/@?([^/]+)$/, 205 + }, 205 206 }, 207 + buildUrl: (info) => (info.handle ? `https://tangled.sh/@${info.handle}` : null), 208 + requiredFields: { handle: true }, 206 209 }, 207 - buildUrl: (info) => (info.handle ? `https://tangled.sh/@${info.handle}` : null), 208 - requiredFields: { handle: true }, 209 - }, 210 - 211 - FRONTPAGE_FYI: { 212 - emoji: '📰', 213 - name: 'frontpage.fyi', 214 - contentSupport: 'only-profiles', 215 - parsing: { 216 - hostname: 'frontpage.fyi', 217 - patterns: { 218 - // frontpage.fyi/profile/handle 219 - profileHandle: /^\/profile\/([^/]+)$/, 210 + ], 211 + [ 212 + 'FRONTPAGE_FYI', 213 + { 214 + emoji: '📰', 215 + name: 'frontpage.fyi', 216 + contentSupport: 'only-profiles', 217 + parsing: { 218 + hostname: 'frontpage.fyi', 219 + patterns: { 220 + profileHandle: /^\/profile\/([^/]+)$/, 221 + }, 220 222 }, 223 + buildUrl: (info) => (info.handle ? `https://frontpage.fyi/profile/${info.handle}` : null), 224 + requiredFields: { handle: true, plcOnly: true }, 221 225 }, 222 - buildUrl: (info) => (info.handle ? `https://frontpage.fyi/profile/${info.handle}` : null), 223 - requiredFields: { handle: true, plcOnly: true }, 224 - }, 225 - 226 - BOAT_KELINCI: { 227 - emoji: '⛵', 228 - name: 'boat.kelinci', 229 - contentSupport: 'only-profiles', 230 - parsing: { 231 - hostname: 'boat.kelinci.net', 232 - patterns: { 233 - // Extract DID from query parameter ?q=did:plc:xyz 234 - queryParam: 'q', 226 + ], 227 + [ 228 + 'BOAT_KELINCI', 229 + { 230 + emoji: '⛵', 231 + name: 'boat.kelinci', 232 + contentSupport: 'only-profiles', 233 + parsing: { 234 + hostname: 'boat.kelinci.net', 235 + patterns: { 236 + queryParam: 'q', 237 + }, 235 238 }, 239 + buildUrl: (info) => `https://boat.kelinci.net/plc-oplogs?q=${info.did}`, 240 + requiredFields: { plcOnly: true }, 236 241 }, 237 - buildUrl: (info) => `https://boat.kelinci.net/plc-oplogs?q=${info.did}`, 238 - requiredFields: { plcOnly: true }, 239 - }, 240 - 241 - PLC_DIRECTORY: { 242 - emoji: '🪪', 243 - name: 'plc.directory', 244 - contentSupport: 'only-profiles', 245 - parsing: { 246 - hostname: 'plc.directory', 247 - patterns: { 248 - // plc.directory/did:plc:xyz 249 - profileDid: /^\/(did:plc:[^/]+)/, 242 + ], 243 + [ 244 + 'PLC_DIRECTORY', 245 + { 246 + emoji: '🪪', 247 + name: 'plc.directory', 248 + contentSupport: 'only-profiles', 249 + parsing: { 250 + hostname: 'plc.directory', 251 + patterns: { 252 + profileDid: /^\/(did:plc:[^/]+)/, 253 + }, 250 254 }, 255 + buildUrl: (info) => `https://plc.directory/${info.did}`, 256 + requiredFields: { plcOnly: true }, 251 257 }, 252 - buildUrl: (info) => `https://plc.directory/${info.did}`, 253 - requiredFields: { plcOnly: true }, 254 - }, 255 - 256 - TOOLIFY_BLUE: { 257 - emoji: '🔧', 258 - name: 'toolify.blue', 259 - contentSupport: 'profiles-and-posts', 260 - parsing: { 261 - hostname: 'toolify.blue', 262 - patterns: { 263 - // Matches /profile/IDENTIFIER where IDENTIFIER can be handle or DID 264 - profileIdentifier: /^\/profile\/([^/]+)/, 258 + ], 259 + [ 260 + 'TOOLIFY_BLUE', 261 + { 262 + emoji: '🔧', 263 + name: 'toolify.blue', 264 + contentSupport: 'profiles-and-posts', 265 + parsing: { 266 + hostname: 'toolify.blue', 267 + patterns: { 268 + profileIdentifier: /^\/profile\/([^/]+)/, 269 + }, 265 270 }, 271 + buildUrl: (info) => `https://toolify.blue${info.bskyAppPath}`, 272 + requiredFields: { plcOnly: true }, 266 273 }, 267 - buildUrl: (info) => `https://toolify.blue${info.bskyAppPath}`, 268 - requiredFields: { plcOnly: true }, 274 + ], 275 + ]; 276 + 277 + export const SERVICES: Record<string, ServiceConfig> = SERVICE_LIST.reduce<Record<string, ServiceConfig>>( 278 + (acc, [key, config]) => { 279 + acc[key] = config; 280 + return acc; 269 281 }, 270 - }; 282 + {}, 283 + ); 271 284 272 285 /** 273 286 * Builds a list of destination link objects from canonical info using service configuration. ··· 281 294 const destinations: { label: string; url: string }[] = []; 282 295 283 296 for (const service of Object.values(SERVICES)) { 284 - // Check required fields 285 297 if (service.requiredFields) { 286 298 if (service.requiredFields.handle && !info.handle) continue; 287 299 if (service.requiredFields.rkey && !info.rkey) continue; 288 300 if (service.requiredFields.plcOnly && isDidWeb) continue; 289 301 } 290 302 291 - // Strict mode filtering 292 303 if (strictMode && info.rkey) { 293 - // When viewing content (posts/feeds/lists), apply strict filtering 294 304 if (info.nsid === 'app.bsky.feed.post') { 295 - // For posts: include only-posts, profiles-and-posts, and full 296 305 if (!['only-posts', 'profiles-and-posts', 'full'].includes(service.contentSupport)) { 297 306 continue; 298 307 } 299 308 } else if (info.nsid === 'app.bsky.feed.generator' || info.nsid === 'app.bsky.graph.list') { 300 - // For feeds/lists: include only full support services 301 309 if (service.contentSupport !== 'full') { 302 310 continue; 303 311 }
+5 -1
tests/transform.test.ts
··· 313 313 else if (dest.label.includes('bsky.app')) acc['bsky.app'] = dest.url; 314 314 else if (dest.label.includes('pdsls.dev')) acc['pdsls.dev'] = dest.url; 315 315 else if (dest.label.includes('atp.tools')) acc['atp.tools'] = dest.url; 316 + else if (dest.label.includes('alpha.weaver.sh')) acc['alpha.weaver.sh'] = dest.url; 316 317 else if (dest.label.includes('clearsky')) acc.clearsky = dest.url; 317 318 else if (dest.label.includes('skythread')) acc.skythread = dest.url; 318 319 else if (dest.label.includes('cred.blue')) acc['cred.blue'] = dest.url; ··· 330 331 expect(destMap['pdsls.dev']).toBe( 331 332 'https://pdsls.dev/at://did:plc:kkkcb7sys7623hcf7oefcffg/app.bsky.feed.post/3lqcw7n4gly2u', 332 333 ); 334 + expect(destMap['alpha.weaver.sh']).toBe( 335 + 'https://alpha.weaver.sh/record/at://did:plc:kkkcb7sys7623hcf7oefcffg/app.bsky.feed.post/3lqcw7n4gly2u', 336 + ); 333 337 expect(destMap['atp.tools']).toBe( 334 338 'https://atp.tools/at:/did:plc:kkkcb7sys7623hcf7oefcffg/app.bsky.feed.post/3lqcw7n4gly2u', 335 339 ); 336 - expect(destMap.clearsky).toBe('https://clearsky.app/did:plc:kkkcb7sys7623hcf7oefcffg/blocked-by'); 340 + expect(destMap.clearsky).toBe('https://clearsky.app/now.alice.mosphere.at/blocking/blocked-by'); 337 341 expect(destMap.skythread).toBe( 338 342 'https://blue.mackuba.eu/skythread/?author=did:plc:kkkcb7sys7623hcf7oefcffg&post=3lqcw7n4gly2u', 339 343 );