import { requireSession } from '@/lib/session' import { restoreAgent } from '@/lib/oauth' import { requireListScopes } from '@/lib/scope-gate' import { getBlockEvent, updateBlockEvent } from '@/lib/lexicon' import { canStartJobForDid, MAX_ACTIVE_JOBS_PER_USER, startAppendJob, isEventJobRunning } from '@/lib/queue' import { generateId } from '@/lib/utils' // POST /api/blocks/append // Blocks additional accounts and appends them to an existing block event record. // If the event has a listUri, newly blocked accounts are also added to that list. 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; selectedDids: string[] } try { body = await request.json() } catch { return Response.json({ error: 'Invalid request body' }, { status: 400 }) } const { eventRkey, selectedDids } = body if (!eventRkey || !selectedDids?.length) { return Response.json({ error: 'eventRkey and selectedDids are required' }, { status: 400 }) } try { const agent = await restoreAgent(session.did) const { record } = await getBlockEvent(agent, session.did, eventRkey) if (record.pendingDids?.length) { return Response.json( { error: 'This event has pending accounts. Resume it before appending new accounts.' }, { status: 409 } ) } // Safety: filter out any DIDs already processed to avoid duplicate blocks const alreadyProcessed = new Set([ ...(record.blockedDids ?? []), ...(record.failedDids ?? []), ...(record.pendingDids ?? []), ]) const didsToBlock = selectedDids.filter((did) => !alreadyProcessed.has(did)) if (didsToBlock.length === 0) { return Response.json({ error: 'All selected accounts are already processed' }, { status: 400 }) } if (isEventJobRunning(eventRkey)) { return Response.json( { error: 'A block job is already running for this event. Wait for it to finish before starting another.' }, { status: 409 } ) } if (!canStartJobForDid(session.did)) { return Response.json( { error: `Too many active block jobs. Wait for one to finish before starting another. Limit: ${MAX_ACTIVE_JOBS_PER_USER}.` }, { status: 429 } ) } // The job writes listitem records when the event has a shared list — check // scopes before persisting anything so the job can't fail silently mid-run. // Checked after the job-state gates so those errors take precedence. if (record.listUri) { const scopeGate = await requireListScopes(session.did) if (scopeGate) return scopeGate } await updateBlockEvent(agent, session.did, eventRkey, { ...record, pendingDids: [...(record.pendingDids ?? []), ...didsToBlock], stats: { total: (record.blockedDids ?? []).length + (record.failedDids ?? []).length + didsToBlock.length, completed: (record.blockedDids ?? []).length, failed: (record.failedDids ?? []).length, }, }) const jobId = generateId() await startAppendJob( jobId, session.did, eventRkey, didsToBlock, { blockedDids: record.blockedDids ?? [], blockedRkeys: record.blockedRkeys ?? [], failedDids: record.failedDids ?? [], }, record.listUri // pass through so the job also adds listitems if a list exists ) return Response.json({ jobId, eventRkey, total: didsToBlock.length }) } catch (err) { console.error('[api/blocks/append]', err) return Response.json({ error: 'Failed to start append job' }, { status: 500 }) } }