tuwunel_api/client/
unstable.rs1use axum::extract::State;
2use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD as b64};
3use futures::StreamExt;
4use ruma::{OwnedRoomId, UInt, api::client::membership::mutual_rooms};
5use tuwunel_core::{Err, Result, err};
6
7use crate::{ClientIp, Ruma};
8
9const PAGE_SIZE: usize = 1000;
11
12#[tracing::instrument(skip_all, fields(%client), name = "mutual_rooms")]
19pub(crate) async fn get_mutual_rooms_route(
20 State(services): State<crate::State>,
21 ClientIp(client): ClientIp,
22 body: Ruma<mutual_rooms::v1::Request>,
23) -> Result<mutual_rooms::v1::Response> {
24 let sender_user = body.sender_user();
25
26 if sender_user == body.user_id {
27 return Err!(Request(InvalidParam("You cannot request rooms in common with yourself.")));
28 }
29
30 if body.user_id.validate_historical().is_err() {
31 return Err!(Request(InvalidParam("The user_id is not a compliant user identifier.")));
32 }
33
34 let all: Vec<OwnedRoomId> = services
35 .state_cache
36 .get_shared_rooms(sender_user, &body.user_id)
37 .map(ToOwned::to_owned)
38 .collect()
39 .await;
40
41 let count = UInt::try_from(all.len()).unwrap_or(UInt::MAX);
42
43 let start = match body.from.as_deref() {
44 | None => 0,
45 | Some(token) => {
46 let cursor = decode_cursor(token)
47 .ok_or_else(|| err!(Request(InvalidParam("Invalid `from` token."))))?;
48
49 all.partition_point(|room_id| room_id.as_str() <= cursor.as_str())
50 },
51 };
52
53 let end = start.saturating_add(PAGE_SIZE).min(all.len());
54 let next_batch = (end < all.len()).then(|| b64.encode(all[end.saturating_sub(1)].as_str()));
55
56 let joined = if start == 0 && end == all.len() {
57 all
58 } else {
59 all[start..end].to_vec()
60 };
61
62 Ok(mutual_rooms::v1::Response { joined, count, next_batch })
63}
64
65fn decode_cursor(token: &str) -> Option<OwnedRoomId> {
67 let bytes = b64.decode(token).ok()?;
68 let room_id = str::from_utf8(&bytes).ok()?;
69
70 OwnedRoomId::parse(room_id).ok()
71}