Skip to main content

tuwunel_service/membership/
invite.rs

1use futures::FutureExt;
2use ruma::{
3	OwnedServerName, RoomId, UserId,
4	api::{
5		error::ErrorKind,
6		federation::membership::{RawStrippedState, create_invite},
7	},
8	events::room::member::{MembershipState, RoomMemberEventContent},
9};
10use tuwunel_core::{
11	Err, Result, at, err, implement, matrix::event::gen_event_id_canonical_json, pdu::PduBuilder,
12};
13
14use super::Service;
15
16#[implement(Service)]
17#[tracing::instrument(
18    level = "debug",
19    skip_all,
20    fields(%sender_user, %room_id, %user_id)
21)]
22pub async fn invite(
23	&self,
24	sender_user: &UserId,
25	user_id: &UserId,
26	room_id: &RoomId,
27	reason: Option<&String>,
28	is_direct: bool,
29) -> Result {
30	if self.services.globals.user_is_local(user_id) {
31		self.local_invite(sender_user, user_id, room_id, reason, is_direct)
32			.boxed()
33			.await?;
34	} else {
35		self.remote_invite(sender_user, user_id, room_id, reason, is_direct)
36			.boxed()
37			.await?;
38	}
39
40	Ok(())
41}
42
43#[implement(Service)]
44#[tracing::instrument(name = "remote", level = "debug", skip_all)]
45async fn remote_invite(
46	&self,
47	sender_user: &UserId,
48	user_id: &UserId,
49	room_id: &RoomId,
50	reason: Option<&String>,
51	is_direct: bool,
52) -> Result {
53	let (pdu, pdu_json, invite_room_state, room_version_id) = {
54		let state_lock = self.services.state.mutex.lock(room_id).await;
55
56		let mut content = RoomMemberEventContent {
57			is_direct: Some(is_direct),
58			reason: reason.cloned(),
59			..RoomMemberEventContent::new(MembershipState::Invite)
60		};
61
62		self.services
63			.profile
64			.fill_profile_data(user_id, &mut content)
65			.await;
66
67		let (pdu, pdu_json) = self
68			.services
69			.timeline
70			.create_hash_and_sign_event(
71				PduBuilder::state(user_id.to_string(), &content),
72				sender_user,
73				room_id,
74				&state_lock,
75			)
76			.await?;
77
78		let room_version_id = self
79			.services
80			.state
81			.get_room_version(room_id)
82			.await?;
83
84		let invite_room_state = self
85			.services
86			.state
87			.summary_pdus(&pdu, &pdu_json, &room_version_id)
88			.await;
89
90		drop(state_lock);
91
92		(pdu, pdu_json, invite_room_state, room_version_id)
93	};
94
95	let response = self
96		.services
97		.federation
98		.execute(user_id.server_name(), create_invite::v2::Request {
99			room_id: room_id.to_owned(),
100			event_id: (*pdu.event_id).to_owned(),
101			room_version: room_version_id.clone(),
102			event: self
103				.services
104				.federation
105				.format_pdu_into(pdu_json.clone(), Some(&room_version_id))
106				.await,
107			invite_room_state: invite_room_state
108				.into_iter()
109				.map(RawStrippedState::Pdu)
110				.collect(),
111			via: self
112				.services
113				.state_cache
114				.servers_route_via(room_id)
115				.await
116				.ok(),
117		})
118		.await
119		.map_err(|e| match e.kind() {
120			| ErrorKind::IncompatibleRoomVersion { .. } | ErrorKind::UnsupportedRoomVersion =>
121				err!(Request(UnsupportedRoomVersion(
122					"Server {} does not support room version {room_version_id}.",
123					user_id.server_name(),
124				))),
125			// MSC4311: the remote rejected our well-formed invite over create-event
126			// validation; the client cannot make it succeed, so surface a 5xx.
127			| ErrorKind::MissingParam => err!(BadServerResponse(
128				"Remote server could not validate the invite's create event."
129			)),
130			| _ => e,
131		})?;
132
133	// We do not add the event_id field to the pdu here because of signature and
134	// hashes checks
135	let (event_id, value) = gen_event_id_canonical_json(&response.event, &room_version_id)
136		.map_err(|e| {
137			err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
138		})?;
139
140	if pdu.event_id != event_id {
141		return Err!(Request(BadJson(warn!(
142			%pdu.event_id, %event_id,
143			"Server {} sent event with wrong event ID",
144			user_id.server_name()
145		))));
146	}
147
148	let origin: OwnedServerName = serde_json::from_value(serde_json::to_value(
149		value
150			.get("origin")
151			.ok_or_else(|| err!(Request(BadJson("Event missing origin field."))))?,
152	)?)
153	.map_err(|e| {
154		err!(Request(BadJson(warn!("Origin field in event is not a valid server name: {e}"))))
155	})?;
156
157	let pdu_id = self
158		.services
159		.event_handler
160		.handle_incoming_pdu(&origin, room_id, &event_id, value, true)
161		.await?
162		.map(at!(0))
163		.ok_or_else(|| {
164			err!(Request(InvalidParam("Could not accept incoming PDU as timeline event.")))
165		})?;
166
167	self.services
168		.sending
169		.send_pdu_room(room_id, &pdu_id)
170		.await?;
171
172	Ok(())
173}
174
175#[implement(Service)]
176#[tracing::instrument(name = "local", level = "debug", skip_all)]
177async fn local_invite(
178	&self,
179	sender_user: &UserId,
180	user_id: &UserId,
181	room_id: &RoomId,
182	reason: Option<&String>,
183	is_direct: bool,
184) -> Result {
185	if self.services.users.invites_blocked(user_id).await {
186		return Err!(Request(InviteBlocked("{user_id} has blocked invites.")));
187	}
188
189	if !self
190		.services
191		.state_cache
192		.is_joined(sender_user, room_id)
193		.await
194	{
195		return Err!(Request(Forbidden(
196			"You must be joined in the room you are trying to invite from."
197		)));
198	}
199
200	let state_lock = self.services.state.mutex.lock(room_id).await;
201
202	let mut content = RoomMemberEventContent {
203		is_direct: Some(is_direct),
204		reason: reason.cloned(),
205		..RoomMemberEventContent::new(MembershipState::Invite)
206	};
207
208	self.services
209		.profile
210		.fill_profile_data(user_id, &mut content)
211		.await;
212
213	self.services
214		.timeline
215		.build_and_append_pdu(
216			PduBuilder::state(user_id.to_string(), &content),
217			sender_user,
218			room_id,
219			&state_lock,
220		)
221		.await?;
222
223	drop(state_lock);
224
225	Ok(())
226}