feat(admin): add bulk delete user messages tool (#2472)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-05 22:20:14 +02:00
1 parent 12718eabbc
commit fed8b2d089
12 files changed
+242 -5

No files matched your search

@@ -5,6 +5,7 @@ import {
BulkAddGuildMembersRequest,
BulkUpdateGuildFeaturesRequest,
} from '@fluxer/schema/src/domains/admin/AdminGuildSchemas';
import {BulkDeleteUserMessagesRequest} from '@fluxer/schema/src/domains/admin/AdminMessageSchemas';
import {BulkJobResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import {
BulkScheduleUserDeletionRequest,
@@ -185,4 +186,35 @@ export function BulkAdminController(app: HonoApp) {
return ctx.json({job_id: jobId.toString()});
},
);
app.post(
'/admin/bulk/delete-user-messages',
RateLimitMiddleware(RateLimitConfigs.ADMIN_BULK_OPERATION),
requireAdminACL(AdminACLs.BULK_DELETE_USER_MESSAGES),
Validator('json', BulkDeleteUserMessagesRequest),
OpenAPI({
operationId: 'bulk_delete_user_messages',
summary: 'Bulk delete user messages',
description:
'Enqueue a background job that deletes every message authored by each of the given users across all channels. Returns a job_id immediately; observe progress at /admin/jobs/:job_id.',
responseSchema: BulkJobResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
}),
async (ctx) => {
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const body = ctx.req.valid('json');
const jobId = await getWorkerService().addJob(
'bulkDeleteMessagesForUsers',
{
user_ids: body.user_ids.map((id) => id.toString()),
admin_user_id: adminUserId.toString(),
audit_log_reason: auditLogReason,
},
{requestedByUserId: adminUserId, ...(auditLogReason && {auditLogReason})},
);
return ctx.json({job_id: jobId.toString()});
},
);
}
@@ -56,6 +56,7 @@ const LANE_CONFIG = {
'bulkUpdateGuildFeatures',
'bulkAddGuildMembers',
'bulkBanFileShas',
'bulkDeleteMessagesForUsers',
] as const,
retiredTasks: ['sendScheduledMessage'],
concurrency: 8,
@@ -4,6 +4,7 @@ import type {WorkerTaskHandler} from '@pkgs/worker/src/contracts/WorkerTask';
import applicationProcessDeletion from './tasks/ApplicationProcessDeletion';
import bulkAddGuildMembers from './tasks/admin_bulk/BulkAddGuildMembers';
import bulkBanFileShas from './tasks/admin_bulk/BulkBanFileShas';
import bulkDeleteMessagesForUsers from './tasks/admin_bulk/BulkDeleteMessagesForUsers';
import bulkScheduleUserDeletion from './tasks/admin_bulk/BulkScheduleUserDeletion';
import bulkUpdateGuildFeatures from './tasks/admin_bulk/BulkUpdateGuildFeatures';
import bulkUpdateSuspiciousActivityFlags from './tasks/admin_bulk/BulkUpdateSuspiciousActivityFlags';
@@ -49,6 +50,7 @@ export const workerTasks: Record<WorkerTaskName, WorkerTaskHandler> = {
batchGuildAuditLogMessageDeletes,
bulkAddGuildMembers: bulkAddGuildMembers,
bulkBanFileShas: bulkBanFileShas,
bulkDeleteMessagesForUsers: bulkDeleteMessagesForUsers,
bulkDeleteSelfMessagesImmediate,
bulkDeleteUserMessages,
bulkDeleteUserMessagesScoped,
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {WorkerTaskHandler} from '@pkgs/worker/src/contracts/WorkerTask';
import {JobCancelledError} from '@pkgs/worker/src/contracts/WorkerTask';
import {AdminAuditService} from '../../../admin/services/AdminAuditService';
import {createUserID} from '../../../BrandedTypes';
import {UserMessageDeletionService} from '../../../channel/services/message/UserMessageDeletionService';
import {getWorkerDependencies} from '../../WorkerContext';
interface Payload {
user_ids: Array<string>;
admin_user_id: string;
audit_log_reason: string | null;
}
const handler: WorkerTaskHandler = async (rawPayload, helpers) => {
const payload: Payload = {
user_ids: rawPayload.user_ids as Array<string>,
admin_user_id: rawPayload.admin_user_id as string,
audit_log_reason: (rawPayload.audit_log_reason as string | null) ?? null,
};
const deps = getWorkerDependencies();
const auditService = new AdminAuditService(deps.adminRepository, deps.snowflakeService);
const deletionService = new UserMessageDeletionService({
channelRepository: deps.channelRepository,
gatewayService: deps.gatewayService,
storageService: deps.storageService,
purgeQueue: deps.purgeQueue,
});
const adminUserId = createUserID(BigInt(payload.admin_user_id));
const total = payload.user_ids.length;
const successful: Array<string> = [];
const failed: Array<{
id: string;
error: string;
}> = [];
let deletedMessages = 0;
await helpers.setContextLink(`/users?ids=${payload.user_ids.slice(0, 50).join(',')}`);
await helpers.reportProgress(0, total, `Deleting all messages from ${total} users`);
for (let i = 0; i < payload.user_ids.length; i++) {
if (await helpers.shouldCancel()) throw new JobCancelledError();
const rawUserId = payload.user_ids[i]!;
try {
const userId = createUserID(BigInt(rawUserId));
const deleted = await deletionService.deleteUserMessagesBulk(userId);
deletedMessages += deleted;
await auditService.createAuditLog({
adminUserId,
targetType: 'message_deletion',
targetId: BigInt(userId),
action: 'delete_all_user_messages',
auditLogReason: null,
metadata: new Map([['message_count', deleted.toString()]]),
});
successful.push(rawUserId);
} catch (err) {
failed.push({id: rawUserId, error: err instanceof Error ? err.message : String(err)});
}
await helpers.reportProgress(i + 1, total, `${deletedMessages} messages deleted`);
}
await auditService.createAuditLog({
adminUserId,
targetType: 'message_deletion',
targetId: BigInt(0),
action: 'bulk_delete_user_messages',
auditLogReason: payload.audit_log_reason,
metadata: new Map([
['user_count', total.toString()],
['message_count', deletedMessages.toString()],
['successful', successful.length.toString()],
['failed', failed.length.toString()],
]),
});
await helpers.reportProgress(total, total, `${deletedMessages} messages deleted, ${failed.length} users failed`);
helpers.logger.info(
{successful: successful.length, failed: failed.length, deletedMessages},
'bulkDeleteMessagesForUsers complete',
);
};
export default handler;