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: robust t.co link expansion fallback for link cards

jack aa76ee80 67ed40fd

+10 -64
+10 -1
src/index.ts
··· 833 833 if (tco && expanded) text = text.replace(tco, expanded); 834 834 } 835 835 836 + // Fallback: Regex for t.co links (if entities failed or missed one) 836 837 const tcoRegex = /https:\/\/t\.co\/[a-zA-Z0-9]+/g; 837 838 const matches = text.match(tcoRegex) || []; 838 839 for (const tco of matches) { 840 + // Avoid re-resolving if we already handled it via entities 841 + if (urls.some(u => u.url === tco)) continue; 842 + 843 + console.log(`[${twitterUsername}] 🔍 Resolving fallback link: ${tco}`); 839 844 const resolved = await expandUrl(tco); 840 - if (resolved !== tco) text = text.replace(tco, resolved); 845 + if (resolved !== tco) { 846 + text = text.replace(tco, resolved); 847 + // Add to urls array so it can be used for card embedding later 848 + urls.push({ url: tco, expanded_url: resolved }); 849 + } 841 850 } 842 851 843 852 // 2. Media Handling
-63
src/test-link.ts
··· 1 - import 'dotenv/config'; 2 - import { TwitterClient } from '@steipete/bird/dist/lib/twitter-client.js'; 3 - 4 - // Provided credentials 5 - const AUTH_TOKEN = '30f5905989c9984bef1ca849910db8c84ae31c04'; 6 - const CT0 = '012bae351eeebe4a19df0a21c73d5705d887246beee09c96da2a5a6935495688d3479b8e3fa963433a43b3b37675c16f4a192d018dd388108a6d6aa3ed3d9ba3233238bc71830583a1613ec042d3ba55'; 7 - 8 - async function main() { 9 - console.log('🧪 Starting Link Extraction Test...'); 10 - 11 - const client = new TwitterClient({ 12 - cookies: { 13 - authToken: AUTH_TOKEN, 14 - ct0: CT0, 15 - }, 16 - }); 17 - 18 - const username = 'NVIDIANetworkng'; 19 - // Searching for the specific tweet ID: 2003547578848206861 20 - // Note: search usually doesn't find by ID directly unless we use specific operators or search from user 21 - // Let's try searching from user and look for the ID. 22 - 23 - console.log(`🔍 Fetching tweets for @${username}...`); 24 - 25 - try { 26 - const result = (await client.search(`from:${username}`, 20)) as any; 27 - 28 - if (!result.success || !result.tweets) { 29 - console.error('❌ Failed to fetch tweets:', result.error); 30 - return; 31 - } 32 - 33 - const targetId = '2003547578848206861'; 34 - const targetTweet = result.tweets.find((t: any) => (t.id_str || t.id) === targetId); 35 - 36 - if (targetTweet) { 37 - console.log('✅ Found target tweet!'); 38 - console.log('--- RAW TWEET OBJECT ---'); 39 - console.log(JSON.stringify(targetTweet, null, 2)); 40 - console.log('------------------------'); 41 - 42 - console.log('--- Entities ---'); 43 - console.log(JSON.stringify(targetTweet.entities, null, 2)); 44 - 45 - if (targetTweet.card) { 46 - console.log('--- Card Data (Bird internal?) ---'); 47 - console.log(JSON.stringify(targetTweet.card, null, 2)); 48 - } 49 - } else { 50 - console.log(`❌ Target tweet ${targetId} not found in last 20 results.`); 51 - // Just print the first one to see structure anyway 52 - if (result.tweets.length > 0) { 53 - console.log('Printing first tweet for structure analysis:'); 54 - console.log(JSON.stringify(result.tweets[0], null, 2)); 55 - } 56 - } 57 - 58 - } catch (err) { 59 - console.error('❌ Error:', err); 60 - } 61 - } 62 - 63 - main();