Skip to main content

tuwunel_api/client/
send.rs

1use std::collections::BTreeMap;
2
3use axum::extract::State;
4use futures::{FutureExt, future::try_join4};
5use ruma::{
6	DeviceId, RoomId, TransactionId, UserId,
7	api::client::message::send_message_event,
8	events::{
9		AnyMessageLikeEventContent, MessageLikeEventType,
10		reaction::ReactionEventContent,
11		room::{encrypted::Relation, redaction::RoomRedactionEventContent},
12	},
13	serde::Raw,
14};
15use serde::Deserialize;
16use serde_json::from_str;
17use tuwunel_core::{
18	Err, Result, debug_warn, err,
19	matrix::{Event, pdu::PduBuilder},
20	utils::{self},
21	warn,
22};
23use tuwunel_service::Services;
24
25use crate::{Ruma, client::utils::is_self_redaction};
26
27#[derive(Deserialize)]
28struct ExtractRelatesTo {
29	#[serde(rename = "m.relates_to")]
30	relates_to: Relation,
31}
32
33/// # `PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}`
34///
35/// Send a message event into the room.
36///
37/// - Is a NOOP if the txn id was already used before and returns the same event
38///   id again
39/// - The only requirement for the content is that it has to be valid json
40/// - Tries to send the event into the room, auth rules will determine if it is
41///   allowed
42pub(crate) async fn send_message_event_route(
43	State(services): State<crate::State>,
44	body: Ruma<send_message_event::v3::Request>,
45) -> Result<send_message_event::v3::Response> {
46	let sender_user = body.sender_user();
47	let sender_device = body.sender_device.as_deref();
48	let appservice_info = body.appservice_info.as_ref();
49
50	// Forbid m.room.encrypted if encryption is disabled
51	if body.event_type == MessageLikeEventType::RoomEncrypted && !services.config.allow_encryption
52	{
53		return Err!(Request(Forbidden("Encryption has been disabled")));
54	}
55
56	// MSC4169: clients sending m.room.redaction via /send put `redacts` in
57	// `content`. Pre-v11 auth rules read it from the top level; lift it so
58	// `redacts_id(...)` resolves regardless of room version. Mirrors the
59	// /redact handler.
60	let redaction_content = || {
61		body.body
62			.body
63			.deserialize_as_unchecked::<RoomRedactionEventContent>()
64			.inspect_err(|_| {
65				debug_warn!(
66					%sender_user,
67					event = %body.body.body.json(),
68					"Client sent invalid redaction event"
69				);
70			})
71			.ok()
72	};
73
74	let redacts_id = body
75		.event_type
76		.eq(&MessageLikeEventType::RoomRedaction)
77		.then(redaction_content)
78		.flatten()
79		.and_then(|content| content.redacts);
80
81	if body.event_type == MessageLikeEventType::RoomRedaction
82		&& services.config.disable_local_redactions
83		&& !services.admin.user_is_admin(sender_user).await
84	{
85		warn!(
86			%sender_user,
87			?redacts_id,
88			"Local redactions are disabled, non-admin user attempted to redact an event"
89		);
90
91		return Err!(Request(Forbidden("Redactions are disabled on this server.")));
92	}
93
94	if services.users.is_suspended(sender_user).await {
95		if body.event_type != MessageLikeEventType::RoomRedaction {
96			return Err!(Request(UserSuspended(
97				"Cannot send non-redaction events while suspended."
98			)));
99		}
100
101		let is_self = match &redacts_id {
102			| None => false,
103			| Some(redacts_id) => is_self_redaction(&services, sender_user, redacts_id).await,
104		};
105
106		if !is_self {
107			return Err!(Request(UserSuspended("Can only redact own events while suspended.")));
108		}
109	}
110
111	let state_lock = services.state.mutex.lock(&body.room_id).await;
112
113	let (existing_txnid, ..) = try_join4(
114		check_existing_txnid(&services, sender_user, sender_device, &body.txn_id).map(Ok),
115		check_duplicate_reaction(&services, &body.event_type, sender_user, &body.body.body),
116		check_public_call_invite(&services, &body.event_type, &body.room_id),
117		check_nested_thread(&services, &body.body.body),
118	)
119	.await?;
120
121	if let Some(existing_txnid) = existing_txnid {
122		return existing_txnid;
123	}
124
125	let mut unsigned = BTreeMap::new();
126	unsigned.insert("transaction_id".to_owned(), body.txn_id.to_string().into());
127
128	let content = from_str(body.body.body.json().get())
129		.map_err(|e| err!(Request(BadJson("Invalid JSON body: {e}"))))?;
130
131	let event_id = services
132		.timeline
133		.build_and_append_pdu(
134			PduBuilder {
135				event_type: body.event_type.clone().into(),
136				content,
137				unsigned: Some(unsigned),
138				timestamp: appservice_info.and(body.timestamp),
139				redacts: redacts_id,
140				..Default::default()
141			},
142			sender_user,
143			&body.room_id,
144			&state_lock,
145		)
146		.await?;
147
148	services.transaction_ids.add_txnid(
149		sender_user,
150		sender_device,
151		&body.txn_id,
152		event_id.as_bytes(),
153	);
154
155	drop(state_lock);
156
157	Ok(send_message_event::v3::Response { event_id })
158}
159
160async fn check_public_call_invite(
161	services: &Services,
162	event_type: &MessageLikeEventType,
163	room_id: &RoomId,
164) -> Result {
165	if *event_type != MessageLikeEventType::CallInvite {
166		return Ok(());
167	}
168
169	if !services.directory.is_public_room(room_id).await {
170		return Ok(());
171	}
172
173	Err!(Request(Forbidden("Room call invites are not allowed in public rooms")))
174}
175
176// Forbid duplicate reactions
177async fn check_duplicate_reaction(
178	services: &Services,
179	event_type: &MessageLikeEventType,
180	sender_user: &UserId,
181	body: &Raw<AnyMessageLikeEventContent>,
182) -> Result {
183	if *event_type != MessageLikeEventType::Reaction {
184		return Ok(());
185	}
186
187	let Ok(content) = body.deserialize_as_unchecked::<ReactionEventContent>() else {
188		return Ok(());
189	};
190
191	if !services
192		.pdu_metadata
193		.event_has_relation(
194			&content.relates_to.event_id,
195			Some(sender_user),
196			None,
197			Some(&content.relates_to.key),
198		)
199		.await
200	{
201		return Ok(());
202	}
203
204	Err!(Request(DuplicateAnnotation("Duplicate reactions are not allowed.")))
205}
206
207// MSC3440/Matrix 1.4: a thread may only target an event which itself carries
208// no rel_type; the spec assigns this rejection 400 M_UNKNOWN.
209async fn check_nested_thread(
210	services: &Services,
211	body: &Raw<AnyMessageLikeEventContent>,
212) -> Result {
213	let Ok(ExtractRelatesTo { relates_to: Relation::Thread(thread) }) =
214		body.deserialize_as_unchecked()
215	else {
216		return Ok(());
217	};
218
219	let Ok(root) = services.timeline.get_pdu(&thread.event_id).await else {
220		return Ok(());
221	};
222
223	let nested = root
224		.get_content()
225		.is_ok_and(|content: ExtractRelatesTo| content.relates_to.rel_type().is_some());
226
227	if !nested {
228		return Ok(());
229	}
230
231	Err!(Request(Unknown("Cannot start threads from an event with a relation.")))
232}
233
234/// Check if this is a new transaction id. Returns Some when the transaction id
235/// exists and the send must then be terminated by returning the contained
236/// result.
237async fn check_existing_txnid(
238	services: &Services,
239	sender_user: &UserId,
240	sender_device: Option<&DeviceId>,
241	txn_id: &TransactionId,
242) -> Option<Result<send_message_event::v3::Response>> {
243	let Ok(response) = services
244		.transaction_ids
245		.existing_txnid(sender_user, sender_device, txn_id)
246		.await
247	else {
248		return None;
249	};
250
251	// The client might have sent a txnid of the /sendToDevice endpoint
252	// This txnid has no response associated with it
253	if response.is_empty() {
254		return Some(Err!(Request(InvalidParam(
255			"Tried to use txn_id already used for an incompatible endpoint."
256		))));
257	}
258
259	let Ok(Ok(event_id)) = utils::string_from_bytes(&response).map(TryInto::try_into) else {
260		return Some(Err!(Database("Invalid event_id in txn_id data: {response:?}.")));
261	};
262
263	Some(Ok(send_message_event::v3::Response { event_id }))
264}