1use futures_util::future::join_all;
22use matrix_sdk_base::{RoomHero, RoomInfo, RoomState};
23use ruma::{
24 api::client::{membership::joined_members, state::get_state_events},
25 directory::PublicRoomJoinRule,
26 events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule},
27 room::RoomType,
28 space::SpaceRoomJoinRule,
29 OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName, RoomId, RoomOrAliasId, ServerName,
30};
31use tokio::try_join;
32use tracing::{instrument, warn};
33
34use crate::{room_directory_search::RoomDirectorySearch, Client, Room};
35
36#[derive(Debug, Clone)]
38pub struct RoomPreview {
39 pub room_id: OwnedRoomId,
44
45 pub canonical_alias: Option<OwnedRoomAliasId>,
47
48 pub name: Option<String>,
50
51 pub topic: Option<String>,
53
54 pub avatar_url: Option<OwnedMxcUri>,
56
57 pub num_joined_members: u64,
59
60 pub num_active_members: Option<u64>,
62
63 pub room_type: Option<RoomType>,
65
66 pub join_rule: SpaceRoomJoinRule,
68
69 pub is_world_readable: Option<bool>,
72
73 pub state: Option<RoomState>,
77
78 pub is_direct: Option<bool>,
80
81 pub heroes: Option<Vec<RoomHero>>,
83}
84
85impl RoomPreview {
86 fn from_room_info(
91 room_info: RoomInfo,
92 is_direct: Option<bool>,
93 num_joined_members: u64,
94 num_active_members: Option<u64>,
95 state: Option<RoomState>,
96 computed_display_name: Option<String>,
97 ) -> Self {
98 RoomPreview {
99 room_id: room_info.room_id().to_owned(),
100 canonical_alias: room_info.canonical_alias().map(ToOwned::to_owned),
101 name: computed_display_name.or_else(|| room_info.name().map(ToOwned::to_owned)),
102 topic: room_info.topic().map(ToOwned::to_owned),
103 avatar_url: room_info.avatar_url().map(ToOwned::to_owned),
104 room_type: room_info.room_type().cloned(),
105 join_rule: match room_info.join_rule() {
106 JoinRule::Invite => SpaceRoomJoinRule::Invite,
107 JoinRule::Knock => SpaceRoomJoinRule::Knock,
108 JoinRule::Private => SpaceRoomJoinRule::Private,
109 JoinRule::Restricted(_) => SpaceRoomJoinRule::Restricted,
110 JoinRule::KnockRestricted(_) => SpaceRoomJoinRule::KnockRestricted,
111 JoinRule::Public => SpaceRoomJoinRule::Public,
112 _ => {
113 SpaceRoomJoinRule::Private
116 }
117 },
118 is_world_readable: room_info
119 .history_visibility()
120 .map(|vis| *vis == HistoryVisibility::WorldReadable),
121 num_joined_members,
122 num_active_members,
123 state,
124 is_direct,
125 heroes: Some(room_info.heroes().to_vec()),
126 }
127 }
128
129 pub(crate) async fn from_known_room(room: &Room) -> Self {
135 let is_direct = room.is_direct().await.ok();
136
137 let display_name = room.display_name().await.ok().map(|name| name.to_string());
138
139 Self::from_room_info(
140 room.clone_info(),
141 is_direct,
142 room.joined_members_count(),
143 Some(room.active_members_count()),
144 Some(room.state()),
145 display_name,
146 )
147 }
148
149 #[instrument(skip(client))]
150 pub(crate) async fn from_remote_room(
151 client: &Client,
152 room_id: OwnedRoomId,
153 room_or_alias_id: &RoomOrAliasId,
154 via: Vec<OwnedServerName>,
155 ) -> crate::Result<Self> {
156 match Self::from_room_summary(client, room_id.clone(), room_or_alias_id, via.clone()).await
159 {
160 Ok(res) => return Ok(res),
161 Err(err) => {
162 warn!("error when previewing room from the room summary endpoint: {err}");
163 }
164 }
165
166 match Self::from_room_directory_search(client, &room_id, room_or_alias_id, via).await {
168 Ok(Some(res)) => return Ok(res),
169 Ok(None) => warn!("Room '{room_or_alias_id}' not found in room directory search."),
170 Err(err) => {
171 warn!("Searching for '{room_or_alias_id}' in room directory search failed: {err}");
172 }
173 }
174
175 Self::from_state_events(client, &room_id).await
177 }
178
179 pub(crate) async fn from_room_directory_search(
183 client: &Client,
184 room_id: &RoomId,
185 room_or_alias_id: &RoomOrAliasId,
186 via: Vec<OwnedServerName>,
187 ) -> crate::Result<Option<Self>> {
188 let search_term = if room_or_alias_id.is_room_alias_id() {
190 Some(room_or_alias_id.as_str()[1..].to_owned())
191 } else {
192 None
193 };
194
195 let batch_size = if search_term.is_some() { 20 } else { 100 };
198
199 if via.is_empty() {
200 search_for_room_preview_in_room_directory(
202 client.clone(),
203 search_term,
204 batch_size,
205 None,
206 room_id,
207 )
208 .await
209 } else {
210 let mut futures = Vec::new();
211 for server in via {
213 futures.push(search_for_room_preview_in_room_directory(
214 client.clone(),
215 search_term.clone(),
216 batch_size,
217 Some(server),
218 room_id,
219 ));
220 }
221
222 let joined_results = join_all(futures).await;
223
224 Ok(joined_results.into_iter().flatten().next().flatten())
225 }
226 }
227
228 pub async fn from_room_summary(
235 client: &Client,
236 room_id: OwnedRoomId,
237 room_or_alias_id: &RoomOrAliasId,
238 via: Vec<OwnedServerName>,
239 ) -> crate::Result<Self> {
240 let own_server_name = client.session_meta().map(|s| s.user_id.server_name());
241 let via = ensure_server_names_is_not_empty(own_server_name, via, room_or_alias_id);
242
243 let request = ruma::api::client::room::get_summary::msc3266::Request::new(
244 room_or_alias_id.to_owned(),
245 via,
246 );
247
248 let response = client.send(request).await?;
249
250 let cached_room = client.get_room(&room_id);
254 let state = if cached_room.is_none() {
255 None
256 } else {
257 response.membership.map(|membership| RoomState::from(&membership))
258 };
259
260 let num_active_members = cached_room.as_ref().map(|r| r.active_members_count());
261
262 let is_direct = if let Some(cached_room) = &cached_room {
263 cached_room.is_direct().await.ok()
264 } else {
265 None
266 };
267
268 Ok(RoomPreview {
269 room_id,
270 canonical_alias: response.canonical_alias,
271 name: response.name,
272 topic: response.topic,
273 avatar_url: response.avatar_url,
274 num_joined_members: response.num_joined_members.into(),
275 num_active_members,
276 room_type: response.room_type,
277 join_rule: response.join_rule,
278 is_world_readable: Some(response.world_readable),
279 state,
280 is_direct,
281 heroes: cached_room.map(|r| r.heroes()),
282 })
283 }
284
285 pub async fn from_state_events(client: &Client, room_id: &RoomId) -> crate::Result<Self> {
297 let state_request = get_state_events::v3::Request::new(room_id.to_owned());
298 let joined_members_request = joined_members::v3::Request::new(room_id.to_owned());
299
300 let (state, joined_members) =
301 try_join!(async { client.send(state_request).await }, async {
302 client.send(joined_members_request).await
303 })?;
304
305 let num_joined_members = joined_members.joined.len().try_into().unwrap_or(u64::MAX);
308
309 let mut room_info = RoomInfo::new(room_id, RoomState::Joined);
310
311 for ev in state.room_state {
312 let ev = match ev.deserialize() {
313 Ok(ev) => ev,
314 Err(err) => {
315 warn!("failed to deserialize state event: {err}");
316 continue;
317 }
318 };
319 room_info.handle_state_event(&ev.into());
320 }
321
322 let room = client.get_room(room_id);
323 let state = room.as_ref().map(|room| room.state());
324 let num_active_members = room.as_ref().map(|r| r.active_members_count());
325 let is_direct = if let Some(room) = room { room.is_direct().await.ok() } else { None };
326
327 Ok(Self::from_room_info(
328 room_info,
329 is_direct,
330 num_joined_members,
331 num_active_members,
332 state,
333 None,
334 ))
335 }
336}
337
338async fn search_for_room_preview_in_room_directory(
339 client: Client,
340 filter: Option<String>,
341 batch_size: u32,
342 server: Option<OwnedServerName>,
343 expected_room_id: &RoomId,
344) -> crate::Result<Option<RoomPreview>> {
345 let mut directory_search = RoomDirectorySearch::new(client);
346 directory_search.search(filter, batch_size, server).await?;
347
348 let (results, _) = directory_search.results();
349
350 for room_description in results {
351 if room_description.room_id != expected_room_id {
353 continue;
354 }
355 return Ok(Some(RoomPreview {
356 room_id: room_description.room_id,
357 canonical_alias: room_description.alias,
358 name: room_description.name,
359 topic: room_description.topic,
360 avatar_url: room_description.avatar_url,
361 num_joined_members: room_description.joined_members,
362 num_active_members: None,
363 room_type: None,
365 join_rule: match room_description.join_rule {
366 PublicRoomJoinRule::Public => SpaceRoomJoinRule::Public,
367 PublicRoomJoinRule::Knock => SpaceRoomJoinRule::Knock,
368 PublicRoomJoinRule::_Custom(rule) => SpaceRoomJoinRule::_Custom(rule),
369 _ => {
370 panic!("Unexpected PublicRoomJoinRule {:?}", room_description.join_rule)
371 }
372 },
373 is_world_readable: Some(room_description.is_world_readable),
374 state: None,
375 is_direct: None,
376 heroes: None,
377 }));
378 }
379
380 Ok(None)
381}
382
383fn ensure_server_names_is_not_empty(
386 own_server_name: Option<&ServerName>,
387 server_names: Vec<OwnedServerName>,
388 room_or_alias_id: &RoomOrAliasId,
389) -> Vec<OwnedServerName> {
390 let mut server_names = server_names;
391
392 if let Some((own_server, alias_server)) = own_server_name.zip(room_or_alias_id.server_name()) {
393 if server_names.is_empty() && own_server != alias_server {
394 server_names.push(alias_server.to_owned());
395 }
396 }
397
398 server_names
399}
400
401#[cfg(test)]
402mod tests {
403 use ruma::{owned_server_name, room_alias_id, room_id, server_name, RoomOrAliasId, ServerName};
404
405 use crate::room_preview::ensure_server_names_is_not_empty;
406
407 #[test]
408 fn test_ensure_server_names_is_not_empty_when_no_own_server_name_is_provided() {
409 let own_server_name: Option<&ServerName> = None;
410 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").into();
411
412 let server_names =
413 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
414
415 assert!(server_names.is_empty());
418 }
419
420 #[test]
421 fn test_ensure_server_names_is_not_empty_when_room_alias_or_id_has_no_server_name() {
422 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
423 let room_or_alias_id: &RoomOrAliasId = room_id!("!test").into();
424
425 let server_names =
426 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
427
428 assert!(server_names.is_empty());
430 }
431
432 #[test]
433 fn test_ensure_server_names_is_not_empty_with_same_server_name() {
434 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
435 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").into();
436
437 let server_names =
438 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
439
440 assert!(server_names.is_empty());
443 }
444
445 #[test]
446 fn test_ensure_server_names_is_not_empty_with_different_room_id_server_name() {
447 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
448 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:matrix.org").into();
449
450 let server_names =
451 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
452
453 assert!(!server_names.is_empty());
455 assert_eq!(server_names[0], owned_server_name!("matrix.org"));
456 }
457
458 #[test]
459 fn test_ensure_server_names_is_not_empty_with_different_room_alias_server_name() {
460 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
461 let room_or_alias_id: &RoomOrAliasId = room_alias_id!("#test:matrix.org").into();
462
463 let server_names =
464 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
465
466 assert!(!server_names.is_empty());
468 assert_eq!(server_names[0], owned_server_name!("matrix.org"));
469 }
470}