chore(moderation): remove the built-in NCMEC integration (#3332)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-09 01:39:04 +02:00
1 parent f390e610b9
commit 454fefdb92
146 files changed
+234 -4075

No files matched your search

+1 -7
View File
@@ -31,11 +31,7 @@ import type {UserCacheService} from '@app/api/infrastructure/UserCacheService';
import type {InviteRepository} from '@app/api/invite/InviteRepository';
import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository';
import {JobAdminService} from '@app/api/jobs/JobAdminService';
import {
getGuildDiscoveryRepository,
getKVAccountDeletionQueue,
getNcmecSubmissionService,
} from '@app/api/middleware/ServiceSingletons';
import {getGuildDiscoveryRepository, getKVAccountDeletionQueue} from '@app/api/middleware/ServiceSingletons';
import type {IApplicationRepository} from '@app/api/oauth/repositories/IApplicationRepository';
import type {ReportService} from '@app/api/report/ReportService';
import type {StoreEntitlementService} from '@app/api/store_billing/StoreEntitlementService';
@@ -140,7 +136,6 @@ export class AdminService {
channelRepository: this.channelRepository,
guildRepository: this.guildRepository,
auditService: this.auditService,
ncmecSubmissionService: getNcmecSubmissionService(),
});
this.messageShredService = new AdminMessageShredService({
apiContext: this.apiContext,
@@ -159,7 +154,6 @@ export class AdminService {
storageService: this.storageService,
auditService: this.auditService,
userCacheService: this.userCacheService,
ncmecSubmissionService: getNcmecSubmissionService(),
reporterResolutionNotifier,
});
this.voiceService = new AdminVoiceService({
@@ -2,7 +2,6 @@
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
import {recordAdminRead} from '@app/api/admin/AdminAuditRecorder';
import {createAttachmentID, createChannelID, createMessageID, createReportID} from '@app/api/BrandedTypes';
import {requireAdminACL} from '@app/api/middleware/AdminMiddleware';
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware';
@@ -25,13 +24,11 @@ import {
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,
@@ -124,40 +121,6 @@ export function MessageAdminController(app: HonoApp) {
return ctx.json(response);
},
);
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),
@@ -26,7 +26,6 @@ import {
messageResponseAccessForGuild,
} from '@app/api/channel/services/message/MessageResponseDataService';
import {resolveNsfwScopeChannel} from '@app/api/channel/utils/ThreadNsfwScope';
import type {NcmecAttachmentStatusResponse, NcmecSubmissionService} from '@app/api/csam/NcmecSubmissionService';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import {getPurgeQueue, getStorageService} from '@app/api/middleware/ServiceSingletons';
import {getMessageSearchService} from '@app/api/SearchFactory';
@@ -49,7 +48,6 @@ interface AdminMessageServiceDeps {
channelRepository: IChannelRepository;
guildRepository: IGuildRepositoryAggregate;
auditService: AdminAuditService;
ncmecSubmissionService: NcmecSubmissionService;
}
interface ChannelNsfwContext {
@@ -92,10 +90,8 @@ export class AdminMessageService {
around: messageId,
})
).reverse();
const attachmentStatuses = await this.getAttachmentStatusesForMessages(messageResponses);
const priorReports = await this.getPriorReportsForMessages(messageResponses);
const adminMessages = messageResponses.map((message) =>
this.mapMessageResponseToAdminMessage(message, channelNsfwContext, attachmentStatuses, priorReports),
this.mapMessageResponseToAdminMessage(message, channelNsfwContext),
);
return {
messages: adminMessages,
@@ -207,10 +203,8 @@ export class AdminMessageService {
after: afterId,
});
const messageResponses = afterId ? messages : [...messages].reverse();
const attachmentStatuses = await this.getAttachmentStatusesForMessages(messageResponses);
const priorReports = await this.getPriorReportsForMessages(messageResponses);
const adminMessages = messageResponses.map((message) =>
this.mapMessageResponseToAdminMessage(message, channelNsfwContext, attachmentStatuses, priorReports),
this.mapMessageResponseToAdminMessage(message, channelNsfwContext),
);
return {
messages: adminMessages,
@@ -241,10 +235,8 @@ export class AdminMessageService {
messages: result.messages,
access: await this.getMessageResponseAccessForAdmin(channelId),
});
const attachmentStatuses = await this.getAttachmentStatusesForMessages(messageResponses);
const priorReports = await this.getPriorReportsForMessages(messageResponses);
const adminMessages = messageResponses.map((message) =>
this.mapMessageResponseToAdminMessage(message, channelNsfwContext, attachmentStatuses, priorReports),
this.mapMessageResponseToAdminMessage(message, channelNsfwContext),
);
return {
messages: adminMessages,
@@ -253,15 +245,6 @@ export class AdminMessageService {
};
}
private async getAttachmentStatusesForMessages(
messages: Array<MessageResponse>,
): Promise<Map<string, NcmecAttachmentStatusResponse>> {
const attachmentIds = messages.flatMap((message) =>
(message.attachments ?? []).map((attachment) => createAttachmentID(BigInt(attachment.id))),
);
return this.deps.ncmecSubmissionService.getAttachmentStatuses(attachmentIds);
}
private async getMessageResponseAccessForAdmin(channelId: ChannelID): Promise<MessageResponseAccessContext> {
const channel = await this.deps.channelRepository.findUnique(channelId);
const access = channel ? messageResponseAccessForChannel(channel) : messageResponseAccessForGuild(null);
@@ -287,17 +270,7 @@ export class AdminMessageService {
});
}
private async getPriorReportsForMessages(messages: Array<MessageResponse>): Promise<Map<string, Array<string>>> {
const authorIds = messages.map((message) => createUserID(BigInt(message.author.id)));
return this.deps.ncmecSubmissionService.getUserPriorReportIds(authorIds);
}
private mapMessageResponseToAdminMessage(
message: MessageResponse,
channelNsfwContext: ChannelNsfwContext,
attachmentStatuses: Map<string, NcmecAttachmentStatusResponse>,
priorReports: Map<string, Array<string>>,
) {
private mapMessageResponseToAdminMessage(message: MessageResponse, channelNsfwContext: ChannelNsfwContext) {
return {
id: message.id,
channel_id: message.channel_id ?? '',
@@ -313,7 +286,6 @@ export class AdminMessageService {
author_avatar: message.author.avatar,
content: message.content ?? '',
timestamp: message.timestamp,
user_prior_ncmec_report_ids: priorReports.get(message.author.id) ?? [],
attachments:
message.attachments?.map((attachment) => ({
id: attachment.id,
@@ -324,9 +296,6 @@ export class AdminMessageService {
width: attachment.width ?? null,
height: attachment.height ?? null,
size: attachment.size == null ? null : assertSafeByteSize(attachment.size, 'admin message attachment size'),
ncmec_status: attachmentStatuses.get(attachment.id)?.status ?? 'not_submitted',
ncmec_report_id: attachmentStatuses.get(attachment.id)?.ncmec_report_id ?? null,
ncmec_failure_reason: attachmentStatuses.get(attachment.id)?.failure_reason ?? null,
})) ?? [],
};
}
@@ -24,7 +24,6 @@ import {
messageResponseAccessForGuild,
} from '@app/api/channel/services/message/MessageResponseDataService';
import {resolveNsfwScopeChannel} from '@app/api/channel/utils/ThreadNsfwScope';
import type {NcmecAttachmentStatusResponse, NcmecSubmissionService} from '@app/api/csam/NcmecSubmissionService';
import type {MessageAttachment} from '@app/api/database/types/MessageTypes';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {IStorageService} from '@app/api/infrastructure/IStorageService';
@@ -63,7 +62,6 @@ interface AdminReportServiceDeps {
storageService: IStorageService;
auditService: AdminAuditService;
userCacheService: UserCacheService;
ncmecSubmissionService: NcmecSubmissionService;
reporterResolutionNotifier: ReporterResolutionNotifier;
}
@@ -378,8 +376,6 @@ export class AdminReportService {
if (!includeContext) {
return baseResponse;
}
const attachmentStatusesById = await this.getAttachmentStatusesById(report);
const priorReportsByAuthor = await this.getPriorReportsForContext(report);
const authorBotFlags = await this.getContextAuthorBotFlags(report, requestCache);
const messageContext = (
await Promise.all(
@@ -389,8 +385,6 @@ export class AdminReportService {
report.reportedChannelId ?? null,
report.reportedGuildId ?? report.guildContextId ?? null,
reportLookupCache,
attachmentStatusesById,
priorReportsByAuthor,
authorBotFlags,
),
),
@@ -566,8 +560,6 @@ export class AdminReportService {
fallbackChannelId: ChannelID | null,
fallbackGuildId: GuildID | null,
reportLookupCache: ReportLookupCache,
attachmentStatusesById: Map<string, NcmecAttachmentStatusResponse>,
priorReportsByAuthor: Map<string, Array<string>>,
authorBotFlags: Map<string, boolean | null>,
) {
const channelId = message.channelId ?? fallbackChannelId;
@@ -581,9 +573,7 @@ export class AdminReportService {
message.attachments && message.attachments.length > 0
? (
await Promise.all(
message.attachments.map((attachment) =>
this.mapReportAttachmentToResponse(attachment, channelId, attachmentStatusesById),
),
message.attachments.map((attachment) => this.mapReportAttachmentToResponse(attachment, channelId)),
)
).filter(
(
@@ -597,9 +587,6 @@ export class AdminReportService {
width: number | null;
height: number | null;
size: number | null;
ncmec_status: string;
ncmec_report_id: string | null;
ncmec_failure_reason: string | null;
} => attachment !== null,
)
: [];
@@ -627,21 +614,9 @@ export class AdminReportService {
missing_attachments: message.missingAttachments
.filter((attachment) => attachment.attachment_id != null && attachment.filename)
.map(mapMissingAttachmentToResponse),
user_prior_ncmec_report_ids: message.authorId ? (priorReportsByAuthor.get(authorId) ?? []) : [],
};
}
private async getPriorReportsForContext(report: IARSubmission): Promise<Map<string, Array<string>>> {
const authorIds = new Set<string>();
for (const message of report.messageContext ?? []) {
if (message.authorId) authorIds.add(message.authorId.toString());
}
if (report.reportedUserId) authorIds.add(report.reportedUserId.toString());
if (authorIds.size === 0) return new Map();
const userIds = [...authorIds].map((value) => createUserID(BigInt(value)));
return this.deps.ncmecSubmissionService.getUserPriorReportIds(userIds);
}
private async getChannelNsfwState(
channelId: ChannelID | null,
reportLookupCache: ReportLookupCache,
@@ -700,7 +675,6 @@ export class AdminReportService {
private async mapReportAttachmentToResponse(
attachment: MessageAttachment,
channelId: ChannelID | null,
attachmentStatusesById: Map<string, NcmecAttachmentStatusResponse>,
): Promise<{
id: string;
filename: string;
@@ -710,9 +684,6 @@ export class AdminReportService {
width: number | null;
height: number | null;
size: number | null;
ncmec_status: string;
ncmec_report_id: string | null;
ncmec_failure_reason: string | null;
} | null> {
if (!attachment || attachment.attachment_id == null || !attachment.filename || !channelId) {
return null;
@@ -736,9 +707,6 @@ export class AdminReportService {
width: attachment.width ?? null,
height: attachment.height ?? null,
size: attachment.size != null ? assertSafeByteSize(attachment.size, 'admin report attachment size') : null,
ncmec_status: attachmentStatusesById.get(attachment.attachment_id.toString())?.status ?? 'not_submitted',
ncmec_report_id: attachmentStatusesById.get(attachment.attachment_id.toString())?.ncmec_report_id ?? null,
ncmec_failure_reason: attachmentStatusesById.get(attachment.attachment_id.toString())?.failure_reason ?? null,
};
} catch (error) {
Logger.error(
@@ -749,13 +717,6 @@ export class AdminReportService {
return null;
}
private async getAttachmentStatusesById(report: IARSubmission) {
const attachmentIds = (report.messageContext ?? []).flatMap((message) =>
message.attachments.map((attachment) => attachment.attachment_id),
);
return this.deps.ncmecSubmissionService.getAttachmentStatuses(attachmentIds);
}
private async buildUserTag(userId: UserID | null, requestCache: RequestCache): Promise<UserTagInfo | null> {
if (!userId) {
return null;
@@ -61,7 +61,6 @@ describe('Admin audit log access filter', () => {
expect(getAdminAuditAccess(action)).toBe('read');
}
expect(getAdminAuditAccess('update_flags')).toBe('write');
expect(getAdminAuditAccess('NCMEC Report')).toBe('write');
});
for (const search of ['disabled', 'enabled'] as const) {
@@ -116,47 +116,6 @@ export const MessageAdminAuditCases: ReadonlyArray<AdminAuditCoverageCase> = [
};
},
},
{
method: 'POST',
route: '/admin/messages/ncmec-reports',
auditLogReason: expect.stringMatching(/^NCMEC Report \S+ - \S+$/),
async prepare({harness}) {
const {author, channelId} = await createAuthorChannel(harness);
const message = await createMessageWithImageAttachment(harness, author.token, channelId);
const attachment = message.attachments[0];
return {
request: {
path: '/admin/messages/ncmec-reports',
body: {
channel_id: channelId,
message_id: message.id,
attachment_id: attachment.id,
filename: attachment.filename,
reporter_full_name: 'Audit Coverage Reporter',
confirmed_viewed: true,
},
},
expected: {
action: 'NCMEC Report',
targetType: 'user',
targetId: author.userId,
metadata: {
channel_id: channelId,
message_id: message.id,
attachment_id: attachment.id,
reported_user_email: author.email,
reported_user_email_verified: 'true',
reported_user_date_of_birth: '2000-01-01',
reported_user_last_active_ip: '127.0.0.1',
attachment_upload_mode: 'form_data',
attachment_upload_request_ip: '127.0.0.1',
attachment_upload_requested_at: expect.any(String),
attachment_upload_ip_source: 'attachment upload request handled directly by Fluxer API',
},
},
};
},
},
{
method: 'GET',
route: '/admin/messages/shreds/:job_id',