diff --git a/public/index.html b/public/index.html
index f8bc926..69d77ef 100644
--- a/public/index.html
+++ b/public/index.html
@@ -360,6 +360,19 @@
}
};
+ const deleteAllPosts = async (id) => {
+ if (!confirm('DANGER: This will delete ALL posts on the associated Bluesky account. Are you absolutely sure?')) return;
+ const confirmName = prompt('Type "DELETE" to confirm:');
+ if (confirmName !== 'DELETE') return;
+
+ try {
+ const res = await axios.post(`/api/mappings/${id}/delete-all-posts`, {}, { headers: { Authorization: `Bearer ${token}` } });
+ alert(res.data.message);
+ } catch (err) {
+ alert('Failed to delete posts: ' + (err.response?.data?.error || err.message));
+ }
+ };
+
const updateTwitter = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
@@ -603,6 +616,7 @@
+
>
)}
diff --git a/src/bsky.ts b/src/bsky.ts
new file mode 100644
index 0000000..418d955
--- /dev/null
+++ b/src/bsky.ts
@@ -0,0 +1,81 @@
+import { BskyAgent } from '@atproto/api';
+import { getConfig } from './config-manager.js';
+
+const activeAgents = new Map();
+
+export async function getAgent(mapping: {
+ bskyIdentifier: string;
+ bskyPassword: string;
+ bskyServiceUrl?: string;
+}): Promise {
+ const serviceUrl = mapping.bskyServiceUrl || 'https://bsky.social';
+ const cacheKey = `${mapping.bskyIdentifier}-${serviceUrl}`;
+ const existing = activeAgents.get(cacheKey);
+ if (existing) return existing;
+
+ const agent = new BskyAgent({ service: serviceUrl });
+ try {
+ await agent.login({ identifier: mapping.bskyIdentifier, password: mapping.bskyPassword });
+ activeAgents.set(cacheKey, agent);
+ return agent;
+ } catch (err) {
+ console.error(`Failed to login to Bluesky for ${mapping.bskyIdentifier} on ${serviceUrl}:`, err);
+ return null;
+ }
+}
+
+export async function deleteAllPosts(mappingId: string): Promise {
+ const config = getConfig();
+ const mapping = config.mappings.find(m => m.id === mappingId);
+ if (!mapping) throw new Error('Mapping not found');
+
+ const agent = await getAgent(mapping);
+ if (!agent) throw new Error('Failed to authenticate with Bluesky');
+
+ let cursor: string | undefined;
+ let deletedCount = 0;
+
+ console.log(`[${mapping.bskyIdentifier}] 🗑️ Starting deletion of all posts...`);
+
+ // Safety loop limit to prevent infinite loops
+ let loops = 0;
+ while (loops < 1000) {
+ loops++;
+ try {
+ const { data } = await agent.com.atproto.repo.listRecords({
+ repo: agent.session!.did,
+ collection: 'app.bsky.feed.post',
+ limit: 50, // Keep batch size reasonable
+ cursor,
+ });
+
+ if (data.records.length === 0) break;
+
+ console.log(`[${mapping.bskyIdentifier}] 🗑️ Deleting batch of ${data.records.length} posts...`);
+
+ // Use p-limit like approach or just Promise.all since 50 is manageable
+ await Promise.all(data.records.map(r =>
+ agent.com.atproto.repo.deleteRecord({
+ repo: agent.session!.did,
+ collection: 'app.bsky.feed.post',
+ rkey: r.uri.split('/').pop()!,
+ }).catch(e => console.warn(`Failed to delete record ${r.uri}:`, e))
+ ));
+
+ deletedCount += data.records.length;
+ cursor = data.cursor;
+
+ if (!cursor) break;
+
+ // Small delay to be nice to the server
+ await new Promise(r => setTimeout(r, 500));
+
+ } catch (err) {
+ console.error(`[${mapping.bskyIdentifier}] ❌ Error during deletion loop:`, err);
+ throw err;
+ }
+ }
+
+ console.log(`[${mapping.bskyIdentifier}] ✅ Deleted ${deletedCount} posts.`);
+ return deletedCount;
+}
diff --git a/src/db.ts b/src/db.ts
index c78ef38..f5314c1 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -233,6 +233,11 @@ export const dbService = {
stmt.run(username.toLowerCase());
},
+ deleteTweetsByBskyIdentifier(bskyIdentifier: string) {
+ const stmt = db.prepare('DELETE FROM processed_tweets WHERE bsky_identifier = ?');
+ stmt.run(bskyIdentifier.toLowerCase());
+ },
+
repairUnknownIdentifiers(twitterUsername: string, bskyIdentifier: string) {
const stmt = db.prepare(
'UPDATE processed_tweets SET bsky_identifier = ? WHERE bsky_identifier = "unknown" AND twitter_username = ?',
diff --git a/src/index.ts b/src/index.ts
index a963926..abe0137 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1369,30 +1369,7 @@ async function processTweets(
}
}
-const activeAgents = new Map();
-
-async function getAgent(mapping: {
- bskyIdentifier: string;
- bskyPassword: string;
- bskyServiceUrl?: string;
-}): Promise {
- const serviceUrl = mapping.bskyServiceUrl || 'https://bsky.social';
- const cacheKey = `${mapping.bskyIdentifier}-${serviceUrl}`;
- const existing = activeAgents.get(cacheKey);
- if (existing) return existing;
-
- const agent = new BskyAgent({ service: serviceUrl });
- try {
- await agent.login({ identifier: mapping.bskyIdentifier, password: mapping.bskyPassword });
- activeAgents.set(cacheKey, agent);
- return agent;
- } catch (err) {
- console.error(`Failed to login to Bluesky for ${mapping.bskyIdentifier} on ${serviceUrl}:`, err);
- return null;
- }
-}
-
-
+import { getAgent } from './bsky.js';
async function importHistory(twitterUsername: string, bskyIdentifier: string, limit = 15, dryRun = false, ignoreCancellation = false): Promise {
const config = getConfig();
diff --git a/src/server.ts b/src/server.ts
index dede06b..34504ad 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -4,6 +4,7 @@ import bcrypt from 'bcryptjs';
import cors from 'cors';
import express from 'express';
import jwt from 'jsonwebtoken';
+import { deleteAllPosts } from './bsky.js';
import { getConfig, saveConfig } from './config-manager.js';
import { dbService } from './db.js';
@@ -203,6 +204,31 @@ app.delete('/api/mappings/:id/cache', authenticateToken, requireAdmin, (req, res
res.json({ success: true, message: 'Cache cleared for all associated accounts' });
});
+app.post('/api/mappings/:id/delete-all-posts', authenticateToken, requireAdmin, async (req, res) => {
+ const { id } = req.params;
+ const config = getConfig();
+ const mapping = config.mappings.find((m) => m.id === id);
+ if (!mapping) {
+ res.status(404).json({ error: 'Mapping not found' });
+ return;
+ }
+
+ try {
+ const deletedCount = await deleteAllPosts(id);
+
+ // Clear local cache to stay in sync
+ dbService.deleteTweetsByBskyIdentifier(mapping.bskyIdentifier);
+
+ res.json({
+ success: true,
+ message: `Deleted ${deletedCount} posts from ${mapping.bskyIdentifier} and cleared local cache.`
+ });
+ } catch (err) {
+ console.error('Failed to delete all posts:', err);
+ res.status(500).json({ error: (err as Error).message });
+ }
+});
+
// --- Twitter Config Routes (Admin Only) ---
app.get('/api/twitter-config', authenticateToken, requireAdmin, (_req, res) => {