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 events::room::history_visibility::HistoryVisibility,
26 room::{JoinRuleSummary, RoomType},
27 OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName, RoomId, RoomOrAliasId, ServerName,
28};
29use tokio::try_join;
30use tracing::{instrument, warn};
31
32use crate::{room_directory_search::RoomDirectorySearch, Client, Error, Room};
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 if server_names.is_empty() && own_server != alias_server {
388 server_names.push(alias_server.to_owned());
389 }
390 }
391
392 server_names
393}
394
395#[cfg(test)]
396mod tests {
397 use ruma::{owned_server_name, room_alias_id, room_id, server_name, RoomOrAliasId, ServerName};
398
399 use crate::room_preview::ensure_server_names_is_not_empty;
400
401 #[test]
402 fn test_ensure_server_names_is_not_empty_when_no_own_server_name_is_provided() {
403 let own_server_name: Option<&ServerName> = None;
404 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").into();
405
406 let server_names =
407 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
408
409 assert!(server_names.is_empty());
412 }
413
414 #[test]
415 fn test_ensure_server_names_is_not_empty_when_room_alias_or_id_has_no_server_name() {
416 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
417 let room_or_alias_id: &RoomOrAliasId = room_id!("!test").into();
418
419 let server_names =
420 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
421
422 assert!(server_names.is_empty());
424 }
425
426 #[test]
427 fn test_ensure_server_names_is_not_empty_with_same_server_name() {
428 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
429 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").into();
430
431 let server_names =
432 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
433
434 assert!(server_names.is_empty());
437 }
438
439 #[test]
440 fn test_ensure_server_names_is_not_empty_with_different_room_id_server_name() {
441 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
442 let room_or_alias_id: &RoomOrAliasId = room_id!("!test:matrix.org").into();
443
444 let server_names =
445 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
446
447 assert!(!server_names.is_empty());
449 assert_eq!(server_names[0], owned_server_name!("matrix.org"));
450 }
451
452 #[test]
453 fn test_ensure_server_names_is_not_empty_with_different_room_alias_server_name() {
454 let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
455 let room_or_alias_id: &RoomOrAliasId = room_alias_id!("#test:matrix.org").into();
456
457 let server_names =
458 ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
459
460 assert!(!server_names.is_empty());
462 assert_eq!(server_names[0], owned_server_name!("matrix.org"));
463 }
464}