From 454fefdb926a69aa18379d6eb9851be04fafd79e Mon Sep 17 00:00:00 2001 From: Hampus Date: Fri, 9 Oct 2026 01:39:04 +0200 Subject: [PATCH] chore(moderation): remove the built-in NCMEC integration (#3332) --- config/env/development.env | 1 - deploy/self-hosting/.env.example | 8 - deploy/self-hosting/docker-compose.yml | 5 - fluxer_admin/openapi-admin.json | 172 +-- fluxer_admin/src/acl.rs | 41 - fluxer_admin/src/api/guilds.rs | 6 +- fluxer_admin/src/api/messages.rs | 34 +- fluxer_admin/src/api/types/messages.rs | 6 - fluxer_admin/src/routes/message_actions.rs | 48 +- fluxer_admin/src/routes/messages.rs | 2 - .../src/templates/components/message_data.rs | 22 +- .../src/templates/components/message_list.rs | 135 +-- .../src/templates/pages/messages_page.rs | 34 +- .../src/templates/pages/report_detail.rs | 128 +- fluxer_admin/tests/api_deserialization.rs | 41 +- fluxer_admin/tests/htmx_acceptance.rs | 98 +- .../fixtures/api/report_evidence_detail.json | 27 +- .../fixtures/api/report_webhook_detail.json | 27 +- fluxer_api/package.json | 1 - fluxer_api/src/api/Config.ts | 7 - fluxer_api/src/api/Tables.ts | 16 - fluxer_api/src/api/admin/AdminService.ts | 8 +- .../controllers/MessageAdminController.ts | 37 - .../api/admin/services/AdminMessageService.ts | 39 +- .../api/admin/services/AdminReportService.ts | 41 +- .../tests/AdminAuditAccessFilter.test.ts | 1 - .../audit_coverage/MessageAdminAuditCases.ts | 41 - .../AttachmentUploadTraceRepository.ts | 10 - .../channel/tests/CrosspostModeration.test.ts | 143 +-- .../tests/EmbedAttachmentPurge.test.ts | 48 +- fluxer_api/src/api/config/APIConfig.ts | 7 - fluxer_api/src/api/csam/NcmecReporter.ts | 498 -------- fluxer_api/src/api/csam/NcmecRepository.ts | 35 - .../src/api/csam/NcmecSubmissionService.ts | 1064 ----------------- .../src/api/csam/tests/NcmecReporter.test.ts | 202 ---- .../src/api/database/types/CsamTypes.ts | 75 -- .../tests/ChannelLoadGateCoverage.test.ts | 1 - .../src/api/middleware/ServiceMiddleware.ts | 5 - .../src/api/middleware/ServiceSingletons.ts | 37 - .../tests/LazyServiceProvider.test.ts | 1 - fluxer_api/src/api/openapi/openapi.json | 4 - .../tests/ReportWebhookMessages.test.ts | 62 +- fluxer_api/src/api/test/Setup.ts | 11 - .../test/fixtures/ncmec/NcmecXmlFixtures.ts | 28 - .../api/test/msw/handlers/NcmecHandlers.ts | 284 ----- fluxer_api/src/api/test/msw/server.ts | 2 - fluxer_api/src/api/types/HonoEnv.ts | 2 - .../src/api/utils/ExternalResponseLimits.ts | 1 - .../src/api/worker/WorkerDependencies.ts | 5 - fluxer_api/src/api/worker/WorkerLaneConfig.ts | 3 +- .../src/api/worker/WorkerTaskRegistry.ts | 2 - .../tasks/FinalizeNcmecAttachmentReport.ts | 26 - .../api/worker/tests/WorkerLaneConfig.test.ts | 2 +- .../src/content/docs/admin-api/api-keys.mdx | 4 +- .../src/content/docs/admin-api/index.mdx | 8 +- .../src/content/docs/admin-api/messages.mdx | 105 +- .../src/content/docs/admin-api/reports.mdx | 58 +- .../src/content/docs/admin-api/users.mdx | 9 +- .../src/content/docs/gateway/events.md | 2 +- .../src/content/docs/http-api/errors.md | 12 - .../content/docs/operator/configuration.mdx | 32 +- .../docs/topics/announcement-channels.md | 2 +- packages/config/src/ConfigLoader.ts | 6 - packages/config/src/MasterConfig.ts | 7 - .../src/config_loader/EnvironmentOverrides.ts | 5 - packages/constants/src/AdminACLs.ts | 1 - packages/constants/src/ApiErrorCodes.ts | 2 - packages/constants/src/ReportCategories.ts | 3 - .../constants/src/ValidationErrorCodes.ts | 1 - .../moderation/NcmecAlreadySubmittedError.ts | 12 - .../moderation/NcmecSubmissionFailedError.ts | 13 - packages/errors/src/i18n/ErrorCodeMappings.ts | 4 - packages/errors/src/i18n/ErrorI18nMessages.ts | 4 - packages/errors/src/i18n/locales/ar.ts | 3 - packages/errors/src/i18n/locales/bg.ts | 3 - packages/errors/src/i18n/locales/cs.ts | 3 - packages/errors/src/i18n/locales/da.ts | 3 - packages/errors/src/i18n/locales/de.ts | 3 - packages/errors/src/i18n/locales/el.ts | 3 - packages/errors/src/i18n/locales/en-GB.ts | 3 - packages/errors/src/i18n/locales/es-419.ts | 3 - packages/errors/src/i18n/locales/es-ES.ts | 3 - packages/errors/src/i18n/locales/fi.ts | 3 - packages/errors/src/i18n/locales/fr.ts | 3 - packages/errors/src/i18n/locales/he.ts | 3 - packages/errors/src/i18n/locales/hi.ts | 3 - packages/errors/src/i18n/locales/hr.ts | 3 - packages/errors/src/i18n/locales/hu.ts | 3 - packages/errors/src/i18n/locales/id.ts | 3 - packages/errors/src/i18n/locales/it.ts | 3 - packages/errors/src/i18n/locales/ja.ts | 3 - packages/errors/src/i18n/locales/ko.ts | 3 - packages/errors/src/i18n/locales/lt.ts | 3 - packages/errors/src/i18n/locales/nl.ts | 3 - packages/errors/src/i18n/locales/no.ts | 3 - packages/errors/src/i18n/locales/pl.ts | 3 - packages/errors/src/i18n/locales/pt-BR.ts | 3 - packages/errors/src/i18n/locales/ro.ts | 3 - packages/errors/src/i18n/locales/ru.ts | 3 - packages/errors/src/i18n/locales/sv-SE.ts | 3 - packages/errors/src/i18n/locales/th.ts | 3 - packages/errors/src/i18n/locales/tr.ts | 3 - packages/errors/src/i18n/locales/uk.ts | 3 - packages/errors/src/i18n/locales/vi.ts | 3 - packages/errors/src/i18n/locales/zh-CN.ts | 3 - packages/errors/src/i18n/locales/zh-TW.ts | 3 - .../errors/src/i18n/weblate/locales/ar.json | 3 - .../errors/src/i18n/weblate/locales/bg.json | 3 - .../errors/src/i18n/weblate/locales/cs.json | 3 - .../errors/src/i18n/weblate/locales/da.json | 3 - .../errors/src/i18n/weblate/locales/de.json | 3 - .../errors/src/i18n/weblate/locales/el.json | 3 - .../src/i18n/weblate/locales/en-GB.json | 3 - .../src/i18n/weblate/locales/es-419.json | 3 - .../src/i18n/weblate/locales/es-ES.json | 3 - .../errors/src/i18n/weblate/locales/fi.json | 3 - .../errors/src/i18n/weblate/locales/fr.json | 3 - .../errors/src/i18n/weblate/locales/he.json | 3 - .../errors/src/i18n/weblate/locales/hi.json | 3 - .../errors/src/i18n/weblate/locales/hr.json | 3 - .../errors/src/i18n/weblate/locales/hu.json | 3 - .../errors/src/i18n/weblate/locales/id.json | 3 - .../errors/src/i18n/weblate/locales/it.json | 3 - .../errors/src/i18n/weblate/locales/ja.json | 3 - .../errors/src/i18n/weblate/locales/ko.json | 3 - .../errors/src/i18n/weblate/locales/lt.json | 3 - .../errors/src/i18n/weblate/locales/nl.json | 3 - .../errors/src/i18n/weblate/locales/no.json | 3 - .../errors/src/i18n/weblate/locales/pl.json | 3 - .../src/i18n/weblate/locales/pt-BR.json | 3 - .../errors/src/i18n/weblate/locales/ro.json | 3 - .../errors/src/i18n/weblate/locales/ru.json | 3 - .../src/i18n/weblate/locales/sv-SE.json | 3 - .../errors/src/i18n/weblate/locales/th.json | 3 - .../errors/src/i18n/weblate/locales/tr.json | 3 - .../errors/src/i18n/weblate/locales/uk.json | 3 - .../errors/src/i18n/weblate/locales/vi.json | 3 - .../src/i18n/weblate/locales/zh-CN.json | 3 - .../src/i18n/weblate/locales/zh-TW.json | 3 - .../errors/src/i18n/weblate/messages.json | 3 - .../src/domains/admin/AdminMessageSchemas.ts | 12 - .../schema/src/domains/admin/AdminSchemas.ts | 20 - pnpm-lock.yaml | 61 - pnpm-workspace.yaml | 1 - tools/dev/cassandra_target_schema.json | 138 --- tools/i18n_auto/src/tokens.rs | 2 +- 146 files changed, 234 insertions(+), 4075 deletions(-) delete mode 100644 fluxer_api/src/api/csam/NcmecReporter.ts delete mode 100644 fluxer_api/src/api/csam/NcmecRepository.ts delete mode 100644 fluxer_api/src/api/csam/NcmecSubmissionService.ts delete mode 100644 fluxer_api/src/api/csam/tests/NcmecReporter.test.ts delete mode 100644 fluxer_api/src/api/database/types/CsamTypes.ts delete mode 100644 fluxer_api/src/api/test/fixtures/ncmec/NcmecXmlFixtures.ts delete mode 100644 fluxer_api/src/api/test/msw/handlers/NcmecHandlers.ts delete mode 100644 fluxer_api/src/api/worker/tasks/FinalizeNcmecAttachmentReport.ts delete mode 100644 packages/constants/src/ReportCategories.ts delete mode 100644 packages/errors/src/domains/moderation/NcmecAlreadySubmittedError.ts delete mode 100644 packages/errors/src/domains/moderation/NcmecSubmissionFailedError.ts diff --git a/config/env/development.env b/config/env/development.env index 578830e23..bd07226f7 100644 --- a/config/env/development.env +++ b/config/env/development.env @@ -112,7 +112,6 @@ FLUXER_SEARCH_USERNAME= FLUXER_SEARCH_PASSWORD= FLUXER_SEARCH_TLS_REJECT_UNAUTHORIZED=false FLUXER_STRIPE_ENABLED=false -FLUXER_NCMEC_ENABLED=false FLUXER_CLAMAV_ENABLED=false FLUXER_DISCOVERY_ENABLED=true FLUXER_SELF_HOSTED=true diff --git a/deploy/self-hosting/.env.example b/deploy/self-hosting/.env.example index 4969ef347..d9b9fc56f 100644 --- a/deploy/self-hosting/.env.example +++ b/deploy/self-hosting/.env.example @@ -142,14 +142,6 @@ MEILI_MASTER_KEY=CHANGE_ME #FLUXER_VISIONARIES_GUILD_ID= #FLUXER_VISIONARIES_GUILD_VISIONARY_ROLE_ID= -# NCMEC CyberTipline reporting, off by default. All four values are required -# once it is on. The values below are examples. -#FLUXER_NCMEC_ENABLED=true -#FLUXER_NCMEC_BASE_URL=https://report.cybertip.org/ispws -#FLUXER_NCMEC_USERNAME= -#FLUXER_NCMEC_PASSWORD= -#FLUXER_NCMEC_REPORTER_EMAIL=trust@example.com - # Upload virus scanning, off by default. No ClamAV container ships, so point # this at your own. The values below are examples. #FLUXER_CLAMAV_ENABLED=true diff --git a/deploy/self-hosting/docker-compose.yml b/deploy/self-hosting/docker-compose.yml index 6676a1666..edc4155cf 100644 --- a/deploy/self-hosting/docker-compose.yml +++ b/deploy/self-hosting/docker-compose.yml @@ -132,11 +132,6 @@ x-fluxer-env: &fluxer-env FLUXER_VISIONARIES_GUILD_ID: ${FLUXER_VISIONARIES_GUILD_ID:-} FLUXER_VISIONARIES_GUILD_VISIONARY_ROLE_ID: ${FLUXER_VISIONARIES_GUILD_VISIONARY_ROLE_ID:-} - FLUXER_NCMEC_ENABLED: ${FLUXER_NCMEC_ENABLED:-} - FLUXER_NCMEC_BASE_URL: ${FLUXER_NCMEC_BASE_URL:-} - FLUXER_NCMEC_USERNAME: ${FLUXER_NCMEC_USERNAME:-} - FLUXER_NCMEC_PASSWORD: ${FLUXER_NCMEC_PASSWORD:-} - FLUXER_NCMEC_REPORTER_EMAIL: ${FLUXER_NCMEC_REPORTER_EMAIL:-} FLUXER_CLAMAV_ENABLED: ${FLUXER_CLAMAV_ENABLED:-} FLUXER_CLAMAV_HOST: ${FLUXER_CLAMAV_HOST:-} FLUXER_CLAMAV_PORT: ${FLUXER_CLAMAV_PORT:-} diff --git a/fluxer_admin/openapi-admin.json b/fluxer_admin/openapi-admin.json index 4d799fbf1..8f7b624a5 100644 --- a/fluxer_admin/openapi-admin.json +++ b/fluxer_admin/openapi-admin.json @@ -4883,65 +4883,6 @@ ] } }, - "/admin/messages/ncmec-reports": { - "post": { - "operationId": "create_admin_ncmec_report", - "summary": "Report an attachment to NCMEC", - "tags": ["Admin"], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": {"schema": {"$ref": "#/components/schemas/NcmecAttachmentSubmitResultResponse"}} - } - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ThrottledError"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "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.", - "security": [{"adminApiKey": []}], - "requestBody": { - "required": true, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ReportAttachmentToNcmecRequest"}}} - } - } - }, "/admin/messages/shreds/{job_id}": { "get": { "operationId": "get_admin_message_shred", @@ -10280,7 +10221,7 @@ "type": "object", "properties": { "acls": { - "maxItems": 106, + "maxItems": 105, "type": "array", "items": {"$ref": "#/components/schemas/AdminAclType"}, "description": "List of access control permissions to assign" @@ -10591,37 +10532,9 @@ "content_type": {"nullable": true, "type": "string"}, "width": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]}, "height": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]}, - "size": { - "nullable": true, - "allOf": [{"$ref": "#/components/schemas/NonNegativeSafeIntegerType"}] - }, - "ncmec_status": { - "description": "NCMEC submission status", - "x-enumNames": ["not_submitted", "submitting", "submitted", "failed"], - "x-enumDescriptions": [ - "Report has not been submitted to NCMEC", - "Report submission to NCMEC is in progress", - "Report has been submitted to NCMEC", - "Report submission to NCMEC failed" - ], - "enum": ["not_submitted", "submitting", "submitted", "failed"], - "type": "string" - }, - "ncmec_report_id": {"nullable": true, "type": "string", "minLength": 1, "maxLength": 256}, - "ncmec_failure_reason": {"nullable": true, "type": "string", "minLength": 1, "maxLength": 4000} + "size": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/NonNegativeSafeIntegerType"}]} }, - "required": [ - "id", - "filename", - "url", - "nsfw", - "content_type", - "width", - "height", - "ncmec_status", - "ncmec_report_id", - "ncmec_failure_reason" - ], + "required": ["id", "filename", "url", "nsfw", "content_type", "width", "height"], "additionalProperties": false } }, @@ -10653,11 +10566,6 @@ "required": ["id", "filename", "nsfw", "content_type", "width", "height", "size"], "additionalProperties": false } - }, - "user_prior_ncmec_report_ids": { - "maxItems": 100, - "type": "array", - "items": {"type": "string", "minLength": 1, "maxLength": 256} } }, "required": [ @@ -11004,29 +10912,6 @@ } ] }, - "ReportAttachmentToNcmecRequest": { - "type": "object", - "properties": { - "channel_id": {"$ref": "#/components/schemas/SnowflakeType"}, - "message_id": {"$ref": "#/components/schemas/SnowflakeType"}, - "attachment_id": {"$ref": "#/components/schemas/SnowflakeType"}, - "filename": {"type": "string", "minLength": 1, "maxLength": 255}, - "source_report_id": {"allOf": [{"$ref": "#/components/schemas/SnowflakeType"}]}, - "reporter_full_name": {"type": "string", "minLength": 1, "maxLength": 200}, - "confirmed_viewed": {"type": "boolean", "enum": [true]} - }, - "required": ["channel_id", "message_id", "attachment_id", "filename", "reporter_full_name", "confirmed_viewed"] - }, - "NcmecAttachmentSubmitResultResponse": { - "type": "object", - "properties": { - "success": {"type": "boolean", "enum": [true]}, - "ncmec_report_id": {"type": "string", "minLength": 1, "maxLength": 256}, - "audit_log_reason": {"type": "string", "minLength": 1, "maxLength": 4000} - }, - "required": ["success", "ncmec_report_id", "audit_log_reason"], - "additionalProperties": false - }, "AdminMessageSearchResponse": { "anyOf": [ { @@ -13514,7 +13399,7 @@ }, "acls": { "description": "Replacement list of access control permissions for the key", - "maxItems": 106, + "maxItems": 105, "type": "array", "items": {"$ref": "#/components/schemas/AdminAclType"} } @@ -13532,7 +13417,7 @@ "type": "string" }, "acls": { - "maxItems": 106, + "maxItems": 105, "type": "array", "items": {"type": "string"}, "description": "List of access control permissions for the key" @@ -13562,7 +13447,7 @@ "maximum": 365 }, "acls": { - "maxItems": 106, + "maxItems": 105, "type": "array", "items": {"$ref": "#/components/schemas/AdminAclType"}, "description": "List of access control permissions for the key" @@ -13583,7 +13468,7 @@ "type": "string" }, "acls": { - "maxItems": 106, + "maxItems": 105, "type": "array", "items": {"type": "string"}, "description": "List of access control permissions for the key" @@ -13596,7 +13481,7 @@ "type": "object", "properties": { "acls": { - "maxItems": 106, + "maxItems": 105, "type": "array", "items": {"type": "string", "minLength": 1, "maxLength": 64}, "description": "Every admin access control permission the admin API recognises" @@ -13651,7 +13536,6 @@ "bulk:delete:user_messages", "bulk:update:guild_features", "bulk:update:user_flags", - "csam:submit_ncmec", "discovery:remove", "discovery:review", "gateway:memory_stats", @@ -14599,41 +14483,11 @@ "content_type": {"nullable": true, "type": "string"}, "width": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]}, "height": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]}, - "size": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/NonNegativeSafeIntegerType"}]}, - "ncmec_status": { - "description": "NCMEC submission status", - "x-enumNames": ["not_submitted", "submitting", "submitted", "failed"], - "x-enumDescriptions": [ - "Report has not been submitted to NCMEC", - "Report submission to NCMEC is in progress", - "Report has been submitted to NCMEC", - "Report submission to NCMEC failed" - ], - "enum": ["not_submitted", "submitting", "submitted", "failed"], - "type": "string" - }, - "ncmec_report_id": {"nullable": true, "type": "string", "minLength": 1, "maxLength": 256}, - "ncmec_failure_reason": {"nullable": true, "type": "string", "minLength": 1, "maxLength": 4000} + "size": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/NonNegativeSafeIntegerType"}]} }, - "required": [ - "id", - "filename", - "url", - "nsfw", - "content_type", - "width", - "height", - "ncmec_status", - "ncmec_report_id", - "ncmec_failure_reason" - ], + "required": ["id", "filename", "url", "nsfw", "content_type", "width", "height"], "additionalProperties": false } - }, - "user_prior_ncmec_report_ids": { - "maxItems": 100, - "type": "array", - "items": {"type": "string", "minLength": 1, "maxLength": 256} } }, "required": [ @@ -16766,7 +16620,7 @@ "description": "ISO 8601 timestamp when the pending deletion was scheduled", "type": "string" }, - "acls": {"maxItems": 106, "type": "array", "items": {"type": "string"}}, + "acls": {"maxItems": 105, "type": "array", "items": {"type": "string"}}, "traits": {"maxItems": 100, "type": "array", "items": {"type": "string"}}, "has_totp": {"type": "boolean"}, "authenticator_types": {"maxItems": 10, "type": "array", "items": {"$ref": "#/components/schemas/Int32Type"}}, @@ -17350,8 +17204,6 @@ "MAX_THREAD_MEMBERS", "MAX_WEBHOOKS_PER_CHANNEL", "MAX_WEBHOOKS_PER_GUILD", - "NCMEC_ALREADY_SUBMITTED", - "NCMEC_SUBMISSION_FAILED", "MEDIA_METADATA_ERROR", "METHOD_NOT_ALLOWED", "MISSING_ACCESS", @@ -17647,8 +17499,6 @@ "This thread has reached its member limit of {count}.", "You've reached the maximum of {count, plural, one {# webhook} other {# webhooks}} per channel.", "You've reached the maximum of {count, plural, one {# webhook} other {# webhooks}} per community.", - "This content has already been submitted to NCMEC.", - "We couldn't submit the report to NCMEC. Please try again later.", "Media metadata error.", "Method not allowed.", "You don't have access to this resource or feature.", diff --git a/fluxer_admin/src/acl.rs b/fluxer_admin/src/acl.rs index 2a309f88d..ce45258bf 100644 --- a/fluxer_admin/src/acl.rs +++ b/fluxer_admin/src/acl.rs @@ -43,7 +43,6 @@ pub const BULK_DELETE_USERS: &str = "bulk:delete:users"; pub const BULK_DELETE_USER_MESSAGES: &str = "bulk:delete:user_messages"; pub const BULK_UPDATE_GUILD_FEATURES: &str = "bulk:update:guild_features"; pub const BULK_UPDATE_USER_FLAGS: &str = "bulk:update:user_flags"; -pub const CSAM_SUBMIT_NCMEC: &str = "csam:submit_ncmec"; pub const DISCOVERY_REMOVE: &str = "discovery:remove"; pub const DISCOVERY_REVIEW: &str = "discovery:review"; pub const GATEWAY_MEMORY_STATS: &str = "gateway:memory_stats"; @@ -150,7 +149,6 @@ pub const ALL_ACLS: &[&str] = &[ BULK_DELETE_USER_MESSAGES, BULK_UPDATE_GUILD_FEATURES, BULK_UPDATE_USER_FLAGS, - CSAM_SUBMIT_NCMEC, DISCOVERY_REMOVE, DISCOVERY_REVIEW, GATEWAY_MEMORY_STATS, @@ -215,13 +213,6 @@ pub const ALL_ACLS: &[&str] = &[ VOICE_SERVER_UPDATE, ]; -pub const NCMEC_ATTACHMENT_REPORT: &[&str] = &[ - CSAM_SUBMIT_NCMEC, - MESSAGE_DELETE, - USER_DELETE, - ARCHIVE_TRIGGER_USER, -]; - pub fn has_permission(admin_acls: &[String], required: &str) -> bool { let required_alias = acl_key_alias(required); admin_acls.iter().any(|acl| { @@ -236,10 +227,6 @@ pub fn has_any_permission(admin_acls: &[String], required: &[&str]) -> bool { required.iter().any(|acl| has_permission(admin_acls, acl)) } -pub fn has_all_permissions(admin_acls: &[String], required: &[&str]) -> bool { - required.iter().all(|acl| has_permission(admin_acls, acl)) -} - fn acl_key_alias(acl: &str) -> String { acl.replace(':', "_").to_ascii_uppercase() } @@ -293,34 +280,6 @@ mod tests { assert!(has_any_permission(&acls(&[]), &[])); } - #[test] - fn has_all_permissions_needs_every_acl() { - assert!(has_all_permissions(&acls(&[USER_LOOKUP]), &[])); - assert!(has_all_permissions( - &acls(&[USER_LOOKUP, GUILD_LOOKUP]), - &[GUILD_LOOKUP, USER_LOOKUP] - )); - assert!(!has_all_permissions( - &acls(&[USER_LOOKUP]), - &[GUILD_LOOKUP, USER_LOOKUP] - )); - assert!(has_all_permissions( - &acls(&[WILDCARD]), - NCMEC_ATTACHMENT_REPORT - )); - for missing in NCMEC_ATTACHMENT_REPORT { - let held = NCMEC_ATTACHMENT_REPORT - .iter() - .copied() - .filter(|acl| acl != missing) - .collect::>(); - assert!( - !has_all_permissions(&acls(&held), NCMEC_ATTACHMENT_REPORT), - "{missing}" - ); - } - } - #[test] fn has_any_permission_none_match() { let user_acls = acls(&[USER_LOOKUP]); diff --git a/fluxer_admin/src/api/guilds.rs b/fluxer_admin/src/api/guilds.rs index f1731b7c3..b5b03e04b 100644 --- a/fluxer_admin/src/api/guilds.rs +++ b/fluxer_admin/src/api/guilds.rs @@ -337,9 +337,9 @@ fn guild_update_response( .map_err(ApiError::Parse)?, features: guild.features.into_iter().map(String::from).collect(), nsfw_level: guild.nsfw_level.map(i32::from), - nsfw: guild.nsfw, - content_warning_level: guild.content_warning_level.map(i32::from), - content_warning_text: guild.content_warning_text.map(String::from), + nsfw: None, + content_warning_level: None, + content_warning_text: None, description: None, vanity_url_code: None, }, diff --git a/fluxer_admin/src/api/messages.rs b/fluxer_admin/src/api/messages.rs index d4ad3cee6..b80088335 100644 --- a/fluxer_admin/src/api/messages.rs +++ b/fluxer_admin/src/api/messages.rs @@ -5,8 +5,7 @@ use crate::api::generated::{snowflake, types as generated_types}; use super::client::{AdminApiClient, ApiError, ApiResult}; use super::types::{ BrowseChannelResponse, DeleteAllUserMessagesResponse, LookupMessageResponse, - MessageShredResponse, MessageShredStatusResponse, NcmecAttachmentSubmitResult, - SearchChannelMessagesResponse, + MessageShredResponse, MessageShredStatusResponse, SearchChannelMessagesResponse, }; impl AdminApiClient { @@ -30,37 +29,6 @@ impl AdminApiClient { Ok(()) } - pub async fn report_attachment_to_ncmec( - &self, - channel_id: &str, - message_id: &str, - attachment_id: &str, - filename: &str, - reporter_full_name: &str, - source_report_id: Option<&str>, - ) -> ApiResult { - let body = generated_types::ReportAttachmentToNcmecRequest { - attachment_id: snowflake(attachment_id), - channel_id: snowflake(channel_id), - confirmed_viewed: true, - filename: generated_types::ReportAttachmentToNcmecRequestFilename::try_from(filename) - .map_err(|e| ApiError::Parse(e.to_string()))?, - message_id: snowflake(message_id), - reporter_full_name: - generated_types::ReportAttachmentToNcmecRequestReporterFullName::try_from( - reporter_full_name, - ) - .map_err(|e| ApiError::Parse(e.to_string()))?, - source_report_id: source_report_id.map(snowflake), - }; - let response = self - .generated() - .create_admin_ncmec_report(&body) - .await - .map_err(|e| self.generated_error(e))?; - self.generated_value(response.into_inner()) - } - pub async fn lookup_message( &self, channel_id: &str, diff --git a/fluxer_admin/src/api/types/messages.rs b/fluxer_admin/src/api/types/messages.rs index 3eaae6779..8b2bb5965 100644 --- a/fluxer_admin/src/api/types/messages.rs +++ b/fluxer_admin/src/api/types/messages.rs @@ -34,12 +34,6 @@ pub struct DeleteAllUserMessagesResponse { pub extra: serde_json::Value, } -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct NcmecAttachmentSubmitResult { - #[serde(flatten)] - pub data: serde_json::Value, -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct BrowseChannelResponse { #[serde(flatten)] diff --git a/fluxer_admin/src/routes/message_actions.rs b/fluxer_admin/src/routes/message_actions.rs index 1891527d7..91f52fcd2 100644 --- a/fluxer_admin/src/routes/message_actions.rs +++ b/fluxer_admin/src/routes/message_actions.rs @@ -8,7 +8,6 @@ use crate::{ flash::{self, FlashData}, }, state::AppState, - templates::components::message_list::MessageActions, utils::forms::MultiValueForm, }; use axum::{ @@ -106,45 +105,6 @@ pub(crate) async fn messages_post( Err(e) => json_error(StatusCode::BAD_REQUEST, &format!("{e}")), } } - "report-to-ncmec" => { - let attachment_id = form.clean("attachment_id"); - let filename = form.clean("filename"); - let reporter_full_name = form.clean("reporter_full_name"); - let source_report_id = form.clean("source_report_id"); - let confirmed_viewed = form.bool_value("confirmed_viewed"); - let (Some(cid), Some(mid), Some(aid), Some(name), Some(reporter)) = ( - &channel_id, - &message_id, - &attachment_id, - &filename, - &reporter_full_name, - ) else { - return json_error( - StatusCode::BAD_REQUEST, - "Missing required NCMEC report fields", - ); - }; - if !confirmed_viewed { - return json_error( - StatusCode::BAD_REQUEST, - "Missing required NCMEC report fields", - ); - } - match client - .report_attachment_to_ncmec( - cid, - mid, - aid, - name, - reporter, - source_report_id.as_deref(), - ) - .await - { - Ok(resp) => Json(resp.data).into_response(), - Err(e) => json_error(StatusCode::BAD_REQUEST, &format!("{e}")), - } - } _ => Redirect::to(&format!("{base}/messages")).into_response(), } } @@ -322,11 +282,7 @@ pub(crate) async fn messages_browse_fragment( .as_ref() .map(|user| user.acls.as_slice()) .unwrap_or(&[]); - let actions = MessageActions { - delete: acl::has_permission(admin_acls, acl::MESSAGE_DELETE), - ncmec: acl::has_all_permissions(admin_acls, acl::NCMEC_ATTACHMENT_REPORT), - source_report_id: None, - }; + let can_delete = acl::has_permission(admin_acls, acl::MESSAGE_DELETE); let client = AdminApiClient::new(state.http_client(), config, &auth.0.session); let result = client .browse_channel( @@ -339,7 +295,7 @@ pub(crate) async fn messages_browse_fragment( match result { Ok(resp) => { let markup = crate::templates::pages::messages_page::browse_messages_fragment( - config, &resp.data, actions, None, + config, &resp.data, can_delete, None, ); Html(markup.into_string()).into_response() } diff --git a/fluxer_admin/src/routes/messages.rs b/fluxer_admin/src/routes/messages.rs index b39e97abf..0e1159e37 100644 --- a/fluxer_admin/src/routes/messages.rs +++ b/fluxer_admin/src/routes/messages.rs @@ -73,7 +73,6 @@ async fn messages_page( .unwrap_or(&[]); let can_lookup = acl::has_permission(admin_acls, acl::MESSAGE_LOOKUP); let can_delete = acl::has_permission(admin_acls, acl::MESSAGE_DELETE); - let can_report_ncmec = acl::has_all_permissions(admin_acls, acl::NCMEC_ATTACHMENT_REPORT); let client = AdminApiClient::new(state.http_client(), config, &auth.0.session); let channel_id = query.channel_id.as_deref().filter(|s| !s.is_empty()); let message_id = query.message_id.as_deref().filter(|s| !s.is_empty()); @@ -138,7 +137,6 @@ async fn messages_page( prefill_channel_id: channel_id, can_lookup, can_delete, - can_report_ncmec, lookup_result: lookup_result.as_ref(), browse_result: browse_result.as_ref(), search_result: search_result.as_ref(), diff --git a/fluxer_admin/src/templates/components/message_data.rs b/fluxer_admin/src/templates/components/message_data.rs index 62a79b639..5b48e0cf6 100644 --- a/fluxer_admin/src/templates/components/message_data.rs +++ b/fluxer_admin/src/templates/components/message_data.rs @@ -12,9 +12,6 @@ pub struct Attachment { pub width: Option, pub height: Option, pub size: Option, - pub ncmec_status: String, - pub ncmec_report_id: Option, - pub ncmec_failure_reason: Option, } #[derive(Debug, PartialEq)] @@ -113,14 +110,6 @@ fn attachment_from_value(value: &Value) -> Attachment { width: value["width"].as_u64().map(|n| n as u32), height: value["height"].as_u64().map(|n| n as u32), size: value["size"].as_u64(), - ncmec_status: value["ncmec_status"] - .as_str() - .unwrap_or("not_submitted") - .to_owned(), - ncmec_report_id: value["ncmec_report_id"].as_str().map(ToOwned::to_owned), - ncmec_failure_reason: value["ncmec_failure_reason"] - .as_str() - .map(ToOwned::to_owned), } } @@ -168,10 +157,7 @@ mod tests { "content_type": "image/png", "width": 640, "height": 480, - "size": 4096, - "ncmec_status": "submitted", - "ncmec_report_id": "report-id", - "ncmec_failure_reason": "previous failure" + "size": 4096 }] }); @@ -202,9 +188,6 @@ mod tests { width: Some(640), height: Some(480), size: Some(4096), - ncmec_status: "submitted".into(), - ncmec_report_id: Some("report-id".into()), - ncmec_failure_reason: Some("previous failure".into()), }], missing_attachments: vec![], } @@ -290,9 +273,6 @@ mod tests { width: None, height: None, size: None, - ncmec_status: "not_submitted".into(), - ncmec_report_id: None, - ncmec_failure_reason: None, }] ); } diff --git a/fluxer_admin/src/templates/components/message_list.rs b/fluxer_admin/src/templates/components/message_list.rs index 3f3930f63..59b2fca44 100644 --- a/fluxer_admin/src/templates/components/message_list.rs +++ b/fluxer_admin/src/templates/components/message_list.rs @@ -13,47 +13,13 @@ use crate::utils::timestamps::format_admin_timestamp; use super::message_data::{Attachment, Message}; -#[derive(Clone, Copy, Debug, Default)] -pub struct MessageActions<'a> { - pub delete: bool, - pub ncmec: bool, - pub source_report_id: Option<&'a str>, -} - fn is_image(att: &Attachment) -> bool { att.content_type .as_deref() .is_some_and(|ct| ct.starts_with("image/")) } -fn ncmec_badge(att: &Attachment) -> Markup { - match att.ncmec_status.as_str() { - "submitted" => { - let label = att - .ncmec_report_id - .as_deref() - .map(|id| format!("NCMEC {id}")) - .unwrap_or_else(|| "Reported to NCMEC".into()); - html! { - span class="rounded bg-green-100 px-2 py-0.5 text-[11px] text-green-800" - title=(label) { (label) } - } - } - "failed" => { - let title = att - .ncmec_failure_reason - .as_deref() - .unwrap_or("NCMEC report failed"); - html! { - span class="rounded bg-red-100 px-2 py-0.5 text-[11px] text-red-800" - title=(title) { "NCMEC Failed" } - } - } - _ => html! {}, - } -} - -fn render_image_attachments(msg: &Message, actions: MessageActions<'_>) -> Markup { +fn render_image_attachments(msg: &Message, include_delete: bool) -> Markup { let mut images = msg.attachments.iter().filter(|a| is_image(a)).peekable(); if images.peek().is_none() { return html! {}; @@ -79,47 +45,15 @@ fn render_image_attachments(msg: &Message, actions: MessageActions<'_>) -> Marku a href=(att.url) target="_blank" rel="noopener noreferrer" class="font-medium text-blue-600 hover:underline" { (att.filename) } (attachment_nsfw_badge(att.nsfw.unwrap_or(false))) - (ncmec_badge(att)) } - @if actions.delete || actions.ncmec { + @if include_delete { div class="flex flex-wrap gap-2" { - @if actions.delete { - button type="button" - class="delete-message-btn rounded bg-white px-2.5 py-1 \ - text-red-600 text-xs shadow-sm ring-1 ring-neutral-200 \ - transition-colors hover:bg-red-50 hover:text-red-700" - data-channel-id=(msg.channel_id) - data-message-id=(msg.id) { "Delete" } - } - @if actions.ncmec { - button type="button" - class="ncmec-report-btn rounded bg-white px-2.5 py-1 text-xs \ - shadow-sm ring-1 ring-neutral-200 transition-colors \ - hover:bg-neutral-100 disabled:cursor-not-allowed \ - disabled:opacity-70" - data-channel-id=(msg.channel_id) - data-message-id=(msg.id) - data-attachment-id=(att.id) - data-filename=(att.filename) - data-content-type=(att.content_type.as_deref().unwrap_or("")) - data-size=(att.size.map(|s| s.to_string()).unwrap_or_default()) - data-author-id=(msg.author_id) - data-ncmec-status=(att.ncmec_status) - data-ncmec-report-id=(att.ncmec_report_id.as_deref().unwrap_or("")) - data-source-report-id=[actions.source_report_id] - disabled[att.ncmec_status == "submitted"] - title=(if att.ncmec_status == "submitted" { - "Already reported to NCMEC" - } else { - "Report this image to NCMEC" - }) { - @if att.ncmec_status == "submitted" { - "Reported to NCMEC" - } @else { - "Report to NCMEC" - } - } - } + button type="button" + class="delete-message-btn rounded bg-white px-2.5 py-1 \ + text-red-600 text-xs shadow-sm ring-1 ring-neutral-200 \ + transition-colors hover:bg-red-50 hover:text-red-700" + data-channel-id=(msg.channel_id) + data-message-id=(msg.id) { "Delete" } } } } @@ -147,7 +81,6 @@ fn render_other_attachments(msg: &Message, has_content_or_images: bool) -> Marku a href=(att.url) target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:underline" { (att.filename) } (attachment_nsfw_badge(att.nsfw.unwrap_or(false))) - (ncmec_badge(att)) } } } @@ -181,7 +114,7 @@ fn message_row( base_path: &str, avatar_url: &str, msg: &Message, - actions: MessageActions<'_>, + include_delete: bool, is_highlighted: bool, is_grouped: bool, ) -> Markup { @@ -213,14 +146,14 @@ fn message_row( text-sm leading-snug" { (msg.content) } } @if !msg.attachments.is_empty() { - (render_image_attachments(msg, actions)) + (render_image_attachments(msg, include_delete)) (render_other_attachments(msg, has_content_or_images)) } (render_missing_attachments(msg)) div class="mt-1 text-neutral-400 text-xs" { span class="" { (msg.id) } } - @if actions.delete && !has_images { + @if include_delete && !has_images { div class="absolute top-0 right-2 hidden group-hover:block" { button type="button" class="delete-message-btn rounded bg-white px-2 py-0.5 \ @@ -296,7 +229,7 @@ fn message_row( text-sm leading-snug" { (msg.content) } } @if !msg.attachments.is_empty() { - (render_image_attachments(msg, actions)) + (render_image_attachments(msg, include_delete)) (render_other_attachments(msg, has_content_or_images)) } (render_missing_attachments(msg)) @@ -304,7 +237,7 @@ fn message_row( span class="" { (msg.id) } } } - @if actions.delete && !has_images { + @if include_delete && !has_images { div class="absolute top-1 right-2 hidden group-hover:block" { button type="button" class="delete-message-btn rounded bg-white px-2 py-0.5 \ @@ -322,7 +255,7 @@ pub fn message_list( config: &AdminConfig, base_path: &str, messages: &[Message], - actions: MessageActions<'_>, + include_delete: bool, highlight_message_id: Option<&str>, ) -> Markup { html! { @@ -337,7 +270,7 @@ pub fn message_list( ); (message_row( base_path, &avatar_url, msg, - actions, is_highlighted, is_grouped, + include_delete, is_highlighted, is_grouped, )) } } @@ -385,38 +318,6 @@ pub fn message_deletion_script(csrf_token: &str) -> Markup { toast('error', 'Failed to delete message.'); }); } - function reportNcmec(b) { - var name = prompt('Type your full name to confirm you personally viewed this image and want to submit it to NCMEC.'); - if (!name || !name.trim()) return; - var fields = new URLSearchParams(); - fields.append('channel_id', b.dataset.channelId || ''); - fields.append('message_id', b.dataset.messageId || ''); - fields.append('attachment_id', b.dataset.attachmentId || ''); - fields.append('filename', b.dataset.filename || ''); - if (b.dataset.sourceReportId) fields.append('source_report_id', b.dataset.sourceReportId); - fields.append('reporter_full_name', name.trim()); - fields.append('confirmed_viewed', 'true'); - b.disabled = true; - b.textContent = 'Reporting...'; - toast('info', 'Submitting NCMEC report...'); - post('report-to-ncmec', fields).then(function(r) { - return r.json().catch(function() { - return null; - }).then(function(data) { - if (!r.ok || !data || data.success !== true) throw new Error(data && (data.error || data.message) || 'Failed to report attachment to NCMEC'); - return data; - }); - }).then(function(data) { - b.textContent = 'Reported to NCMEC'; - b.dataset.ncmecStatus = 'submitted'; - if (data.ncmec_report_id) b.dataset.ncmecReportId = data.ncmec_report_id; - toast('success', 'NCMEC report submitted.'); - }).catch(function(err) { - b.disabled = false; - b.textContent = 'Report to NCMEC'; - toast('error', err && err.message ? err.message : 'Failed to report attachment to NCMEC.'); - }); - } document.addEventListener('click', function(e) { var t = e.target; if (!(t instanceof HTMLElement)) return; @@ -424,12 +325,6 @@ pub fn message_deletion_script(csrf_token: &str) -> Markup { if (d instanceof HTMLButtonElement) { e.preventDefault(); deleteMessage(d); - return; - } - var n = t.closest('.ncmec-report-btn'); - if (n instanceof HTMLButtonElement && !n.disabled) { - e.preventDefault(); - reportNcmec(n); } }); })();"# diff --git a/fluxer_admin/src/templates/pages/messages_page.rs b/fluxer_admin/src/templates/pages/messages_page.rs index b5478284c..c3a7625de 100644 --- a/fluxer_admin/src/templates/pages/messages_page.rs +++ b/fluxer_admin/src/templates/pages/messages_page.rs @@ -11,7 +11,7 @@ use crate::{ submit_button, }, message_data::{Message, ordered_messages, value_id}, - message_list::{MessageActions, message_deletion_script, message_list}, + message_list::{message_deletion_script, message_list}, page_container::{card, page_header}, }, layout::LayoutOptions, @@ -193,7 +193,6 @@ pub struct MessagesPageParams<'a> { pub prefill_channel_id: Option<&'a str>, pub can_lookup: bool, pub can_delete: bool, - pub can_report_ncmec: bool, pub lookup_result: Option<&'a Value>, pub browse_result: Option<&'a Value>, pub search_result: Option<&'a Value>, @@ -208,11 +207,6 @@ pub fn messages_page( auth: &AuthContext, params: &MessagesPageParams<'_>, ) -> Markup { - let actions = MessageActions { - delete: params.can_delete, - ncmec: params.can_report_ncmec, - source_report_id: None, - }; let content = html! { (page_header("Message Tools", None)) @@ -223,19 +217,19 @@ pub fn messages_page( @if let (Some(result), Some(channel_id), Some(query)) = ( params.search_result, params.browse_channel_id, params.search_query_text, ) { - (search_result_card(config, result, channel_id, query, actions)) + (search_result_card(config, result, channel_id, query, params.can_delete)) } @if params.search_result.is_none() && let (Some(result), Some(channel_id)) = ( params.browse_result, params.browse_channel_id, ) { (browse_result_card( - config, result, channel_id, actions, + config, result, channel_id, params.can_delete, params.csrf_token, params.context_limit, )) } @if let Some(result) = params.lookup_result { - (lookup_result_card(config, result, actions, params.context_limit)) + (lookup_result_card(config, result, params.can_delete, params.context_limit)) } @if params.can_lookup { (browse_channel_form(config, params.csrf_token, params.prefill_channel_id)) @@ -266,7 +260,7 @@ pub fn messages_page( pub fn browse_messages_fragment( config: &AdminConfig, result: &Value, - actions: MessageActions<'_>, + show_delete: bool, highlight_message_id: Option<&str>, ) -> Markup { let messages = response_messages(result); @@ -286,7 +280,7 @@ pub fn browse_messages_fragment( config, &config.base_path, &messages, - actions, + show_delete, highlight_message_id, )) } @@ -297,7 +291,7 @@ fn browse_result_card( config: &AdminConfig, result: &Value, channel_id: &str, - actions: MessageActions<'_>, + show_delete: bool, csrf_token: &str, context_limit: u32, ) -> Markup { @@ -334,7 +328,7 @@ fn browse_result_card( config, channel_id, messages: &messages, - actions, + show_delete, has_more, has_newer: false, focus_message_id: None, @@ -349,7 +343,7 @@ fn search_result_card( result: &Value, channel_id: &str, query_text: &str, - actions: MessageActions<'_>, + show_delete: bool, ) -> Markup { let messages = response_messages(result); let total = result @@ -375,7 +369,7 @@ fn search_result_card( config, &config.base_path, &messages, - actions, + show_delete, None, )) } @@ -385,7 +379,7 @@ fn search_result_card( fn lookup_result_card( config: &AdminConfig, result: &Value, - actions: MessageActions<'_>, + show_delete: bool, context_limit: u32, ) -> Markup { let messages = response_messages(result); @@ -407,7 +401,7 @@ fn lookup_result_card( config, channel_id, messages: &messages, - actions, + show_delete, has_more: true, has_newer: true, focus_message_id: message_id, @@ -421,7 +415,7 @@ struct MessageScrollPane<'a> { config: &'a AdminConfig, channel_id: &'a str, messages: &'a [Message], - actions: MessageActions<'a>, + show_delete: bool, has_more: bool, has_newer: bool, focus_message_id: Option<&'a str>, @@ -460,7 +454,7 @@ fn message_scroll_pane(pane: MessageScrollPane<'_>) -> Markup { pane.config, &pane.config.base_path, pane.messages, - pane.actions, + pane.show_delete, pane.focus_message_id, )) } diff --git a/fluxer_admin/src/templates/pages/report_detail.rs b/fluxer_admin/src/templates/pages/report_detail.rs index 175851bc0..cff3257b0 100644 --- a/fluxer_admin/src/templates/pages/report_detail.rs +++ b/fluxer_admin/src/templates/pages/report_detail.rs @@ -18,7 +18,7 @@ use crate::{ form::{FORM_CONTROL_CLASS, FORM_LABEL_CLASS, csrf_input, opt_out_checkbox}, media::{guild_icon_url, initials, user_avatar_url}, message_data::ordered_messages, - message_list::{MessageActions, message_deletion_script, message_list}, + message_list::{message_deletion_script, message_list}, nsfw_indicators::{ adult_content_badge, channel_nsfw_state_badge, content_warning_badge, }, @@ -634,10 +634,14 @@ fn padded_discriminator(value: Option<&str>) -> Option { non_empty(value).map(|value| format!("{value:0>4}")) } -fn snapshot_tag(username: Option<&str>, discriminator: Option<&str>) -> Option { +fn snapshot_tag( + username: Option<&str>, + discriminator: Option<&str>, + is_bot: bool, +) -> Option { let username = non_empty(username)?; Some(match padded_discriminator(discriminator) { - Some(discriminator) => format!("{username}#{discriminator}"), + Some(discriminator) => user_tag(username, &discriminator, is_bot), None => username.to_owned(), }) } @@ -759,11 +763,16 @@ fn snapshot_user_group( } else { None }; - let tag = snapshot_tag(user.username.as_deref(), user.discriminator.as_deref()); + let is_bot = live_user.is_some_and(|live| live.bot); + let tag = snapshot_tag( + user.username.as_deref(), + user.discriminator.as_deref(), + is_bot, + ); let tag_change = match live_user { None => SnapshotChange::Unknown, Some(live) => { - let live_tag = snapshot_tag(Some(&live.username), Some(&live.discriminator)); + let live_tag = snapshot_tag(Some(&live.username), Some(&live.discriminator), is_bot); if live_tag == tag { SnapshotChange::Same } else { @@ -997,7 +1006,7 @@ fn nav_link(href: &str, label: &str) -> Markup { fn message_context_section( config: &AdminConfig, report: &ReportEntry, - actions: MessageActions<'_>, + include_delete: bool, csrf_token: Option<&str>, ) -> Markup { let Some(values) = &report.message_context else { @@ -1020,7 +1029,7 @@ fn message_context_section( config, &config.base_path, &messages, - actions, + include_delete, report.reported_message_id.as_deref(), )) } @@ -1031,16 +1040,6 @@ fn message_context_section( } } -fn report_message_actions<'a>(auth: &AuthContext, report: &'a ReportEntry) -> MessageActions<'a> { - MessageActions { - delete: has_acl(auth, acl::MESSAGE_DELETE), - ncmec: auth.admin_user.as_ref().is_some_and(|admin| { - acl::has_all_permissions(&admin.acls, acl::NCMEC_ATTACHMENT_REPORT) - }), - source_report_id: Some(&report.report_id), - } -} - pub fn report_detail_page( config: &AdminConfig, auth: &AuthContext, @@ -1064,7 +1063,7 @@ pub fn report_detail_page( (report_answers_section(report)) (reported_entity_section(config, report)) (profile_snapshot_section(config, report, live)) - (message_context_section(config, report, report_message_actions(auth, report), Some(csrf_token))) + (message_context_section(config, report, has_acl(auth, acl::MESSAGE_DELETE), Some(csrf_token))) (additional_info_section(report)) } div class="space-y-6" { @@ -1089,7 +1088,7 @@ pub fn report_detail_fragment(config: &AdminConfig, report: &ReportEntry) -> Mar (basic_info_section_fragment(config, report)) (report_answers_section(report)) (reported_entity_section(config, report)) - (message_context_section(config, report, MessageActions::default(), None)) + (message_context_section(config, report, false, None)) (additional_info_section(report)) a href={(base) "/reports/" (&report.report_id)} class="inline-flex min-h-[44px] w-full items-center justify-center \ @@ -1608,9 +1607,7 @@ mod tests { #[test] fn message_context_shows_webhook_authors_without_user_links() { let config = test_config(); - let markup = - message_context_section(&config, &webhook_report(), MessageActions::default(), None) - .into_string(); + let markup = message_context_section(&config, &webhook_report(), false, None).into_string(); assert!( markup.contains(r#"data-message-webhook="1700000000000000500""#), "{markup}" @@ -1668,7 +1665,6 @@ mod tests { })) } - const NCMEC_BUTTON: &str = r#"class="ncmec-report-btn"#; const DELETE_BUTTON: &str = r#"class="delete-message-btn"#; fn render_page(auth: &AuthContext, report: &ReportEntry) -> String { @@ -1684,65 +1680,21 @@ mod tests { } #[test] - fn ncmec_button_sends_the_report_id_when_the_admin_has_the_acls() { - let auth = auth_with_acls(acl::NCMEC_ATTACHMENT_REPORT); - let markup = render_page(&auth, &image_report("child_safety")); - assert!(markup.contains(NCMEC_BUTTON), "{markup}"); - assert!( - markup.contains(r#"data-source-report-id="1800000000000000009""#), - "{markup}" - ); - assert!( - markup.contains("fields.append('source_report_id', b.dataset.sourceReportId)"), - "{markup}" - ); - assert!(markup.contains(DELETE_BUTTON), "{markup}"); - } - - #[test] - fn ncmec_button_is_hidden_without_the_acls() { + fn message_delete_button_follows_the_acl() { let auth = auth_with_acls(&[acl::REPORT_VIEW, acl::MESSAGE_DELETE]); let markup = render_page(&auth, &image_report("child_safety")); assert!(markup.contains("image.png"), "{markup}"); - assert!(!markup.contains(NCMEC_BUTTON), "{markup}"); - assert!(!markup.contains("data-source-report-id"), "{markup}"); assert!(markup.contains(DELETE_BUTTON), "{markup}"); - for missing in acl::NCMEC_ATTACHMENT_REPORT { - let held = acl::NCMEC_ATTACHMENT_REPORT - .iter() - .copied() - .filter(|acl| acl != missing) - .collect::>(); - let markup = render_page(&auth_with_acls(&held), &image_report("child_safety")); - assert!(!markup.contains(NCMEC_BUTTON), "{missing}\n{markup}"); - assert_eq!( - markup.contains(DELETE_BUTTON), - *missing != acl::MESSAGE_DELETE, - "{missing}\n{markup}" - ); - } + let markup = render_page(&auth_with_acls(&[acl::WILDCARD]), &image_report("spam")); + assert!(markup.contains(DELETE_BUTTON), "{markup}"); let markup = render_page(&auth_with_acls(&[acl::REPORT_VIEW]), &image_report("spam")); - assert!(!markup.contains(NCMEC_BUTTON), "{markup}"); + assert!(markup.contains("image.png"), "{markup}"); assert!(!markup.contains(DELETE_BUTTON), "{markup}"); let markup = render_page(&test_auth(), &image_report("child_safety")); - assert!(!markup.contains(NCMEC_BUTTON), "{markup}"); - } - - #[test] - fn ncmec_button_follows_the_acls_not_the_report_category() { - let auth = auth_with_acls(acl::NCMEC_ATTACHMENT_REPORT); - let markup = render_page(&auth, &image_report("spam")); - assert!(markup.contains(NCMEC_BUTTON), "{markup}"); - assert!( - markup.contains(r#"data-source-report-id="1800000000000000009""#), - "{markup}" - ); - - let markup = render_page(&auth_with_acls(&[acl::WILDCARD]), &image_report("spam")); - assert!(markup.contains(NCMEC_BUTTON), "{markup}"); + assert!(!markup.contains(DELETE_BUTTON), "{markup}"); } #[test] @@ -1750,7 +1702,6 @@ mod tests { let markup = report_detail_fragment(&test_config(), &image_report("child_safety")).into_string(); assert!(markup.contains("image.png"), "{markup}"); - assert!(!markup.contains(NCMEC_BUTTON), "{markup}"); assert!(!markup.contains(DELETE_BUTTON), "{markup}"); } @@ -1903,13 +1854,8 @@ mod tests { #[test] fn message_context_marks_bot_authors_only() { - let markup = message_context_section( - &test_config(), - &context_report(), - MessageActions::default(), - None, - ) - .into_string(); + let markup = + message_context_section(&test_config(), &context_report(), false, None).into_string(); assert!( markup.contains(r#"data-message-author-bot="1700000000000000900""#), "{markup}" @@ -1952,9 +1898,7 @@ mod tests { message("1700000000000000590", "Joined", "2026-10-05T16:45:00.000Z") ] })); - let markup = - message_context_section(&test_config(), &grouped, MessageActions::default(), None) - .into_string(); + let markup = message_context_section(&test_config(), &grouped, false, None).into_string(); assert!(markup.contains("Oct 5, 2026, 4:45 PM UTC"), "{markup}"); assert!(!markup.contains("2026-10-05T16:4"), "{markup}"); assert!( @@ -1973,13 +1917,8 @@ mod tests { #[test] fn message_context_names_attachments_that_were_not_preserved() { - let markup = message_context_section( - &test_config(), - &context_report(), - MessageActions::default(), - None, - ) - .into_string(); + let markup = + message_context_section(&test_config(), &context_report(), false, None).into_string(); assert!( markup.contains(r#"data-missing-attachment="1700000000000000610""#), "{markup}" @@ -1998,13 +1937,8 @@ mod tests { fragment.contains("harbor-notes.pdf was not preserved in the report snapshot"), "{fragment}" ); - let markup = message_context_section( - &test_config(), - &image_report("spam"), - MessageActions::default(), - None, - ) - .into_string(); + let markup = message_context_section(&test_config(), &image_report("spam"), false, None) + .into_string(); assert!(!markup.contains("data-missing-attachment"), "{markup}"); assert!(!markup.contains("data-message-author-bot"), "{markup}"); } diff --git a/fluxer_admin/tests/api_deserialization.rs b/fluxer_admin/tests/api_deserialization.rs index 8ca8bba7a..5739a2de0 100644 --- a/fluxer_admin/tests/api_deserialization.rs +++ b/fluxer_admin/tests/api_deserialization.rs @@ -957,8 +957,7 @@ fn deserialize_webhook_message_report() { "author_global_name": null, "author_discriminator": "0000", "author_avatar": "a_1f2e3d", - "webhook_id": "1556008115710000001", - "user_prior_ncmec_report_ids": [] + "webhook_id": "1556008115710000001" }, { "id": "1556008115697091780", @@ -974,8 +973,7 @@ fn deserialize_webhook_message_report() { "author_global_name": null, "author_discriminator": "6741", "author_avatar": null, - "webhook_id": null, - "user_prior_ncmec_report_ids": [] + "webhook_id": null } ] })); @@ -1164,8 +1162,7 @@ fn evidence_report_json() -> serde_json::Value { "width": 640, "height": 480, "size": 4096 - }], - "user_prior_ncmec_report_ids": [] + }] }, { "id": "1556008115697091780", @@ -1183,8 +1180,7 @@ fn evidence_report_json() -> serde_json::Value { "author_avatar": null, "webhook_id": null, "author_bot": false, - "missing_attachments": [], - "user_prior_ncmec_report_ids": [] + "missing_attachments": [] } ] })) @@ -1242,6 +1238,35 @@ fn deserialize_report_with_bot_flags_profile_snapshot_and_missing_attachments() } } +#[test] +fn deserialize_report_context_with_unknown_message_and_attachment_keys() { + let mut json = evidence_report_json(); + json["message_context"][1]["retired_list"] = serde_json::json!([]); + json["message_context"][1]["attachments"] = serde_json::json!([{ + "id": "1556008115697091791", + "url": "https://reports.example.test/attachments/image.png?sig=1", + "filename": "image.png", + "nsfw": false, + "content_type": "image/png", + "width": 64, + "height": 64, + "size": 4096, + "retired_status": "not_submitted", + "retired_id": null + }]); + let generated: generated_types::ReportAdminResponseSchema = + serde_json::from_value(json).expect("generated report type"); + let report: types::ReportEntry = + serde_json::from_value(serde_json::to_value(generated).expect("serialize generated")) + .expect("hand-written report type from generated"); + let context = report.message_context.as_ref().expect("message context"); + assert_eq!(context[1]["attachments"][0]["filename"], "image.png"); + assert_eq!( + context[1]["attachments"][0]["url"], + "https://reports.example.test/attachments/image.png?sig=1" + ); +} + #[test] fn deserialize_report_without_bot_flags_snapshot_or_hold() { let nulls = report_json(serde_json::json!({ diff --git a/fluxer_admin/tests/htmx_acceptance.rs b/fluxer_admin/tests/htmx_acceptance.rs index 311563156..c161a8c2b 100644 --- a/fluxer_admin/tests/htmx_acceptance.rs +++ b/fluxer_admin/tests/htmx_acceptance.rs @@ -429,79 +429,13 @@ async fn report_detail_renders_unknown_status_and_type_values() { } #[tokio::test] -async fn ncmec_submissions_forward_the_source_report_id() { - let received = Arc::new(Mutex::new(Vec::::new())); - let sink = received.clone(); - let api = Router::new() - .route( - "/admin/messages/ncmec-reports", - routing::post(move |Json(body): Json| { - let sink = sink.clone(); - async move { - sink.lock().unwrap().push(body); - json_response(json!({ - "success": true, - "ncmec_report_id": "ncmec-1", - "audit_log_reason": "Submitted" - })) - } - }), - ) - .fallback(mock_api); - let app = setup_with_api(api).await; - let (headers, body) = get_with_headers(&app, "/messages", &[]).await; - let csrf_token = csrf_cookie(&headers) - .unwrap_or_else(|| panic!("messages page did not set csrf_token cookie\n{body}")); - let cookie = format!("{}; csrf_token={}", app.session_cookie, csrf_token); - let fields = "channel_id=1600000000000000101&message_id=1800000000000001001\ - &attachment_id=1800000000000001002&filename=image.png\ - &reporter_full_name=Casey%20Admin&confirmed_viewed=true"; - for form in [ - format!("_csrf={csrf_token}&{fields}&source_report_id=1800000000000000009"), - format!("_csrf={csrf_token}&{fields}"), - ] { - let (status, _, response) = post_form_with_headers( - &app, - "/messages?action=report-to-ncmec", - &[("Cookie", &cookie), ("x-csrf-token", &csrf_token)], - &form, - ) - .await; - assert_eq!(status, StatusCode::OK, "{response}"); - assert!(response.contains("ncmec-1"), "{response}"); - } - let received = received.lock().unwrap(); - assert_eq!(received.len(), 2, "{received:?}"); - assert_eq!(received[0]["source_report_id"], "1800000000000000009"); - assert_eq!(received[0]["attachment_id"], "1800000000000001002"); - assert_eq!(received[0]["confirmed_viewed"], true); - assert!( - received[1] - .get("source_report_id") - .is_none_or(Value::is_null), - "{received:?}" - ); -} - -#[tokio::test] -async fn message_tools_offer_ncmec_only_with_every_required_acl() { - let full = fluxer_admin::acl::NCMEC_ATTACHMENT_REPORT; - let cases: [(&[&str], bool, bool); 4] = [ - (full, true, true), - (&["*"], true, true), - (&["csam:submit_ncmec", "message:lookup"], false, false), - ( - &[ - "csam:submit_ncmec", - "message:delete", - "message:lookup", - "user:delete", - ], - false, - true, - ), +async fn message_tools_offer_delete_only_with_the_acl() { + let cases: [(&[&str], bool); 3] = [ + (&["*"], true), + (&["message:lookup"], false), + (&["message:delete", "message:lookup"], true), ]; - for (acls, ncmec, delete) in cases { + for (acls, delete) in cases { let app = setup_with_api(message_tools_api(acls)).await; for path in [ "/messages?channel_id=1600000000000000101&message_id=1800000000000001001", @@ -509,20 +443,11 @@ async fn message_tools_offer_ncmec_only_with_every_required_acl() { ] { let body = get(&app, path, &[]).await; assert!(body.contains("tools-image.png"), "{acls:?} {path}\n{body}"); - assert_eq!( - body.contains(r#"class="ncmec-report-btn"#), - ncmec, - "{acls:?} {path}\n{body}" - ); assert_eq!( body.contains(r#"class="delete-message-btn"#), delete, "{acls:?} {path}\n{body}" ); - assert!( - !body.contains("data-source-report-id"), - "{acls:?} {path}\n{body}" - ); } } } @@ -682,8 +607,7 @@ fn evidence_report() -> Value { "width": 640, "height": 480, "size": 4096 - }], - "user_prior_ncmec_report_ids": [] + }] }, { "id": "1800000000000000999", @@ -701,8 +625,7 @@ fn evidence_report() -> Value { "author_avatar": null, "webhook_id": null, "author_bot": false, - "missing_attachments": [], - "user_prior_ncmec_report_ids": [] + "missing_attachments": [] } ] }) @@ -1890,10 +1813,7 @@ fn message_tools_api(acls: &[&str]) -> Router { "content_type": "image/png", "width": 64, "height": 64, - "size": 4096, - "ncmec_status": "not_submitted", - "ncmec_report_id": null, - "ncmec_failure_reason": null + "size": 4096 }] }); let lookup = json!({"messages": [message.clone()], "message_id": "1800000000000001001"}); diff --git a/fluxer_admin/tests/parity/fixtures/api/report_evidence_detail.json b/fluxer_admin/tests/parity/fixtures/api/report_evidence_detail.json index 88d9aeaff..9e4e9fab4 100644 --- a/fluxer_admin/tests/parity/fixtures/api/report_evidence_detail.json +++ b/fluxer_admin/tests/parity/fixtures/api/report_evidence_detail.json @@ -80,8 +80,7 @@ "author_avatar": null, "webhook_id": null, "author_bot": false, - "missing_attachments": [], - "user_prior_ncmec_report_ids": [] + "missing_attachments": [] }, { "id": "1556352159119835293", @@ -104,8 +103,7 @@ "author_avatar": null, "webhook_id": null, "author_bot": false, - "missing_attachments": [], - "user_prior_ncmec_report_ids": [] + "missing_attachments": [] }, { "id": "1556352159124029598", @@ -128,8 +126,7 @@ "author_avatar": "f829b914", "webhook_id": null, "author_bot": false, - "missing_attachments": [], - "user_prior_ncmec_report_ids": [] + "missing_attachments": [] }, { "id": "1556352159128223903", @@ -152,8 +149,7 @@ "author_avatar": "f829b914", "webhook_id": null, "author_bot": false, - "missing_attachments": [], - "user_prior_ncmec_report_ids": [] + "missing_attachments": [] }, { "id": "1556352159140806818", @@ -176,8 +172,7 @@ "author_avatar": null, "webhook_id": "1556352159132418208", "author_bot": null, - "missing_attachments": [], - "user_prior_ncmec_report_ids": [] + "missing_attachments": [] }, { "id": "1556352159153389733", @@ -200,8 +195,7 @@ "author_avatar": null, "webhook_id": null, "author_bot": true, - "missing_attachments": [], - "user_prior_ncmec_report_ids": [] + "missing_attachments": [] }, { "id": "1556352159161778343", @@ -224,8 +218,7 @@ "author_avatar": null, "webhook_id": null, "author_bot": true, - "missing_attachments": [], - "user_prior_ncmec_report_ids": [] + "missing_attachments": [] }, { "id": "1556352159212110003", @@ -248,8 +241,7 @@ "author_avatar": null, "webhook_id": "1556352159203721393", "author_bot": null, - "missing_attachments": [], - "user_prior_ncmec_report_ids": [] + "missing_attachments": [] }, { "id": "1556352159186944173", @@ -272,8 +264,7 @@ "author_avatar": null, "webhook_id": null, "author_bot": null, - "missing_attachments": [], - "user_prior_ncmec_report_ids": [] + "missing_attachments": [] } ], "message_responses": [ diff --git a/fluxer_admin/tests/parity/fixtures/api/report_webhook_detail.json b/fluxer_admin/tests/parity/fixtures/api/report_webhook_detail.json index 5cef9e859..0111e34bc 100644 --- a/fluxer_admin/tests/parity/fixtures/api/report_webhook_detail.json +++ b/fluxer_admin/tests/parity/fixtures/api/report_webhook_detail.json @@ -77,8 +77,7 @@ "author_global_name": null, "author_discriminator": "7915", "author_avatar": null, - "webhook_id": null, - "user_prior_ncmec_report_ids": [] + "webhook_id": null }, { "id": "1556114449163617486", @@ -99,8 +98,7 @@ "author_global_name": null, "author_discriminator": "0943", "author_avatar": null, - "webhook_id": null, - "user_prior_ncmec_report_ids": [] + "webhook_id": null }, { "id": "1556114449167811791", @@ -121,8 +119,7 @@ "author_global_name": null, "author_discriminator": "9014", "author_avatar": "f829b914", - "webhook_id": null, - "user_prior_ncmec_report_ids": [] + "webhook_id": null }, { "id": "1556114449172006096", @@ -143,8 +140,7 @@ "author_global_name": null, "author_discriminator": "9014", "author_avatar": "f829b914", - "webhook_id": null, - "user_prior_ncmec_report_ids": [] + "webhook_id": null }, { "id": "1556114449184589011", @@ -165,8 +161,7 @@ "author_global_name": null, "author_discriminator": "0000", "author_avatar": null, - "webhook_id": "1556114449176200401", - "user_prior_ncmec_report_ids": [] + "webhook_id": "1556114449176200401" }, { "id": "1556114449255892196", @@ -187,8 +182,7 @@ "author_global_name": null, "author_discriminator": "0000", "author_avatar": null, - "webhook_id": "1556114449247503586", - "user_prior_ncmec_report_ids": [] + "webhook_id": "1556114449247503586" }, { "id": "1556114449230726366", @@ -209,8 +203,7 @@ "author_global_name": null, "author_discriminator": "0000", "author_avatar": null, - "webhook_id": null, - "user_prior_ncmec_report_ids": [] + "webhook_id": null }, { "id": "1556114449205560536", @@ -231,8 +224,7 @@ "author_global_name": null, "author_discriminator": "1238", "author_avatar": null, - "webhook_id": null, - "user_prior_ncmec_report_ids": [] + "webhook_id": null }, { "id": "1556114449197171926", @@ -253,8 +245,7 @@ "author_global_name": null, "author_discriminator": "1238", "author_avatar": null, - "webhook_id": null, - "user_prior_ncmec_report_ids": [] + "webhook_id": null } ], "message_responses": [ diff --git a/fluxer_api/package.json b/fluxer_api/package.json index 491965393..9c94f9158 100644 --- a/fluxer_api/package.json +++ b/fluxer_api/package.json @@ -61,7 +61,6 @@ "bowser": "catalog:", "cassandra-driver": "catalog:", "emoji-regex": "catalog:", - "fast-xml-parser": "catalog:", "hi-base32": "catalog:", "hono": "catalog:", "html-entities": "catalog:", diff --git a/fluxer_api/src/api/Config.ts b/fluxer_api/src/api/Config.ts index 6181d04c6..24da902c9 100644 --- a/fluxer_api/src/api/Config.ts +++ b/fluxer_api/src/api/Config.ts @@ -383,13 +383,6 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig { port: master.integrations.clamav.port, failOpen: master.integrations.clamav.fail_open, }, - ncmec: { - enabled: master.integrations.ncmec.enabled, - baseUrl: master.integrations.ncmec.base_url, - username: master.integrations.ncmec.username, - password: master.integrations.ncmec.password, - reporterEmail: master.integrations.ncmec.reporter_email ?? '', - }, admin: { oauthClientSecret: master.services.admin.oauth_client_secret, }, diff --git a/fluxer_api/src/api/Tables.ts b/fluxer_api/src/api/Tables.ts index 9d2c8930d..1f474193f 100644 --- a/fluxer_api/src/api/Tables.ts +++ b/fluxer_api/src/api/Tables.ts @@ -149,12 +149,6 @@ import { type WebhooksBySourceChannelRow, } from '@app/api/database/types/ChannelTypes'; import {USER_CONNECTION_STORAGE_COLUMNS, type UserConnectionStorageRow} from '@app/api/database/types/ConnectionTypes'; -import { - NCMEC_ATTACHMENT_SUBMISSION_COLUMNS, - NCMEC_USER_WORKFLOW_COLUMNS, - type NcmecAttachmentSubmissionRow, - type NcmecUserWorkflowRow, -} from '@app/api/database/types/CsamTypes'; import { GUILD_DISCOVERY_BY_STATUS_COLUMNS, GUILD_DISCOVERY_COLUMNS, @@ -1294,16 +1288,6 @@ export const AttachmentUploadTracesByAttachment = defineTable({ - name: 'ncmec_attachment_submissions', - columns: NCMEC_ATTACHMENT_SUBMISSION_COLUMNS, - primaryKey: ['attachment_id'], -}); -export const NcmecUserWorkflows = defineTable({ - name: 'ncmec_user_workflows', - columns: NCMEC_USER_WORKFLOW_COLUMNS, - primaryKey: ['user_id'], -}); export const BillingCustomers = defineTable({ name: 'billing_customers', columns: BILLING_CUSTOMER_COLUMNS, diff --git a/fluxer_api/src/api/admin/AdminService.ts b/fluxer_api/src/api/admin/AdminService.ts index ebcd1319b..194da8cec 100644 --- a/fluxer_api/src/api/admin/AdminService.ts +++ b/fluxer_api/src/api/admin/AdminService.ts @@ -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({ diff --git a/fluxer_api/src/api/admin/controllers/MessageAdminController.ts b/fluxer_api/src/api/admin/controllers/MessageAdminController.ts index cdf4e917c..6009fea0d 100644 --- a/fluxer_api/src/api/admin/controllers/MessageAdminController.ts +++ b/fluxer_api/src/api/admin/controllers/MessageAdminController.ts @@ -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), diff --git a/fluxer_api/src/api/admin/services/AdminMessageService.ts b/fluxer_api/src/api/admin/services/AdminMessageService.ts index 1a07a86c8..b9aa34fc0 100644 --- a/fluxer_api/src/api/admin/services/AdminMessageService.ts +++ b/fluxer_api/src/api/admin/services/AdminMessageService.ts @@ -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, - ): Promise> { - 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 { 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): Promise>> { - const authorIds = messages.map((message) => createUserID(BigInt(message.author.id))); - return this.deps.ncmecSubmissionService.getUserPriorReportIds(authorIds); - } - - private mapMessageResponseToAdminMessage( - message: MessageResponse, - channelNsfwContext: ChannelNsfwContext, - attachmentStatuses: Map, - priorReports: Map>, - ) { + 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, })) ?? [], }; } diff --git a/fluxer_api/src/api/admin/services/AdminReportService.ts b/fluxer_api/src/api/admin/services/AdminReportService.ts index e85841f95..5c1b50b00 100644 --- a/fluxer_api/src/api/admin/services/AdminReportService.ts +++ b/fluxer_api/src/api/admin/services/AdminReportService.ts @@ -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, - priorReportsByAuthor: Map>, authorBotFlags: Map, ) { 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>> { - const authorIds = new Set(); - 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, ): 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 { if (!userId) { return null; diff --git a/fluxer_api/src/api/admin/tests/AdminAuditAccessFilter.test.ts b/fluxer_api/src/api/admin/tests/AdminAuditAccessFilter.test.ts index 12333bb59..678024cd5 100644 --- a/fluxer_api/src/api/admin/tests/AdminAuditAccessFilter.test.ts +++ b/fluxer_api/src/api/admin/tests/AdminAuditAccessFilter.test.ts @@ -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) { diff --git a/fluxer_api/src/api/admin/tests/audit_coverage/MessageAdminAuditCases.ts b/fluxer_api/src/api/admin/tests/audit_coverage/MessageAdminAuditCases.ts index c2fb6d75a..a3f413e2e 100644 --- a/fluxer_api/src/api/admin/tests/audit_coverage/MessageAdminAuditCases.ts +++ b/fluxer_api/src/api/admin/tests/audit_coverage/MessageAdminAuditCases.ts @@ -116,47 +116,6 @@ export const MessageAdminAuditCases: ReadonlyArray = [ }; }, }, - { - 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', diff --git a/fluxer_api/src/api/channel/repositories/message/AttachmentUploadTraceRepository.ts b/fluxer_api/src/api/channel/repositories/message/AttachmentUploadTraceRepository.ts index 6ed594e60..3f0c65c99 100644 --- a/fluxer_api/src/api/channel/repositories/message/AttachmentUploadTraceRepository.ts +++ b/fluxer_api/src/api/channel/repositories/message/AttachmentUploadTraceRepository.ts @@ -12,10 +12,6 @@ const GET_UPLOAD_TRACE_BY_KEY_QUERY = AttachmentUploadTracesByKey.select({ where: AttachmentUploadTracesByKey.where.eq('upload_key'), limit: 1, }); -const GET_UPLOAD_TRACE_BY_ATTACHMENT_QUERY = AttachmentUploadTracesByAttachment.select({ - where: AttachmentUploadTracesByAttachment.where.eq('attachment_id'), - limit: 1, -}); export type AttachmentUploadMode = 'form_data' | 'presigned_singlepart' | 'presigned_multipart'; @@ -48,12 +44,6 @@ export class AttachmentUploadTraceRepository { return await fetchOne(GET_UPLOAD_TRACE_BY_KEY_QUERY.bind({upload_key: uploadKey})); } - async getByAttachmentId(attachmentId: AttachmentID): Promise { - return await fetchOne( - GET_UPLOAD_TRACE_BY_ATTACHMENT_QUERY.bind({attachment_id: BigInt(attachmentId)}), - ); - } - async getPendingUpload(input: GetPendingAttachmentUploadInput): Promise { const existing = await this.getByUploadKey(input.uploadKey); if ( diff --git a/fluxer_api/src/api/channel/tests/CrosspostModeration.test.ts b/fluxer_api/src/api/channel/tests/CrosspostModeration.test.ts index 1cd053779..d5b61bcc7 100644 --- a/fluxer_api/src/api/channel/tests/CrosspostModeration.test.ts +++ b/fluxer_api/src/api/channel/tests/CrosspostModeration.test.ts @@ -1,16 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createTestAccount, setUserACLs, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; -import { - type ChannelID, - createAttachmentID, - createChannelID, - createGuildID, - createMessageID, - createUserID, - type MessageID, - type UserID, -} from '@app/api/BrandedTypes'; +import {createChannelID, createGuildID, createMessageID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {CrosspostTaskNames} from '@app/api/channel/services/message/CrosspostPropagation'; @@ -26,7 +17,6 @@ import { import {loadFixture, sendMessageWithAttachments} from '@app/api/channel/tests/AttachmentTestUtils'; import {sendChannelMessage} from '@app/api/channel/tests/ChannelTestUtils'; import {readMessageRow, writeMessageRow} from '@app/api/channel/tests/CrosspostTestUtils'; -import {NcmecRepository} from '@app/api/csam/NcmecRepository'; import type {MessageEmbed} from '@app/api/database/types/MessageTypes'; import type {EmbedService} from '@app/api/infrastructure/EmbedService'; import { @@ -38,7 +28,6 @@ import { import { getCacheService, getChannelRepository, - getNcmecSubmissionService, getPurgeQueue, getStorageService, } from '@app/api/middleware/ServiceSingletons'; @@ -157,11 +146,6 @@ class CrosspostQueueWorker implements IWorkerService { } } -interface FabricatedCopy { - copyId: string; - attachment: {id: string; filename: string} | null; -} - async function fabricateCopy(params: { harness: ApiTestHarness; owner: TestAccount; @@ -169,8 +153,7 @@ async function fabricateCopy(params: { world: AnnouncementWorld; sourceId: string; extraFlags?: number; - withImage?: boolean; -}): Promise { +}): Promise<{copyId: string}> { const webhook = await createWebhook(params.harness, params.target, params.owner.token, 'Follower'); const {json: message} = await executeWebhook( params.harness, @@ -179,14 +162,10 @@ async function fabricateCopy(params: { {content: 'copied update', wait: true}, 200, ); - const sourceAttachments = params.withImage - ? (await readMessageRow(params.world.a.ann.id, params.sourceId))!.attachments - : []; const row = await readMessageRow(params.target, message!.id); await writeMessageRow(row!, { flags: MessageFlags.IS_CROSSPOST | (params.extraFlags ?? 0), - attachments: - sourceAttachments.length > 0 ? sourceAttachments.map((attachment) => attachment.toMessageAttachment()) : null, + attachments: null, message_reference: { channel_id: createChannelID(BigInt(params.world.a.ann.id)), guild_id: createGuildID(BigInt(params.world.a.guild.id)), @@ -194,11 +173,7 @@ async function fabricateCopy(params: { type: MessageReferenceTypes.DEFAULT, }, }); - const attachment = sourceAttachments[0]; - return { - copyId: message!.id, - attachment: attachment ? {id: attachment.id.toString(), filename: attachment.filename} : null, - }; + return {copyId: message!.id}; } async function createAdmin(harness: ApiTestHarness): Promise { @@ -206,9 +181,6 @@ async function createAdmin(harness: ApiTestHarness): Promise { AdminACLs.AUTHENTICATE, AdminACLs.MESSAGE_DELETE, AdminACLs.MESSAGE_SHRED, - AdminACLs.CSAM_SUBMIT_NCMEC, - AdminACLs.USER_DELETE, - AdminACLs.ARCHIVE_TRIGGER_USER, ]); } @@ -219,17 +191,6 @@ async function adminDeleteMessage(harness: ApiTestHarness, admin: TestAccount, c .execute(); } -async function deleteMessageSilently(channelId: string, messageId: string, fallbackUserId: string): Promise { - const service = getNcmecSubmissionService() as unknown as { - deleteMessageSilently(channelId: ChannelID, messageId: MessageID, fallbackUserId: UserID): Promise; - }; - await service.deleteMessageSilently( - createChannelID(BigInt(channelId)), - createMessageID(BigInt(messageId)), - createUserID(BigInt(fallbackUserId)), - ); -} - function blockedEmbedService(): EmbedService { return { processUrlWithCachePolicy: async (url: string) => ({ @@ -331,73 +292,6 @@ describe('Crosspost moderation', () => { expect(jobs[0]!.payload).toEqual({channelId: world.a.ann.id, messageId: source.id, mode: 'purge'}); }); - test('an NCMEC report on a copy records the source author and its takedown purges the family', async () => { - const {json: source} = await sendMessageWithAttachments( - harness, - world.a.member.token, - world.a.ann.id, - {content: 'update', attachments: [{id: 0, filename: 'yeah.png'}]}, - [{index: 0, filename: 'yeah.png', data: loadFixture('yeah.png')}], - ); - const {copyId, attachment} = await fabricateCopy({ - harness, - owner: world.b.owner, - target: world.b.t1.id, - world, - sourceId: source.id, - withImage: true, - }); - expect(attachment).not.toBeNull(); - const admin = await createAdmin(harness); - await createBuilder(harness, admin.token) - .post('/admin/messages/ncmec-reports') - .body({ - channel_id: world.b.t1.id, - message_id: copyId, - attachment_id: attachment!.id, - filename: attachment!.filename, - reporter_full_name: 'Crosspost Reporter', - confirmed_viewed: true, - }) - .expect(HTTP_STATUS.OK) - .execute(); - const submission = await new NcmecRepository().getAttachmentSubmission( - createAttachmentID(BigInt(attachment!.id)), - ); - expect(submission?.user_id?.toString()).toBe(world.a.member.userId); - expect(submission?.message_id.toString()).toBe(copyId); - expect(submission?.channel_id.toString()).toBe(world.b.t1.id); - worker.clear(); - const deletedBefore = deletedKeys().length; - await deleteMessageSilently(world.b.t1.id, copyId, world.a.member.userId); - expect(await readMessageRow(world.b.t1.id, copyId)).toBeNull(); - expect(deletedKeys().slice(deletedBefore)).toEqual([]); - expect( - harness.storageService.hasObject( - Config.s3.buckets.cdn, - `attachments/${world.a.ann.id}/${attachment!.id}/${attachment!.filename}`, - ), - ).toBe(true); - const jobs = worker.syncJobs(); - expect(jobs).toHaveLength(1); - expect(jobs[0]!.payload).toEqual({ - channelId: world.a.ann.id, - messageId: source.id, - mode: 'purge', - deleteSource: true, - }); - }); - - test('an NCMEC takedown of a published source purges its copies', async () => { - const source = await sendChannelMessage(harness, world.a.member.token, world.a.ann.id, 'update'); - await publish(harness, world.a.member.token, world.a.ann.id, source.id); - worker.clear(); - await deleteMessageSilently(world.a.ann.id, source.id, world.a.member.userId); - const jobs = worker.syncJobs(); - expect(jobs).toHaveLength(1); - expect(jobs[0]!.payload).toEqual({channelId: world.a.ann.id, messageId: source.id, mode: 'purge'}); - }); - test('a message shred of a published source purges its copies', async () => { const source = await sendChannelMessage(harness, world.a.member.token, world.a.ann.id, 'update'); await publish(harness, world.a.member.token, world.a.ann.id, source.id); @@ -585,35 +479,6 @@ describe('Crosspost moderation', () => { expectNoTargetObjectsDeleted(); }); - test('an NCMEC takedown of a delivered copy records the source author and removes the family', async () => { - const sourceId = await publishAndDeliver('image update', true); - const sourceKey = await sourceAttachmentKey(sourceId); - const [copyId] = await listCopies(c.t1.id, sourceId); - const copy = await readMessageRow(c.t1.id, copyId!); - const attachment = copy!.attachments[0]!; - expect(`attachments/${world.a.ann.id}/${attachment.id}/${attachment.filename}`).toBe(sourceKey); - const admin = await createAdmin(harness); - await createBuilder(harness, admin.token) - .post('/admin/messages/ncmec-reports') - .body({ - channel_id: c.t1.id, - message_id: copyId, - attachment_id: attachment.id.toString(), - filename: attachment.filename, - reporter_full_name: 'Crosspost Reporter', - confirmed_viewed: true, - }) - .expect(HTTP_STATUS.OK) - .execute(); - const submission = await new NcmecRepository().getAttachmentSubmission(attachment.id); - expect(submission?.user_id?.toString()).toBe(world.a.member.userId); - await deleteMessageSilently(c.t1.id, copyId!, world.a.member.userId); - await worker.drain(); - await expectFamilyGone(sourceId); - expect(deletedKeys().filter((key) => key === sourceKey)).toHaveLength(1); - expectNoTargetObjectsDeleted(); - }); - test('a blocked unfurl of a published source deletes every copy', async () => { const sourceId = await publishAndDeliver(`see https://${BLOCKED_DOMAIN}/page`); expect(await listCopies(c.t1.id, sourceId)).toHaveLength(1); diff --git a/fluxer_api/src/api/channel/tests/EmbedAttachmentPurge.test.ts b/fluxer_api/src/api/channel/tests/EmbedAttachmentPurge.test.ts index b1960245b..d1da4bc75 100644 --- a/fluxer_api/src/api/channel/tests/EmbedAttachmentPurge.test.ts +++ b/fluxer_api/src/api/channel/tests/EmbedAttachmentPurge.test.ts @@ -1,15 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createTestAccount, setUserACLs, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; -import { - type ChannelID, - createChannelID, - createMessageID, - createReportID, - createUserID, - type MessageID, - type UserID, -} from '@app/api/BrandedTypes'; +import {createReportID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import { createChannel, @@ -18,7 +10,6 @@ import { sendMessageWithAttachments, } from '@app/api/channel/tests/AttachmentTestUtils'; import {acceptInvite, createChannelInvite} from '@app/api/channel/tests/ChannelTestUtils'; -import {getNcmecSubmissionService} from '@app/api/middleware/ServiceSingletons'; import {ReportRepository} from '@app/api/report/ReportRepository'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import {HTTP_STATUS} from '@app/api/test/TestConstants'; @@ -210,13 +201,7 @@ describe('Embed attachment purge', () => { } async function createAdmin(): Promise { - return setUserACLs(harness, await createTestAccount(harness), [ - AdminACLs.AUTHENTICATE, - AdminACLs.MESSAGE_DELETE, - AdminACLs.CSAM_SUBMIT_NCMEC, - AdminACLs.USER_DELETE, - AdminACLs.ARCHIVE_TRIGGER_USER, - ]); + return setUserACLs(harness, await createTestAccount(harness), [AdminACLs.AUTHENTICATE, AdminACLs.MESSAGE_DELETE]); } function embedKeys(message: MessageResponse): Array { @@ -274,24 +259,9 @@ describe('Embed attachment purge', () => { expect(deletedKeys()).toEqual(expect.arrayContaining(keys)); }); - it('removes files shown only through an embed on a silent NCMEC delete', async () => { - const message = await sendEmbedOnly(); - const keys = embedKeys(message); - const service = getNcmecSubmissionService() as unknown as { - deleteMessageSilently(channelId: ChannelID, messageId: MessageID, fallbackUserId: UserID): Promise; - }; - await service.deleteMessageSilently( - createChannelID(BigInt(channelId)), - createMessageID(BigInt(message.id)), - createUserID(BigInt(account.userId)), - ); - expect(deletedKeys()).toEqual(expect.arrayContaining(keys)); - }); - it('keeps reported embed files as report evidence after the author deletes the message', async () => { const message = await sendEmbedOnly(); const [imageKey] = embedKeys(message); - const [, , attachmentId, filename] = imageKey!.split('/'); const reporter = await createTestAccount(harness); const invite = await createChannelInvite(harness, account.token, channelId); await acceptInvite(harness, reporter.token, invite.code); @@ -306,20 +276,6 @@ describe('Embed attachment purge', () => { const row = await new ReportRepository().getReport(createReportID(BigInt(report.report_id))); const context = row!.messageContext!.find((entry) => entry.messageId.toString() === message.id); expect(context?.attachments.map((attachment) => attachment.filename).sort()).toEqual(['image.png', 'thumb.png']); - const admin = await createAdmin(); - await createBuilder(harness, admin.token) - .post('/admin/messages/ncmec-reports') - .body({ - channel_id: channelId, - message_id: message.id, - attachment_id: attachmentId, - filename, - reporter_full_name: 'Embed Reporter', - confirmed_viewed: true, - source_report_id: report.report_id, - }) - .expect(HTTP_STATUS.OK) - .execute(); }); it('does not expose the ownership marker on embed media flags', async () => { diff --git a/fluxer_api/src/api/config/APIConfig.ts b/fluxer_api/src/api/config/APIConfig.ts index c898785d7..55642355a 100644 --- a/fluxer_api/src/api/config/APIConfig.ts +++ b/fluxer_api/src/api/config/APIConfig.ts @@ -363,13 +363,6 @@ export interface APIConfig { crosspost?: number; }; }; - ncmec: { - enabled: boolean; - baseUrl?: string; - username?: string; - password?: string; - reporterEmail?: string; - }; } export interface BlueskyOAuthKeyConfig { diff --git a/fluxer_api/src/api/csam/NcmecReporter.ts b/fluxer_api/src/api/csam/NcmecReporter.ts deleted file mode 100644 index 6cd9159db..000000000 --- a/fluxer_api/src/api/csam/NcmecReporter.ts +++ /dev/null @@ -1,498 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {Config} from '@app/api/Config'; -import {Logger} from '@app/api/Logger'; -import {EXTERNAL_RESPONSE_LIMITS} from '@app/api/utils/ExternalResponseLimits'; -import * as FetchUtils from '@app/api/utils/FetchUtils'; -import {ms} from 'itty-time'; - -const NCMEC_REQUEST_TIMEOUT_MS = ms('2 minutes'); - -type NcmecOperation = 'report' | 'evidence' | 'fileinfo' | 'finish' | 'retract'; -type NcmecApiConfig = - | { - enabled: true; - baseUrl: string; - username: string; - password: string; - } - | { - enabled: false; - baseUrl: null; - username: null; - password: null; - }; - -interface NcmecApiDeps { - config: NcmecApiConfig; - fetch: typeof fetch; -} - -class NcmecRequestError extends Error { - constructor( - message: string, - public readonly httpStatus: number, - public readonly responseCode: number | null, - public readonly responseDescription: string | null, - public readonly requestId: string | null, - public readonly body: string | null, - ) { - super(message); - } -} - -interface NcmecResponseMeta { - responseCode: number | null; - responseDescription: string | null; - requestId: string | null; -} - -export interface NcmecApiClient { - submitReport(reportXml: string): Promise; - uploadEvidence( - reportId: string, - body: Uint8Array, - filename: string, - ): Promise<{ - fileId: string; - md5: string | null; - }>; - submitFileDetails(fileDetailsXml: string): Promise; - finish(reportId: string): Promise<{ - reportId: string; - fileIds: Array; - }>; - retract(reportId: string): Promise; -} - -export class NcmecReporter implements NcmecApiClient { - constructor(private readonly deps: NcmecApiDeps) {} - - async submitReport(reportXml: string): Promise { - return this.telemetered('report', async () => { - const {res, text} = await this.postXml('/submit', reportXml); - this.assertOk(res, text); - const reportId = takeTag(text, 'reportId'); - if (!reportId) throw this.fail('NCMEC /submit returned no reportId.', res, text); - return reportId; - }); - } - - async uploadEvidence( - reportId: string, - body: Uint8Array, - filename: string, - ): Promise<{ - fileId: string; - md5: string | null; - }> { - return this.telemetered('evidence', async () => { - const form = new FormData(); - form.append('id', reportId); - const blobBytes = new Uint8Array(body.byteLength); - blobBytes.set(body); - form.append('file', new Blob([blobBytes]), filename); - const {res, text} = await this.postForm('/upload', form); - this.assertOk(res, text); - const fileId = takeTag(text, 'fileId'); - if (!fileId) throw this.fail('NCMEC /upload returned no fileId.', res, text); - return {fileId, md5: takeTag(text, 'hash')}; - }); - } - - async submitFileDetails(fileDetailsXml: string): Promise { - await this.telemetered('fileinfo', async () => { - const {res, text} = await this.postXml('/fileinfo', fileDetailsXml); - this.assertOk(res, text); - }); - } - - async finish(reportId: string): Promise<{ - reportId: string; - fileIds: Array; - }> { - return this.telemetered('finish', async () => { - const form = new FormData(); - form.append('id', reportId); - const {res, text} = await this.postForm('/finish', form); - this.assertOk(res, text); - const returned = takeTag(text, 'reportId'); - if (!returned) throw this.fail('NCMEC /finish returned no reportId.', res, text); - return {reportId: returned, fileIds: takeTags(text, 'fileId')}; - }); - } - - async retract(reportId: string): Promise { - await this.telemetered('retract', async () => { - const form = new FormData(); - form.append('id', reportId); - const {res, text} = await this.postForm('/retract', form); - this.assertOk(res, text); - }); - } - - private postXml( - path: string, - body: string, - ): Promise<{ - res: Response; - text: string; - }> { - return this.post(path, {body, headers: {'Content-Type': 'text/xml; charset=utf-8'}}); - } - - private postForm( - path: string, - form: FormData, - ): Promise<{ - res: Response; - text: string; - }> { - return this.post(path, {body: form}); - } - - private async post( - path: string, - init: RequestInit, - ): Promise<{ - res: Response; - text: string; - }> { - const cfg = this.requireEnabled(); - const res = await this.deps.fetch(`${cfg.baseUrl}${path}`, { - method: 'POST', - ...init, - headers: { - Authorization: basicAuth(cfg.username, cfg.password), - ...(init.headers ?? {}), - }, - signal: AbortSignal.timeout(NCMEC_REQUEST_TIMEOUT_MS), - }); - const text = await FetchUtils.streamToStringWithLimit(res.body, { - maxBytes: EXTERNAL_RESPONSE_LIMITS.ncmecResponseBytes, - headers: res.headers, - url: res.url, - description: 'NCMEC response', - }).catch(() => ''); - return {res, text}; - } - - private assertOk(res: Response, body: string): void { - const meta = parseResponseMeta(res, body); - if (res.ok && meta.responseCode === 0) return; - throw this.fail( - meta.responseCode === null - ? `NCMEC request failed (http ${res.status}).` - : `NCMEC request failed (http ${res.status}, responseCode ${meta.responseCode}).`, - res, - body, - meta, - ); - } - - private fail(message: string, res: Response, body: string, meta = parseResponseMeta(res, body)): NcmecRequestError { - Logger.warn( - { - status: res.status, - responseCode: meta.responseCode, - description: meta.responseDescription, - body: body || '', - }, - 'NCMEC request failed', - ); - return new NcmecRequestError( - message, - res.status, - meta.responseCode, - meta.responseDescription, - meta.requestId, - body || null, - ); - } - - private requireEnabled(): Extract< - NcmecApiConfig, - { - enabled: true; - } - > { - if (!this.deps.config.enabled) throw new Error('NCMEC reporting is disabled.'); - return this.deps.config; - } - - private async telemetered(operation: NcmecOperation, fn: () => Promise): Promise { - if (!this.deps.config.enabled) { - throw new Error('NCMEC reporting is disabled.'); - } - try { - const out = await fn(); - return out; - } catch (error) { - Logger.error({error, operation}, 'NCMEC operation failed'); - throw error; - } - } -} - -export function createNcmecApiConfig(): NcmecApiConfig { - if (!Config.ncmec.enabled) return {enabled: false, baseUrl: null, username: null, password: null}; - return { - enabled: true, - baseUrl: normaliseBaseUrl(Config.ncmec.baseUrl), - username: requireConfigValue(Config.ncmec.username, 'NCMEC username'), - password: requireConfigValue(Config.ncmec.password, 'NCMEC password'), - }; -} - -function basicAuth(user: string, pass: string): string { - return `Basic ${Buffer.from(`${user}:${pass}`, 'utf-8').toString('base64')}`; -} - -function normaliseBaseUrl(raw: string | undefined | null): string { - const trimmed = (raw ?? '').trim(); - if (!trimmed) throw new Error('NCMEC base URL is required when reporting is enabled.'); - return trimmed.replace(/\/+$/, ''); -} - -function requireConfigValue(value: string | undefined | null, label: string): string { - const trimmed = (value ?? '').trim(); - if (!trimmed) throw new Error(`${label} is required when reporting is enabled.`); - return trimmed; -} - -function requireNonBlank(value: string | undefined | null, label: string): string { - const trimmed = (value ?? '').trim(); - if (!trimmed) throw new Error(`${label} is required.`); - return trimmed; -} - -function takeTag(xml: string, tag: string): string | null { - if (!xml) return null; - const match = xml.match(new RegExp(`<${tag}>([\\s\\S]*?)`, 'i')); - return match?.[1]?.trim() || null; -} - -function takeTags(xml: string, tag: string): Array { - if (!xml) return []; - return [...xml.matchAll(new RegExp(`<${tag}>([\\s\\S]*?)`, 'gi'))] - .map((match) => match[1]?.trim() ?? '') - .filter(Boolean); -} - -function parseResponseCode(value: string | null): number | null { - if (!value) return null; - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) ? parsed : null; -} - -function parseResponseMeta(res: Response, body: string): NcmecResponseMeta { - return { - responseCode: parseResponseCode(takeTag(body, 'responseCode')), - responseDescription: takeTag(body, 'responseDescription'), - requestId: res.headers.get('Request-ID'), - }; -} - -interface NcmecReportedUser { - id: bigint | null; - screenName: string | null; - displayName: string | null; - espService: string | null; - permanentlyDisabledAt: Date | null; - person: NcmecReportedUserPerson | null; - ipCaptureEvents: ReadonlyArray; -} - -interface NcmecReportedUserPerson { - emails: ReadonlyArray; - phones: ReadonlyArray; - dateOfBirth: string | null; -} - -interface NcmecContactEmail { - address: string; - type?: 'Home' | 'Work' | 'Business' | 'Recovery'; - verified?: boolean | null; -} - -interface NcmecContactPhone { - number: string; - type?: 'Mobile' | 'Home' | 'Business' | 'Work' | 'Fax' | 'Internet' | 'Recovery'; - verified?: boolean | null; -} - -interface NcmecIpCaptureEvent { - ipAddress: string; - eventName?: 'Login' | 'Registration' | 'Purchase' | 'Upload' | 'Other' | 'Unknown'; - dateTime?: Date | null; - possibleProxy?: boolean | null; -} - -interface NcmecReportXmlInput { - attachmentUrl: string; - reportedAt: Date; - reporterFullName: string; - reporterEmail: string; - reportedUser: NcmecReportedUser; - priorNcmecReportIds: ReadonlyArray; - additionalInfo: string | null; -} - -interface NcmecFileDetailsXmlInput { - reportId: string; - fileId: string; - filename: string; - uploadedToEspTimestamp: Date | null; - fileViewedByEsp?: boolean; - ipCaptureEvent: NcmecIpCaptureEvent | null; - additionalInfo: string | null; -} - -export function buildNcmecReportXml(input: NcmecReportXmlInput): string { - const schemaLocation = `${schemaRoot()}/xsd`; - const {firstName, lastName} = splitName(input.reporterFullName); - const reporterEmail = requireNonBlank(input.reporterEmail, 'NCMEC reporter email'); - const screenName = input.reportedUser.screenName - ? ` ${escapeXml(input.reportedUser.screenName)}\n` - : ''; - const displayName = input.reportedUser.displayName - ? ` ${escapeXml(input.reportedUser.displayName)}\n` - : ''; - const reportedUserPerson = renderReportedUserPerson(input.reportedUser.person ?? null); - const espIdentifier = - input.reportedUser.id !== null - ? ` ${escapeXml(input.reportedUser.id.toString())}\n` - : ''; - const espService = input.reportedUser.espService - ? ` ${escapeXml(input.reportedUser.espService)}\n` - : ''; - const ipCaptureEvents = (input.reportedUser.ipCaptureEvents ?? []) - .map((event) => renderIpCaptureEvent(event, ' ')) - .join(''); - const priorReports = input.priorNcmecReportIds - .map((id) => ` ${escapeXml(id)}\n`) - .join(''); - const disabled = input.reportedUser.permanentlyDisabledAt - ? ` true\n` - : ''; - const allEmailsReported = - input.reportedUser.person && input.reportedUser.person.emails.length > 0 - ? ' true\n' - : ''; - const reportAdditionalInfo = input.additionalInfo - ? ` ${escapeXml(input.additionalInfo)}\n` - : ''; - return ` - - - Child Pornography (possession, manufacture, and distribution) - Fluxer - ${escapeXml(input.reportedAt.toISOString())} - - - - ${escapeXml(input.attachmentUrl)} - - - - - ${escapeXml(firstName)} - ${escapeXml(lastName)} - ${escapeXml(reporterEmail)} - - - -${reportedUserPerson}${espIdentifier}${espService}${screenName}${displayName}${ipCaptureEvents}${priorReports}${disabled}${allEmailsReported} -${reportAdditionalInfo}`; -} - -export function buildNcmecFileDetailsXml(input: NcmecFileDetailsXmlInput): string { - const schemaLocation = `${schemaRoot()}/xsd`; - const uploadedToEspTimestamp = input.uploadedToEspTimestamp - ? ` ${escapeXml(input.uploadedToEspTimestamp.toISOString())}\n` - : ''; - const fileViewedByEsp = input.fileViewedByEsp === true ? ' true\n' : ''; - const ipCaptureEvent = input.ipCaptureEvent ? renderIpCaptureEvent(input.ipCaptureEvent, ' ') : ''; - const additionalInfo = input.additionalInfo - ? ` ${escapeXml(input.additionalInfo)}\n` - : ''; - return ` - - ${escapeXml(input.reportId)} - ${escapeXml(input.fileId)} - ${escapeXml(input.filename)} -${uploadedToEspTimestamp}${fileViewedByEsp}${ipCaptureEvent}${additionalInfo}`; -} - -function schemaRoot(): string { - const trimmed = (Config.ncmec.baseUrl ?? '').trim().replace(/\/+$/, ''); - return trimmed || 'https://report.cybertip.org/ispws'; -} - -function splitName(fullName: string): { - firstName: string; - lastName: string; -} { - const parts = fullName.trim().split(/\s+/).filter(Boolean); - if (parts.length === 0) return {firstName: 'Unknown', lastName: 'Reporter'}; - if (parts.length === 1) return {firstName: parts[0]!, lastName: 'Reporter'}; - return {firstName: parts[0]!, lastName: parts.slice(1).join(' ')}; -} - -function renderReportedUserPerson(input: NcmecReportedUserPerson | null): string { - if (!input) return ''; - const phones = input.phones.map((phone) => renderPhone(phone)).join(''); - const emails = input.emails.map((email) => renderEmail(email)).join(''); - const dateOfBirth = input.dateOfBirth - ? ` ${escapeXml(input.dateOfBirth)}\n` - : ''; - if (!phones && !emails && !dateOfBirth) { - return ''; - } - return ` \n${phones}${emails}${dateOfBirth} \n`; -} - -function renderPhone(phone: NcmecContactPhone): string { - const attrs: Array = []; - if (phone.type) { - attrs.push(`type="${escapeXml(phone.type)}"`); - } - if (typeof phone.verified === 'boolean') { - attrs.push(`verified="${phone.verified ? 'true' : 'false'}"`); - } - const attrText = attrs.length > 0 ? ` ${attrs.join(' ')}` : ''; - return ` ${escapeXml(phone.number)}\n`; -} - -function renderEmail(email: NcmecContactEmail): string { - const attrs: Array = []; - if (email.type) { - attrs.push(`type="${escapeXml(email.type)}"`); - } - if (typeof email.verified === 'boolean') { - attrs.push(`verified="${email.verified ? 'true' : 'false'}"`); - } - const attrText = attrs.length > 0 ? ` ${attrs.join(' ')}` : ''; - return ` ${escapeXml(email.address)}\n`; -} - -function renderIpCaptureEvent(event: NcmecIpCaptureEvent, indent: string): string { - const eventName = event.eventName ? `${indent} ${escapeXml(event.eventName)}\n` : ''; - const dateTime = event.dateTime - ? `${indent} ${escapeXml(event.dateTime.toISOString())}\n` - : ''; - const possibleProxy = - typeof event.possibleProxy === 'boolean' - ? `${indent} ${event.possibleProxy ? 'true' : 'false'}\n` - : ''; - return `${indent}\n${indent} ${escapeXml(event.ipAddress)}\n${eventName}${dateTime}${possibleProxy}${indent}\n`; -} - -function escapeXml(value: string): string { - return value - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll("'", '''); -} diff --git a/fluxer_api/src/api/csam/NcmecRepository.ts b/fluxer_api/src/api/csam/NcmecRepository.ts deleted file mode 100644 index eba5486cf..000000000 --- a/fluxer_api/src/api/csam/NcmecRepository.ts +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {AttachmentID, UserID} from '@app/api/BrandedTypes'; -import {fetchOne, upsertOne} from '@app/api/database/CassandraQueryExecution'; -import type {NcmecAttachmentSubmissionRow, NcmecUserWorkflowRow} from '@app/api/database/types/CsamTypes'; -import {NcmecAttachmentSubmissions, NcmecUserWorkflows} from '@app/api/Tables'; - -const GET_ATTACHMENT_SUBMISSION_QUERY = NcmecAttachmentSubmissions.select({ - where: NcmecAttachmentSubmissions.where.eq('attachment_id'), - limit: 1, -}); -const GET_USER_WORKFLOW_QUERY = NcmecUserWorkflows.select({ - where: NcmecUserWorkflows.where.eq('user_id'), - limit: 1, -}); - -export class NcmecRepository { - async getAttachmentSubmission(attachmentId: AttachmentID): Promise { - return await fetchOne( - GET_ATTACHMENT_SUBMISSION_QUERY.bind({attachment_id: BigInt(attachmentId)}), - ); - } - - async upsertAttachmentSubmission(row: NcmecAttachmentSubmissionRow): Promise { - await upsertOne(NcmecAttachmentSubmissions.insert(row)); - } - - async getUserWorkflow(userId: UserID): Promise { - return await fetchOne(GET_USER_WORKFLOW_QUERY.bind({user_id: BigInt(userId)})); - } - - async upsertUserWorkflow(row: NcmecUserWorkflowRow): Promise { - await upsertOne(NcmecUserWorkflows.insert(row)); - } -} diff --git a/fluxer_api/src/api/csam/NcmecSubmissionService.ts b/fluxer_api/src/api/csam/NcmecSubmissionService.ts deleted file mode 100644 index e78321795..000000000 --- a/fluxer_api/src/api/csam/NcmecSubmissionService.ts +++ /dev/null @@ -1,1064 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {AdminArchiveService} from '@app/api/admin/services/AdminArchiveService'; -import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService'; -import {AdminUserUpdatePropagator} from '@app/api/admin/services/AdminUserUpdatePropagator'; -import { - type AttachmentID, - type ChannelID, - createAttachmentID, - createChannelID, - createMessageID, - createUserID, - type MessageID, - type ReportID, - type UserID, -} from '@app/api/BrandedTypes'; -import {Config} from '@app/api/Config'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; -import type {AttachmentUploadTraceRepository} from '@app/api/channel/repositories/message/AttachmentUploadTraceRepository'; -import {withThreadContext} from '@app/api/channel/services/ChannelGatewayDispatch'; -import { - enqueueCrosspostFamilyPurgeFromCopies, - enqueueCrosspostSourceRemoval, -} from '@app/api/channel/services/message/CrosspostPropagation'; -import { - attachmentStorageChannelId, - collectMessageAttachments, - decrementThreadMessageCount, - makeAttachmentCdnKey, - makeAttachmentCdnUrl, - purgeMessageAttachments, -} from '@app/api/channel/services/message/MessageHelpers'; -import type {NcmecApiClient} from '@app/api/csam/NcmecReporter'; -import {buildNcmecFileDetailsXml, buildNcmecReportXml} from '@app/api/csam/NcmecReporter'; -import type {NcmecRepository} from '@app/api/csam/NcmecRepository'; -import type {AttachmentUploadTraceByAttachmentRow} from '@app/api/database/types/AttachmentUploadTypes'; -import type {NcmecAttachmentSubmissionRow, NcmecUserWorkflowRow} from '@app/api/database/types/CsamTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; -import type {IPurgeQueue} from '@app/api/infrastructure/CachePurgeQueue'; -import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; -import type {IStorageService} from '@app/api/infrastructure/IStorageService'; -import type {KVAccountDeletionQueueService} from '@app/api/infrastructure/KVAccountDeletionQueueService'; -import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; -import {Logger} from '@app/api/Logger'; -import type {Embed} from '@app/api/models/Embed'; -import type {EmbedMedia} from '@app/api/models/EmbedMedia'; -import type {Message} from '@app/api/models/Message'; -import type {User} from '@app/api/models/User'; -import type {IARMessageContext, IARSubmission} from '@app/api/report/IReportRepository'; -import type {ReportRepository} from '@app/api/report/ReportRepository'; -import {deleteMessageSearchDocuments} from '@app/api/search/MessageSearchIndexCleanup'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; -import {reschedulePendingDeletion} from '@app/api/user/services/PendingDeletionCoordinator'; -import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; -import {MessageFlags} from '@fluxer/constants/src/ChannelConstants'; -import {DeletionReasons} from '@fluxer/constants/src/Core'; -import {CATEGORY_CHILD_SAFETY} from '@fluxer/constants/src/ReportCategories'; -import {UserFlags} from '@fluxer/constants/src/UserConstants'; -import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; -import {UnknownMessageError} from '@fluxer/errors/src/domains/channel/UnknownMessageError'; -import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; -import {NcmecAlreadySubmittedError} from '@fluxer/errors/src/domains/moderation/NcmecAlreadySubmittedError'; -import {NcmecSubmissionFailedError} from '@fluxer/errors/src/domains/moderation/NcmecSubmissionFailedError'; -import {UnknownReportError} from '@fluxer/errors/src/domains/moderation/UnknownReportError'; -import type {NcmecSubmissionStatus} from '@fluxer/schema/src/domains/admin/AdminSchemas'; -import {snowflakeToDate} from '@fluxer/snowflake/src/Snowflake'; -import type {IWorkerService} from '@pkgs/worker/src/contracts/IWorkerService'; -import {ms} from 'itty-time'; - -export interface NcmecAttachmentStatusResponse { - status: NcmecSubmissionStatus; - ncmec_report_id: string | null; - submitted_at: string | null; - submitted_by_admin_id: string | null; - failure_reason: string | null; -} - -interface NcmecAttachmentSubmitResult { - success: true; - ncmec_report_id: string; - audit_log_reason: string; -} - -interface NcmecUserUpdatePropagator { - propagateUserUpdate(params: {userId: UserID; oldUser: User; updatedUser: User}): Promise; -} - -interface SubmitAttachmentToNcmecInput { - channelId: ChannelID; - messageId: MessageID; - attachmentId: AttachmentID; - filename: string; - reporterFullName: string; - adminUserId: UserID; - sourceReportId?: ReportID | null; -} - -interface ResolvedAttachment { - channelId: ChannelID; - messageId: MessageID; - attachmentId: AttachmentID; - filename: string; - contentType: string | null; - reportedAt: Date; - userId: UserID | null; - sourceReportId: ReportID | null; - cdnBucket: string; - storageKey: string; - cdnUrl: string; -} - -interface ReportedUserSnapshot { - screenName: string | null; - displayName: string | null; - email: string | null; - emailVerified: boolean; - dateOfBirth: string | null; - lastActiveIp: string | null; -} - -interface NcmecSubmissionServiceDeps { - reportRepository: ReportRepository; - ncmecApi: NcmecApiClient; - ncmecRepository: NcmecRepository; - attachmentUploadTraceRepository: AttachmentUploadTraceRepository; - storageService: IStorageService; - channelRepository: IChannelRepository; - userRepository: IUserRepository; - guildRepository: IGuildRepositoryAggregate; - gatewayService: IGatewayService; - userCacheService: UserCacheService; - adminArchiveService: AdminArchiveService; - adminAuditService: AdminAuditService; - purgeQueue: IPurgeQueue; - workerService: IWorkerService; - deletionQueue: KVAccountDeletionQueueService; - updatePropagator?: NcmecUserUpdatePropagator; -} - -const NCMEC_DELETION_GRACE_DAYS = 60; -const DEFAULT_FINALIZE_TASK_DELAY_MS = ms('15 seconds'); -const DEFAULT_MAX_FINALIZE_REQUEUES = 240; - -function getFinalizeDelayMs(): number { - const raw = ( - Config.ncmec as { - finalizeDelayMs?: number; - } - ).finalizeDelayMs; - return Number.isFinite(raw) && raw! > 0 ? raw! : DEFAULT_FINALIZE_TASK_DELAY_MS; -} - -function getMaxFinalizeRequeues(): number { - const raw = ( - Config.ncmec as { - maxFinalizeRequeues?: number; - } - ).maxFinalizeRequeues; - return Number.isFinite(raw) && raw! > 0 ? raw! : DEFAULT_MAX_FINALIZE_REQUEUES; -} - -export class NcmecSubmissionService { - private readonly updatePropagator: NcmecUserUpdatePropagator; - - constructor(private readonly deps: NcmecSubmissionServiceDeps) { - this.updatePropagator = - deps.updatePropagator ?? - new AdminUserUpdatePropagator({ - userCacheService: deps.userCacheService, - userRepository: deps.userRepository, - guildRepository: deps.guildRepository, - gatewayService: deps.gatewayService, - }); - } - - async assertChildSafetyReport(reportId: ReportID): Promise { - const report = await this.deps.reportRepository.getReport(reportId); - if (!report || report.category !== CATEGORY_CHILD_SAFETY) { - throw new UnknownReportError(); - } - return report; - } - - async getAttachmentStatus(attachmentId: AttachmentID): Promise { - const submission = await this.deps.ncmecRepository.getAttachmentSubmission(attachmentId); - return toAttachmentStatusResponse(submission); - } - - async getAttachmentStatuses(attachmentIds: Array): Promise> { - const ids = [...new Set(attachmentIds.map((id) => id.toString()))].map((value) => - createAttachmentID(BigInt(value)), - ); - const rows = await Promise.all(ids.map((id) => this.getAttachmentStatus(id))); - const result = new Map(); - for (let i = 0; i < ids.length; i++) { - result.set(ids[i]!.toString(), rows[i]!); - } - return result; - } - - async getUserPriorReportIds(userIds: Array): Promise>> { - const unique = [...new Set(userIds.map((id) => id.toString()))]; - const out = new Map>(); - await Promise.all( - unique.map(async (value) => { - const workflow = await this.deps.ncmecRepository.getUserWorkflow(createUserID(BigInt(value))); - if (workflow?.previous_report_ids && workflow.previous_report_ids.size > 0) { - out.set(value, [...workflow.previous_report_ids].sort()); - } - }), - ); - return out; - } - - async submitAttachmentToNcmec(input: SubmitAttachmentToNcmecInput): Promise { - const existing = await this.deps.ncmecRepository.getAttachmentSubmission(input.attachmentId); - if (existing?.status === 'submitted' || existing?.status === 'submitting') { - throw new NcmecAlreadySubmittedError(); - } - const attachment = await this.resolveAttachment(input); - const adminUser = await this.deps.userRepository.findUnique(input.adminUserId); - const reporterEmail = requireReporterEmail((Config.ncmec.reporterEmail?.trim() || adminUser?.email) ?? null); - const reportedUser = await this.snapshotReportedUser(attachment.userId); - const attachmentUploadTrace = await this.deps.attachmentUploadTraceRepository.getByAttachmentId( - attachment.attachmentId, - ); - const existingWorkflow = attachment.userId - ? await this.deps.ncmecRepository.getUserWorkflow(attachment.userId) - : null; - const priorReportIds = [...(existingWorkflow?.previous_report_ids ?? new Set())].sort(); - const contextLabel = `${attachment.channelId.toString()}/${attachment.attachmentId.toString()}`; - const reportAdditionalInfo = buildAdditionalInfo({ - channelId: attachment.channelId, - messageId: attachment.messageId, - attachmentId: attachment.attachmentId, - userId: attachment.userId, - sourceReportId: attachment.sourceReportId, - priorReportIds, - reportedUser, - attachmentUploadTrace, - }); - const fileAdditionalInfo = reportAdditionalInfo; - const reportXml = buildNcmecReportXml({ - attachmentUrl: attachment.cdnUrl, - reportedAt: attachment.reportedAt, - reporterFullName: input.reporterFullName, - reporterEmail, - reportedUser: { - id: attachment.userId !== null ? BigInt(attachment.userId) : null, - screenName: reportedUser.screenName, - displayName: reportedUser.displayName, - espService: 'Fluxer', - permanentlyDisabledAt: existingWorkflow?.deleted_at ?? null, - person: buildReportedUserPerson(reportedUser), - ipCaptureEvents: buildReportedUserIpCaptureEvents(attachmentUploadTrace), - }, - priorNcmecReportIds: priorReportIds, - additionalInfo: reportAdditionalInfo, - }); - await this.writeAttachmentSubmission({ - existing, - attachment, - status: 'submitting', - ncmecReportId: null, - ncmecFileId: null, - reporterFullName: input.reporterFullName, - adminUserId: input.adminUserId, - failureReason: null, - }); - let ncmecReportId: string | null = null; - try { - ncmecReportId = await this.deps.ncmecApi.submitReport(reportXml); - const upload = await this.deps.ncmecApi.uploadEvidence( - ncmecReportId, - await this.readAttachmentBytes(attachment), - attachment.filename, - ); - await this.deps.ncmecApi.submitFileDetails( - buildNcmecFileDetailsXml({ - reportId: ncmecReportId, - fileId: upload.fileId, - filename: attachment.filename, - uploadedToEspTimestamp: getBestEffortUploadTimestamp(attachmentUploadTrace), - fileViewedByEsp: true, - ipCaptureEvent: buildBestEffortUploadIpCaptureEvent(attachmentUploadTrace), - additionalInfo: fileAdditionalInfo, - }), - ); - await this.deps.ncmecApi.finish(ncmecReportId); - await this.writeAttachmentSubmission({ - existing, - attachment, - status: 'submitted', - ncmecReportId, - ncmecFileId: upload.fileId, - reporterFullName: input.reporterFullName, - adminUserId: input.adminUserId, - failureReason: null, - }); - const auditLogReason = formatNcmecAuditLogReason(ncmecReportId, contextLabel); - if (attachment.userId !== null) { - await this.deps.adminAuditService.createAuditLog({ - adminUserId: input.adminUserId, - targetType: 'user', - targetId: BigInt(attachment.userId), - action: 'NCMEC Report', - auditLogReason, - metadata: buildAuditMetadata({attachment, reportedUser, attachmentUploadTrace}), - }); - const workflow = await this.advanceUserWorkflow({ - userId: attachment.userId, - adminUserId: input.adminUserId, - ncmecReportId, - auditLogReason, - contextLabel, - existingWorkflow, - }); - await this.deps.workerService.addJob( - 'finalizeNcmecAttachmentReport', - {attachmentId: attachment.attachmentId.toString(), requeueCount: 0}, - {jobKey: finalizeJobKey(attachment.attachmentId), runAt: new Date(Date.now() + getFinalizeDelayMs())}, - ); - Logger.info( - { - attachmentId: attachment.attachmentId.toString(), - userId: attachment.userId.toString(), - ncmecReportId, - archiveId: workflow.archive_id?.toString() ?? null, - }, - 'NCMEC attachment report submitted', - ); - } else { - await this.deps.adminAuditService.createAuditLog({ - adminUserId: input.adminUserId, - targetType: 'report', - targetId: BigInt(attachment.sourceReportId ?? input.attachmentId), - action: 'NCMEC Report', - auditLogReason, - metadata: buildAuditMetadata({attachment, reportedUser, attachmentUploadTrace}), - }); - Logger.info( - { - attachmentId: attachment.attachmentId.toString(), - sourceReportId: attachment.sourceReportId?.toString() ?? null, - ncmecReportId, - }, - 'NCMEC attachment report submitted (legacy report, unknown author — skipped ban/archive)', - ); - } - return {success: true, ncmec_report_id: ncmecReportId, audit_log_reason: auditLogReason}; - } catch (error) { - if (ncmecReportId) { - try { - await this.deps.ncmecApi.retract(ncmecReportId); - } catch (retractError) { - Logger.warn( - {error: retractError, attachmentId: input.attachmentId.toString(), ncmecReportId}, - 'Failed to retract failed NCMEC attachment submission', - ); - } - } - const rawMessage = error instanceof Error ? error.message : 'Unknown error'; - await this.writeAttachmentSubmission({ - existing, - attachment, - status: 'failed', - ncmecReportId: null, - ncmecFileId: null, - reporterFullName: input.reporterFullName, - adminUserId: input.adminUserId, - failureReason: rawMessage, - }); - Logger.error( - { - error, - attachmentId: input.attachmentId.toString(), - channelId: input.channelId.toString(), - messageId: input.messageId.toString(), - }, - 'NCMEC attachment report submission failed', - ); - throw new NcmecSubmissionFailedError(sanitizeFailureMessage(rawMessage)); - } - } - - async finalizeAttachmentReport(attachmentId: AttachmentID, requeueCount = 0): Promise { - const submission = await this.deps.ncmecRepository.getAttachmentSubmission(attachmentId); - if (submission?.status !== 'submitted' || submission.content_deleted_at) { - return; - } - if (submission.user_id === null) { - Logger.warn({attachmentId: attachmentId.toString()}, 'Skipping finalize: submission has no user_id'); - return; - } - const submissionUserId = submission.user_id; - const workflow = await this.deps.ncmecRepository.getUserWorkflow(createUserID(submissionUserId)); - if (!workflow) { - Logger.warn({attachmentId: attachmentId.toString()}, 'Missing NCMEC user workflow during finalization'); - return; - } - const maxRequeues = getMaxFinalizeRequeues(); - if (requeueCount >= maxRequeues) { - Logger.error( - { - attachmentId: attachmentId.toString(), - requeueCount, - maxRequeues, - archiveId: workflow.archive_id?.toString() ?? null, - }, - 'NCMEC finalize gave up after max requeues; manual intervention required', - ); - return; - } - const auditLogReason = - workflow.archive_audit_log_reason ?? - (submission.ncmec_report_id - ? formatNcmecAuditLogReason( - submission.ncmec_report_id, - `${submission.channel_id.toString()}/${submission.attachment_id.toString()}`, - ) - : null); - const requesterAdminId = - workflow.archive_requested_by_admin_id ?? - workflow.deleted_by_admin_id ?? - submission.submitted_by_admin_id ?? - null; - if (requesterAdminId === null) { - Logger.error( - {attachmentId: attachmentId.toString(), userId: submissionUserId.toString()}, - 'NCMEC finalize missing requester admin id; manual intervention required', - ); - return; - } - const requester = createUserID(requesterAdminId); - const refreshedWorkflow = await this.ensureArchiveAfterBan( - workflow, - createUserID(submissionUserId), - requester, - auditLogReason, - ); - if (!refreshedWorkflow.archive_id) { - await this.requeueFinalizer(attachmentId, requeueCount + 1); - return; - } - const archive = await this.deps.adminArchiveService.getArchive( - 'user', - submissionUserId, - refreshedWorkflow.archive_id, - ); - if (!archive?.completed_at) { - await this.requeueFinalizer(attachmentId, requeueCount + 1); - return; - } - const archiveCompletedAt = new Date(archive.completed_at); - if (!refreshedWorkflow.archive_completed_at) { - await this.deps.ncmecRepository.upsertUserWorkflow({ - ...refreshedWorkflow, - archive_completed_at: archiveCompletedAt, - updated_at: new Date(), - }); - } - await this.deleteMessageSilently( - createChannelID(submission.channel_id), - createMessageID(submission.message_id), - createUserID(submissionUserId), - ); - await this.deps.ncmecRepository.upsertAttachmentSubmission({ - ...submission, - archive_completed_at: archiveCompletedAt, - content_deleted_at: new Date(), - updated_at: new Date(), - }); - if (!refreshedWorkflow.deletion_job_queued_at) { - await this.deps.ncmecRepository.upsertUserWorkflow({ - ...refreshedWorkflow, - archive_completed_at: archiveCompletedAt, - deletion_job_queued_at: new Date(), - updated_at: new Date(), - }); - } - } - - private async resolveAttachment(input: SubmitAttachmentToNcmecInput): Promise { - const liveMessage = await this.deps.channelRepository.getMessage(input.channelId, input.messageId); - const liveAuthorId = liveMessage ? await this.resolveMessageAuthorId(liveMessage) : null; - if (liveMessage && liveAuthorId) { - const liveAttachment = collectMessageAttachments(liveMessage).find( - (attachment) => attachment.id === input.attachmentId && attachment.filename === input.filename, - ); - const storageChannelId = attachmentStorageChannelId(liveMessage); - if (liveAttachment) { - return this.buildAttachmentContext(input, { - contentType: liveAttachment.contentType, - reportedAt: snowflakeToDate(input.messageId), - userId: liveAuthorId, - sourceReportId: input.sourceReportId ?? null, - bucket: Config.s3.buckets.cdn, - storageKey: makeAttachmentCdnKey(storageChannelId, input.attachmentId, input.filename), - }); - } - for (const embedChannelId of new Set([input.channelId, storageChannelId])) { - const embedMedia = findEmbedReferencedAttachmentMedia( - liveMessage, - makeAttachmentCdnUrl(embedChannelId, input.attachmentId, input.filename), - ); - if (embedMedia) { - return this.buildAttachmentContext(input, { - contentType: embedMedia.contentType, - reportedAt: snowflakeToDate(input.messageId), - userId: liveAuthorId, - sourceReportId: input.sourceReportId ?? null, - bucket: Config.s3.buckets.cdn, - storageKey: makeAttachmentCdnKey(embedChannelId, input.attachmentId, input.filename), - }); - } - } - } - if (!input.sourceReportId) { - throw new UnknownMessageError(); - } - const report = await this.deps.reportRepository.getReport(input.sourceReportId); - if (!report) { - throw new UnknownReportError(); - } - const context = findAttachmentInReport( - report, - input.channelId, - input.messageId, - input.attachmentId, - input.filename, - ); - if (context) { - return this.buildAttachmentContext(input, { - contentType: context.attachment.content_type, - reportedAt: context.message.timestamp, - userId: context.message.authorId, - sourceReportId: input.sourceReportId, - bucket: Config.s3.buckets.reports, - storageKey: makeAttachmentCdnKey(input.channelId, input.attachmentId, input.filename), - }); - } - throw new UnknownMessageError(); - } - - private async resolveMessageAuthorId(message: Message): Promise { - if ((message.flags & MessageFlags.IS_CROSSPOST) === 0 || !message.reference?.messageId) { - return message.authorId; - } - const source = await this.deps.channelRepository.getMessage( - message.reference.channelId, - message.reference.messageId, - ); - return source?.authorId ?? message.authorId; - } - - private buildAttachmentContext( - input: SubmitAttachmentToNcmecInput, - context: { - contentType: string | null; - reportedAt: Date; - userId: UserID | null; - sourceReportId: ReportID | null; - bucket: string; - storageKey: string; - }, - ): ResolvedAttachment { - validateImageAttachment(context.contentType); - return { - channelId: input.channelId, - messageId: input.messageId, - attachmentId: input.attachmentId, - filename: input.filename, - contentType: context.contentType, - reportedAt: context.reportedAt, - userId: context.userId, - sourceReportId: context.sourceReportId, - cdnBucket: context.bucket, - storageKey: context.storageKey, - cdnUrl: - context.bucket === Config.s3.buckets.cdn - ? `${Config.endpoints.media}/${context.storageKey}` - : makeAttachmentCdnUrl(input.channelId, input.attachmentId, input.filename), - }; - } - - private async readAttachmentBytes(attachment: ResolvedAttachment): Promise { - const buffer = await this.deps.storageService.readObject(attachment.cdnBucket, attachment.storageKey); - return new Uint8Array(buffer); - } - - private async snapshotReportedUser(userId: UserID | null): Promise { - if (userId === null) { - return { - screenName: null, - displayName: null, - email: null, - emailVerified: false, - dateOfBirth: null, - lastActiveIp: null, - }; - } - const user = await this.deps.userRepository.findUnique(userId); - if (!user) { - return { - screenName: null, - displayName: null, - email: null, - emailVerified: false, - dateOfBirth: null, - lastActiveIp: null, - }; - } - return { - screenName: user.username ?? null, - displayName: user.globalName ?? null, - email: user.email ?? null, - emailVerified: user.emailVerified ?? false, - dateOfBirth: user.dateOfBirth ?? null, - lastActiveIp: user.lastActiveIp ?? null, - }; - } - - private async advanceUserWorkflow(args: { - userId: UserID; - adminUserId: UserID; - ncmecReportId: string; - auditLogReason: string; - contextLabel: string; - existingWorkflow: NcmecUserWorkflowRow | null; - }): Promise { - const now = new Date(); - let workflow: NcmecUserWorkflowRow = args.existingWorkflow ?? { - user_id: BigInt(args.userId), - deleted_at: null, - deleted_by_admin_id: null, - deletion_private_reason: null, - deletion_ncmec_report_id: null, - archive_id: null, - archive_requested_at: null, - archive_requested_by_admin_id: null, - archive_audit_log_reason: null, - archive_completed_at: null, - deletion_job_queued_at: null, - previous_report_ids: null, - created_at: now, - updated_at: now, - }; - workflow = await this.banUserIfNeeded( - workflow, - args.userId, - args.adminUserId, - args.ncmecReportId, - args.contextLabel, - ); - workflow = await this.ensureArchiveAfterBan(workflow, args.userId, args.adminUserId, args.auditLogReason); - const priorReportIds = new Set(workflow.previous_report_ids ?? []); - priorReportIds.add(args.ncmecReportId); - const updated: NcmecUserWorkflowRow = { - ...workflow, - archive_audit_log_reason: workflow.archive_audit_log_reason ?? args.auditLogReason, - previous_report_ids: priorReportIds, - updated_at: new Date(), - }; - await this.deps.ncmecRepository.upsertUserWorkflow(updated); - return updated; - } - - private async banUserIfNeeded( - workflow: NcmecUserWorkflowRow, - userId: UserID, - adminUserId: UserID, - ncmecReportId: string, - contextLabel: string, - ): Promise { - if (workflow.deleted_at) return workflow; - const user = await this.deps.userRepository.findUnique(userId); - if (!user) return workflow; - const privateReason = `Confirmed CSAM - NCMEC Report ${ncmecReportId} - ${contextLabel}`; - const pendingDeletionAt = new Date(); - pendingDeletionAt.setDate(pendingDeletionAt.getDate() + NCMEC_DELETION_GRACE_DAYS); - const updatedUser = await this.deps.userRepository.updateDeletionSchedule(user, { - flags: user.flags | UserFlags.DELETED | UserFlags.DISABLED, - temp_banned_until: null, - pending_deletion_at: pendingDeletionAt, - deletion_reason_code: DeletionReasons.CHILD_SEXUAL_CONTENT, - deletion_public_reason: null, - deletion_audit_log_reason: privateReason, - }); - await reschedulePendingDeletion({ - userId, - currentPendingDeletionAt: user.pendingDeletionAt, - nextPendingDeletionAt: pendingDeletionAt, - deletionReasonCode: DeletionReasons.CHILD_SEXUAL_CONTENT, - userRepository: this.deps.userRepository, - deletionQueue: this.deps.deletionQueue, - }); - await this.deps.userRepository.deleteAllAuthSessions(userId); - await this.updatePropagator.propagateUserUpdate({userId, oldUser: user, updatedUser}); - return { - ...workflow, - deleted_at: new Date(), - deleted_by_admin_id: BigInt(adminUserId), - deletion_private_reason: privateReason, - deletion_ncmec_report_id: ncmecReportId, - updated_at: new Date(), - }; - } - - private async ensureArchiveAfterBan( - workflow: NcmecUserWorkflowRow, - userId: UserID, - adminUserId: UserID, - auditLogReason: string | null, - ): Promise { - if (BigInt(adminUserId) === 0n) return workflow; - const banAt = workflow.deleted_at; - const archivedAt = workflow.archive_requested_at; - const hasPostBanArchive = - workflow.archive_id !== null && archivedAt !== null && banAt !== null && archivedAt >= banAt; - if (hasPostBanArchive) return workflow; - if (workflow.archive_id !== null && banAt === null) return workflow; - const archive = await this.deps.adminArchiveService.triggerUserArchive(userId, adminUserId, true); - await this.deps.adminAuditService.createAuditLog({ - adminUserId, - targetType: 'user', - targetId: BigInt(userId), - action: 'trigger_user_archive', - auditLogReason, - metadata: new Map([['archive_id', archive.archive_id]]), - }); - return { - ...workflow, - archive_id: BigInt(archive.archive_id), - archive_requested_at: new Date(), - archive_requested_by_admin_id: BigInt(adminUserId), - archive_audit_log_reason: workflow.archive_audit_log_reason ?? auditLogReason, - archive_completed_at: null, - deletion_job_queued_at: null, - updated_at: new Date(), - }; - } - - private async writeAttachmentSubmission(args: { - existing: NcmecAttachmentSubmissionRow | null; - attachment: ResolvedAttachment; - status: 'submitting' | 'submitted' | 'failed'; - ncmecReportId: string | null; - ncmecFileId: string | null; - reporterFullName: string; - adminUserId: UserID; - failureReason: string | null; - }): Promise { - const now = new Date(); - await this.deps.ncmecRepository.upsertAttachmentSubmission({ - attachment_id: BigInt(args.attachment.attachmentId), - user_id: args.attachment.userId !== null ? BigInt(args.attachment.userId) : null, - channel_id: BigInt(args.attachment.channelId), - message_id: BigInt(args.attachment.messageId), - filename: args.attachment.filename, - source_report_id: args.attachment.sourceReportId ? BigInt(args.attachment.sourceReportId) : null, - status: args.status, - ncmec_report_id: args.ncmecReportId, - ncmec_file_id: args.ncmecFileId, - submitted_at: args.status === 'submitted' ? now : null, - submitted_by_admin_id: BigInt(args.adminUserId), - reporter_full_name: args.reporterFullName, - failure_reason: args.failureReason, - archive_completed_at: args.existing?.archive_completed_at ?? null, - content_deleted_at: args.existing?.content_deleted_at ?? null, - created_at: args.existing?.created_at ?? now, - updated_at: now, - }); - } - - private async deleteMessageSilently( - channelId: ChannelID, - messageId: MessageID, - fallbackUserId: UserID, - ): Promise { - const channel = await this.deps.channelRepository.findUnique(channelId); - const message = await this.deps.channelRepository.getMessage(channelId, messageId); - if (!message) return; - await purgeMessageAttachments(message, this.deps.storageService, this.deps.purgeQueue); - await this.deps.channelRepository.deleteMessage( - channelId, - messageId, - message.authorId ?? fallbackUserId, - message.pinnedTimestamp || undefined, - ); - await decrementThreadMessageCount(this.deps.channelRepository, channel, [messageId]); - await deleteMessageSearchDocuments([messageId], {context: {source: 'ncmec_submission_delete'}}); - await enqueueCrosspostSourceRemoval(this.deps.workerService, {messages: [message], mode: 'purge'}); - await enqueueCrosspostFamilyPurgeFromCopies(this.deps.workerService, {messages: [message]}); - if (!channel) return; - if (channel.guildId) { - await this.deps.gatewayService.dispatchGuild({ - guildId: channel.guildId, - event: 'MESSAGE_DELETE', - data: withThreadContext(channel, {channel_id: channelId.toString(), id: messageId.toString()}), - }); - return; - } - for (const recipientId of channel.recipientIds) { - await this.deps.gatewayService.dispatchPresence({ - userId: recipientId, - event: 'MESSAGE_DELETE', - data: {channel_id: channelId.toString(), id: messageId.toString()}, - }); - } - } - - private async requeueFinalizer(attachmentId: AttachmentID, requeueCount: number): Promise { - await this.deps.workerService.addJob( - 'finalizeNcmecAttachmentReport', - {attachmentId: attachmentId.toString(), requeueCount}, - {jobKey: finalizeJobKey(attachmentId), runAt: new Date(Date.now() + getFinalizeDelayMs())}, - ); - } -} - -function toAttachmentStatusResponse(submission: NcmecAttachmentSubmissionRow | null): NcmecAttachmentStatusResponse { - if (!submission) { - return { - status: 'not_submitted', - ncmec_report_id: null, - submitted_at: null, - submitted_by_admin_id: null, - failure_reason: null, - }; - } - return { - status: submission.status as NcmecSubmissionStatus, - ncmec_report_id: submission.ncmec_report_id, - submitted_at: submission.submitted_at?.toISOString() ?? null, - submitted_by_admin_id: submission.submitted_by_admin_id?.toString() ?? null, - failure_reason: submission.failure_reason, - }; -} - -function buildAdditionalInfo(args: { - channelId: ChannelID; - messageId: MessageID; - attachmentId: AttachmentID; - userId: UserID | null; - sourceReportId: ReportID | null; - priorReportIds: ReadonlyArray; - reportedUser: ReportedUserSnapshot; - attachmentUploadTrace: AttachmentUploadTraceByAttachmentRow | null; -}): string { - const lines = [ - 'Fluxer admin attachment report.', - `Channel ID: ${args.channelId.toString()}`, - `Message ID: ${args.messageId.toString()}`, - `Attachment ID: ${args.attachmentId.toString()}`, - `Reported User ID: ${args.userId !== null ? args.userId.toString() : 'unknown'}`, - ]; - if (args.reportedUser.email) { - lines.push(`Reported User Email: ${args.reportedUser.email}`); - lines.push(`Reported User Email Verified: ${args.reportedUser.emailVerified ? 'true' : 'false'}`); - } - if (args.reportedUser.dateOfBirth) { - lines.push(`Reported User Date of Birth: ${args.reportedUser.dateOfBirth}`); - } - if (args.attachmentUploadTrace) { - lines.push(`Attachment Upload Mode: ${args.attachmentUploadTrace.upload_mode}`); - lines.push(`Attachment Upload Request IP: ${args.attachmentUploadTrace.request_ip}`); - lines.push(`Attachment Upload Request Time: ${args.attachmentUploadTrace.requested_at.toISOString()}`); - lines.push(`Attachment Upload IP Source: ${describeBestEffortUploadIpSource(args.attachmentUploadTrace)}`); - if (args.attachmentUploadTrace.completion_ip) { - lines.push(`Attachment Upload Completion IP: ${args.attachmentUploadTrace.completion_ip}`); - } - if (args.attachmentUploadTrace.completed_at) { - lines.push(`Attachment Upload Completion Time: ${args.attachmentUploadTrace.completed_at.toISOString()}`); - } - } - if (!args.attachmentUploadTrace && args.reportedUser.lastActiveIp) { - lines.push(`Attachment Upload IP Unavailable; Current Last Active IP: ${args.reportedUser.lastActiveIp}`); - } - if (args.sourceReportId) lines.push(`Source Report ID: ${args.sourceReportId.toString()}`); - if (args.priorReportIds.length > 0) { - lines.push(`Previous NCMEC Report IDs: ${args.priorReportIds.join(', ')}`); - } - return lines.join('\n'); -} - -function buildReportedUserPerson(reportedUser: ReportedUserSnapshot): { - emails: Array<{ - address: string; - verified: boolean; - type: 'Home'; - }>; - phones: Array<{ - number: string; - type: 'Mobile'; - }>; - dateOfBirth: string | null; -} | null { - const emails = reportedUser.email - ? [{address: reportedUser.email, verified: reportedUser.emailVerified, type: 'Home' as const}] - : []; - const phones: Array<{number: string; type: 'Mobile'}> = []; - if (emails.length === 0 && !reportedUser.dateOfBirth) { - return null; - } - return { - emails, - phones, - dateOfBirth: reportedUser.dateOfBirth, - }; -} - -function buildReportedUserIpCaptureEvents(trace: AttachmentUploadTraceByAttachmentRow | null): Array<{ - ipAddress: string; - eventName: 'Upload'; - dateTime: Date; -}> { - if (!trace) { - return []; - } - const events = [{ipAddress: trace.request_ip, eventName: 'Upload' as const, dateTime: trace.requested_at}]; - if (trace.completion_ip && trace.completed_at && trace.completion_ip !== trace.request_ip) { - events.push({ipAddress: trace.completion_ip, eventName: 'Upload', dateTime: trace.completed_at}); - } - return events; -} - -function buildBestEffortUploadIpCaptureEvent(trace: AttachmentUploadTraceByAttachmentRow | null): { - ipAddress: string; - eventName: 'Upload'; - dateTime: Date; -} | null { - if (!trace) { - return null; - } - if (trace.completion_ip && trace.completed_at) { - return {ipAddress: trace.completion_ip, eventName: 'Upload', dateTime: trace.completed_at}; - } - return {ipAddress: trace.request_ip, eventName: 'Upload', dateTime: trace.requested_at}; -} - -function getBestEffortUploadTimestamp(trace: AttachmentUploadTraceByAttachmentRow | null): Date | null { - return trace?.completed_at ?? trace?.requested_at ?? null; -} - -function describeBestEffortUploadIpSource(trace: AttachmentUploadTraceByAttachmentRow): string { - if (trace.completion_ip && trace.completed_at) { - return 'multipart completion callback observed by Fluxer API'; - } - if (trace.upload_mode === 'form_data') { - return 'attachment upload request handled directly by Fluxer API'; - } - return 'presigned upload authorization request observed by Fluxer API'; -} - -function buildAuditMetadata(args: { - attachment: ResolvedAttachment; - reportedUser: ReportedUserSnapshot; - attachmentUploadTrace: AttachmentUploadTraceByAttachmentRow | null; -}): Map { - const metadata = new Map([ - ['channel_id', args.attachment.channelId.toString()], - ['message_id', args.attachment.messageId.toString()], - ['attachment_id', args.attachment.attachmentId.toString()], - ]); - if (args.reportedUser.email) metadata.set('reported_user_email', args.reportedUser.email); - metadata.set('reported_user_email_verified', args.reportedUser.emailVerified ? 'true' : 'false'); - if (args.reportedUser.dateOfBirth) metadata.set('reported_user_date_of_birth', args.reportedUser.dateOfBirth); - if (args.reportedUser.lastActiveIp) metadata.set('reported_user_last_active_ip', args.reportedUser.lastActiveIp); - if (args.attachmentUploadTrace) { - metadata.set('attachment_upload_mode', args.attachmentUploadTrace.upload_mode); - metadata.set('attachment_upload_request_ip', args.attachmentUploadTrace.request_ip); - metadata.set('attachment_upload_requested_at', args.attachmentUploadTrace.requested_at.toISOString()); - metadata.set('attachment_upload_ip_source', describeBestEffortUploadIpSource(args.attachmentUploadTrace)); - if (args.attachmentUploadTrace.completion_ip) { - metadata.set('attachment_upload_completion_ip', args.attachmentUploadTrace.completion_ip); - } - if (args.attachmentUploadTrace.completed_at) { - metadata.set('attachment_upload_completed_at', args.attachmentUploadTrace.completed_at.toISOString()); - } - } - return metadata; -} - -function findEmbedReferencedAttachmentMedia(message: Message, targetCdnUrl: string): EmbedMedia | null { - const scan = (embeds: Array): EmbedMedia | null => { - for (const embed of embeds) { - for (const media of [embed.image, embed.thumbnail, embed.video, embed.audio]) { - if (media?.url === targetCdnUrl) { - return media; - } - } - } - return null; - }; - const direct = scan(message.embeds); - if (direct) { - return direct; - } - for (const snapshot of message.messageSnapshots) { - const found = scan(snapshot.embeds); - if (found) { - return found; - } - } - return null; -} - -function findAttachmentInReport( - report: IARSubmission, - channelId: ChannelID, - messageId: MessageID, - attachmentId: AttachmentID, - filename: string, -): { - message: IARMessageContext; - attachment: IARMessageContext['attachments'][number]; -} | null { - for (const message of report.messageContext ?? []) { - if (message.messageId !== messageId) continue; - if (message.channelId !== channelId) continue; - const attachment = message.attachments.find( - (item) => item.attachment_id === attachmentId && item.filename === filename, - ); - if (attachment) return {message, attachment}; - } - return null; -} - -function formatNcmecAuditLogReason(reportId: string, contextLabel: string): string { - return `NCMEC Report ${reportId} - ${contextLabel}`; -} - -function finalizeJobKey(attachmentId: AttachmentID): string { - return `ncmec-finalize:${attachmentId.toString()}`; -} - -function sanitizeFailureMessage(rawMessage: string): string { - const lowered = rawMessage.toLowerCase(); - if (lowered.includes('http 401') || lowered.includes('unauthorized')) { - return 'NCMEC authentication failed. Check the service credentials.'; - } - if (lowered.includes('responsecode')) { - const match = rawMessage.match(/responseCode\s+(-?\d+)/i); - return match ? `NCMEC rejected the submission (responseCode ${match[1]}).` : 'NCMEC rejected the submission.'; - } - if (lowered.includes('http ')) { - const match = rawMessage.match(/http\s+(\d{3})/i); - return match ? `NCMEC request failed (HTTP ${match[1]}).` : 'NCMEC request failed.'; - } - return 'NCMEC submission failed. See server logs for details.'; -} - -function validateImageAttachment(contentType: string | null | undefined): void { - if (contentType?.startsWith('image/') || contentType?.startsWith('video/')) return; - throw InputValidationError.fromCode('attachment_id', ValidationErrorCodes.NCMEC_ATTACHMENT_MUST_BE_IMAGE_OR_VIDEO); -} - -function requireReporterEmail(email: string | null): string { - const trimmed = email?.trim(); - if (trimmed) { - return trimmed; - } - throw new Error('NCMEC reporter email is required for submission.'); -} diff --git a/fluxer_api/src/api/csam/tests/NcmecReporter.test.ts b/fluxer_api/src/api/csam/tests/NcmecReporter.test.ts deleted file mode 100644 index 13e3058a4..000000000 --- a/fluxer_api/src/api/csam/tests/NcmecReporter.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import { - buildNcmecFileDetailsXml, - buildNcmecReportXml, - createNcmecApiConfig, - NcmecReporter, -} from '@app/api/csam/NcmecReporter'; -import {INVALID_REPORT_XML, MALFORMED_REPORT_XML} from '@app/api/test/fixtures/ncmec/NcmecXmlFixtures'; -import {fakeNcmecServer} from '@app/api/test/Setup'; -import {describe, expect, test} from 'vitest'; - -function reporter(): NcmecReporter { - return new NcmecReporter({config: createNcmecApiConfig(), fetch}); -} - -function validReportXml(overrides: {priorNcmecReportIds?: Array} = {}): string { - return buildNcmecReportXml({ - attachmentUrl: 'https://cdn.example.com/x.png', - reportedAt: new Date('2026-04-15T12:00:00Z'), - reporterFullName: 'Lilith Example', - reporterEmail: 'admin@example.com', - reportedUser: { - id: 42n, - screenName: 'evil-user', - displayName: 'Evil User', - espService: 'Fluxer', - permanentlyDisabledAt: null, - person: { - emails: [{address: 'evil-user@example.com', type: 'Home', verified: true}], - phones: [{number: '+15551234567', type: 'Mobile'}], - dateOfBirth: '1999-02-03', - }, - ipCaptureEvents: [ - { - ipAddress: '203.0.113.10', - eventName: 'Upload', - dateTime: new Date('2026-04-15T11:55:00Z'), - }, - ], - }, - priorNcmecReportIds: overrides.priorNcmecReportIds ?? [], - additionalInfo: 'Test report.', - }); -} - -describe('NcmecReporter', () => { - test('submitReport returns a numeric report ID', async () => { - const reportId = await reporter().submitReport(validReportXml()); - expect(reportId).toMatch(/^\d+$/); - expect(fakeNcmecServer.getReport(reportId)).not.toBeNull(); - }); - test('uploadEvidence returns the server-computed md5', async () => { - const r = reporter(); - const reportId = await r.submitReport(validReportXml()); - const upload = await r.uploadEvidence(reportId, new Uint8Array([1, 2, 3]), 'evidence.png'); - expect(upload.fileId).toMatch(/^file-\d+$/); - expect(upload.md5).toMatch(/^[0-9a-f]{32}$/); - }); - test('submitFileDetails marks the file as having details submitted', async () => { - const r = reporter(); - const reportId = await r.submitReport(validReportXml()); - const {fileId} = await r.uploadEvidence(reportId, new Uint8Array([4, 5, 6]), 'evidence.png'); - await r.submitFileDetails( - buildNcmecFileDetailsXml({ - reportId, - fileId, - filename: 'evidence.png', - uploadedToEspTimestamp: new Date('2026-04-15T11:55:00Z'), - fileViewedByEsp: true, - ipCaptureEvent: { - ipAddress: '203.0.113.10', - eventName: 'Upload', - dateTime: new Date('2026-04-15T11:55:00Z'), - }, - additionalInfo: 'context', - }), - ); - const record = fakeNcmecServer.getReport(reportId)!; - expect(record.files.find((f) => f.fileId === fileId)?.submittedDetails).toBe(true); - }); - test('submitFileDetails throws when file details fail documented validation', async () => { - await expect( - reporter().submitFileDetails(` - - missing-ids.jpg -`), - ).rejects.toThrow('responseCode 4100'); - }); - test('finish returns every uploaded file id', async () => { - const r = reporter(); - const reportId = await r.submitReport(validReportXml()); - const a = await r.uploadEvidence(reportId, new Uint8Array([7]), 'a.png'); - const b = await r.uploadEvidence(reportId, new Uint8Array([8]), 'b.png'); - const result = await r.finish(reportId); - expect(result.reportId).toBe(reportId); - expect(result.fileIds).toEqual([a.fileId, b.fileId]); - }); - test('retract marks the report as retracted', async () => { - const r = reporter(); - const reportId = await r.submitReport(validReportXml()); - await r.retract(reportId); - expect(fakeNcmecServer.getReport(reportId)!.retracted).toBe(true); - }); - test('submitReport throws when the server rejects the XML', async () => { - await expect(reporter().submitReport(INVALID_REPORT_XML)).rejects.toThrow('responseCode 4100'); - }); - test('submitReport surfaces the documented malformed-xml response code', async () => { - await expect(reporter().submitReport(MALFORMED_REPORT_XML)).rejects.toThrow('responseCode 4110'); - }); - test('submitReport throws when the server returns 401', async () => { - fakeNcmecServer.setFailure('unauthorized'); - await expect(reporter().submitReport(validReportXml())).rejects.toThrow(/http 401/); - fakeNcmecServer.setFailure('none'); - }); - test('buildNcmecReportXml emits personOrUserReported with priorCTReports and espIdentifier', () => { - const xml = validReportXml({priorNcmecReportIds: ['NCMEC-100', 'NCMEC-200']}); - expect(xml).toContain(''); - expect(xml).toContain(''); - expect(xml).toContain('42'); - expect(xml).toContain('Fluxer'); - expect(xml).toContain('evil-user'); - expect(xml).toContain('Evil User'); - expect(xml).toContain('evil-user@example.com'); - expect(xml).toContain('+15551234567'); - expect(xml).toContain('1999-02-03'); - expect(xml).toContain('203.0.113.10'); - expect(xml).toContain('Upload'); - expect(xml).toContain('NCMEC-100'); - expect(xml).toContain('NCMEC-200'); - expect(xml).toContain('true'); - expect(xml).toContain('Child Pornography (possession, manufacture, and distribution)'); - expect(xml).toContain('Fluxer'); - }); - test('buildNcmecReportXml emits incident additionalInfo at the report root', () => { - const xml = validReportXml(); - expect(xml).toContain('Test report.'); - expect(xml).toMatch(/<\/personOrUserReported>\s*Test report\.<\/additionalInfo>\s*<\/report>/); - }); - test('buildNcmecReportXml emits accountPermanentlyDisabled when the user was banned', () => { - const xml = buildNcmecReportXml({ - attachmentUrl: 'https://cdn.example.com/x.png', - reportedAt: new Date('2026-04-15T12:00:00Z'), - reporterFullName: 'Lilith Example', - reporterEmail: 'admin@example.com', - reportedUser: { - id: 42n, - screenName: null, - displayName: null, - espService: null, - permanentlyDisabledAt: new Date('2026-04-14T08:00:00Z'), - person: null, - ipCaptureEvents: [], - }, - priorNcmecReportIds: [], - additionalInfo: null, - }); - expect(xml).toContain(' { - const xml = buildNcmecFileDetailsXml({ - reportId: 'NCMEC-100', - fileId: 'file-100', - filename: 'evidence.png', - uploadedToEspTimestamp: new Date('2026-04-15T11:55:00Z'), - fileViewedByEsp: true, - ipCaptureEvent: { - ipAddress: '203.0.113.10', - eventName: 'Upload', - dateTime: new Date('2026-04-15T11:55:00Z'), - }, - additionalInfo: 'Uploaded from Fluxer', - }); - expect(xml).toContain('2026-04-15T11:55:00.000Z'); - expect(xml).toContain('true'); - expect(xml).toContain(''); - expect(xml).toContain('203.0.113.10'); - expect(xml).toContain('Uploaded from Fluxer'); - }); - test('buildNcmecReportXml requires a reporter email address', () => { - expect(() => - buildNcmecReportXml({ - attachmentUrl: 'https://cdn.example.com/x.png', - reportedAt: new Date('2026-04-15T12:00:00Z'), - reporterFullName: 'Lilith Example', - reporterEmail: '' as string, - reportedUser: { - id: 42n, - screenName: null, - displayName: null, - espService: null, - permanentlyDisabledAt: null, - person: null, - ipCaptureEvents: [], - }, - priorNcmecReportIds: [], - additionalInfo: null, - }), - ).toThrow('NCMEC reporter email'); - }); -}); diff --git a/fluxer_api/src/api/database/types/CsamTypes.ts b/fluxer_api/src/api/database/types/CsamTypes.ts deleted file mode 100644 index d0f6b7a86..000000000 --- a/fluxer_api/src/api/database/types/CsamTypes.ts +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export interface NcmecAttachmentSubmissionRow { - attachment_id: bigint; - user_id: bigint | null; - channel_id: bigint; - message_id: bigint; - filename: string; - source_report_id: bigint | null; - status: string; - ncmec_report_id: string | null; - ncmec_file_id: string | null; - submitted_at: Date | null; - submitted_by_admin_id: bigint | null; - reporter_full_name: string | null; - failure_reason: string | null; - archive_completed_at: Date | null; - content_deleted_at: Date | null; - created_at: Date; - updated_at: Date; -} - -export const NCMEC_ATTACHMENT_SUBMISSION_COLUMNS = [ - 'attachment_id', - 'user_id', - 'channel_id', - 'message_id', - 'filename', - 'source_report_id', - 'status', - 'ncmec_report_id', - 'ncmec_file_id', - 'submitted_at', - 'submitted_by_admin_id', - 'reporter_full_name', - 'failure_reason', - 'archive_completed_at', - 'content_deleted_at', - 'created_at', - 'updated_at', -] as const satisfies ReadonlyArray; - -export interface NcmecUserWorkflowRow { - user_id: bigint; - deleted_at: Date | null; - deleted_by_admin_id: bigint | null; - deletion_private_reason: string | null; - deletion_ncmec_report_id: string | null; - archive_id: bigint | null; - archive_requested_at: Date | null; - archive_requested_by_admin_id: bigint | null; - archive_audit_log_reason: string | null; - archive_completed_at: Date | null; - deletion_job_queued_at: Date | null; - previous_report_ids: Set | null; - created_at: Date; - updated_at: Date; -} - -export const NCMEC_USER_WORKFLOW_COLUMNS = [ - 'user_id', - 'deleted_at', - 'deleted_by_admin_id', - 'deletion_private_reason', - 'deletion_ncmec_report_id', - 'archive_id', - 'archive_requested_at', - 'archive_requested_by_admin_id', - 'archive_audit_log_reason', - 'archive_completed_at', - 'deletion_job_queued_at', - 'previous_report_ids', - 'created_at', - 'updated_at', -] as const satisfies ReadonlyArray; diff --git a/fluxer_api/src/api/experiment/tests/ChannelLoadGateCoverage.test.ts b/fluxer_api/src/api/experiment/tests/ChannelLoadGateCoverage.test.ts index 31f784f79..d1441f2aa 100644 --- a/fluxer_api/src/api/experiment/tests/ChannelLoadGateCoverage.test.ts +++ b/fluxer_api/src/api/experiment/tests/ChannelLoadGateCoverage.test.ts @@ -37,7 +37,6 @@ const EXPECTED_LOADS: Readonly> = { 'channel/services/message/UserMessageDeletionService.ts': [2, 'admin_or_system'], 'channel/services/thread/ThreadCreationService.ts': [4, 'gated'], 'channel/services/thread/ThreadDeletionService.ts': [1, 'admin_or_system'], - 'csam/NcmecSubmissionService.ts': [1, 'admin_or_system'], 'guild/services/channel/ChannelOperationsService.ts': [2, 'gated'], 'guild/services/GuildDiscoveryService.ts': [1, 'gated'], 'guild/services/data/GuildOperationsService.ts': [2, 'gated'], diff --git a/fluxer_api/src/api/middleware/ServiceMiddleware.ts b/fluxer_api/src/api/middleware/ServiceMiddleware.ts index 20fc0696f..195a91538 100644 --- a/fluxer_api/src/api/middleware/ServiceMiddleware.ts +++ b/fluxer_api/src/api/middleware/ServiceMiddleware.ts @@ -92,7 +92,6 @@ import { getKVBulkMessageDeletionQueue, getKVThreadAutoArchiveQueue, getLimitConfigService, - getNcmecSubmissionService, getOAuth2TokenRepository, getPasswordChangeRepository, getPremiumStateReconciliationQueueService, @@ -428,10 +427,6 @@ class RequestServices implements RequestScopedServices { return getLimitConfigService(); } - get ncmecSubmissionService() { - return getNcmecSubmissionService(); - } - get oauth2TokenRepository() { return getOAuth2TokenRepository(); } diff --git a/fluxer_api/src/api/middleware/ServiceSingletons.ts b/fluxer_api/src/api/middleware/ServiceSingletons.ts index d3090212e..8c653912d 100644 --- a/fluxer_api/src/api/middleware/ServiceSingletons.ts +++ b/fluxer_api/src/api/middleware/ServiceSingletons.ts @@ -5,16 +5,12 @@ import {AdminApiKeyRepository} from '@app/api/admin/repositories/AdminApiKeyRepo import {AdminArchiveRepository} from '@app/api/admin/repositories/AdminArchiveRepository'; import {AdminApiKeyService} from '@app/api/admin/services/AdminApiKeyService'; import {AdminArchiveService} from '@app/api/admin/services/AdminArchiveService'; -import {AdminAuditService} from '@app/api/admin/services/AdminAuditService'; import {Config} from '@app/api/Config'; import {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {AttachmentUploadTraceRepository} from '@app/api/channel/repositories/message/AttachmentUploadTraceRepository'; import {StreamPreviewService} from '@app/api/channel/services/StreamPreviewService'; import type {APIConfig} from '@app/api/config/APIConfig'; import {ConnectionRepository} from '@app/api/connection/ConnectionRepository'; -import {createNcmecApiConfig, NcmecReporter} from '@app/api/csam/NcmecReporter'; -import {NcmecRepository} from '@app/api/csam/NcmecRepository'; -import {NcmecSubmissionService} from '@app/api/csam/NcmecSubmissionService'; import {DonationRepository} from '@app/api/donation/DonationRepository'; import {createEmailProvider} from '@app/api/email/EmailProviderFactory'; import {FavoriteMemeRepository} from '@app/api/favorite_meme/FavoriteMemeRepository'; @@ -321,40 +317,7 @@ export function getKVAccountDeletionQueue(): KVAccountDeletionQueueService { } export const getThemeService = singleton(() => new ThemeService(getStorageService())); -const getNcmecReporter = singleton(() => new NcmecReporter({config: createNcmecApiConfig(), fetch})); -const getNcmecRepository = singleton(() => new NcmecRepository()); export const getAttachmentUploadTraceRepository = singleton(() => new AttachmentUploadTraceRepository()); -export const getNcmecSubmissionService = singleton( - () => - new NcmecSubmissionService({ - reportRepository: getReportRepository(), - ncmecApi: getNcmecReporter(), - ncmecRepository: getNcmecRepository(), - attachmentUploadTraceRepository: getAttachmentUploadTraceRepository(), - storageService: getStorageService(), - channelRepository: getChannelRepository(), - userRepository: getUserRepository(), - guildRepository: getGuildRepository(), - gatewayService: getGatewayService(), - userCacheService: createUserCacheService(), - adminArchiveService: new AdminArchiveService( - getAdminArchiveRepository(), - getUserRepository(), - getGuildRepository(), - getStorageService(), - getSnowflakeService(), - getWorkerService(), - ), - adminAuditService: new AdminAuditService(getAdminRepository(), getSnowflakeService(), { - userRepository: getUserRepository(), - guildRepository: getGuildRepository(), - channelRepository: getChannelRepository(), - }), - purgeQueue: getPurgeQueue(), - workerService: getWorkerService(), - deletionQueue: getKVAccountDeletionQueue(), - }), -); let _virusScanInitPromise: Promise | null = null; diff --git a/fluxer_api/src/api/middleware/tests/LazyServiceProvider.test.ts b/fluxer_api/src/api/middleware/tests/LazyServiceProvider.test.ts index 6c2db7ac2..e3230711c 100644 --- a/fluxer_api/src/api/middleware/tests/LazyServiceProvider.test.ts +++ b/fluxer_api/src/api/middleware/tests/LazyServiceProvider.test.ts @@ -48,7 +48,6 @@ const REQUEST_SERVICE_VARIABLES: ReadonlyArray = [ 'limitConfigService', 'mediaService', 'messageRequestService', - 'ncmecSubmissionService', 'oauth2ApplicationsRequestService', 'oauth2RequestService', 'oauth2Service', diff --git a/fluxer_api/src/api/openapi/openapi.json b/fluxer_api/src/api/openapi/openapi.json index f80285fee..cdae83658 100644 --- a/fluxer_api/src/api/openapi/openapi.json +++ b/fluxer_api/src/api/openapi/openapi.json @@ -39734,8 +39734,6 @@ "MAX_THREAD_MEMBERS", "MAX_WEBHOOKS_PER_CHANNEL", "MAX_WEBHOOKS_PER_GUILD", - "NCMEC_ALREADY_SUBMITTED", - "NCMEC_SUBMISSION_FAILED", "MEDIA_METADATA_ERROR", "METHOD_NOT_ALLOWED", "MISSING_ACCESS", @@ -40031,8 +40029,6 @@ "This thread has reached its member limit of {count}.", "You've reached the maximum of {count, plural, one {# webhook} other {# webhooks}} per channel.", "You've reached the maximum of {count, plural, one {# webhook} other {# webhooks}} per community.", - "This content has already been submitted to NCMEC.", - "We couldn't submit the report to NCMEC. Please try again later.", "Media metadata error.", "Method not allowed.", "You don't have access to this resource or feature.", diff --git a/fluxer_api/src/api/report/tests/ReportWebhookMessages.test.ts b/fluxer_api/src/api/report/tests/ReportWebhookMessages.test.ts index 6e4b4a41d..3950ffe51 100644 --- a/fluxer_api/src/api/report/tests/ReportWebhookMessages.test.ts +++ b/fluxer_api/src/api/report/tests/ReportWebhookMessages.test.ts @@ -11,7 +11,7 @@ import { } from '@app/api/auth/tests/AuthTestUtils'; import {createGuildID, createReportID, createUserID} from '@app/api/BrandedTypes'; import {authorizeBot, createTestBotAccount} from '@app/api/bot/tests/BotTestUtils'; -import {Config} from '@app/api/Config'; +import {Config, getConfig} from '@app/api/Config'; import {loadFixture} from '@app/api/channel/tests/AttachmentTestUtils'; import { acceptInvite, @@ -27,7 +27,7 @@ import {getPngDataUrl} from '@app/api/emoji/tests/EmojiTestUtils'; import {GuildMemberRepository} from '@app/api/guild/repositories/GuildMemberRepository'; import {resetActivityEventsForTests, startActivityEvents} from '@app/api/infrastructure/activity/ActivityEvents'; import type {ActivityPublisher} from '@app/api/infrastructure/activity/ActivitySpool'; -import {getRateLimitService} from '@app/api/middleware/ServiceSingletons'; +import {getInstanceConfigRepository, getRateLimitService} from '@app/api/middleware/ServiceSingletons'; import {getReportFlowVariant, type ReportFlowStepInput} from '@app/api/report/flows/ReportFlowRegistry'; import {ReportRepository} from '@app/api/report/ReportRepository'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; @@ -46,6 +46,7 @@ import { executeWebhook, executeWebhookWithAttachments, } from '@app/api/webhook/tests/WebhookTestUtils'; +import {AccountIdentityModes, TagStyles} from '@fluxer/constants/src/AccountIdentityConstants'; import {AdminACLs} from '@fluxer/constants/src/AdminACLs'; import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; @@ -336,14 +337,7 @@ async function expectStoredAuthor(reportId: string, world: World, kind: AuthorKi } async function createAdmin(harness: ApiTestHarness): Promise { - return setUserACLs(harness, await createTestAccount(harness), [ - AdminACLs.AUTHENTICATE, - AdminACLs.REPORT_VIEW, - AdminACLs.CSAM_SUBMIT_NCMEC, - AdminACLs.MESSAGE_DELETE, - AdminACLs.USER_DELETE, - AdminACLs.ARCHIVE_TRIGGER_USER, - ]); + return setUserACLs(harness, await createTestAccount(harness), [AdminACLs.AUTHENTICATE, AdminACLs.REPORT_VIEW]); } describe('Reports of webhook and bot messages', () => { @@ -577,6 +571,33 @@ describe('Reports of webhook and bot messages', () => { }); }); + test('the webhook creator tag follows the tag style of the instance', async () => { + const config = getConfig(); + const originalSelfHosted = config.instance.selfHosted; + config.instance.selfHosted = true; + try { + await getInstanceConfigRepository().setAccountIdentityMode(AccountIdentityModes.EMAIL, 'setup', TagStyles.NONE); + const world = await setupWorld(harness); + const report = await submitFlow(harness, world.reporter.token, world.channelId, world.messages.webhook) + .expect(HTTP_STATUS.OK) + .execute(); + const admin = await createAdmin(harness); + const detail = await createBuilder(harness, admin.token) + .get(`/admin/reports/${report.report_id}`) + .expect(HTTP_STATUS.OK) + .execute(); + expect(detail).toMatchObject({ + reported_webhook_creator_id: world.owner.userId, + reported_webhook_creator_tag: world.creator.username, + reported_webhook_creator_username: world.creator.username, + reported_webhook_creator_discriminator: '0000', + }); + } finally { + config.instance.selfHosted = originalSelfHosted; + getInstanceConfigRepository().clearCacheForTesting(); + } + }); + test('the admin API shows webhook fields and search filters by webhook', async () => { const world = await setupWorld(harness); const webhookReport = await submitFlow(harness, world.reporter.token, world.channelId, world.messages.webhook) @@ -624,7 +645,6 @@ describe('Reports of webhook and bot messages', () => { author_id: world.webhook.id, author_username: WEBHOOK_USERNAME, webhook_id: world.webhook.id, - user_prior_ncmec_report_ids: [], }); const neighbor = detail.message_context!.find((entry) => entry.id === world.messages.user)!; expect(neighbor).toMatchObject({author_id: world.author.userId, webhook_id: null}); @@ -751,7 +771,7 @@ describe('Reports of webhook and bot messages', () => { }); }); - test('webhook attachments are kept as evidence and can go to NCMEC through the report', async () => { + test('webhook attachments are kept as evidence after the message is deleted', async () => { const world = await setupWorld(harness); const {json: message} = await executeWebhookWithAttachments(harness, { webhookId: world.webhook.id, @@ -770,25 +790,15 @@ describe('Reports of webhook and bot messages', () => { const key = `attachments/${world.channelId}/${attachment.id}/evidence.png`; expect(harness.storageService.hasObject(Config.s3.buckets.reports, key)).toBe(true); await deleteWebhookMessageByToken(harness, world.webhook.id, world.webhook.token, message!.id); + expect(harness.storageService.hasObject(Config.s3.buckets.reports, key)).toBe(true); const admin = await createAdmin(harness); - await createBuilder(harness, admin.token) - .post('/admin/messages/ncmec-reports') - .body({ - channel_id: world.channelId, - message_id: message!.id, - attachment_id: attachment.id, - filename: 'evidence.png', - reporter_full_name: 'Webhook Reporter', - confirmed_viewed: true, - source_report_id: result.report_id, - }) - .expect(HTTP_STATUS.OK) - .execute(); const detail = await createBuilder(harness, admin.token) .get(`/admin/reports/${result.report_id}`) .expect(HTTP_STATUS.OK) .execute(); const evidence = detail.message_context!.find((entry) => entry.id === message!.id)!; - expect(evidence.attachments[0]?.ncmec_status).not.toBe('not_submitted'); + expect(evidence.attachments.map((entry) => ({id: entry.id, filename: entry.filename}))).toEqual([ + {id: attachment.id, filename: 'evidence.png'}, + ]); }); }); diff --git a/fluxer_api/src/api/test/Setup.ts b/fluxer_api/src/api/test/Setup.ts index 637be1f1a..d97f610ea 100644 --- a/fluxer_api/src/api/test/Setup.ts +++ b/fluxer_api/src/api/test/Setup.ts @@ -23,7 +23,6 @@ import {MockKVProvider} from '@app/api/test/mocks/MockKVProvider'; import {MockSnowflakeService} from '@app/api/test/mocks/MockSnowflakeService'; import {NoopLogger} from '@app/api/test/mocks/NoopLogger'; import {RepositoryBackedMessageResponseDataService} from '@app/api/test/mocks/RepositoryBackedMessageResponseDataService'; -import {fakeNcmecServer} from '@app/api/test/msw/handlers/NcmecHandlers'; import {server} from '@app/api/test/msw/server'; import {mapUserToPartialResponse} from '@app/api/user/UserMappers'; import {loadConfig} from '@fluxer/config/src/ConfigLoader'; @@ -184,13 +183,6 @@ const testApiConfig: APIConfig = { sandboxUserIds: [], sandboxEntitlesAll: false, }, - ncmec: { - ...apiConfig.ncmec, - enabled: true, - baseUrl: fakeNcmecServer.baseUrl, - username: 'usr123', - password: 'pswd123', - }, }; testApiConfig.dev.relaxRegistrationRateLimits = true; testApiConfig.dev.disableRateLimits = true; @@ -211,8 +203,6 @@ setInjectedMessageResponseDataService(new RepositoryBackedMessageResponseDataSer enableSearchTaskTracking(); -export {fakeNcmecServer}; - beforeAll(async () => { server.listen({ onUnhandledRequest: 'error', @@ -224,7 +214,6 @@ afterEach(async () => { resetCassandraQueryExecutorForTesting(); getInstanceConfigRepository().clearCacheForTesting(); server.resetHandlers(); - fakeNcmecServer.reset(); }); afterAll(async () => { diff --git a/fluxer_api/src/api/test/fixtures/ncmec/NcmecXmlFixtures.ts b/fluxer_api/src/api/test/fixtures/ncmec/NcmecXmlFixtures.ts deleted file mode 100644 index 6955cf7b1..000000000 --- a/fluxer_api/src/api/test/fixtures/ncmec/NcmecXmlFixtures.ts +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export const INVALID_REPORT_XML = ` - - - - John - Smith - jsmith@example.com - - -`; -export const MALFORMED_REPORT_XML = ` - - - Child Pornography (possession, manufacture, and distribution) - 2012-10-15T08:00:00-07:00 - - - - John - Smith - jsmith@example.com - - -; - finished: boolean; - retracted: boolean; - authUsername: string; -} - -const BASE_URL = 'https://ncmec.test/ispws'; -const USERNAME = 'usr123'; -const PASSWORD = 'pswd123'; -const xmlParser = new XMLParser({ignoreAttributes: false, parseTagValue: false, trimValues: true}); - -class MswNcmecServer { - readonly baseUrl = BASE_URL; - private reportCounter = 1000000; - private fileCounter = 1; - private requestCounter = 1; - private readonly reports = new Map(); - private failureMode: FakeNcmecFailure = 'none'; - - reset(): void { - this.reports.clear(); - this.reportCounter = 1000000; - this.fileCounter = 1; - this.requestCounter = 1; - this.failureMode = 'none'; - } - - setFailure(mode: FakeNcmecFailure): void { - this.failureMode = mode; - } - - getReports(): Array { - return [...this.reports.values()]; - } - - getReport(reportId: string): FakeNcmecReportRecord | null { - return this.reports.get(reportId) ?? null; - } - - createHandlers(): Array { - return [ - http.get(`${BASE_URL}/status`, ({request}) => this.handleStatus(request)), - http.get(`${BASE_URL}/xsd`, () => this.text(200, '', 'text/plain; charset=utf-8')), - http.post(`${BASE_URL}/submit`, ({request}) => this.handleSubmit(request)), - http.post(`${BASE_URL}/upload`, ({request}) => this.handleUpload(request)), - http.post(`${BASE_URL}/fileinfo`, ({request}) => this.handleFileInfo(request)), - http.post(`${BASE_URL}/finish`, ({request}) => this.handleFinish(request)), - http.post(`${BASE_URL}/retract`, ({request}) => this.handleRetract(request)), - ]; - } - - private handleStatus(request: Request): HttpResponse { - const unauthorized = this.maybeUnauthorized(request); - if (unauthorized) return unauthorized; - return this.xml( - 200, - ok(`Remote User : ${USERNAME}, Remote Ip : 127.0.0.1`), - ); - } - - private async handleSubmit(request: Request): Promise> { - const failure = this.effectiveFailure(request); - const unauthorized = this.maybeUnauthorized(request, failure); - if (unauthorized) return unauthorized; - if (failure === 'submit') return this.xml(200, errorXml(1100, 'Save failed')); - if (!isXmlContentType(request)) return this.xml(200, errorXml(4000, 'Invalid request')); - const body = await request.text(); - if (XMLValidator.validate(body) !== true) return this.xml(200, errorXml(4110, 'Malformed XML submittal')); - const report = parseReportDocument(body); - if (!hasRequiredReportFields(report)) return this.xml(200, errorXml(4100, 'Validation failed')); - const reportId = String(this.reportCounter++); - this.reports.set(reportId, { - reportId, - reportXml: body, - files: [], - finished: false, - retracted: false, - authUsername: USERNAME, - }); - return this.xml(200, ok(`${reportId}`)); - } - - private async handleUpload(request: Request): Promise> { - const failure = this.effectiveFailure(request); - const unauthorized = this.maybeUnauthorized(request, failure); - if (unauthorized) return unauthorized; - if (failure === 'upload') return this.xml(200, errorXml(1111, 'File upload failed')); - const form = await request.formData(); - const reportId = String(form.get('id') ?? ''); - const file = form.get('file'); - const record = this.reports.get(reportId); - if (!reportId || !record) return this.xml(200, errorXml(5001, 'Report does not exist')); - if (record.retracted) return this.xml(200, errorXml(5101, 'Report already retracted')); - if (record.finished) return this.xml(200, errorXml(5102, 'Report already finished')); - if (!(file instanceof Blob)) return this.xml(200, errorXml(4200, 'Malformed file submittal')); - const buffer = Buffer.from(await file.arrayBuffer()); - const md5 = crypto.createHash('md5').update(buffer).digest('hex'); - const fileId = `file-${this.fileCounter++}`; - const fileRecord: FakeNcmecFileRecord = { - fileId, - filename: (file as File).name || 'upload.bin', - size: buffer.byteLength, - md5, - submittedDetails: false, - detailsXml: null, - }; - record.files.push(fileRecord); - return this.xml(200, ok(`${reportId}${fileId}${md5}`)); - } - - private async handleFileInfo(request: Request): Promise> { - const failure = this.effectiveFailure(request); - const unauthorized = this.maybeUnauthorized(request, failure); - if (unauthorized) return unauthorized; - if (failure === 'fileinfo') return this.xml(200, errorXml(1300, 'Update failed')); - if (!isXmlContentType(request)) return this.xml(200, errorXml(4000, 'Invalid request')); - const body = await request.text(); - if (XMLValidator.validate(body) !== true) return this.xml(200, errorXml(4110, 'Malformed XML submittal')); - const fileDetails = parseFileDetailsDocument(body); - if (!hasFileDetailsFields(fileDetails)) return this.xml(200, errorXml(4100, 'Validation failed')); - const reportId = nonBlankString(fileDetails?.reportId); - const fileId = nonBlankString(fileDetails?.fileId); - const record = reportId ? this.reports.get(reportId) : null; - if (!record) return this.xml(200, errorXml(5001, 'Report does not exist')); - const file = record.files.find((candidate) => candidate.fileId === fileId); - if (!file) return this.xml(200, errorXml(5002, 'File does not exist')); - file.submittedDetails = true; - file.detailsXml = body; - return this.xml(200, ok(`${reportId}`)); - } - - private async handleFinish(request: Request): Promise> { - const failure = this.effectiveFailure(request); - const unauthorized = this.maybeUnauthorized(request, failure); - if (unauthorized) return unauthorized; - if (failure === 'finish') return this.xml(200, errorXml(1300, 'Update failed')); - const form = await request.formData(); - const reportId = String(form.get('id') ?? ''); - const record = this.reports.get(reportId); - if (!record) return this.xml(200, errorXml(5001, 'Report does not exist')); - record.finished = true; - const files = record.files.map((file) => ` ${file.fileId}`).join('\n'); - return this.xml( - 200, - ` - - 0 - ${reportId} - -${files} - -`, - ); - } - - private async handleRetract(request: Request): Promise> { - const failure = this.effectiveFailure(request); - const unauthorized = this.maybeUnauthorized(request, failure); - if (unauthorized) return unauthorized; - if (failure === 'retract') return this.xml(200, errorXml(1300, 'Update failed')); - const form = await request.formData(); - const reportId = String(form.get('id') ?? ''); - const record = this.reports.get(reportId); - if (!record) return this.xml(200, errorXml(5001, 'Report does not exist')); - record.retracted = true; - return this.xml(200, ok(`${reportId}`)); - } - - private effectiveFailure(request: Request): FakeNcmecFailure { - return ((request.headers.get('x-test-fail') ?? '') || this.failureMode) as FakeNcmecFailure; - } - - private maybeUnauthorized(request: Request, failure = this.failureMode): HttpResponse | null { - if (failure === 'unauthorized') return this.text(401, ''); - const auth = request.headers.get('authorization') ?? ''; - if (!auth.toLowerCase().startsWith('basic ')) return this.text(401, ''); - const decoded = Buffer.from(auth.slice(6), 'base64').toString('utf-8'); - return decoded === `${USERNAME}:${PASSWORD}` ? null : this.text(401, ''); - } - - private xml(status: number, body: string): HttpResponse { - return this.text(status, body, 'application/xml; charset=utf-8'); - } - - private text(status: number, body: string, contentType = 'text/plain; charset=utf-8'): HttpResponse { - return new HttpResponse(body, { - status, - headers: { - 'content-type': contentType, - 'Request-ID': `req-${this.requestCounter++}`, - }, - }); - } -} - -function ok(extra: string): string { - return ` - - 0 - Success - ${extra} -`; -} - -function errorXml(code: number, description: string): string { - return ` - - ${code} - ${description} -`; -} - -function isXmlContentType(request: Request): boolean { - return (request.headers.get('content-type') ?? '').toLowerCase().includes('text/xml'); -} - -function parseReportDocument(xml: string): Record | null { - const parsed = xmlParser.parse(xml) as Record; - return objectField(parsed, 'report'); -} - -function parseFileDetailsDocument(xml: string): Record | null { - const parsed = xmlParser.parse(xml) as Record; - return objectField(parsed, 'fileDetails'); -} - -function hasRequiredReportFields(report: Record | null): boolean { - const incidentSummary = objectField(report, 'incidentSummary'); - const reporter = objectField(report, 'reporter'); - const reportingPerson = objectField(reporter, 'reportingPerson'); - return Boolean( - nonBlankString(incidentSummary?.incidentType) && - nonBlankString(incidentSummary?.incidentDateTime) && - nonBlankString(reportingPerson?.email), - ); -} - -function hasFileDetailsFields(fileDetails: Record | null): boolean { - return Boolean(nonBlankString(fileDetails?.reportId) && nonBlankString(fileDetails?.fileId)); -} - -function objectField(value: unknown, key: string): Record | null { - if (!value || typeof value !== 'object' || Array.isArray(value)) return null; - const field = (value as Record)[key]; - if (!field || typeof field !== 'object' || Array.isArray(field)) return null; - return field as Record; -} - -function nonBlankString(value: unknown): string | null { - if (typeof value === 'string') { - const trimmed = value.trim(); - return trimmed || null; - } - if (typeof value === 'number' || typeof value === 'bigint') { - return String(value); - } - return null; -} - -export const fakeNcmecServer = new MswNcmecServer(); - -export function createNcmecHandlers(): Array { - return fakeNcmecServer.createHandlers(); -} diff --git a/fluxer_api/src/api/test/msw/server.ts b/fluxer_api/src/api/test/msw/server.ts index 2c20bc59f..8a4b6a11e 100644 --- a/fluxer_api/src/api/test/msw/server.ts +++ b/fluxer_api/src/api/test/msw/server.ts @@ -1,13 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import {createNcmecHandlers} from '@app/api/test/msw/handlers/NcmecHandlers'; import {createOnionooDetailsHandler} from '@app/api/test/msw/handlers/OnionooHandlers'; import {createOpenNsfwHandlers} from '@app/api/test/msw/handlers/OpenNsfwHandlers'; import {createPwnedPasswordsRangeHandler} from '@app/api/test/msw/handlers/PwnedPasswordsHandlers'; import {setupServer} from 'msw/node'; export const server = setupServer( - ...createNcmecHandlers(), ...createOpenNsfwHandlers(), createOnionooDetailsHandler(), createPwnedPasswordsRangeHandler(), diff --git a/fluxer_api/src/api/types/HonoEnv.ts b/fluxer_api/src/api/types/HonoEnv.ts index 1d28ff320..cb138e570 100644 --- a/fluxer_api/src/api/types/HonoEnv.ts +++ b/fluxer_api/src/api/types/HonoEnv.ts @@ -18,7 +18,6 @@ import type {StreamService} from '@app/api/channel/services/StreamService'; import type {ThreadService} from '@app/api/channel/services/thread/ThreadService'; import type {ConnectionRequestService} from '@app/api/connection/ConnectionRequestService'; import type {ConnectionService} from '@app/api/connection/ConnectionService'; -import type {NcmecSubmissionService} from '@app/api/csam/NcmecSubmissionService'; import type {DonationService} from '@app/api/donation/DonationService'; import type {FavoriteMemeRequestService} from '@app/api/favorite_meme/FavoriteMemeRequestService'; import type {FavoriteMemeService} from '@app/api/favorite_meme/FavoriteMemeService'; @@ -160,7 +159,6 @@ export interface HonoEnv { reportService: ReportService; reportRequestService: ReportRequestService; contactChangeLogService: UserContactChangeLogService; - ncmecSubmissionService: NcmecSubmissionService; requestCache: RequestCache; rpcService: RpcService; searchService: SearchService; diff --git a/fluxer_api/src/api/utils/ExternalResponseLimits.ts b/fluxer_api/src/api/utils/ExternalResponseLimits.ts index 5a6d8df2a..29ff6a329 100644 --- a/fluxer_api/src/api/utils/ExternalResponseLimits.ts +++ b/fluxer_api/src/api/utils/ExternalResponseLimits.ts @@ -20,7 +20,6 @@ export const EXTERNAL_RESPONSE_LIMITS = { oEmbedBytes: 256 * 1024, pwnedPasswordsBytes: 1024 * 1024, externalTemplateBytes: 512 * 1024, - ncmecResponseBytes: 64 * 1024, fileBlocklistBytes: 25 * 1024 * 1024, urlBlocklistBytes: 25 * 1024 * 1024, } as const; diff --git a/fluxer_api/src/api/worker/WorkerDependencies.ts b/fluxer_api/src/api/worker/WorkerDependencies.ts index c636efdd6..550dd2de7 100644 --- a/fluxer_api/src/api/worker/WorkerDependencies.ts +++ b/fluxer_api/src/api/worker/WorkerDependencies.ts @@ -9,7 +9,6 @@ import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; import type {ConnectionRepository} from '@app/api/connection/ConnectionRepository'; import {ConnectionService} from '@app/api/connection/ConnectionService'; -import type {NcmecSubmissionService} from '@app/api/csam/NcmecSubmissionService'; import {DonationRepository} from '@app/api/donation/DonationRepository'; import type {IDonationRepository} from '@app/api/donation/IDonationRepository'; import type {FavoriteMemeRepository} from '@app/api/favorite_meme/FavoriteMemeRepository'; @@ -75,7 +74,6 @@ import { getKVActivityTracker, getKVBulkMessageDeletionQueue, getLimitConfigService, - getNcmecSubmissionService, getOAuth2TokenRepository, getPremiumStateReconciliationQueueService, getPurgeQueue, @@ -167,7 +165,6 @@ export interface WorkerDependencies { channelService: ChannelService; guildAuditLogService: GuildAuditLogService; contactChangeLogService: UserContactChangeLogService; - ncmecSubmissionService: NcmecSubmissionService; donationRepository: IDonationRepository; guildService: GuildService; billingRepository: BillingRepository; @@ -207,7 +204,6 @@ export async function initializeWorkerDependencies(snowflakeService: ISnowflakeS const connectionService = new ConnectionService(connectionRepository, gatewayService); const mediaService = getMediaService(); const discriminatorService = getDiscriminatorService(); - const ncmecSubmissionService = getNcmecSubmissionService(); const avatarService = getAvatarService(); const entityAssetService = getEntityAssetService(); await ensureVirusScanInitialized(); @@ -329,7 +325,6 @@ export async function initializeWorkerDependencies(snowflakeService: ISnowflakeS storeEntitlementService, guildAuditLogService, contactChangeLogService, - ncmecSubmissionService, get stripe() { return getStripeClient(); }, diff --git a/fluxer_api/src/api/worker/WorkerLaneConfig.ts b/fluxer_api/src/api/worker/WorkerLaneConfig.ts index 2e7350092..8e5c225a8 100644 --- a/fluxer_api/src/api/worker/WorkerLaneConfig.ts +++ b/fluxer_api/src/api/worker/WorkerLaneConfig.ts @@ -44,7 +44,6 @@ const LANE_CONFIG = { 'bulkDeleteUserMessagesScoped', 'deleteUserMessagesInGuildByTime', 'messageShred', - 'finalizeNcmecAttachmentReport', 'harvestGuildData', 'harvestUserData', 'batchGuildAuditLogMessageDeletes', @@ -61,7 +60,7 @@ const LANE_CONFIG = { 'bulkDeleteMessagesForUsers', 'drainActivitySpool', ] as const, - retiredTasks: ['sendScheduledMessage'], + retiredTasks: ['sendScheduledMessage', 'finalizeNcmecAttachmentReport'], concurrency: 8, maxAckPending: 50, ackWaitMs: 60000, diff --git a/fluxer_api/src/api/worker/WorkerTaskRegistry.ts b/fluxer_api/src/api/worker/WorkerTaskRegistry.ts index 6830a2f1f..e830cdfb5 100644 --- a/fluxer_api/src/api/worker/WorkerTaskRegistry.ts +++ b/fluxer_api/src/api/worker/WorkerTaskRegistry.ts @@ -20,7 +20,6 @@ import expireAttachments from '@app/api/worker/tasks/ExpireAttachments'; import expireReportSnapshots from '@app/api/worker/tasks/ExpireReportSnapshots'; import expireStaleJobs from '@app/api/worker/tasks/ExpireStaleJobs'; import extractEmbeds from '@app/api/worker/tasks/ExtractEmbeds'; -import finalizeNcmecAttachmentReport from '@app/api/worker/tasks/FinalizeNcmecAttachmentReport'; import flushUserActivityBuffer from '@app/api/worker/tasks/FlushUserActivityBuffer'; import handleMentionChunk from '@app/api/worker/tasks/HandleMentionChunk'; import handleMentions from '@app/api/worker/tasks/HandleMentions'; @@ -93,7 +92,6 @@ export const workerTasks: Record = { expireReportSnapshots, expireStaleJobs, extractEmbeds, - finalizeNcmecAttachmentReport, handleMentions, handleMentionChunk, harvestGuildData, diff --git a/fluxer_api/src/api/worker/tasks/FinalizeNcmecAttachmentReport.ts b/fluxer_api/src/api/worker/tasks/FinalizeNcmecAttachmentReport.ts deleted file mode 100644 index 902d0d16b..000000000 --- a/fluxer_api/src/api/worker/tasks/FinalizeNcmecAttachmentReport.ts +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {createAttachmentID} from '@app/api/BrandedTypes'; -import {Logger} from '@app/api/Logger'; -import {getWorkerDependencies} from '@app/api/worker/WorkerContext'; -import type {WorkerTaskHandler} from '@pkgs/worker/src/contracts/WorkerTask'; -import {z} from 'zod'; - -const PayloadSchema = z.object({ - attachmentId: z.string(), - requeueCount: z.number().int().min(0).optional(), -}); -const finalizeNcmecAttachmentReport: WorkerTaskHandler = async (payload, helpers) => { - const validated = PayloadSchema.parse(payload); - helpers.logger.debug({payload: validated}, 'Processing finalizeNcmecAttachmentReport task'); - const attachmentId = createAttachmentID(BigInt(validated.attachmentId)); - try { - const deps = getWorkerDependencies(); - await deps.ncmecSubmissionService.finalizeAttachmentReport(attachmentId, validated.requeueCount ?? 0); - } catch (error) { - Logger.error({error, attachmentId: attachmentId.toString()}, 'Failed to finalize NCMEC attachment report'); - throw error; - } -}; - -export default finalizeNcmecAttachmentReport; diff --git a/fluxer_api/src/api/worker/tests/WorkerLaneConfig.test.ts b/fluxer_api/src/api/worker/tests/WorkerLaneConfig.test.ts index 81ed15ce8..e8fd15194 100644 --- a/fluxer_api/src/api/worker/tests/WorkerLaneConfig.test.ts +++ b/fluxer_api/src/api/worker/tests/WorkerLaneConfig.test.ts @@ -70,7 +70,7 @@ describe('WorkerLaneConfig', () => { expect(retired).toEqual({ realtime: [], unfurl: [], - lifecycle: ['sendScheduledMessage'], + lifecycle: ['sendScheduledMessage', 'finalizeNcmecAttachmentReport'], batch: [], crosspost: [], }); diff --git a/fluxer_docs/src/content/docs/admin-api/api-keys.mdx b/fluxer_docs/src/content/docs/admin-api/api-keys.mdx index ab934e16c..7d55739f8 100644 --- a/fluxer_docs/src/content/docs/admin-api/api-keys.mdx +++ b/fluxer_docs/src/content/docs/admin-api/api-keys.mdx @@ -140,7 +140,7 @@ The acting credential must already have every value in `acls`, unless it has `*` | --- | --- | --- | | name1 | string | The name given to the key (1-100 characters) | | expires_in_days?2 | integer | The number of days until the key expires (1-365) | -| acls3 | array[string] | The [ACLs](/admin-api/#acl-registry) stored on the key, each a registry value (at most 106) | +| acls3 | array[string] | The [ACLs](/admin-api/#acl-registry) stored on the key, each a registry value (at most 105) | 1 A value that is empty after trimming is rejected, so whitespace alone is not a name @@ -218,7 +218,7 @@ An update never rotates the credential, and no field on this route changes the e | Field | Type | Description | | --- | --- | --- | | name?1 | string | The replacement name for the key (1-100 characters) | -| acls?2 | array[string] | The complete replacement set of [ACLs](/admin-api/#acl-registry), each a registry value (at most 106) | +| acls?2 | array[string] | The complete replacement set of [ACLs](/admin-api/#acl-registry), each a registry value (at most 105) | 1 A value that is empty after trimming is rejected diff --git a/fluxer_docs/src/content/docs/admin-api/index.mdx b/fluxer_docs/src/content/docs/admin-api/index.mdx index 98d3dec3a..49fbf2740 100644 --- a/fluxer_docs/src/content/docs/admin-api/index.mdx +++ b/fluxer_docs/src/content/docs/admin-api/index.mdx @@ -36,7 +36,7 @@ Admin authorisation runs before request validation on every operation except [Qu An operation names one or more ACLs and admits the request when the caller holds at least one of them. The wildcard `*` satisfies every requirement on its own. -Where an operation stacks several requirements, Fluxer evaluates each separately and the caller must satisfy all of them. [Create NCMEC report](/admin-api/messages/#create-ncmec-report) is the widest example and stacks `csam:submit_ncmec`, `message:delete`, `user:delete`, and `archive:trigger:user` as independent requirements. +Where an operation stacks several requirements, Fluxer evaluates each separately and the caller must satisfy all of them. [List users](/admin-api/users/#list-users) is one example and stacks `user:view:email` or `user:view:ip` on top of `user:lookup` when a request uses a selector that needs one. The effective ACL set of a session or an Admin OAuth2 bearer credential is the set stored on the account. The effective set of an Admin API key is the set stored on the key, subject to the owner check above. [Set user ACLs](/admin-api/users/#set-user-acls) writes an account's set. Holding `admin:authenticate` or `*` is the whole definition of an account that can reach the Admin API. @@ -61,7 +61,7 @@ A body with none of those fields resolves to no ACL at all and applies no change Fluxer checks each ACL an Admin grants against the ACLs that Admin holds. [Set user ACLs](/admin-api/users/#set-user-acls) and [Create Admin API key](/admin-api/api-keys/#create-admin-api-key) both refuse to write an ACL the acting Admin does not itself hold, with 403 `MISSING_ACL`. A wildcard holder is exempt. Set user ACLs also refuses the acting Admin's own account with 403 `ACCESS_DENIED`, and it looks up the target account before it checks the granted ACLs, so an unknown ID fails first with 404 `UNKNOWN_USER`. -[Set user ACLs](/admin-api/users/#set-user-acls), [Create Admin API key](/admin-api/api-keys/#create-admin-api-key), and [Update Admin API key](/admin-api/api-keys/#update-admin-api-key) each accept at most 106 ACLs and validate every entry against the registry, so a value outside it returns 400 `INVALID_FORM_BODY`. +[Set user ACLs](/admin-api/users/#set-user-acls), [Create Admin API key](/admin-api/api-keys/#create-admin-api-key), and [Update Admin API key](/admin-api/api-keys/#update-admin-api-key) each accept at most 105 ACLs and validate every entry against the registry, so a value outside it returns 400 `INVALID_FORM_BODY`. :::caution[`*` satisfies every present and future ACL] A key whose owning account holds `*` skips the owner check. An Admin holding `*` can grant any ACL. @@ -311,7 +311,6 @@ An entry with any other action has `access` set to `write`. | Value | Description | | --- | --- | -| NCMEC Report | An attachment was submitted through the NCMEC reporting integration | | annotate_ban | A note was appended to the current ban of an account | | approve_discovery_application | A discovery application was approved | | approve_registration | A pending registration was approved | @@ -482,7 +481,7 @@ Returns every Admin permission string the instance recognises, in [ACL registry] | Field | Type | Description | | --- | --- | --- | -| acls1 | array[string] | The permission strings the Admin API recognises (at most 106 entries) | +| acls1 | array[string] | The permission strings the Admin API recognises (at most 105 entries) | 1 The response is the registry itself and does not vary with the caller's own ACL set @@ -550,7 +549,6 @@ The registry is returned in this order by [List ACLs](#list-acls). A value outsi | bulk:delete:user_messages | Queues the bulk message deletion task | | bulk:update:guild_features | Queues the bulk guild feature task | | bulk:update:user_flags | Queues the bulk account flag task | -| csam:submit_ncmec | Submits an attachment through the NCMEC reporting integration | | discovery:remove | Removes a guild from [discovery](/admin-api/discovery/) | | discovery:review | Reads, reviews, and edits discovery applications, categories, and listings | | gateway:memory_stats | Reads main [Gateway](/admin-api/gateway/) statistics, memory statistics, and voice state counts | diff --git a/fluxer_docs/src/content/docs/admin-api/messages.mdx b/fluxer_docs/src/content/docs/admin-api/messages.mdx index 3e720f0e7..71bc0d2b6 100644 --- a/fluxer_docs/src/content/docs/admin-api/messages.mdx +++ b/fluxer_docs/src/content/docs/admin-api/messages.mdx @@ -1,18 +1,18 @@ --- # SPDX-License-Identifier: AGPL-3.0-or-later title: Admin messages -description: Message browsing and search, permanent deletion, shred jobs, and NCMEC submission. +description: Message browsing and search, permanent deletion, and shred jobs. --- import RouteHeader from '@/components/RouteHeader.astro'; -Admin message routes read, search, and permanently delete the messages of one channel. Others report the progress of a shred job and submit one attachment to NCMEC, the National Center for Missing and Exploited Children. +Admin message routes read, search, and permanently delete the messages of one channel. Another reports the progress of a shred job. Shredding and bulk deletion for a whole account live under [Admin users](/admin-api/users/). ## Admin message object -A flattened moderation view of one message. It has the author identity, the content and timestamp, the attachments with their NCMEC submission state, and the NSFW context of the channel and guild. +A flattened moderation view of one message. It has the author identity, the content and timestamp, the attachments, and the NSFW context of the channel and guild. Every operation that returns Admin message objects also returns the matching public message objects in a parallel `message_responses` array, so a client that needs the full wire shape joins on `id`. @@ -40,14 +40,11 @@ Every operation that returns Admin message objects also returns the matching pub | content | string | The message content (0-4,000 characters) | | timestamp | ISO8601 timestamp | When the message was sent | | attachments | array[[Admin message attachment](#admin-message-attachment-object) object] | The attachments of the message (at most 10) | -| user_prior_ncmec_report_ids?3 | array[string] | The NCMEC report IDs previously filed against the author, ascending (at most 100) | 1 Every object in one response has the same values, taken from the channel named in the request path or query. All are null when the channel no longer exists 2 Always absent from the objects these operations return. The same member is set on the [report message context object](/admin-api/reports/#report-message-context-object) -3 Written on every object these operations return, and empty when no NCMEC report has been filed against the author - ### Example ```json @@ -66,14 +63,13 @@ Every operation that returns Admin message objects also returns the matching pub "author_avatar": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "content": "read the pinned message", "timestamp": "2026-03-04T18:19:07.220Z", - "attachments": [], - "user_prior_ncmec_report_ids": [] + "attachments": [] } ``` ### Admin message attachment object -An attachment entry has the ordinary attachment metadata together with the NCMEC submission state Fluxer holds for it. +An attachment entry has the ordinary attachment metadata. #### Structure @@ -86,26 +82,9 @@ An attachment entry has the ordinary attachment metadata together with the NCMEC | content_type | ?string | The recorded MIME type, or null when none was recorded | | width | ?integer | The pixel width, or null when no dimensions were recorded | | height | ?integer | The pixel height, or null when no dimensions were recorded | -| size?4 | ?integer | The attachment size in bytes, or null when no size was recorded | -| ncmec_status | string | The [NCMEC submission status](#ncmec-submission-status) of this attachment | -| ncmec_report_id5 | ?string | The report ID NCMEC assigned (1-256 characters), or null when none has been assigned | -| ncmec_failure_reason6 | ?string | The recorded reason the last submission failed (1-4,000 characters), or null | +| size?3 | ?integer | The attachment size in bytes, or null when no size was recorded | -4 Always emitted, so it is null and never absent when no size was recorded - -5 Assigned only once NCMEC accepts the report, so it is null while `ncmec_status` is `not_submitted` and after a failed submission - -6 Set only when `ncmec_status` is `failed`, and it stores the raw upstream error. The sanitised wording appears in the failing response body instead - -#### NCMEC submission status - -| Value | Description | -| --- | --- | -| not_submitted | The attachment has never been submitted to NCMEC | -| submitted | The attachment was submitted to NCMEC and NCMEC accepted the report | -| failed | The submission failed, the opened report was retracted, and the attachment can be submitted again | - -A submission that is still running is stored as `submitting` and returned unchanged, so tolerate a status outside this table. An attachment in that state is refused a second submission by [Create NCMEC report](#create-ncmec-report). +3 Always emitted, so it is null and never absent when no size was recorded ## Message shred status object @@ -396,73 +375,3 @@ One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded with t ### Rate limit 100 requests per minute for each authenticated user, on the shared `admin:message:operation` bucket. - -## Create NCMEC report - - - -Submits one image or video attachment to NCMEC and starts the account enforcement workflow for its author. Requires `csam:submit_ncmec`, `message:delete`, `user:delete`, and `archive:trigger:user`. - -A caller missing more than one of the four is refused with 403 `MISSING_ACL` naming the first it does not hold, in the order listed. The wildcard satisfies them all. - -This operation ignores `X-Audit-Log-Reason`. The reason on every audit entry the workflow produces is built from the assigned NCMEC report ID and the channel and attachment IDs, and it is returned as `audit_log_reason`. - -### JSON body - -| Field | Type | Description | -| --- | --- | --- | -| channel_id | snowflake | The channel the attachment was sent in | -| message_id | snowflake | The message the attachment belongs to | -| attachment_id | snowflake | The attachment to submit | -| filename | string | The exact filename of the attachment (1-255 characters) | -| source_report_id?1 | snowflake | The report whose frozen evidence has the attachment | -| reporter_full_name | string | The full legal name of the submitting Admin (1-200 characters) | -| confirmed_viewed2 | boolean | Confirmation that the submitting Admin has viewed the attachment | - -1 Required when the live message no longer exists or has no author, in which case the attachment is read from that report's frozen evidence. Supplying it otherwise only records the report ID - -2 The literal value `true` is the only accepted value. Any other value fails body validation - -### Response body - -| Field | Type | Description | -| --- | --- | --- | -| success | boolean | Always true | -| ncmec_report_id | string | The report ID NCMEC assigned to this submission (1-256 characters) | -| audit_log_reason3 | string | The reason recorded on every audit entry the workflow produced (1-4,000 characters) | - -3 The NCMEC report ID followed by the channel ID and attachment ID, so one string identifies the submission in the audit log and at NCMEC - -### Response - -| Status | Body | Condition | -| --- | --- | --- | -| 200 | response body | Attachment was submitted and the workflow advanced | -| 400 | [error response](/admin-api/#error-response) | The resolved attachment is neither an image nor a video and the request returns `INVALID_FORM_BODY` against `attachment_id` | -| 4041 | [error response](/admin-api/#error-response) | `UNKNOWN_MESSAGE` when nothing resolves the attachment, or `UNKNOWN_REPORT` when `source_report_id` names no report | -| 409 | [error response](/admin-api/#error-response) | `NCMEC_ALREADY_SUBMITTED` when a submission for the attachment is already running or has already succeeded | -| 500 | [error response](/admin-api/#error-response) | Submission fails and the request returns `NCMEC_SUBMISSION_FAILED` | - -1 The attachment resolves from a live message with an author, or from frozen report evidence - -:::danger[A successful request files a real NCMEC report] -The request disables the author's account and schedules that account for deletion. Nothing withdraws a completed submission. Fluxer retracts the report only when a step after it was opened fails. -::: - -### Side effects - -Fluxer resolves the attachment from the live message when that message still exists and has an author, and from the frozen evidence named by `source_report_id` otherwise. Fluxer accepts the attachment only when its resolved content type begins with `image/` or `video/`. - -The attachment is `submitting` while the request runs, then `submitted` with its report ID on success. A failed submission attempts to retract the opened report, marks the attachment `failed` with the raw reason, and returns `NCMEC_SUBMISSION_FAILED`. A failed retraction is logged without changing that recorded state. - -When the resolved attachment has an author whose account an earlier NCMEC submission has not already disabled, Fluxer sets the account's deleted and disabled flags, clears any temporary ban, and records the deletion reason for child sexual content. It then schedules deletion 60 days ahead, deletes every authentication session, sends [User Update](/gateway/events/#user-update) to that account, and triggers a user archive for that account. When the account's public user fields change, Fluxer also sends [Guild Member Update](/gateway/events/#guild-member-update) to each guild the account is in. - -Message content is deleted only after that archive completes. The archive is re-checked every 15 seconds by default, for at most 240 attempts. Once the archive is complete the reported message is deleted, its attachments are purged, and [Message Delete](/gateway/events/#message-delete) is sent. - -A successful submission records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with the action `NCMEC Report`. It targets the reported user, or targets the source report when the attachment resolved to no author. Triggering the archive records a second entry with the action `trigger_user_archive` targeting the same user, skipped when the account already has an archive taken after the enforcement. Both entries record the reason returned as `audit_log_reason`. - -An attachment that resolved to no author is submitted to NCMEC and audited. No account is disabled, no archive is triggered, and no message content is deleted. - -### Rate limit - -100 requests per minute for each authenticated user, on the shared `admin:message:operation` bucket. diff --git a/fluxer_docs/src/content/docs/admin-api/reports.mdx b/fluxer_docs/src/content/docs/admin-api/reports.mdx index 3fa0396bf..fc5b39d71 100644 --- a/fluxer_docs/src/content/docs/admin-api/reports.mdx +++ b/fluxer_docs/src/content/docs/admin-api/reports.mdx @@ -22,7 +22,7 @@ Fluxer reads the tag, username, display name, and discriminator of both accounts ## Admin report object -A report is written once, when the reporter files it. [Update report](#update-report) moves it from PENDING to RESOLVED with an optional comment. [Schedule user deletion](/admin-api/users/#schedule-user-deletion) for any reason other than a user request also resolves every pending report against the deleted account, with no public comment, and sends each reporter the generic notice [Update report](#update-report) describes. A report is deleted at the end of its [retention period](#report-retention). +A report is written once, when the reporter files it. [Update report](#update-report) moves it from PENDING to RESOLVED with an optional comment. [Schedule user deletion](/admin-api/users/#schedule-user-deletion) for one of the abuse reasons listed there also resolves the pending reports against the deleted account, with no public comment, and sends each reporter the generic notice [Update report](#update-report) describes. A report in the `child_safety`, `underage_user`, or `self_harm` category is never resolved this way and stays PENDING. A report is deleted at the end of its [retention period](#report-retention). [List reports](#list-reports) returns this object without message context. [Get report](#get-report) returns the [Admin report detail object](#admin-report-detail-object), which extends it. @@ -32,7 +32,7 @@ A report is written once, when the reporter files it. [Update report](#update-re | --- | --- | --- | | report_id | snowflake | The ID of the report | | reporter_id1 | ?snowflake | The account that filed the report | -| reporter_tag1 2 | ?string | The reporter username and four-digit discriminator joined by `#` | +| reporter_tag1 2 18 | ?string | The reporter username and four-digit discriminator joined by `#` | | reporter_username1 2 | ?string | The username of the reporter | | reporter_global_name1 2 | ?string | The display name of the reporter, or null when unset | | reporter_discriminator1 2 | ?string | The zero-padded four-digit reporter discriminator | @@ -50,7 +50,7 @@ A report is written once, when the reporter files it. [Update report](#update-re | reporter_good_faith_confirmed?7 13 | ?boolean | Whether the reporter confirmed the notice in good faith | | additional_info | ?string | The free-text detail the reporter supplied | | reported_user_id14 | ?snowflake | The reported account, or null for a guild report | -| reported_user_tag2 | ?string | The reported username and four-digit discriminator joined by `#` | +| reported_user_tag2 18 | ?string | The reported username and four-digit discriminator joined by `#` | | reported_user_username2 | ?string | The username of the reported account | | reported_user_global_name2 | ?string | The display name of the reported account, or null when unset | | reported_user_discriminator2 | ?string | The zero-padded four-digit reported account discriminator | @@ -67,7 +67,7 @@ A report is written once, when the reporter files it. [Update report](#update-re | reported_webhook_guild_id?4 7 14 15 | ?snowflake | The guild the webhook belonged to | | reported_webhook_created_at?7 14 | ?ISO8601 timestamp | When the webhook was created, derived from its ID | | reported_webhook_creator_id?4 7 14 15 | ?snowflake | The account that created the webhook | -| reported_webhook_creator_tag?4 7 14 15 16 | ?string | The creator username and four-digit discriminator joined by `#` | +| reported_webhook_creator_tag?4 7 14 15 16 18 | ?string | The creator username and four-digit discriminator joined by `#` | | reported_webhook_creator_username?4 7 14 15 16 | ?string | The username of the creator | | reported_webhook_creator_global_name?4 7 14 15 16 | ?string | The display name of the creator, or null when unset | | reported_webhook_creator_discriminator?4 7 14 15 16 | ?string | The zero-padded four-digit creator discriminator | @@ -128,6 +128,8 @@ A report is written once, when the reporter files it. [Update report](#update-re 17 Null when the report names no reported account, which includes a report about a message a webhook sent +18 On an instance with the `none` tag style the tag of an account that is not a bot is the bare username, with no `#0000` + ### Example ```json @@ -235,15 +237,14 @@ A message report captures the reported message together with at most 25 messages | content | string | The captured message content, empty when the message had none | | timestamp6 | ISO8601 timestamp | When the message was sent | | attachments7 | array[[report message attachment](#report-message-attachment-object) object] | The captured attachment copies | -| author_id9 | snowflake | The author of the message | -| author_username9 | string | The captured author username | +| author_id8 | snowflake | The author of the message | +| author_username8 | string | The captured author username | | author_global_name4 | ?string | The display name of the author | -| author_discriminator9 | string | The captured zero-padded four-digit author discriminator | -| author_avatar9 | ?string | The captured author avatar hash, or null when unset | -| webhook_id?3 10 | ?snowflake | The [webhook](/http-api/webhooks/) that sent the message | -| author_bot?3 11 | ?boolean | Whether the author account is a bot | -| missing_attachments?3 12 | array[[report missing attachment](#report-missing-attachment-object) object] | Attachments that could not be copied into the report | -| user_prior_ncmec_report_ids?3 8 | array[string] | The NCMEC report IDs previously filed against the author | +| author_discriminator8 | string | The captured zero-padded four-digit author discriminator | +| author_avatar8 | ?string | The captured author avatar hash, or null when unset | +| webhook_id?3 9 | ?snowflake | The [webhook](/http-api/webhooks/) that sent the message | +| author_bot?3 10 | ?boolean | Whether the author account is a bot | +| missing_attachments?3 11 | array[[report missing attachment](#report-missing-attachment-object) object] | Attachments that could not be copied into the report | 1 The channel of the captured entry, otherwise the reported channel. An entry for which neither records a channel is left out of `message_context` @@ -259,15 +260,13 @@ A message report captures the reported message together with at most 25 messages 7 Fluxer drops an evidence copy that records no file name, resolves to no channel, or receives no download URL, so this array can be shorter than the original -8 Sorted ascending, with at most 100 entries, and empty when no NCMEC report has been filed against the author or a webhook sent the message +8 For a message a webhook sent, the webhook ID, the name and avatar hash the message was sent with, and the discriminator `0000`, as the [message object](/http-api/messages/#message-object) shows the author of a webhook message -9 For a message a webhook sent, the webhook ID, the name and avatar hash the message was sent with, and the discriminator `0000`, as the [message object](/http-api/messages/#message-object) shows the author of a webhook message +9 Null for a message an account sent -10 Null for a message an account sent +10 Read from the author account when the response is produced. Null for a message a webhook sent and for an author account that no longer resolves -11 Read from the author account when the response is produced. Null for a message a webhook sent and for an author account that no longer resolves - -12 Empty when every attachment was copied, and on a report filed before missing attachments were recorded. An attachment is listed here when its copy failed or, for a file embedded in the message, when the file was no longer stored +11 Empty when every attachment was copied, and on a report filed before missing attachments were recorded. An attachment is listed here when its copy failed or, for a file embedded in the message, when the file was no longer stored ### Report message attachment object @@ -277,27 +276,14 @@ A message report captures the reported message together with at most 25 messages | --- | --- | --- | | id | snowflake | The ID of the attachment | | filename | string | The captured file name | -| url13 | string | The download URL for the evidence copy | +| url12 | string | The download URL for the evidence copy | | nsfw | ?boolean | Whether the attachment was marked NSFW | | content_type | ?string | The captured MIME type, or null when unknown | | width | ?integer | The pixel width, or null for a non-image attachment | | height | ?integer | The pixel height, or null for a non-image attachment | | size?3 | ?integer | The size in bytes, or null when unknown | -| ncmec_status14 | string | The [NCMEC submission status](#ncmec-submission-statuses) | -| ncmec_report_id | ?string | The NCMEC report ID, or null when the attachment was never submitted | -| ncmec_failure_reason | ?string | The failure detail, or null unless `ncmec_status` is failed | -13 The URL expires after five minutes. Fetch the report again for fresh attachment links - -14 An in-flight submission is stored as `submitting`, a value the registry below does not list - -#### NCMEC submission statuses - -| Value | Description | -| --- | --- | -| not_submitted | The attachment has never been submitted to NCMEC | -| submitted | The attachment has been submitted to NCMEC | -| failed | Submission to NCMEC was attempted and failed | +12 The URL expires after five minutes. Fetch the report again for fresh attachment links ### Report missing attachment object @@ -313,9 +299,9 @@ An attachment of a captured message that Fluxer could not copy into the report. | content_type | ?string | The captured MIME type, or null when unknown | | width | ?integer | The pixel width, or null for a non-image attachment | | height | ?integer | The pixel height, or null for a non-image attachment | -| size15 | ?integer | The size in bytes, or null when unknown | +| size13 | ?integer | The size in bytes, or null when unknown | -15 `0` for a file embedded in the message that was no longer stored when the report was filed +13 `0` for a file embedded in the message that was no longer stored when the report was filed ## Report profile snapshot object @@ -710,7 +696,7 @@ Resolving a report notifies the reporter whatever `notify_reporter` says, as the The report moves to RESOLVED and records `resolved_at`, `resolved_by_admin_id`, and the public comment. -Fluxer records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with target type `report`, target ID equal to the report ID, and action `resolve_report`. The metadata is `report_id`, `report_type`, `notify_reporter`, `reporter_dm_sent`, `reporter_email_sent`, and `reporter_dsa_email_sent`. When Fluxer does not attempt a notice, `reporter_notice_skipped` names the reason: `reporter_account_deleted`, `reporter_report_banned`, `reporter_lookup_failed`, or `no_reporter_email`. +Fluxer records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with target type `report`, target ID equal to the report ID, and action `resolve_report`. The metadata is `report_id`, `report_type`, `notify_reporter`, `reporter_dm_sent`, `reporter_email_sent`, and `reporter_dsa_email_sent`, plus `resolution` when the request sets one. When Fluxer does not attempt a notice, `reporter_notice_skipped` names the reason: `reporter_account_deleted`, `reporter_report_banned`, `reporter_lookup_failed`, or `no_reporter_email`. Fluxer notifies the reporter of every resolution. A reporter with an account receives a system direct message, and an email when the account has an address. A Digital Services Act notice has no reporter account, so its verified address receives a decision email with the report ID, the comment when one is shared, and how to appeal. Nothing is sent when the reporter account is deleted or barred from reporting. diff --git a/fluxer_docs/src/content/docs/admin-api/users.mdx b/fluxer_docs/src/content/docs/admin-api/users.mdx index 8abdf39fe..c70c15648 100644 --- a/fluxer_docs/src/content/docs/admin-api/users.mdx +++ b/fluxer_docs/src/content/docs/admin-api/users.mdx @@ -59,7 +59,7 @@ When the caller lacks `user:view:email`, `user:view:dob`, or `user:view:ip`, Flu | deletion_audit_log_reason | ?string | The private reason stored with the pending deletion, and null without `audit_log:view` | | deletion_scheduled_by | ?snowflake | The ID of the account that scheduled the pending deletion, or null when it was not recorded | | deletion_scheduled_at | ?ISO8601 timestamp | The time the pending deletion was scheduled, or null when it was not recorded | -| acls7 | array[string] | Effective [Admin ACLs](/admin-api/#acl-registry), with at most 106 entries | +| acls7 | array[string] | Effective [Admin ACLs](/admin-api/#acl-registry), with at most 105 entries | | traits8 | array[string] | The free-form operator labels set on the account, with at most 100 entries | | has_totp9 | boolean | Whether a TOTP authenticator is registered | | authenticator_types9 12 | array[integer] | Registered [authenticator types](/http-api/users/#authenticator-types), with at most 10 entries | @@ -135,6 +135,7 @@ The `flags` field of the [Admin user object](#admin-user-object) is a 64-bit bit | 1 << 4 | FRIENDLY_BOT | Bot accepts friend requests from users | | 1 << 5 | FRIENDLY_BOT_MANUAL_APPROVAL | Bot requires manual approval for friend requests | | 1 << 6 | SPAMMER | Account is flagged as a spammer | +| 1 << 7 | PROFILE_HIDDEN | Profile details are hidden from other users | | 1 << 33 | HIGH_GLOBAL_RATE_LIMIT | Account has elevated global rate limits | | 1 << 34 | DELETED | Account has been deleted | | 1 << 36 | SELF_DELETED | Account was self-deleted | @@ -1147,7 +1148,7 @@ A path naming the acting account fails with 403 `ACCESS_DENIED` before the accou | Field | Type | Description | | --- | --- | --- | -| acls1 | array[string] | Replacement [Admin ACLs](/admin-api/#acl-registry), with at most 106 values | +| acls1 | array[string] | Replacement [Admin ACLs](/admin-api/#acl-registry), with at most 105 values | 1 Required. A value outside the [ACL registry](/admin-api/#acl-registry) fails body validation, and a repeated value is collapsed @@ -1595,9 +1596,9 @@ Fluxer cancels a Stripe subscription on the account without proration and refund The account holder is emailed the deadline and the supplied `public_reason` when `notify_user` is true and the account has an email address. The wording depends on `reason_code`: `USER_REQUESTED` and `INACTIVITY` get a neutral notice, and every other code gets an enforcement notice. A failed email is logged and the schedule is still stored. -For every reason other than `USER_REQUESTED`, Fluxer also blocks the account's email address and resolves the pending reports against the account. These enforcement steps are best-effort and can fail without cancelling the deletion schedule. +For every reason other than `USER_REQUESTED`, Fluxer also blocks the account's email address. For the reasons `SPAM`, `CHEATING_OR_EXPLOITATION`, `COORDINATED_RAIDING`, `AUTOMATION_OR_SELFBOT`, `SCAM_OR_SOCIAL_ENGINEERING`, `HARASSMENT_OR_BULLYING`, `BAN_EVASION`, `TOKEN_OR_CREDENTIAL_SCAM`, `HATE_SPEECH_OR_EXTREMIST_CONTENT`, `MALICIOUS_LINKS_OR_MALWARE`, and `IMPERSONATION_OR_FAKE_IDENTITY`, Fluxer also resolves the pending reports against the account. A pending report in the `child_safety`, `underage_user`, or `self_harm` category is left for a moderator and stays pending. Every other reason leaves all reports pending. These enforcement steps are best-effort and can fail without cancelling the deletion schedule. -[User Update](/gateway/events/#user-update) is emitted after the sessions have already been deleted. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `schedule_deletion`, target type `user`, and the metadata keys `days`, `reason_code`, `pending_deletion_at`, `notify_user`, and `notification_sent`. An attempted email adds `notification_template`. A replacement adds `replaced_pending_deletion_at`, `replaced_scheduled_by`, `replaced_scheduled_at`, and `replaced_reason_code` for the schedule it replaced. The identifier bans record their own blocklist entries. For every reason other than `USER_REQUESTED`, Fluxer also records a second entry with action `auto_resolve_reports_on_deletion`, target type `user`, and the metadata keys `outcome`, `found_count`, `resolved_count`, `failed_count`, and `notified_count`. `outcome` is `completed`, `search_unavailable` when the instance has no report search backend and every report stays pending, or `enumeration_failed` when listing the pending reports failed part way and only those already found were resolved. Each resolved report sends its reporter the generic notice [Update report](/admin-api/reports/#update-report) describes, with no public comment. +[User Update](/gateway/events/#user-update) is emitted after the sessions have already been deleted. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `schedule_deletion`, target type `user`, and the metadata keys `days`, `reason_code`, `pending_deletion_at`, `notify_user`, and `notification_sent`. An attempted email adds `notification_template`. A replacement adds `replaced_pending_deletion_at`, `replaced_scheduled_by`, `replaced_scheduled_at`, and `replaced_reason_code` for the schedule it replaced. The identifier bans record their own blocklist entries. For each reason that resolves reports, Fluxer also records a second entry with action `auto_resolve_reports_on_deletion`, target type `user`, and the metadata keys `outcome`, `found_count`, `resolved_count`, `failed_count`, and `notified_count`. `found_count` leaves out the reports in the three categories that stay pending. No such entry is written for any other reason. `outcome` is `completed`, `search_unavailable` when the instance has no report search backend and every report stays pending, or `enumeration_failed` when listing the pending reports failed part way and only those already found were resolved. Each resolved report sends its reporter the generic notice [Update report](/admin-api/reports/#update-report) describes, with no public comment. ### Rate limit diff --git a/fluxer_docs/src/content/docs/gateway/events.md b/fluxer_docs/src/content/docs/gateway/events.md index b39a78663..f7cfd466b 100644 --- a/fluxer_docs/src/content/docs/gateway/events.md +++ b/fluxer_docs/src/content/docs/gateway/events.md @@ -770,7 +770,7 @@ One visible message was deleted. | guild_id? | snowflake | Guild the channel belongs to | | member?2 | [guild member](/http-api/guild-members/#guild-member-object) object | The author's guild member object, present in a guild channel | -1 Both fields are omitted when an instance administrator deleted the message through the Admin API, when Fluxer deleted it after a CSAM report, when Fluxer deleted it because content moderation blocked a link preview in it, or when Fluxer removed a published message and its copies together, and `author_id` is also omitted for a message with no author +1 Both fields are omitted when an instance administrator deleted the message through the Admin API, when Fluxer deleted it because content moderation blocked a link preview in it, or when Fluxer removed a published message and its copies together, and `author_id` is also omitted for a message with no author 2 The `user` field is removed from it, and the whole field is absent when `author_id` is absent or the author is no longer a member diff --git a/fluxer_docs/src/content/docs/http-api/errors.md b/fluxer_docs/src/content/docs/http-api/errors.md index ea642cf78..d4c13bd68 100644 --- a/fluxer_docs/src/content/docs/http-api/errors.md +++ b/fluxer_docs/src/content/docs/http-api/errors.md @@ -774,14 +774,6 @@ The requested scope isn't supported or you don't have permission to request it You don't have the permissions required to perform this action -### `NCMEC_ALREADY_SUBMITTED` - -This content has already been submitted to NCMEC - -### `NCMEC_SUBMISSION_FAILED` - -We couldn't submit the report to NCMEC - ### `NOT_A_BOT_APPLICATION` This application isn't a bot @@ -1843,10 +1835,6 @@ You must start a session before sending messages Name can't be empty after normalization -### `NCMEC_ATTACHMENT_MUST_BE_IMAGE_OR_VIDEO` - -Only image or video attachments can be reported to NCMEC - ### `NEW_EMAIL_MUST_BE_DIFFERENT` New email must be different from your current email diff --git a/fluxer_docs/src/content/docs/operator/configuration.mdx b/fluxer_docs/src/content/docs/operator/configuration.mdx index 34850a5a9..35296a3e6 100644 --- a/fluxer_docs/src/content/docs/operator/configuration.mdx +++ b/fluxer_docs/src/content/docs/operator/configuration.mdx @@ -516,7 +516,7 @@ Default `http://localhost:3900`. The S3 API address. `media-proxy` defaults to e #### `FLUXER_S3_PUBLIC_ENDPOINT` -No default. The host substituted into presigned URLs, which are the only place it appears. Compose forwards it from `.env`, and uses `FLUXER_S3_ENDPOINT` when it is unset, except on `app-proxy`, which gets only a value `.env` sets and adds it to its Content Security Policy. +No default. The host substituted into presigned URLs, which are the only place it appears. Compose forwards it from `.env`, and uses `FLUXER_S3_ENDPOINT` when it is unset, except on `app-proxy`, which gets only a value `.env` sets and adds it to its Content Security Policy. `admin` reads it to find the origin that report downloads are presigned against. See `FLUXER_S3_BUCKET_REPORTS`. #### `FLUXER_S3_FORCE_PATH_STYLE` @@ -548,7 +548,7 @@ Default `fluxer-uploads`. Raw uploads. `media-proxy` defaults to `uploads`, `app #### `FLUXER_S3_BUCKET_REPORTS` -Default `fluxer-reports`. Abuse report evidence. Read by `api` and `worker`. Compose forwards it from `.env` with a default of `fluxer-reports`, so every service and `seaweedfs-init` name the same bucket. +Default `fluxer-reports`. Abuse report evidence. Read by `api`, `worker`, and `admin`. Compose forwards it from `.env` with a default of `fluxer-reports`, so every service and `seaweedfs-init` name the same bucket. `admin` allows one image origin for report attachments and profile snapshots in its Content Security Policy, the origin `api` presigns report downloads against. It works that origin out from this name, `FLUXER_S3_PUBLIC_ENDPOINT` or else `FLUXER_S3_ENDPOINT`, `FLUXER_S3_FORCE_PATH_STYLE` and `FLUXER_S3_BUCKET_UPLOADS`, so give `admin` the same values as `api`. The images load only when that origin is an address browsers can reach. #### `FLUXER_S3_BUCKET_HARVESTS` @@ -1002,26 +1002,6 @@ With prices saved in the dashboard, add the retired ID to its legacy prices befo All are optional. -#### `FLUXER_NCMEC_ENABLED` - -Default `false`. NCMEC reporting. Compose forwards it from `.env`. - -#### `FLUXER_NCMEC_BASE_URL` - -Default empty. The reporting endpoint, `https://report.cybertip.org/ispws` in production. Required when reporting is on. Compose forwards it from `.env`. - -#### `FLUXER_NCMEC_USERNAME` - -Default empty. The reporting login. Required when reporting is on. Compose forwards it from `.env`. - -#### `FLUXER_NCMEC_PASSWORD` - -Default empty. The reporting password. Required when reporting is on. Compose forwards it from `.env`. - -#### `FLUXER_NCMEC_REPORTER_EMAIL` - -Default empty. The contact address on reports. Required when reporting is on. Compose forwards it from `.env`. - #### `FLUXER_CLAMAV_ENABLED` Default `false`. Upload virus scanning. Compose forwards it from `.env`, and no ClamAV container ships. @@ -1046,7 +1026,7 @@ With feeds off, Fluxer checks none of this data. The first worker start with fee #### `FLUXER_REPORT_RETENTION_DRY_RUN` -Default unset, which keeps the dry run. Read by `worker`. The daily [report retention](/admin-api/reports/#report-retention) job deletes reports 365 days after they were filed, with their evidence copies and search entries, unless a legal hold is in place. Until this is `false` or `0`, the job only logs what it would delete. Any other value, or no value, keeps the dry run. Check the counts in the `Processed report retention` log line before you turn deletion on, because deleted evidence cannot be restored. Compose does not forward it, so add it to `worker` through an override file. +Default unset, which keeps the dry run. Read by `worker`. The daily [report retention](/admin-api/reports/#report-retention) job deletes reports 365 days after they were filed, with their evidence copies and search entries, unless a legal hold is in place. Until this is `false` or `0`, the job only logs what it would delete. Any other value, or no value, keeps the dry run. Check the counts in the `Processed report retention` log line before you turn deletion on, because deleted evidence cannot be restored. Compose forwards it from `.env`. #### `FLUXER_BREACHED_PASSWORD_CHECK_ENABLED` @@ -1735,10 +1715,6 @@ Compose writes each of these from the `.env` name given, so setting the name its `BUILD_VERSION`, `FLUXER_BUILD_VERSION` and `RELEASE_CHANNEL` describe the shipped build. `POD_NAME` and `HOSTNAME` come from the platform. -#### Report retention - -`FLUXER_REPORT_RETENTION_DRY_RUN`, so the [report retention](/admin-api/reports/#report-retention) job stays in dry run until an override sets it on `worker`. - #### Test and development switches `FLUXER_TEST_MODE_ENABLED`, `FLUXER_TEST_HARNESS_TOKEN`, `FLUXER_DISABLE_RATE_LIMITS`, `FLUXER_VALIDATE_RESPONSES` and `FLUXER_RELAX_REGISTRATION_RATE_LIMITS`. None of them belongs on a production instance. @@ -2197,7 +2173,7 @@ Volume names are prefixed with the Compose project name, so `postgres-data` is ` | --- | --- | --- | | fluxer | `FLUXER_S3_BUCKET_CDN` | Avatars, guild and entity assets, themes, entrance sounds, memes, and processed attachments | | fluxer-uploads | `FLUXER_S3_BUCKET_UPLOADS` | Raw attachment uploads, before processing | -| fluxer-reports | `FLUXER_S3_BUCKET_REPORTS` | Abuse report evidence, and NCMEC payloads where that integration is on | +| fluxer-reports | `FLUXER_S3_BUCKET_REPORTS` | Abuse report evidence | | fluxer-harvests | `FLUXER_S3_BUCKET_HARVESTS` | User and guild data archives | `FLUXER_S3_BUCKET_STATIC` names an optional bucket. The shipped stack does not use or create it. diff --git a/fluxer_docs/src/content/docs/topics/announcement-channels.md b/fluxer_docs/src/content/docs/topics/announcement-channels.md index d7531e203..a9099b6c2 100644 --- a/fluxer_docs/src/content/docs/topics/announcement-channels.md +++ b/fluxer_docs/src/content/docs/topics/announcement-channels.md @@ -76,7 +76,7 @@ An edit of a published message reaches every copy that still exists, including c Deleting a published message keeps each copy and edits it: the content becomes `[Original message deleted]`, attachments, embeds, and stickers are removed, `edited_timestamp` is set, and the `SOURCE_MESSAGE_DELETED` flag is added. The files of the published message are deleted with it. Deleting a copy removes that copy alone, and the files it showed stay with the published message. -When an instance administrator or a CSAM report removes a published message, every copy is deleted outright. When one of them removes a copy, the published message and every other copy are deleted too, so identical content leaves every guild at once. A published message that link preview moderation deletes takes its copies with it the same way. +When an instance administrator removes a published message, every copy is deleted outright. When an instance administrator removes a copy, the published message and every other copy are deleted too, so identical content leaves every guild at once. A published message that link preview moderation deletes takes its copies with it the same way. ## Cleanup diff --git a/packages/config/src/ConfigLoader.ts b/packages/config/src/ConfigLoader.ts index 966c2bbf2..1093dab19 100644 --- a/packages/config/src/ConfigLoader.ts +++ b/packages/config/src/ConfigLoader.ts @@ -197,12 +197,6 @@ function defaultConfig(): MasterConfig { prices: {}, legacy_prices: {}, }, - ncmec: { - enabled: false, - base_url: '', - username: '', - password: '', - }, clamav: { enabled: false, host: '127.0.0.1', diff --git a/packages/config/src/MasterConfig.ts b/packages/config/src/MasterConfig.ts index 7b1d8dd2a..652eb07c8 100644 --- a/packages/config/src/MasterConfig.ts +++ b/packages/config/src/MasterConfig.ts @@ -221,13 +221,6 @@ export interface MasterConfig { prices?: Record; legacy_prices?: Record | undefined>; }; - ncmec: { - enabled: boolean; - base_url: string; - username: string; - password: string; - reporter_email?: string; - }; clamav: { enabled: boolean; host: string; diff --git a/packages/config/src/config_loader/EnvironmentOverrides.ts b/packages/config/src/config_loader/EnvironmentOverrides.ts index 29397817d..a4ebe7a51 100644 --- a/packages/config/src/config_loader/EnvironmentOverrides.ts +++ b/packages/config/src/config_loader/EnvironmentOverrides.ts @@ -224,11 +224,6 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record = { FLUXER_STRIPE_PRICE_GIFT_1_YEAR_INR: {path: ['integrations', 'stripe', 'prices', 'gift_1_year_inr']}, FLUXER_STRIPE_PRICE_GIFT_1_YEAR_PLN: {path: ['integrations', 'stripe', 'prices', 'gift_1_year_pln']}, FLUXER_STRIPE_PRICE_GIFT_1_YEAR_TRY: {path: ['integrations', 'stripe', 'prices', 'gift_1_year_try']}, - FLUXER_NCMEC_ENABLED: {path: ['integrations', 'ncmec', 'enabled'], parse: parseBoolean}, - FLUXER_NCMEC_BASE_URL: {path: ['integrations', 'ncmec', 'base_url']}, - FLUXER_NCMEC_USERNAME: {path: ['integrations', 'ncmec', 'username']}, - FLUXER_NCMEC_PASSWORD: {path: ['integrations', 'ncmec', 'password']}, - FLUXER_NCMEC_REPORTER_EMAIL: {path: ['integrations', 'ncmec', 'reporter_email']}, FLUXER_CLAMAV_ENABLED: {path: ['integrations', 'clamav', 'enabled'], parse: parseBoolean}, FLUXER_CLAMAV_HOST: {path: ['integrations', 'clamav', 'host']}, FLUXER_CLAMAV_PORT: {path: ['integrations', 'clamav', 'port'], parse: parseInteger}, diff --git a/packages/constants/src/AdminACLs.ts b/packages/constants/src/AdminACLs.ts index 14ae92145..a9f45173b 100644 --- a/packages/constants/src/AdminACLs.ts +++ b/packages/constants/src/AdminACLs.ts @@ -44,7 +44,6 @@ export const AdminACLs = { BULK_DELETE_USER_MESSAGES: 'bulk:delete:user_messages', BULK_UPDATE_GUILD_FEATURES: 'bulk:update:guild_features', BULK_UPDATE_USER_FLAGS: 'bulk:update:user_flags', - CSAM_SUBMIT_NCMEC: 'csam:submit_ncmec', DISCOVERY_REMOVE: 'discovery:remove', DISCOVERY_REVIEW: 'discovery:review', GATEWAY_MEMORY_STATS: 'gateway:memory_stats', diff --git a/packages/constants/src/ApiErrorCodes.ts b/packages/constants/src/ApiErrorCodes.ts index ec98a2fb0..78796e7aa 100644 --- a/packages/constants/src/ApiErrorCodes.ts +++ b/packages/constants/src/ApiErrorCodes.ts @@ -167,8 +167,6 @@ export const APIErrorCodes = { MAX_THREAD_MEMBERS: 'MAX_THREAD_MEMBERS', MAX_WEBHOOKS_PER_CHANNEL: 'MAX_WEBHOOKS_PER_CHANNEL', MAX_WEBHOOKS_PER_GUILD: 'MAX_WEBHOOKS_PER_GUILD', - NCMEC_ALREADY_SUBMITTED: 'NCMEC_ALREADY_SUBMITTED', - NCMEC_SUBMISSION_FAILED: 'NCMEC_SUBMISSION_FAILED', MEDIA_METADATA_ERROR: 'MEDIA_METADATA_ERROR', METHOD_NOT_ALLOWED: 'METHOD_NOT_ALLOWED', MISSING_ACCESS: 'MISSING_ACCESS', diff --git a/packages/constants/src/ReportCategories.ts b/packages/constants/src/ReportCategories.ts deleted file mode 100644 index 2c090a6f6..000000000 --- a/packages/constants/src/ReportCategories.ts +++ /dev/null @@ -1,3 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export const CATEGORY_CHILD_SAFETY = 'child_safety' as const; diff --git a/packages/constants/src/ValidationErrorCodes.ts b/packages/constants/src/ValidationErrorCodes.ts index 36c1c818d..fdce5ce3d 100644 --- a/packages/constants/src/ValidationErrorCodes.ts +++ b/packages/constants/src/ValidationErrorCodes.ts @@ -166,7 +166,6 @@ export const ValidationErrorCodes = { NO_ORIGINAL_EMAIL_ON_RECORD: 'NO_ORIGINAL_EMAIL_ON_RECORD', NO_UPLOADED_PARTS_TO_FINALIZE: 'NO_UPLOADED_PARTS_TO_FINALIZE', NO_VALID_MEDIA_IN_MESSAGE: 'NO_VALID_MEDIA_IN_MESSAGE', - NCMEC_ATTACHMENT_MUST_BE_IMAGE_OR_VIDEO: 'NCMEC_ATTACHMENT_MUST_BE_IMAGE_OR_VIDEO', NOT_A_VALID_UNICODE_EMOJI: 'NOT_A_VALID_UNICODE_EMOJI', ORIGINAL_EMAIL_ALREADY_VERIFIED: 'ORIGINAL_EMAIL_ALREADY_VERIFIED', ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST: 'ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST', diff --git a/packages/errors/src/domains/moderation/NcmecAlreadySubmittedError.ts b/packages/errors/src/domains/moderation/NcmecAlreadySubmittedError.ts deleted file mode 100644 index 5d71a5731..000000000 --- a/packages/errors/src/domains/moderation/NcmecAlreadySubmittedError.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; -import {ConflictError} from '@fluxer/errors/src/domains/core/ConflictError'; - -export class NcmecAlreadySubmittedError extends ConflictError { - constructor() { - super({ - code: APIErrorCodes.NCMEC_ALREADY_SUBMITTED, - }); - } -} diff --git a/packages/errors/src/domains/moderation/NcmecSubmissionFailedError.ts b/packages/errors/src/domains/moderation/NcmecSubmissionFailedError.ts deleted file mode 100644 index 3254d955d..000000000 --- a/packages/errors/src/domains/moderation/NcmecSubmissionFailedError.ts +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; -import {InternalServerError} from '@fluxer/errors/src/domains/core/InternalServerError'; - -export class NcmecSubmissionFailedError extends InternalServerError { - constructor(reason?: string) { - super({ - code: APIErrorCodes.NCMEC_SUBMISSION_FAILED, - ...(reason != null && {data: {reason}}), - }); - } -} diff --git a/packages/errors/src/i18n/ErrorCodeMappings.ts b/packages/errors/src/i18n/ErrorCodeMappings.ts index db7980292..22f6338d3 100644 --- a/packages/errors/src/i18n/ErrorCodeMappings.ts +++ b/packages/errors/src/i18n/ErrorCodeMappings.ts @@ -208,8 +208,6 @@ export const ErrorCodeToI18nKey = { [APIErrorCodes.MAX_STICKERS]: 'stickers_and_emojis.max_stickers_reached', [APIErrorCodes.MAX_WEBHOOKS_PER_CHANNEL]: 'webhooks.max_webhooks_per_channel_reached', [APIErrorCodes.MAX_WEBHOOKS_PER_GUILD]: 'webhooks.max_webhooks_per_guild_reached', - [APIErrorCodes.NCMEC_ALREADY_SUBMITTED]: 'content_and_safety.ncmec_already_submitted', - [APIErrorCodes.NCMEC_SUBMISSION_FAILED]: 'content_and_safety.ncmec_submission_failed', [APIErrorCodes.MEDIA_METADATA_ERROR]: 'media_and_memes.media_metadata_error', [APIErrorCodes.METHOD_NOT_ALLOWED]: 'http.method_not_allowed', [ValidationErrorCodes.MEDIA_ALREADY_IN_FAVORITE_MEMES]: 'media_and_memes.media_already_in_favorites', @@ -437,8 +435,6 @@ export const ErrorCodeToI18nKey = { [ValidationErrorCodes.USER_MUST_BE_A_BOT_TO_BE_MARKED_AS_A_SYSTEM_USER]: 'admin_and_system.user_must_be_bot_for_system_user', [ValidationErrorCodes.USER_IS_NOT_BANNED]: 'moderation_and_reports.user_is_not_banned', - [ValidationErrorCodes.NCMEC_ATTACHMENT_MUST_BE_IMAGE_OR_VIDEO]: - 'moderation_and_reports.ncmec_attachment_must_be_image_or_video', [ValidationErrorCodes.EMAIL_ALREADY_IN_USE]: 'email.email_already_in_use', [ValidationErrorCodes.EMAIL_IS_REQUIRED]: 'email.email_required', [ValidationErrorCodes.EMAIL_LENGTH_INVALID]: 'email.length_invalid', diff --git a/packages/errors/src/i18n/ErrorI18nMessages.ts b/packages/errors/src/i18n/ErrorI18nMessages.ts index 3aaa16886..9382e0926 100644 --- a/packages/errors/src/i18n/ErrorI18nMessages.ts +++ b/packages/errors/src/i18n/ErrorI18nMessages.ts @@ -262,8 +262,6 @@ export const ERROR_I18N_MESSAGES = { 'Connection verification failed. Make sure the verification token is correctly placed.', 'content_and_safety.content_blocked': 'This content was blocked by safety systems.', 'content_and_safety.explicit_content_cannot_be_sent': "Explicit content can't be sent.", - 'content_and_safety.ncmec_already_submitted': 'This content has already been submitted to NCMEC.', - 'content_and_safety.ncmec_submission_failed': "We couldn't submit the report to NCMEC. Please try again later.", 'content_and_safety.nsfw_age_restricted': 'NSFW content is age restricted.', 'discovery.already_applied': 'This community has already applied for Discovery.', 'discovery.application_already_reviewed': 'This Discovery application has already been reviewed.', @@ -445,8 +443,6 @@ export const ERROR_I18N_MESSAGES = { 'moderation_and_reports.invalid_dsa_verification_code': 'Invalid DSA verification code.', 'moderation_and_reports.invalid_report_flow_answers': "These answers don't match the report form. Start the report again.", - 'moderation_and_reports.ncmec_attachment_must_be_image_or_video': - 'Only image or video attachments can be reported to NCMEC.', 'moderation_and_reports.report_already_resolved': 'Report already resolved.', 'moderation_and_reports.report_banned': "You've been banned from submitting reports.", 'moderation_and_reports.report_flow_outdated': diff --git a/packages/errors/src/i18n/locales/ar.ts b/packages/errors/src/i18n/locales/ar.ts index 9174cf4c1..28fbe1cc1 100644 --- a/packages/errors/src/i18n/locales/ar.ts +++ b/packages/errors/src/i18n/locales/ar.ts @@ -215,8 +215,6 @@ const ERROR_I18N_AR_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "فشل التحقق من الاتصال. تأكد من وضع رمز التحقق في المكان الصحيح.", "content_and_safety.content_blocked": "حظرت أنظمة السلامة هذا المحتوى.", "content_and_safety.explicit_content_cannot_be_sent": "لا يمكن إرسال المحتوى الصريح.", - "content_and_safety.ncmec_already_submitted": "تم إرسال هذا المحتوى بالفعل إلى NCMEC.", - "content_and_safety.ncmec_submission_failed": "لم نتمكن من إرسال البلاغ إلى NCMEC. حاول مجددًا لاحقًا.", "content_and_safety.nsfw_age_restricted": "محتوى NSFW مقيّد بالعمر.", "discovery.already_applied": "لقد تقدم هذا المجتمع بالفعل بطلب الاكتشاف.", "discovery.application_already_reviewed": "تمت مراجعة طلب الاكتشاف هذا بالفعل.", @@ -381,7 +379,6 @@ const ERROR_I18N_AR_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "رمز تحقق DSA غير صالح.", "moderation_and_reports.invalid_report_flow_answers": "هذه الإجابات لا تتطابق مع نموذج الإبلاغ. ابدأ البلاغ من جديد.", "moderation_and_reports.ip_ban_declined": "لا يمكن إضافة عنوان IP هذا إلى قائمة الحظر.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "يمكن الإبلاغ إلى NCMEC عن مرفقات الصور أو الفيديو فقط.", "moderation_and_reports.report_already_resolved": "تم حل البلاغ بالفعل.", "moderation_and_reports.report_banned": "تم حظرك من إرسال البلاغات.", "moderation_and_reports.report_flow_outdated": "تغيّر نموذج الإبلاغ أثناء تعبئتك له. ابدأ البلاغ من جديد.", diff --git a/packages/errors/src/i18n/locales/bg.ts b/packages/errors/src/i18n/locales/bg.ts index 98671e72d..a4bfb1940 100644 --- a/packages/errors/src/i18n/locales/bg.ts +++ b/packages/errors/src/i18n/locales/bg.ts @@ -215,8 +215,6 @@ const ERROR_I18N_BG_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Проверката на връзката се провали. Увери се, че токенът за проверка е поставен правилно.", "content_and_safety.content_blocked": "Това съдържание беше блокирано от системите за безопасност.", "content_and_safety.explicit_content_cannot_be_sent": "Изрично съдържание не може да бъде изпратено.", - "content_and_safety.ncmec_already_submitted": "Това съдържание вече е изпратено до NCMEC.", - "content_and_safety.ncmec_submission_failed": "Не успяхме да изпратим сигнала до NCMEC. Опитай отново по-късно.", "content_and_safety.nsfw_age_restricted": "NSFW съдържанието е с възрастово ограничение.", "discovery.already_applied": "Тази общност вече е кандидатствала за Откриване.", "discovery.application_already_reviewed": "Това заявление за Откриване вече е разгледано.", @@ -381,7 +379,6 @@ const ERROR_I18N_BG_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Невалиден код за потвърждение на DSA.", "moderation_and_reports.invalid_report_flow_answers": "Тези отговори не съответстват на формуляра за сигнал. Започни сигнала отначало.", "moderation_and_reports.ip_ban_declined": "Този IP адрес не може да бъде добавен към списъка с блокирани адреси.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Само прикачени файлове с изображения или видеоклипове могат да се докладват на NCMEC.", "moderation_and_reports.report_already_resolved": "Сигналът вече е разгледан.", "moderation_and_reports.report_banned": "Имаш забрана да подаваш сигнали.", "moderation_and_reports.report_flow_outdated": "Формулярът за сигнал се промени, докато го попълваше. Започни сигнала отначало.", diff --git a/packages/errors/src/i18n/locales/cs.ts b/packages/errors/src/i18n/locales/cs.ts index 9fdffa0ef..2a5676e71 100644 --- a/packages/errors/src/i18n/locales/cs.ts +++ b/packages/errors/src/i18n/locales/cs.ts @@ -215,8 +215,6 @@ const ERROR_I18N_CS_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Ověření propojení se nezdařilo. Zkontrolujte, zda je ověřovací token správně umístěn.", "content_and_safety.content_blocked": "Tento obsah byl zablokován bezpečnostními systémy.", "content_and_safety.explicit_content_cannot_be_sent": "Explicitní obsah nelze odeslat.", - "content_and_safety.ncmec_already_submitted": "Tento obsah již byl odeslán NCMEC.", - "content_and_safety.ncmec_submission_failed": "Nepodařilo se odeslat nahlášení organizaci NCMEC. Zkuste to znovu později.", "content_and_safety.nsfw_age_restricted": "Obsah NSFW je věkově omezený.", "discovery.already_applied": "Tato komunita již požádala o zařazení do Objevování.", "discovery.application_already_reviewed": "Tato žádost o zařazení do Objevování už byla zkontrolována.", @@ -381,7 +379,6 @@ const ERROR_I18N_CS_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Neplatný ověřovací kód DSA.", "moderation_and_reports.invalid_report_flow_answers": "Tyto odpovědi neodpovídají formuláři nahlášení. Začněte nahlášení znovu.", "moderation_and_reports.ip_ban_declined": "Tuto IP adresu nelze přidat na seznam blokovaných.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Organizaci NCMEC lze nahlásit pouze přílohy s obrázky nebo videi.", "moderation_and_reports.report_already_resolved": "Nahlášení již bylo vyřešeno.", "moderation_and_reports.report_banned": "Máte zakázáno odesílat nahlášení.", "moderation_and_reports.report_flow_outdated": "Formulář nahlášení se během jeho vyplňování změnil. Nahlášení začněte znovu.", diff --git a/packages/errors/src/i18n/locales/da.ts b/packages/errors/src/i18n/locales/da.ts index 461828d60..852e74051 100644 --- a/packages/errors/src/i18n/locales/da.ts +++ b/packages/errors/src/i18n/locales/da.ts @@ -215,8 +215,6 @@ const ERROR_I18N_DA_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Verifikation af forbindelsen mislykkedes. Sørg for, at verifikationstokenet er placeret korrekt.", "content_and_safety.content_blocked": "Dette indhold blev blokeret af sikkerhedssystemer.", "content_and_safety.explicit_content_cannot_be_sent": "Eksplicit indhold kan ikke sendes.", - "content_and_safety.ncmec_already_submitted": "Dette indhold er allerede indsendt til NCMEC.", - "content_and_safety.ncmec_submission_failed": "Vi kunne ikke indsende anmeldelsen til NCMEC. Prøv igen senere.", "content_and_safety.nsfw_age_restricted": "NSFW-indhold er aldersbegrænset.", "discovery.already_applied": "Dette fællesskab har allerede ansøgt om at blive vist i Opdag.", "discovery.application_already_reviewed": "Denne ansøgning til Opdag er allerede blevet behandlet.", @@ -381,7 +379,6 @@ const ERROR_I18N_DA_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Ugyldig DSA-bekræftelseskode.", "moderation_and_reports.invalid_report_flow_answers": "Disse svar passer ikke til anmeldelsesformularen. Start anmeldelsen forfra.", "moderation_and_reports.ip_ban_declined": "Denne IP-adresse kan ikke føjes til bloklisten.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Kun vedhæftede billeder eller videoer kan anmeldes til NCMEC.", "moderation_and_reports.report_already_resolved": "Anmeldelsen er allerede afsluttet.", "moderation_and_reports.report_banned": "Du er blevet udelukket fra at indsende anmeldelser.", "moderation_and_reports.report_flow_outdated": "Anmeldelsesformularen blev ændret, mens du udfyldte den. Start anmeldelsen forfra.", diff --git a/packages/errors/src/i18n/locales/de.ts b/packages/errors/src/i18n/locales/de.ts index cc6a347c2..3823d9092 100644 --- a/packages/errors/src/i18n/locales/de.ts +++ b/packages/errors/src/i18n/locales/de.ts @@ -215,8 +215,6 @@ const ERROR_I18N_DE_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Die Verbindungsüberprüfung ist fehlgeschlagen. Stelle sicher, dass das Verifizierungs-Token richtig platziert ist.", "content_and_safety.content_blocked": "Dieser Inhalt wurde von den Sicherheitssystemen blockiert.", "content_and_safety.explicit_content_cannot_be_sent": "Explizite Inhalte können nicht gesendet werden.", - "content_and_safety.ncmec_already_submitted": "Dieser Inhalt wurde bereits an NCMEC gemeldet.", - "content_and_safety.ncmec_submission_failed": "Wir konnten die Meldung nicht an NCMEC senden. Versuch es bitte später noch einmal.", "content_and_safety.nsfw_age_restricted": "NSFW-Inhalte sind altersbeschränkt.", "discovery.already_applied": "Diese Community hat bereits die Aufnahme unter „Entdecken“ beantragt.", "discovery.application_already_reviewed": "Dieser Antrag auf Aufnahme unter „Entdecken“ wurde bereits geprüft.", @@ -381,7 +379,6 @@ const ERROR_I18N_DE_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Ungültiger DSA-Bestätigungscode.", "moderation_and_reports.invalid_report_flow_answers": "Diese Antworten passen nicht zum Meldeformular. Starte die Meldung erneut.", "moderation_and_reports.ip_ban_declined": "Diese IP-Adresse kann nicht zur Blockierliste hinzugefügt werden.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Nur Bild- oder Video-Anhänge können bei NCMEC gemeldet werden.", "moderation_and_reports.report_already_resolved": "Meldung bereits erledigt.", "moderation_and_reports.report_banned": "Du bist für das Einreichen von Meldungen gesperrt.", "moderation_and_reports.report_flow_outdated": "Das Meldeformular hat sich geändert, während du es ausgefüllt hast. Starte die Meldung erneut.", diff --git a/packages/errors/src/i18n/locales/el.ts b/packages/errors/src/i18n/locales/el.ts index c3e37d9ae..0e3cb25b4 100644 --- a/packages/errors/src/i18n/locales/el.ts +++ b/packages/errors/src/i18n/locales/el.ts @@ -215,8 +215,6 @@ const ERROR_I18N_EL_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Η επαλήθευση σύνδεσης απέτυχε. Βεβαιώσου ότι το token επαλήθευσης έχει τοποθετηθεί σωστά.", "content_and_safety.content_blocked": "Αυτό το περιεχόμενο αποκλείστηκε από τα συστήματα ασφαλείας.", "content_and_safety.explicit_content_cannot_be_sent": "Το ακατάλληλο περιεχόμενο δεν μπορεί να σταλεί.", - "content_and_safety.ncmec_already_submitted": "Αυτό το περιεχόμενο έχει ήδη σταλεί στο NCMEC.", - "content_and_safety.ncmec_submission_failed": "Δεν μπορέσαμε να στείλουμε την αναφορά στο NCMEC. Δοκίμασε ξανά αργότερα.", "content_and_safety.nsfw_age_restricted": "Το περιεχόμενο NSFW έχει ηλικιακό περιορισμό.", "discovery.already_applied": "Αυτή η κοινότητα έχει ήδη υποβάλει αίτηση για την Ανακάλυψη.", "discovery.application_already_reviewed": "Αυτή η αίτηση για την Ανακάλυψη έχει ήδη εξεταστεί.", @@ -381,7 +379,6 @@ const ERROR_I18N_EL_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Μη έγκυρος κωδικός επαλήθευσης DSA.", "moderation_and_reports.invalid_report_flow_answers": "Αυτές οι απαντήσεις δεν ταιριάζουν με τη φόρμα αναφοράς. Ξεκίνα ξανά την αναφορά.", "moderation_and_reports.ip_ban_declined": "Αυτή η διεύθυνση IP δεν μπορεί να προστεθεί στη λίστα αποκλεισμού.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Στο NCMEC μπορούν να αναφερθούν μόνο συνημμένα εικόνας ή βίντεο.", "moderation_and_reports.report_already_resolved": "Η αναφορά έχει ήδη διευθετηθεί.", "moderation_and_reports.report_banned": "Έχεις αποκλειστεί από την υποβολή αναφορών.", "moderation_and_reports.report_flow_outdated": "Η φόρμα αναφοράς άλλαξε ενώ τη συμπλήρωνες. Ξεκίνα ξανά την αναφορά.", diff --git a/packages/errors/src/i18n/locales/en-GB.ts b/packages/errors/src/i18n/locales/en-GB.ts index 9ddae7c7c..13490a8ba 100644 --- a/packages/errors/src/i18n/locales/en-GB.ts +++ b/packages/errors/src/i18n/locales/en-GB.ts @@ -215,8 +215,6 @@ const ERROR_I18N_EN_GB_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Connection verification failed. Make sure the verification token is correctly placed.", "content_and_safety.content_blocked": "This content was blocked by safety systems.", "content_and_safety.explicit_content_cannot_be_sent": "Explicit content can't be sent.", - "content_and_safety.ncmec_already_submitted": "This content has already been submitted to NCMEC.", - "content_and_safety.ncmec_submission_failed": "We couldn't submit the report to NCMEC. Please try again later.", "content_and_safety.nsfw_age_restricted": "NSFW content is age restricted.", "discovery.already_applied": "This community has already applied for Discovery.", "discovery.application_already_reviewed": "This Discovery application has already been reviewed.", @@ -381,7 +379,6 @@ const ERROR_I18N_EN_GB_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Invalid DSA verification code.", "moderation_and_reports.invalid_report_flow_answers": "These answers don't match the report form. Start the report again.", "moderation_and_reports.ip_ban_declined": "This IP address cannot be added to the blocklist.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Only image or video attachments can be reported to NCMEC.", "moderation_and_reports.report_already_resolved": "Report already resolved.", "moderation_and_reports.report_banned": "You've been banned from submitting reports.", "moderation_and_reports.report_flow_outdated": "The report form changed while you were filling it in. Start the report again.", diff --git a/packages/errors/src/i18n/locales/es-419.ts b/packages/errors/src/i18n/locales/es-419.ts index 999650fc9..d96f557ca 100644 --- a/packages/errors/src/i18n/locales/es-419.ts +++ b/packages/errors/src/i18n/locales/es-419.ts @@ -215,8 +215,6 @@ const ERROR_I18N_ES_419_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Falló la verificación de la conexión. Asegúrate de que el token de verificación esté colocado correctamente.", "content_and_safety.content_blocked": "Este contenido fue bloqueado por sistemas de seguridad.", "content_and_safety.explicit_content_cannot_be_sent": "No se puede enviar contenido explícito.", - "content_and_safety.ncmec_already_submitted": "Este contenido ya fue remitido a NCMEC.", - "content_and_safety.ncmec_submission_failed": "No se pudo enviar el reporte a NCMEC. Inténtalo de nuevo más tarde.", "content_and_safety.nsfw_age_restricted": "El contenido NSFW tiene restricción de edad.", "discovery.already_applied": "Esta comunidad ya ha solicitado aparecer en Descubrir.", "discovery.application_already_reviewed": "Esta solicitud para aparecer en Descubrir ya ha sido revisada.", @@ -381,7 +379,6 @@ const ERROR_I18N_ES_419_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Código de verificación DSA no válido.", "moderation_and_reports.invalid_report_flow_answers": "Estas respuestas no coinciden con el formulario de reporte. Vuelve a iniciar el reporte.", "moderation_and_reports.ip_ban_declined": "Esta dirección IP no se puede agregar a la lista de bloqueo.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Solo se pueden reportar a NCMEC archivos adjuntos de imagen o video.", "moderation_and_reports.report_already_resolved": "El reporte ya se resolvió.", "moderation_and_reports.report_banned": "Se te ha prohibido enviar reportes.", "moderation_and_reports.report_flow_outdated": "El formulario de reporte cambió mientras lo completabas. Vuelve a iniciar el reporte.", diff --git a/packages/errors/src/i18n/locales/es-ES.ts b/packages/errors/src/i18n/locales/es-ES.ts index 06838a238..ac4e39d1f 100644 --- a/packages/errors/src/i18n/locales/es-ES.ts +++ b/packages/errors/src/i18n/locales/es-ES.ts @@ -215,8 +215,6 @@ const ERROR_I18N_ES_ES_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "La verificación de la conexión ha fallado. Asegúrate de que el token de verificación esté colocado correctamente.", "content_and_safety.content_blocked": "Este contenido ha sido bloqueado por los sistemas de seguridad.", "content_and_safety.explicit_content_cannot_be_sent": "No se puede enviar contenido explícito.", - "content_and_safety.ncmec_already_submitted": "Este contenido ya ha sido enviado a NCMEC.", - "content_and_safety.ncmec_submission_failed": "No hemos podido enviar la denuncia a NCMEC. Inténtalo de nuevo más tarde.", "content_and_safety.nsfw_age_restricted": "El contenido NSFW tiene restricción de edad.", "discovery.already_applied": "Esta comunidad ya ha solicitado aparecer en Descubrir.", "discovery.application_already_reviewed": "Esta solicitud para aparecer en Descubrir ya ha sido revisada.", @@ -381,7 +379,6 @@ const ERROR_I18N_ES_ES_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Código de verificación DSA no válido.", "moderation_and_reports.invalid_report_flow_answers": "Estas respuestas no coinciden con el formulario de denuncia. Vuelve a empezar la denuncia.", "moderation_and_reports.ip_ban_declined": "Esta dirección IP no se puede añadir a la lista de bloqueo.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Solo se pueden denunciar adjuntos de imagen o vídeo a NCMEC.", "moderation_and_reports.report_already_resolved": "La denuncia ya se ha resuelto.", "moderation_and_reports.report_banned": "Se te ha prohibido enviar denuncias.", "moderation_and_reports.report_flow_outdated": "El formulario de denuncia ha cambiado mientras lo rellenabas. Vuelve a empezar la denuncia.", diff --git a/packages/errors/src/i18n/locales/fi.ts b/packages/errors/src/i18n/locales/fi.ts index 3abfc08ba..1454e4e2f 100644 --- a/packages/errors/src/i18n/locales/fi.ts +++ b/packages/errors/src/i18n/locales/fi.ts @@ -215,8 +215,6 @@ const ERROR_I18N_FI_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Yhteyden vahvistus epäonnistui. Varmista, että vahvistustunnus on sijoitettu oikeaan paikkaan.", "content_and_safety.content_blocked": "Turvajärjestelmät estivät tämän sisällön.", "content_and_safety.explicit_content_cannot_be_sent": "Seksuaalisesti eksplisiittistä sisältöä ei voi lähettää.", - "content_and_safety.ncmec_already_submitted": "Tämä sisältö on jo toimitettu NCMEC:lle.", - "content_and_safety.ncmec_submission_failed": "Ilmoituksen lähettäminen NCMEC:lle epäonnistui. Yritä uudelleen myöhemmin.", "content_and_safety.nsfw_age_restricted": "NSFW-sisällöllä on ikärajoitus.", "discovery.already_applied": "Tämä yhteisö on jo hakenut Tutustu-näkymään.", "discovery.application_already_reviewed": "Tämä hakemus Tutustu-näkymään on jo käsitelty.", @@ -381,7 +379,6 @@ const ERROR_I18N_FI_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Virheellinen DSA-vahvistuskoodi.", "moderation_and_reports.invalid_report_flow_answers": "Nämä vastaukset eivät vastaa ilmoituslomaketta. Aloita ilmoitus uudelleen.", "moderation_and_reports.ip_ban_declined": "Tätä IP-osoitetta ei voi lisätä estolistalle.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "NCMEC:lle voi ilmoittaa vain kuva- tai videoliitteistä.", "moderation_and_reports.report_already_resolved": "Ilmoitus on jo ratkaistu.", "moderation_and_reports.report_banned": "Sinut on estetty lähettämästä ilmoituksia.", "moderation_and_reports.report_flow_outdated": "Ilmoituslomake muuttui, kun täytit sitä. Aloita ilmoitus uudelleen.", diff --git a/packages/errors/src/i18n/locales/fr.ts b/packages/errors/src/i18n/locales/fr.ts index 8e87d210f..db0c2ea7f 100644 --- a/packages/errors/src/i18n/locales/fr.ts +++ b/packages/errors/src/i18n/locales/fr.ts @@ -215,8 +215,6 @@ const ERROR_I18N_FR_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "La vérification de la connexion a échoué. Vérifiez que le jeton de vérification est placé correctement.", "content_and_safety.content_blocked": "Ce contenu a été bloqué par les systèmes de sécurité.", "content_and_safety.explicit_content_cannot_be_sent": "Le contenu explicite ne peut pas être envoyé.", - "content_and_safety.ncmec_already_submitted": "Ce contenu a déjà été soumis à NCMEC.", - "content_and_safety.ncmec_submission_failed": "Impossible d’envoyer le signalement au NCMEC. Réessayez plus tard.", "content_and_safety.nsfw_age_restricted": "Le contenu NSFW est soumis à une restriction d'âge.", "discovery.already_applied": "Cette communauté a déjà demandé à figurer dans Découverte.", "discovery.application_already_reviewed": "Cette demande d’inscription dans Découverte a déjà été examinée.", @@ -381,7 +379,6 @@ const ERROR_I18N_FR_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Code de vérification DSA invalide.", "moderation_and_reports.invalid_report_flow_answers": "Ces réponses ne correspondent pas au formulaire de signalement. Veuillez recommencer le signalement.", "moderation_and_reports.ip_ban_declined": "Cette adresse IP ne peut pas être ajoutée à la liste de blocage.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Seules les pièces jointes de type image ou vidéo peuvent être signalées au NCMEC.", "moderation_and_reports.report_already_resolved": "Signalement déjà résolu.", "moderation_and_reports.report_banned": "Vous n’êtes plus autorisé à envoyer des signalements.", "moderation_and_reports.report_flow_outdated": "Le formulaire de signalement a changé pendant que vous le remplissiez. Veuillez recommencer le signalement.", diff --git a/packages/errors/src/i18n/locales/he.ts b/packages/errors/src/i18n/locales/he.ts index 4aad41b42..50337edba 100644 --- a/packages/errors/src/i18n/locales/he.ts +++ b/packages/errors/src/i18n/locales/he.ts @@ -215,8 +215,6 @@ const ERROR_I18N_HE_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "אימות החיבור נכשל. נא לוודא שאסימון האימות ממוקם נכון.", "content_and_safety.content_blocked": "תוכן זה נחסם על ידי מערכות הבטיחות.", "content_and_safety.explicit_content_cannot_be_sent": "לא ניתן לשלוח תוכן מפורש.", - "content_and_safety.ncmec_already_submitted": "תוכן זה כבר הוגש ל-NCMEC.", - "content_and_safety.ncmec_submission_failed": "לא הצלחנו להגיש את הדיווח ל-NCMEC. ניתן לנסות שוב מאוחר יותר.", "content_and_safety.nsfw_age_restricted": "תוכן NSFW מוגבל בגיל.", "discovery.already_applied": "קהילה זו כבר הגישה בקשה לגילוי.", "discovery.application_already_reviewed": "בקשת הגילוי הזו כבר נבדקה.", @@ -381,7 +379,6 @@ const ERROR_I18N_HE_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "קוד אימות DSA שגוי.", "moderation_and_reports.invalid_report_flow_answers": "התשובות האלה לא תואמות לטופס הדיווח. יש להתחיל את הדיווח מחדש.", "moderation_and_reports.ip_ban_declined": "לא ניתן להוסיף כתובת IP זו לרשימת החסימה.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "ניתן לדווח ל-NCMEC רק על קבצים מצורפים שהם תמונות או סרטונים.", "moderation_and_reports.report_already_resolved": "הדיווח כבר טופל.", "moderation_and_reports.report_banned": "נחסמת מלהגיש דיווחים.", "moderation_and_reports.report_flow_outdated": "טופס הדיווח השתנה במהלך המילוי. יש להתחיל את הדיווח מחדש.", diff --git a/packages/errors/src/i18n/locales/hi.ts b/packages/errors/src/i18n/locales/hi.ts index f29debd1c..670034402 100644 --- a/packages/errors/src/i18n/locales/hi.ts +++ b/packages/errors/src/i18n/locales/hi.ts @@ -215,8 +215,6 @@ const ERROR_I18N_HI_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "कनेक्शन वेरिफ़िकेशन फेल हो गया। पक्का करें कि वेरिफ़िकेशन टोकन सही जगह पर रखा गया है।", "content_and_safety.content_blocked": "इस कंटेंट को सेफ्टी सिस्टम ने ब्लॉक कर दिया है।", "content_and_safety.explicit_content_cannot_be_sent": "एक्सप्लिसिट कंटेंट नहीं भेजा जा सकता।", - "content_and_safety.ncmec_already_submitted": "यह कंटेंट पहले ही NCMEC को भेजा जा चुका है।", - "content_and_safety.ncmec_submission_failed": "हम NCMEC को रिपोर्ट भेज नहीं पाए। कृपया बाद में फिर से कोशिश करें।", "content_and_safety.nsfw_age_restricted": "NSFW कंटेंट उम्र के हिसाब से प्रतिबंधित है।", "discovery.already_applied": "इस कम्युनिटी ने पहले ही डिस्कवरी के लिए अप्लाई कर दिया है।", "discovery.application_already_reviewed": "इस डिस्कवरी आवेदन की पहले ही समीक्षा हो चुकी है।", @@ -381,7 +379,6 @@ const ERROR_I18N_HI_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "अमान्य DSA वेरिफ़िकेशन कोड।", "moderation_and_reports.invalid_report_flow_answers": "ये जवाब रिपोर्ट फ़ॉर्म से मेल नहीं खाते। रिपोर्ट को फिर से शुरू करें।", "moderation_and_reports.ip_ban_declined": "इस IP पते को ब्लॉकलिस्ट में नहीं जोड़ा जा सकता।", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "सिर्फ़ इमेज या वीडियो अटैचमेंट ही NCMEC को रिपोर्ट किए जा सकते हैं।", "moderation_and_reports.report_already_resolved": "रिपोर्ट पहले ही हल हो चुकी है।", "moderation_and_reports.report_banned": "आपको रिपोर्ट सबमिट करने से बैन कर दिया गया है।", "moderation_and_reports.report_flow_outdated": "रिपोर्ट फ़ॉर्म भरते समय बदल गया था। कृपया रिपोर्ट दोबारा शुरू करें।", diff --git a/packages/errors/src/i18n/locales/hr.ts b/packages/errors/src/i18n/locales/hr.ts index c8f10495c..7813d84e5 100644 --- a/packages/errors/src/i18n/locales/hr.ts +++ b/packages/errors/src/i18n/locales/hr.ts @@ -215,8 +215,6 @@ const ERROR_I18N_HR_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Provjera veze nije uspjela. Uvjeri se da je token za provjeru ispravno postavljen.", "content_and_safety.content_blocked": "Sigurnosni sustavi blokirali su ovaj sadržaj.", "content_and_safety.explicit_content_cannot_be_sent": "Eksplicitni sadržaj ne može se poslati.", - "content_and_safety.ncmec_already_submitted": "Ovaj sadržaj je već poslan NCMEC-u.", - "content_and_safety.ncmec_submission_failed": "Nismo mogli poslati izvješće NCMEC-u. Pokušaj ponovno kasnije.", "content_and_safety.nsfw_age_restricted": "NSFW sadržaj je dobno ograničen.", "discovery.already_applied": "Ova zajednica već je podnijela prijavu za otkrivanje.", "discovery.application_already_reviewed": "Ova prijava za otkrivanje već je pregledana.", @@ -381,7 +379,6 @@ const ERROR_I18N_HR_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Nevažeći DSA kod za provjeru.", "moderation_and_reports.invalid_report_flow_answers": "Ovi odgovori ne odgovaraju obrascu prijave. Započni prijavu ponovno.", "moderation_and_reports.ip_ban_declined": "Ovu IP adresu nije moguće dodati na popis blokiranih.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "NCMEC-u se mogu prijaviti samo slikovni ili video privitci.", "moderation_and_reports.report_already_resolved": "Prijava je već riješena.", "moderation_and_reports.report_banned": "Zabranjeno ti je podnositi prijave.", "moderation_and_reports.report_flow_outdated": "Obrazac prijave promijenio se tijekom ispunjavanja. Započni prijavu ponovno.", diff --git a/packages/errors/src/i18n/locales/hu.ts b/packages/errors/src/i18n/locales/hu.ts index a45f15cf2..da702d2f5 100644 --- a/packages/errors/src/i18n/locales/hu.ts +++ b/packages/errors/src/i18n/locales/hu.ts @@ -215,8 +215,6 @@ const ERROR_I18N_HU_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "A kapcsolat ellenőrzése sikertelen. Ellenőrizd, hogy az ellenőrző token a helyén van.", "content_and_safety.content_blocked": "Ezt a tartalmat a biztonsági rendszerek blokkolták.", "content_and_safety.explicit_content_cannot_be_sent": "Explicit tartalom nem küldhető.", - "content_and_safety.ncmec_already_submitted": "Ezt a tartalmat már beküldtük az NCMEC-nek.", - "content_and_safety.ncmec_submission_failed": "Nem sikerült beküldeni a bejelentést az NCMEC-nek. Próbáld meg újra később.", "content_and_safety.nsfw_age_restricted": "Az NSFW-tartalom korhatáros.", "discovery.already_applied": "Ez a közösség már jelentkezett a Felfedezésbe.", "discovery.application_already_reviewed": "Ezt a Felfedezésbe való jelentkezést már elbírálták.", @@ -381,7 +379,6 @@ const ERROR_I18N_HU_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Érvénytelen DSA-ellenőrzőkód.", "moderation_and_reports.invalid_report_flow_answers": "Ezek a válaszok nem egyeznek a bejelentési űrlappal. Kezdd elölről a bejelentést.", "moderation_and_reports.ip_ban_declined": "Ez az IP-cím nem adható hozzá a tiltólistához.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Csak kép- vagy videómellékleteket lehet jelenteni az NCMEC-nek.", "moderation_and_reports.report_already_resolved": "A bejelentést már lezárták.", "moderation_and_reports.report_banned": "Eltiltottak a bejelentések küldésétől.", "moderation_and_reports.report_flow_outdated": "A bejelentési űrlap megváltozott, amíg kitöltötted. Kezdd elölről a bejelentést.", diff --git a/packages/errors/src/i18n/locales/id.ts b/packages/errors/src/i18n/locales/id.ts index c904bc477..cba95e617 100644 --- a/packages/errors/src/i18n/locales/id.ts +++ b/packages/errors/src/i18n/locales/id.ts @@ -215,8 +215,6 @@ const ERROR_I18N_ID_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Verifikasi koneksi gagal. Pastikan token verifikasi ditempatkan dengan benar.", "content_and_safety.content_blocked": "Konten ini diblokir oleh sistem keamanan.", "content_and_safety.explicit_content_cannot_be_sent": "Konten eksplisit tidak bisa dikirim.", - "content_and_safety.ncmec_already_submitted": "Konten ini sudah dilaporkan ke NCMEC.", - "content_and_safety.ncmec_submission_failed": "Laporan tidak bisa dikirim ke NCMEC. Coba lagi nanti.", "content_and_safety.nsfw_age_restricted": "Konten NSFW dibatasi usia.", "discovery.already_applied": "Komunitas ini sudah mendaftar ke Discovery.", "discovery.application_already_reviewed": "Pengajuan Discovery ini sudah ditinjau.", @@ -381,7 +379,6 @@ const ERROR_I18N_ID_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Kode verifikasi DSA tidak valid.", "moderation_and_reports.invalid_report_flow_answers": "Jawaban ini tidak sesuai dengan formulir laporan. Mulai ulang laporanmu.", "moderation_and_reports.ip_ban_declined": "Alamat IP ini tidak dapat ditambahkan ke daftar blokir.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Hanya lampiran berupa gambar atau video yang dapat dilaporkan ke NCMEC.", "moderation_and_reports.report_already_resolved": "Laporan sudah ditangani.", "moderation_and_reports.report_banned": "Kamu dilarang mengirim laporan.", "moderation_and_reports.report_flow_outdated": "Formulir laporan berubah saat kamu mengisinya. Mulai ulang laporanmu.", diff --git a/packages/errors/src/i18n/locales/it.ts b/packages/errors/src/i18n/locales/it.ts index 071354142..55a3ac46c 100644 --- a/packages/errors/src/i18n/locales/it.ts +++ b/packages/errors/src/i18n/locales/it.ts @@ -215,8 +215,6 @@ const ERROR_I18N_IT_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Verifica della connessione fallita. Assicurati che il token di verifica sia posizionato correttamente.", "content_and_safety.content_blocked": "Questo contenuto è stato bloccato dai sistemi di sicurezza.", "content_and_safety.explicit_content_cannot_be_sent": "Non è possibile inviare contenuti sessualmente espliciti.", - "content_and_safety.ncmec_already_submitted": "Questo contenuto è già stato inviato a NCMEC.", - "content_and_safety.ncmec_submission_failed": "Impossibile inviare la segnalazione a NCMEC. Riprova più tardi.", "content_and_safety.nsfw_age_restricted": "Il contenuto NSFW ha restrizioni di età.", "discovery.already_applied": "Questa community ha già inviato una candidatura per Scopri.", "discovery.application_already_reviewed": "Questa candidatura per Scopri è già stata esaminata.", @@ -381,7 +379,6 @@ const ERROR_I18N_IT_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Codice di verifica DSA non valido.", "moderation_and_reports.invalid_report_flow_answers": "Queste risposte non corrispondono al modulo di segnalazione. Ricomincia la segnalazione.", "moderation_and_reports.ip_ban_declined": "Questo indirizzo IP non può essere aggiunto all'elenco di blocco.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Solo allegati di immagini o video possono essere segnalati a NCMEC.", "moderation_and_reports.report_already_resolved": "La segnalazione è già stata risolta.", "moderation_and_reports.report_banned": "Non hai più il permesso di inviare segnalazioni.", "moderation_and_reports.report_flow_outdated": "Il modulo di segnalazione è cambiato mentre lo stavi compilando. Ricomincia la segnalazione.", diff --git a/packages/errors/src/i18n/locales/ja.ts b/packages/errors/src/i18n/locales/ja.ts index b432d872b..2749a3872 100644 --- a/packages/errors/src/i18n/locales/ja.ts +++ b/packages/errors/src/i18n/locales/ja.ts @@ -215,8 +215,6 @@ const ERROR_I18N_JA_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "接続の検証に失敗しました。検証トークンが正しく配置されているかご確認ください。", "content_and_safety.content_blocked": "このコンテンツは安全システムによってブロックされました。", "content_and_safety.explicit_content_cannot_be_sent": "不適切なコンテンツは送信できません。", - "content_and_safety.ncmec_already_submitted": "このコンテンツはすでにNCMECに提出されています。", - "content_and_safety.ncmec_submission_failed": "NCMECへの報告を送信できませんでした。後でもう一度お試しください。", "content_and_safety.nsfw_age_restricted": "NSFWコンテンツには年齢制限があります。", "discovery.already_applied": "このコミュニティはすでに「見つける」への掲載を申請しています。", "discovery.application_already_reviewed": "「見つける」への掲載申請はすでに審査済みです。", @@ -381,7 +379,6 @@ const ERROR_I18N_JA_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "DSA確認コードが無効です。", "moderation_and_reports.invalid_report_flow_answers": "これらの回答は報告フォームと一致しません。最初から報告し直してください。", "moderation_and_reports.ip_ban_declined": "このIPアドレスはブロックリストに追加できません。", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "NCMECに報告できるのは画像または動画の添付ファイルのみです。", "moderation_and_reports.report_already_resolved": "報告はすでに解決済みです。", "moderation_and_reports.report_banned": "報告の送信は禁止されています。", "moderation_and_reports.report_flow_outdated": "入力中に報告フォームが変更されました。最初から報告し直してください。", diff --git a/packages/errors/src/i18n/locales/ko.ts b/packages/errors/src/i18n/locales/ko.ts index 433554a08..ee2c2eb23 100644 --- a/packages/errors/src/i18n/locales/ko.ts +++ b/packages/errors/src/i18n/locales/ko.ts @@ -215,8 +215,6 @@ const ERROR_I18N_KO_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "연결 인증에 실패했어요. 인증 토큰이 올바른 위치에 있는지 확인해 주세요.", "content_and_safety.content_blocked": "안전 시스템이 이 콘텐츠를 차단했어요.", "content_and_safety.explicit_content_cannot_be_sent": "선정적인 콘텐츠는 보낼 수 없어요.", - "content_and_safety.ncmec_already_submitted": "이 콘텐츠는 이미 NCMEC에 제출된 상태예요.", - "content_and_safety.ncmec_submission_failed": "NCMEC에 신고를 제출하지 못했어요. 나중에 다시 시도해 주세요.", "content_and_safety.nsfw_age_restricted": "NSFW 콘텐츠에는 연령 제한이 적용돼요.", "discovery.already_applied": "이 커뮤니티는 이미 디스커버리에 신청했어요.", "discovery.application_already_reviewed": "이 디스커버리 신청은 이미 검토되었어요.", @@ -381,7 +379,6 @@ const ERROR_I18N_KO_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "DSA 인증 코드가 올바르지 않아요.", "moderation_and_reports.invalid_report_flow_answers": "이 답변은 신고 양식과 맞지 않아요. 신고를 다시 시작해 주세요.", "moderation_and_reports.ip_ban_declined": "이 IP 주소는 차단 목록에 추가할 수 없어요.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "이미지 또는 동영상 첨부 파일만 NCMEC에 신고할 수 있어요.", "moderation_and_reports.report_already_resolved": "이미 해결된 신고예요.", "moderation_and_reports.report_banned": "신고 제출이 금지되었어요.", "moderation_and_reports.report_flow_outdated": "작성하는 동안 신고 양식이 변경되었어요. 신고를 다시 시작해 주세요.", diff --git a/packages/errors/src/i18n/locales/lt.ts b/packages/errors/src/i18n/locales/lt.ts index 25ffeabce..062a52617 100644 --- a/packages/errors/src/i18n/locales/lt.ts +++ b/packages/errors/src/i18n/locales/lt.ts @@ -215,8 +215,6 @@ const ERROR_I18N_LT_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Ryšio patvirtinimas nepavyko. Įsitikink, kad patvirtinimo žetonas įdėtas teisingai.", "content_and_safety.content_blocked": "Šį turinį užblokavo saugumo sistemos.", "content_and_safety.explicit_content_cannot_be_sent": "Nepadorus turinys negali būti siunčiamas.", - "content_and_safety.ncmec_already_submitted": "Šis turinys jau išsiųstas NCMEC.", - "content_and_safety.ncmec_submission_failed": "Nepavyko išsiųsti pranešimo NCMEC. Pabandyk dar kartą vėliau.", "content_and_safety.nsfw_age_restricted": "NSFW turiniui taikomas amžiaus ribojimas.", "discovery.already_applied": "Ši bendruomenė jau pateikė Atradimo paraišką.", "discovery.application_already_reviewed": "Ši Atradimo paraiška jau peržiūrėta.", @@ -381,7 +379,6 @@ const ERROR_I18N_LT_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Neteisingas DSA patvirtinimo kodas.", "moderation_and_reports.invalid_report_flow_answers": "Šie atsakymai neatitinka pranešimo formos. Pradėk pranešimą iš naujo.", "moderation_and_reports.ip_ban_declined": "Šio IP adreso negalima įtraukti į blokavimo sąrašą.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "NCMEC galima pranešti tik apie paveikslėlių ar vaizdo įrašų priedus.", "moderation_and_reports.report_already_resolved": "Pranešimas jau išnagrinėtas.", "moderation_and_reports.report_banned": "Tau uždrausta teikti pranešimus.", "moderation_and_reports.report_flow_outdated": "Pranešimo forma pasikeitė, kol ją pildei. Pradėk pranešimą iš naujo.", diff --git a/packages/errors/src/i18n/locales/nl.ts b/packages/errors/src/i18n/locales/nl.ts index e66064453..e0b9c3ea7 100644 --- a/packages/errors/src/i18n/locales/nl.ts +++ b/packages/errors/src/i18n/locales/nl.ts @@ -215,8 +215,6 @@ const ERROR_I18N_NL_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Verificatie van de verbinding mislukt. Controleer of het verificatietoken juist is geplaatst.", "content_and_safety.content_blocked": "Deze inhoud is geblokkeerd door veiligheidssystemen.", "content_and_safety.explicit_content_cannot_be_sent": "Seksueel expliciete inhoud kan niet worden verzonden.", - "content_and_safety.ncmec_already_submitted": "Deze inhoud is al naar NCMEC verstuurd.", - "content_and_safety.ncmec_submission_failed": "We konden de melding niet indienen bij NCMEC. Probeer het later opnieuw.", "content_and_safety.nsfw_age_restricted": "Voor NSFW-inhoud geldt een leeftijdsgrens.", "discovery.already_applied": "Deze community heeft zich al aangemeld voor Ontdekken.", "discovery.application_already_reviewed": "Deze aanvraag voor Ontdekken is al beoordeeld.", @@ -381,7 +379,6 @@ const ERROR_I18N_NL_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Ongeldige DSA-verificatiecode.", "moderation_and_reports.invalid_report_flow_answers": "Deze antwoorden passen niet bij het meldingsformulier. Begin opnieuw met je melding.", "moderation_and_reports.ip_ban_declined": "Dit IP-adres kan niet aan de blokkeerlijst worden toegevoegd.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Alleen bijlagen met afbeeldingen of video’s kunnen bij NCMEC worden gemeld.", "moderation_and_reports.report_already_resolved": "De melding is al afgehandeld.", "moderation_and_reports.report_banned": "Je bent uitgesloten van het indienen van meldingen.", "moderation_and_reports.report_flow_outdated": "Het meldingsformulier is gewijzigd terwijl je het invulde. Begin opnieuw met je melding.", diff --git a/packages/errors/src/i18n/locales/no.ts b/packages/errors/src/i18n/locales/no.ts index 60ba99e45..5887e82dc 100644 --- a/packages/errors/src/i18n/locales/no.ts +++ b/packages/errors/src/i18n/locales/no.ts @@ -215,8 +215,6 @@ const ERROR_I18N_NO_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Tilkoblingsverifisering mislyktes. Sjekk at verifiseringstokenet er riktig plassert.", "content_and_safety.content_blocked": "Dette innholdet ble blokkert av sikkerhetssystemene.", "content_and_safety.explicit_content_cannot_be_sent": "Eksplisitt innhold kan ikke sendes.", - "content_and_safety.ncmec_already_submitted": "Dette innholdet er allerede rapportert til NCMEC.", - "content_and_safety.ncmec_submission_failed": "Klarte ikke å sende rapporten til NCMEC. Prøv igjen senere.", "content_and_safety.nsfw_age_restricted": "NSFW-innhold har aldersgrense.", "discovery.already_applied": "Dette fellesskapet har allerede søkt om å bli oppført i Oppdag.", "discovery.application_already_reviewed": "Denne søknaden om å bli oppført i Oppdag er allerede vurdert.", @@ -381,7 +379,6 @@ const ERROR_I18N_NO_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Ugyldig DSA-bekreftelseskode.", "moderation_and_reports.invalid_report_flow_answers": "Disse svarene stemmer ikke med rapportskjemaet. Start rapporten på nytt.", "moderation_and_reports.ip_ban_declined": "Denne IP-adressen kan ikke legges til i blokkeringslisten.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Bare bilde- eller videovedlegg kan rapporteres til NCMEC.", "moderation_and_reports.report_already_resolved": "Rapporten er allerede behandlet.", "moderation_and_reports.report_banned": "Du er utestengt fra å sende rapporter.", "moderation_and_reports.report_flow_outdated": "Rapportskjemaet ble endret mens du fylte det ut. Start rapporten på nytt.", diff --git a/packages/errors/src/i18n/locales/pl.ts b/packages/errors/src/i18n/locales/pl.ts index 2369cbcbc..36556eb4f 100644 --- a/packages/errors/src/i18n/locales/pl.ts +++ b/packages/errors/src/i18n/locales/pl.ts @@ -215,8 +215,6 @@ const ERROR_I18N_PL_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Nie udało się zweryfikować połączenia. Upewnij się, że token weryfikacyjny jest poprawnie umieszczony.", "content_and_safety.content_blocked": "Systemy bezpieczeństwa zablokowały tę treść.", "content_and_safety.explicit_content_cannot_be_sent": "Nie można wysyłać treści o jednoznacznie seksualnym charakterze.", - "content_and_safety.ncmec_already_submitted": "Tę treść już zgłoszono do NCMEC.", - "content_and_safety.ncmec_submission_failed": "Nie mogliśmy przesłać zgłoszenia do NCMEC. Spróbuj ponownie później.", "content_and_safety.nsfw_age_restricted": "Treści dla dorosłych są objęte ograniczeniem wiekowym.", "discovery.already_applied": "Ta społeczność została już zgłoszona do sekcji Odkrywanie.", "discovery.application_already_reviewed": "To zgłoszenie do sekcji Odkrywanie zostało już rozpatrzone.", @@ -381,7 +379,6 @@ const ERROR_I18N_PL_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Nieprawidłowy kod weryfikacyjny DSA.", "moderation_and_reports.invalid_report_flow_answers": "Te odpowiedzi nie pasują do formularza zgłoszenia. Rozpocznij zgłoszenie ponownie.", "moderation_and_reports.ip_ban_declined": "Ten adres IP nie może zostać dodany do listy blokowanych.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Do NCMEC można zgłaszać tylko załączniki w postaci obrazów lub filmów.", "moderation_and_reports.report_already_resolved": "Zgłoszenie zostało już rozpatrzone.", "moderation_and_reports.report_banned": "Zablokowano Ci możliwość przesyłania zgłoszeń.", "moderation_and_reports.report_flow_outdated": "Formularz zgłoszenia zmienił się podczas jego wypełniania. Rozpocznij zgłoszenie ponownie.", diff --git a/packages/errors/src/i18n/locales/pt-BR.ts b/packages/errors/src/i18n/locales/pt-BR.ts index 15263ea0d..90b36efb7 100644 --- a/packages/errors/src/i18n/locales/pt-BR.ts +++ b/packages/errors/src/i18n/locales/pt-BR.ts @@ -215,8 +215,6 @@ const ERROR_I18N_PT_BR_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "A verificação da conexão falhou. Certifique-se de que o token de verificação está corretamente posicionado.", "content_and_safety.content_blocked": "Este conteúdo foi bloqueado por sistemas de segurança.", "content_and_safety.explicit_content_cannot_be_sent": "Conteúdo explícito não pode ser enviado.", - "content_and_safety.ncmec_already_submitted": "Este conteúdo já foi enviado ao NCMEC.", - "content_and_safety.ncmec_submission_failed": "Não foi possível enviar a denúncia ao NCMEC. Tente novamente mais tarde.", "content_and_safety.nsfw_age_restricted": "Conteúdo NSFW tem restrição de idade.", "discovery.already_applied": "Esta comunidade já solicitou a inclusão em Descobrir.", "discovery.application_already_reviewed": "Esta solicitação de inclusão em Descobrir já foi analisada.", @@ -381,7 +379,6 @@ const ERROR_I18N_PT_BR_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Código de verificação do DSA inválido.", "moderation_and_reports.invalid_report_flow_answers": "Estas respostas não correspondem ao formulário de denúncia. Inicie a denúncia novamente.", "moderation_and_reports.ip_ban_declined": "Este endereço IP não pode ser adicionado à lista de bloqueio.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Apenas anexos de imagem ou vídeo podem ser denunciados ao NCMEC.", "moderation_and_reports.report_already_resolved": "A denúncia já foi resolvida.", "moderation_and_reports.report_banned": "Você foi impedido de enviar denúncias.", "moderation_and_reports.report_flow_outdated": "O formulário de denúncia mudou enquanto você o preenchia. Comece a denúncia novamente.", diff --git a/packages/errors/src/i18n/locales/ro.ts b/packages/errors/src/i18n/locales/ro.ts index 2ac1e0cbc..a7e2fc19f 100644 --- a/packages/errors/src/i18n/locales/ro.ts +++ b/packages/errors/src/i18n/locales/ro.ts @@ -215,8 +215,6 @@ const ERROR_I18N_RO_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Verificarea conexiunii a eșuat. Asigură-te că tokenul de verificare este plasat corect.", "content_and_safety.content_blocked": "Acest conținut a fost blocat de sistemele de siguranță.", "content_and_safety.explicit_content_cannot_be_sent": "Conținutul explicit nu poate fi trimis.", - "content_and_safety.ncmec_already_submitted": "Acest conținut a fost deja trimis la NCMEC.", - "content_and_safety.ncmec_submission_failed": "Nu am putut trimite raportul către NCMEC. Încearcă din nou mai târziu.", "content_and_safety.nsfw_age_restricted": "Conținutul NSFW este restricționat pe bază de vârstă.", "discovery.already_applied": "Această comunitate a trimis deja o cerere pentru Discovery.", "discovery.application_already_reviewed": "Această cerere pentru Discovery a fost deja analizată.", @@ -381,7 +379,6 @@ const ERROR_I18N_RO_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Cod de verificare DSA invalid.", "moderation_and_reports.invalid_report_flow_answers": "Aceste răspunsuri nu corespund formularului de raportare. Repornește raportarea.", "moderation_and_reports.ip_ban_declined": "Această adresă IP nu poate fi adăugată pe lista de blocare.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Doar atașamentele de tip imagine sau video pot fi raportate la NCMEC.", "moderation_and_reports.report_already_resolved": "Raportul a fost deja rezolvat.", "moderation_and_reports.report_banned": "Ți s-a interzis să trimiți rapoarte.", "moderation_and_reports.report_flow_outdated": "Formularul de raportare s-a modificat în timp ce îl completai. Repornește raportarea.", diff --git a/packages/errors/src/i18n/locales/ru.ts b/packages/errors/src/i18n/locales/ru.ts index d68d539ae..f63f708ec 100644 --- a/packages/errors/src/i18n/locales/ru.ts +++ b/packages/errors/src/i18n/locales/ru.ts @@ -215,8 +215,6 @@ const ERROR_I18N_RU_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Не удалось проверить подключение. Убедись, что токен подтверждения размещён правильно.", "content_and_safety.content_blocked": "Этот контент был заблокирован системами безопасности.", "content_and_safety.explicit_content_cannot_be_sent": "Неприемлемый контент нельзя отправить.", - "content_and_safety.ncmec_already_submitted": "Этот контент уже был отправлен в NCMEC.", - "content_and_safety.ncmec_submission_failed": "Не удалось отправить жалобу в NCMEC. Попробуй ещё раз позже.", "content_and_safety.nsfw_age_restricted": "Контент NSFW имеет возрастное ограничение.", "discovery.already_applied": "Это сообщество уже подало заявку на участие в каталоге.", "discovery.application_already_reviewed": "Эта заявка на добавление в каталог уже рассмотрена.", @@ -381,7 +379,6 @@ const ERROR_I18N_RU_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Неверный код подтверждения DSA.", "moderation_and_reports.invalid_report_flow_answers": "Эти ответы не соответствуют форме жалобы. Начни жалобу заново.", "moderation_and_reports.ip_ban_declined": "Этот IP-адрес нельзя добавить в чёрный список.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Пожаловаться в NCMEC можно только на вложения с изображением или видео.", "moderation_and_reports.report_already_resolved": "Жалоба уже рассмотрена.", "moderation_and_reports.report_banned": "Ты забанен и не можешь отправлять жалобы.", "moderation_and_reports.report_flow_outdated": "Форма жалобы изменилась во время заполнения. Начни жалобу заново.", diff --git a/packages/errors/src/i18n/locales/sv-SE.ts b/packages/errors/src/i18n/locales/sv-SE.ts index 80f98b297..83b698a7a 100644 --- a/packages/errors/src/i18n/locales/sv-SE.ts +++ b/packages/errors/src/i18n/locales/sv-SE.ts @@ -215,8 +215,6 @@ const ERROR_I18N_SV_SE_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Anslutningsverifieringen misslyckades. Kontrollera att verifieringstoken är korrekt placerad.", "content_and_safety.content_blocked": "Det här innehållet blockerades av säkerhetssystemen.", "content_and_safety.explicit_content_cannot_be_sent": "Sexuellt explicit innehåll kan inte skickas.", - "content_and_safety.ncmec_already_submitted": "Det här innehållet har redan skickats till NCMEC.", - "content_and_safety.ncmec_submission_failed": "Vi kunde inte skicka anmälan till NCMEC. Försök igen senare.", "content_and_safety.nsfw_age_restricted": "NSFW-innehåll är åldersbegränsat.", "discovery.already_applied": "Den här communityn har redan ansökt om att visas i Upptäck.", "discovery.application_already_reviewed": "Den här ansökan till Upptäck har redan granskats.", @@ -381,7 +379,6 @@ const ERROR_I18N_SV_SE_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Ogiltig DSA-verifieringskod.", "moderation_and_reports.invalid_report_flow_answers": "Svaren stämmer inte med anmälningsformuläret. Börja om med anmälan.", "moderation_and_reports.ip_ban_declined": "Den här IP-adressen kan inte läggas till i blocklistan.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Endast bild- eller videobilagor kan anmälas till NCMEC.", "moderation_and_reports.report_already_resolved": "Anmälan har redan behandlats.", "moderation_and_reports.report_banned": "Du har förbjudits att skicka in anmälningar.", "moderation_and_reports.report_flow_outdated": "Anmälningsformuläret ändrades medan du fyllde i det. Börja om med anmälan.", diff --git a/packages/errors/src/i18n/locales/th.ts b/packages/errors/src/i18n/locales/th.ts index 0da2eca7b..242682d2c 100644 --- a/packages/errors/src/i18n/locales/th.ts +++ b/packages/errors/src/i18n/locales/th.ts @@ -215,8 +215,6 @@ const ERROR_I18N_TH_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "การยืนยันการเชื่อมต่อล้มเหลว ตรวจสอบให้แน่ใจว่าวางโทเค็นการยืนยันไว้ถูกต้อง", "content_and_safety.content_blocked": "เนื้อหานี้ถูกบล็อกโดยระบบความปลอดภัย", "content_and_safety.explicit_content_cannot_be_sent": "ไม่สามารถส่งเนื้อหาที่ไม่เหมาะสมได้", - "content_and_safety.ncmec_already_submitted": "เนื้อหานี้ถูกส่งไปยัง NCMEC แล้ว", - "content_and_safety.ncmec_submission_failed": "ไม่สามารถส่งรายงานไปยัง NCMEC ได้ ลองอีกครั้งในภายหลัง", "content_and_safety.nsfw_age_restricted": "เนื้อหา NSFW มีข้อจำกัดด้านอายุ", "discovery.already_applied": "คอมมูนิตี้นี้สมัครเข้า Discovery แล้ว", "discovery.application_already_reviewed": "คำขอเข้า Discovery นี้ได้รับการพิจารณาแล้ว", @@ -381,7 +379,6 @@ const ERROR_I18N_TH_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "รหัสยืนยัน DSA ไม่ถูกต้อง", "moderation_and_reports.invalid_report_flow_answers": "คำตอบเหล่านี้ไม่ตรงกับแบบฟอร์มรายงาน โปรดเริ่มรายงานใหม่อีกครั้ง", "moderation_and_reports.ip_ban_declined": "ไม่สามารถเพิ่มที่อยู่ IP นี้ไปยังรายการบล็อกได้", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "เฉพาะไฟล์แนบที่เป็นรูปภาพหรือวิดีโอเท่านั้นที่สามารถรายงานไปยัง NCMEC ได้", "moderation_and_reports.report_already_resolved": "รายงานนี้ได้รับการดำเนินการแล้ว", "moderation_and_reports.report_banned": "คุณถูกแบนจากการส่งรายงาน", "moderation_and_reports.report_flow_outdated": "แบบฟอร์มรายงานมีการเปลี่ยนแปลงขณะที่คุณกำลังกรอกข้อมูลอยู่ โปรดเริ่มรายงานใหม่อีกครั้ง", diff --git a/packages/errors/src/i18n/locales/tr.ts b/packages/errors/src/i18n/locales/tr.ts index d27a46be9..50b249ed9 100644 --- a/packages/errors/src/i18n/locales/tr.ts +++ b/packages/errors/src/i18n/locales/tr.ts @@ -215,8 +215,6 @@ const ERROR_I18N_TR_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Bağlantı doğrulaması başarısız. Doğrulama belirtecinin doğru yerleştirildiğinden emin ol.", "content_and_safety.content_blocked": "Bu içerik güvenlik sistemleri tarafından engellendi.", "content_and_safety.explicit_content_cannot_be_sent": "Müstehcen içerik gönderilemez.", - "content_and_safety.ncmec_already_submitted": "Bu içerik zaten NCMEC'e gönderildi.", - "content_and_safety.ncmec_submission_failed": "Raporu NCMEC'e gönderemedik. Lütfen daha sonra tekrar dene.", "content_and_safety.nsfw_age_restricted": "NSFW içerik yaş sınırlı.", "discovery.already_applied": "Bu topluluk Keşfet'e zaten başvurdu.", "discovery.application_already_reviewed": "Bu Keşfet başvurusu zaten incelendi.", @@ -381,7 +379,6 @@ const ERROR_I18N_TR_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Geçersiz DSA doğrulama kodu.", "moderation_and_reports.invalid_report_flow_answers": "Bu yanıtlar rapor formuyla eşleşmiyor. Raporu yeniden başlat.", "moderation_and_reports.ip_ban_declined": "Bu IP adresi engelleme listesine eklenemez.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "NCMEC'e yalnızca resim veya video ekleri bildirilebilir.", "moderation_and_reports.report_already_resolved": "Rapor zaten çözüldü.", "moderation_and_reports.report_banned": "Rapor göndermen yasaklandı.", "moderation_and_reports.report_flow_outdated": "Rapor formu sen doldururken değişti. Raporu yeniden başlat.", diff --git a/packages/errors/src/i18n/locales/uk.ts b/packages/errors/src/i18n/locales/uk.ts index 62d09e810..615457778 100644 --- a/packages/errors/src/i18n/locales/uk.ts +++ b/packages/errors/src/i18n/locales/uk.ts @@ -215,8 +215,6 @@ const ERROR_I18N_UK_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Не вдалося підтвердити підключення. Переконайся, що токен підтвердження розміщено правильно.", "content_and_safety.content_blocked": "Цей вміст заблоковано системами безпеки.", "content_and_safety.explicit_content_cannot_be_sent": "Відвертий вміст не можна надсилати.", - "content_and_safety.ncmec_already_submitted": "Цей вміст уже надіслано до NCMEC.", - "content_and_safety.ncmec_submission_failed": "Не вдалося надіслати звіт до NCMEC. Спробуй пізніше.", "content_and_safety.nsfw_age_restricted": "Вміст NSFW має вікове обмеження.", "discovery.already_applied": "Ця спільнота вже подала заявку до Discovery.", "discovery.application_already_reviewed": "Цю заявку до Discovery вже розглянуто.", @@ -381,7 +379,6 @@ const ERROR_I18N_UK_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Недійсний код підтвердження DSA.", "moderation_and_reports.invalid_report_flow_answers": "Ці відповіді не відповідають формі звіту. Почни звіт заново.", "moderation_and_reports.ip_ban_declined": "Цю IP-адресу неможливо додати до чорного списку.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "До NCMEC можна надсилати звіти лише про вкладення із зображеннями або відео.", "moderation_and_reports.report_already_resolved": "Звіт вже розглянуто.", "moderation_and_reports.report_banned": "Тобі заборонено надсилати звіти.", "moderation_and_reports.report_flow_outdated": "Форма звіту змінилася під час заповнення. Почни звіт заново.", diff --git a/packages/errors/src/i18n/locales/vi.ts b/packages/errors/src/i18n/locales/vi.ts index dcf1be2ef..e40eb72bf 100644 --- a/packages/errors/src/i18n/locales/vi.ts +++ b/packages/errors/src/i18n/locales/vi.ts @@ -215,8 +215,6 @@ const ERROR_I18N_VI_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "Xác minh kết nối thất bại. Đảm bảo token xác minh được đặt đúng cách.", "content_and_safety.content_blocked": "Nội dung này đã bị chặn bởi hệ thống an toàn.", "content_and_safety.explicit_content_cannot_be_sent": "Không thể gửi nội dung nhạy cảm.", - "content_and_safety.ncmec_already_submitted": "Nội dung này đã được gửi đến NCMEC.", - "content_and_safety.ncmec_submission_failed": "Không thể gửi báo cáo đến NCMEC. Vui lòng thử lại sau.", "content_and_safety.nsfw_age_restricted": "Nội dung NSFW bị giới hạn độ tuổi.", "discovery.already_applied": "Cộng đồng này đã nộp đơn đăng ký Khám phá.", "discovery.application_already_reviewed": "Đơn đăng ký Khám phá này đã được xem xét.", @@ -381,7 +379,6 @@ const ERROR_I18N_VI_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "Mã xác minh DSA không hợp lệ.", "moderation_and_reports.invalid_report_flow_answers": "Những câu trả lời này không khớp với biểu mẫu báo cáo. Hãy bắt đầu lại báo cáo.", "moderation_and_reports.ip_ban_declined": "Không thể thêm địa chỉ IP này vào danh sách chặn.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Chỉ có thể báo cáo tệp đính kèm là hình ảnh hoặc video cho NCMEC.", "moderation_and_reports.report_already_resolved": "Báo cáo đã được giải quyết.", "moderation_and_reports.report_banned": "Bạn đã bị cấm gửi báo cáo.", "moderation_and_reports.report_flow_outdated": "Biểu mẫu báo cáo đã thay đổi trong khi bạn đang điền. Hãy bắt đầu lại báo cáo.", diff --git a/packages/errors/src/i18n/locales/zh-CN.ts b/packages/errors/src/i18n/locales/zh-CN.ts index f9de7011a..677019eb2 100644 --- a/packages/errors/src/i18n/locales/zh-CN.ts +++ b/packages/errors/src/i18n/locales/zh-CN.ts @@ -215,8 +215,6 @@ const ERROR_I18N_ZH_CN_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "连接验证失败。请确保验证令牌放置正确。", "content_and_safety.content_blocked": "此内容已被安全系统拦截。", "content_and_safety.explicit_content_cannot_be_sent": "无法发送露骨内容。", - "content_and_safety.ncmec_already_submitted": "此内容已提交至 NCMEC。", - "content_and_safety.ncmec_submission_failed": "无法向 NCMEC 提交举报。请稍后重试。", "content_and_safety.nsfw_age_restricted": "NSFW 内容有年龄限制。", "discovery.already_applied": "该社区已提交过社区发现申请。", "discovery.application_already_reviewed": "该社区发现申请已被审核。", @@ -381,7 +379,6 @@ const ERROR_I18N_ZH_CN_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "DSA 验证码无效。", "moderation_and_reports.invalid_report_flow_answers": "这些回答与举报表单不匹配。请重新开始举报。", "moderation_and_reports.ip_ban_declined": "此 IP 地址无法添加到黑名单。", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "仅图片或视频附件可以举报至 NCMEC。", "moderation_and_reports.report_already_resolved": "举报已解决。", "moderation_and_reports.report_banned": "你已被禁止提交举报。", "moderation_and_reports.report_flow_outdated": "举报表单在你填写过程中已更新。请重新开始举报。", diff --git a/packages/errors/src/i18n/locales/zh-TW.ts b/packages/errors/src/i18n/locales/zh-TW.ts index dba734ced..e6b9e2b0a 100644 --- a/packages/errors/src/i18n/locales/zh-TW.ts +++ b/packages/errors/src/i18n/locales/zh-TW.ts @@ -215,8 +215,6 @@ const ERROR_I18N_ZH_TW_MESSAGES = defineErrorI18nLocaleMessages({ "connections.verification_failed": "連線驗證失敗。請確認驗證權杖是否放置正確。", "content_and_safety.content_blocked": "此內容已被安全系統封鎖。", "content_and_safety.explicit_content_cannot_be_sent": "無法傳送露骨內容。", - "content_and_safety.ncmec_already_submitted": "此內容已提交至 NCMEC。", - "content_and_safety.ncmec_submission_failed": "我們無法將檢舉提交至 NCMEC。請稍後再試。", "content_and_safety.nsfw_age_restricted": "NSFW 內容設有年齡限制。", "discovery.already_applied": "此社群已申請探索功能。", "discovery.application_already_reviewed": "此探索申請已審核完成。", @@ -381,7 +379,6 @@ const ERROR_I18N_ZH_TW_MESSAGES = defineErrorI18nLocaleMessages({ "moderation_and_reports.invalid_dsa_verification_code": "無效的 DSA 驗證碼。", "moderation_and_reports.invalid_report_flow_answers": "這些回答與檢舉表單不符。請重新開始檢舉。", "moderation_and_reports.ip_ban_declined": "無法將此 IP 位址加入封鎖名單。", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "只有圖片或影片附件可向 NCMEC 檢舉。", "moderation_and_reports.report_already_resolved": "檢舉已解決。", "moderation_and_reports.report_banned": "你已被禁止送出檢舉。", "moderation_and_reports.report_flow_outdated": "檢舉表單在你填寫時已變更。請重新開始檢舉。", diff --git a/packages/errors/src/i18n/weblate/locales/ar.json b/packages/errors/src/i18n/weblate/locales/ar.json index 6f82aee16..c1c876f7c 100644 --- a/packages/errors/src/i18n/weblate/locales/ar.json +++ b/packages/errors/src/i18n/weblate/locales/ar.json @@ -211,8 +211,6 @@ "connections.verification_failed": "فشل التحقق من الاتصال. تأكد من وضع رمز التحقق في المكان الصحيح.", "content_and_safety.content_blocked": "حظرت أنظمة السلامة هذا المحتوى.", "content_and_safety.explicit_content_cannot_be_sent": "لا يمكن إرسال المحتوى الصريح.", - "content_and_safety.ncmec_already_submitted": "تم إرسال هذا المحتوى بالفعل إلى NCMEC.", - "content_and_safety.ncmec_submission_failed": "لم نتمكن من إرسال البلاغ إلى NCMEC. حاول مجددًا لاحقًا.", "content_and_safety.nsfw_age_restricted": "محتوى NSFW مقيّد بالعمر.", "discovery.already_applied": "لقد تقدم هذا المجتمع بالفعل بطلب الاكتشاف.", "discovery.application_already_reviewed": "تمت مراجعة طلب الاكتشاف هذا بالفعل.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "رمز تحقق DSA غير صالح.", "moderation_and_reports.invalid_report_flow_answers": "هذه الإجابات لا تتطابق مع نموذج الإبلاغ. ابدأ البلاغ من جديد.", "moderation_and_reports.ip_ban_declined": "لا يمكن إضافة عنوان IP هذا إلى قائمة الحظر.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "يمكن الإبلاغ إلى NCMEC عن مرفقات الصور أو الفيديو فقط.", "moderation_and_reports.report_already_resolved": "تم حل البلاغ بالفعل.", "moderation_and_reports.report_banned": "تم حظرك من إرسال البلاغات.", "moderation_and_reports.report_flow_outdated": "تغيّر نموذج الإبلاغ أثناء تعبئتك له. ابدأ البلاغ من جديد.", diff --git a/packages/errors/src/i18n/weblate/locales/bg.json b/packages/errors/src/i18n/weblate/locales/bg.json index c91041a9a..ee4640cb4 100644 --- a/packages/errors/src/i18n/weblate/locales/bg.json +++ b/packages/errors/src/i18n/weblate/locales/bg.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Проверката на връзката се провали. Увери се, че токенът за проверка е поставен правилно.", "content_and_safety.content_blocked": "Това съдържание беше блокирано от системите за безопасност.", "content_and_safety.explicit_content_cannot_be_sent": "Изрично съдържание не може да бъде изпратено.", - "content_and_safety.ncmec_already_submitted": "Това съдържание вече е изпратено до NCMEC.", - "content_and_safety.ncmec_submission_failed": "Не успяхме да изпратим сигнала до NCMEC. Опитай отново по-късно.", "content_and_safety.nsfw_age_restricted": "NSFW съдържанието е с възрастово ограничение.", "discovery.already_applied": "Тази общност вече е кандидатствала за Откриване.", "discovery.application_already_reviewed": "Това заявление за Откриване вече е разгледано.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Невалиден код за потвърждение на DSA.", "moderation_and_reports.invalid_report_flow_answers": "Тези отговори не съответстват на формуляра за сигнал. Започни сигнала отначало.", "moderation_and_reports.ip_ban_declined": "Този IP адрес не може да бъде добавен към списъка с блокирани адреси.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Само прикачени файлове с изображения или видеоклипове могат да се докладват на NCMEC.", "moderation_and_reports.report_already_resolved": "Сигналът вече е разгледан.", "moderation_and_reports.report_banned": "Имаш забрана да подаваш сигнали.", "moderation_and_reports.report_flow_outdated": "Формулярът за сигнал се промени, докато го попълваше. Започни сигнала отначало.", diff --git a/packages/errors/src/i18n/weblate/locales/cs.json b/packages/errors/src/i18n/weblate/locales/cs.json index 931ca6496..0d4cf2fce 100644 --- a/packages/errors/src/i18n/weblate/locales/cs.json +++ b/packages/errors/src/i18n/weblate/locales/cs.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Ověření propojení se nezdařilo. Zkontrolujte, zda je ověřovací token správně umístěn.", "content_and_safety.content_blocked": "Tento obsah byl zablokován bezpečnostními systémy.", "content_and_safety.explicit_content_cannot_be_sent": "Explicitní obsah nelze odeslat.", - "content_and_safety.ncmec_already_submitted": "Tento obsah již byl odeslán NCMEC.", - "content_and_safety.ncmec_submission_failed": "Nepodařilo se odeslat nahlášení organizaci NCMEC. Zkuste to znovu později.", "content_and_safety.nsfw_age_restricted": "Obsah NSFW je věkově omezený.", "discovery.already_applied": "Tato komunita již požádala o zařazení do Objevování.", "discovery.application_already_reviewed": "Tato žádost o zařazení do Objevování už byla zkontrolována.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Neplatný ověřovací kód DSA.", "moderation_and_reports.invalid_report_flow_answers": "Tyto odpovědi neodpovídají formuláři nahlášení. Začněte nahlášení znovu.", "moderation_and_reports.ip_ban_declined": "Tuto IP adresu nelze přidat na seznam blokovaných.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Organizaci NCMEC lze nahlásit pouze přílohy s obrázky nebo videi.", "moderation_and_reports.report_already_resolved": "Nahlášení již bylo vyřešeno.", "moderation_and_reports.report_banned": "Máte zakázáno odesílat nahlášení.", "moderation_and_reports.report_flow_outdated": "Formulář nahlášení se během jeho vyplňování změnil. Nahlášení začněte znovu.", diff --git a/packages/errors/src/i18n/weblate/locales/da.json b/packages/errors/src/i18n/weblate/locales/da.json index b1de7f61e..e626eb6cd 100644 --- a/packages/errors/src/i18n/weblate/locales/da.json +++ b/packages/errors/src/i18n/weblate/locales/da.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Verifikation af forbindelsen mislykkedes. Sørg for, at verifikationstokenet er placeret korrekt.", "content_and_safety.content_blocked": "Dette indhold blev blokeret af sikkerhedssystemer.", "content_and_safety.explicit_content_cannot_be_sent": "Eksplicit indhold kan ikke sendes.", - "content_and_safety.ncmec_already_submitted": "Dette indhold er allerede indsendt til NCMEC.", - "content_and_safety.ncmec_submission_failed": "Vi kunne ikke indsende anmeldelsen til NCMEC. Prøv igen senere.", "content_and_safety.nsfw_age_restricted": "NSFW-indhold er aldersbegrænset.", "discovery.already_applied": "Dette fællesskab har allerede ansøgt om at blive vist i Opdag.", "discovery.application_already_reviewed": "Denne ansøgning til Opdag er allerede blevet behandlet.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Ugyldig DSA-bekræftelseskode.", "moderation_and_reports.invalid_report_flow_answers": "Disse svar passer ikke til anmeldelsesformularen. Start anmeldelsen forfra.", "moderation_and_reports.ip_ban_declined": "Denne IP-adresse kan ikke føjes til bloklisten.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Kun vedhæftede billeder eller videoer kan anmeldes til NCMEC.", "moderation_and_reports.report_already_resolved": "Anmeldelsen er allerede afsluttet.", "moderation_and_reports.report_banned": "Du er blevet udelukket fra at indsende anmeldelser.", "moderation_and_reports.report_flow_outdated": "Anmeldelsesformularen blev ændret, mens du udfyldte den. Start anmeldelsen forfra.", diff --git a/packages/errors/src/i18n/weblate/locales/de.json b/packages/errors/src/i18n/weblate/locales/de.json index 6b5afd885..d5e248d40 100644 --- a/packages/errors/src/i18n/weblate/locales/de.json +++ b/packages/errors/src/i18n/weblate/locales/de.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Die Verbindungsüberprüfung ist fehlgeschlagen. Stelle sicher, dass das Verifizierungs-Token richtig platziert ist.", "content_and_safety.content_blocked": "Dieser Inhalt wurde von den Sicherheitssystemen blockiert.", "content_and_safety.explicit_content_cannot_be_sent": "Explizite Inhalte können nicht gesendet werden.", - "content_and_safety.ncmec_already_submitted": "Dieser Inhalt wurde bereits an NCMEC gemeldet.", - "content_and_safety.ncmec_submission_failed": "Wir konnten die Meldung nicht an NCMEC senden. Versuch es bitte später noch einmal.", "content_and_safety.nsfw_age_restricted": "NSFW-Inhalte sind altersbeschränkt.", "discovery.already_applied": "Diese Community hat bereits die Aufnahme unter „Entdecken“ beantragt.", "discovery.application_already_reviewed": "Dieser Antrag auf Aufnahme unter „Entdecken“ wurde bereits geprüft.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Ungültiger DSA-Bestätigungscode.", "moderation_and_reports.invalid_report_flow_answers": "Diese Antworten passen nicht zum Meldeformular. Starte die Meldung erneut.", "moderation_and_reports.ip_ban_declined": "Diese IP-Adresse kann nicht zur Blockierliste hinzugefügt werden.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Nur Bild- oder Video-Anhänge können bei NCMEC gemeldet werden.", "moderation_and_reports.report_already_resolved": "Meldung bereits erledigt.", "moderation_and_reports.report_banned": "Du bist für das Einreichen von Meldungen gesperrt.", "moderation_and_reports.report_flow_outdated": "Das Meldeformular hat sich geändert, während du es ausgefüllt hast. Starte die Meldung erneut.", diff --git a/packages/errors/src/i18n/weblate/locales/el.json b/packages/errors/src/i18n/weblate/locales/el.json index 31db67617..af91359cf 100644 --- a/packages/errors/src/i18n/weblate/locales/el.json +++ b/packages/errors/src/i18n/weblate/locales/el.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Η επαλήθευση σύνδεσης απέτυχε. Βεβαιώσου ότι το token επαλήθευσης έχει τοποθετηθεί σωστά.", "content_and_safety.content_blocked": "Αυτό το περιεχόμενο αποκλείστηκε από τα συστήματα ασφαλείας.", "content_and_safety.explicit_content_cannot_be_sent": "Το ακατάλληλο περιεχόμενο δεν μπορεί να σταλεί.", - "content_and_safety.ncmec_already_submitted": "Αυτό το περιεχόμενο έχει ήδη σταλεί στο NCMEC.", - "content_and_safety.ncmec_submission_failed": "Δεν μπορέσαμε να στείλουμε την αναφορά στο NCMEC. Δοκίμασε ξανά αργότερα.", "content_and_safety.nsfw_age_restricted": "Το περιεχόμενο NSFW έχει ηλικιακό περιορισμό.", "discovery.already_applied": "Αυτή η κοινότητα έχει ήδη υποβάλει αίτηση για την Ανακάλυψη.", "discovery.application_already_reviewed": "Αυτή η αίτηση για την Ανακάλυψη έχει ήδη εξεταστεί.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Μη έγκυρος κωδικός επαλήθευσης DSA.", "moderation_and_reports.invalid_report_flow_answers": "Αυτές οι απαντήσεις δεν ταιριάζουν με τη φόρμα αναφοράς. Ξεκίνα ξανά την αναφορά.", "moderation_and_reports.ip_ban_declined": "Αυτή η διεύθυνση IP δεν μπορεί να προστεθεί στη λίστα αποκλεισμού.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Στο NCMEC μπορούν να αναφερθούν μόνο συνημμένα εικόνας ή βίντεο.", "moderation_and_reports.report_already_resolved": "Η αναφορά έχει ήδη διευθετηθεί.", "moderation_and_reports.report_banned": "Έχεις αποκλειστεί από την υποβολή αναφορών.", "moderation_and_reports.report_flow_outdated": "Η φόρμα αναφοράς άλλαξε ενώ τη συμπλήρωνες. Ξεκίνα ξανά την αναφορά.", diff --git a/packages/errors/src/i18n/weblate/locales/en-GB.json b/packages/errors/src/i18n/weblate/locales/en-GB.json index 4c9d1d26c..08f1dd794 100644 --- a/packages/errors/src/i18n/weblate/locales/en-GB.json +++ b/packages/errors/src/i18n/weblate/locales/en-GB.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Connection verification failed. Make sure the verification token is correctly placed.", "content_and_safety.content_blocked": "This content was blocked by safety systems.", "content_and_safety.explicit_content_cannot_be_sent": "Explicit content can't be sent.", - "content_and_safety.ncmec_already_submitted": "This content has already been submitted to NCMEC.", - "content_and_safety.ncmec_submission_failed": "We couldn't submit the report to NCMEC. Please try again later.", "content_and_safety.nsfw_age_restricted": "NSFW content is age restricted.", "discovery.already_applied": "This community has already applied for Discovery.", "discovery.application_already_reviewed": "This Discovery application has already been reviewed.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Invalid DSA verification code.", "moderation_and_reports.invalid_report_flow_answers": "These answers don't match the report form. Start the report again.", "moderation_and_reports.ip_ban_declined": "This IP address cannot be added to the blocklist.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Only image or video attachments can be reported to NCMEC.", "moderation_and_reports.report_already_resolved": "Report already resolved.", "moderation_and_reports.report_banned": "You've been banned from submitting reports.", "moderation_and_reports.report_flow_outdated": "The report form changed while you were filling it in. Start the report again.", diff --git a/packages/errors/src/i18n/weblate/locales/es-419.json b/packages/errors/src/i18n/weblate/locales/es-419.json index 4a2c25f72..a6dc20951 100644 --- a/packages/errors/src/i18n/weblate/locales/es-419.json +++ b/packages/errors/src/i18n/weblate/locales/es-419.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Falló la verificación de la conexión. Asegúrate de que el token de verificación esté colocado correctamente.", "content_and_safety.content_blocked": "Este contenido fue bloqueado por sistemas de seguridad.", "content_and_safety.explicit_content_cannot_be_sent": "No se puede enviar contenido explícito.", - "content_and_safety.ncmec_already_submitted": "Este contenido ya fue remitido a NCMEC.", - "content_and_safety.ncmec_submission_failed": "No se pudo enviar el reporte a NCMEC. Inténtalo de nuevo más tarde.", "content_and_safety.nsfw_age_restricted": "El contenido NSFW tiene restricción de edad.", "discovery.already_applied": "Esta comunidad ya ha solicitado aparecer en Descubrir.", "discovery.application_already_reviewed": "Esta solicitud para aparecer en Descubrir ya ha sido revisada.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Código de verificación DSA no válido.", "moderation_and_reports.invalid_report_flow_answers": "Estas respuestas no coinciden con el formulario de reporte. Vuelve a iniciar el reporte.", "moderation_and_reports.ip_ban_declined": "Esta dirección IP no se puede agregar a la lista de bloqueo.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Solo se pueden reportar a NCMEC archivos adjuntos de imagen o video.", "moderation_and_reports.report_already_resolved": "El reporte ya se resolvió.", "moderation_and_reports.report_banned": "Se te ha prohibido enviar reportes.", "moderation_and_reports.report_flow_outdated": "El formulario de reporte cambió mientras lo completabas. Vuelve a iniciar el reporte.", diff --git a/packages/errors/src/i18n/weblate/locales/es-ES.json b/packages/errors/src/i18n/weblate/locales/es-ES.json index bb7075fd5..3da035a1c 100644 --- a/packages/errors/src/i18n/weblate/locales/es-ES.json +++ b/packages/errors/src/i18n/weblate/locales/es-ES.json @@ -211,8 +211,6 @@ "connections.verification_failed": "La verificación de la conexión ha fallado. Asegúrate de que el token de verificación esté colocado correctamente.", "content_and_safety.content_blocked": "Este contenido ha sido bloqueado por los sistemas de seguridad.", "content_and_safety.explicit_content_cannot_be_sent": "No se puede enviar contenido explícito.", - "content_and_safety.ncmec_already_submitted": "Este contenido ya ha sido enviado a NCMEC.", - "content_and_safety.ncmec_submission_failed": "No hemos podido enviar la denuncia a NCMEC. Inténtalo de nuevo más tarde.", "content_and_safety.nsfw_age_restricted": "El contenido NSFW tiene restricción de edad.", "discovery.already_applied": "Esta comunidad ya ha solicitado aparecer en Descubrir.", "discovery.application_already_reviewed": "Esta solicitud para aparecer en Descubrir ya ha sido revisada.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Código de verificación DSA no válido.", "moderation_and_reports.invalid_report_flow_answers": "Estas respuestas no coinciden con el formulario de denuncia. Vuelve a empezar la denuncia.", "moderation_and_reports.ip_ban_declined": "Esta dirección IP no se puede añadir a la lista de bloqueo.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Solo se pueden denunciar adjuntos de imagen o vídeo a NCMEC.", "moderation_and_reports.report_already_resolved": "La denuncia ya se ha resuelto.", "moderation_and_reports.report_banned": "Se te ha prohibido enviar denuncias.", "moderation_and_reports.report_flow_outdated": "El formulario de denuncia ha cambiado mientras lo rellenabas. Vuelve a empezar la denuncia.", diff --git a/packages/errors/src/i18n/weblate/locales/fi.json b/packages/errors/src/i18n/weblate/locales/fi.json index e92547bb6..1a39a1950 100644 --- a/packages/errors/src/i18n/weblate/locales/fi.json +++ b/packages/errors/src/i18n/weblate/locales/fi.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Yhteyden vahvistus epäonnistui. Varmista, että vahvistustunnus on sijoitettu oikeaan paikkaan.", "content_and_safety.content_blocked": "Turvajärjestelmät estivät tämän sisällön.", "content_and_safety.explicit_content_cannot_be_sent": "Seksuaalisesti eksplisiittistä sisältöä ei voi lähettää.", - "content_and_safety.ncmec_already_submitted": "Tämä sisältö on jo toimitettu NCMEC:lle.", - "content_and_safety.ncmec_submission_failed": "Ilmoituksen lähettäminen NCMEC:lle epäonnistui. Yritä uudelleen myöhemmin.", "content_and_safety.nsfw_age_restricted": "NSFW-sisällöllä on ikärajoitus.", "discovery.already_applied": "Tämä yhteisö on jo hakenut Tutustu-näkymään.", "discovery.application_already_reviewed": "Tämä hakemus Tutustu-näkymään on jo käsitelty.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Virheellinen DSA-vahvistuskoodi.", "moderation_and_reports.invalid_report_flow_answers": "Nämä vastaukset eivät vastaa ilmoituslomaketta. Aloita ilmoitus uudelleen.", "moderation_and_reports.ip_ban_declined": "Tätä IP-osoitetta ei voi lisätä estolistalle.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "NCMEC:lle voi ilmoittaa vain kuva- tai videoliitteistä.", "moderation_and_reports.report_already_resolved": "Ilmoitus on jo ratkaistu.", "moderation_and_reports.report_banned": "Sinut on estetty lähettämästä ilmoituksia.", "moderation_and_reports.report_flow_outdated": "Ilmoituslomake muuttui, kun täytit sitä. Aloita ilmoitus uudelleen.", diff --git a/packages/errors/src/i18n/weblate/locales/fr.json b/packages/errors/src/i18n/weblate/locales/fr.json index 1f1449ca8..cda4b5401 100644 --- a/packages/errors/src/i18n/weblate/locales/fr.json +++ b/packages/errors/src/i18n/weblate/locales/fr.json @@ -211,8 +211,6 @@ "connections.verification_failed": "La vérification de la connexion a échoué. Vérifiez que le jeton de vérification est placé correctement.", "content_and_safety.content_blocked": "Ce contenu a été bloqué par les systèmes de sécurité.", "content_and_safety.explicit_content_cannot_be_sent": "Le contenu explicite ne peut pas être envoyé.", - "content_and_safety.ncmec_already_submitted": "Ce contenu a déjà été soumis à NCMEC.", - "content_and_safety.ncmec_submission_failed": "Impossible d’envoyer le signalement au NCMEC. Réessayez plus tard.", "content_and_safety.nsfw_age_restricted": "Le contenu NSFW est soumis à une restriction d'âge.", "discovery.already_applied": "Cette communauté a déjà demandé à figurer dans Découverte.", "discovery.application_already_reviewed": "Cette demande d’inscription dans Découverte a déjà été examinée.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Code de vérification DSA invalide.", "moderation_and_reports.invalid_report_flow_answers": "Ces réponses ne correspondent pas au formulaire de signalement. Veuillez recommencer le signalement.", "moderation_and_reports.ip_ban_declined": "Cette adresse IP ne peut pas être ajoutée à la liste de blocage.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Seules les pièces jointes de type image ou vidéo peuvent être signalées au NCMEC.", "moderation_and_reports.report_already_resolved": "Signalement déjà résolu.", "moderation_and_reports.report_banned": "Vous n’êtes plus autorisé à envoyer des signalements.", "moderation_and_reports.report_flow_outdated": "Le formulaire de signalement a changé pendant que vous le remplissiez. Veuillez recommencer le signalement.", diff --git a/packages/errors/src/i18n/weblate/locales/he.json b/packages/errors/src/i18n/weblate/locales/he.json index ccad744db..132602769 100644 --- a/packages/errors/src/i18n/weblate/locales/he.json +++ b/packages/errors/src/i18n/weblate/locales/he.json @@ -211,8 +211,6 @@ "connections.verification_failed": "אימות החיבור נכשל. נא לוודא שאסימון האימות ממוקם נכון.", "content_and_safety.content_blocked": "תוכן זה נחסם על ידי מערכות הבטיחות.", "content_and_safety.explicit_content_cannot_be_sent": "לא ניתן לשלוח תוכן מפורש.", - "content_and_safety.ncmec_already_submitted": "תוכן זה כבר הוגש ל-NCMEC.", - "content_and_safety.ncmec_submission_failed": "לא הצלחנו להגיש את הדיווח ל-NCMEC. ניתן לנסות שוב מאוחר יותר.", "content_and_safety.nsfw_age_restricted": "תוכן NSFW מוגבל בגיל.", "discovery.already_applied": "קהילה זו כבר הגישה בקשה לגילוי.", "discovery.application_already_reviewed": "בקשת הגילוי הזו כבר נבדקה.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "קוד אימות DSA שגוי.", "moderation_and_reports.invalid_report_flow_answers": "התשובות האלה לא תואמות לטופס הדיווח. יש להתחיל את הדיווח מחדש.", "moderation_and_reports.ip_ban_declined": "לא ניתן להוסיף כתובת IP זו לרשימת החסימה.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "ניתן לדווח ל-NCMEC רק על קבצים מצורפים שהם תמונות או סרטונים.", "moderation_and_reports.report_already_resolved": "הדיווח כבר טופל.", "moderation_and_reports.report_banned": "נחסמת מלהגיש דיווחים.", "moderation_and_reports.report_flow_outdated": "טופס הדיווח השתנה במהלך המילוי. יש להתחיל את הדיווח מחדש.", diff --git a/packages/errors/src/i18n/weblate/locales/hi.json b/packages/errors/src/i18n/weblate/locales/hi.json index 574b0f6be..9a8ce1c7f 100644 --- a/packages/errors/src/i18n/weblate/locales/hi.json +++ b/packages/errors/src/i18n/weblate/locales/hi.json @@ -211,8 +211,6 @@ "connections.verification_failed": "कनेक्शन वेरिफ़िकेशन फेल हो गया। पक्का करें कि वेरिफ़िकेशन टोकन सही जगह पर रखा गया है।", "content_and_safety.content_blocked": "इस कंटेंट को सेफ्टी सिस्टम ने ब्लॉक कर दिया है।", "content_and_safety.explicit_content_cannot_be_sent": "एक्सप्लिसिट कंटेंट नहीं भेजा जा सकता।", - "content_and_safety.ncmec_already_submitted": "यह कंटेंट पहले ही NCMEC को भेजा जा चुका है।", - "content_and_safety.ncmec_submission_failed": "हम NCMEC को रिपोर्ट भेज नहीं पाए। कृपया बाद में फिर से कोशिश करें।", "content_and_safety.nsfw_age_restricted": "NSFW कंटेंट उम्र के हिसाब से प्रतिबंधित है।", "discovery.already_applied": "इस कम्युनिटी ने पहले ही डिस्कवरी के लिए अप्लाई कर दिया है।", "discovery.application_already_reviewed": "इस डिस्कवरी आवेदन की पहले ही समीक्षा हो चुकी है।", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "अमान्य DSA वेरिफ़िकेशन कोड।", "moderation_and_reports.invalid_report_flow_answers": "ये जवाब रिपोर्ट फ़ॉर्म से मेल नहीं खाते। रिपोर्ट को फिर से शुरू करें।", "moderation_and_reports.ip_ban_declined": "इस IP पते को ब्लॉकलिस्ट में नहीं जोड़ा जा सकता।", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "सिर्फ़ इमेज या वीडियो अटैचमेंट ही NCMEC को रिपोर्ट किए जा सकते हैं।", "moderation_and_reports.report_already_resolved": "रिपोर्ट पहले ही हल हो चुकी है।", "moderation_and_reports.report_banned": "आपको रिपोर्ट सबमिट करने से बैन कर दिया गया है।", "moderation_and_reports.report_flow_outdated": "रिपोर्ट फ़ॉर्म भरते समय बदल गया था। कृपया रिपोर्ट दोबारा शुरू करें।", diff --git a/packages/errors/src/i18n/weblate/locales/hr.json b/packages/errors/src/i18n/weblate/locales/hr.json index 3a67e8501..16e6ac934 100644 --- a/packages/errors/src/i18n/weblate/locales/hr.json +++ b/packages/errors/src/i18n/weblate/locales/hr.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Provjera veze nije uspjela. Uvjeri se da je token za provjeru ispravno postavljen.", "content_and_safety.content_blocked": "Sigurnosni sustavi blokirali su ovaj sadržaj.", "content_and_safety.explicit_content_cannot_be_sent": "Eksplicitni sadržaj ne može se poslati.", - "content_and_safety.ncmec_already_submitted": "Ovaj sadržaj je već poslan NCMEC-u.", - "content_and_safety.ncmec_submission_failed": "Nismo mogli poslati izvješće NCMEC-u. Pokušaj ponovno kasnije.", "content_and_safety.nsfw_age_restricted": "NSFW sadržaj je dobno ograničen.", "discovery.already_applied": "Ova zajednica već je podnijela prijavu za otkrivanje.", "discovery.application_already_reviewed": "Ova prijava za otkrivanje već je pregledana.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Nevažeći DSA kod za provjeru.", "moderation_and_reports.invalid_report_flow_answers": "Ovi odgovori ne odgovaraju obrascu prijave. Započni prijavu ponovno.", "moderation_and_reports.ip_ban_declined": "Ovu IP adresu nije moguće dodati na popis blokiranih.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "NCMEC-u se mogu prijaviti samo slikovni ili video privitci.", "moderation_and_reports.report_already_resolved": "Prijava je već riješena.", "moderation_and_reports.report_banned": "Zabranjeno ti je podnositi prijave.", "moderation_and_reports.report_flow_outdated": "Obrazac prijave promijenio se tijekom ispunjavanja. Započni prijavu ponovno.", diff --git a/packages/errors/src/i18n/weblate/locales/hu.json b/packages/errors/src/i18n/weblate/locales/hu.json index 4b6a17bba..2caacabb6 100644 --- a/packages/errors/src/i18n/weblate/locales/hu.json +++ b/packages/errors/src/i18n/weblate/locales/hu.json @@ -211,8 +211,6 @@ "connections.verification_failed": "A kapcsolat ellenőrzése sikertelen. Ellenőrizd, hogy az ellenőrző token a helyén van.", "content_and_safety.content_blocked": "Ezt a tartalmat a biztonsági rendszerek blokkolták.", "content_and_safety.explicit_content_cannot_be_sent": "Explicit tartalom nem küldhető.", - "content_and_safety.ncmec_already_submitted": "Ezt a tartalmat már beküldtük az NCMEC-nek.", - "content_and_safety.ncmec_submission_failed": "Nem sikerült beküldeni a bejelentést az NCMEC-nek. Próbáld meg újra később.", "content_and_safety.nsfw_age_restricted": "Az NSFW-tartalom korhatáros.", "discovery.already_applied": "Ez a közösség már jelentkezett a Felfedezésbe.", "discovery.application_already_reviewed": "Ezt a Felfedezésbe való jelentkezést már elbírálták.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Érvénytelen DSA-ellenőrzőkód.", "moderation_and_reports.invalid_report_flow_answers": "Ezek a válaszok nem egyeznek a bejelentési űrlappal. Kezdd elölről a bejelentést.", "moderation_and_reports.ip_ban_declined": "Ez az IP-cím nem adható hozzá a tiltólistához.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Csak kép- vagy videómellékleteket lehet jelenteni az NCMEC-nek.", "moderation_and_reports.report_already_resolved": "A bejelentést már lezárták.", "moderation_and_reports.report_banned": "Eltiltottak a bejelentések küldésétől.", "moderation_and_reports.report_flow_outdated": "A bejelentési űrlap megváltozott, amíg kitöltötted. Kezdd elölről a bejelentést.", diff --git a/packages/errors/src/i18n/weblate/locales/id.json b/packages/errors/src/i18n/weblate/locales/id.json index de3ce10fb..ab415bf04 100644 --- a/packages/errors/src/i18n/weblate/locales/id.json +++ b/packages/errors/src/i18n/weblate/locales/id.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Verifikasi koneksi gagal. Pastikan token verifikasi ditempatkan dengan benar.", "content_and_safety.content_blocked": "Konten ini diblokir oleh sistem keamanan.", "content_and_safety.explicit_content_cannot_be_sent": "Konten eksplisit tidak bisa dikirim.", - "content_and_safety.ncmec_already_submitted": "Konten ini sudah dilaporkan ke NCMEC.", - "content_and_safety.ncmec_submission_failed": "Laporan tidak bisa dikirim ke NCMEC. Coba lagi nanti.", "content_and_safety.nsfw_age_restricted": "Konten NSFW dibatasi usia.", "discovery.already_applied": "Komunitas ini sudah mendaftar ke Discovery.", "discovery.application_already_reviewed": "Pengajuan Discovery ini sudah ditinjau.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Kode verifikasi DSA tidak valid.", "moderation_and_reports.invalid_report_flow_answers": "Jawaban ini tidak sesuai dengan formulir laporan. Mulai ulang laporanmu.", "moderation_and_reports.ip_ban_declined": "Alamat IP ini tidak dapat ditambahkan ke daftar blokir.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Hanya lampiran berupa gambar atau video yang dapat dilaporkan ke NCMEC.", "moderation_and_reports.report_already_resolved": "Laporan sudah ditangani.", "moderation_and_reports.report_banned": "Kamu dilarang mengirim laporan.", "moderation_and_reports.report_flow_outdated": "Formulir laporan berubah saat kamu mengisinya. Mulai ulang laporanmu.", diff --git a/packages/errors/src/i18n/weblate/locales/it.json b/packages/errors/src/i18n/weblate/locales/it.json index a5ddef5fe..600aaf668 100644 --- a/packages/errors/src/i18n/weblate/locales/it.json +++ b/packages/errors/src/i18n/weblate/locales/it.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Verifica della connessione fallita. Assicurati che il token di verifica sia posizionato correttamente.", "content_and_safety.content_blocked": "Questo contenuto è stato bloccato dai sistemi di sicurezza.", "content_and_safety.explicit_content_cannot_be_sent": "Non è possibile inviare contenuti sessualmente espliciti.", - "content_and_safety.ncmec_already_submitted": "Questo contenuto è già stato inviato a NCMEC.", - "content_and_safety.ncmec_submission_failed": "Impossibile inviare la segnalazione a NCMEC. Riprova più tardi.", "content_and_safety.nsfw_age_restricted": "Il contenuto NSFW ha restrizioni di età.", "discovery.already_applied": "Questa community ha già inviato una candidatura per Scopri.", "discovery.application_already_reviewed": "Questa candidatura per Scopri è già stata esaminata.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Codice di verifica DSA non valido.", "moderation_and_reports.invalid_report_flow_answers": "Queste risposte non corrispondono al modulo di segnalazione. Ricomincia la segnalazione.", "moderation_and_reports.ip_ban_declined": "Questo indirizzo IP non può essere aggiunto all'elenco di blocco.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Solo allegati di immagini o video possono essere segnalati a NCMEC.", "moderation_and_reports.report_already_resolved": "La segnalazione è già stata risolta.", "moderation_and_reports.report_banned": "Non hai più il permesso di inviare segnalazioni.", "moderation_and_reports.report_flow_outdated": "Il modulo di segnalazione è cambiato mentre lo stavi compilando. Ricomincia la segnalazione.", diff --git a/packages/errors/src/i18n/weblate/locales/ja.json b/packages/errors/src/i18n/weblate/locales/ja.json index 1b2ca90f1..e9e72c9cd 100644 --- a/packages/errors/src/i18n/weblate/locales/ja.json +++ b/packages/errors/src/i18n/weblate/locales/ja.json @@ -211,8 +211,6 @@ "connections.verification_failed": "接続の検証に失敗しました。検証トークンが正しく配置されているかご確認ください。", "content_and_safety.content_blocked": "このコンテンツは安全システムによってブロックされました。", "content_and_safety.explicit_content_cannot_be_sent": "不適切なコンテンツは送信できません。", - "content_and_safety.ncmec_already_submitted": "このコンテンツはすでにNCMECに提出されています。", - "content_and_safety.ncmec_submission_failed": "NCMECへの報告を送信できませんでした。後でもう一度お試しください。", "content_and_safety.nsfw_age_restricted": "NSFWコンテンツには年齢制限があります。", "discovery.already_applied": "このコミュニティはすでに「見つける」への掲載を申請しています。", "discovery.application_already_reviewed": "「見つける」への掲載申請はすでに審査済みです。", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "DSA確認コードが無効です。", "moderation_and_reports.invalid_report_flow_answers": "これらの回答は報告フォームと一致しません。最初から報告し直してください。", "moderation_and_reports.ip_ban_declined": "このIPアドレスはブロックリストに追加できません。", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "NCMECに報告できるのは画像または動画の添付ファイルのみです。", "moderation_and_reports.report_already_resolved": "報告はすでに解決済みです。", "moderation_and_reports.report_banned": "報告の送信は禁止されています。", "moderation_and_reports.report_flow_outdated": "入力中に報告フォームが変更されました。最初から報告し直してください。", diff --git a/packages/errors/src/i18n/weblate/locales/ko.json b/packages/errors/src/i18n/weblate/locales/ko.json index 50eaa4d11..ca236432f 100644 --- a/packages/errors/src/i18n/weblate/locales/ko.json +++ b/packages/errors/src/i18n/weblate/locales/ko.json @@ -211,8 +211,6 @@ "connections.verification_failed": "연결 인증에 실패했어요. 인증 토큰이 올바른 위치에 있는지 확인해 주세요.", "content_and_safety.content_blocked": "안전 시스템이 이 콘텐츠를 차단했어요.", "content_and_safety.explicit_content_cannot_be_sent": "선정적인 콘텐츠는 보낼 수 없어요.", - "content_and_safety.ncmec_already_submitted": "이 콘텐츠는 이미 NCMEC에 제출된 상태예요.", - "content_and_safety.ncmec_submission_failed": "NCMEC에 신고를 제출하지 못했어요. 나중에 다시 시도해 주세요.", "content_and_safety.nsfw_age_restricted": "NSFW 콘텐츠에는 연령 제한이 적용돼요.", "discovery.already_applied": "이 커뮤니티는 이미 디스커버리에 신청했어요.", "discovery.application_already_reviewed": "이 디스커버리 신청은 이미 검토되었어요.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "DSA 인증 코드가 올바르지 않아요.", "moderation_and_reports.invalid_report_flow_answers": "이 답변은 신고 양식과 맞지 않아요. 신고를 다시 시작해 주세요.", "moderation_and_reports.ip_ban_declined": "이 IP 주소는 차단 목록에 추가할 수 없어요.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "이미지 또는 동영상 첨부 파일만 NCMEC에 신고할 수 있어요.", "moderation_and_reports.report_already_resolved": "이미 해결된 신고예요.", "moderation_and_reports.report_banned": "신고 제출이 금지되었어요.", "moderation_and_reports.report_flow_outdated": "작성하는 동안 신고 양식이 변경되었어요. 신고를 다시 시작해 주세요.", diff --git a/packages/errors/src/i18n/weblate/locales/lt.json b/packages/errors/src/i18n/weblate/locales/lt.json index e4a1d18e7..4e79205b0 100644 --- a/packages/errors/src/i18n/weblate/locales/lt.json +++ b/packages/errors/src/i18n/weblate/locales/lt.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Ryšio patvirtinimas nepavyko. Įsitikink, kad patvirtinimo žetonas įdėtas teisingai.", "content_and_safety.content_blocked": "Šį turinį užblokavo saugumo sistemos.", "content_and_safety.explicit_content_cannot_be_sent": "Nepadorus turinys negali būti siunčiamas.", - "content_and_safety.ncmec_already_submitted": "Šis turinys jau išsiųstas NCMEC.", - "content_and_safety.ncmec_submission_failed": "Nepavyko išsiųsti pranešimo NCMEC. Pabandyk dar kartą vėliau.", "content_and_safety.nsfw_age_restricted": "NSFW turiniui taikomas amžiaus ribojimas.", "discovery.already_applied": "Ši bendruomenė jau pateikė Atradimo paraišką.", "discovery.application_already_reviewed": "Ši Atradimo paraiška jau peržiūrėta.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Neteisingas DSA patvirtinimo kodas.", "moderation_and_reports.invalid_report_flow_answers": "Šie atsakymai neatitinka pranešimo formos. Pradėk pranešimą iš naujo.", "moderation_and_reports.ip_ban_declined": "Šio IP adreso negalima įtraukti į blokavimo sąrašą.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "NCMEC galima pranešti tik apie paveikslėlių ar vaizdo įrašų priedus.", "moderation_and_reports.report_already_resolved": "Pranešimas jau išnagrinėtas.", "moderation_and_reports.report_banned": "Tau uždrausta teikti pranešimus.", "moderation_and_reports.report_flow_outdated": "Pranešimo forma pasikeitė, kol ją pildei. Pradėk pranešimą iš naujo.", diff --git a/packages/errors/src/i18n/weblate/locales/nl.json b/packages/errors/src/i18n/weblate/locales/nl.json index 63b205ac7..86d0af181 100644 --- a/packages/errors/src/i18n/weblate/locales/nl.json +++ b/packages/errors/src/i18n/weblate/locales/nl.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Verificatie van de verbinding mislukt. Controleer of het verificatietoken juist is geplaatst.", "content_and_safety.content_blocked": "Deze inhoud is geblokkeerd door veiligheidssystemen.", "content_and_safety.explicit_content_cannot_be_sent": "Seksueel expliciete inhoud kan niet worden verzonden.", - "content_and_safety.ncmec_already_submitted": "Deze inhoud is al naar NCMEC verstuurd.", - "content_and_safety.ncmec_submission_failed": "We konden de melding niet indienen bij NCMEC. Probeer het later opnieuw.", "content_and_safety.nsfw_age_restricted": "Voor NSFW-inhoud geldt een leeftijdsgrens.", "discovery.already_applied": "Deze community heeft zich al aangemeld voor Ontdekken.", "discovery.application_already_reviewed": "Deze aanvraag voor Ontdekken is al beoordeeld.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Ongeldige DSA-verificatiecode.", "moderation_and_reports.invalid_report_flow_answers": "Deze antwoorden passen niet bij het meldingsformulier. Begin opnieuw met je melding.", "moderation_and_reports.ip_ban_declined": "Dit IP-adres kan niet aan de blokkeerlijst worden toegevoegd.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Alleen bijlagen met afbeeldingen of video’s kunnen bij NCMEC worden gemeld.", "moderation_and_reports.report_already_resolved": "De melding is al afgehandeld.", "moderation_and_reports.report_banned": "Je bent uitgesloten van het indienen van meldingen.", "moderation_and_reports.report_flow_outdated": "Het meldingsformulier is gewijzigd terwijl je het invulde. Begin opnieuw met je melding.", diff --git a/packages/errors/src/i18n/weblate/locales/no.json b/packages/errors/src/i18n/weblate/locales/no.json index 0e267bc89..498105be7 100644 --- a/packages/errors/src/i18n/weblate/locales/no.json +++ b/packages/errors/src/i18n/weblate/locales/no.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Tilkoblingsverifisering mislyktes. Sjekk at verifiseringstokenet er riktig plassert.", "content_and_safety.content_blocked": "Dette innholdet ble blokkert av sikkerhetssystemene.", "content_and_safety.explicit_content_cannot_be_sent": "Eksplisitt innhold kan ikke sendes.", - "content_and_safety.ncmec_already_submitted": "Dette innholdet er allerede rapportert til NCMEC.", - "content_and_safety.ncmec_submission_failed": "Klarte ikke å sende rapporten til NCMEC. Prøv igjen senere.", "content_and_safety.nsfw_age_restricted": "NSFW-innhold har aldersgrense.", "discovery.already_applied": "Dette fellesskapet har allerede søkt om å bli oppført i Oppdag.", "discovery.application_already_reviewed": "Denne søknaden om å bli oppført i Oppdag er allerede vurdert.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Ugyldig DSA-bekreftelseskode.", "moderation_and_reports.invalid_report_flow_answers": "Disse svarene stemmer ikke med rapportskjemaet. Start rapporten på nytt.", "moderation_and_reports.ip_ban_declined": "Denne IP-adressen kan ikke legges til i blokkeringslisten.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Bare bilde- eller videovedlegg kan rapporteres til NCMEC.", "moderation_and_reports.report_already_resolved": "Rapporten er allerede behandlet.", "moderation_and_reports.report_banned": "Du er utestengt fra å sende rapporter.", "moderation_and_reports.report_flow_outdated": "Rapportskjemaet ble endret mens du fylte det ut. Start rapporten på nytt.", diff --git a/packages/errors/src/i18n/weblate/locales/pl.json b/packages/errors/src/i18n/weblate/locales/pl.json index dfe920ce4..3fb574528 100644 --- a/packages/errors/src/i18n/weblate/locales/pl.json +++ b/packages/errors/src/i18n/weblate/locales/pl.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Nie udało się zweryfikować połączenia. Upewnij się, że token weryfikacyjny jest poprawnie umieszczony.", "content_and_safety.content_blocked": "Systemy bezpieczeństwa zablokowały tę treść.", "content_and_safety.explicit_content_cannot_be_sent": "Nie można wysyłać treści o jednoznacznie seksualnym charakterze.", - "content_and_safety.ncmec_already_submitted": "Tę treść już zgłoszono do NCMEC.", - "content_and_safety.ncmec_submission_failed": "Nie mogliśmy przesłać zgłoszenia do NCMEC. Spróbuj ponownie później.", "content_and_safety.nsfw_age_restricted": "Treści dla dorosłych są objęte ograniczeniem wiekowym.", "discovery.already_applied": "Ta społeczność została już zgłoszona do sekcji Odkrywanie.", "discovery.application_already_reviewed": "To zgłoszenie do sekcji Odkrywanie zostało już rozpatrzone.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Nieprawidłowy kod weryfikacyjny DSA.", "moderation_and_reports.invalid_report_flow_answers": "Te odpowiedzi nie pasują do formularza zgłoszenia. Rozpocznij zgłoszenie ponownie.", "moderation_and_reports.ip_ban_declined": "Ten adres IP nie może zostać dodany do listy blokowanych.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Do NCMEC można zgłaszać tylko załączniki w postaci obrazów lub filmów.", "moderation_and_reports.report_already_resolved": "Zgłoszenie zostało już rozpatrzone.", "moderation_and_reports.report_banned": "Zablokowano Ci możliwość przesyłania zgłoszeń.", "moderation_and_reports.report_flow_outdated": "Formularz zgłoszenia zmienił się podczas jego wypełniania. Rozpocznij zgłoszenie ponownie.", diff --git a/packages/errors/src/i18n/weblate/locales/pt-BR.json b/packages/errors/src/i18n/weblate/locales/pt-BR.json index 4d2160d1f..f89bbd452 100644 --- a/packages/errors/src/i18n/weblate/locales/pt-BR.json +++ b/packages/errors/src/i18n/weblate/locales/pt-BR.json @@ -211,8 +211,6 @@ "connections.verification_failed": "A verificação da conexão falhou. Certifique-se de que o token de verificação está corretamente posicionado.", "content_and_safety.content_blocked": "Este conteúdo foi bloqueado por sistemas de segurança.", "content_and_safety.explicit_content_cannot_be_sent": "Conteúdo explícito não pode ser enviado.", - "content_and_safety.ncmec_already_submitted": "Este conteúdo já foi enviado ao NCMEC.", - "content_and_safety.ncmec_submission_failed": "Não foi possível enviar a denúncia ao NCMEC. Tente novamente mais tarde.", "content_and_safety.nsfw_age_restricted": "Conteúdo NSFW tem restrição de idade.", "discovery.already_applied": "Esta comunidade já solicitou a inclusão em Descobrir.", "discovery.application_already_reviewed": "Esta solicitação de inclusão em Descobrir já foi analisada.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Código de verificação do DSA inválido.", "moderation_and_reports.invalid_report_flow_answers": "Estas respostas não correspondem ao formulário de denúncia. Inicie a denúncia novamente.", "moderation_and_reports.ip_ban_declined": "Este endereço IP não pode ser adicionado à lista de bloqueio.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Apenas anexos de imagem ou vídeo podem ser denunciados ao NCMEC.", "moderation_and_reports.report_already_resolved": "A denúncia já foi resolvida.", "moderation_and_reports.report_banned": "Você foi impedido de enviar denúncias.", "moderation_and_reports.report_flow_outdated": "O formulário de denúncia mudou enquanto você o preenchia. Comece a denúncia novamente.", diff --git a/packages/errors/src/i18n/weblate/locales/ro.json b/packages/errors/src/i18n/weblate/locales/ro.json index 50bfad2d7..1cf658bc8 100644 --- a/packages/errors/src/i18n/weblate/locales/ro.json +++ b/packages/errors/src/i18n/weblate/locales/ro.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Verificarea conexiunii a eșuat. Asigură-te că tokenul de verificare este plasat corect.", "content_and_safety.content_blocked": "Acest conținut a fost blocat de sistemele de siguranță.", "content_and_safety.explicit_content_cannot_be_sent": "Conținutul explicit nu poate fi trimis.", - "content_and_safety.ncmec_already_submitted": "Acest conținut a fost deja trimis la NCMEC.", - "content_and_safety.ncmec_submission_failed": "Nu am putut trimite raportul către NCMEC. Încearcă din nou mai târziu.", "content_and_safety.nsfw_age_restricted": "Conținutul NSFW este restricționat pe bază de vârstă.", "discovery.already_applied": "Această comunitate a trimis deja o cerere pentru Discovery.", "discovery.application_already_reviewed": "Această cerere pentru Discovery a fost deja analizată.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Cod de verificare DSA invalid.", "moderation_and_reports.invalid_report_flow_answers": "Aceste răspunsuri nu corespund formularului de raportare. Repornește raportarea.", "moderation_and_reports.ip_ban_declined": "Această adresă IP nu poate fi adăugată pe lista de blocare.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Doar atașamentele de tip imagine sau video pot fi raportate la NCMEC.", "moderation_and_reports.report_already_resolved": "Raportul a fost deja rezolvat.", "moderation_and_reports.report_banned": "Ți s-a interzis să trimiți rapoarte.", "moderation_and_reports.report_flow_outdated": "Formularul de raportare s-a modificat în timp ce îl completai. Repornește raportarea.", diff --git a/packages/errors/src/i18n/weblate/locales/ru.json b/packages/errors/src/i18n/weblate/locales/ru.json index 9bb5fd364..d2dae9de5 100644 --- a/packages/errors/src/i18n/weblate/locales/ru.json +++ b/packages/errors/src/i18n/weblate/locales/ru.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Не удалось проверить подключение. Убедись, что токен подтверждения размещён правильно.", "content_and_safety.content_blocked": "Этот контент был заблокирован системами безопасности.", "content_and_safety.explicit_content_cannot_be_sent": "Неприемлемый контент нельзя отправить.", - "content_and_safety.ncmec_already_submitted": "Этот контент уже был отправлен в NCMEC.", - "content_and_safety.ncmec_submission_failed": "Не удалось отправить жалобу в NCMEC. Попробуй ещё раз позже.", "content_and_safety.nsfw_age_restricted": "Контент NSFW имеет возрастное ограничение.", "discovery.already_applied": "Это сообщество уже подало заявку на участие в каталоге.", "discovery.application_already_reviewed": "Эта заявка на добавление в каталог уже рассмотрена.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Неверный код подтверждения DSA.", "moderation_and_reports.invalid_report_flow_answers": "Эти ответы не соответствуют форме жалобы. Начни жалобу заново.", "moderation_and_reports.ip_ban_declined": "Этот IP-адрес нельзя добавить в чёрный список.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Пожаловаться в NCMEC можно только на вложения с изображением или видео.", "moderation_and_reports.report_already_resolved": "Жалоба уже рассмотрена.", "moderation_and_reports.report_banned": "Ты забанен и не можешь отправлять жалобы.", "moderation_and_reports.report_flow_outdated": "Форма жалобы изменилась во время заполнения. Начни жалобу заново.", diff --git a/packages/errors/src/i18n/weblate/locales/sv-SE.json b/packages/errors/src/i18n/weblate/locales/sv-SE.json index 18412e9c0..55f4ca3bf 100644 --- a/packages/errors/src/i18n/weblate/locales/sv-SE.json +++ b/packages/errors/src/i18n/weblate/locales/sv-SE.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Anslutningsverifieringen misslyckades. Kontrollera att verifieringstoken är korrekt placerad.", "content_and_safety.content_blocked": "Det här innehållet blockerades av säkerhetssystemen.", "content_and_safety.explicit_content_cannot_be_sent": "Sexuellt explicit innehåll kan inte skickas.", - "content_and_safety.ncmec_already_submitted": "Det här innehållet har redan skickats till NCMEC.", - "content_and_safety.ncmec_submission_failed": "Vi kunde inte skicka anmälan till NCMEC. Försök igen senare.", "content_and_safety.nsfw_age_restricted": "NSFW-innehåll är åldersbegränsat.", "discovery.already_applied": "Den här communityn har redan ansökt om att visas i Upptäck.", "discovery.application_already_reviewed": "Den här ansökan till Upptäck har redan granskats.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Ogiltig DSA-verifieringskod.", "moderation_and_reports.invalid_report_flow_answers": "Svaren stämmer inte med anmälningsformuläret. Börja om med anmälan.", "moderation_and_reports.ip_ban_declined": "Den här IP-adressen kan inte läggas till i blocklistan.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Endast bild- eller videobilagor kan anmälas till NCMEC.", "moderation_and_reports.report_already_resolved": "Anmälan har redan behandlats.", "moderation_and_reports.report_banned": "Du har förbjudits att skicka in anmälningar.", "moderation_and_reports.report_flow_outdated": "Anmälningsformuläret ändrades medan du fyllde i det. Börja om med anmälan.", diff --git a/packages/errors/src/i18n/weblate/locales/th.json b/packages/errors/src/i18n/weblate/locales/th.json index 87e0b0857..b1daf4104 100644 --- a/packages/errors/src/i18n/weblate/locales/th.json +++ b/packages/errors/src/i18n/weblate/locales/th.json @@ -211,8 +211,6 @@ "connections.verification_failed": "การยืนยันการเชื่อมต่อล้มเหลว ตรวจสอบให้แน่ใจว่าวางโทเค็นการยืนยันไว้ถูกต้อง", "content_and_safety.content_blocked": "เนื้อหานี้ถูกบล็อกโดยระบบความปลอดภัย", "content_and_safety.explicit_content_cannot_be_sent": "ไม่สามารถส่งเนื้อหาที่ไม่เหมาะสมได้", - "content_and_safety.ncmec_already_submitted": "เนื้อหานี้ถูกส่งไปยัง NCMEC แล้ว", - "content_and_safety.ncmec_submission_failed": "ไม่สามารถส่งรายงานไปยัง NCMEC ได้ ลองอีกครั้งในภายหลัง", "content_and_safety.nsfw_age_restricted": "เนื้อหา NSFW มีข้อจำกัดด้านอายุ", "discovery.already_applied": "คอมมูนิตี้นี้สมัครเข้า Discovery แล้ว", "discovery.application_already_reviewed": "คำขอเข้า Discovery นี้ได้รับการพิจารณาแล้ว", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "รหัสยืนยัน DSA ไม่ถูกต้อง", "moderation_and_reports.invalid_report_flow_answers": "คำตอบเหล่านี้ไม่ตรงกับแบบฟอร์มรายงาน โปรดเริ่มรายงานใหม่อีกครั้ง", "moderation_and_reports.ip_ban_declined": "ไม่สามารถเพิ่มที่อยู่ IP นี้ไปยังรายการบล็อกได้", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "เฉพาะไฟล์แนบที่เป็นรูปภาพหรือวิดีโอเท่านั้นที่สามารถรายงานไปยัง NCMEC ได้", "moderation_and_reports.report_already_resolved": "รายงานนี้ได้รับการดำเนินการแล้ว", "moderation_and_reports.report_banned": "คุณถูกแบนจากการส่งรายงาน", "moderation_and_reports.report_flow_outdated": "แบบฟอร์มรายงานมีการเปลี่ยนแปลงขณะที่คุณกำลังกรอกข้อมูลอยู่ โปรดเริ่มรายงานใหม่อีกครั้ง", diff --git a/packages/errors/src/i18n/weblate/locales/tr.json b/packages/errors/src/i18n/weblate/locales/tr.json index f43c30ca6..816d04548 100644 --- a/packages/errors/src/i18n/weblate/locales/tr.json +++ b/packages/errors/src/i18n/weblate/locales/tr.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Bağlantı doğrulaması başarısız. Doğrulama belirtecinin doğru yerleştirildiğinden emin ol.", "content_and_safety.content_blocked": "Bu içerik güvenlik sistemleri tarafından engellendi.", "content_and_safety.explicit_content_cannot_be_sent": "Müstehcen içerik gönderilemez.", - "content_and_safety.ncmec_already_submitted": "Bu içerik zaten NCMEC'e gönderildi.", - "content_and_safety.ncmec_submission_failed": "Raporu NCMEC'e gönderemedik. Lütfen daha sonra tekrar dene.", "content_and_safety.nsfw_age_restricted": "NSFW içerik yaş sınırlı.", "discovery.already_applied": "Bu topluluk Keşfet'e zaten başvurdu.", "discovery.application_already_reviewed": "Bu Keşfet başvurusu zaten incelendi.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Geçersiz DSA doğrulama kodu.", "moderation_and_reports.invalid_report_flow_answers": "Bu yanıtlar rapor formuyla eşleşmiyor. Raporu yeniden başlat.", "moderation_and_reports.ip_ban_declined": "Bu IP adresi engelleme listesine eklenemez.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "NCMEC'e yalnızca resim veya video ekleri bildirilebilir.", "moderation_and_reports.report_already_resolved": "Rapor zaten çözüldü.", "moderation_and_reports.report_banned": "Rapor göndermen yasaklandı.", "moderation_and_reports.report_flow_outdated": "Rapor formu sen doldururken değişti. Raporu yeniden başlat.", diff --git a/packages/errors/src/i18n/weblate/locales/uk.json b/packages/errors/src/i18n/weblate/locales/uk.json index e7fb12c7c..c83e07287 100644 --- a/packages/errors/src/i18n/weblate/locales/uk.json +++ b/packages/errors/src/i18n/weblate/locales/uk.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Не вдалося підтвердити підключення. Переконайся, що токен підтвердження розміщено правильно.", "content_and_safety.content_blocked": "Цей вміст заблоковано системами безпеки.", "content_and_safety.explicit_content_cannot_be_sent": "Відвертий вміст не можна надсилати.", - "content_and_safety.ncmec_already_submitted": "Цей вміст уже надіслано до NCMEC.", - "content_and_safety.ncmec_submission_failed": "Не вдалося надіслати звіт до NCMEC. Спробуй пізніше.", "content_and_safety.nsfw_age_restricted": "Вміст NSFW має вікове обмеження.", "discovery.already_applied": "Ця спільнота вже подала заявку до Discovery.", "discovery.application_already_reviewed": "Цю заявку до Discovery вже розглянуто.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Недійсний код підтвердження DSA.", "moderation_and_reports.invalid_report_flow_answers": "Ці відповіді не відповідають формі звіту. Почни звіт заново.", "moderation_and_reports.ip_ban_declined": "Цю IP-адресу неможливо додати до чорного списку.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "До NCMEC можна надсилати звіти лише про вкладення із зображеннями або відео.", "moderation_and_reports.report_already_resolved": "Звіт вже розглянуто.", "moderation_and_reports.report_banned": "Тобі заборонено надсилати звіти.", "moderation_and_reports.report_flow_outdated": "Форма звіту змінилася під час заповнення. Почни звіт заново.", diff --git a/packages/errors/src/i18n/weblate/locales/vi.json b/packages/errors/src/i18n/weblate/locales/vi.json index 8ea5921a7..7623db095 100644 --- a/packages/errors/src/i18n/weblate/locales/vi.json +++ b/packages/errors/src/i18n/weblate/locales/vi.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Xác minh kết nối thất bại. Đảm bảo token xác minh được đặt đúng cách.", "content_and_safety.content_blocked": "Nội dung này đã bị chặn bởi hệ thống an toàn.", "content_and_safety.explicit_content_cannot_be_sent": "Không thể gửi nội dung nhạy cảm.", - "content_and_safety.ncmec_already_submitted": "Nội dung này đã được gửi đến NCMEC.", - "content_and_safety.ncmec_submission_failed": "Không thể gửi báo cáo đến NCMEC. Vui lòng thử lại sau.", "content_and_safety.nsfw_age_restricted": "Nội dung NSFW bị giới hạn độ tuổi.", "discovery.already_applied": "Cộng đồng này đã nộp đơn đăng ký Khám phá.", "discovery.application_already_reviewed": "Đơn đăng ký Khám phá này đã được xem xét.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Mã xác minh DSA không hợp lệ.", "moderation_and_reports.invalid_report_flow_answers": "Những câu trả lời này không khớp với biểu mẫu báo cáo. Hãy bắt đầu lại báo cáo.", "moderation_and_reports.ip_ban_declined": "Không thể thêm địa chỉ IP này vào danh sách chặn.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Chỉ có thể báo cáo tệp đính kèm là hình ảnh hoặc video cho NCMEC.", "moderation_and_reports.report_already_resolved": "Báo cáo đã được giải quyết.", "moderation_and_reports.report_banned": "Bạn đã bị cấm gửi báo cáo.", "moderation_and_reports.report_flow_outdated": "Biểu mẫu báo cáo đã thay đổi trong khi bạn đang điền. Hãy bắt đầu lại báo cáo.", diff --git a/packages/errors/src/i18n/weblate/locales/zh-CN.json b/packages/errors/src/i18n/weblate/locales/zh-CN.json index 046255697..232aab356 100644 --- a/packages/errors/src/i18n/weblate/locales/zh-CN.json +++ b/packages/errors/src/i18n/weblate/locales/zh-CN.json @@ -211,8 +211,6 @@ "connections.verification_failed": "连接验证失败。请确保验证令牌放置正确。", "content_and_safety.content_blocked": "此内容已被安全系统拦截。", "content_and_safety.explicit_content_cannot_be_sent": "无法发送露骨内容。", - "content_and_safety.ncmec_already_submitted": "此内容已提交至 NCMEC。", - "content_and_safety.ncmec_submission_failed": "无法向 NCMEC 提交举报。请稍后重试。", "content_and_safety.nsfw_age_restricted": "NSFW 内容有年龄限制。", "discovery.already_applied": "该社区已提交过社区发现申请。", "discovery.application_already_reviewed": "该社区发现申请已被审核。", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "DSA 验证码无效。", "moderation_and_reports.invalid_report_flow_answers": "这些回答与举报表单不匹配。请重新开始举报。", "moderation_and_reports.ip_ban_declined": "此 IP 地址无法添加到黑名单。", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "仅图片或视频附件可以举报至 NCMEC。", "moderation_and_reports.report_already_resolved": "举报已解决。", "moderation_and_reports.report_banned": "你已被禁止提交举报。", "moderation_and_reports.report_flow_outdated": "举报表单在你填写过程中已更新。请重新开始举报。", diff --git a/packages/errors/src/i18n/weblate/locales/zh-TW.json b/packages/errors/src/i18n/weblate/locales/zh-TW.json index 4c7227cb4..407710b78 100644 --- a/packages/errors/src/i18n/weblate/locales/zh-TW.json +++ b/packages/errors/src/i18n/weblate/locales/zh-TW.json @@ -211,8 +211,6 @@ "connections.verification_failed": "連線驗證失敗。請確認驗證權杖是否放置正確。", "content_and_safety.content_blocked": "此內容已被安全系統封鎖。", "content_and_safety.explicit_content_cannot_be_sent": "無法傳送露骨內容。", - "content_and_safety.ncmec_already_submitted": "此內容已提交至 NCMEC。", - "content_and_safety.ncmec_submission_failed": "我們無法將檢舉提交至 NCMEC。請稍後再試。", "content_and_safety.nsfw_age_restricted": "NSFW 內容設有年齡限制。", "discovery.already_applied": "此社群已申請探索功能。", "discovery.application_already_reviewed": "此探索申請已審核完成。", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "無效的 DSA 驗證碼。", "moderation_and_reports.invalid_report_flow_answers": "這些回答與檢舉表單不符。請重新開始檢舉。", "moderation_and_reports.ip_ban_declined": "無法將此 IP 位址加入封鎖名單。", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "只有圖片或影片附件可向 NCMEC 檢舉。", "moderation_and_reports.report_already_resolved": "檢舉已解決。", "moderation_and_reports.report_banned": "你已被禁止送出檢舉。", "moderation_and_reports.report_flow_outdated": "檢舉表單在你填寫時已變更。請重新開始檢舉。", diff --git a/packages/errors/src/i18n/weblate/messages.json b/packages/errors/src/i18n/weblate/messages.json index 2e55d17d1..d3bdc5a94 100644 --- a/packages/errors/src/i18n/weblate/messages.json +++ b/packages/errors/src/i18n/weblate/messages.json @@ -211,8 +211,6 @@ "connections.verification_failed": "Connection verification failed. Make sure the verification token is correctly placed.", "content_and_safety.content_blocked": "This content was blocked by safety systems.", "content_and_safety.explicit_content_cannot_be_sent": "Explicit content can't be sent.", - "content_and_safety.ncmec_already_submitted": "This content has already been submitted to NCMEC.", - "content_and_safety.ncmec_submission_failed": "We couldn't submit the report to NCMEC. Please try again later.", "content_and_safety.nsfw_age_restricted": "NSFW content is age restricted.", "discovery.already_applied": "This community has already applied for Discovery.", "discovery.application_already_reviewed": "This Discovery application has already been reviewed.", @@ -377,7 +375,6 @@ "moderation_and_reports.invalid_dsa_verification_code": "Invalid DSA verification code.", "moderation_and_reports.invalid_report_flow_answers": "These answers don't match the report form. Start the report again.", "moderation_and_reports.ip_ban_declined": "This IP address cannot be added to the blocklist.", - "moderation_and_reports.ncmec_attachment_must_be_image_or_video": "Only image or video attachments can be reported to NCMEC.", "moderation_and_reports.report_already_resolved": "Report already resolved.", "moderation_and_reports.report_banned": "You've been banned from submitting reports.", "moderation_and_reports.report_flow_outdated": "The report form changed while you were filling it in. Start the report again.", diff --git a/packages/schema/src/domains/admin/AdminMessageSchemas.ts b/packages/schema/src/domains/admin/AdminMessageSchemas.ts index 971c821e5..6c5e02819 100644 --- a/packages/schema/src/domains/admin/AdminMessageSchemas.ts +++ b/packages/schema/src/domains/admin/AdminMessageSchemas.ts @@ -31,18 +31,6 @@ export const DeleteMessageRequest = z.object({ export type DeleteMessageRequest = z.infer; -export const ReportAttachmentToNcmecRequest = z.object({ - channel_id: SnowflakeType, - message_id: SnowflakeType, - attachment_id: SnowflakeType, - filename: FilenameType, - source_report_id: SnowflakeType.optional(), - reporter_full_name: z.string().trim().min(1).max(200), - confirmed_viewed: z.literal(true), -}); - -export type ReportAttachmentToNcmecRequest = z.infer; - const MessageShredEntryType = z.object({ channel_id: SnowflakeType, message_id: SnowflakeType, diff --git a/packages/schema/src/domains/admin/AdminSchemas.ts b/packages/schema/src/domains/admin/AdminSchemas.ts index f6716e1a7..82a3a464a 100644 --- a/packages/schema/src/domains/admin/AdminSchemas.ts +++ b/packages/schema/src/domains/admin/AdminSchemas.ts @@ -1141,21 +1141,6 @@ export const BulkBanFileShasRequest = z.object({ export type BulkBanFileShasRequest = z.infer; -const NcmecSubmissionStatusEnum = createNamedStringLiteralUnion( - [ - ['not_submitted', 'not_submitted', 'Report has not been submitted to NCMEC'], - ['submitting', 'submitting', 'Report submission to NCMEC is in progress'], - ['submitted', 'submitted', 'Report has been submitted to NCMEC'], - ['failed', 'failed', 'Report submission to NCMEC failed'], - ], - 'NCMEC submission status', -); -export type NcmecSubmissionStatus = z.infer; -export const NcmecAttachmentSubmitResultResponse = z.object({ - success: z.literal(true), - ncmec_report_id: createStringType(1, 256), - audit_log_reason: createStringType(1, 4000), -}); export const CodesResponse = z.object({ codes: z.array(z.string()), }); @@ -1385,9 +1370,6 @@ const AdminMessageAttachmentSchema = z.object({ width: Int32Type.nullable(), height: Int32Type.nullable(), size: NonNegativeSafeIntegerType.nullable().optional(), - ncmec_status: NcmecSubmissionStatusEnum, - ncmec_report_id: createStringType(1, 256).nullable(), - ncmec_failure_reason: createStringType(1, 4000).nullable(), }); export const AdminMessageSchema = z.object({ id: SnowflakeStringType, @@ -1410,7 +1392,6 @@ export const AdminMessageSchema = z.object({ content: createStringType(0, 4000), timestamp: z.string(), attachments: z.array(AdminMessageAttachmentSchema).max(10), - user_prior_ncmec_report_ids: z.array(createStringType(1, 256)).max(100).optional(), }); export const LookupMessageResponse = z.object({ messages: z.array(AdminMessageSchema).max(100), @@ -1483,7 +1464,6 @@ const ReportMessageContextSchema = z.object({ .array(ReportMissingAttachmentSchema) .optional() .describe('Attachments of the message that could not be copied into the report when it was filed'), - user_prior_ncmec_report_ids: z.array(createStringType(1, 256)).max(100).optional(), }); const ReportProfileSnapshotAssetSchema = z.object({ hash: z.string().describe('The asset hash at the time the report was filed'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad5848e96..79550fa6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,9 +225,6 @@ catalogs: fake-indexeddb: specifier: 6.2.5 version: 6.2.5 - fast-xml-parser: - specifier: 5.11.1 - version: 5.11.1 favico.js: specifier: 0.3.10 version: 0.3.10 @@ -662,9 +659,6 @@ importers: emoji-regex: specifier: 'catalog:' version: 11.0.0 - fast-xml-parser: - specifier: 'catalog:' - version: 5.11.1 hi-base32: specifier: 'catalog:' version: 0.5.1 @@ -5262,9 +5256,6 @@ packages: resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==} engines: {node: '>= 20.19.0'} - '@nodable/entities@3.0.0': - resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==} - '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} @@ -7008,9 +6999,6 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - anynum@1.0.1: - resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} - app-builder-lib@27.0.0-alpha.8: resolution: {integrity: sha512-EnGEDEYPSxyIC3L0AlJNBZ9uymoqJlAwwYRX65XEeIvmjedeMeKDv0GKYJv4H2NKWG4pvTN50v5cb8ZROPUaVg==} engines: {node: '>=22.12.0'} @@ -7969,13 +7957,6 @@ packages: fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} - fast-xml-builder@1.3.1: - resolution: {integrity: sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==} - - fast-xml-parser@5.11.1: - resolution: {integrity: sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==} - hasBin: true - favico.js@0.3.10: resolution: {integrity: sha512-S5KvqAOczRjlyjQPPZPSlUEybBkfBgKosY/pzTIxkvKgigB+DkITvIEI70dxQarbv4PZ+UD77QzquCAcU/6LHQ==} @@ -8451,9 +8432,6 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} - is-unsafe@2.0.2: - resolution: {integrity: sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==} - isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -9380,10 +9358,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-expression-matcher@1.6.2: - resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==} - engines: {node: '>=14.0.0'} - path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -10321,9 +10295,6 @@ packages: '@types/node': optional: true - strnum@2.4.2: - resolution: {integrity: sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==} - style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} @@ -11099,10 +11070,6 @@ packages: utf-8-validate: optional: true - xml-naming@0.3.0: - resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==} - engines: {node: '>=16.0.0'} - xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} @@ -14390,8 +14357,6 @@ snapshots: '@noble/hashes@2.4.0': {} - '@nodable/entities@3.0.0': {} - '@open-draft/deferred-promise@2.2.0': {} '@open-draft/deferred-promise@3.0.0': {} @@ -15875,8 +15840,6 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 - anynum@1.0.1: {} - app-builder-lib@27.0.0-alpha.8(dmg-builder@27.0.0-alpha.8)(electron-builder-squirrel-windows@27.0.0-alpha.8)(supports-color@7.2.0): dependencies: '@electron/asar': 4.1.1 @@ -17003,20 +16966,6 @@ snapshots: dependencies: fast-string-width: 3.0.2 - fast-xml-builder@1.3.1: - dependencies: - path-expression-matcher: 1.6.2 - xml-naming: 0.3.0 - - fast-xml-parser@5.11.1: - dependencies: - '@nodable/entities': 3.0.0 - fast-xml-builder: 1.3.1 - is-unsafe: 2.0.2 - path-expression-matcher: 1.6.2 - strnum: 2.4.2 - xml-naming: 0.3.0 - favico.js@0.3.10: {} fd-package-json@2.0.0: @@ -17608,8 +17557,6 @@ snapshots: is-unicode-supported@2.1.0: {} - is-unsafe@2.0.2: {} - isarray@1.0.0: {} isbinaryfile@4.0.10: {} @@ -18809,8 +18756,6 @@ snapshots: path-exists@4.0.0: {} - path-expression-matcher@1.6.2: {} - path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -19943,10 +19888,6 @@ snapshots: optionalDependencies: '@types/node': 26.6.1 - strnum@2.4.2: - dependencies: - anynum: 1.0.1 - style-mod@4.1.3: {} style-to-js@1.1.21: @@ -20655,8 +20596,6 @@ snapshots: ws@8.21.3: {} - xml-naming@0.3.0: {} - xmlbuilder@15.1.1: {} xstate@5.33.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 82995e139..1f51ab425 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -110,7 +110,6 @@ catalog: eslint-plugin-react-google-translate: 1.0.4 eventemitter3: 5.0.4 fake-indexeddb: 6.2.5 - fast-xml-parser: 5.11.1 favico.js: 0.3.10 framer-motion: 13.4.0 hi-base32: 0.5.1 diff --git a/tools/dev/cassandra_target_schema.json b/tools/dev/cassandra_target_schema.json index 5cc7548ca..6667f7ac0 100644 --- a/tools/dev/cassandra_target_schema.json +++ b/tools/dev/cassandra_target_schema.json @@ -6641,144 +6641,6 @@ "primary_key": "((user_id))", "options": "" }, - { - "name": "ncmec_attachment_submissions", - "columns": [ - { - "name": "attachment_id", - "type": "bigint" - }, - { - "name": "user_id", - "type": "bigint" - }, - { - "name": "channel_id", - "type": "bigint" - }, - { - "name": "message_id", - "type": "bigint" - }, - { - "name": "filename", - "type": "text" - }, - { - "name": "source_report_id", - "type": "bigint" - }, - { - "name": "status", - "type": "text" - }, - { - "name": "ncmec_report_id", - "type": "text" - }, - { - "name": "ncmec_file_id", - "type": "text" - }, - { - "name": "submitted_at", - "type": "timestamp" - }, - { - "name": "submitted_by_admin_id", - "type": "bigint" - }, - { - "name": "reporter_full_name", - "type": "text" - }, - { - "name": "failure_reason", - "type": "text" - }, - { - "name": "archive_completed_at", - "type": "timestamp" - }, - { - "name": "content_deleted_at", - "type": "timestamp" - }, - { - "name": "created_at", - "type": "timestamp" - }, - { - "name": "updated_at", - "type": "timestamp" - } - ], - "primary_key": "(attachment_id)", - "options": "" - }, - { - "name": "ncmec_user_workflows", - "columns": [ - { - "name": "user_id", - "type": "bigint" - }, - { - "name": "deleted_at", - "type": "timestamp" - }, - { - "name": "deleted_by_admin_id", - "type": "bigint" - }, - { - "name": "deletion_private_reason", - "type": "text" - }, - { - "name": "deletion_ncmec_report_id", - "type": "text" - }, - { - "name": "archive_id", - "type": "bigint" - }, - { - "name": "archive_requested_at", - "type": "timestamp" - }, - { - "name": "archive_requested_by_admin_id", - "type": "bigint" - }, - { - "name": "archive_audit_log_reason", - "type": "text" - }, - { - "name": "archive_completed_at", - "type": "timestamp" - }, - { - "name": "deletion_job_queued_at", - "type": "timestamp" - }, - { - "name": "previous_report_ids", - "type": "set" - }, - { - "name": "created_at", - "type": "timestamp" - }, - { - "name": "updated_at", - "type": "timestamp" - } - ], - "primary_key": "(user_id)", - "options": "" - }, { "name": "notes", "columns": [ diff --git a/tools/i18n_auto/src/tokens.rs b/tools/i18n_auto/src/tokens.rs index e26e50e44..5a5f5c72e 100644 --- a/tools/i18n_auto/src/tokens.rs +++ b/tools/i18n_auto/src/tokens.rs @@ -353,7 +353,7 @@ mod tests { )); assert!(should_keep_unchanged("{start} – {end}", "de")); assert!(should_keep_unchanged("C++", "de")); - assert!(!should_keep_unchanged("NCMEC {ncmecReportId}", "de")); + assert!(!should_keep_unchanged("DSA {reportId}", "de")); assert!(!should_keep_unchanged("Hello", "de")); assert!(!should_keep_unchanged("{productName}", "en-US")); }