1use futures_util::future::join_all;
22use matrix_sdk_base::{RoomHero, 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<RoomHero>>,
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(room_info.heroes().to_vec()),
112 }
113 }
114
115 pub(crate) async fn from_known_room(room: &Room) -> Self {
121 let is_direct = room.is_direct().await.ok();
122
123 let display_name = room.display_name().await.ok().map(|name| name.to_string());
124
125 Self::from_room_info(
126 room.clone_info(),
127 is_direct,
128 room.joined_members_count(),
129 Some(room.active_members_count()),
130 Some(room.state()),
131 display_name,
132 )
133 }
134
135 #[instrument(skip(client))]
136 pub(crate) async fn from_remote_room(
137 client: &Client,
138 room_id: OwnedRoomId,
139 room_or_alias_id: &RoomOrAliasId,
140 via: Vec<OwnedServerName>,
141 ) -> crate::Result<Self> {
142 match Self::from_room_summary(client, room_id.clone(), room_or_alias_id, via.clone()).await
145 {
146 Ok(res) => return Ok(res),
147 Err(err) => {
148 warn!("error when previewing room from the room summary endpoint: {err}");
149 }
150 }
151
152 match Self::from_room_directory_search(client, &room_id, room_or_alias_id, via).await {
154 Ok(Some(res)) => return Ok(res),
155 Ok(None) => warn!("Room '{room_or_alias_id}' not found in room directory search."),
156 Err(err) => {
157 warn!("Searching for '{room_or_alias_id}' in room directory search failed: {err}");
158 }
159 }
160
161 match Self::from_state_events(client, &room_id).await {
163 Ok(res) => return Ok(res),
164 Err(err) => {
165 warn!("error when building room preview from state events: {err}");
166 }
167 }
168
169 if let Some(room) = client.get_room(&room_id) {
172 Ok(Self::from_known_room(&room).await)
173 } else {
174 Err(Error::InsufficientData)
175 }
176 }
177
178 pub(crate) async fn from_room_directory_search(
182 client: &Client,
183 room_id: &RoomId,
184 room_or_alias_id: &RoomOrAliasId,
185 via: Vec<OwnedServerName>,
186 ) -> crate::Result<Option<Self>> {
187 let search_term = if room_or_alias_id.is_room_alias_id() {
189 Some(room_or_alias_id.as_str()[1..].to_owned())
190 } else {
191 None
192 };
193
194 let batch_size = if search_term.is_some() { 20 } else { 100 };
197
198 if via.is_empty() {
199 search_for_room_preview_in_room_directory(
201 client.clone(),
202 search_term,
203 batch_size,
204 None,
205 room_id,
206 )
207 .await
208 } else {
209 let mut futures = Vec::new();
210 for server in via {
212 futures.push(search_for_room_preview_in_room_directory(
213 client.clone(),
214 search_term.clone(),
215 batch_size,
216 Some(server),
217 room_id,
218 ));
219 }
220
221 let joined_results = join_all(futures).await;
222
223 Ok(joined_results.into_iter().flatten().next().flatten())
224 }
225 }
226
227 pub async fn from_room_summary(
234 client: &Client,
235 room_id: OwnedRoomId,
236 room_or_alias_id: &RoomOrAliasId,
237 via: Vec<OwnedServerName>,
238 ) -> crate::Result<Self> {
239 let own_server_name = client.session_meta().map(|s| s.user_id.server_name());
240 let via = ensure_server_names_is_not_empty(own_server_name, via, room_or_alias_id);
241
242 let request = ruma::api::client::room::get_summary::v1::Request::new(
243 room_or_alias_id.to_owned(),
244 via,
245 );
246
247 let response = client.send(request).await?;
248
249 let cached_room = client.get_room(&room_id);
253 let state = if cached_room.is_none() {
254 None
255 } else {
256 response.membership.map(|membership| RoomState::from(&membership))
257 };
258
259 let num_active_members = cached_room.as_ref().map(|r| r.active_members_count());
260
261 let is_direct = if let Some(cached_room) = &cached_room {
262 cached_room.is_direct().await.ok()
263 } else {
264 None
265 };
266
267 let summary = response.summary;
268
269 Ok(RoomPreview {
270 room_id,
271 canonical_alias: summary.canonical_alias,
272 name: summary.name,
273 topic: summary.topic,
274 avatar_url: summary.avatar_url,
275 num_joined_members: summary.num_joined_members.into(),
276 num_active_members,
277 room_type: summary.room_type,
278 join_rule: Some(summary.join_rule),
279 is_world_readable: Some(summary.world_readable),
280 state,
281 is_direct,
282 heroes: cached_room.map(|r| r.heroes()),
283 })
284 }
285
286 pub async fn from_state_events(client: &Client, room_id: &RoomId) -> crate::Result<Self> {
298 let state_request = get_state_events::v3::Request::new(room_id.to_owned());
299 let joined_members_request = joined_members::v3::Request::new(room_id.to_owned());
300
301 let (state, joined_members) =
302 try_join!(async { client.send(state_request).await }, async {
303 client.send(joined_members_request).await
304 })?;
305
306 let num_joined_members = joined_members.joined.len().try_into().unwrap_or(u64::MAX);
309
310 let mut room_info = RoomInfo::new(room_id, RoomState::Joined);
311
312 for ev in state.room_state {
313 let ev = match ev.deserialize() {
314 Ok(ev) => ev,
315 Err(err) => {
316 warn!("failed to deserialize state event: {err}");
317 continue;
318 }
319 };
320 room_info.handle_state_event(&ev.into());
321 }
322
323 let room = client.get_room(room_id);
324 let state = room.as_ref().map(|room| room.state());
325 let num_active_members = room.as_ref().map(|r| r.active_members_count());
326 let is_direct = if let Some(room) = room { room.is_direct().await.ok() } else { None };
327
328 Ok(Self::from_room_info(
329 room_info,
330 is_direct,
331 num_joined_members,
332 num_active_members,
333 state,
334 None,
335 ))
336 }
337}
338
339async fn search_for_room_preview_in_room_directory(
340 client: Client,
341 filter: Option<String>,
342 batch_size: u32,
343 server: Option<OwnedServerName>,
344 expected_room_id: &RoomId,
345) -> crate::Result<Option<RoomPreview>> {
346 let mut directory_search = RoomDirectorySearch::new(client);
347 directory_search.search(filter, batch_size, server).await?;
348
349 let (results, _) = directory_search.results();
350
351 for room_description in results {
352 if room_description.room_id != expected_room_id {
354 continue;
355 }
356 return Ok(Some(RoomPreview {
357 room_id: room_description.room_id,
358 canonical_alias: room_description.alias,
359 name: room_description.name,
360 topic: room_description.topic,
361 avatar_url: room_description.avatar_url,
362 num_joined_members: room_description.joined_members,
363 num_active_members: None,
364 room_type: None,
366 join_rule: Some(room_description.join_rule.into()),
367 is_world_readable: Some(room_description.is_world_readable),
368 state: None,
369 is_direct: None,
370 heroes: None,
371 }));
372 }
373
374 Ok(None)
375}
376
377fn ensure_server_names_is_not_empty(
380 own_server_name: Option<&ServerName>,
381 server_names: Vec<OwnedServerName>,
382 room_or_alias_id: &RoomOrAliasId,
383) -> Vec<OwnedServerName> {
384 let mut server_names = server_names;
385
386 if let Some((own_server, alias_server)) = own_server_name.zip(room_or_alias_id.server_name())
387 && server_names.is_empty()
388 && own_server != alias_server
389 {
390 server_names.push(alias_server.to_owned());
391 }
392
393 server_names
394}
395
396#[cfg(test)]
397mod tests {
398 use ruma::{RoomOrAliasId, ServerName, owned_server_name, room_alias_id, room_id, server_name};
399
400 use crate::room_preview::ensure_server_names_is_not_empty;
401
402 #[test]
403 fn test_ensure_server_names_is_not_empty_when_no_own_server_name_is_provided() {
404 let own_server_name: Option<&ServerName> = None;
405 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").into();
406
407 let server_names =
408 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
409
410 assert!(server_names.is_empty());
413 }
414
415 #[test]
416 fn test_ensure_server_names_is_not_empty_when_room_alias_or_id_has_no_server_name() {
417 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
418 let room_or_alias_id: &RoomOrAliasId = room_id!("!test").into();
419
420 let server_names =
421 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
422
423 assert!(server_names.is_empty());
425 }
426
427 #[test]
428 fn test_ensure_server_names_is_not_empty_with_same_server_name() {
429 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
430 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").into();
431
432 let server_names =
433 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
434
435 assert!(server_names.is_empty());
438 }
439
440 #[test]
441 fn test_ensure_server_names_is_not_empty_with_different_room_id_server_name() {
442 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
443 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:matrix.org").into();
444
445 let server_names =
446 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
447
448 assert!(!server_names.is_empty());
450 assert_eq!(server_names[0], owned_server_name!("matrix.org"));
451 }
452
453 #[test]
454 fn test_ensure_server_names_is_not_empty_with_different_room_alias_server_name() {
455 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
456 let room_or_alias_id: &RoomOrAliasId = room_alias_id!("#test:matrix.org").into();
457
458 let server_names =
459 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
460
461 assert!(!server_names.is_empty());
463 assert_eq!(server_names[0], owned_server_name!("matrix.org"));
464 }
465}