mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
feat(admin): add bulk delete user messages tool (#2472)
This commit is contained in:
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;
|
||||
Reference in new issue
Block a user