use futures_util::future::join_all;
use matrix_sdk_base::{RoomHero, RoomInfo, RoomState};
use ruma::{
api::client::{membership::joined_members, state::get_state_events},
directory::PublicRoomJoinRule,
events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule},
room::RoomType,
space::SpaceRoomJoinRule,
OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName, RoomId, RoomOrAliasId, ServerName,
};
use tokio::try_join;
use tracing::{instrument, warn};
use crate::{room_directory_search::RoomDirectorySearch, Client, Room};
#[derive(Debug, Clone)]
pub struct RoomPreview {
pub room_id: OwnedRoomId,
pub canonical_alias: Option<OwnedRoomAliasId>,
pub name: Option<String>,
pub topic: Option<String>,
pub avatar_url: Option<OwnedMxcUri>,
pub num_joined_members: u64,
pub num_active_members: Option<u64>,
pub room_type: Option<RoomType>,
pub join_rule: SpaceRoomJoinRule,
pub is_world_readable: Option<bool>,
pub state: Option<RoomState>,
pub is_direct: Option<bool>,
pub heroes: Option<Vec<RoomHero>>,
}
impl RoomPreview {
fn from_room_info(
room_info: RoomInfo,
is_direct: Option<bool>,
num_joined_members: u64,
num_active_members: Option<u64>,
state: Option<RoomState>,
computed_display_name: Option<String>,
) -> Self {
RoomPreview {
room_id: room_info.room_id().to_owned(),
canonical_alias: room_info.canonical_alias().map(ToOwned::to_owned),
name: computed_display_name.or_else(|| room_info.name().map(ToOwned::to_owned)),
topic: room_info.topic().map(ToOwned::to_owned),
avatar_url: room_info.avatar_url().map(ToOwned::to_owned),
room_type: room_info.room_type().cloned(),
join_rule: match room_info.join_rule() {
JoinRule::Invite => SpaceRoomJoinRule::Invite,
JoinRule::Knock => SpaceRoomJoinRule::Knock,
JoinRule::Private => SpaceRoomJoinRule::Private,
JoinRule::Restricted(_) => SpaceRoomJoinRule::Restricted,
JoinRule::KnockRestricted(_) => SpaceRoomJoinRule::KnockRestricted,
JoinRule::Public => SpaceRoomJoinRule::Public,
_ => {
SpaceRoomJoinRule::Private
}
},
is_world_readable: room_info
.history_visibility()
.map(|vis| *vis == HistoryVisibility::WorldReadable),
num_joined_members,
num_active_members,
state,
is_direct,
heroes: Some(room_info.heroes().to_vec()),
}
}
pub(crate) async fn from_joined(room: &Room) -> Self {
let is_direct = room.is_direct().await.ok();
let display_name = room.compute_display_name().await.ok().map(|name| name.to_string());
Self::from_room_info(
room.clone_info(),
is_direct,
room.joined_members_count(),
Some(room.active_members_count()),
Some(room.state()),
display_name,
)
}
#[instrument(skip(client))]
pub(crate) async fn from_not_joined(
client: &Client,
room_id: OwnedRoomId,
room_or_alias_id: &RoomOrAliasId,
via: Vec<OwnedServerName>,
) -> crate::Result<Self> {
match Self::from_room_summary(client, room_id.clone(), room_or_alias_id, via.clone()).await
{
Ok(res) => return Ok(res),
Err(err) => {
warn!("error when previewing room from the room summary endpoint: {err}");
}
}
match Self::from_room_directory_search(client, &room_id, room_or_alias_id, via).await {
Ok(Some(res)) => return Ok(res),
Ok(None) => warn!("Room '{room_or_alias_id}' not found in room directory search."),
Err(err) => {
warn!("Searching for '{room_or_alias_id}' in room directory search failed: {err}");
}
}
Self::from_state_events(client, &room_id).await
}
pub(crate) async fn from_room_directory_search(
client: &Client,
room_id: &RoomId,
room_or_alias_id: &RoomOrAliasId,
via: Vec<OwnedServerName>,
) -> crate::Result<Option<Self>> {
let search_term = if room_or_alias_id.is_room_alias_id() {
Some(room_or_alias_id.as_str()[1..].to_owned())
} else {
None
};
let batch_size = if search_term.is_some() { 20 } else { 100 };
if via.is_empty() {
search_for_room_preview_in_room_directory(
client.clone(),
search_term,
batch_size,
None,
room_id,
)
.await
} else {
let mut futures = Vec::new();
for server in via {
futures.push(search_for_room_preview_in_room_directory(
client.clone(),
search_term.clone(),
batch_size,
Some(server),
room_id,
));
}
let joined_results = join_all(futures).await;
Ok(joined_results.into_iter().flatten().next().flatten())
}
}
pub async fn from_room_summary(
client: &Client,
room_id: OwnedRoomId,
room_or_alias_id: &RoomOrAliasId,
via: Vec<OwnedServerName>,
) -> crate::Result<Self> {
let own_server_name = client.session_meta().map(|s| s.user_id.server_name());
let via = ensure_server_names_is_not_empty(own_server_name, via, room_or_alias_id);
let request = ruma::api::client::room::get_summary::msc3266::Request::new(
room_or_alias_id.to_owned(),
via,
);
let response = client.send(request, None).await?;
let cached_room = client.get_room(&room_id);
let state = if cached_room.is_none() {
None
} else {
response.membership.map(|membership| RoomState::from(&membership))
};
let num_active_members = cached_room.as_ref().map(|r| r.active_members_count());
let is_direct = if let Some(cached_room) = &cached_room {
cached_room.is_direct().await.ok()
} else {
None
};
Ok(RoomPreview {
room_id,
canonical_alias: response.canonical_alias,
name: response.name,
topic: response.topic,
avatar_url: response.avatar_url,
num_joined_members: response.num_joined_members.into(),
num_active_members,
room_type: response.room_type,
join_rule: response.join_rule,
is_world_readable: Some(response.world_readable),
state,
is_direct,
heroes: cached_room.map(|r| r.heroes()),
})
}
pub async fn from_state_events(client: &Client, room_id: &RoomId) -> crate::Result<Self> {
let state_request = get_state_events::v3::Request::new(room_id.to_owned());
let joined_members_request = joined_members::v3::Request::new(room_id.to_owned());
let (state, joined_members) =
try_join!(async { client.send(state_request, None).await }, async {
client.send(joined_members_request, None).await
})?;
let num_joined_members = joined_members.joined.len().try_into().unwrap_or(u64::MAX);
let mut room_info = RoomInfo::new(room_id, RoomState::Joined);
for ev in state.room_state {
let ev = match ev.deserialize() {
Ok(ev) => ev,
Err(err) => {
warn!("failed to deserialize state event: {err}");
continue;
}
};
room_info.handle_state_event(&ev.into());
}
let room = client.get_room(room_id);
let state = room.as_ref().map(|room| room.state());
let num_active_members = room.as_ref().map(|r| r.active_members_count());
let is_direct = if let Some(room) = room { room.is_direct().await.ok() } else { None };
Ok(Self::from_room_info(
room_info,
is_direct,
num_joined_members,
num_active_members,
state,
None,
))
}
}
async fn search_for_room_preview_in_room_directory(
client: Client,
filter: Option<String>,
batch_size: u32,
server: Option<OwnedServerName>,
expected_room_id: &RoomId,
) -> crate::Result<Option<RoomPreview>> {
let mut directory_search = RoomDirectorySearch::new(client);
directory_search.search(filter, batch_size, server).await?;
let (results, _) = directory_search.results();
for room_description in results {
if room_description.room_id != expected_room_id {
continue;
}
return Ok(Some(RoomPreview {
room_id: room_description.room_id,
canonical_alias: room_description.alias,
name: room_description.name,
topic: room_description.topic,
avatar_url: room_description.avatar_url,
num_joined_members: room_description.joined_members,
num_active_members: None,
room_type: None,
join_rule: match room_description.join_rule {
PublicRoomJoinRule::Public => SpaceRoomJoinRule::Public,
PublicRoomJoinRule::Knock => SpaceRoomJoinRule::Knock,
PublicRoomJoinRule::_Custom(rule) => SpaceRoomJoinRule::_Custom(rule),
_ => {
panic!("Unexpected PublicRoomJoinRule {:?}", room_description.join_rule)
}
},
is_world_readable: Some(room_description.is_world_readable),
state: None,
is_direct: None,
heroes: None,
}));
}
Ok(None)
}
fn ensure_server_names_is_not_empty(
own_server_name: Option<&ServerName>,
server_names: Vec<OwnedServerName>,
room_or_alias_id: &RoomOrAliasId,
) -> Vec<OwnedServerName> {
let mut server_names = server_names;
if let Some((own_server, alias_server)) = own_server_name.zip(room_or_alias_id.server_name()) {
if server_names.is_empty() && own_server != alias_server {
server_names.push(alias_server.to_owned());
}
}
server_names
}
#[cfg(test)]
mod tests {
use ruma::{owned_server_name, room_alias_id, room_id, server_name, RoomOrAliasId, ServerName};
use crate::room_preview::ensure_server_names_is_not_empty;
#[test]
fn test_ensure_server_names_is_not_empty_when_no_own_server_name_is_provided() {
let own_server_name: Option<&ServerName> = None;
let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").into();
let server_names =
ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
assert!(server_names.is_empty());
}
#[test]
fn test_ensure_server_names_is_not_empty_when_room_alias_or_id_has_no_server_name() {
let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
let room_or_alias_id: &RoomOrAliasId = room_id!("!test").into();
let server_names =
ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
assert!(server_names.is_empty());
}
#[test]
fn test_ensure_server_names_is_not_empty_with_same_server_name() {
let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
let room_or_alias_id: &RoomOrAliasId = room_id!("!test:localhost").into();
let server_names =
ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
assert!(server_names.is_empty());
}
#[test]
fn test_ensure_server_names_is_not_empty_with_different_room_id_server_name() {
let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
let room_or_alias_id: &RoomOrAliasId = room_id!("!test:matrix.org").into();
let server_names =
ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
assert!(!server_names.is_empty());
assert_eq!(server_names[0], owned_server_name!("matrix.org"));
}
#[test]
fn test_ensure_server_names_is_not_empty_with_different_room_alias_server_name() {
let own_server_name: Option<&ServerName> = Some(server_name!("localhost"));
let room_or_alias_id: &RoomOrAliasId = room_alias_id!("#test:matrix.org").into();
let server_names =
ensure_server_names_is_not_empty(own_server_name, Vec::new(), room_or_alias_id);
assert!(!server_names.is_empty());
assert_eq!(server_names[0], owned_server_name!("matrix.org"));
}
}