This repository has no description
0

Configure Feed

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

fix things

+1703 -208
+12
app/.env
··· 1 + # Supabase configuration 2 + SUPABASE_URL=your-supabase-url 3 + SUPABASE_SERVICE_ROLE_KEY=your-service-role-key 4 + 5 + # Bluesky Jetstream configuration 6 + JETSTREAM_URL=wss://jetstream2.us-west.bsky.network/subscribe 7 + FLUSHING_STATUS_NSID=im.flushing.right.now 8 + 9 + # Optional: Bluesky API configuration 10 + # Only needed if you want to authenticate with the Bluesky API 11 + # BLUESKY_API_USERNAME=your-bluesky-username 12 + # BLUESKY_API_PASSWORD=your-bluesky-password
+5 -1
app/.env.example
··· 1 1 # Supabase configuration 2 - NEXT_PUBLIC_SUPABASE_URL=your-supabase-url 2 + SUPABASE_URL=your-supabase-url 3 3 SUPABASE_SERVICE_ROLE_KEY=your-service-role-key 4 + 5 + # Bluesky Jetstream configuration 6 + JETSTREAM_URL=wss://jetstream2.us-west.bsky.network/subscribe 7 + FLUSHING_STATUS_NSID=im.flushing.right.now 4 8 5 9 # Optional: Bluesky API configuration 6 10 # Only needed if you want to authenticate with the Bluesky API
+1
app/.gitignore
··· 1 + node_modules
+773
app/package-lock.json
··· 1 + { 2 + "name": "im-flushing", 3 + "version": "0.1.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "im-flushing", 9 + "version": "0.1.0", 10 + "dependencies": { 11 + "@atproto/api": "^0.12.0", 12 + "@supabase/supabase-js": "^2.49.1", 13 + "cbor-web": "^8.1.0", 14 + "dotenv": "^16.4.7", 15 + "next": "^14.1.0", 16 + "react": "^18.2.0", 17 + "react-dom": "^18.2.0", 18 + "ws": "^8.16.0" 19 + }, 20 + "devDependencies": { 21 + "@types/node": "^20.10.5", 22 + "@types/react": "^18.2.45", 23 + "@types/react-dom": "^18.2.18", 24 + "typescript": "^5.3.3" 25 + } 26 + }, 27 + "node_modules/@atproto/api": { 28 + "version": "0.12.29", 29 + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.12.29.tgz", 30 + "integrity": "sha512-PyzPLjGWR0qNOMrmj3Nt3N5NuuANSgOk/33Bu3j+rFjjPrHvk9CI6iQPU6zuDaDCoyOTRJRafw8X/aMQw+ilgw==", 31 + "license": "MIT", 32 + "dependencies": { 33 + "@atproto/common-web": "^0.3.0", 34 + "@atproto/lexicon": "^0.4.0", 35 + "@atproto/syntax": "^0.3.0", 36 + "@atproto/xrpc": "^0.5.0", 37 + "await-lock": "^2.2.2", 38 + "multiformats": "^9.9.0", 39 + "tlds": "^1.234.0" 40 + } 41 + }, 42 + "node_modules/@atproto/common-web": { 43 + "version": "0.3.2", 44 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.3.2.tgz", 45 + "integrity": "sha512-Vx0JtL1/CssJbFAb0UOdvTrkbUautsDfHNOXNTcX2vyPIxH9xOameSqLLunM1hZnOQbJwyjmQCt6TV+bhnanDg==", 46 + "license": "MIT", 47 + "dependencies": { 48 + "graphemer": "^1.4.0", 49 + "multiformats": "^9.9.0", 50 + "uint8arrays": "3.0.0", 51 + "zod": "^3.23.8" 52 + } 53 + }, 54 + "node_modules/@atproto/lexicon": { 55 + "version": "0.4.8", 56 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.8.tgz", 57 + "integrity": "sha512-NPhu4MNHqft4muvHvcU0BrkWoEpTI+OmbQzvZzzRpw54MW3PfrQ4TPEpAfPOrWugPB9y4mD+l8OMN1c9eOGWMQ==", 58 + "license": "MIT", 59 + "dependencies": { 60 + "@atproto/common-web": "^0.4.0", 61 + "@atproto/syntax": "^0.3.4", 62 + "iso-datestring-validator": "^2.2.2", 63 + "multiformats": "^9.9.0", 64 + "zod": "^3.23.8" 65 + } 66 + }, 67 + "node_modules/@atproto/lexicon/node_modules/@atproto/common-web": { 68 + "version": "0.4.0", 69 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.0.tgz", 70 + "integrity": "sha512-ZYL0P9myHybNgwh/hBY0HaBzqiLR1B5/ie5bJpLQAg0whRzNA28t8/nU2vh99tbsWcAF0LOD29M8++LyENJLNQ==", 71 + "license": "MIT", 72 + "dependencies": { 73 + "graphemer": "^1.4.0", 74 + "multiformats": "^9.9.0", 75 + "uint8arrays": "3.0.0", 76 + "zod": "^3.23.8" 77 + } 78 + }, 79 + "node_modules/@atproto/syntax": { 80 + "version": "0.3.4", 81 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.4.tgz", 82 + "integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==", 83 + "license": "MIT" 84 + }, 85 + "node_modules/@atproto/xrpc": { 86 + "version": "0.5.0", 87 + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.5.0.tgz", 88 + "integrity": "sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==", 89 + "license": "MIT", 90 + "dependencies": { 91 + "@atproto/lexicon": "^0.4.0", 92 + "zod": "^3.21.4" 93 + } 94 + }, 95 + "node_modules/@next/env": { 96 + "version": "14.2.24", 97 + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.24.tgz", 98 + "integrity": "sha512-LAm0Is2KHTNT6IT16lxT+suD0u+VVfYNQqM+EJTKuFRRuY2z+zj01kueWXPCxbMBDt0B5vONYzabHGUNbZYAhA==", 99 + "license": "MIT" 100 + }, 101 + "node_modules/@next/swc-darwin-arm64": { 102 + "version": "14.2.24", 103 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.24.tgz", 104 + "integrity": "sha512-7Tdi13aojnAZGpapVU6meVSpNzgrFwZ8joDcNS8cJVNuP3zqqrLqeory9Xec5TJZR/stsGJdfwo8KeyloT3+rQ==", 105 + "cpu": [ 106 + "arm64" 107 + ], 108 + "license": "MIT", 109 + "optional": true, 110 + "os": [ 111 + "darwin" 112 + ], 113 + "engines": { 114 + "node": ">= 10" 115 + } 116 + }, 117 + "node_modules/@next/swc-darwin-x64": { 118 + "version": "14.2.24", 119 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.24.tgz", 120 + "integrity": "sha512-lXR2WQqUtu69l5JMdTwSvQUkdqAhEWOqJEYUQ21QczQsAlNOW2kWZCucA6b3EXmPbcvmHB1kSZDua/713d52xg==", 121 + "cpu": [ 122 + "x64" 123 + ], 124 + "license": "MIT", 125 + "optional": true, 126 + "os": [ 127 + "darwin" 128 + ], 129 + "engines": { 130 + "node": ">= 10" 131 + } 132 + }, 133 + "node_modules/@next/swc-linux-arm64-gnu": { 134 + "version": "14.2.24", 135 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.24.tgz", 136 + "integrity": "sha512-nxvJgWOpSNmzidYvvGDfXwxkijb6hL9+cjZx1PVG6urr2h2jUqBALkKjT7kpfurRWicK6hFOvarmaWsINT1hnA==", 137 + "cpu": [ 138 + "arm64" 139 + ], 140 + "license": "MIT", 141 + "optional": true, 142 + "os": [ 143 + "linux" 144 + ], 145 + "engines": { 146 + "node": ">= 10" 147 + } 148 + }, 149 + "node_modules/@next/swc-linux-arm64-musl": { 150 + "version": "14.2.24", 151 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.24.tgz", 152 + "integrity": "sha512-PaBgOPhqa4Abxa3y/P92F3kklNPsiFjcjldQGT7kFmiY5nuFn8ClBEoX8GIpqU1ODP2y8P6hio6vTomx2Vy0UQ==", 153 + "cpu": [ 154 + "arm64" 155 + ], 156 + "license": "MIT", 157 + "optional": true, 158 + "os": [ 159 + "linux" 160 + ], 161 + "engines": { 162 + "node": ">= 10" 163 + } 164 + }, 165 + "node_modules/@next/swc-linux-x64-gnu": { 166 + "version": "14.2.24", 167 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.24.tgz", 168 + "integrity": "sha512-vEbyadiRI7GOr94hd2AB15LFVgcJZQWu7Cdi9cWjCMeCiUsHWA0U5BkGPuoYRnTxTn0HacuMb9NeAmStfBCLoQ==", 169 + "cpu": [ 170 + "x64" 171 + ], 172 + "license": "MIT", 173 + "optional": true, 174 + "os": [ 175 + "linux" 176 + ], 177 + "engines": { 178 + "node": ">= 10" 179 + } 180 + }, 181 + "node_modules/@next/swc-linux-x64-musl": { 182 + "version": "14.2.24", 183 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.24.tgz", 184 + "integrity": "sha512-df0FC9ptaYsd8nQCINCzFtDWtko8PNRTAU0/+d7hy47E0oC17tI54U/0NdGk7l/76jz1J377dvRjmt6IUdkpzQ==", 185 + "cpu": [ 186 + "x64" 187 + ], 188 + "license": "MIT", 189 + "optional": true, 190 + "os": [ 191 + "linux" 192 + ], 193 + "engines": { 194 + "node": ">= 10" 195 + } 196 + }, 197 + "node_modules/@next/swc-win32-arm64-msvc": { 198 + "version": "14.2.24", 199 + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.24.tgz", 200 + "integrity": "sha512-ZEntbLjeYAJ286eAqbxpZHhDFYpYjArotQ+/TW9j7UROh0DUmX7wYDGtsTPpfCV8V+UoqHBPU7q9D4nDNH014Q==", 201 + "cpu": [ 202 + "arm64" 203 + ], 204 + "license": "MIT", 205 + "optional": true, 206 + "os": [ 207 + "win32" 208 + ], 209 + "engines": { 210 + "node": ">= 10" 211 + } 212 + }, 213 + "node_modules/@next/swc-win32-ia32-msvc": { 214 + "version": "14.2.24", 215 + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.24.tgz", 216 + "integrity": "sha512-9KuS+XUXM3T6v7leeWU0erpJ6NsFIwiTFD5nzNg8J5uo/DMIPvCp3L1Ao5HjbHX0gkWPB1VrKoo/Il4F0cGK2Q==", 217 + "cpu": [ 218 + "ia32" 219 + ], 220 + "license": "MIT", 221 + "optional": true, 222 + "os": [ 223 + "win32" 224 + ], 225 + "engines": { 226 + "node": ">= 10" 227 + } 228 + }, 229 + "node_modules/@next/swc-win32-x64-msvc": { 230 + "version": "14.2.24", 231 + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.24.tgz", 232 + "integrity": "sha512-cXcJ2+x0fXQ2CntaE00d7uUH+u1Bfp/E0HsNQH79YiLaZE5Rbm7dZzyAYccn3uICM7mw+DxoMqEfGXZtF4Fgaw==", 233 + "cpu": [ 234 + "x64" 235 + ], 236 + "license": "MIT", 237 + "optional": true, 238 + "os": [ 239 + "win32" 240 + ], 241 + "engines": { 242 + "node": ">= 10" 243 + } 244 + }, 245 + "node_modules/@supabase/auth-js": { 246 + "version": "2.68.0", 247 + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.68.0.tgz", 248 + "integrity": "sha512-odG7nb7aOmZPUXk6SwL2JchSsn36Ppx11i2yWMIc/meUO2B2HK9YwZHPK06utD9Ql9ke7JKDbwGin/8prHKxxQ==", 249 + "license": "MIT", 250 + "dependencies": { 251 + "@supabase/node-fetch": "^2.6.14" 252 + } 253 + }, 254 + "node_modules/@supabase/functions-js": { 255 + "version": "2.4.4", 256 + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", 257 + "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", 258 + "license": "MIT", 259 + "dependencies": { 260 + "@supabase/node-fetch": "^2.6.14" 261 + } 262 + }, 263 + "node_modules/@supabase/node-fetch": { 264 + "version": "2.6.15", 265 + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", 266 + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", 267 + "license": "MIT", 268 + "dependencies": { 269 + "whatwg-url": "^5.0.0" 270 + }, 271 + "engines": { 272 + "node": "4.x || >=6.0.0" 273 + } 274 + }, 275 + "node_modules/@supabase/postgrest-js": { 276 + "version": "1.19.2", 277 + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.2.tgz", 278 + "integrity": "sha512-MXRbk4wpwhWl9IN6rIY1mR8uZCCG4MZAEji942ve6nMwIqnBgBnZhZlON6zTTs6fgveMnoCILpZv1+K91jN+ow==", 279 + "license": "MIT", 280 + "dependencies": { 281 + "@supabase/node-fetch": "^2.6.14" 282 + } 283 + }, 284 + "node_modules/@supabase/realtime-js": { 285 + "version": "2.11.2", 286 + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", 287 + "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", 288 + "license": "MIT", 289 + "dependencies": { 290 + "@supabase/node-fetch": "^2.6.14", 291 + "@types/phoenix": "^1.5.4", 292 + "@types/ws": "^8.5.10", 293 + "ws": "^8.18.0" 294 + } 295 + }, 296 + "node_modules/@supabase/storage-js": { 297 + "version": "2.7.1", 298 + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", 299 + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", 300 + "license": "MIT", 301 + "dependencies": { 302 + "@supabase/node-fetch": "^2.6.14" 303 + } 304 + }, 305 + "node_modules/@supabase/supabase-js": { 306 + "version": "2.49.1", 307 + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.1.tgz", 308 + "integrity": "sha512-lKaptKQB5/juEF5+jzmBeZlz69MdHZuxf+0f50NwhL+IE//m4ZnOeWlsKRjjsM0fVayZiQKqLvYdBn0RLkhGiQ==", 309 + "license": "MIT", 310 + "dependencies": { 311 + "@supabase/auth-js": "2.68.0", 312 + "@supabase/functions-js": "2.4.4", 313 + "@supabase/node-fetch": "2.6.15", 314 + "@supabase/postgrest-js": "1.19.2", 315 + "@supabase/realtime-js": "2.11.2", 316 + "@supabase/storage-js": "2.7.1" 317 + } 318 + }, 319 + "node_modules/@swc/counter": { 320 + "version": "0.1.3", 321 + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", 322 + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", 323 + "license": "Apache-2.0" 324 + }, 325 + "node_modules/@swc/helpers": { 326 + "version": "0.5.5", 327 + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", 328 + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", 329 + "license": "Apache-2.0", 330 + "dependencies": { 331 + "@swc/counter": "^0.1.3", 332 + "tslib": "^2.4.0" 333 + } 334 + }, 335 + "node_modules/@types/node": { 336 + "version": "20.17.24", 337 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz", 338 + "integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==", 339 + "license": "MIT", 340 + "dependencies": { 341 + "undici-types": "~6.19.2" 342 + } 343 + }, 344 + "node_modules/@types/phoenix": { 345 + "version": "1.6.6", 346 + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", 347 + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", 348 + "license": "MIT" 349 + }, 350 + "node_modules/@types/prop-types": { 351 + "version": "15.7.14", 352 + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", 353 + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", 354 + "dev": true, 355 + "license": "MIT" 356 + }, 357 + "node_modules/@types/react": { 358 + "version": "18.3.18", 359 + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", 360 + "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", 361 + "dev": true, 362 + "license": "MIT", 363 + "dependencies": { 364 + "@types/prop-types": "*", 365 + "csstype": "^3.0.2" 366 + } 367 + }, 368 + "node_modules/@types/react-dom": { 369 + "version": "18.3.5", 370 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", 371 + "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", 372 + "dev": true, 373 + "license": "MIT", 374 + "peerDependencies": { 375 + "@types/react": "^18.0.0" 376 + } 377 + }, 378 + "node_modules/@types/ws": { 379 + "version": "8.18.0", 380 + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", 381 + "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==", 382 + "license": "MIT", 383 + "dependencies": { 384 + "@types/node": "*" 385 + } 386 + }, 387 + "node_modules/await-lock": { 388 + "version": "2.2.2", 389 + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", 390 + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", 391 + "license": "MIT" 392 + }, 393 + "node_modules/busboy": { 394 + "version": "1.6.0", 395 + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 396 + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 397 + "dependencies": { 398 + "streamsearch": "^1.1.0" 399 + }, 400 + "engines": { 401 + "node": ">=10.16.0" 402 + } 403 + }, 404 + "node_modules/caniuse-lite": { 405 + "version": "1.0.30001702", 406 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz", 407 + "integrity": "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==", 408 + "funding": [ 409 + { 410 + "type": "opencollective", 411 + "url": "https://opencollective.com/browserslist" 412 + }, 413 + { 414 + "type": "tidelift", 415 + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 416 + }, 417 + { 418 + "type": "github", 419 + "url": "https://github.com/sponsors/ai" 420 + } 421 + ], 422 + "license": "CC-BY-4.0" 423 + }, 424 + "node_modules/cbor-web": { 425 + "version": "8.1.0", 426 + "resolved": "https://registry.npmjs.org/cbor-web/-/cbor-web-8.1.0.tgz", 427 + "integrity": "sha512-2hWHHMVrfffgoEmsAUh8vCxHoLa1vgodtC73+C5cSarkJlwTapnqAzcHINlP6Ej0DXuP4OmmJ9LF+JaNM5Lj/g==", 428 + "license": "MIT", 429 + "engines": { 430 + "node": ">=12.19" 431 + } 432 + }, 433 + "node_modules/client-only": { 434 + "version": "0.0.1", 435 + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 436 + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", 437 + "license": "MIT" 438 + }, 439 + "node_modules/csstype": { 440 + "version": "3.1.3", 441 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 442 + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", 443 + "dev": true, 444 + "license": "MIT" 445 + }, 446 + "node_modules/dotenv": { 447 + "version": "16.4.7", 448 + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", 449 + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", 450 + "license": "BSD-2-Clause", 451 + "engines": { 452 + "node": ">=12" 453 + }, 454 + "funding": { 455 + "url": "https://dotenvx.com" 456 + } 457 + }, 458 + "node_modules/graceful-fs": { 459 + "version": "4.2.11", 460 + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 461 + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 462 + "license": "ISC" 463 + }, 464 + "node_modules/graphemer": { 465 + "version": "1.4.0", 466 + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", 467 + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", 468 + "license": "MIT" 469 + }, 470 + "node_modules/iso-datestring-validator": { 471 + "version": "2.2.2", 472 + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", 473 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", 474 + "license": "MIT" 475 + }, 476 + "node_modules/js-tokens": { 477 + "version": "4.0.0", 478 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 479 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 480 + "license": "MIT" 481 + }, 482 + "node_modules/loose-envify": { 483 + "version": "1.4.0", 484 + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 485 + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 486 + "license": "MIT", 487 + "dependencies": { 488 + "js-tokens": "^3.0.0 || ^4.0.0" 489 + }, 490 + "bin": { 491 + "loose-envify": "cli.js" 492 + } 493 + }, 494 + "node_modules/multiformats": { 495 + "version": "9.9.0", 496 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 497 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 498 + "license": "(Apache-2.0 AND MIT)" 499 + }, 500 + "node_modules/nanoid": { 501 + "version": "3.3.9", 502 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz", 503 + "integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==", 504 + "funding": [ 505 + { 506 + "type": "github", 507 + "url": "https://github.com/sponsors/ai" 508 + } 509 + ], 510 + "license": "MIT", 511 + "bin": { 512 + "nanoid": "bin/nanoid.cjs" 513 + }, 514 + "engines": { 515 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 516 + } 517 + }, 518 + "node_modules/next": { 519 + "version": "14.2.24", 520 + "resolved": "https://registry.npmjs.org/next/-/next-14.2.24.tgz", 521 + "integrity": "sha512-En8VEexSJ0Py2FfVnRRh8gtERwDRaJGNvsvad47ShkC2Yi8AXQPXEA2vKoDJlGFSj5WE5SyF21zNi4M5gyi+SQ==", 522 + "license": "MIT", 523 + "dependencies": { 524 + "@next/env": "14.2.24", 525 + "@swc/helpers": "0.5.5", 526 + "busboy": "1.6.0", 527 + "caniuse-lite": "^1.0.30001579", 528 + "graceful-fs": "^4.2.11", 529 + "postcss": "8.4.31", 530 + "styled-jsx": "5.1.1" 531 + }, 532 + "bin": { 533 + "next": "dist/bin/next" 534 + }, 535 + "engines": { 536 + "node": ">=18.17.0" 537 + }, 538 + "optionalDependencies": { 539 + "@next/swc-darwin-arm64": "14.2.24", 540 + "@next/swc-darwin-x64": "14.2.24", 541 + "@next/swc-linux-arm64-gnu": "14.2.24", 542 + "@next/swc-linux-arm64-musl": "14.2.24", 543 + "@next/swc-linux-x64-gnu": "14.2.24", 544 + "@next/swc-linux-x64-musl": "14.2.24", 545 + "@next/swc-win32-arm64-msvc": "14.2.24", 546 + "@next/swc-win32-ia32-msvc": "14.2.24", 547 + "@next/swc-win32-x64-msvc": "14.2.24" 548 + }, 549 + "peerDependencies": { 550 + "@opentelemetry/api": "^1.1.0", 551 + "@playwright/test": "^1.41.2", 552 + "react": "^18.2.0", 553 + "react-dom": "^18.2.0", 554 + "sass": "^1.3.0" 555 + }, 556 + "peerDependenciesMeta": { 557 + "@opentelemetry/api": { 558 + "optional": true 559 + }, 560 + "@playwright/test": { 561 + "optional": true 562 + }, 563 + "sass": { 564 + "optional": true 565 + } 566 + } 567 + }, 568 + "node_modules/picocolors": { 569 + "version": "1.1.1", 570 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 571 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 572 + "license": "ISC" 573 + }, 574 + "node_modules/postcss": { 575 + "version": "8.4.31", 576 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", 577 + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", 578 + "funding": [ 579 + { 580 + "type": "opencollective", 581 + "url": "https://opencollective.com/postcss/" 582 + }, 583 + { 584 + "type": "tidelift", 585 + "url": "https://tidelift.com/funding/github/npm/postcss" 586 + }, 587 + { 588 + "type": "github", 589 + "url": "https://github.com/sponsors/ai" 590 + } 591 + ], 592 + "license": "MIT", 593 + "dependencies": { 594 + "nanoid": "^3.3.6", 595 + "picocolors": "^1.0.0", 596 + "source-map-js": "^1.0.2" 597 + }, 598 + "engines": { 599 + "node": "^10 || ^12 || >=14" 600 + } 601 + }, 602 + "node_modules/react": { 603 + "version": "18.3.1", 604 + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", 605 + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", 606 + "license": "MIT", 607 + "dependencies": { 608 + "loose-envify": "^1.1.0" 609 + }, 610 + "engines": { 611 + "node": ">=0.10.0" 612 + } 613 + }, 614 + "node_modules/react-dom": { 615 + "version": "18.3.1", 616 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", 617 + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", 618 + "license": "MIT", 619 + "dependencies": { 620 + "loose-envify": "^1.1.0", 621 + "scheduler": "^0.23.2" 622 + }, 623 + "peerDependencies": { 624 + "react": "^18.3.1" 625 + } 626 + }, 627 + "node_modules/scheduler": { 628 + "version": "0.23.2", 629 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", 630 + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", 631 + "license": "MIT", 632 + "dependencies": { 633 + "loose-envify": "^1.1.0" 634 + } 635 + }, 636 + "node_modules/source-map-js": { 637 + "version": "1.2.1", 638 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 639 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 640 + "license": "BSD-3-Clause", 641 + "engines": { 642 + "node": ">=0.10.0" 643 + } 644 + }, 645 + "node_modules/streamsearch": { 646 + "version": "1.1.0", 647 + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 648 + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", 649 + "engines": { 650 + "node": ">=10.0.0" 651 + } 652 + }, 653 + "node_modules/styled-jsx": { 654 + "version": "5.1.1", 655 + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", 656 + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", 657 + "license": "MIT", 658 + "dependencies": { 659 + "client-only": "0.0.1" 660 + }, 661 + "engines": { 662 + "node": ">= 12.0.0" 663 + }, 664 + "peerDependencies": { 665 + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" 666 + }, 667 + "peerDependenciesMeta": { 668 + "@babel/core": { 669 + "optional": true 670 + }, 671 + "babel-plugin-macros": { 672 + "optional": true 673 + } 674 + } 675 + }, 676 + "node_modules/tlds": { 677 + "version": "1.256.0", 678 + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.256.0.tgz", 679 + "integrity": "sha512-ZmyVB9DAw+FFTmLElGYJgdZFsKLYd/I59Bg9NHkCGPwAbVZNRilFWDMAdX8UG+bHuv7kfursd5XGqo/9wi26lA==", 680 + "license": "MIT", 681 + "bin": { 682 + "tlds": "bin.js" 683 + } 684 + }, 685 + "node_modules/tr46": { 686 + "version": "0.0.3", 687 + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 688 + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", 689 + "license": "MIT" 690 + }, 691 + "node_modules/tslib": { 692 + "version": "2.8.1", 693 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 694 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 695 + "license": "0BSD" 696 + }, 697 + "node_modules/typescript": { 698 + "version": "5.8.2", 699 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", 700 + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", 701 + "dev": true, 702 + "license": "Apache-2.0", 703 + "bin": { 704 + "tsc": "bin/tsc", 705 + "tsserver": "bin/tsserver" 706 + }, 707 + "engines": { 708 + "node": ">=14.17" 709 + } 710 + }, 711 + "node_modules/uint8arrays": { 712 + "version": "3.0.0", 713 + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", 714 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 715 + "license": "MIT", 716 + "dependencies": { 717 + "multiformats": "^9.4.2" 718 + } 719 + }, 720 + "node_modules/undici-types": { 721 + "version": "6.19.8", 722 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 723 + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 724 + "license": "MIT" 725 + }, 726 + "node_modules/webidl-conversions": { 727 + "version": "3.0.1", 728 + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 729 + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", 730 + "license": "BSD-2-Clause" 731 + }, 732 + "node_modules/whatwg-url": { 733 + "version": "5.0.0", 734 + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 735 + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 736 + "license": "MIT", 737 + "dependencies": { 738 + "tr46": "~0.0.3", 739 + "webidl-conversions": "^3.0.0" 740 + } 741 + }, 742 + "node_modules/ws": { 743 + "version": "8.18.1", 744 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", 745 + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", 746 + "license": "MIT", 747 + "engines": { 748 + "node": ">=10.0.0" 749 + }, 750 + "peerDependencies": { 751 + "bufferutil": "^4.0.1", 752 + "utf-8-validate": ">=5.0.2" 753 + }, 754 + "peerDependenciesMeta": { 755 + "bufferutil": { 756 + "optional": true 757 + }, 758 + "utf-8-validate": { 759 + "optional": true 760 + } 761 + } 762 + }, 763 + "node_modules/zod": { 764 + "version": "3.24.2", 765 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", 766 + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", 767 + "license": "MIT", 768 + "funding": { 769 + "url": "https://github.com/sponsors/colinhacks" 770 + } 771 + } 772 + } 773 + }
+5 -5
app/package.json
··· 9 9 "lint": "next lint" 10 10 }, 11 11 "dependencies": { 12 + "@atproto/api": "^0.12.0", 13 + "@supabase/supabase-js": "^2.49.1", 14 + "cbor-web": "^8.1.0", 15 + "dotenv": "^16.4.7", 12 16 "next": "^14.1.0", 13 17 "react": "^18.2.0", 14 18 "react-dom": "^18.2.0", 15 - "@atproto/api": "^0.12.0", 16 - "@supabase/supabase-js": "^2.39.0", 17 - "cbor-web": "^8.1.0", 18 - "dotenv": "^16.3.1", 19 19 "ws": "^8.16.0" 20 20 }, 21 21 "devDependencies": { ··· 24 24 "@types/react-dom": "^18.2.18", 25 25 "typescript": "^5.3.3" 26 26 } 27 - } 27 + }
+178 -171
app/scripts/firehose-worker.js
··· 1 - const WebSocket = require('ws'); 2 - const cbor = require('cbor-web'); 3 - const { createClient } = require('@supabase/supabase-js'); 4 - require('dotenv').config(); 1 + const WebSocket = require("ws"); 2 + const { createClient } = require("@supabase/supabase-js"); 3 + const path = require("path"); 4 + require("dotenv").config({ path: path.resolve(__dirname, "../../.env") }); // Load environment variables from .env file in app root 5 5 6 - // Constants 7 - const FIREHOSE_URL = 'wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos'; 8 - const FLUSHING_STATUS_NSID = 'im.flushing.right.now'; 6 + const JETSTREAM_URL = "wss://jetstream2.us-west.bsky.network/subscribe"; 7 + const FLUSHING_STATUS_NSID = "im.flushing.right.now"; 9 8 10 - // Supabase setup - ensure you have these set in your .env file 9 + // Supabase setup from .env file 11 10 const supabaseUrl = process.env.SUPABASE_URL; 12 11 const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; 13 - const supabase = createClient(supabaseUrl, supabaseKey); 14 12 15 - // Reconnection parameters 16 - const MAX_RECONNECT_DELAY = 30000; // 30 seconds 17 - let reconnectAttempts = 0; 18 - let ws = null; 13 + if (!supabaseUrl || !supabaseKey) { 14 + console.error("Missing Supabase credentials. Add SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY to your .env file"); 15 + process.exit(1); 16 + } 19 17 20 - // Connect to the firehose 21 - function connectToFirehose() { 22 - console.log('Connecting to Bluesky firehose...'); 23 - 24 - ws = new WebSocket(FIREHOSE_URL); 25 - 26 - ws.on('open', () => { 27 - console.log('Connected to firehose.'); 28 - // Reset reconnect counter on successful connection 29 - reconnectAttempts = 0; 30 - }); 31 - 32 - ws.on('message', async (data) => { 18 + const supabase = createClient(supabaseUrl, supabaseKey); 19 + 20 + // Ensure the table exists 21 + async function setupDatabase() { 33 22 try { 34 - // In a real implementation, parse CBOR data to extract repo operations 35 - // For now, log the message to track activity 36 - console.log('Received message from firehose'); 37 - 38 - // Decode the CBOR message (this is a simplified version) 39 - // The actual implementation would need to handle the header and payload separately 40 - const decoded = cbor.decode(data); 41 - 42 - // Process the message if it's a commit 43 - if (decoded.op === 1 && decoded.t === '#commit') { 44 - // Process repo commit 45 - const commit = decoded.payload; 23 + console.log("Setting up database..."); 46 24 47 - // Check if this commit contains a flushing record 48 - const flushingOps = commit.ops.filter(op => { 49 - return op.path.startsWith(FLUSHING_STATUS_NSID) && op.action === 'create'; 50 - }); 25 + // Check if the table already exists 26 + const { error: queryError } = await supabase 27 + .from('flushing_records') 28 + .select('id', { count: 'exact', head: true }); 51 29 52 - if (flushingOps.length > 0) { 53 - console.log(`Found ${flushingOps.length} flushing records in commit from ${commit.repo}`); 54 - 55 - // Process each flushing record 56 - for (const op of flushingOps) { 57 - await processFlushingRecord(commit.repo, op.path, op.cid, commit.blocks); 58 - } 30 + // If no error, table exists 31 + if (!queryError) { 32 + console.log("Table 'flushing_records' already exists"); 33 + return; 34 + } 35 + 36 + // Create the table using SQL 37 + const { error: sqlError } = await supabase.sql` 38 + CREATE TABLE IF NOT EXISTS flushing_records ( 39 + id SERIAL PRIMARY KEY, 40 + did TEXT NOT NULL, 41 + collection TEXT NOT NULL, 42 + type TEXT NOT NULL, 43 + created_at TIMESTAMP WITH TIME ZONE NOT NULL, 44 + emoji TEXT, 45 + text TEXT, 46 + cid TEXT NOT NULL, 47 + uri TEXT UNIQUE NOT NULL, 48 + indexed_at TIMESTAMP WITH TIME ZONE DEFAULT now() 49 + ); 50 + 51 + CREATE INDEX IF NOT EXISTS flushing_records_did_idx ON flushing_records(did); 52 + `; 53 + 54 + if (sqlError) { 55 + console.error("Error creating table:", sqlError); 56 + process.exit(1); 59 57 } 60 - } 61 - } catch (error) { 62 - console.error('Error processing message:', error); 58 + console.log("Table created successfully"); 59 + } catch (err) { 60 + console.error("Error setting up database:", err); 61 + process.exit(1); 63 62 } 64 - }); 65 - 66 - ws.on('error', (error) => { 67 - console.error('WebSocket error:', error); 68 - }); 69 - 70 - ws.on('close', (code, reason) => { 71 - console.log(`Connection closed: ${code} - ${reason}`); 72 - 73 - // Implement exponential backoff for reconnection 74 - reconnectAttempts++; 75 - const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), MAX_RECONNECT_DELAY); 76 - 77 - console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts})...`); 78 - setTimeout(connectToFirehose, delay); 79 - }); 80 63 } 81 64 82 - // Process a flushing record and store it in Supabase 83 - async function processFlushingRecord(authorDid, recordPath, cid, blocks) { 84 - try { 85 - // Extract the record data from the blocks (simplified) 86 - // In a real implementation, you would need to properly decode the IPLD blocks 87 - const recordData = blocks[cid]; 88 - 89 - if (!recordData) { 90 - console.error('Record data not found in blocks'); 91 - return; 92 - } 93 - 94 - // Extract the record URI 95 - const uri = `at://${authorDid}/${recordPath}`; 96 - 97 - // Check if we already have this record 98 - const { data: existingRecord } = await supabase 99 - .from('flushing_entries') 100 - .select('id') 101 - .eq('uri', uri) 102 - .single(); 103 - 104 - if (existingRecord) { 105 - console.log('Record already exists, skipping'); 106 - return; 107 - } 108 - 109 - // Create a new entry in Supabase 110 - const newEntry = { 111 - uri, 112 - cid, 113 - author_did: authorDid, 114 - text: recordData.text, 115 - emoji: recordData.emoji, 116 - created_at: recordData.createdAt 117 - }; 118 - 119 - const { error } = await supabase 120 - .from('flushing_entries') 121 - .insert(newEntry); 122 - 123 - if (error) { 124 - console.error('Error inserting record:', error); 125 - } else { 126 - console.log('Successfully stored new flushing record'); 127 - } 128 - 129 - // Also try to resolve the author's handle if we don't have it 130 - const { data: authorData } = await supabase 131 - .from('users') 132 - .select('handle') 133 - .eq('did', authorDid) 134 - .single(); 135 - 136 - if (!authorData || !authorData.handle) { 137 - // TODO: Use the Bluesky API to resolve the handle from the DID 138 - // This would be done with the BskyAgent or direct API call 139 - console.log('Need to resolve handle for DID:', authorDid); 140 - } 141 - } catch (error) { 142 - console.error('Error processing flushing record:', error); 143 - } 144 - } 65 + let messageCount = 0; 66 + let flushingFoundCount = 0; 67 + 68 + function connect() { 69 + console.log("Connecting to Jetstream"); 70 + 71 + const wsUrl = JETSTREAM_URL + "?wantedCollections=" + FLUSHING_STATUS_NSID; 72 + const ws = new WebSocket(wsUrl); 73 + 74 + ws.on("open", () => { 75 + console.log("Connected to Jetstream"); 76 + }); 77 + 78 + ws.on("message", async (data) => { 79 + messageCount++; 80 + if (messageCount % 1000 === 0) { 81 + console.log("Messages:", messageCount); 82 + } 83 + 84 + try { 85 + const message = JSON.parse(data.toString()); 86 + 87 + if (message.kind === "commit" && 88 + message.commit && 89 + message.commit.collection === FLUSHING_STATUS_NSID) { 90 + 91 + flushingFoundCount++; 92 + console.log("Found flushing record:", flushingFoundCount); 93 + console.log(JSON.stringify(message, null, 2)); 94 + 95 + const recordPath = message.commit.collection + "/" + message.commit.rkey; 96 + const authorDid = message.did; 97 + const cid = message.commit.cid || "cid_" + Date.now(); 98 + 99 + let recordText = "No text found"; 100 + let recordEmoji = "🚽"; 101 + let recordCreatedAt = new Date().toISOString(); 102 + let recordType = FLUSHING_STATUS_NSID; 103 + 104 + if (message.commit.record) { 105 + if (message.commit.record.text) { 106 + recordText = message.commit.record.text; 107 + } 108 + if (message.commit.record.emoji) { 109 + recordEmoji = message.commit.record.emoji; 110 + } 111 + if (message.commit.record.createdAt) { 112 + recordCreatedAt = message.commit.record.createdAt; 113 + } 114 + if (message.commit.record.$type) { 115 + recordType = message.commit.record.$type; 116 + } 117 + } 118 + 119 + console.log("Author:", authorDid); 120 + console.log("Path:", recordPath); 121 + console.log("Text:", recordText); 122 + console.log("Emoji:", recordEmoji); 123 + console.log("Created at:", recordCreatedAt); 124 + 125 + const uri = "at://" + authorDid + "/" + recordPath; 126 + console.log("URI:", uri); 145 127 146 - // Create the necessary tables if they don't exist 147 - async function setupDatabase() { 148 - try { 149 - // Create flushing_entries table 150 - const { error: entriesError } = await supabase.rpc('create_flushing_entries_table_if_not_exists'); 151 - if (entriesError) { 152 - console.error('Error creating flushing_entries table:', entriesError); 153 - } 154 - 155 - // Create users table 156 - const { error: usersError } = await supabase.rpc('create_users_table_if_not_exists'); 157 - if (usersError) { 158 - console.error('Error creating users table:', usersError); 159 - } 160 - } catch (error) { 161 - console.error('Error setting up database:', error); 162 - } 128 + // Save to Supabase 129 + try { 130 + // Check if record already exists 131 + const { data: existingData, error: checkError } = await supabase 132 + .from("flushing_records") 133 + .select("id") 134 + .eq("uri", uri) 135 + .limit(1); 136 + 137 + if (checkError) { 138 + console.error("Error checking for existing record:", checkError.message); 139 + return; 140 + } 141 + 142 + if (existingData && existingData.length > 0) { 143 + console.log("Record already exists, skipping"); 144 + return; 145 + } 146 + 147 + // Insert new record 148 + const newRecord = { 149 + did: authorDid, 150 + collection: message.commit.collection, 151 + type: recordType, 152 + created_at: recordCreatedAt, 153 + emoji: recordEmoji, 154 + text: recordText, 155 + cid: cid, 156 + uri: uri 157 + }; 158 + 159 + const { error: insertError } = await supabase 160 + .from("flushing_records") 161 + .insert(newRecord); 162 + 163 + if (insertError) { 164 + console.error("Error saving record:", insertError.message); 165 + } else { 166 + console.log("Record saved successfully"); 167 + } 168 + } catch (err) { 169 + console.error("Error interacting with database:", err.message); 170 + } 171 + } 172 + } catch (err) { 173 + console.error("Error processing message:", err.message); 174 + } 175 + }); 176 + 177 + ws.on("error", (error) => { 178 + console.error("WebSocket error:", error.message); 179 + }); 180 + 181 + ws.on("close", () => { 182 + console.log("Connection closed, reconnecting in 5s"); 183 + setTimeout(connect, 5000); 184 + }); 163 185 } 164 186 165 187 // Start the worker 166 188 async function start() { 167 - console.log('Starting firehose worker...'); 168 - 169 - // Check if we have the required environment variables 170 - if (!supabaseUrl || !supabaseKey) { 171 - console.error('Missing Supabase credentials. Please set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY environment variables.'); 172 - process.exit(1); 173 - } 174 - 175 - // Setup the database 176 - await setupDatabase(); 177 - 178 - // Connect to the firehose 179 - connectToFirehose(); 189 + await setupDatabase(); 190 + connect(); 180 191 } 181 192 182 - // Handle process termination 183 - process.on('SIGINT', () => { 184 - console.log('Shutting down...'); 185 - if (ws) { 186 - ws.close(); 187 - } 188 - process.exit(0); 189 - }); 193 + // Run the worker 194 + start(); 190 195 191 - // Start the worker 192 - start(); 196 + process.on("SIGINT", () => { 197 + console.log("Shutting down"); 198 + process.exit(0); 199 + });
+92 -31
app/src/app/api/bluesky/feed/route.ts
··· 36 36 if (supabaseUrl && supabaseKey) { 37 37 const supabase = createClient(supabaseUrl, supabaseKey); 38 38 39 - // Fetch the latest entries from Supabase 40 - const { data: entries, error } = await supabase 41 - .from('flushing_entries') 42 - .select(` 43 - id, 44 - uri, 45 - cid, 46 - author_did, 47 - author_handle, 48 - text, 49 - emoji, 50 - created_at 51 - `) 52 - .order('created_at', { ascending: false }) 53 - .limit(MAX_ENTRIES); 39 + // First check if we're using the new flushing_records table 40 + const { data: recordsExists, error: checkError } = await supabase 41 + .from('flushing_records') 42 + .select('id', { count: 'exact', head: true }); 43 + 44 + let entries; 45 + let error; 46 + 47 + if (!checkError) { 48 + // Use the new flushing_records table 49 + console.log('Using flushing_records table'); 50 + ({ data: entries, error } = await supabase 51 + .from('flushing_records') 52 + .select(` 53 + id, 54 + uri, 55 + cid, 56 + did, 57 + text, 58 + emoji, 59 + created_at 60 + `) 61 + .order('created_at', { ascending: false }) 62 + .limit(MAX_ENTRIES)); 63 + } else { 64 + // Fall back to the old flushing_entries table 65 + console.log('Falling back to flushing_entries table'); 66 + ({ data: entries, error } = await supabase 67 + .from('flushing_entries') 68 + .select(` 69 + id, 70 + uri, 71 + cid, 72 + author_did as did, 73 + author_handle, 74 + text, 75 + emoji, 76 + created_at 77 + `) 78 + .order('created_at', { ascending: false }) 79 + .limit(MAX_ENTRIES)); 80 + } 54 81 55 82 if (error) { 56 83 throw new Error(`Supabase error: ${error.message}`); 57 84 } 58 85 59 86 // Transform the data to match our client-side model 60 - const processedEntries = (entries || []).map(entry => ({ 61 - id: entry.id, 62 - uri: entry.uri, 63 - cid: entry.cid, 64 - authorDid: entry.author_did, 65 - authorHandle: entry.author_handle || 'unknown', 66 - text: entry.text, 67 - emoji: entry.emoji, 68 - createdAt: entry.created_at 87 + const processedEntries = await Promise.all((entries || []).map(async entry => { 88 + // For the new table, we need to resolve handles from DIDs 89 + // For the old table, we might already have handles 90 + const authorDid = entry.did; 91 + let authorHandle = entry.author_handle || null; 92 + 93 + // If we don't have a handle (which will always be the case for the new table), resolve it 94 + if (!authorHandle) { 95 + const resolvedHandle = await resolveDidToHandle(authorDid); 96 + authorHandle = resolvedHandle || 'unknown'; 97 + } 98 + 99 + return { 100 + id: entry.id, 101 + uri: entry.uri, 102 + cid: entry.cid, 103 + authorDid: authorDid, 104 + authorHandle: authorHandle, 105 + text: entry.text, 106 + emoji: entry.emoji, 107 + createdAt: entry.created_at 108 + }; 69 109 })); 70 110 71 111 // Update the cache ··· 76 116 } else { 77 117 // If no Supabase credentials, fall back to mock data 78 118 console.log('No Supabase credentials, using mock data'); 79 - const mockEntries = await getMockEntries(); 119 + const mockEntries = getMockEntries(); 80 120 81 121 // Update cache 82 122 cachedEntries = mockEntries; ··· 95 135 96 136 // Function to generate mock entries for testing 97 137 // This is used when Supabase is not configured 98 - async function getMockEntries() { 138 + function getMockEntries() { 99 139 // Create some mock entries for testing 100 140 const mockEntries = [ 101 141 { ··· 133 173 return mockEntries; 134 174 } 135 175 136 - // Function to attempt to resolve a DID to a handle using the Bluesky API 137 - // This is used when we have a record with an author_did but no author_handle 176 + // Function to attempt to resolve a DID to a handle 177 + // First tries PLC directory, then falls back to Bluesky API if needed 138 178 async function resolveDidToHandle(did: string): Promise<string | null> { 139 179 try { 140 - await agent.login({ identifier: 'user.bsky.social', password: 'none' }); 141 - const response = await agent.getProfile({ actor: did }); 142 - return response.data.handle; 180 + // Try PLC directory first (faster and doesn't require auth) 181 + if (did && did.startsWith('did:plc:')) { 182 + const plcResponse = await fetch(`https://plc.directory/${did}/data`); 183 + if (plcResponse.ok) { 184 + const plcData = await plcResponse.json(); 185 + if (plcData && plcData.alsoKnownAs && plcData.alsoKnownAs.length > 0) { 186 + // alsoKnownAs contains values like 'at://user.bsky.social' 187 + const handle = plcData.alsoKnownAs[0].split('//')[1]; 188 + if (handle) return handle; 189 + } 190 + } 191 + } 192 + 193 + // Fall back to Bluesky API 194 + console.log(`Falling back to Bluesky API for DID: ${did}`); 195 + try { 196 + // Try to resolve DID directly with Bluesky API 197 + await agent.login({ identifier: 'user.bsky.social', password: 'none' }); 198 + const response = await agent.getProfile({ actor: did }); 199 + return response.data.handle; 200 + } catch (apiError) { 201 + console.error(`Failed to resolve handle with Bluesky API for DID ${did}:`, apiError); 202 + return null; 203 + } 143 204 } catch (error) { 144 205 console.error(`Failed to resolve handle for DID ${did}:`, error); 145 206 return null;
+232
contextual info for claude/bluesky_jetstream.md
··· 1 + # Jetstream 2 + 3 + Jetstream is a streaming service that consumes an ATProto `com.atproto.sync.subscribeRepos` stream and converts it into lightweight, friendly JSON. 4 + 5 + Jetstream converts the CBOR-encoded MST blocks produced by the ATProto firehose and translates them into JSON objects that are easier to interface with using standard tooling available in programming languages. 6 + 7 + ### Public Instances 8 + 9 + As of writing, there are 4 official public Jetstream instances operated by Bluesky. 10 + 11 + | Hostname | Region | 12 + | --------------------------------- | ------- | 13 + | `jetstream1.us-east.bsky.network` | US-East | 14 + | `jetstream2.us-east.bsky.network` | US-East | 15 + | `jetstream1.us-west.bsky.network` | US-West | 16 + | `jetstream2.us-west.bsky.network` | US-West | 17 + 18 + Connect to these instances over WSS: `wss://jetstream2.us-west.bsky.network/subscribe` 19 + 20 + We will monitor and operate these instances and do our best to keep them available for public use by developers. 21 + 22 + Feel free to have multiple connections to Jetstream instances if needed. We encourage you to make use of Jetstream wherever you may consider using the `com.atproto.sync.subscribeRepos` firehose if you don't need the features of the full sync protocol. 23 + 24 + Because cursors for Jetstream are time-based (unix microseconds), you can use the same cursor for multiple instances to get roughly the same data. 25 + 26 + When switching between instances, it may be prudent to rewind your cursor a few seconds for gapless playback if you process events idempotently. 27 + 28 + ## Running Jetstream 29 + 30 + To run Jetstream, make sure you have docker and docker compose installed and run `make up` in the repo root. 31 + 32 + This will pull the latest built image from GHCR and start a Jetstream instance at `http://localhost:6008` 33 + 34 + - To build Jetstream from source via Docker and start it up, run `make rebuild` 35 + 36 + Once started, you can connect to the event stream at: `ws://localhost:6008/subscribe` 37 + 38 + Prometheus metrics are exposed at `http://localhost:6009/metrics` 39 + 40 + A [Grafana Dashboard](#dashboard-preview) for Jetstream is available at `./grafana-dashboard.json` and should be easy to copy/paste into Grafana's dashboard import prompt. 41 + 42 + - This dashboard has a few device-specific graphs for disk and network usage that require NodeExporter and may need to be tuned to your setup. 43 + 44 + ## Consuming Jetstream 45 + 46 + To consume Jetstream you can use any websocket client 47 + 48 + Connect to `ws://localhost:6008/subscribe` to start the stream 49 + 50 + The following Query Parameters are supported: 51 + 52 + - `wantedCollections` - An array of [Collection NSIDs](https://atproto.com/specs/nsid) to filter which records you receive on your stream (default empty = all collections) 53 + - `wantedCollections` supports NSID path prefixes i.e. `app.bsky.graph.*`, or `app.bsky.*`. The prefix before the `.*` must pass NSID validation and Jetstream **does not** support incomplete prefixes i.e. `app.bsky.graph.fo*`. 54 + - Regardless of desired collections, all subscribers recieve Account and Identity events. 55 + - You can specify at most 100 wanted collections/prefixes. 56 + - `wantedDids` - An array of Repo DIDs to filter which records you receive on your stream (Default empty = all repos) 57 + - You can specify at most 10,000 wanted DIDs. 58 + - `maxMessageSizeBytes` - The maximum size of a payload that this client would like to receive. Zero means no limit, negative values are treated as zero. (Default "0" or empty = no maximum size) 59 + - `cursor` - A unix microseconds timestamp cursor to begin playback from 60 + - An absent cursor or a cursor from the future will result in live-tail operation 61 + - When reconnecting, use the `time_us` from your most recently processed event and maybe provide a negative buffer (i.e. subtract a few seconds) to ensure gapless playback 62 + - `compress` - Set to `true` to enable `zstd` [compression](#compression) 63 + - `requireHello` - Set to `true` to pause replay/live-tail until the server recevies a [`SubscriberOptionsUpdatePayload`](#options-updates) over the socket in a [Subscriber Sourced Message](#subscriber-sourced-messages) 64 + 65 + ### Examples 66 + 67 + A simple example that hits the public instance looks like: 68 + 69 + ```bash 70 + $ websocat wss://jetstream2.us-east.bsky.network/subscribe\?wantedCollections=app.bsky.feed.post 71 + ``` 72 + 73 + A maximal example using all parameters looks like: 74 + 75 + ```bash 76 + $ websocat "ws://localhost:6008/subscribe?wantedCollections=app.bsky.feed.post&wantedCollections=app.bsky.feed.like&wantedCollections=app.bsky.graph.follow&wantedDids=did:plc:q6gjnaw2blty4crticxkmujt&cursor=1725519626134432" 77 + ``` 78 + 79 + ### Example events: 80 + 81 + Jetstream events have 3 `kinds`s (so far): 82 + 83 + - `commit`: a Commit to a repo which involves either a create, update, or delete of a record 84 + - `identity`: an Identity update for a DID which indicates that you may want to purge an identity cache and revalidate the DID doc and handle 85 + - `account`: an Account event that indicates a change in account status i.e. from `active` to `deactivated`, or to `takendown` if the PDS has taken down the repo. 86 + 87 + Jetstream Commits have 3 `operations`: 88 + 89 + - `create`: Create a new record with the contents provided 90 + - `update`: Update an existing record and replace it with the contents provided 91 + - `delete`: Delete an existing record with the DID, Collection, and RKey provided 92 + 93 + #### A like committed to a repo 94 + 95 + ```json 96 + { 97 + "did": "did:plc:eygmaihciaxprqvxpfvl6flk", 98 + "time_us": 1725911162329308, 99 + "kind": "commit", 100 + "commit": { 101 + "rev": "3l3qo2vutsw2b", 102 + "operation": "create", 103 + "collection": "app.bsky.feed.like", 104 + "rkey": "3l3qo2vuowo2b", 105 + "record": { 106 + "$type": "app.bsky.feed.like", 107 + "createdAt": "2024-09-09T19:46:02.102Z", 108 + "subject": { 109 + "cid": "bafyreidc6sydkkbchcyg62v77wbhzvb2mvytlmsychqgwf2xojjtirmzj4", 110 + "uri": "at://did:plc:wa7b35aakoll7hugkrjtf3xf/app.bsky.feed.post/3l3pte3p2e325" 111 + } 112 + }, 113 + "cid": "bafyreidwaivazkwu67xztlmuobx35hs2lnfh3kolmgfmucldvhd3sgzcqi" 114 + } 115 + } 116 + ``` 117 + 118 + #### A deleted follow record 119 + 120 + ```json 121 + { 122 + "did": "did:plc:rfov6bpyztcnedeyyzgfq42k", 123 + "time_us": 1725516666833633, 124 + "kind": "commit", 125 + "commit": { 126 + "rev": "3l3f6nzl3cv2s", 127 + "operation": "delete", 128 + "collection": "app.bsky.graph.follow", 129 + "rkey": "3l3dn7tku762u" 130 + } 131 + } 132 + ``` 133 + 134 + #### An identity update 135 + 136 + ```json 137 + { 138 + "did": "did:plc:ufbl4k27gp6kzas5glhz7fim", 139 + "time_us": 1725516665234703, 140 + "kind": "identity", 141 + "identity": { 142 + "did": "did:plc:ufbl4k27gp6kzas5glhz7fim", 143 + "handle": "yohenrique.bsky.social", 144 + "seq": 1409752997, 145 + "time": "2024-09-05T06:11:04.870Z" 146 + } 147 + } 148 + ``` 149 + 150 + #### An account becoming active 151 + 152 + ```json 153 + { 154 + "did": "did:plc:ufbl4k27gp6kzas5glhz7fim", 155 + "time_us": 1725516665333808, 156 + "kind": "account", 157 + "account": { 158 + "active": true, 159 + "did": "did:plc:ufbl4k27gp6kzas5glhz7fim", 160 + "seq": 1409753013, 161 + "time": "2024-09-05T06:11:04.870Z" 162 + } 163 + } 164 + ``` 165 + 166 + ### Compression 167 + 168 + Jetstream supports `zstd`-based compression of messages. Jetstream uses a custom dictionary for compression that can be found in `pkg/models/zstd_dictionary` and is required to decode compressed messages from the server. 169 + 170 + `zstd` compressed Jetstream messages are ~56% smaller on average than the raw JSON version of the Jetstream firehose. 171 + 172 + The provided client library uses compression by default, using an embedded copy of the Dictionary from the `models` package. 173 + 174 + To request a compressed stream, pass the `Socket-Encoding: zstd` header through when initiating the websocket _or_ pass `compress=true` in the query string. 175 + 176 + ### Subscriber Sourced messages 177 + 178 + Subscribers can send Text messages to Jetstream over the websocket using the `SubscriberSourcedMessage` framing below: 179 + 180 + ```go 181 + type SubscriberSourcedMessage struct { 182 + Type string `json:"type"` 183 + Payload json.RawMessage `json:"payload"` 184 + } 185 + ``` 186 + 187 + The supported message types are as follows: 188 + 189 + - `options_update` 190 + 191 + #### Options Updates 192 + 193 + A client can update their `wantedCollections` and `wantedDids` after connecting to the socket by sending a Subscriber Sourced Message. 194 + 195 + To send an Options Update, provide the string `options_update` in the `type` field and a `SubscriberOptionsUpdatePayload` in the `payload` field. 196 + 197 + The shape for a `SubscriberOptionsUpdatePayload` is as follows: 198 + 199 + ```go 200 + type SubscriberOptionsUpdateMsg struct { 201 + WantedCollections []string `json:"wantedCollections"` 202 + WantedDIDs []string `json:"wantedDids"` 203 + MaxMessageSizeBytes int `json:"maxMessageSizeBytes"` 204 + } 205 + ``` 206 + 207 + If either array is empty, the relevant filter will be disabled (i.e. sending empty `wantedDids` will mean a client gets messages for all DIDs again). 208 + 209 + Some limitations apply around the size of the message: right now the message can be at most 10MB in size and can contain up to 100 collection filters _and_ up to 10,000 DID filters. 210 + 211 + Additionally, a client can connect with `?requireHello=true` in the query params to pause replay/live-tail until the first Options Update message is sent by the client over the socket. 212 + 213 + Invalid Options Updates in `requireHello` mode or normal operating mode will result in the client being disconnected. 214 + 215 + An example Subscriber Sourced Message with an Options Update payload is as follows: 216 + 217 + ```json 218 + { 219 + "type": "options_update", 220 + "payload": { 221 + "wantedCollections": ["app.bsky.feed.post"], 222 + "wantedDids": ["did:plc:q6gjnaw2blty4crticxkmujt"], 223 + "maxMessageSizeBytes": 1000000 224 + } 225 + } 226 + ``` 227 + 228 + The above payload will filter such that a client receives only posts, and only from a the specified DID. 229 + 230 + ### Dashboard Preview 231 + 232 + ![A screenshot of the Jetstream Grafana Dashboard](./docs/dash.png)
+137
contextual info for claude/jetstream6-worker.js
··· 1 + const WebSocket = require("ws"); 2 + const { createClient } = require("@supabase/supabase-js"); 3 + 4 + const JETSTREAM_URL = "wss://jetstream2.us-west.bsky.network/subscribe"; 5 + const FLUSHING_STATUS_NSID = "im.flushing.right.now"; 6 + 7 + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; 8 + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; 9 + let supabase = null; 10 + 11 + if (supabaseUrl && supabaseKey) { 12 + console.log("Connected to Supabase"); 13 + supabase = createClient(supabaseUrl, supabaseKey); 14 + } else { 15 + console.log("No Supabase credentials found"); 16 + } 17 + 18 + let messageCount = 0; 19 + let flushingFoundCount = 0; 20 + 21 + function connect() { 22 + console.log("Connecting to Jetstream"); 23 + 24 + const wsUrl = JETSTREAM_URL + "?wantedCollections=" + FLUSHING_STATUS_NSID; 25 + const ws = new WebSocket(wsUrl); 26 + 27 + ws.on("open", () => { 28 + console.log("Connected to Jetstream"); 29 + }); 30 + 31 + ws.on("message", async (data) => { 32 + messageCount++; 33 + if (messageCount % 1000 === 0) { 34 + console.log("Messages:", messageCount); 35 + } 36 + 37 + try { 38 + const message = JSON.parse(data.toString()); 39 + 40 + if (message.kind === "commit" && 41 + message.commit && 42 + message.commit.collection === FLUSHING_STATUS_NSID) { 43 + 44 + flushingFoundCount++; 45 + console.log("Found flushing record:", flushingFoundCount); 46 + console.log(JSON.stringify(message, null, 2)); 47 + 48 + const recordPath = message.commit.collection + "/" + message.commit.rkey; 49 + const authorDid = message.did; 50 + const cid = message.commit.cid || "cid_" + Date.now(); 51 + 52 + let recordText = "No text found"; 53 + let recordEmoji = "🚽"; 54 + let recordCreatedAt = new Date().toISOString(); 55 + 56 + if (message.commit.record) { 57 + if (message.commit.record.text) { 58 + recordText = message.commit.record.text; 59 + } 60 + if (message.commit.record.emoji) { 61 + recordEmoji = message.commit.record.emoji; 62 + } 63 + if (message.commit.record.createdAt) { 64 + recordCreatedAt = message.commit.record.createdAt; 65 + } 66 + } 67 + 68 + console.log("Author:", authorDid); 69 + console.log("Path:", recordPath); 70 + console.log("Text:", recordText); 71 + console.log("Emoji:", recordEmoji); 72 + console.log("Created at:", recordCreatedAt); 73 + 74 + const uri = "at://" + authorDid + "/" + recordPath; 75 + console.log("URI:", uri); 76 + 77 + if (supabase && message.commit.operation !== "delete") { 78 + try { 79 + const { data: existingData, error: existingError } = await supabase 80 + .from("flushing_entries") 81 + .select("id") 82 + .eq("uri", uri) 83 + .limit(1); 84 + 85 + if (existingError) { 86 + console.error("Error checking record:", existingError.message); 87 + } else if (existingData && existingData.length > 0) { 88 + console.log("Record exists, skipping"); 89 + } else { 90 + const record = { 91 + uri: uri, 92 + cid: cid, 93 + author_did: authorDid, 94 + author_handle: null, 95 + text: recordText, 96 + emoji: recordEmoji, 97 + created_at: recordCreatedAt 98 + }; 99 + 100 + console.log("Saving to Supabase"); 101 + 102 + const { error } = await supabase 103 + .from("flushing_entries") 104 + .insert(record); 105 + 106 + if (error) { 107 + console.error("Error:", error.message); 108 + } else { 109 + console.log("Success"); 110 + } 111 + } 112 + } catch (err) { 113 + console.error("Error:", err.message); 114 + } 115 + } 116 + } 117 + } catch (err) { 118 + console.error("Error processing message:", err.message); 119 + } 120 + }); 121 + 122 + ws.on("error", (error) => { 123 + console.error("WebSocket error:", error.message); 124 + }); 125 + 126 + ws.on("close", () => { 127 + console.log("Connection closed, reconnecting in 5s"); 128 + setTimeout(connect, 5000); 129 + }); 130 + } 131 + 132 + connect(); 133 + 134 + process.on("SIGINT", () => { 135 + console.log("Shutting down"); 136 + process.exit(0); 137 + });
+38
contextual info for claude/jetstream_worker_logs.md
··· 1 + I'm passing the env variables like this: 2 + 3 + pm2 start /opt/firehose-worker/app/scripts/jetstream13-worker.js --name firehose-worker --env NEXT_PUBLIC_SUPABASE_URL=https://zdzjtziydmwkxbzlkwxv.supabase.co --env SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inpkemp0eml5ZG13a3hiemxrd3h2Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTM5ODQ0MywiZXhwIjoyMDU2OTc0NDQzfQ.NF-bxFMB4kwbLXR4PWcbNp6FYBoPccMRZs6KtZif60k 4 + 5 + And I get this: 6 + 7 + 8 + 0|firehose | No Supabase credentials found, running in log-only mode 9 + 0|firehose | Connecting to Jetstream... 10 + 0|firehose | Connected to Jetstream successfully! 11 + 12 + 0|firehose-worker | FOUND A FLUSHING RECORD! Total found: 1 13 + 0|firehose-worker | { 14 + 0|firehose-worker | "did": "did:plc:gq4fo3u6tqzzdkjlwzpb23tj", 15 + 0|firehose-worker | "time_us": 1741442662774073, 16 + 0|firehose-worker | "kind": "commit", 17 + 0|firehose-worker | "commit": { 18 + 0|firehose-worker | "rev": "3ljuovov4o52p", 19 + 0|firehose-worker | "operation": "create", 20 + 0|firehose-worker | "collection": "im.flushing.right.now", 21 + 0|firehose-worker | "rkey": "3ljuovouvtf2p", 22 + 0|firehose-worker | "record": { 23 + 0|firehose-worker | "$type": "im.flushing.right.now", 24 + 0|firehose-worker | "createdAt": "2025-03-08T14:04:22.617Z", 25 + 0|firehose-worker | "emoji": "💧", 26 + 0|firehose-worker | "text": "testing 123" 27 + 0|firehose-worker | }, 28 + 0|firehose-worker | "cid": "bafyreif6yrp5fzban3bpr3cohww7wtqysvs3gkvcjv4q334spxtakz7zqu" 29 + 0|firehose-worker | } 30 + 0|firehose-worker | } 31 + 0|firehose-worker | Author DID: did:plc:gq4fo3u6tqzzdkjlwzpb23tj 32 + 0|firehose-worker | Record path: im.flushing.right.now/3ljuovouvtf2p 33 + 0|firehose-worker | Text: testing 123 34 + 0|firehose-worker | Emoji: 💧 35 + 0|firehose-worker | Created at: 2025-03-08T14:04:22.617Z 36 + 0|firehose-worker | Full URI: at://did:plc:gq4fo3u6tqzzdkjlwzpb23tj/im.flushing.right.now/3ljuovouvtf2p 37 + 0|firehose-worker | Missing required data, cannot create record 38 +
+185
contextual info for claude/worker_code.md
··· 1 + ⏺ Let's update the regex patterns to be more precise based on your feedback: 2 + 3 + cat > /opt/firehose-worker/app/scripts/regex-worker.js << 'EOL' 4 + const WebSocket = require("ws"); 5 + const { createClient } = require("@supabase/supabase-js"); 6 + 7 + // Constants 8 + const FIREHOSE_URL = 9 + "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos"; 10 + const FLUSHING_STATUS_NSID = "im.flushing.right.now"; 11 + 12 + // Supabase setup 13 + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; 14 + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; 15 + let supabase = null; 16 + 17 + if (supabaseUrl && supabaseKey) { 18 + console.log("Supabase credentials found, connecting..."); 19 + supabase = createClient(supabaseUrl, supabaseKey); 20 + } else { 21 + console.log("No Supabase credentials found, running in log-only mode"); 22 + } 23 + 24 + // Initialize WebSocket and counters 25 + let ws = null; 26 + let messageCount = 0; 27 + let flushingFoundCount = 0; 28 + 29 + // Main connection function 30 + function connect() { 31 + console.log("Connecting to Bluesky firehose..."); 32 + 33 + ws = new WebSocket(FIREHOSE_URL); 34 + 35 + ws.on("open", () => { 36 + console.log("Connected to firehose successfully!"); 37 + }); 38 + 39 + ws.on("message", async (data) => { 40 + messageCount++; 41 + 42 + // Log message count periodically 43 + if (messageCount % 1000 === 0) { 44 + console.log("Messages received:", messageCount); 45 + } 46 + 47 + // Check for flushing records 48 + const strData = data.toString(); 49 + if (strData.includes(FLUSHING_STATUS_NSID)) { 50 + try { 51 + flushingFoundCount++; 52 + console.log("FOUND A FLUSHING RECORD! Total found:", 53 + flushingFoundCount); 54 + console.log("----------START RAW DATA LOG----------"); 55 + console.log(strData); 56 + console.log("----------END RAW DATA LOG----------"); 57 + 58 + // DID extraction - exact length pattern for did:plc:XXX 59 + let authorDid = null; 60 + const didMatch = strData.match(/did:plc:[a-z0-9]{24}/); 61 + if (didMatch) { 62 + authorDid = didMatch[0]; 63 + console.log("Author DID:", authorDid); 64 + } 65 + 66 + // Path extraction - look for the NS with exact record ID length 67 + let recordPath = null; 68 + const pathMatch = strData.match(new RegExp(FLUSHING_STATUS_NSID + 69 + "/([a-z0-9]{11}[a-z0-9]*)")); 70 + if (pathMatch) { 71 + recordPath = FLUSHING_STATUS_NSID + "/" + 72 + pathMatch[1].substring(0, 11); // Take just first 11 chars 73 + console.log("Record path:", recordPath); 74 + } 75 + 76 + // CID extraction - just use a timestamp for now 77 + const cid = "cid_" + Date.now(); 78 + 79 + // Extract text - looking for the content between dtext and e$type 80 + let text = "No text found"; 81 + // Try to find text by looking for the sequence: dtext, length 82 + indicator, content 83 + // More greedy pattern to capture spaces and special chars 84 + const textMatch = strData.match(/dtext[a-z]([^e$]+)/); 85 + if (textMatch) { 86 + text = textMatch[1]; 87 + console.log("Text:", text); 88 + } 89 + 90 + // Extract emoji - looking specifically for emoji character 91 + let emoji = "🚽"; // Default emoji 92 + const emojiMatch = strData.match(/eemoji[a-z]([^i]+)/); 93 + if (emojiMatch) { 94 + emoji = emojiMatch[1]; 95 + console.log("Emoji:", emoji); 96 + } 97 + 98 + // Extract createdAt - ISO format date 99 + let createdAt = new Date().toISOString(); 100 + const createdAtMatch = 101 + strData.match(/icreatedAt[a-z]([\d-]+T[\d:.]+Z)/); 102 + if (createdAtMatch) { 103 + createdAt = createdAtMatch[1]; 104 + console.log("Created at:", createdAt); 105 + } 106 + 107 + // Only proceed if we have the minimum required data 108 + if (authorDid && recordPath) { 109 + // Create URI 110 + const uri = "at://" + authorDid + "/" + recordPath; 111 + console.log("Full URI:", uri); 112 + 113 + // If Supabase is configured, store the record 114 + if (supabase) { 115 + try { 116 + // Check if the record already exists 117 + const { data: existingData, error: existingError } = await 118 + supabase 119 + .from("flushing_entries") 120 + .select("id") 121 + .eq("uri", uri) 122 + .limit(1); 123 + 124 + if (existingError) { 125 + console.error("Error checking existing record:", 126 + existingError.message); 127 + } else if (existingData && existingData.length > 0) { 128 + console.log("Record already exists, skipping"); 129 + } else { 130 + // Create the record with the extracted data 131 + const record = { 132 + uri: uri, 133 + cid: cid, 134 + author_did: authorDid, 135 + author_handle: null, // Skip handle resolution for now 136 + text: text, 137 + emoji: emoji, 138 + created_at: createdAt 139 + }; 140 + 141 + console.log("Saving record to Supabase:", 142 + JSON.stringify(record)); 143 + 144 + const { error } = await supabase 145 + .from("flushing_entries") 146 + .insert(record); 147 + 148 + if (error) { 149 + console.error("Error storing record:", error.message); 150 + } else { 151 + console.log("Successfully stored record in Supabase!"); 152 + } 153 + } 154 + } catch (err) { 155 + console.error("Supabase operation error:", err.message); 156 + } 157 + } 158 + } else { 159 + console.log("Missing required data, cannot create record"); 160 + } 161 + } catch (err) { 162 + console.error("Error processing flushing record:", err.message); 163 + } 164 + } 165 + }); 166 + 167 + ws.on("error", (error) => { 168 + console.error("WebSocket error:", error.message); 169 + }); 170 + 171 + ws.on("close", () => { 172 + console.log("Connection closed, reconnecting in 5 seconds..."); 173 + setTimeout(connect, 5000); 174 + }); 175 + } 176 + 177 + // Start the connection 178 + connect(); 179 + 180 + // Handle termination 181 + process.on("SIGINT", () => { 182 + console.log("Shutting down..."); 183 + if (ws) ws.close(); 184 + process.exit(0); 185 + });
+45
contextual info for claude/worker_logs.md
··· 1 + 0|firehose-worker | Messages received: 14000 2 + 0|firehose-worker | Messages received: 15000 3 + 0|firehose-worker | Messages received: 16000 4 + 0|firehose-worker | FOUND A FLUSHING RECORD! Total found: 2 5 + 0|firehose-worker | ----------START RAW DATA LOG---------- 6 + UiU��ɞ�_�l�e3�'O�=q�9�&��L�%@dpathx#im.flushing.right.now/3ljtrya7cnt2dfactionfcreatecrevm3ljtrya7lh32dcseq/�prev�drepox did:plc:gq4fo3u6tqzzdkjlwzpb23tjdtimex2025-03-08T05:26:49.480Zeblobs�esincem3ljtrxh4aaq2sfblocksY�:�eroots��*X%q i�4<�M� 7 + ��U�8�׵�������M�Hgversion�q �5�W}��� ��dL���ÿص���ʶ�d0�ae��akX app.bsky.feed.like/3limmtry4hd24apat�*X%q �ŗ-?G�G)�uP�.Q��e�7_��ʖ��iav�*X%q �3�|�6��W���^�����3�Gd ���al�*X%q ��c=�R��HTPt�f�>Q�`�4wY�ǎ�M�Q�q �ŗ-?G�G)�uP�.Q��e�7_��ʖ��i�ae��akX app.bsky.feed.like/3lirms5gfgr2wapat�*X%q O<c�V� 8 + �ѓUP�� 9 + D"��H"v�pܢ�b�al�*X%q �Rڠ���q�.�at�S;��+�c�e�VL�x�q O<c�V�%q I���d 10 + �ѓUP�� 11 + -[�|���G{k@v�h�ae��akX%app.bsky.graph.listitem/3lcfutwqu3i2vapat�*X%q ��o_M]� 12 + 0|firehose-worker | zӗ��!r�׾�A�h�4��9��av�*X%q ۾�wσ�$��j�����x��M�{��� 13 + 0|firehose-worker | ֟Xb�akJg55a2clf27apt�*X%q ҙh��m�6��lG�SM�g9J��њ� 14 + 0|firehose-worker | 施aav�*X%q f���AY���)LT��p#�C��ٖ����t�al�*X%q �z��kO��*��P�����me�n@�v���N��q ҙh��m�6��lG�SM�g9J��њ� 15 + �_뇏�T��շ�1� av�*X%q ��X���l�4��GKjp.bsk����*Y��;���akIqish3fb2vapat�*X%q �Ŧ4����Bv��[�jk��q���E�3`��av�*X%q dh���\���Z'���3�G�1��+JIk�akX#blue.badge.collection/3lciipwj74w27apat�*X%q ���asC��y��t 16 + M���kǤ���b�W6AHav�*X%q ��E����m�P��n����FxSI�)pb�󔶮al�*X%q 2�(fY�8��' ���hò�Y�99������ ���asC��y��t 17 + M���kǤ���b�W6AH�ae��akX(fyi.unravel.frontpage.vote/3lhtvfwjjgz2zapat�*X%q w������'*��e�L���a� 18 + 0|firehose-worker | ��)���av�*X%q ��],�iʀ�.B���9KƦp�U��r����akKjjoqcgmxc24apat�*X%q #�D�a2��V�Q��"B�4g�U+�.R�-sav�*X%q �h�9;�Ma��ס����q #�D�a2��V�Q��"B�4g�U+�.R�-s�ae��akX$xyz.statusphere.status/3lcv5ip27f22fapat�*X%q ��w��8�)S��MB�c`�ly�̘�]��,av�*X%q �2Lo€U�l���3��� ̽v_�J��"�}�al�*X%q ;�Y������dQ�k4����������.�q ;�Y������dQ�k4����������.�ae��akX(fyi.unravel.frontpage.vote/3ljplvf7sok26apat�av�*X%q �����R��l�y)��v��N���rίD~Q|�akImgo7kx62qapat�*X%q ^�p[�ͅF9����X<qq� 19 + ��h!^�Fǥav�*X%q ��Z�U/��$�u��� 20 + 0|firehose-worker | ��5P����Oа���h�akX#im.flushing.right.now/3ljtmiqg5wf2qapat�*X%q ��7��[9��͠X���˳��ء� 21 + 0|firehose-worker | r���0��av�*X%q ]�:��_bI�y�ʥ1"�X��0����?^��al�� q ��7��[9��͠X���˳��ء� 22 + 0|firehose-worker | r���0���ae��akX#im.flushing.right.now/3ljtn776gtg26apat�*X%q $@�z����R1�P�� 23 + ݏ�텗Y�-)av�*X%q &{U�M�0?P �K�ݚ�[�-����ch�ې`�akIqwyrcig2kapat�av�*X%q ���n� 24 + ��/Ϧ�����akIraud3lj2dapat�*X%q ���o*��!����EeLl�^'LI� 25 + �:8k��E�l����䐕oav�*X%q �lkB�.�����)�u�p&�l�A9 ��Z���R 26 + �akHoneu3624apt�*X%q q��q�;>��N� R�={���2��Y�p��b�av�*X%q [t:{ݻӇ/��n���^1�g�E0��.w��akX%s.dame.counting.turtles/3ljqwqisfbr26apat�*X%q SRd��:�n���.�E�t�ǻR�H�*Қ�tav�*X%q 8ɔ)Bm�)���]�Bd�#�y0@Ю��d�,I�akQnow/3ljqyedf2ky2vaat�*X%q r7�F9j*Әy�j���?�W{�>�-0�� 27 + 0|firehose-worker | *�av�*X%q #�P���r��40����^�F�?��9iV5o�akJseb5qoqd2bapat�av�*X%q 2�Jz��X��=�J�/T� �)Dt]�B#�n�akItvncktx22apat�*X%q �4m�X����e�� ��$dO�a�9ώ���8�av�*X%q �q����2'�l%��p�����2tW�[Ѱ�d�֤akX&social.psky.chat.message/3lchdaiqvnklmapat�*X%q �n���/e����1� 28 + 0|firehose-worker | ���l�:��yb�av�*X%q +�) 29 + C9����|<�C�|�D 30 + $��;s*u� ��n��1�av�*X%q vy���$���ܜٝ�#4�d�akX#tools.atp.typing.test/23ipsx246q2lsapat�*X%q ��6�W�滊�[�� 31 + 0|firehose-worker | �n|`o��*Zv�S2i�g�W��#�,r�C �av�*X%q ��2� 32 + 0|firehose-worker | Kx�-\G��8a��Ä����DW%��CCAal��q q��q�;>��N� R�={���2��Y�p��b��ae��akX#im.flushing.right.now/3ljtrxh3sla2saUiU��ɞ�_�l�e3�'O�=q�9�&��L�%@�dtextx(now let's add some Fun! and numbers? 123e$typeuim.flushing.right.noweemojif⏱️icreatedAtx2025-03-08T05:26:49.380Z�q i�4<�M� 33 + ��U�8�׵�������M�H�cdidx did:plc:gq4fo3u6tqzzdkjlwzpb23tjcrevm3ljtrya7lh32dcsigX@��p-��0�k��]͉�[��d��q1�Z�c1{s�$�Đ@FU�=���a��u�B-��**ddata�*X%q �5�W}��� ��dL���ÿص���ʶ�d0dprev�gversionfcommit�*X%q i�4<�M� 34 + ��U�8�׵�������M�Hfrebase�ftooBig� 35 + 0|firehose-worker | ----------END RAW DATA LOG---------- 36 + 0|firehose-worker | Author DID: did:plc:gq4fo3u6tqzzdkjlwzpb23tj 37 + 0|firehose-worker | Record path: im.flushing.right.now/3ljtrya7cnt2d 38 + 0|firehose-worker | Text: (now let's add some Fun! and numbers? 123 39 + 0|firehose-worker | Full URI: at://did:plc:gq4fo3u6tqzzdkjlwzpb23tj/im.flushing.right.now/3ljtrya7cnt2d 40 + 0|firehose-worker | Saving record to Supabase: {"uri":"at://did:plc:gq4fo3u6tqzzdkjlwzpb23tj/im.flushing.right.now/3ljtrya7cnt2d","cid":"cid_1741411609593","author_did":"did:plc:gq4fo3u6tqzzdkjlwzpb23tj","author_handle":null,"text":"(now let's add some Fun! and numbers? 123","emoji":"🚽","created_at":"2025-03-08T05:26:49.593Z"} 41 + 0|firehose-worker | Successfully stored record in Supabase! 42 + 0|firehose-worker | Messages received: 17000 43 + 0|firehose-worker | Messages received: 18000 44 + 0|firehose-worker | Messages received: 19000 45 + 0|firehose-worker | Messages received: 20000