Skip to main content

tuwunel_api/server/
invite.rs

1use 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/// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}`
29///
30/// Invites a remote user to a room.
31#[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	// ACL check origin
43	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	// Make sure we're not ACL'ed from their room.
123	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	// Generate event id
134	let event_id = gen_event_id(&signed_event, &body.room_version)?;
135
136	// Add event_id back
137	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 we are active in the room, the remote server will notify us about the
190	// join/invite through /send. If we are not in the room, we need to manually
191	// record the invited state for client /sync through update_membership(), and
192	// send the invite PDU to the relevant appservices.
193	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}