1use axum::extract::State;
2use futures::future::{try_join, try_join3};
3use ruma::{
4 OwnedEventId, RoomId, UserId,
5 api::client::message::{
6 send_message_event, send_message_event::v3::Response as SendMessageResponse,
7 },
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::{Err, Result, debug_warn, err, result::NotFound, warn};
18use tuwunel_matrix::{Event, PduEvent, pdu::PduBuilder};
19use tuwunel_service::{
20 Services,
21 transaction_ids::{RoomScope, RoomTxn},
22};
23
24use crate::{
25 Ruma,
26 utils::{is_self_redaction, txnid_event_id, txnid_unsigned},
27};
28
29#[derive(Deserialize)]
30struct ExtractRelatesTo {
31 #[serde(rename = "m.relates_to")]
32 relates_to: Relation,
33}
34
35pub(crate) async fn send_message_event_route(
45 State(services): State<crate::State>,
46 body: Ruma<send_message_event::v3::Request>,
47) -> Result<send_message_event::v3::Response> {
48 let sender_user = body.sender_user();
49 let sender_device = body.sender_device.as_deref();
50 let appservice_info = body.appservice_info.as_ref();
51 let redacts_id =
52 check_send_policy(&services, sender_user, &body.event_type, &body.body.body).await?;
53
54 let state_lock = services.state.mutex.lock(&body.room_id).await;
55 let event_type = body.event_type.to_cow_str();
56 let txn = RoomTxn {
57 user_id: sender_user,
58 device_id: sender_device,
59 txn_id: &body.txn_id,
60 room_id: &body.room_id,
61 };
62
63 let content_checks = check_send_content(
64 &services,
65 sender_user,
66 &body.room_id,
67 &body.event_type,
68 &body.body.body,
69 );
70
71 let (existing_txnid, ()) =
72 try_join(check_existing_txnid(&services, txn, &event_type), content_checks).await?;
73
74 if let Some(existing_txnid) = existing_txnid {
75 return Ok(existing_txnid);
76 }
77
78 let content = from_str(body.body.body.json().get())
79 .map_err(|e| err!(Request(BadJson("Invalid JSON body: {e}"))))?;
80
81 let pdu = PduBuilder {
82 event_type: body.event_type.clone().into(),
83 content,
84 unsigned: txnid_unsigned(&body.txn_id).into(),
85 timestamp: appservice_info.and(body.timestamp),
86 redacts: redacts_id,
87 ..Default::default()
88 };
89
90 let event_id = services
91 .timeline
92 .build_and_append_pdu(pdu, sender_user, &body.room_id, &state_lock)
93 .await?;
94
95 services.transaction_ids.add_room_txnid(
96 txn,
97 RoomScope::Send(&event_type),
98 event_id.as_bytes(),
99 );
100
101 drop(state_lock);
102
103 Ok(SendMessageResponse { event_id })
104}
105
106#[tracing::instrument(level = "debug", skip_all)]
114pub(crate) async fn check_send_policy(
115 services: &Services,
116 sender_user: &UserId,
117 event_type: &MessageLikeEventType,
118 content: &Raw<AnyMessageLikeEventContent>,
119) -> Result<Option<OwnedEventId>> {
120 if *event_type == MessageLikeEventType::RoomEncrypted && !services.config.encryption.allow {
122 return Err!(Request(Forbidden("Encryption has been disabled")));
123 }
124
125 let redaction_content = || {
130 content
131 .deserialize_as_unchecked::<RoomRedactionEventContent>()
132 .inspect_err(|_| {
133 debug_warn!(
134 %sender_user,
135 event = %content.json(),
136 "Client sent invalid redaction event"
137 );
138 })
139 .ok()
140 };
141
142 let redacts_id = event_type
143 .eq(&MessageLikeEventType::RoomRedaction)
144 .then(redaction_content)
145 .flatten()
146 .and_then(|content| content.redacts);
147
148 if *event_type == MessageLikeEventType::RoomRedaction
149 && services.config.rooms.redaction.disable_local
150 && !services.admin.user_is_admin(sender_user).await
151 {
152 warn!(
153 %sender_user,
154 ?redacts_id,
155 "Local redactions are disabled, non-admin user attempted to redact an event"
156 );
157
158 return Err!(Request(Forbidden("Redactions are disabled on this server.")));
159 }
160
161 if services.users.is_suspended(sender_user).await {
162 if *event_type != MessageLikeEventType::RoomRedaction {
163 return Err!(Request(UserSuspended(
164 "Cannot send non-redaction events while suspended."
165 )));
166 }
167
168 let is_self = match &redacts_id {
169 | None => false,
170 | Some(redacts_id) => is_self_redaction(services, sender_user, redacts_id).await,
171 };
172
173 if !is_self {
174 return Err!(Request(UserSuspended("Can only redact own events while suspended.")));
175 }
176 }
177
178 Ok(redacts_id)
179}
180
181#[tracing::instrument(level = "debug", skip_all)]
187pub(crate) async fn check_send_content(
188 services: &Services,
189 sender_user: &UserId,
190 room_id: &RoomId,
191 event_type: &MessageLikeEventType,
192 content: &Raw<AnyMessageLikeEventContent>,
193) -> Result {
194 try_join3(
195 check_duplicate_reaction(services, event_type, sender_user, content),
196 check_public_call_invite(services, event_type, room_id),
197 check_nested_thread(services, content),
198 )
199 .await
200 .map(|((), (), ())| ())
201}
202
203async fn check_public_call_invite(
204 services: &Services,
205 event_type: &MessageLikeEventType,
206 room_id: &RoomId,
207) -> Result {
208 if *event_type != MessageLikeEventType::CallInvite {
209 return Ok(());
210 }
211
212 if !services.directory.is_public_room(room_id).await {
213 return Ok(());
214 }
215
216 Err!(Request(Forbidden("Room call invites are not allowed in public rooms")))
217}
218
219async fn check_duplicate_reaction(
221 services: &Services,
222 event_type: &MessageLikeEventType,
223 sender_user: &UserId,
224 body: &Raw<AnyMessageLikeEventContent>,
225) -> Result {
226 if *event_type != MessageLikeEventType::Reaction {
227 return Ok(());
228 }
229
230 let Ok(content) = body.deserialize_as_unchecked::<ReactionEventContent>() else {
231 return Ok(());
232 };
233
234 if !services
235 .pdu_metadata
236 .event_has_relation(
237 &content.relates_to.event_id,
238 Some(sender_user),
239 None,
240 Some(&content.relates_to.key),
241 )
242 .await
243 {
244 return Ok(());
245 }
246
247 Err!(Request(DuplicateAnnotation("Duplicate reactions are not allowed.")))
248}
249
250async fn check_nested_thread(
253 services: &Services,
254 body: &Raw<AnyMessageLikeEventContent>,
255) -> Result {
256 let Ok(ExtractRelatesTo { relates_to: Relation::Thread(thread) }) =
257 body.deserialize_as_unchecked()
258 else {
259 return Ok(());
260 };
261
262 let Ok(root) = services.timeline.get_pdu(&thread.event_id).await else {
263 return Ok(());
264 };
265
266 let nested = root
267 .get_content()
268 .is_ok_and(|content: ExtractRelatesTo| content.relates_to.rel_type().is_some());
269
270 if !nested {
271 return Ok(());
272 }
273
274 Err!(Request(Unknown("Cannot start threads from an event with a relation.")))
275}
276
277async fn check_existing_txnid(
282 services: &Services,
283 txn: RoomTxn<'_>,
284 event_type: &str,
285) -> Result<Option<SendMessageResponse>> {
286 let scope = RoomScope::Send(event_type);
287 let response = services
288 .transaction_ids
289 .existing_room_txnid(txn, scope)
290 .await;
291
292 if let Some(response) = response.optional()? {
293 return txnid_response(&response).map(Some);
294 }
295
296 let response = services
297 .transaction_ids
298 .existing_txnid(txn.user_id, txn.device_id, txn.txn_id)
299 .await;
300
301 let Some(response) = response.optional()? else {
302 return Ok(None);
303 };
304
305 let Some(response) = legacy_txnid_response(&response)? else {
306 return Ok(None);
307 };
308
309 let event_id = &response.event_id;
310 let Some(pdu) = services
311 .timeline
312 .get_non_outlier_pdu(event_id)
313 .await
314 .optional()?
315 else {
316 return Ok(None);
317 };
318
319 if !legacy_txnid_matches(&pdu, txn.room_id, event_type, txn.user_id) {
320 return Ok(None);
321 }
322
323 services
324 .transaction_ids
325 .add_room_txnid(txn, scope, event_id.as_bytes());
326
327 Ok(Some(response))
328}
329
330fn txnid_response(response: &[u8]) -> Result<SendMessageResponse> {
331 txnid_event_id(response).map(SendMessageResponse::new)
332}
333
334fn legacy_txnid_response(response: &[u8]) -> Result<Option<SendMessageResponse>> {
335 if response.is_empty() {
336 return Ok(None);
337 }
338
339 txnid_response(response).map(Some)
340}
341
342fn legacy_txnid_matches(
343 pdu: &PduEvent,
344 room_id: &RoomId,
345 event_type: &str,
346 sender_user: &UserId,
347) -> bool {
348 pdu.room_id == room_id && pdu.kind.to_cow_str() == event_type && pdu.sender == sender_user
349}
350
351#[cfg(test)]
352mod tests {
353 use ruma::{event_id, room_id, user_id};
354 use serde_json::json;
355
356 use super::{PduEvent, legacy_txnid_matches, legacy_txnid_response};
357
358 #[test]
359 fn legacy_response_requires_a_valid_nonempty_event_id() {
360 assert!(
361 legacy_txnid_response(b"")
362 .expect("empty marker is valid")
363 .is_none()
364 );
365
366 legacy_txnid_response(b"not an event ID").expect_err("invalid event ID");
367 legacy_txnid_response(&[0xFF]).expect_err("invalid UTF-8");
368
369 let response = legacy_txnid_response(b"$event:example.com")
370 .expect("valid event ID")
371 .expect("nonempty response");
372
373 assert_eq!(response.event_id, event_id!("$event:example.com"));
374 }
375
376 #[test]
377 fn legacy_response_requires_matching_provenance() {
378 let pdu = pdu();
379 assert!(legacy_txnid_matches(
380 &pdu,
381 room_id!("!room:example.com"),
382 "m.room.message",
383 user_id!("@alice:example.com"),
384 ));
385
386 assert!(!legacy_txnid_matches(
387 &pdu,
388 room_id!("!other:example.com"),
389 "m.room.message",
390 user_id!("@alice:example.com"),
391 ));
392
393 assert!(!legacy_txnid_matches(
394 &pdu,
395 room_id!("!room:example.com"),
396 "m.room.encrypted",
397 user_id!("@alice:example.com"),
398 ));
399
400 assert!(!legacy_txnid_matches(
401 &pdu,
402 room_id!("!room:example.com"),
403 "m.room.message",
404 user_id!("@bob:example.com"),
405 ));
406 }
407
408 fn pdu() -> PduEvent {
409 serde_json::from_value(json!({
410 "type": "m.room.message",
411 "content": {},
412 "event_id": "$event:example.com",
413 "room_id": "!room:example.com",
414 "sender": "@alice:example.com",
415 "prev_events": ["$prev:example.com"],
416 "auth_events": ["$auth:example.com"],
417 "origin_server_ts": 1,
418 "depth": 1,
419 "hashes": { "sha256": "thishashcoversallfieldsincasethisisredacted" },
420 }))
421 .expect("valid PDU")
422 }
423}