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