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.

Fix: Skip retweets and fix screenshot aspect ratio

jack cfdf1825 d2236320

+29 -9
+29 -9
src/index.ts
··· 93 entities?: TweetEntities; 94 extended_entities?: TweetEntities; 95 quoted_status_id_str?: string; 96 is_quote_status?: boolean; 97 in_reply_to_status_id_str?: string; 98 in_reply_to_status_id?: string; ··· 281 // biome-ignore lint/suspicious/noExplicitAny: raw types match compatible structure 282 extended_entities: raw.extended_entities as any, 283 quoted_status_id_str: raw.quoted_status_id_str, 284 is_quote_status: !!raw.quoted_status_id_str, 285 in_reply_to_status_id_str: raw.in_reply_to_status_id_str, 286 // biome-ignore lint/suspicious/noExplicitAny: missing in LegacyTweetRaw type ··· 427 return data.blob; 428 } 429 430 - async function captureTweetScreenshot(tweetUrl: string): Promise<Buffer | null> { 431 const browserPaths = [ 432 '/usr/bin/google-chrome', 433 '/usr/bin/chromium-browser', ··· 494 495 const element = await page.$('#container'); 496 if (element) { 497 const buffer = await element.screenshot({ type: 'png', omitBackground: true }); 498 - console.log(`[SCREENSHOT] ✅ Captured successfully (${(buffer.length / 1024).toFixed(2)} KB)`); 499 - return buffer as Buffer; 500 } 501 } catch (err) { 502 console.error(`[SCREENSHOT] ❌ Error capturing tweet:`, (err as Error).message); ··· 806 807 if (processedTweets[tweetId]) continue; 808 809 console.log(`\n[${twitterUsername}] 🔍 Inspecting tweet: ${tweetId}`); 810 updateAppStatus({ 811 state: 'processing', ··· 1013 1014 // Try to capture screenshot for external QTs if we have space for images 1015 if (images.length < 4 && !videoBlob) { 1016 - const ssBuffer = await captureTweetScreenshot(externalQuoteUrl); 1017 - if (ssBuffer) { 1018 try { 1019 let blob: BlobRef; 1020 if (dryRun) { 1021 - console.log(`[${twitterUsername}] 🧪 [DRY RUN] Would upload screenshot for quote (${(ssBuffer.length/1024).toFixed(2)} KB)`); 1022 - blob = { ref: { toString: () => 'mock-ss-blob' }, mimeType: 'image/png', size: ssBuffer.length } as any; 1023 } else { 1024 - blob = await uploadToBluesky(agent, ssBuffer, 'image/png'); 1025 } 1026 - images.push({ alt: `Quote Tweet: ${externalQuoteUrl}`, image: blob }); 1027 } catch (e) { 1028 console.warn(`[${twitterUsername}] ⚠️ Failed to upload screenshot blob.`); 1029 }
··· 93 entities?: TweetEntities; 94 extended_entities?: TweetEntities; 95 quoted_status_id_str?: string; 96 + retweeted_status_id_str?: string; 97 is_quote_status?: boolean; 98 in_reply_to_status_id_str?: string; 99 in_reply_to_status_id?: string; ··· 282 // biome-ignore lint/suspicious/noExplicitAny: raw types match compatible structure 283 extended_entities: raw.extended_entities as any, 284 quoted_status_id_str: raw.quoted_status_id_str, 285 + retweeted_status_id_str: raw.retweeted_status_id_str, 286 is_quote_status: !!raw.quoted_status_id_str, 287 in_reply_to_status_id_str: raw.in_reply_to_status_id_str, 288 // biome-ignore lint/suspicious/noExplicitAny: missing in LegacyTweetRaw type ··· 429 return data.blob; 430 } 431 432 + interface ScreenshotResult { 433 + buffer: Buffer; 434 + width: number; 435 + height: number; 436 + } 437 + 438 + async function captureTweetScreenshot(tweetUrl: string): Promise<ScreenshotResult | null> { 439 const browserPaths = [ 440 '/usr/bin/google-chrome', 441 '/usr/bin/chromium-browser', ··· 502 503 const element = await page.$('#container'); 504 if (element) { 505 + const box = await element.boundingBox(); 506 const buffer = await element.screenshot({ type: 'png', omitBackground: true }); 507 + if (box) { 508 + console.log(`[SCREENSHOT] ✅ Captured successfully (${(buffer.length / 1024).toFixed(2)} KB) - ${Math.round(box.width)}x${Math.round(box.height)}`); 509 + return { buffer: buffer as Buffer, width: Math.round(box.width), height: Math.round(box.height) }; 510 + } 511 } 512 } catch (err) { 513 console.error(`[SCREENSHOT] ❌ Error capturing tweet:`, (err as Error).message); ··· 817 818 if (processedTweets[tweetId]) continue; 819 820 + if (tweet.retweeted_status_id_str) { 821 + console.log(`[${twitterUsername}] ⏩ Skipping retweet ${tweetId}.`); 822 + continue; 823 + } 824 + 825 console.log(`\n[${twitterUsername}] 🔍 Inspecting tweet: ${tweetId}`); 826 updateAppStatus({ 827 state: 'processing', ··· 1029 1030 // Try to capture screenshot for external QTs if we have space for images 1031 if (images.length < 4 && !videoBlob) { 1032 + const ssResult = await captureTweetScreenshot(externalQuoteUrl); 1033 + if (ssResult) { 1034 try { 1035 let blob: BlobRef; 1036 if (dryRun) { 1037 + console.log(`[${twitterUsername}] 🧪 [DRY RUN] Would upload screenshot for quote (${(ssResult.buffer.length/1024).toFixed(2)} KB)`); 1038 + blob = { ref: { toString: () => 'mock-ss-blob' }, mimeType: 'image/png', size: ssResult.buffer.length } as any; 1039 } else { 1040 + blob = await uploadToBluesky(agent, ssResult.buffer, 'image/png'); 1041 } 1042 + images.push({ 1043 + alt: `Quote Tweet: ${externalQuoteUrl}`, 1044 + image: blob, 1045 + aspectRatio: { width: ssResult.width, height: ssResult.height } 1046 + }); 1047 } catch (e) { 1048 console.warn(`[${twitterUsername}] ⚠️ Failed to upload screenshot blob.`); 1049 }