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