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