forked from
j4ck.xyz/tweets2bsky
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.
1import { BskyAgent } from '@atproto/api';
2import { getConfig } from './config-manager.js';
3
4const activeAgents = new Map<string, BskyAgent>();
5
6export async function getAgent(mapping: {
7 bskyIdentifier: string;
8 bskyPassword: string;
9 bskyServiceUrl?: string;
10}): Promise<BskyAgent | null> {
11 const serviceUrl = mapping.bskyServiceUrl || 'https://bsky.social';
12 const cacheKey = `${mapping.bskyIdentifier}-${serviceUrl}`;
13 const existing = activeAgents.get(cacheKey);
14 if (existing) return existing;
15
16 const agent = new BskyAgent({ service: serviceUrl });
17 try {
18 await agent.login({ identifier: mapping.bskyIdentifier, password: mapping.bskyPassword });
19 activeAgents.set(cacheKey, agent);
20 return agent;
21 } catch (err) {
22 console.error(`Failed to login to Bluesky for ${mapping.bskyIdentifier} on ${serviceUrl}:`, err);
23 return null;
24 }
25}
26
27export async function deleteAllPosts(mappingId: string): Promise<number> {
28 const config = getConfig();
29 const mapping = config.mappings.find(m => m.id === mappingId);
30 if (!mapping) throw new Error('Mapping not found');
31
32 const agent = await getAgent(mapping);
33 if (!agent) throw new Error('Failed to authenticate with Bluesky');
34
35 let cursor: string | undefined;
36 let deletedCount = 0;
37
38 console.log(`[${mapping.bskyIdentifier}] 🗑️ Starting deletion of all posts...`);
39
40 // Safety loop limit to prevent infinite loops
41 let loops = 0;
42 while (loops < 1000) {
43 loops++;
44 try {
45 const { data } = await agent.com.atproto.repo.listRecords({
46 repo: agent.session!.did,
47 collection: 'app.bsky.feed.post',
48 limit: 50, // Keep batch size reasonable
49 cursor,
50 });
51
52 if (data.records.length === 0) break;
53
54 console.log(`[${mapping.bskyIdentifier}] 🗑️ Deleting batch of ${data.records.length} posts...`);
55
56 // Use p-limit like approach or just Promise.all since 50 is manageable
57 await Promise.all(data.records.map(r =>
58 agent.com.atproto.repo.deleteRecord({
59 repo: agent.session!.did,
60 collection: 'app.bsky.feed.post',
61 rkey: r.uri.split('/').pop()!,
62 }).catch(e => console.warn(`Failed to delete record ${r.uri}:`, e))
63 ));
64
65 deletedCount += data.records.length;
66 cursor = data.cursor;
67
68 if (!cursor) break;
69
70 // Small delay to be nice to the server
71 await new Promise(r => setTimeout(r, 500));
72
73 } catch (err) {
74 console.error(`[${mapping.bskyIdentifier}] ❌ Error during deletion loop:`, err);
75 throw err;
76 }
77 }
78
79 console.log(`[${mapping.bskyIdentifier}] ✅ Deleted ${deletedCount} posts.`);
80 return deletedCount;
81}