Skip to main content

tuwunel_service/membership/
leave.rs

1use std::collections::HashSet;
2
3use futures::{FutureExt, StreamExt, TryFutureExt, future::ready, pin_mut};
4use ruma::{
5	CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, RoomId, UserId,
6	api::federation,
7	canonical_json::to_canonical_value,
8	events::{
9		AnyStrippedStateEvent, StateEventType,
10		room::member::{MembershipState, RoomMemberEventContent},
11	},
12	serde::Raw,
13};
14use tuwunel_core::{
15	Err, Error, Result, debug_info, debug_warn, err, implement,
16	matrix::{PduCount, pdu::check_rules, room_version},
17	pdu::PduBuilder,
18	utils::{self, FutureBoolExt, future::ReadyBoolExt},
19	warn,
20};
21
22use super::Service;
23use crate::rooms::timeline::RoomMutexGuard;
24
25#[implement(Service)]
26#[tracing::instrument(
27    level = "debug",
28    skip_all,
29    fields(%room_id, %user_id)
30)]
31pub async fn leave(
32	&self,
33	user_id: &UserId,
34	room_id: &RoomId,
35	reason: Option<String>,
36	remote_leave_now: bool,
37	state_lock: &RoomMutexGuard,
38) -> Result {
39	let leave_content = RoomMemberEventContent {
40		membership: MembershipState::Leave,
41		reason: reason.clone(),
42		join_authorized_via_users_server: None,
43		is_direct: None,
44		avatar_url: None,
45		displayname: None,
46		third_party_invite: None,
47		blurhash: None,
48	};
49
50	let is_banned = self.services.metadata.is_banned(room_id);
51	let is_disabled = self.services.metadata.is_disabled(room_id);
52	pin_mut!(is_banned, is_disabled);
53	if is_banned.or(is_disabled).await {
54		return self
55			.clear_local_leave(user_id, room_id, leave_content, None)
56			.await;
57	}
58
59	let member_event = self
60		.services
61		.state_accessor
62		.room_state_get_content::<RoomMemberEventContent>(
63			room_id,
64			&StateEventType::RoomMember,
65			user_id.as_str(),
66		)
67		.await;
68
69	let dont_have_room = self
70		.services
71		.state_cache
72		.server_in_room(self.services.globals.server_name(), room_id)
73		.is_false()
74		.and(ready(member_event.as_ref().is_err()));
75
76	let not_knocked = self
77		.services
78		.state_cache
79		.is_knocked(user_id, room_id)
80		.is_false();
81
82	if remote_leave_now || dont_have_room.and(not_knocked).await {
83		self.leave_via_remote(user_id, room_id, reason, leave_content)
84			.await
85	} else {
86		self.leave_locally(user_id, room_id, reason, leave_content, member_event, state_lock)
87			.await
88	}
89}
90
91#[implement(Service)]
92async fn leave_via_remote(
93	&self,
94	user_id: &UserId,
95	room_id: &RoomId,
96	reason: Option<String>,
97	leave_content: RoomMemberEventContent,
98) -> Result {
99	if let Err(e) = self
100		.remote_leave(user_id, room_id, reason)
101		.boxed()
102		.await
103	{
104		warn!(%user_id, "Failed to leave room {room_id} remotely: {e}");
105	}
106
107	let last_state = self
108		.last_known_strip_state(user_id, room_id)
109		.await;
110
111	self.clear_local_leave(user_id, room_id, leave_content, last_state)
112		.await
113}
114
115#[implement(Service)]
116async fn last_known_strip_state(
117	&self,
118	user_id: &UserId,
119	room_id: &RoomId,
120) -> Option<Vec<Raw<AnyStrippedStateEvent>>> {
121	self.services
122		.state_cache
123		.invite_state(user_id, room_id)
124		.or_else(|_| {
125			self.services
126				.state_cache
127				.knock_state(user_id, room_id)
128		})
129		.or_else(|_| {
130			self.services
131				.state_cache
132				.left_state(user_id, room_id)
133		})
134		.await
135		.ok()
136}
137
138#[implement(Service)]
139async fn leave_locally(
140	&self,
141	user_id: &UserId,
142	room_id: &RoomId,
143	reason: Option<String>,
144	leave_content: RoomMemberEventContent,
145	member_event: Result<RoomMemberEventContent>,
146	state_lock: &RoomMutexGuard,
147) -> Result {
148	let Ok(event) = member_event else {
149		debug_warn!(
150			"Trying to leave a room you are not a member of, marking room as left locally."
151		);
152
153		return self
154			.clear_local_leave(user_id, room_id, leave_content, None)
155			.await;
156	};
157
158	if !is_leaveable(&event.membership) {
159		debug_warn!(
160			current = ?event.membership,
161			"Room state shows non-leaveable membership; clearing local caches.",
162		);
163
164		return self
165			.clear_local_leave(user_id, room_id, leave_content, None)
166			.await;
167	}
168
169	let build_result = self
170		.services
171		.timeline
172		.build_and_append_pdu(
173			PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
174				membership: MembershipState::Leave,
175				reason,
176				join_authorized_via_users_server: None,
177				is_direct: None,
178				..event
179			}),
180			user_id,
181			room_id,
182			state_lock,
183		)
184		.await;
185
186	// On state-res auth-check rejection, re-read membership. The pre-check above
187	// and the auth_check inside build_and_append_pdu both run under state_lock,
188	// so they observe the same state; re-reading here narrows the swallow to
189	// non-leaveable membership (Leave/Ban/_Custom), which is the stale-state
190	// population this branch targets. Genuine auth_check rejections against
191	// fresh Invite/Join/Knock state propagate unchanged.
192	match build_result {
193		| Ok(_) => Ok(()),
194		| Err(Error::AuthCheck(inner)) => {
195			let current = self
196				.services
197				.state_accessor
198				.room_state_get_content::<RoomMemberEventContent>(
199					room_id,
200					&StateEventType::RoomMember,
201					user_id.as_str(),
202				)
203				.await
204				.map(|c| c.membership);
205
206			if current.as_ref().is_ok_and(is_leaveable) {
207				return Err(Error::AuthCheck(inner));
208			}
209
210			warn!(
211				error = %inner,
212				?current,
213				"Auth refused self-leave PDU; clearing local caches.",
214			);
215
216			self.clear_local_leave(user_id, room_id, leave_content, None)
217				.await
218		},
219		| Err(e) => Err(e),
220	}
221}
222
223#[implement(Service)]
224async fn clear_local_leave(
225	&self,
226	user_id: &UserId,
227	room_id: &RoomId,
228	leave_content: RoomMemberEventContent,
229	last_state: Option<Vec<Raw<AnyStrippedStateEvent>>>,
230) -> Result {
231	let count = self.services.globals.next_count();
232	self.services
233		.state_cache
234		.update_membership(
235			room_id,
236			user_id,
237			leave_content,
238			user_id,
239			last_state,
240			None,
241			true,
242			PduCount::Normal(*count),
243		)
244		.await
245}
246
247#[implement(Service)]
248#[tracing::instrument(name = "remote", level = "debug", skip_all)]
249async fn remote_leave(
250	&self,
251	user_id: &UserId,
252	room_id: &RoomId,
253	reason: Option<String>,
254) -> Result {
255	let mut make_leave_response_and_server =
256		Err!(BadServerResponse("No remote server available to assist in leaving {room_id}."));
257
258	let mut servers: HashSet<OwnedServerName> = self
259		.services
260		.state_cache
261		.servers_invite_via(room_id)
262		.chain(self.services.state_cache.room_servers(room_id))
263		.map(ToOwned::to_owned)
264		.collect()
265		.await;
266
267	match self
268		.services
269		.state_cache
270		.invite_state(user_id, room_id)
271		.await
272	{
273		| Ok(invite_state) => {
274			servers.extend(
275				invite_state
276					.iter()
277					.filter_map(|event| event.get_field("sender").ok().flatten())
278					.filter_map(|sender: &str| UserId::parse(sender).ok())
279					.map(|user| user.server_name().to_owned()),
280			);
281		},
282		| _ => {
283			match self
284				.services
285				.state_cache
286				.knock_state(user_id, room_id)
287				.await
288			{
289				| Ok(knock_state) => {
290					servers.extend(
291						knock_state
292							.iter()
293							.filter_map(|event| event.get_field("sender").ok().flatten())
294							.filter_map(|sender: &str| UserId::parse(sender).ok())
295							.filter_map(|sender| {
296								if !self.services.globals.user_is_local(&sender) {
297									Some(sender.server_name().to_owned())
298								} else {
299									None
300								}
301							}),
302					);
303				},
304				| _ => {},
305			}
306		},
307	}
308
309	servers.insert(user_id.server_name().to_owned());
310	if let Some(room_id_server_name) = room_id.server_name() {
311		servers.insert(room_id_server_name.to_owned());
312	}
313
314	debug_info!("servers in remote_leave_room: {servers:?}");
315
316	for remote_server in servers
317		.into_iter()
318		.filter(|server| !self.services.globals.server_is_ours(server))
319	{
320		let make_leave_response = self
321			.services
322			.federation
323			.execute(&remote_server, federation::membership::prepare_leave_event::v1::Request {
324				room_id: room_id.to_owned(),
325				user_id: user_id.to_owned(),
326			})
327			.await;
328
329		make_leave_response_and_server = make_leave_response.map(|r| (r, remote_server));
330
331		if make_leave_response_and_server.is_ok() {
332			break;
333		}
334	}
335
336	let (make_leave_response, remote_server) = make_leave_response_and_server?;
337
338	let Some(room_version_id) = make_leave_response.room_version else {
339		return Err!(BadServerResponse(warn!(
340			"No room version was returned by {remote_server} for {room_id}, room version is \
341			 likely not supported by tuwunel"
342		)));
343	};
344
345	if !self
346		.services
347		.config
348		.supported_room_version(&room_version_id)
349	{
350		return Err!(BadServerResponse(warn!(
351			"Remote room version {room_version_id} for {room_id} is not supported by conduwuit",
352		)));
353	}
354
355	let room_version_rules = room_version::rules(&room_version_id)?;
356
357	let mut event = serde_json::from_str::<CanonicalJsonObject>(make_leave_response.event.get())
358		.map_err(|e| {
359			err!(BadServerResponse(warn!(
360				"Invalid make_leave event json received from {remote_server} for {room_id}: \
361				 {e:?}"
362			)))
363		})?;
364
365	let mut content = RoomMemberEventContent {
366		reason,
367		..RoomMemberEventContent::new(MembershipState::Leave)
368	};
369
370	self.services
371		.profile
372		.fill_profile_data(user_id, &mut content)
373		.await;
374
375	event.insert("content".into(), to_canonical_value(content)?);
376
377	event.insert(
378		"origin".into(),
379		CanonicalJsonValue::String(
380			self.services
381				.globals
382				.server_name()
383				.as_str()
384				.to_owned(),
385		),
386	);
387
388	event.insert(
389		"origin_server_ts".into(),
390		CanonicalJsonValue::Integer(utils::millis_since_unix_epoch().try_into()?),
391	);
392
393	event.insert("room_id".into(), CanonicalJsonValue::String(room_id.as_str().into()));
394
395	event.insert("state_key".into(), CanonicalJsonValue::String(user_id.as_str().into()));
396
397	event.insert("sender".into(), CanonicalJsonValue::String(user_id.as_str().into()));
398
399	event.insert("type".into(), CanonicalJsonValue::String("m.room.member".into()));
400
401	let event_id = self
402		.services
403		.server_keys
404		.gen_id_hash_and_sign_event(&mut event, &room_version_id)?;
405
406	check_rules(&event, &room_version_rules.event_format)?;
407
408	self.services
409		.federation
410		.execute(&remote_server, federation::membership::create_leave_event::v2::Request {
411			room_id: room_id.to_owned(),
412			event_id,
413			pdu: self
414				.services
415				.federation
416				.format_pdu_into(event.clone(), Some(&room_version_id))
417				.await,
418		})
419		.await?;
420
421	Ok(())
422}
423
424/// Membership states permitted to transition to `Leave` via a self-leave PDU.
425/// ruma's `MembershipState` is `#[non_exhaustive]`; future variants are
426/// conservatively treated as non-leaveable.
427fn is_leaveable(state: &MembershipState) -> bool {
428	matches!(state, MembershipState::Invite | MembershipState::Join | MembershipState::Knock,)
429}