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: correct PDS resolution for video upload and refine fallback links

jack 1212f5e6 500e78bc

+23 -9
+23 -9
src/index.ts
··· 267 268 try { 269 // 1. Get Service Auth 270 - // The audience (aud) must be the user's PDS DID (did:web:HOST) 271 - const pdsUrl = new URL(agent.service as any); 272 - const pdsHost = pdsUrl.host; 273 console.log(`[VIDEO] ๐ŸŒ PDS Host detected: ${pdsHost}`); 274 console.log(`[VIDEO] ๐Ÿ”‘ Requesting service auth token for audience: did:web:${pdsHost}...`); 275 ··· 317 const statusUrl = new URL("https://video.bsky.app/xrpc/app.bsky.video.getJobStatus"); 318 statusUrl.searchParams.append("jobId", jobStatus.jobId); 319 320 - // Using a plain fetch for the status check as it doesn't always need auth 321 const statusResponse = await fetch(statusUrl); 322 if (!statusResponse.ok) { 323 console.warn(`[VIDEO] โš ๏ธ Job status fetch failed (${statusResponse.status}), retrying...`); ··· 565 566 console.warn(`[${twitterUsername}] โš ๏ธ Video too large (${(buffer.length / 1024 / 1024).toFixed(2)}MB). Fallback to link.`); 567 const tweetUrl = `https://twitter.com/${twitterUsername}/status/${tweetId}`; 568 - if (!text.includes(tweetUrl)) text += `\n\nOriginal Tweet: ${tweetUrl}`; 569 } catch (err) { 570 console.error(`[${twitterUsername}] โŒ Failed video upload flow:`, (err as Error).message); 571 const tweetUrl = `https://twitter.com/${twitterUsername}/status/${tweetId}`; 572 - if (!text.includes(tweetUrl)) text += `\n\nOriginal Tweet: ${tweetUrl}`; 573 } 574 } 575 } ··· 591 console.log(`[${twitterUsername}] ๐Ÿ”„ Found quoted tweet in local history.`); 592 quoteEmbed = { $type: 'app.bsky.embed.record', record: { uri: quoteRef.uri, cid: quoteRef.cid } }; 593 } else { 594 - // If it's NOT in our managed account history, it's external 595 const quoteUrlEntity = urls.find((u) => u.expanded_url?.includes(quoteId)); 596 - externalQuoteUrl = quoteUrlEntity?.expanded_url || `https://twitter.com/i/status/${quoteId}`; 597 - console.log(`[${twitterUsername}] ๐Ÿ”— Quoted tweet is external: ${externalQuoteUrl}`); 598 } 599 } 600
··· 267 268 try { 269 // 1. Get Service Auth 270 + // We need to resolve the actual PDS host for this DID 271 + console.log(`[VIDEO] ๐Ÿ” Resolving PDS host for DID: ${agent.session!.did}...`); 272 + const { data: repoDesc } = await agent.com.atproto.repo.describeRepo({ repo: agent.session!.did! }); 273 + 274 + // didDoc might be present in repoDesc 275 + const pdsService = (repoDesc as any).didDoc?.service?.find((s: any) => s.id === '#atproto_pds' || s.type === 'AtProtoPds'); 276 + const pdsUrl = pdsService?.serviceEndpoint; 277 + const pdsHost = pdsUrl ? new URL(pdsUrl).host : 'bsky.social'; 278 + 279 console.log(`[VIDEO] ๐ŸŒ PDS Host detected: ${pdsHost}`); 280 console.log(`[VIDEO] ๐Ÿ”‘ Requesting service auth token for audience: did:web:${pdsHost}...`); 281 ··· 323 const statusUrl = new URL("https://video.bsky.app/xrpc/app.bsky.video.getJobStatus"); 324 statusUrl.searchParams.append("jobId", jobStatus.jobId); 325 326 const statusResponse = await fetch(statusUrl); 327 if (!statusResponse.ok) { 328 console.warn(`[VIDEO] โš ๏ธ Job status fetch failed (${statusResponse.status}), retrying...`); ··· 570 571 console.warn(`[${twitterUsername}] โš ๏ธ Video too large (${(buffer.length / 1024 / 1024).toFixed(2)}MB). Fallback to link.`); 572 const tweetUrl = `https://twitter.com/${twitterUsername}/status/${tweetId}`; 573 + if (!text.includes(tweetUrl)) text += `\n\nVideo: ${tweetUrl}`; 574 } catch (err) { 575 console.error(`[${twitterUsername}] โŒ Failed video upload flow:`, (err as Error).message); 576 const tweetUrl = `https://twitter.com/${twitterUsername}/status/${tweetId}`; 577 + if (!text.includes(tweetUrl)) text += `\n\nVideo: ${tweetUrl}`; 578 } 579 } 580 } ··· 596 console.log(`[${twitterUsername}] ๐Ÿ”„ Found quoted tweet in local history.`); 597 quoteEmbed = { $type: 'app.bsky.embed.record', record: { uri: quoteRef.uri, cid: quoteRef.cid } }; 598 } else { 599 const quoteUrlEntity = urls.find((u) => u.expanded_url?.includes(quoteId)); 600 + const qUrl = quoteUrlEntity?.expanded_url || `https://twitter.com/i/status/${quoteId}`; 601 + 602 + // Check if it's a self-quote 603 + const isSelfQuote = qUrl.toLowerCase().includes(`twitter.com/${twitterUsername.toLowerCase()}/`) || 604 + qUrl.toLowerCase().includes(`x.com/${twitterUsername.toLowerCase()}/`); 605 + 606 + if (!isSelfQuote) { 607 + externalQuoteUrl = qUrl; 608 + console.log(`[${twitterUsername}] ๐Ÿ”— Quoted tweet is external: ${externalQuoteUrl}`); 609 + } else { 610 + console.log(`[${twitterUsername}] ๐Ÿ” Quoted tweet is a self-quote, skipping 'QT:' link.`); 611 + } 612 } 613 } 614