matrix-rust-sdk/crates/matrix-sdk-base/src/rooms/normal.rs

566 lines
18 KiB
Rust

// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
convert::TryFrom,
sync::{Arc, RwLock as SyncRwLock},
};
use futures::{
future,
stream::{self, StreamExt},
};
use ruma::{
api::client::r0::sync::sync_events::RoomSummary as RumaSummary,
events::{
receipt::Receipt,
room::{
create::CreateEventContent, encryption::EncryptionEventContent,
guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule,
tombstone::TombstoneEventContent,
},
tag::Tags,
AnyRoomAccountDataEvent, AnyStateEventContent, AnySyncStateEvent, EventType,
},
receipt::ReceiptType,
EventId, MxcUri, RoomAliasId, RoomId, UserId,
};
use serde::{Deserialize, Serialize};
use tracing::info;
use super::{BaseRoomInfo, RoomMember};
use crate::{
deserialized_responses::UnreadNotificationsCount,
store::{Result as StoreResult, StateStore},
};
/// The underlying room data structure collecting state for joined, left and
/// invited rooms.
#[derive(Debug, Clone)]
pub struct Room {
room_id: Arc<RoomId>,
own_user_id: Arc<UserId>,
inner: Arc<SyncRwLock<RoomInfo>>,
store: Arc<dyn StateStore>,
}
/// The room summary containing member counts and members that should be used to
/// calculate the room display name.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RoomSummary {
/// The heroes of the room, members that should be used for the room display
/// name.
heroes: Vec<String>,
/// The number of members that are considered to be joined to the room.
joined_member_count: u64,
/// The number of members that are considered to be invited to the room.
invited_member_count: u64,
}
/// Enum keeping track in which state the room is, e.g. if our own user is
/// joined, invited, or has left the room.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum RoomType {
/// The room is in a joined state.
Joined,
/// The room is in a left state.
Left,
/// The room is in a invited state.
Invited,
}
impl Room {
pub(crate) fn new(
own_user_id: &UserId,
store: Arc<dyn StateStore>,
room_id: &RoomId,
room_type: RoomType,
) -> Self {
let room_id = Arc::new(room_id.clone());
let room_info = RoomInfo {
room_id,
room_type,
notification_counts: Default::default(),
summary: Default::default(),
members_synced: false,
last_prev_batch: None,
base_info: BaseRoomInfo::new(),
};
Self::restore(own_user_id, store, room_info)
}
pub(crate) fn restore(
own_user_id: &UserId,
store: Arc<dyn StateStore>,
room_info: RoomInfo,
) -> Self {
Self {
own_user_id: Arc::new(own_user_id.clone()),
room_id: room_info.room_id.clone(),
store,
inner: Arc::new(SyncRwLock::new(room_info)),
}
}
/// Get the unique room id of the room.
pub fn room_id(&self) -> &RoomId {
&self.room_id
}
/// Get our own user id.
pub fn own_user_id(&self) -> &UserId {
&self.own_user_id
}
/// Get the type of the room.
pub fn room_type(&self) -> RoomType {
self.inner.read().unwrap().room_type
}
/// Get the unread notification counts.
pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
self.inner.read().unwrap().notification_counts
}
/// Check if the room has it's members fully synced.
///
/// Members might be missing if lazy member loading was enabled for the
/// sync.
///
/// Returns true if no members are missing, false otherwise.
pub fn are_members_synced(&self) -> bool {
self.inner.read().unwrap().members_synced
}
/// Get the `prev_batch` token that was received from the last sync. May be
/// `None` if the last sync contained the full room history.
pub fn last_prev_batch(&self) -> Option<String> {
self.inner.read().unwrap().last_prev_batch.clone()
}
/// Get the avatar url of this room.
pub fn avatar_url(&self) -> Option<MxcUri> {
self.inner.read().unwrap().base_info.avatar_url.clone()
}
/// Get the canonical alias of this room.
pub fn canonical_alias(&self) -> Option<RoomAliasId> {
self.inner.read().unwrap().base_info.canonical_alias.clone()
}
/// Get the `m.room.create` content of this room.
///
/// This usually isn't optional but some servers might not send an
/// `m.room.create` event as the first event for a given room, thus this can
/// be optional.
pub fn create_content(&self) -> Option<CreateEventContent> {
self.inner.read().unwrap().base_info.create.clone()
}
/// Is this room considered a direct message.
pub fn is_direct(&self) -> bool {
self.inner.read().unwrap().base_info.dm_target.is_some()
}
/// If this room is a direct message, get the member that we're sharing the
/// room with.
///
/// *Note*: The member list might have been modified in the meantime and
/// the target might not even be in the room anymore. This setting should
/// only be considered as guidance.
pub fn direct_target(&self) -> Option<UserId> {
self.inner.read().unwrap().base_info.dm_target.clone()
}
/// Is the room encrypted.
pub fn is_encrypted(&self) -> bool {
self.inner.read().unwrap().is_encrypted()
}
/// Get the `m.room.encryption` content that enabled end to end encryption
/// in the room.
pub fn encryption_settings(&self) -> Option<EncryptionEventContent> {
self.inner.read().unwrap().base_info.encryption.clone()
}
/// Get the guest access policy of this room.
pub fn guest_access(&self) -> GuestAccess {
self.inner.read().unwrap().base_info.guest_access.clone()
}
/// Get the history visibility policy of this room.
pub fn history_visibility(&self) -> HistoryVisibility {
self.inner.read().unwrap().base_info.history_visibility.clone()
}
/// Is the room considered to be public.
pub fn is_public(&self) -> bool {
matches!(self.join_rule(), JoinRule::Public)
}
/// Get the join rule policy of this room.
pub fn join_rule(&self) -> JoinRule {
self.inner.read().unwrap().base_info.join_rule.clone()
}
/// Get the maximum power level that this room contains.
///
/// This is useful if one wishes to normalize the power levels, e.g. from
/// 0-100 where 100 would be the max power level.
pub fn max_power_level(&self) -> i64 {
self.inner.read().unwrap().base_info.max_power_level
}
/// Get the `m.room.name` of this room.
pub fn name(&self) -> Option<String> {
self.inner.read().unwrap().base_info.name.clone()
}
/// Has the room been tombstoned.
pub fn is_tombstoned(&self) -> bool {
self.inner.read().unwrap().base_info.tombstone.is_some()
}
/// Get the `m.room.tombstone` content of this room if there is one.
pub fn tombstone(&self) -> Option<TombstoneEventContent> {
self.inner.read().unwrap().base_info.tombstone.clone()
}
/// Get the topic of the room.
pub fn topic(&self) -> Option<String> {
self.inner.read().unwrap().base_info.topic.clone()
}
/// Calculate the canonical display name of the room, taking into account
/// its name, aliases and members.
///
/// The display name is calculated according to [this algorithm][spec].
///
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
pub async fn display_name(&self) -> StoreResult<String> {
self.calculate_name().await
}
/// Get the list of users ids that are considered to be joined members of
/// this room.
pub async fn joined_user_ids(&self) -> StoreResult<Vec<UserId>> {
self.store.get_joined_user_ids(self.room_id()).await
}
/// Get the all `RoomMember`s of this room that are known to the store.
pub async fn members(&self) -> StoreResult<Vec<RoomMember>> {
let user_ids = self.store.get_user_ids(self.room_id()).await?;
let mut members = Vec::new();
for u in user_ids {
let m = self.get_member(&u).await?;
if let Some(member) = m {
members.push(member);
}
}
Ok(members)
}
/// Get the list of `RoomMember`s that are considered to be joined members
/// of this room.
pub async fn joined_members(&self) -> StoreResult<Vec<RoomMember>> {
let joined = self.store.get_joined_user_ids(self.room_id()).await?;
let mut members = Vec::new();
for u in joined {
let m = self.get_member(&u).await?;
if let Some(member) = m {
members.push(member);
}
}
Ok(members)
}
/// Get the list of `RoomMember`s that are considered to be joined or
/// invited members of this room.
pub async fn active_members(&self) -> StoreResult<Vec<RoomMember>> {
let joined = self.store.get_joined_user_ids(self.room_id()).await?;
let invited = self.store.get_invited_user_ids(self.room_id()).await?;
let mut members = Vec::new();
for u in joined.iter().chain(&invited) {
let m = self.get_member(u).await?;
if let Some(member) = m {
members.push(member);
}
}
Ok(members)
}
async fn calculate_name(&self) -> StoreResult<String> {
let summary = {
let inner = self.inner.read().unwrap();
if let Some(name) = &inner.base_info.name {
let name = name.trim();
return Ok(name.to_string());
} else if let Some(alias) = &inner.base_info.canonical_alias {
let alias = alias.alias().trim();
return Ok(alias.to_string());
}
inner.summary.clone()
};
// TODO what should we do here? We have correct counts only if lazy
// loading is used.
let joined = summary.joined_member_count;
let invited = summary.invited_member_count;
let heroes_count = summary.heroes.len() as u64;
let is_own_member = |m: &RoomMember| m.user_id() == &*self.own_user_id;
let is_own_user_id = |u: &str| u == self.own_user_id().as_str();
let members: Vec<RoomMember> = if summary.heroes.is_empty() {
self.active_members().await?.into_iter().filter(|u| !is_own_member(u)).take(5).collect()
} else {
let members: Vec<_> = stream::iter(summary.heroes.iter())
.filter(|u| future::ready(!is_own_user_id(u)))
.filter_map(|u| async move {
let user_id = UserId::try_from(u.as_str()).ok()?;
self.get_member(&user_id).await.transpose()
})
.collect()
.await;
let members: StoreResult<Vec<_>> = members.into_iter().collect();
members?
};
info!(
"Calculating name for {}, own user {} hero count {} heroes {:#?}",
self.room_id(),
self.own_user_id,
heroes_count,
summary.heroes
);
let inner = self.inner.read().unwrap();
Ok(inner.base_info.calculate_room_name(joined, invited, members))
}
pub(crate) fn clone_info(&self) -> RoomInfo {
(*self.inner.read().unwrap()).clone()
}
pub(crate) fn update_summary(&self, summary: RoomInfo) {
let mut inner = self.inner.write().unwrap();
*inner = summary;
}
/// Get the `RoomMember` with the given `user_id`.
///
/// Returns `None` if the member was never part of this room, otherwise
/// return a `RoomMember` that can be in a joined, invited, left, banned
/// state.
pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
let member_event =
if let Some(m) = self.store.get_member_event(self.room_id(), user_id).await? {
m
} else {
return Ok(None);
};
let presence =
self.store.get_presence_event(user_id).await?.and_then(|e| e.deserialize().ok());
let profile = self.store.get_profile(self.room_id(), user_id).await?;
let max_power_level = self.max_power_level();
let is_room_creator = self
.inner
.read()
.unwrap()
.base_info
.create
.as_ref()
.map(|c| &c.creator == user_id)
.unwrap_or(false);
let power =
self.store
.get_state_event(self.room_id(), EventType::RoomPowerLevels, "")
.await?
.and_then(|e| e.deserialize().ok())
.and_then(|e| {
if let AnySyncStateEvent::RoomPowerLevels(e) = e {
Some(e)
} else {
None
}
});
let ambiguous = self
.store
.get_users_with_display_name(
self.room_id(),
member_event.content.displayname.as_deref().unwrap_or_else(|| user_id.localpart()),
)
.await?
.len()
> 1;
Ok(Some(RoomMember {
event: member_event.into(),
profile: profile.into(),
presence: presence.into(),
power_levels: power.into(),
max_power_level,
is_room_creator,
display_name_ambiguous: ambiguous,
}))
}
/// Get the `Tags` for this room.
pub async fn tags(&self) -> StoreResult<Option<Tags>> {
if let Some(AnyRoomAccountDataEvent::Tag(event)) = self
.store
.get_room_account_data_event(self.room_id(), EventType::Tag)
.await?
.and_then(|r| r.deserialize().ok())
{
Ok(Some(event.content.tags))
} else {
Ok(None)
}
}
/// Get the read receipt as a `EventId` and `Receipt` tuple for the given
/// `user_id` in this room.
pub async fn user_read_receipt(
&self,
user_id: &UserId,
) -> StoreResult<Option<(EventId, Receipt)>> {
self.store.get_user_room_receipt_event(self.room_id(), ReceiptType::Read, user_id).await
}
/// Get the read receipts as a list of `UserId` and `Receipt` tuples for the
/// given `event_id` in this room.
pub async fn event_read_receipts(
&self,
event_id: &EventId,
) -> StoreResult<Vec<(UserId, Receipt)>> {
self.store.get_event_room_receipt_events(self.room_id(), ReceiptType::Read, event_id).await
}
}
/// The underlying pure data structure for joined and left rooms.
///
/// Holds all the info needed to persist a room into the state store.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RoomInfo {
/// The unique room id of the room.
pub room_id: Arc<RoomId>,
/// The type of the room.
pub room_type: RoomType,
/// The unread notifications counts.
pub notification_counts: UnreadNotificationsCount,
/// The summary of this room.
pub summary: RoomSummary,
/// Flag remembering if the room members are synced.
pub members_synced: bool,
/// The prev batch of this room we received during the last sync.
pub last_prev_batch: Option<String>,
/// Base room info which holds some basic event contents important for the
/// room state.
pub base_info: BaseRoomInfo,
}
impl RoomInfo {
pub(crate) fn mark_as_joined(&mut self) {
self.room_type = RoomType::Joined;
}
pub(crate) fn mark_as_left(&mut self) {
self.room_type = RoomType::Left;
}
pub(crate) fn mark_as_invited(&mut self) {
self.room_type = RoomType::Invited;
}
pub(crate) fn mark_members_synced(&mut self) {
self.members_synced = true;
}
pub(crate) fn mark_members_missing(&mut self) {
self.members_synced = false;
}
pub(crate) fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
if self.last_prev_batch.as_deref() != prev_batch {
self.last_prev_batch = prev_batch.map(|p| p.to_string());
true
} else {
false
}
}
pub(crate) fn is_encrypted(&self) -> bool {
self.base_info.encryption.is_some()
}
pub(crate) fn handle_state_event(&mut self, event: &AnyStateEventContent) -> bool {
self.base_info.handle_state_event(event)
}
pub(crate) fn update_notification_count(
&mut self,
notification_counts: UnreadNotificationsCount,
) {
self.notification_counts = notification_counts;
}
pub(crate) fn update_summary(&mut self, summary: &RumaSummary) -> bool {
let mut changed = false;
if !summary.is_empty() {
if !summary.heroes.is_empty() {
self.summary.heroes = summary.heroes.clone();
changed = true;
}
if let Some(joined) = summary.joined_member_count {
self.summary.joined_member_count = joined.into();
changed = true;
}
if let Some(invited) = summary.invited_member_count {
self.summary.invited_member_count = invited.into();
changed = true;
}
}
changed
}
/// The number of active members (invited + joined) in the room.
///
/// The return value is saturated at `u64::MAX`.
pub fn active_members_count(&self) -> u64 {
self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
}
}