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