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, Error, 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 match Self::from_state_events(client, &room_id).await {
177 Ok(res) => return Ok(res),
178 Err(err) => {
179 warn!("error when building room preview from state events: {err}");
180 }
181 }
182
183 if let Some(room) = client.get_room(&room_id) {
186 Ok(Self::from_known_room(&room).await)
187 } else {
188 Err(Error::InsufficientData)
189 }
190 }
191
192 pub(crate) async fn from_room_directory_search(
196 client: &Client,
197 room_id: &RoomId,
198 room_or_alias_id: &RoomOrAliasId,
199 via: Vec<OwnedServerName>,
200 ) -> crate::Result<Option<Self>> {
201 let search_term = if room_or_alias_id.is_room_alias_id() {
203 Some(room_or_alias_id.as_str()[1..].to_owned())
204 } else {
205 None
206 };
207
208 let batch_size = if search_term.is_some() { 20 } else { 100 };
211
212 if via.is_empty() {
213 search_for_room_preview_in_room_directory(
215 client.clone(),
216 search_term,
217 batch_size,
218 None,
219 room_id,
220 )
221 .await
222 } else {
223 let mut futures = Vec::new();
224 for server in via {
226 futures.push(search_for_room_preview_in_room_directory(
227 client.clone(),
228 search_term.clone(),
229 batch_size,
230 Some(server),
231 room_id,
232 ));
233 }
234
235 let joined_results = join_all(futures).await;
236
237 Ok(joined_results.into_iter().flatten().next().flatten())
238 }
239 }
240
241 pub async fn from_room_summary(
248 client: &Client,
249 room_id: OwnedRoomId,
250 room_or_alias_id: &RoomOrAliasId,
251 via: Vec<OwnedServerName>,
252 ) -> crate::Result<Self> {
253 let own_server_name = client.session_meta().map(|s| s.user_id.server_name());
254 let via = ensure_server_names_is_not_empty(own_server_name, via, room_or_alias_id);
255
256 let request = ruma::api::client::room::get_summary::msc3266::Request::new(
257 room_or_alias_id.to_owned(),
258 via,
259 );
260
261 let response = client.send(request).await?;
262
263 let cached_room = client.get_room(&room_id);
267 let state = if cached_room.is_none() {
268 None
269 } else {
270 response.membership.map(|membership| RoomState::from(&membership))
271 };
272
273 let num_active_members = cached_room.as_ref().map(|r| r.active_members_count());
274
275 let is_direct = if let Some(cached_room) = &cached_room {
276 cached_room.is_direct().await.ok()
277 } else {
278 None
279 };
280
281 Ok(RoomPreview {
282 room_id,
283 canonical_alias: response.canonical_alias,
284 name: response.name,
285 topic: response.topic,
286 avatar_url: response.avatar_url,
287 num_joined_members: response.num_joined_members.into(),
288 num_active_members,
289 room_type: response.room_type,
290 join_rule: response.join_rule,
291 is_world_readable: Some(response.world_readable),
292 state,
293 is_direct,
294 heroes: cached_room.map(|r| r.heroes()),
295 })
296 }
297
298 pub async fn from_state_events(client: &Client, room_id: &RoomId) -> crate::Result<Self> {
310 let state_request = get_state_events::v3::Request::new(room_id.to_owned());
311 let joined_members_request = joined_members::v3::Request::new(room_id.to_owned());
312
313 let (state, joined_members) =
314 try_join!(async { client.send(state_request).await }, async {
315 client.send(joined_members_request).await
316 })?;
317
318 let num_joined_members = joined_members.joined.len().try_into().unwrap_or(u64::MAX);
321
322 let mut room_info = RoomInfo::new(room_id, RoomState::Joined);
323
324 for ev in state.room_state {
325 let ev = match ev.deserialize() {
326 Ok(ev) => ev,
327 Err(err) => {
328 warn!("failed to deserialize state event: {err}");
329 continue;
330 }
331 };
332 room_info.handle_state_event(&ev.into());
333 }
334
335 let room = client.get_room(room_id);
336 let state = room.as_ref().map(|room| room.state());
337 let num_active_members = room.as_ref().map(|r| r.active_members_count());
338 let is_direct = if let Some(room) = room { room.is_direct().await.ok() } else { None };
339
340 Ok(Self::from_room_info(
341 room_info,
342 is_direct,
343 num_joined_members,
344 num_active_members,
345 state,
346 None,
347 ))
348 }
349}
350
351async fn search_for_room_preview_in_room_directory(
352 client: Client,
353 filter: Option<String>,
354 batch_size: u32,
355 server: Option<OwnedServerName>,
356 expected_room_id: &RoomId,
357) -> crate::Result<Option<RoomPreview>> {
358 let mut directory_search = RoomDirectorySearch::new(client);
359 directory_search.search(filter, batch_size, server).await?;
360
361 let (results, _) = directory_search.results();
362
363 for room_description in results {
364 if room_description.room_id != expected_room_id {
366 continue;
367 }
368 return Ok(Some(RoomPreview {
369 room_id: room_description.room_id,
370 canonical_alias: room_description.alias,
371 name: room_description.name,
372 topic: room_description.topic,
373 avatar_url: room_description.avatar_url,
374 num_joined_members: room_description.joined_members,
375 num_active_members: None,
376 room_type: None,
378 join_rule: match room_description.join_rule {
379 PublicRoomJoinRule::Public => SpaceRoomJoinRule::Public,
380 PublicRoomJoinRule::Knock => SpaceRoomJoinRule::Knock,
381 PublicRoomJoinRule::_Custom(rule) => SpaceRoomJoinRule::_Custom(rule),
382 _ => {
383 panic!("Unexpected PublicRoomJoinRule {:?}", room_description.join_rule)
384 }
385 },
386 is_world_readable: Some(room_description.is_world_readable),
387 state: None,
388 is_direct: None,
389 heroes: None,
390 }));
391 }
392
393 Ok(None)
394}
395
396fn ensure_server_names_is_not_empty(
399 own_server_name: Option<&ServerName>,
400 server_names: Vec<OwnedServerName>,
401 room_or_alias_id: &RoomOrAliasId,
402) -> Vec<OwnedServerName> {
403 let mut server_names = server_names;
404
405 if let Some((own_server, alias_server)) = own_server_name.zip(room_or_alias_id.server_name()) {
406 if server_names.is_empty() && own_server != alias_server {
407 server_names.push(alias_server.to_owned());
408 }
409 }
410
411 server_names
412}
413
414#[cfg(test)]
415mod tests {
416 use ruma::{owned_server_name, room_alias_id, room_id, server_name, RoomOrAliasId, ServerName};
417
418 use crate::room_preview::ensure_server_names_is_not_empty;
419
420 #[test]
421 fn test_ensure_server_names_is_not_empty_when_no_own_server_name_is_provided() {
422 let own_server_name: Option<&ServerName> = None;
423 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").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());
431 }
432
433 #[test]
434 fn test_ensure_server_names_is_not_empty_when_room_alias_or_id_has_no_server_name() {
435 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
436 let room_or_alias_id: &RoomOrAliasId = room_id!("!test").into();
437
438 let server_names =
439 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
440
441 assert!(server_names.is_empty());
443 }
444
445 #[test]
446 fn test_ensure_server_names_is_not_empty_with_same_server_name() {
447 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
448 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").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());
456 }
457
458 #[test]
459 fn test_ensure_server_names_is_not_empty_with_different_room_id_server_name() {
460 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
461 let room_or_alias_id: &RoomOrAliasId = room_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
471 #[test]
472 fn test_ensure_server_names_is_not_empty_with_different_room_alias_server_name() {
473 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
474 let room_or_alias_id: &RoomOrAliasId = room_alias_id!("#test:matrix.org").into();
475
476 let server_names =
477 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
478
479 assert!(!server_names.is_empty());
481 assert_eq!(server_names[0], owned_server_name!("matrix.org"));
482 }
483}