mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
chore(moderation): remove the built-in NCMEC integration (#3332)
This commit is contained in:
1 parent
f390e610b9
commit
454fefdb92
146 files changed
+234
-4075
No files matched your search
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user