tangled
alpha
login
or
join now
indexx.dev
/
tweets2bsky
forked from
j4ck.xyz/tweets2bsky
0
fork
atom
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.
0
fork
atom
overview
issues
pulls
pipelines
Fix: Skip retweets and fix screenshot aspect ratio
jack
2 months ago
cfdf1825
d2236320
+29
-9
1 changed file
expand all
collapse all
unified
split
src
index.ts
+29
-9
src/index.ts
···
93
entities?: TweetEntities;
94
extended_entities?: TweetEntities;
95
quoted_status_id_str?: string;
0
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,
0
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> {
0
0
0
0
0
0
431
const browserPaths = [
432
'/usr/bin/google-chrome',
433
'/usr/bin/chromium-browser',
···
494
495
const element = await page.$('#container');
496
if (element) {
0
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;
0
0
500
}
501
} catch (err) {
502
console.error(`[SCREENSHOT] ❌ Error capturing tweet:`, (err as Error).message);
···
806
807
if (processedTweets[tweetId]) continue;
808
0
0
0
0
0
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 });
0
0
0
0
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
}