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.

Merge pull request #7 from j4ckxyz/codex/fix-job-not-starting-for-empty-account

Codex-generated pull request

authored by

jack and committed by
GitHub
b55991bd 6f048a0c

+72 -3
+67 -3
src/index.ts
··· 2027 2027 2028 2028 // Task management 2029 2029 const activeTasks = new Map<string, Promise<void>>(); 2030 + const DEFAULT_BACKFILL_ACCOUNT_TIMEOUT_MS = 2 * 60 * 1000; 2031 + 2032 + const resolveBackfillAccountTimeoutMs = (): number => { 2033 + const raw = Number(process.env.BACKFILL_ACCOUNT_TIMEOUT_MS); 2034 + if (Number.isFinite(raw) && raw >= 15_000) { 2035 + return raw; 2036 + } 2037 + return DEFAULT_BACKFILL_ACCOUNT_TIMEOUT_MS; 2038 + }; 2039 + 2040 + async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutMessage: string): Promise<T> { 2041 + let timeoutHandle: NodeJS.Timeout | undefined; 2042 + const timeoutPromise = new Promise<never>((_, reject) => { 2043 + timeoutHandle = setTimeout(() => { 2044 + reject(new Error(timeoutMessage)); 2045 + }, timeoutMs); 2046 + }); 2047 + 2048 + try { 2049 + return await Promise.race([promise, timeoutPromise]); 2050 + } finally { 2051 + if (timeoutHandle) { 2052 + clearTimeout(timeoutHandle); 2053 + } 2054 + } 2055 + } 2030 2056 2031 2057 async function runAccountTask(mapping: AccountMapping, backfillRequest?: PendingBackfill, dryRun = false) { 2032 2058 if (activeTasks.has(mapping.id)) return; // Already running 2033 2059 2034 2060 const task = (async () => { 2035 2061 try { 2062 + const backfillReq = backfillRequest ?? getPendingBackfills().find((b) => b.id === mapping.id); 2063 + 2064 + if (mapping.twitterUsernames.length === 0) { 2065 + console.warn(`[${mapping.bskyIdentifier}] ⚠️ No Twitter usernames configured. Skipping mapping.`); 2066 + if (backfillReq) { 2067 + clearBackfill(mapping.id, backfillReq.requestId); 2068 + updateAppStatus({ 2069 + state: 'idle', 2070 + currentAccount: undefined, 2071 + processedCount: 0, 2072 + totalCount: 0, 2073 + message: `Backfill skipped for ${mapping.bskyIdentifier}: no source accounts configured`, 2074 + backfillMappingId: undefined, 2075 + backfillRequestId: undefined, 2076 + }); 2077 + } 2078 + return; 2079 + } 2080 + 2036 2081 const agent = await getAgent(mapping); 2037 - if (!agent) return; 2082 + if (!agent) { 2083 + if (backfillReq) { 2084 + console.warn(`[${mapping.bskyIdentifier}] ⚠️ Backfill aborted: unable to authenticate Bluesky account.`); 2085 + clearBackfill(mapping.id, backfillReq.requestId); 2086 + updateAppStatus({ 2087 + state: 'idle', 2088 + currentAccount: undefined, 2089 + processedCount: 0, 2090 + totalCount: mapping.twitterUsernames.length, 2091 + message: `Backfill skipped for ${mapping.bskyIdentifier}: Bluesky login failed`, 2092 + backfillMappingId: undefined, 2093 + backfillRequestId: undefined, 2094 + }); 2095 + } 2096 + return; 2097 + } 2038 2098 2039 - const backfillReq = backfillRequest ?? getPendingBackfills().find((b) => b.id === mapping.id); 2040 2099 const explicitBackfill = Boolean(backfillRequest); 2041 2100 2042 2101 if (backfillReq) { 2043 2102 const limit = backfillReq.limit || 15; 2103 + const backfillAccountTimeoutMs = resolveBackfillAccountTimeoutMs(); 2044 2104 const accountCount = mapping.twitterUsernames.length; 2045 2105 const estimatedTotalTweets = accountCount * limit; 2046 2106 console.log( ··· 2079 2139 backfillMappingId: mapping.id, 2080 2140 backfillRequestId: backfillReq.requestId, 2081 2141 }); 2082 - await importHistory(twitterUsername, mapping.bskyIdentifier, limit, dryRun, false, backfillReq.requestId); 2142 + await withTimeout( 2143 + importHistory(twitterUsername, mapping.bskyIdentifier, limit, dryRun, false, backfillReq.requestId), 2144 + backfillAccountTimeoutMs, 2145 + `[${twitterUsername}] Backfill timed out after ${Math.round(backfillAccountTimeoutMs / 1000)}s`, 2146 + ); 2083 2147 updateAppStatus({ 2084 2148 state: 'backfilling', 2085 2149 currentAccount: twitterUsername,
+5
src/server.ts
··· 1928 1928 return; 1929 1929 } 1930 1930 1931 + if (!Array.isArray(mapping.twitterUsernames) || mapping.twitterUsernames.length === 0) { 1932 + res.status(400).json({ error: 'Mapping has no Twitter source accounts configured.' }); 1933 + return; 1934 + } 1935 + 1931 1936 const parsedLimit = Number(limit); 1932 1937 const safeLimit = Number.isFinite(parsedLimit) ? Math.max(1, Math.min(parsedLimit, 200)) : undefined; 1933 1938 const queuedAt = Date.now();