Something went wrong. Try again.
FlockOff is a mass-block tool for the Atmosphere flockoff.app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154import { restoreAgent } from '@/lib/oauth'import { requireSession } from '@/lib/session'import { requireListScopes } from '@/lib/scope-gate'import { getBlockEvent, updateBlockEvent, deleteBlock, getListSubscriptionRkey, unsubscribeFromList,} from '@/lib/lexicon'import { isEventJobRunning } from '@/lib/queue'import { sleep } from '@/lib/utils'
type ListAction = 'none' | 'unsubscribe' | 'delete'
export async function POST(request: Request) { let session: { did: string } try { session = await requireSession() } catch { return Response.json({ error: 'Not authenticated' }, { status: 401 }) }
let body: { eventRkey: string; listAction?: ListAction } try { body = await request.json() } catch { return Response.json({ error: 'Invalid request body' }, { status: 400 }) }
const { eventRkey, listAction = 'none' } = body if (!eventRkey) { return Response.json({ error: 'eventRkey is required' }, { status: 400 }) }
try { const agent = await restoreAgent(session.did) const { record } = await getBlockEvent(agent, session.did, eventRkey)
if (isEventJobRunning(eventRkey)) { return Response.json( { error: 'A block job is already running for this event.' }, { status: 409 } ) }
// Undoing blocks alone needs only base scopes; unsubscribing from or // deleting the shared list needs list scopes. Check before deleting any // blocks so the user isn't left with a half-completed combined action. if (record.listUri && listAction !== 'none') { const scopeGate = await requireListScopes(session.did) if (scopeGate) return scopeGate }
if (record.blockedRkeys.length === 0) { return Response.json({ removed: 0 }) }
let removed = 0 let failed = 0 const remainingRkeys: string[] = []
// Delete blocks with gentle pacing to respect rate limits for (const rkey of record.blockedRkeys) { let deleted = false try { await deleteBlock(agent, session.did, rkey) removed++ deleted = true } catch (err: unknown) { const status = (err as { status?: number })?.status if (status === 429) { // Back off on rate limit await sleep(30_000) try { await deleteBlock(agent, session.did, rkey) removed++ deleted = true } catch { failed++ } } else if (status === 400) { // Record doesn't exist (already deleted) — count as removed removed++ deleted = true } else { failed++ console.error(`[undo] Failed to delete block rkey ${rkey}:`, err) } }
if (!deleted) remainingRkeys.push(rkey)
// Keep this blocking route from taking minutes on large events. A full // queued/SSE undo would be better long-term; for now pace in small batches // and preserve failed rkeys so a later undo can retry them. if (removed > 0 && removed % 10 === 0) await sleep(1000) }
// Handle list action if a list was previously created let listCleared = false if (record.listUri && listAction !== 'none') { try { // Unsubscribe in both cases const subscriptionRkey = await getListSubscriptionRkey(agent, session.did, record.listUri) if (subscriptionRkey) { await unsubscribeFromList(agent, session.did, subscriptionRkey) }
if (listAction === 'delete') { const listRkey = record.listUri.split('/').pop() if (listRkey) { await agent.com.atproto.repo.deleteRecord({ repo: session.did, collection: 'app.bsky.graph.list', rkey: listRkey, }) } listCleared = true } } catch (err) { // Log but don't fail the whole undo — blocks are already removed console.error('[undo] List action failed:', err) } }
// Update the event record. blockedDids is intentionally preserved — it's // the only record of which accounts were blocked, and is required for re-do. // Only clear rkeys that were actually deleted; failed rkeys remain retryable. const fullyUndone = remainingRkeys.length === 0 await updateBlockEvent(agent, session.did, eventRkey, { ...record, blockedRkeys: remainingRkeys, failedDids: fullyUndone ? [] : record.failedDids, pendingDids: fullyUndone ? [] : record.pendingDids, undoneAt: fullyUndone ? new Date().toISOString() : record.undoneAt, // Clear listUri if the list was deleted so the link doesn't show ...(listCleared ? { listUri: undefined } : {}), stats: fullyUndone ? { total: record.stats?.total ?? 0, completed: 0, failed: 0 } : { total: record.stats?.total ?? record.blockedRkeys.length, completed: remainingRkeys.length, failed, }, })
return Response.json({ removed, failed, remaining: remainingRkeys.length, listAction }) } catch (err) { console.error('[api/blocks/undo]', err) return Response.json({ error: 'Failed to undo blocks' }, { status: 500 }) }}