Files
continuwuity/src/api/client/report.rs
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

247 lines
7.2 KiB
Rust
Raw Normal View History

2025-08-05 21:06:50 +01:00
use std::{fmt::Write as _, time::Duration};
2024-01-17 02:08:07 -05:00
2024-07-16 08:05:25 +00:00
use axum::extract::State;
2026-04-23 20:02:48 +01:00
use axum_client_ip::ClientIp;
2025-09-17 20:46:03 +00:00
use conduwuit::{Err, Event, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt};
use conduwuit_service::Services;
use ruma::{
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
2024-10-02 01:47:53 -04:00
api::client::{
reporting::report_user,
2024-10-02 01:47:53 -04:00
room::{report_content, report_room},
},
2025-07-01 18:00:28 +01:00
events::{Mentions, room::message::RoomMessageEventContent},
};
2024-01-17 02:08:07 -05:00
use tokio::time::sleep;
use crate::Ruma;
2024-03-05 19:48:54 -05:00
struct Report {
sender: OwnedUserId,
room_id: Option<OwnedRoomId>,
event_id: Option<OwnedEventId>,
user_id: Option<OwnedUserId>,
report_type: String,
reason: String,
}
2026-04-22 10:33:16 -04:00
const MAX_REASON_LENGTH: usize = 2000;
2024-10-02 01:47:53 -04:00
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
///
/// Reports an abusive room to homeserver admins
2026-01-04 03:04:37 +00:00
#[tracing::instrument(skip_all, fields(%client), name = "report_room", level = "info")]
2024-10-02 01:47:53 -04:00
pub(crate) async fn report_room_route(
State(services): State<crate::State>,
2026-04-23 20:02:48 +01:00
ClientIp(client): ClientIp,
2024-10-02 01:47:53 -04:00
body: Ruma<report_room::v3::Request>,
) -> Result<report_room::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
2024-10-02 01:47:53 -04:00
2026-04-22 10:33:16 -04:00
if body.reason.len() > MAX_REASON_LENGTH {
return Err!(Request(InvalidParam(
"Reason too long, should be {MAX_REASON_LENGTH} bytes or fewer",
)));
2025-02-25 18:38:12 +00:00
}
2024-10-02 01:47:53 -04:00
delay_response().await;
// We log this early in case the room ID does actually exist, in which case
// admins who scan their logs can see the report and choose to investigate at
// their discretion.
info!(
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
body.room_id, body.reason
);
2024-10-02 01:47:53 -04:00
if !services
.rooms
.state_cache
2025-01-25 23:41:39 +00:00
.server_in_room(&services.server.name, &body.room_id)
2024-10-02 01:47:53 -04:00
.await
{
2025-09-07 23:36:26 +01:00
return Err!(Request(NotFound(
"Room does not exist to us, no local users have joined at all"
)));
2024-10-02 01:47:53 -04:00
}
let report = Report {
sender: sender_user.to_owned(),
2025-07-01 18:00:28 +01:00
room_id: Some(body.room_id.clone()),
event_id: None,
user_id: None,
2025-07-01 18:00:28 +01:00
report_type: "room".to_owned(),
reason: body.reason.clone(),
};
services.admin.send_message(build_report(report)).await.ok();
2024-10-02 01:47:53 -04:00
Ok(report_room::v3::Response::new())
2024-10-02 01:47:53 -04:00
}
/// # `POST /_matrix/client/v3/rooms/{roomId}/report/{eventId}`
///
/// Reports an inappropriate event to homeserver admins
2026-01-04 03:04:37 +00:00
#[tracing::instrument(skip_all, fields(%client), name = "report_event", level = "info")]
2024-04-22 23:48:57 -04:00
pub(crate) async fn report_event_route(
State(services): State<crate::State>,
2026-04-23 20:02:48 +01:00
ClientIp(client): ClientIp,
2024-10-02 01:47:53 -04:00
body: Ruma<report_content::v3::Request>,
2024-04-22 23:48:57 -04:00
) -> Result<report_content::v3::Response> {
2024-01-17 02:08:07 -05:00
// user authentication
let sender_user = body.identity.expect_sender_user()?;
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
delay_response().await;
let reason = body
.reason
.clone()
.unwrap_or_else(|| "<no reason provided>".to_owned());
2024-01-17 02:08:07 -05:00
// check if we know about the reported event ID or if it's invalid
2024-08-08 17:18:30 +00:00
let Ok(pdu) = services.rooms.timeline.get_pdu(&body.event_id).await else {
2025-09-07 23:36:26 +01:00
return Err!(Request(NotFound("Event ID is not known to us or Event ID is invalid")));
};
is_event_report_valid(&services, &pdu.event_id, &body.room_id, sender_user, &reason, &pdu)
.await?;
2025-07-01 01:55:13 +01:00
info!(
"Received event report by user {sender_user} for room {} and event ID {}, with reason: \
\"{}\"",
body.room_id, body.event_id, reason
2025-07-01 01:55:13 +01:00
);
let report = Report {
sender: sender_user.to_owned(),
2025-07-01 18:00:28 +01:00
room_id: Some(body.room_id.clone()),
event_id: Some(body.event_id.clone()),
user_id: None,
2025-07-01 18:00:28 +01:00
report_type: "event".to_owned(),
reason,
};
services.admin.send_message(build_report(report)).await.ok();
Ok(report_content::v3::Response::new())
}
2026-01-04 03:04:37 +00:00
#[tracing::instrument(skip_all, fields(%client), name = "report_user", level = "info")]
2025-07-01 01:55:13 +01:00
pub(crate) async fn report_user_route(
State(services): State<crate::State>,
2026-04-23 20:02:48 +01:00
ClientIp(client): ClientIp,
2025-07-01 01:55:13 +01:00
body: Ruma<report_user::v3::Request>,
) -> Result<report_user::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
2025-07-01 01:55:13 +01:00
2026-04-22 10:33:16 -04:00
if body.reason.len() > MAX_REASON_LENGTH {
return Err!(Request(InvalidParam(
"Reason too long, should be {MAX_REASON_LENGTH} bytes or fewer",
)));
2025-07-01 01:55:13 +01:00
}
delay_response().await;
if !services.users.status(&body.user_id).await.is_found() {
2025-07-01 01:55:13 +01:00
// return 200 as to not reveal if the user exists. Recommended by spec.
return Ok(report_user::v3::Response::new());
2025-07-01 01:55:13 +01:00
}
let report = Report {
sender: sender_user.to_owned(),
room_id: None,
event_id: None,
2025-07-01 18:00:28 +01:00
user_id: Some(body.user_id.clone()),
report_type: "user".to_owned(),
reason: body.reason.clone(),
};
2025-07-01 01:55:13 +01:00
info!(
"Received room report from {sender_user} for user {} with reason: \"{}\"",
body.user_id, body.reason
2025-07-01 01:55:13 +01:00
);
services.admin.send_message(build_report(report)).await.ok();
2025-07-01 01:55:13 +01:00
Ok(report_user::v3::Response::new())
2025-07-01 01:55:13 +01:00
}
/// in the following order:
///
/// check if the room ID from the URI matches the PDU's room ID
/// check if score is in valid range
/// check if report reasoning is less than or equal to 750 characters
/// check if reporting user is in the reporting room
2024-10-02 01:47:53 -04:00
async fn is_event_report_valid(
services: &Services,
event_id: &EventId,
room_id: &RoomId,
sender_user: &UserId,
reason: &str,
pdu: &PduEvent,
) -> Result<()> {
debug_info!(
"Checking if report from user {sender_user} for event {event_id} in room {room_id} is \
valid"
);
2025-09-17 20:46:03 +00:00
if room_id != pdu.room_id_or_hash() {
return Err!(Request(NotFound("Event ID does not belong to the reported room",)));
}
2026-04-22 10:33:16 -04:00
if reason.len() > MAX_REASON_LENGTH {
return Err!(Request(InvalidParam(
"Reason too long, should be {MAX_REASON_LENGTH} bytes or fewer",
)));
2025-02-25 18:38:12 +00:00
}
2024-07-16 08:05:25 +00:00
if !services
.rooms
.state_cache
.room_members(room_id)
2024-08-08 17:18:30 +00:00
.ready_any(|user_id| user_id == sender_user)
.await
{
return Err!(Request(NotFound("You are not in the room you are reporting.",)));
}
Ok(())
}
/// Builds a report message to be sent to the admin room.
fn build_report(report: Report) -> RoomMessageEventContent {
let mut text =
format!("@room New {} report received from {}:\n\n", report.report_type, report.sender);
2026-05-19 20:26:04 +01:00
if let Some(user_id) = report.user_id {
let _ = writeln!(text, "- Reported User ID: `{user_id}`");
}
2026-05-19 20:26:04 +01:00
if let Some(room_id) = report.room_id {
let _ = writeln!(text, "- Reported Room ID: `{room_id}`");
}
2026-05-19 20:26:04 +01:00
if let Some(event_id) = report.event_id {
let _ = writeln!(text, "- Reported Event ID: `{event_id}`");
}
let _ = writeln!(text, "- Report Reason: {}", report.reason);
2025-07-01 18:00:28 +01:00
RoomMessageEventContent::text_markdown(text).add_mentions(Mentions::with_room_mention())
}
/// even though this is kinda security by obscurity, let's still make a small
/// random delay sending a response per spec suggestion regarding
/// enumerating for potential events existing in our server.
async fn delay_response() {
2026-02-20 22:57:45 +00:00
let time_to_wait = rand::random_range(2..5);
debug_info!(
"Got successful /report request, waiting {time_to_wait} seconds before sending \
successful response."
);
sleep(Duration::from_secs(time_to_wait)).await;
}