Skip to main content

tuwunel_api/client/
report.rs

1use 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/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
17///
18/// Reports an abusive room to homeserver admins
19#[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/// # `POST /_matrix/client/v3/users/{userId}/report`
60///
61/// Reports an inappropriate user to homeserver admins (MSC4260).
62#[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/// # `POST /_matrix/client/v3/rooms/{roomId}/report/{eventId}`
101///
102/// Reports an inappropriate event to homeserver admins
103#[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	// check if we know about the reported event ID or if it's invalid
119	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
145/// in the following order:
146///
147/// check if the room ID from the URI matches the PDU's room ID
148/// check if report reasoning is less than or equal to 750 characters
149/// check if reporting user is in the reporting room
150async 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}