tuwunel_api/client/
send.rs1use 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
33pub(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 if body.event_type == MessageLikeEventType::RoomEncrypted && !services.config.allow_encryption
52 {
53 return Err!(Request(Forbidden("Encryption has been disabled")));
54 }
55
56 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
176async 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
207async 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
234async 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 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}