A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.
11
fork

Configure Feed

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

Add scheduler per-account timeout watchdog and docs

jack 9de9d395 c2e10731

+66 -3
+1
Dockerfile
··· 31 31 ENV NODE_ENV=production \ 32 32 HOST=0.0.0.0 \ 33 33 PORT=3000 \ 34 + SCHEDULED_ACCOUNT_TIMEOUT_MS=480000 \ 34 35 CHROME_BIN=/usr/bin/chromium \ 35 36 PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium 36 37
+20 -1
README.md
··· 176 176 - `JWT_EXPIRES_IN` 177 177 - `CORS_ALLOWED_ORIGINS` 178 178 - `BSKY_APPVIEW_URL` (optional override) 179 + - `SCHEDULED_ACCOUNT_TIMEOUT_MS` (default `480000` / 8 minutes, forces a skip when one source account hangs during scheduled checks) 179 180 180 181 ### 4) Persistent data inside Docker 181 182 ··· 213 214 j4ckxyz/tweets-2-bsky:latest 214 215 ``` 215 216 216 - ### 7) Platform support 217 + ### 7) Debug logs (especially useful on Raspberry Pi) 218 + 219 + If runs appear stuck, stream logs live: 220 + 221 + ```bash 222 + docker logs -f tweets-2-bsky 223 + ``` 224 + 225 + For source installs, use whichever runtime you started with: 226 + 227 + ```bash 228 + pm2 logs tweets-2-bsky 229 + # or 230 + tail -f data/runtime/nohup.out 231 + ``` 232 + 233 + If an account hangs during a scheduled cycle, the scheduler now times out that account and moves on automatically. You can tune this with `SCHEDULED_ACCOUNT_TIMEOUT_MS`. 234 + 235 + ### 8) Platform support 217 236 218 237 The Docker build is designed for multi-platform images: 219 238
+25
TROUBLESHOOTING.md
··· 85 85 bun run cli -- status 86 86 ``` 87 87 88 + ### Scheduler appears stuck on one account 89 + If a single source account hangs for a long time (media fetch/processing), scheduled checks now skip that account after a timeout and continue with the next one. 90 + 91 + - Default timeout: `480000` ms (8 minutes) 92 + - Override with env var: `SCHEDULED_ACCOUNT_TIMEOUT_MS` 93 + 94 + Examples: 95 + 96 + ```bash 97 + # Docker 98 + docker run -d --name tweets-2-bsky -e SCHEDULED_ACCOUNT_TIMEOUT_MS=300000 -p 3000:3000 -v tweets2bsky_data:/app/data j4ckxyz/tweets-2-bsky:latest 99 + 100 + # Source install (.env) 101 + echo 'SCHEDULED_ACCOUNT_TIMEOUT_MS=300000' >> .env 102 + ./update.sh 103 + ``` 104 + 105 + To watch logs while debugging on Raspberry Pi: 106 + 107 + ```bash 108 + docker logs -f tweets-2-bsky 109 + # or for source/PM2 110 + pm2 logs tweets-2-bsky 111 + ``` 112 + 88 113 ### Docker: permissions writing `/app/data` 89 114 If the container fails to write `config.json` or `database.sqlite`, ensure `/app/data` is writable by the container process. 90 115
+20 -2
src/index.ts
··· 2029 2029 // Task management 2030 2030 const activeTasks = new Map<string, Promise<void>>(); 2031 2031 const DEFAULT_BACKFILL_ACCOUNT_TIMEOUT_MS = 2 * 60 * 1000; 2032 + const DEFAULT_SCHEDULED_ACCOUNT_TIMEOUT_MS = 8 * 60 * 1000; 2032 2033 const PROFILE_SYNC_INTERVAL_MS = 24 * 60 * 60 * 1000; 2033 2034 let profileSyncStateWriteQueue: Promise<void> = Promise.resolve(); 2034 2035 ··· 2058 2059 return raw; 2059 2060 } 2060 2061 return DEFAULT_BACKFILL_ACCOUNT_TIMEOUT_MS; 2062 + }; 2063 + 2064 + const resolveScheduledAccountTimeoutMs = (): number => { 2065 + const raw = Number(process.env.SCHEDULED_ACCOUNT_TIMEOUT_MS); 2066 + if (Number.isFinite(raw) && raw >= 30_000) { 2067 + return raw; 2068 + } 2069 + return DEFAULT_SCHEDULED_ACCOUNT_TIMEOUT_MS; 2061 2070 }; 2062 2071 2063 2072 const normalizeMappingHandle = (value: string): string => value.trim().replace(/^@/, '').toLowerCase(); ··· 2320 2329 console.log(`${logPrefix} Backfill complete.`); 2321 2330 } else { 2322 2331 updateAppStatus({ backfillMappingId: undefined, backfillRequestId: undefined }); 2332 + const scheduledAccountTimeoutMs = resolveScheduledAccountTimeoutMs(); 2323 2333 2324 2334 // Pre-load processed IDs for optimization 2325 2335 const processedMap = loadProcessedTweets(mapping.bskyIdentifier); ··· 2339 2349 2340 2350 // Use fetchUserTweets with early stopping optimization 2341 2351 // Increase limit slightly since we have early stopping now 2342 - const tweets = await fetchUserTweets(twitterUsername, 50, processedIds); 2352 + const tweets = await withTimeout( 2353 + fetchUserTweets(twitterUsername, 50, processedIds), 2354 + scheduledAccountTimeoutMs, 2355 + `[${twitterUsername}] Scheduled fetch timed out after ${Math.round(scheduledAccountTimeoutMs / 1000)}s`, 2356 + ); 2343 2357 2344 2358 if (!tweets || tweets.length === 0) { 2345 2359 console.log(`[${twitterUsername}] ℹ️ No tweets found (or fetch failed).`); ··· 2347 2361 } 2348 2362 2349 2363 console.log(`[${twitterUsername}] 📥 Fetched ${tweets.length} tweets.`); 2350 - await processTweets(agent, twitterUsername, mapping.bskyIdentifier, tweets, dryRun); 2364 + await withTimeout( 2365 + processTweets(agent, twitterUsername, mapping.bskyIdentifier, tweets, dryRun), 2366 + scheduledAccountTimeoutMs, 2367 + `[${twitterUsername}] Scheduled processing timed out after ${Math.round(scheduledAccountTimeoutMs / 1000)}s`, 2368 + ); 2351 2369 } catch (err) { 2352 2370 sourceErrors += 1; 2353 2371 console.error(`${logPrefix} ❌ Error checking @${twitterUsername}: ${describeError(err)}`);