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

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

No files matched your search

+11 -161
View File
@@ -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.",
-41
View File
@@ -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::<Vec<_>>();
assert!(
!has_all_permissions(&acls(&held), NCMEC_ATTACHMENT_REPORT),
"{missing}"
);
}
}
#[test]
fn has_any_permission_none_match() {
let user_acls = acls(&[USER_LOOKUP]);
+3 -3
View File
@@ -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,
},
+1 -33
View File
@@ -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<NcmecAttachmentSubmitResult> {
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,
-6
View File
@@ -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)]
+2 -46
View File
@@ -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()
}
-2
View File
@@ -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(),
@@ -12,9 +12,6 @@ pub struct Attachment {
pub width: Option<u32>,
pub height: Option<u32>,
pub size: Option<u64>,
pub ncmec_status: String,
pub ncmec_report_id: Option<String>,
pub ncmec_failure_reason: Option<String>,
}
#[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,
}]
);
}
@@ -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);
}
});
})();"#
@@ -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,
))
}
@@ -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<String> {
non_empty(value).map(|value| format!("{value:0>4}"))
}
fn snapshot_tag(username: Option<&str>, discriminator: Option<&str>) -> Option<String> {
fn snapshot_tag(
username: Option<&str>,
discriminator: Option<&str>,
is_bot: bool,
) -> Option<String> {
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::<Vec<_>>();
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}");
}
+33 -8
View File
@@ -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!({
+9 -89
View File
@@ -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::<Value>::new()));
let sink = received.clone();
let api = Router::new()
.route(
"/admin/messages/ncmec-reports",
routing::post(move |Json(body): Json<Value>| {
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"});
@@ -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": [
@@ -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": [