tuwunel_api/server/
invite.rs1use axum::extract::State;
2use base64::{Engine as _, engine::general_purpose};
3use futures::StreamExt;
4use ruma::{
5 CanonicalJsonValue, OwnedRoomId, OwnedUserId, RoomId, UserId,
6 api::{
7 appservice::event::push_events,
8 error::{ErrorKind, IncompatibleRoomVersionErrorData},
9 federation::membership::{RawStrippedState, create_invite},
10 },
11 events::{
12 GlobalAccountDataEventType, StateEventType,
13 push_rules::PushRulesEvent,
14 room::member::{MembershipState, RoomMemberEventContent},
15 },
16 push,
17 serde::JsonObject,
18};
19use tuwunel_core::{
20 Err, Error, Result, err, extract_variant,
21 matrix::{Event, PduCount, PduEvent, event::gen_event_id},
22 utils,
23 utils::hash::sha256,
24};
25
26use crate::{ClientIp, Ruma};
27
28#[tracing::instrument(skip_all, fields(%client), name = "invite")]
32#[expect(
33 deprecated,
34 reason = "Matrix 1.16 still permits receiving the legacy stripped variant for backwards \
35 compatibility."
36)]
37pub(crate) async fn create_invite_route(
38 State(services): State<crate::State>,
39 ClientIp(client): ClientIp,
40 body: Ruma<create_invite::v2::Request>,
41) -> Result<create_invite::v2::Response> {
42 services
44 .event_handler
45 .acl_check(body.origin(), &body.room_id)
46 .await?;
47
48 if !services
49 .config
50 .supported_room_version(&body.room_version)
51 {
52 return Err(Error::BadRequest(
53 ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData::new(
54 body.room_version.clone(),
55 )),
56 "Server does not support this room version.",
57 ));
58 }
59
60 if let Some(server) = body.room_id.server_name()
61 && services
62 .config
63 .is_forbidden_remote_server_name(server)
64 {
65 return Err!(Request(Forbidden("Server is banned on this homeserver.")));
66 }
67
68 let mut signed_event = utils::to_canonical_object(&body.event)
69 .map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?;
70
71 let room_id: OwnedRoomId = signed_event
72 .get("room_id")
73 .try_into()
74 .map(RoomId::to_owned)
75 .map_err(|e| err!(Request(InvalidParam("Invalid room_id property: {e}"))))?;
76
77 if body.room_id != room_id {
78 return Err!(Request(InvalidParam("Event room_id does not match the request path.")));
79 }
80
81 let kind: StateEventType = signed_event
82 .get("type")
83 .and_then(CanonicalJsonValue::as_str)
84 .ok_or_else(|| err!(Request(BadJson("Missing type in event."))))?
85 .into();
86
87 if kind != StateEventType::RoomMember {
88 return Err!(Request(InvalidParam("Event must be m.room.member type.")));
89 }
90
91 let invited_user: OwnedUserId = signed_event
92 .get("state_key")
93 .try_into()
94 .map(UserId::to_owned)
95 .map_err(|e| err!(Request(InvalidParam("Invalid state_key property: {e}"))))?;
96
97 if !services.globals.user_is_local(&invited_user) {
98 return Err!(Request(InvalidParam("User does not belong to this homeserver.")));
99 }
100
101 if services
102 .users
103 .invites_blocked(&invited_user)
104 .await
105 {
106 return Err!(Request(InviteBlocked("{invited_user} has blocked invites.")));
107 }
108
109 let content: RoomMemberEventContent = signed_event
110 .get("content")
111 .cloned()
112 .map(Into::into)
113 .map(serde_json::from_value)
114 .transpose()
115 .map_err(|e| err!(Request(InvalidParam("Invalid content object in event: {e}"))))?
116 .ok_or_else(|| err!(Request(BadJson("Missing content in event."))))?;
117
118 if content.membership != MembershipState::Invite {
119 return Err!(Request(InvalidParam("Event membership must be invite.")));
120 }
121
122 services
124 .event_handler
125 .acl_check(invited_user.server_name(), &body.room_id)
126 .await?;
127
128 services
129 .server_keys
130 .hash_and_sign_event(&mut signed_event, &body.room_version)
131 .map_err(|e| err!(Request(InvalidParam("Failed to sign event: {e}"))))?;
132
133 let event_id = gen_event_id(&signed_event, &body.room_version)?;
135
136 signed_event.insert("event_id".into(), CanonicalJsonValue::String(event_id.to_string()));
138
139 let origin: Option<&str> = signed_event
140 .get("origin")
141 .and_then(CanonicalJsonValue::as_str);
142
143 let sender: &UserId = signed_event
144 .get("sender")
145 .try_into()
146 .map_err(|e| err!(Request(InvalidParam("Invalid sender property: {e}"))))?;
147
148 if sender.server_name() != body.origin() {
149 return Err!(Request(Forbidden("Can only send invites on behalf of your users.")));
150 }
151
152 if origin.is_some_and(|origin| origin != sender.server_name()) {
153 return Err!(Request(Forbidden("Your users can only be from your origin.")));
154 }
155
156 if origin.is_some_and(|origin| origin != body.origin()) {
157 return Err!(Request(Forbidden("Can only send events from your origin.")));
158 }
159
160 if services.metadata.is_banned(&body.room_id).await
161 && !services.admin.user_is_admin(&invited_user).await
162 {
163 return Err!(Request(Forbidden("This room is banned on this homeserver.")));
164 }
165
166 if services.config.block_non_admin_invites
167 && !services.admin.user_is_admin(&invited_user).await
168 {
169 return Err!(Request(Forbidden("This server does not allow room invites.")));
170 }
171
172 let mut invite_state: Vec<_> = body
173 .invite_room_state
174 .clone()
175 .into_iter()
176 .filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
177 .collect();
178
179 let mut event: JsonObject = serde_json::from_str(body.event.get())
180 .map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
181
182 event.insert("event_id".into(), "$placeholder".into());
183
184 let pdu: PduEvent = serde_json::from_value(event.into())
185 .map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
186
187 invite_state.push(pdu.to_format());
188
189 if !services
194 .state_cache
195 .server_in_room(services.globals.server_name(), &body.room_id)
196 .await
197 {
198 let count = services.globals.next_count();
199 services
200 .state_cache
201 .update_membership(
202 &body.room_id,
203 &invited_user,
204 RoomMemberEventContent::new(MembershipState::Invite),
205 sender,
206 Some(invite_state),
207 body.via.clone(),
208 true,
209 PduCount::Normal(*count),
210 )
211 .await?;
212 drop(count);
213
214 services
215 .pusher
216 .get_pushkeys(&invited_user)
217 .map(ToOwned::to_owned)
218 .for_each(async |pushkey| {
219 let Ok(pusher) = services
220 .pusher
221 .get_pusher(&invited_user, &pushkey)
222 .await
223 else {
224 return;
225 };
226
227 let ruleset = services
228 .account_data
229 .get_global(&invited_user, GlobalAccountDataEventType::PushRules)
230 .await
231 .map_or_else(
232 |_| push::Ruleset::server_default(&invited_user),
233 |ev: PushRulesEvent| ev.content.global,
234 );
235
236 services
237 .pusher
238 .send_push_notice(&invited_user, &pusher, &ruleset, &pdu)
239 .await
240 .ok();
241 })
242 .await;
243
244 for appservice in services.appservice.read().await.values() {
245 if appservice.is_user_match(&invited_user) {
246 services
247 .appservice
248 .send_request(appservice.registration.clone(), push_events::v1::Request {
249 events: vec![pdu.to_format()],
250 txn_id: general_purpose::URL_SAFE_NO_PAD
251 .encode(sha256::hash(pdu.event_id.as_bytes()))
252 .into(),
253 ephemeral: Vec::new(),
254 to_device: Vec::new(),
255 })
256 .await?;
257 }
258 }
259 }
260
261 Ok(create_invite::v2::Response {
262 event: services
263 .federation
264 .format_pdu_into(signed_event, Some(&body.room_version))
265 .await,
266 })
267}