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