Skip to main content

tuwunel_service/membership/
leave.rs

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