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 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
423fn is_leaveable(state: &MembershipState) -> bool {
427 matches!(state, MembershipState::Invite | MembershipState::Join | MembershipState::Knock,)
428}