tuwunel_api/client/
report.rs1use axum::extract::State;
2use ruma::{
3 EventId, RoomId, UserId,
4 api::client::{
5 reporting::report_user,
6 room::{report_content, report_room},
7 },
8};
9use tuwunel_core::{Err, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt};
10use tuwunel_service::Services;
11
12use crate::{ClientIp, Ruma};
13
14const REASON_MAX_LEN: usize = 750;
15
16#[tracing::instrument(skip_all, fields(%client), name = "report_room")]
20pub(crate) async fn report_room_route(
21 State(services): State<crate::State>,
22 ClientIp(client): ClientIp,
23 body: Ruma<report_room::v3::Request>,
24) -> Result<report_room::v3::Response> {
25 let sender_user = body.sender_user();
26
27 info!(
28 "Received room report by user {sender_user} for room {} with reason: \"{}\"",
29 body.room_id, body.reason,
30 );
31
32 if body.reason.len() > REASON_MAX_LEN {
33 return Err!(Request(InvalidParam(
34 "Reason too long, should be {REASON_MAX_LEN} characters or fewer"
35 )));
36 }
37
38 if !services
39 .state_cache
40 .server_in_room(&services.server.name, &body.room_id)
41 .await
42 {
43 return Err!(Request(NotFound(
44 "Room does not exist to us, no local users have joined at all"
45 )));
46 }
47
48 services
49 .admin
50 .send_text(&format!(
51 "@room Room report received from {}\nReport Reason: {}\n\nRoom ID: {}",
52 sender_user, body.reason, body.room_id,
53 ))
54 .await;
55
56 Ok(report_room::v3::Response {})
57}
58
59#[tracing::instrument(skip_all, fields(%client), name = "report_user")]
63pub(crate) async fn report_user_route(
64 State(services): State<crate::State>,
65 ClientIp(client): ClientIp,
66 body: Ruma<report_user::v3::Request>,
67) -> Result<report_user::v3::Response> {
68 let sender_user = body.sender_user();
69
70 if body.reason.len().gt(&REASON_MAX_LEN) {
71 return Err!(Request(InvalidParam(
72 "Reason too long, should be {REASON_MAX_LEN} characters or fewer"
73 )));
74 }
75
76 if !services
77 .users
78 .is_active_local(&body.user_id)
79 .await
80 {
81 return Err!(Request(NotFound("User does not exist")));
82 }
83
84 info!(
85 "Received user report by user {sender_user} for user {} with reason: \"{}\"",
86 body.user_id, body.reason,
87 );
88
89 services
90 .admin
91 .send_text(&format!(
92 "@room User report received from {}\nReport Reason: {}\n\nReported User ID: {}",
93 sender_user, body.reason, body.user_id,
94 ))
95 .await;
96
97 Ok(report_user::v3::Response {})
98}
99
100#[tracing::instrument(skip_all, fields(%client), name = "report_event")]
104pub(crate) async fn report_event_route(
105 State(services): State<crate::State>,
106 ClientIp(client): ClientIp,
107 body: Ruma<report_content::v3::Request>,
108) -> Result<report_content::v3::Response> {
109 let sender_user = body.sender_user();
110 let reason = body.reason.as_deref().unwrap_or("");
111
112 info!(
113 "Received event report by user {sender_user} for room {} and event ID {}, with reason: \
114 \"{}\"",
115 body.room_id, body.event_id, reason,
116 );
117
118 let Ok(pdu) = services.timeline.get_pdu(&body.event_id).await else {
120 return Err!(Request(NotFound("Event ID is not known to us or Event ID is invalid")));
121 };
122
123 is_event_report_valid(
124 &services,
125 &pdu.event_id,
126 &body.room_id,
127 sender_user,
128 body.reason.as_ref(),
129 &pdu,
130 )
131 .await?;
132
133 services
134 .admin
135 .send_text(&format!(
136 "@room Event report received from {}\nReport Reason: {}\n\nEvent ID: {}\nRoom ID: \
137 {}\nSent By: {}",
138 sender_user, reason, pdu.event_id, pdu.room_id, pdu.sender,
139 ))
140 .await;
141
142 Ok(report_content::v3::Response {})
143}
144
145async fn is_event_report_valid(
151 services: &Services,
152 event_id: &EventId,
153 room_id: &RoomId,
154 sender_user: &UserId,
155 reason: Option<&String>,
156 pdu: &PduEvent,
157) -> Result {
158 debug_info!(
159 "Checking if report from user {sender_user} for event {event_id} in room {room_id} is \
160 valid"
161 );
162
163 if room_id != pdu.room_id {
164 return Err!(Request(NotFound("Event ID does not belong to the reported room",)));
165 }
166
167 if reason.as_ref().is_some_and(|s| s.len() > 750) {
168 return Err!(Request(
169 InvalidParam("Reason too long, should be 750 characters or fewer",)
170 ));
171 }
172
173 if !services
174 .state_cache
175 .room_members(room_id)
176 .ready_any(|user_id| user_id == sender_user)
177 .await
178 {
179 return Err!(Request(NotFound("You are not in the room you are reporting.",)));
180 }
181
182 Ok(())
183}