use std::{fmt::Write as _, ops::Mul, time::Duration}; use axum::extract::State; use axum_client_ip::InsecureClientIp; use conduwuit::{Err, Error, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt}; use conduwuit_service::Services; use rand::Rng; use ruma::{ EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, api::client::{ error::ErrorKind, report_user, room::{report_content, report_room}, }, events::{Mentions, room::message::RoomMessageEventContent}, int, }; use tokio::time::sleep; use crate::Ruma; struct Report { sender: OwnedUserId, room_id: Option, event_id: Option, user_id: Option, report_type: String, reason: Option, score: Option, } /// # `POST /_matrix/client/v3/rooms/{roomId}/report` /// /// Reports an abusive room to homeserver admins #[tracing::instrument(skip_all, fields(%client), name = "report_room")] pub(crate) async fn report_room_route( State(services): State, InsecureClientIp(client): InsecureClientIp, body: Ruma, ) -> Result { let sender_user = body.sender_user(); if services.users.is_suspended(sender_user).await? { return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); } if body.reason.as_ref().is_some_and(|s| s.len() > 750) { return Err(Error::BadRequest( ErrorKind::InvalidParam, "Reason too long, should be 750 characters or fewer", )); } delay_response().await; if !services .rooms .state_cache .server_in_room(&services.server.name, &body.room_id) .await { return Err!(Request(NotFound( "Room does not exist to us, no local users have joined at all" ))); } info!( "Received room report by user {sender_user} for room {} with reason: \"{}\"", body.room_id, body.reason.as_deref().unwrap_or("") ); let report = Report { sender: sender_user.to_owned(), room_id: Some(body.room_id.clone()), event_id: None, user_id: None, report_type: "room".to_owned(), reason: body.reason.clone(), score: None, }; services.admin.send_message(build_report(report)).await.ok(); Ok(report_room::v3::Response {}) } /// # `POST /_matrix/client/v3/rooms/{roomId}/report/{eventId}` /// /// Reports an inappropriate event to homeserver admins #[tracing::instrument(skip_all, fields(%client), name = "report_event")] pub(crate) async fn report_event_route( State(services): State, InsecureClientIp(client): InsecureClientIp, body: Ruma, ) -> Result { // user authentication let sender_user = body.sender_user(); if services.users.is_suspended(sender_user).await? { return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); } delay_response().await; // check if we know about the reported event ID or if it's invalid let Ok(pdu) = services.rooms.timeline.get_pdu(&body.event_id).await else { 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, body.reason.as_ref(), body.score, &pdu, ) .await?; info!( "Received event report by user {sender_user} for room {} and event ID {}, with reason: \ \"{}\"", body.room_id, body.event_id, body.reason.as_deref().unwrap_or("") ); let report = Report { sender: sender_user.to_owned(), room_id: Some(body.room_id.clone()), event_id: Some(body.event_id.clone()), user_id: None, report_type: "event".to_owned(), reason: body.reason.clone(), score: body.score, }; services.admin.send_message(build_report(report)).await.ok(); Ok(report_content::v3::Response {}) } #[tracing::instrument(skip_all, fields(%client), name = "report_user")] pub(crate) async fn report_user_route( State(services): State, InsecureClientIp(client): InsecureClientIp, body: Ruma, ) -> Result { // user authentication let sender_user = body.sender_user.as_ref().expect("user is authenticated"); if services.users.is_suspended(sender_user).await? { return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); } if body.reason.as_ref().is_some_and(|s| s.len() > 750) { return Err(Error::BadRequest( ErrorKind::InvalidParam, "Reason too long, should be 750 characters or fewer", )); } delay_response().await; if !services.users.is_active_local(&body.user_id).await { // return 200 as to not reveal if the user exists. Recommended by spec. return Ok(report_user::v3::Response {}); } let report = Report { sender: sender_user.to_owned(), room_id: None, event_id: None, user_id: Some(body.user_id.clone()), report_type: "user".to_owned(), reason: body.reason.clone(), score: None, }; info!( "Received room report from {sender_user} for user {} with reason: \"{}\"", body.user_id, body.reason.as_deref().unwrap_or("") ); services.admin.send_message(build_report(report)).await.ok(); Ok(report_user::v3::Response {}) } /// 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 async fn is_event_report_valid( services: &Services, event_id: &EventId, room_id: &RoomId, sender_user: &UserId, reason: Option<&String>, score: Option, pdu: &PduEvent, ) -> Result<()> { debug_info!( "Checking if report from user {sender_user} for event {event_id} in room {room_id} is \ valid" ); if room_id != pdu.room_id { return Err(Error::BadRequest( ErrorKind::NotFound, "Event ID does not belong to the reported room", )); } if score.is_some_and(|s| s > int!(0) || s < int!(-100)) { return Err(Error::BadRequest( ErrorKind::InvalidParam, "Invalid score, must be within 0 to -100", )); } if reason.as_ref().is_some_and(|s| s.len() > 750) { return Err(Error::BadRequest( ErrorKind::InvalidParam, "Reason too long, should be 750 characters or fewer", )); } if !services .rooms .state_cache .room_members(room_id) .ready_any(|user_id| user_id == sender_user) .await { return Err(Error::BadRequest( ErrorKind::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); if report.user_id.is_some() { let _ = writeln!(text, "- Reported User ID: `{}`", report.user_id.unwrap()); } if report.room_id.is_some() { let _ = writeln!(text, "- Reported Room ID: `{}`", report.room_id.unwrap()); } if report.event_id.is_some() { let _ = writeln!(text, "- Reported Event ID: `{}`", report.event_id.unwrap()); } if let Some(score) = report.score { let _ = writeln!(text, "- User-supplied offensiveness score: {}%", score.mul(int!(-1))); } if let Some(reason) = report.reason { let _ = writeln!(text, "- Report Reason: {reason}"); } 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() { let time_to_wait = rand::thread_rng().gen_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; }