1use futures_util::future::join_all;
22use matrix_sdk_base::{RawStateEventWithKeys, RoomHeroWithProfile, RoomInfo, RoomState};
23use ruma::{
24 OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName, RoomId, RoomOrAliasId, ServerName,
25 api::client::{membership::joined_members, state::get_state_events},
26 events::room::history_visibility::HistoryVisibility,
27 room::{JoinRuleSummary, RoomType},
28};
29use tokio::try_join;
30use tracing::{instrument, warn};
31
32use crate::{Client, Error, Room, room_directory_search::RoomDirectorySearch};
33
34#[derive(Debug, Clone)]
36pub struct RoomPreview {
37 pub room_id: OwnedRoomId,
42
43 pub canonical_alias: Option<OwnedRoomAliasId>,
45
46 pub name: Option<String>,
48
49 pub topic: Option<String>,
51
52 pub avatar_url: Option<OwnedMxcUri>,
54
55 pub num_joined_members: u64,
57
58 pub num_active_members: Option<u64>,
60
61 pub room_type: Option<RoomType>,
63
64 pub join_rule: Option<JoinRuleSummary>,
66
67 pub is_world_readable: Option<bool>,
70
71 pub state: Option<RoomState>,
75
76 pub is_direct: Option<bool>,
78
79 pub heroes: Option<Vec<RoomHeroWithProfile>>,
81}
82
83impl RoomPreview {
84 fn from_room_info(
89 room_info: RoomInfo,
90 is_direct: Option<bool>,
91 num_joined_members: u64,
92 num_active_members: Option<u64>,
93 state: Option<RoomState>,
94 computed_display_name: Option<String>,
95 ) -> Self {
96 RoomPreview {
97 room_id: room_info.room_id().to_owned(),
98 canonical_alias: room_info.canonical_alias().map(ToOwned::to_owned),
99 name: computed_display_name.or_else(|| room_info.name().map(ToOwned::to_owned)),
100 topic: room_info.topic().map(ToOwned::to_owned),
101 avatar_url: room_info.avatar_url().map(ToOwned::to_owned),
102 room_type: room_info.room_type().cloned(),
103 join_rule: room_info.join_rule().cloned().map(Into::into),
104 is_world_readable: room_info
105 .history_visibility()
106 .map(|vis| *vis == HistoryVisibility::WorldReadable),
107 num_joined_members,
108 num_active_members,
109 state,
110 is_direct,
111 heroes: Some(
112 room_info.heroes().iter().cloned().map(RoomHeroWithProfile::from).collect(),
113 ),
114 }
115 }
116
117 pub(crate) async fn from_known_room(room: &Room) -> Self {
123 let is_direct = room.is_direct().await.ok();
124
125 let display_name = room.display_name().await.ok().map(|name| name.to_string());
126
127 Self::from_room_info(
128 room.clone_info(),
129 is_direct,
130 room.joined_members_count(),
131 Some(room.active_members_count()),
132 Some(room.state()),
133 display_name,
134 )
135 }
136
137 #[instrument(skip(client))]
138 pub(crate) async fn from_remote_room(
139 client: &Client,
140 room_id: OwnedRoomId,
141 room_or_alias_id: &RoomOrAliasId,
142 via: Vec<OwnedServerName>,
143 ) -> crate::Result<Self> {
144 match Self::from_room_summary(client, room_id.clone(), room_or_alias_id, via.clone()).await
147 {
148 Ok(res) => return Ok(res),
149 Err(err) => {
150 warn!("error when previewing room from the room summary endpoint: {err}");
151 }
152 }
153
154 match Self::from_room_directory_search(client, &room_id, room_or_alias_id, via).await {
156 Ok(Some(res)) => return Ok(res),
157 Ok(None) => warn!("Room '{room_or_alias_id}' not found in room directory search."),
158 Err(err) => {
159 warn!("Searching for '{room_or_alias_id}' in room directory search failed: {err}");
160 }
161 }
162
163 match Self::from_state_events(client, &room_id).await {
165 Ok(res) => return Ok(res),
166 Err(err) => {
167 warn!("error when building room preview from state events: {err}");
168 }
169 }
170
171 if let Some(room) = client.get_room(&room_id) {
174 Ok(Self::from_known_room(&room).await)
175 } else {
176 Err(Error::InsufficientData)
177 }
178 }
179
180 pub(crate) async fn from_room_directory_search(
184 client: &Client,
185 room_id: &RoomId,
186 room_or_alias_id: &RoomOrAliasId,
187 via: Vec<OwnedServerName>,
188 ) -> crate::Result<Option<Self>> {
189 let search_term = if room_or_alias_id.is_room_alias_id() {
191 Some(room_or_alias_id.as_str()[1..].to_owned())
192 } else {
193 None
194 };
195
196 let batch_size = if search_term.is_some() { 20 } else { 100 };
199
200 if via.is_empty() {
201 search_for_room_preview_in_room_directory(
203 client.clone(),
204 search_term,
205 batch_size,
206 None,
207 room_id,
208 )
209 .await
210 } else {
211 let mut futures = Vec::new();
212 for server in via {
214 futures.push(search_for_room_preview_in_room_directory(
215 client.clone(),
216 search_term.clone(),
217 batch_size,
218 Some(server),
219 room_id,
220 ));
221 }
222
223 let joined_results = join_all(futures).await;
224
225 Ok(joined_results.into_iter().flatten().next().flatten())
226 }
227 }
228
229 pub async fn from_room_summary(
236 client: &Client,
237 room_id: OwnedRoomId,
238 room_or_alias_id: &RoomOrAliasId,
239 via: Vec<OwnedServerName>,
240 ) -> crate::Result<Self> {
241 let own_server_name = client.session_meta().map(|s| s.user_id.server_name());
242 let via = ensure_server_names_is_not_empty(own_server_name, via, room_or_alias_id);
243
244 let request = ruma::api::client::room::get_summary::v1::Request::new(
245 room_or_alias_id.to_owned(),
246 via,
247 );
248
249 let response = client.send(request).await?;
250
251 let cached_room = client.get_room(&room_id);
255 let state = if cached_room.is_none() {
256 None
257 } else {
258 response.membership.map(|membership| RoomState::from(&membership))
259 };
260
261 let num_active_members = cached_room.as_ref().map(|r| r.active_members_count());
262
263 let is_direct = if let Some(cached_room) = &cached_room {
264 cached_room.is_direct().await.ok()
265 } else {
266 None
267 };
268
269 let heroes = if let Some(cached_room) = &cached_room {
270 Some(cached_room.heroes().await)
271 } else {
272 None
273 };
274
275 let summary = response.summary;
276
277 Ok(RoomPreview {
278 room_id,
279 canonical_alias: summary.canonical_alias,
280 name: summary.name,
281 topic: summary.topic,
282 avatar_url: summary.avatar_url,
283 num_joined_members: summary.num_joined_members.into(),
284 num_active_members,
285 room_type: summary.room_type,
286 join_rule: Some(summary.join_rule),
287 is_world_readable: Some(summary.world_readable),
288 state,
289 is_direct,
290 heroes,
291 })
292 }
293
294 pub async fn from_state_events(client: &Client, room_id: &RoomId) -> crate::Result<Self> {
306 let state_request = get_state_events::v3::Request::new(room_id.to_owned());
307 let joined_members_request = joined_members::v3::Request::new(room_id.to_owned());
308
309 let (state, joined_members) =
310 try_join!(async { client.send(state_request).await }, async {
311 client.send(joined_members_request).await
312 })?;
313
314 let num_joined_members = joined_members.joined.len().try_into().unwrap_or(u64::MAX);
317
318 let mut room_info = RoomInfo::new(room_id, RoomState::Joined);
319
320 for ev in state.room_state {
321 if let Some(mut raw_event) = RawStateEventWithKeys::try_from_raw_state_event(ev.cast())
322 {
323 room_info.handle_state_event(&mut raw_event);
324 }
325 }
326
327 let room = client.get_room(room_id);
328 let state = room.as_ref().map(|room| room.state());
329 let num_active_members = room.as_ref().map(|r| r.active_members_count());
330 let is_direct = if let Some(room) = room { room.is_direct().await.ok() } else { None };
331
332 Ok(Self::from_room_info(
333 room_info,
334 is_direct,
335 num_joined_members,
336 num_active_members,
337 state,
338 None,
339 ))
340 }
341}
342
343async fn search_for_room_preview_in_room_directory(
344 client: Client,
345 filter: Option<String>,
346 batch_size: u32,
347 server: Option<OwnedServerName>,
348 expected_room_id: &RoomId,
349) -> crate::Result<Option<RoomPreview>> {
350 let mut directory_search = RoomDirectorySearch::new(client);
351 directory_search.search(filter, batch_size, server).await?;
352
353 let (results, _) = directory_search.results();
354
355 for room_description in results {
356 if room_description.room_id != expected_room_id {
358 continue;
359 }
360 return Ok(Some(RoomPreview {
361 room_id: room_description.room_id,
362 canonical_alias: room_description.alias,
363 name: room_description.name,
364 topic: room_description.topic,
365 avatar_url: room_description.avatar_url,
366 num_joined_members: room_description.joined_members,
367 num_active_members: None,
368 room_type: None,
370 join_rule: Some(room_description.join_rule.into()),
371 is_world_readable: Some(room_description.is_world_readable),
372 state: None,
373 is_direct: None,
374 heroes: None,
375 }));
376 }
377
378 Ok(None)
379}
380
381fn ensure_server_names_is_not_empty(
384 own_server_name: Option<&ServerName>,
385 server_names: Vec<OwnedServerName>,
386 room_or_alias_id: &RoomOrAliasId,
387) -> Vec<OwnedServerName> {
388 let mut server_names = server_names;
389
390 if let Some((own_server, alias_server)) = own_server_name.zip(room_or_alias_id.server_name())
391 && server_names.is_empty()
392 && own_server != alias_server
393 {
394 server_names.push(alias_server.to_owned());
395 }
396
397 server_names
398}
399
400#[cfg(test)]
401mod tests {
402 use ruma::{RoomOrAliasId, ServerName, owned_server_name, room_alias_id, room_id, server_name};
403
404 use crate::room_preview::ensure_server_names_is_not_empty;
405
406 #[test]
407 fn test_ensure_server_names_is_not_empty_when_no_own_server_name_is_provided() {
408 let own_server_name: Option<&ServerName> = None;
409 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").into();
410
411 let server_names =
412 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
413
414 assert!(server_names.is_empty());
417 }
418
419 #[test]
420 fn test_ensure_server_names_is_not_empty_when_room_alias_or_id_has_no_server_name() {
421 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
422 let room_or_alias_id: &RoomOrAliasId = room_id!("!test").into();
423
424 let server_names =
425 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
426
427 assert!(server_names.is_empty());
429 }
430
431 #[test]
432 fn test_ensure_server_names_is_not_empty_with_same_server_name() {
433 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
434 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").into();
435
436 let server_names =
437 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
438
439 assert!(server_names.is_empty());
442 }
443
444 #[test]
445 fn test_ensure_server_names_is_not_empty_with_different_room_id_server_name() {
446 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
447 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:matrix.org").into();
448
449 let server_names =
450 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
451
452 assert!(!server_names.is_empty());
454 assert_eq!(server_names[0], owned_server_name!("matrix.org"));
455 }
456
457 #[test]
458 fn test_ensure_server_names_is_not_empty_with_different_room_alias_server_name() {
459 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
460 let room_or_alias_id: &RoomOrAliasId = room_alias_id!("#test:matrix.org").into();
461
462 let server_names =
463 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
464
465 assert!(!server_names.is_empty());
467 assert_eq!(server_names[0], owned_server_name!("matrix.org"));
468 }
469}