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.

feat: add support for custom backfill limits in web UI

jack 90b70a02 435cc19b

+21 -12
+5 -3
public/index.html
··· 207 207 }; 208 208 209 209 const runBackfill = async (id, twitterUsername) => { 210 - if (!confirm(`Run full history backfill for @${twitterUsername}? This may take a while.`)) return; 210 + const limit = prompt(`How many tweets to backfill for @${twitterUsername}?`, "100"); 211 + if (limit === null) return; // Cancelled 212 + 211 213 try { 212 - await axios.post(`/api/backfill/${id}`, {}, { 214 + await axios.post(`/api/backfill/${id}`, { limit: parseInt(limit) || 100 }, { 213 215 headers: { Authorization: `Bearer ${token}` } 214 216 }); 215 217 alert(`Backfill queued for @${twitterUsername}`); ··· 280 282 ); 281 283 } 282 284 283 - const isBackfillQueued = (id) => status.pendingBackfills?.includes(id); 285 + const isBackfillQueued = (id) => status.pendingBackfills?.some(b => (b.id || b) === id); 284 286 285 287 return ( 286 288 <div>
+5 -3
src/index.ts
··· 582 582 const agent = await getAgent(mapping); 583 583 if (!agent) continue; 584 584 585 - if (forceBackfill || pendingBackfills.includes(mapping.id)) { 586 - console.log(`[${mapping.twitterUsername}] Running backfill (limit 100)...`); 587 - await importHistory(mapping.twitterUsername, 100, dryRun); 585 + const backfillReq = pendingBackfills.find(b => b.id === mapping.id); 586 + if (forceBackfill || backfillReq) { 587 + const limit = backfillReq?.limit || 100; 588 + console.log(`[${mapping.twitterUsername}] Running backfill (limit ${limit})...`); 589 + await importHistory(mapping.twitterUsername, limit, dryRun); 588 590 clearBackfill(mapping.id); 589 591 console.log(`[${mapping.twitterUsername}] Backfill complete.`); 590 592 } else {
+11 -6
src/server.ts
··· 17 17 // In-memory state for triggers and scheduling 18 18 let lastCheckTime = Date.now(); 19 19 let nextCheckTime = Date.now() + (getConfig().checkIntervalMinutes || 5) * 60 * 1000; 20 - let pendingBackfills: string[] = []; 20 + interface PendingBackfill { 21 + id: string; 22 + limit?: number; 23 + } 24 + let pendingBackfills: PendingBackfill[] = []; 21 25 22 26 app.use(cors()); 23 27 app.use(express.json()); ··· 176 180 177 181 app.post('/api/backfill/:id', authenticateToken, requireAdmin, (req, res) => { 178 182 const { id } = req.params; 183 + const { limit } = req.body; 179 184 const config = getConfig(); 180 185 const mapping = config.mappings.find((m) => m.id === id); 181 186 ··· 184 189 return; 185 190 } 186 191 187 - if (!pendingBackfills.includes(id)) { 188 - pendingBackfills.push(id); 192 + if (!pendingBackfills.find(b => b.id === id)) { 193 + pendingBackfills.push({ id, limit: limit ? Number(limit) : undefined }); 189 194 } 190 195 191 196 lastCheckTime = 0; ··· 195 200 196 201 app.delete('/api/backfill/:id', authenticateToken, (req, res) => { 197 202 const { id } = req.params; 198 - pendingBackfills = pendingBackfills.filter((bid) => bid !== id); 203 + pendingBackfills = pendingBackfills.filter((bid) => bid.id !== id); 199 204 res.json({ success: true }); 200 205 }); 201 206 ··· 206 211 nextCheckTime = lastCheckTime + (config.checkIntervalMinutes || 5) * 60 * 1000; 207 212 } 208 213 209 - export function getPendingBackfills(): string[] { 214 + export function getPendingBackfills(): PendingBackfill[] { 210 215 return [...pendingBackfills]; 211 216 } 212 217 ··· 215 220 } 216 221 217 222 export function clearBackfill(id: string) { 218 - pendingBackfills = pendingBackfills.filter((bid) => bid !== id); 223 + pendingBackfills = pendingBackfills.filter((bid) => bid.id !== id); 219 224 } 220 225 221 226 // Serve the frontend for any other route (middleware approach for Express 5)