Files
fluxer/fluxer_api/src/api/admin/controllers/MessageAdminController.ts
T

288 lines
10 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
import {
AdminChannelMessageListQuery,
AdminMessageSearchQuery,
AdminMessageSearchResponse,
BrowseChannelResponse,
} from '@fluxer/schema/src/domains/admin/AdminMessageBrowseSchemas';
import {
AdminMessageDetailQuery,
AdminUserMessageDeleteQuery,
AdminUserMessageShredRequest,
DeleteAllUserMessagesResponse,
MessageShredJobIdParam,
MessageShredResponse,
ReportAttachmentToNcmecRequest,
} from '@fluxer/schema/src/domains/admin/AdminMessageSchemas';
import {
DeleteMessageResponse,
LookupMessageResponse,
MessageShredStatusResponse,
NcmecAttachmentSubmitResultResponse,
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import {
ChannelIdMessageIdParam,
ChannelIdParam,
UserIdParam,
} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
import {createAttachmentID, createChannelID, createMessageID, createReportID} from '../../BrandedTypes';
import {requireAdminACL} from '../../middleware/AdminMiddleware';
import {RateLimitMiddleware} from '../../middleware/RateLimitMiddleware';
import {OpenAPI} from '../../middleware/ResponseTypeMiddleware';
import {RateLimitConfigs} from '../../RateLimitConfig';
import type {HonoApp} from '../../types/HonoEnv';
import {Validator} from '../../Validator';
export function MessageAdminController(app: HonoApp) {
app.get(
'/admin/messages',
RateLimitMiddleware(RateLimitConfigs.ADMIN_MESSAGE_OPERATION),
requireAdminACL(AdminACLs.MESSAGE_LOOKUP),
Validator('query', AdminMessageSearchQuery),
OpenAPI({
operationId: 'search_admin_messages',
summary: 'Search messages',
description:
'Searches the messages of a channel by content, or resolves a single message by its ID or by one of its attachments. Passing message_id returns that message with the messages surrounding it; passing attachment_id together with filename returns the message carrying that attachment with its surrounding context. Requires MESSAGE_LOOKUP permission.',
responseSchema: AdminMessageSearchResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const query = ctx.req.valid('query');
if (query.message_id != null) {
return ctx.json(
await adminService.messageService.lookupMessage({
channel_id: query.channel_id,
message_id: query.message_id,
context_limit: query.context_limit,
}),
);
}
if (query.attachment_id != null) {
if (query.filename == null) {
throw InputValidationError.fromCode('filename', ValidationErrorCodes.INVALID_FORMAT);
}
return ctx.json(
await adminService.messageService.lookupMessageByAttachment({
channel_id: query.channel_id,
attachment_id: query.attachment_id,
filename: query.filename,
context_limit: query.context_limit,
}),
);
}
return ctx.json(
await adminService.messageService.searchChannelMessages({
channel_id: query.channel_id,
query: query.q ?? '',
limit: query.limit,
}),
);
},
);
app.post(
'/admin/messages/ncmec-reports',
RateLimitMiddleware(RateLimitConfigs.ADMIN_MESSAGE_OPERATION),
requireAdminACL(AdminACLs.CSAM_SUBMIT_NCMEC),
requireAdminACL(AdminACLs.MESSAGE_DELETE),
requireAdminACL(AdminACLs.USER_DELETE),
requireAdminACL(AdminACLs.ARCHIVE_TRIGGER_USER),
Validator('json', ReportAttachmentToNcmecRequest),
OpenAPI({
operationId: 'create_admin_ncmec_report',
summary: 'Report an attachment to NCMEC',
description:
'Submits a specific image attachment to NCMEC, creates an audit log entry, silently disables the user, triggers one archive for the user, and schedules content deletion after the archive completes.',
responseSchema: NcmecAttachmentSubmitResultResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
}),
async (ctx) => {
const service = ctx.get('ncmecSubmissionService');
const adminUserId = ctx.get('adminUserId');
const body = ctx.req.valid('json');
const result = await service.submitAttachmentToNcmec({
channelId: createChannelID(body.channel_id),
messageId: createMessageID(body.message_id),
attachmentId: createAttachmentID(body.attachment_id),
filename: body.filename,
reporterFullName: body.reporter_full_name,
adminUserId,
sourceReportId: body.source_report_id ? createReportID(body.source_report_id) : null,
});
return ctx.json(result);
},
);
app.get(
'/admin/messages/shreds/:job_id',
RateLimitMiddleware(RateLimitConfigs.ADMIN_MESSAGE_OPERATION),
requireAdminACL(AdminACLs.MESSAGE_SHRED),
Validator('param', MessageShredJobIdParam),
OpenAPI({
operationId: 'get_admin_message_shred',
summary: 'Get message shred job',
description:
'Returns the progress of a queued message shred job, including whether it is complete. Requires MESSAGE_SHRED permission.',
responseSchema: MessageShredStatusResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const {job_id} = ctx.req.valid('param');
return ctx.json(await adminService.messageShredService.getMessageShredStatus(job_id.toString()));
},
);
app.get(
'/admin/channels/:channel_id/messages',
RateLimitMiddleware(RateLimitConfigs.ADMIN_MESSAGE_OPERATION),
requireAdminACL(AdminACLs.MESSAGE_LOOKUP),
Validator('param', ChannelIdParam),
Validator('query', AdminChannelMessageListQuery),
OpenAPI({
operationId: 'list_admin_channel_messages',
summary: 'List channel messages',
description:
'Pages through the messages of a channel, newest first, with cursor-based pagination. Requires MESSAGE_LOOKUP permission.',
responseSchema: BrowseChannelResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const {channel_id} = ctx.req.valid('param');
const {limit, before, after} = ctx.req.valid('query');
return ctx.json(
await adminService.messageService.browseChannel({
channel_id,
before,
after,
limit,
}),
);
},
);
app.get(
'/admin/channels/:channel_id/messages/:message_id',
RateLimitMiddleware(RateLimitConfigs.ADMIN_MESSAGE_OPERATION),
requireAdminACL(AdminACLs.MESSAGE_LOOKUP),
Validator('param', ChannelIdMessageIdParam),
Validator('query', AdminMessageDetailQuery),
OpenAPI({
operationId: 'get_admin_message',
summary: 'Get message',
description:
'Retrieves complete message details including content, attachments, edits, and metadata, together with the messages surrounding it. Requires MESSAGE_LOOKUP permission.',
responseSchema: LookupMessageResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const {channel_id, message_id} = ctx.req.valid('param');
const {context_limit} = ctx.req.valid('query');
return ctx.json(
await adminService.messageService.lookupMessage({
channel_id,
message_id,
context_limit,
}),
);
},
);
app.delete(
'/admin/channels/:channel_id/messages/:message_id',
RateLimitMiddleware(RateLimitConfigs.ADMIN_MESSAGE_OPERATION),
requireAdminACL(AdminACLs.MESSAGE_DELETE),
Validator('param', ChannelIdMessageIdParam),
OpenAPI({
operationId: 'delete_admin_message',
summary: 'Delete message',
description:
'Deletes a single message permanently and purges its attachments. Used for removing inappropriate or harmful content. Logged to audit log. Requires MESSAGE_DELETE permission.',
responseSchema: DeleteMessageResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const {channel_id, message_id} = ctx.req.valid('param');
return ctx.json(
await adminService.messageService.deleteMessage({channel_id, message_id}, adminUserId, auditLogReason),
);
},
);
app.post(
'/admin/users/:user_id/message-shreds',
RateLimitMiddleware(RateLimitConfigs.ADMIN_MESSAGE_OPERATION),
requireAdminACL(AdminACLs.MESSAGE_SHRED),
Validator('param', UserIdParam),
Validator('json', AdminUserMessageShredRequest),
OpenAPI({
operationId: 'shred_admin_user_messages',
summary: 'Shred user messages',
description:
'Queues bulk shredding of the given messages of a user, with attachment deletion. Returns a job ID to track progress asynchronously. Used for large-scale content removal. Requires MESSAGE_SHRED permission.',
responseSchema: MessageShredResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const {user_id} = ctx.req.valid('param');
const {entries} = ctx.req.valid('json');
return ctx.json(
await adminService.messageShredService.queueMessageShred({user_id, entries}, adminUserId, auditLogReason),
);
},
);
app.delete(
'/admin/users/:user_id/messages',
RateLimitMiddleware(RateLimitConfigs.ADMIN_MESSAGE_OPERATION),
requireAdminACL(AdminACLs.MESSAGE_DELETE_ALL),
Validator('param', UserIdParam),
Validator('query', AdminUserMessageDeleteQuery),
OpenAPI({
operationId: 'delete_admin_user_messages',
summary: 'Delete all user messages',
description:
'Deletes all messages from a specific user across all channels. Permanent operation used for account suspension or policy violation. Pass dry_run=false to delete; the default counts without deleting. Requires MESSAGE_DELETE_ALL permission.',
responseSchema: DeleteAllUserMessagesResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const {user_id} = ctx.req.valid('param');
const {dry_run} = ctx.req.valid('query');
return ctx.json(
await adminService.messageDeletionService.deleteAllUserMessages(
{user_id, dry_run},
adminUserId,
auditLogReason,
),
);
},
);
}