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() {
192 Some(room_or_alias_id.as_str()[1..].to_owned())
193 } else {
194 None
195 };
196
197 let batch_size = if search_term.is_some() { 20 } else { 100 };
200
201 if via.is_empty() {
202 search_for_room_preview_in_room_directory(
204 client.clone(),
205 search_term,
206 batch_size,
207 None,
208 room_id,
209 )
210 .await
211 } else {
212 let mut futures = Vec::new();
213 for server in via {
215 futures.push(search_for_room_preview_in_room_directory(
216 client.clone(),
217 search_term.clone(),
218 batch_size,
219 Some(server),
220 room_id,
221 ));
222 }
223
224 let joined_results = join_all(futures).await;
225
226 Ok(joined_results.into_iter().flatten().next().flatten())
227 }
228 }
229
230 pub async fn from_room_summary(
237 client: &Client,
238 room_id: OwnedRoomId,
239 room_or_alias_id: &RoomOrAliasId,
240 via: Vec<OwnedServerName>,
241 ) -> crate::Result<Self> {
242 let own_server_name = client.session_meta().map(|s| s.user_id.server_name());
243 let via = ensure_server_names_is_not_empty(own_server_name, via, room_or_alias_id);
244
245 let request = ruma::api::client::room::get_summary::v1::Request::new(
246 room_or_alias_id.to_owned(),
247 via,
248 );
249
250 let response = client.send(request).await?;
251
252 let cached_room = client.get_room(&room_id);
256 let state = if cached_room.is_none() {
257 None
258 } else {
259 response.membership.map(|membership| RoomState::from(&membership))
260 };
261
262 let num_active_members = cached_room.as_ref().map(|r| r.active_members_count());
263
264 let is_direct = if let Some(cached_room) = &cached_room {
265 cached_room.is_direct().await.ok()
266 } else {
267 None
268 };
269
270 let heroes = if let Some(cached_room) = &cached_room {
271 Some(cached_room.heroes().await)
272 } else {
273 None
274 };
275
276 let summary = response.summary;
277
278 Ok(RoomPreview {
279 room_id,
280 canonical_alias: summary.canonical_alias,
281 name: summary.name,
282 topic: summary.topic,
283 avatar_url: summary.avatar_url,
284 num_joined_members: summary.num_joined_members.into(),
285 num_active_members,
286 room_type: summary.room_type,
287 join_rule: Some(summary.join_rule),
288 is_world_readable: Some(summary.world_readable),
289 state,
290 is_direct,
291 heroes,
292 })
293 }
294
295 pub async fn from_state_events(client: &Client, room_id: &RoomId) -> crate::Result<Self> {
307 let state_request = get_state_events::v3::Request::new(room_id.to_owned());
308 let joined_members_request = joined_members::v3::Request::new(room_id.to_owned());
309
310 let (state, joined_members) =
311 try_join!(async { client.send(state_request).await }, async {
312 client.send(joined_members_request).await
313 })?;
314
315 let num_joined_members = joined_members.joined.len().try_into().unwrap_or(u64::MAX);
318
319 let mut room_info = RoomInfo::new(room_id, RoomState::Joined);
320
321 for ev in state.room_state {
322 if let Some(mut raw_event) = RawStateEventWithKeys::try_from_raw_state_event(ev.cast())
323 {
324 room_info.handle_state_event(&mut raw_event);
325 }
326 }
327
328 let room = client.get_room(room_id);
329 let state = room.as_ref().map(|room| room.state());
330 let num_active_members = room.as_ref().map(|r| r.active_members_count());
331 let is_direct = if let Some(room) = room { room.is_direct().await.ok() } else { None };
332
333 Ok(Self::from_room_info(
334 room_info,
335 is_direct,
336 num_joined_members,
337 num_active_members,
338 state,
339 None,
340 ))
341 }
342}
343
344async fn search_for_room_preview_in_room_directory(
345 client: Client,
346 filter: Option<String>,
347 batch_size: u32,
348 server: Option<OwnedServerName>,
349 expected_room_id: &RoomId,
350) -> crate::Result<Option<RoomPreview>> {
351 let mut directory_search = RoomDirectorySearch::new(client);
352 directory_search.search(filter, batch_size, server).await?;
353
354 let (results, _) = directory_search.results();
355
356 for room_description in results {
357 if room_description.room_id != expected_room_id {
359 continue;
360 }
361 return Ok(Some(RoomPreview {
362 room_id: room_description.room_id,
363 canonical_alias: room_description.alias,
364 name: room_description.name,
365 topic: room_description.topic,
366 avatar_url: room_description.avatar_url,
367 num_joined_members: room_description.joined_members,
368 num_active_members: None,
369 room_type: None,
371 join_rule: Some(room_description.join_rule.into()),
372 is_world_readable: Some(room_description.is_world_readable),
373 state: None,
374 is_direct: None,
375 heroes: None,
376 }));
377 }
378
379 Ok(None)
380}
381
382fn ensure_server_names_is_not_empty(
385 own_server_name: Option<&ServerName>,
386 server_names: Vec<OwnedServerName>,
387 room_or_alias_id: &RoomOrAliasId,
388) -> Vec<OwnedServerName> {
389 let mut server_names = server_names;
390
391 if let Some((own_server, alias_server)) = own_server_name.zip(room_or_alias_id.server_name())
392 && server_names.is_empty()
393 && own_server != alias_server
394 {
395 server_names.push(alias_server.to_owned());
396 }
397
398 server_names
399}
400
401#[cfg(test)]
402mod tests {
403 use ruma::{RoomOrAliasId, ServerName, room_alias_id, room_id, server_name};
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_eq!(server_names, &["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_eq!(server_names, &["matrix.org"]);
467 }
468}