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