use std::{
collections::{BTreeMap, BTreeSet, HashSet},
sync::{Arc, atomic::AtomicBool},
};
use as_variant::as_variant;
use bitflags::bitflags;
use eyeball::Subscriber;
use matrix_sdk_common::{ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK};
use ruma::{
EventId, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
RoomAliasId, RoomId, RoomVersionId,
api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
events::{
AnyPossiblyRedactedStateEventContent, AnyStrippedStateEvent, AnySyncStateEvent,
AnySyncTimelineEvent, StateEventType,
call::member::{
CallMemberStateKey, MembershipData, PossiblyRedactedCallMemberEventContent,
},
direct::OwnedDirectUserIdentifier,
member_hints::PossiblyRedactedMemberHintsEventContent,
room::{
avatar::{self, PossiblyRedactedRoomAvatarEventContent},
canonical_alias::PossiblyRedactedRoomCanonicalAliasEventContent,
encryption::PossiblyRedactedRoomEncryptionEventContent,
guest_access::{GuestAccess, PossiblyRedactedRoomGuestAccessEventContent},
history_visibility::{
HistoryVisibility, PossiblyRedactedRoomHistoryVisibilityEventContent,
},
join_rules::{JoinRule, PossiblyRedactedRoomJoinRulesEventContent},
name::PossiblyRedactedRoomNameEventContent,
pinned_events::{
PossiblyRedactedRoomPinnedEventsEventContent, RoomPinnedEventsEventContent,
},
redaction::SyncRoomRedactionEvent,
tombstone::PossiblyRedactedRoomTombstoneEventContent,
topic::PossiblyRedactedRoomTopicEventContent,
},
rtc::notification::CallIntent,
tag::{TagEventContent, TagName, Tags},
},
room::RoomType,
room_version_rules::{RedactionRules, RoomVersionRules},
serde::Raw,
};
use serde::{Deserialize, Serialize};
use tokio::sync::MutexGuard;
use tracing::{field::debug, info, instrument, warn};
use super::{
AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
RoomHero, RoomNotableTags, RoomState, RoomSummary,
};
use crate::{
MinimalStateEvent, StateChanges, StoreError,
deserialized_responses::RawSyncOrStrippedState,
latest_event::LatestEventValue,
notification_settings::RoomNotificationMode,
read_receipts::RoomReadReceipts,
room::call::CallIntentConsensus,
store::{IncorrectMutexGuardError, SaveLockedStateStore, StateStoreExt},
sync::UnreadNotificationsCount,
utils::{AnyStateEventEnum, RawStateEventWithKeys},
};
const DEFAULT_MAX_POWER_LEVEL: i64 = 100;
impl Room {
pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
self.info.subscribe()
}
pub fn clone_info(&self) -> RoomInfo {
self.info.get()
}
pub async fn update_room_info<F>(&self, f: F)
where
F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
{
self.update_room_info_with_store_guard(&self.store.lock().lock().await, f)
.expect("should have correct mutex!")
}
pub fn update_room_info_with_store_guard<F>(
&self,
guard: &MutexGuard<'_, ()>,
f: F,
) -> Result<(), IncorrectMutexGuardError>
where
F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
{
if !std::ptr::eq(MutexGuard::mutex(guard), self.store.lock()) {
return Err(IncorrectMutexGuardError);
}
let (info, mut reasons) = f(self.clone_info());
self.info.set(info);
if reasons.is_empty() {
reasons = RoomInfoNotableUpdateReasons::NONE;
}
let _ = self
.room_info_notable_update_sender
.send(RoomInfoNotableUpdate { room_id: self.room_id.clone(), reasons });
Ok(())
}
pub async fn update_and_save_room_info<F>(&self, f: F) -> Result<(), StoreError>
where
F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
{
self.update_and_save_room_info_with_store_guard(&self.store.lock().lock().await, f).await
}
pub async fn update_and_save_room_info_with_store_guard<F>(
&self,
guard: &MutexGuard<'_, ()>,
f: F,
) -> Result<(), StoreError>
where
F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
{
let (info, reasons) = f(self.clone_info());
let mut changes = StateChanges::default();
changes.add_room(info.clone());
self.store.save_changes_with_guard(guard, &changes).await?;
self.update_room_info_with_store_guard(guard, |_| (info, reasons))?;
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BaseRoomInfo {
pub(crate) avatar: Option<MinimalStateEvent<PossiblyRedactedRoomAvatarEventContent>>,
pub(crate) canonical_alias:
Option<MinimalStateEvent<PossiblyRedactedRoomCanonicalAliasEventContent>>,
pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
pub(crate) encryption: Option<PossiblyRedactedRoomEncryptionEventContent>,
pub(crate) guest_access: Option<MinimalStateEvent<PossiblyRedactedRoomGuestAccessEventContent>>,
pub(crate) history_visibility:
Option<MinimalStateEvent<PossiblyRedactedRoomHistoryVisibilityEventContent>>,
pub(crate) join_rules: Option<MinimalStateEvent<PossiblyRedactedRoomJoinRulesEventContent>>,
pub(crate) max_power_level: i64,
pub(crate) member_hints: Option<MinimalStateEvent<PossiblyRedactedMemberHintsEventContent>>,
pub(crate) name: Option<MinimalStateEvent<PossiblyRedactedRoomNameEventContent>>,
pub(crate) tombstone: Option<MinimalStateEvent<PossiblyRedactedRoomTombstoneEventContent>>,
pub(crate) topic: Option<MinimalStateEvent<PossiblyRedactedRoomTopicEventContent>>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub(crate) rtc_member_events:
BTreeMap<CallMemberStateKey, MinimalStateEvent<PossiblyRedactedCallMemberEventContent>>,
#[serde(default)]
pub(crate) is_marked_unread: bool,
#[serde(default)]
pub(crate) is_marked_unread_source: AccountDataSource,
#[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
pub(crate) notable_tags: RoomNotableTags,
pub(crate) pinned_events: Option<PossiblyRedactedRoomPinnedEventsEventContent>,
}
impl BaseRoomInfo {
pub fn new() -> Self {
Self::default()
}
pub fn room_version(&self) -> Option<&RoomVersionId> {
Some(&self.create.as_ref()?.content.room_version)
}
pub fn handle_state_event<T: AnyStateEventEnum>(
&mut self,
raw_event: &mut RawStateEventWithKeys<T>,
) -> bool {
match (&raw_event.event_type, raw_event.state_key.as_str()) {
(StateEventType::RoomEncryption, "") => {
if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomEncryption)
}) && event.content.algorithm.is_some()
{
self.encryption = Some(event.content);
true
} else {
false
}
}
(StateEventType::RoomAvatar, "") => {
if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomAvatar)
}) {
self.avatar = Some(event);
true
} else {
self.avatar.take().is_some()
}
}
(StateEventType::RoomName, "") => {
if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomName)
}) {
self.name = Some(event);
true
} else {
self.name.take().is_some()
}
}
(StateEventType::RoomCreate, "") if self.create.is_none() => {
if let Some(any_event) = raw_event.deserialize()
&& let Some(content) = as_variant!(
any_event.get_content(),
AnyPossiblyRedactedStateEventContent::RoomCreate
)
{
self.create = Some(MinimalStateEvent {
content: RoomCreateWithCreatorEventContent::from_event_content(
content,
any_event.get_sender().to_owned(),
),
event_id: any_event.get_event_id().map(ToOwned::to_owned),
});
true
} else {
false
}
}
(StateEventType::RoomHistoryVisibility, "") => {
if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
as_variant!(
any_event,
AnyPossiblyRedactedStateEventContent::RoomHistoryVisibility
)
}) {
self.history_visibility = Some(event);
true
} else {
self.history_visibility.take().is_some()
}
}
(StateEventType::RoomGuestAccess, "") => {
if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomGuestAccess)
}) {
self.guest_access = Some(event);
true
} else {
self.guest_access.take().is_some()
}
}
(StateEventType::MemberHints, "") => {
if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
as_variant!(any_event, AnyPossiblyRedactedStateEventContent::MemberHints)
}) {
self.member_hints = Some(event);
true
} else {
self.member_hints.take().is_some()
}
}
(StateEventType::RoomJoinRules, "") => {
if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomJoinRules)
}) {
match &event.content.join_rule {
JoinRule::Invite
| JoinRule::Knock
| JoinRule::Private
| JoinRule::Restricted(_)
| JoinRule::KnockRestricted(_)
| JoinRule::Public => {
self.join_rules = Some(event);
true
}
r => {
warn!(join_rule = ?r.as_str(), "Encountered a custom join rule, skipping");
self.join_rules.take().is_some()
}
}
} else {
self.join_rules.take().is_some()
}
}
(StateEventType::RoomCanonicalAlias, "") => {
if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomCanonicalAlias)
}) {
self.canonical_alias = Some(event);
true
} else {
self.canonical_alias.take().is_some()
}
}
(StateEventType::RoomTopic, "") => {
if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTopic)
}) {
self.topic = Some(event);
true
} else {
self.topic.take().is_some()
}
}
(StateEventType::RoomTombstone, "") => {
if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTombstone)
}) {
self.tombstone = Some(event);
true
} else {
self.tombstone.take().is_some()
}
}
(StateEventType::RoomPowerLevels, "") => {
if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPowerLevels)
}) {
let new_max = i64::from(
event
.content
.users
.values()
.fold(event.content.users_default, |max_pl, user_pl| {
max_pl.max(*user_pl)
}),
);
if self.max_power_level != new_max {
self.max_power_level = new_max;
true
} else {
false
}
} else if self.max_power_level != DEFAULT_MAX_POWER_LEVEL {
self.max_power_level = DEFAULT_MAX_POWER_LEVEL;
true
} else {
false
}
}
(StateEventType::CallMember, _) => {
if let Ok(call_member_key) = raw_event.state_key.parse::<CallMemberStateKey>() {
if let Some(any_event) = raw_event.deserialize()
&& let Some(content) = as_variant!(
any_event.get_content(),
AnyPossiblyRedactedStateEventContent::CallMember
)
{
let mut event = MinimalStateEvent {
content,
event_id: any_event.get_event_id().map(ToOwned::to_owned),
};
if let Some(origin_server_ts) = any_event.get_origin_server_ts() {
event.content.set_created_ts_if_none(origin_server_ts);
}
self.rtc_member_events.insert(call_member_key, event);
self.rtc_member_events
.retain(|_, ev| !ev.content.active_memberships(None).is_empty());
true
} else {
self.rtc_member_events.remove(&call_member_key).is_some()
}
} else {
false
}
}
(StateEventType::RoomPinnedEvents, "") => {
if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPinnedEvents)
}) {
self.pinned_events = Some(event.content);
true
} else {
self.pinned_events.take().is_some()
}
}
_ => false,
}
}
pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
let redaction_rules = self
.room_version()
.and_then(|room_version| room_version.rules())
.unwrap_or(ROOM_VERSION_RULES_FALLBACK)
.redaction;
if let Some(ev) = &mut self.avatar
&& ev.event_id.as_deref() == Some(redacts)
{
ev.redact(&redaction_rules);
} else if let Some(ev) = &mut self.canonical_alias
&& ev.event_id.as_deref() == Some(redacts)
{
ev.redact(&redaction_rules);
} else if let Some(ev) = &mut self.create
&& ev.event_id.as_deref() == Some(redacts)
{
ev.redact(&redaction_rules);
} else if let Some(ev) = &mut self.guest_access
&& ev.event_id.as_deref() == Some(redacts)
{
ev.redact(&redaction_rules);
} else if let Some(ev) = &mut self.history_visibility
&& ev.event_id.as_deref() == Some(redacts)
{
ev.redact(&redaction_rules);
} else if let Some(ev) = &mut self.join_rules
&& ev.event_id.as_deref() == Some(redacts)
{
ev.redact(&redaction_rules);
} else if let Some(ev) = &mut self.name
&& ev.event_id.as_deref() == Some(redacts)
{
ev.redact(&redaction_rules);
} else if let Some(ev) = &mut self.tombstone
&& ev.event_id.as_deref() == Some(redacts)
{
ev.redact(&redaction_rules);
} else if let Some(ev) = &mut self.topic
&& ev.event_id.as_deref() == Some(redacts)
{
ev.redact(&redaction_rules);
} else {
self.rtc_member_events
.retain(|_, member_event| member_event.event_id.as_deref() != Some(redacts));
}
}
pub fn handle_notable_tags(&mut self, tags: &Tags) {
let mut notable_tags = RoomNotableTags::empty();
if tags.contains_key(&TagName::Favorite) {
notable_tags.insert(RoomNotableTags::FAVOURITE);
}
if tags.contains_key(&TagName::LowPriority) {
notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
}
self.notable_tags = notable_tags;
}
}
impl Default for BaseRoomInfo {
fn default() -> Self {
Self {
avatar: None,
canonical_alias: None,
create: None,
dm_targets: Default::default(),
member_hints: None,
encryption: None,
guest_access: None,
history_visibility: None,
join_rules: None,
max_power_level: DEFAULT_MAX_POWER_LEVEL,
name: None,
tombstone: None,
topic: None,
rtc_member_events: BTreeMap::new(),
is_marked_unread: false,
is_marked_unread_source: AccountDataSource::Unstable,
notable_tags: RoomNotableTags::empty(),
pinned_events: None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RoomInfo {
#[serde(default, alias = "version")]
pub(crate) data_format_version: u8,
pub(crate) room_id: OwnedRoomId,
pub(crate) room_state: RoomState,
pub(crate) notification_counts: UnreadNotificationsCount,
pub(crate) summary: RoomSummary,
pub(crate) members_synced: bool,
pub(crate) last_prev_batch: Option<String>,
pub(crate) sync_info: SyncInfo,
pub(crate) encryption_state_synced: bool,
#[serde(default)]
pub(crate) latest_event_value: LatestEventValue,
#[serde(default)]
pub(crate) read_receipts: RoomReadReceipts,
pub(crate) base_info: Box<BaseRoomInfo>,
#[serde(skip)]
pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) cached_display_name: Option<RoomDisplayName>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
#[serde(default)]
pub(crate) recency_stamp: Option<RoomRecencyStamp>,
}
impl RoomInfo {
#[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
Self {
data_format_version: 1,
room_id: room_id.into(),
room_state,
notification_counts: Default::default(),
summary: Default::default(),
members_synced: false,
last_prev_batch: None,
sync_info: SyncInfo::NoState,
encryption_state_synced: false,
latest_event_value: LatestEventValue::default(),
read_receipts: Default::default(),
base_info: Box::new(BaseRoomInfo::new()),
warned_about_unknown_room_version_rules: Arc::new(false.into()),
cached_display_name: None,
cached_user_defined_notification_mode: None,
recency_stamp: None,
}
}
pub fn mark_as_joined(&mut self) {
self.set_state(RoomState::Joined);
}
pub fn mark_as_left(&mut self) {
self.set_state(RoomState::Left);
}
pub fn mark_as_invited(&mut self) {
self.set_state(RoomState::Invited);
}
pub fn mark_as_knocked(&mut self) {
self.set_state(RoomState::Knocked);
}
pub fn mark_as_banned(&mut self) {
self.set_state(RoomState::Banned);
}
pub fn set_state(&mut self, room_state: RoomState) {
self.room_state = room_state;
}
pub fn mark_members_synced(&mut self) {
self.members_synced = true;
}
pub fn mark_members_missing(&mut self) {
self.members_synced = false;
}
pub fn are_members_synced(&self) -> bool {
self.members_synced
}
pub fn mark_state_partially_synced(&mut self) {
self.sync_info = SyncInfo::PartiallySynced;
}
pub fn mark_state_fully_synced(&mut self) {
self.sync_info = SyncInfo::FullySynced;
}
pub fn mark_state_not_synced(&mut self) {
self.sync_info = SyncInfo::NoState;
}
pub fn mark_encryption_state_synced(&mut self) {
self.encryption_state_synced = true;
}
pub fn mark_encryption_state_missing(&mut self) {
self.encryption_state_synced = false;
}
pub 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_owned());
true
} else {
false
}
}
pub fn state(&self) -> RoomState {
self.room_state
}
#[cfg(not(feature = "experimental-encrypted-state-events"))]
pub fn encryption_state(&self) -> EncryptionState {
if !self.encryption_state_synced {
EncryptionState::Unknown
} else if self.base_info.encryption.is_some() {
EncryptionState::Encrypted
} else {
EncryptionState::NotEncrypted
}
}
#[cfg(feature = "experimental-encrypted-state-events")]
pub fn encryption_state(&self) -> EncryptionState {
if !self.encryption_state_synced {
EncryptionState::Unknown
} else {
self.base_info
.encryption
.as_ref()
.map(|state| {
if state.encrypt_state_events {
EncryptionState::StateEncrypted
} else {
EncryptionState::Encrypted
}
})
.unwrap_or(EncryptionState::NotEncrypted)
}
}
pub fn set_encryption_event(
&mut self,
event: Option<PossiblyRedactedRoomEncryptionEventContent>,
) {
self.base_info.encryption = event;
}
pub fn handle_encryption_state(
&mut self,
requested_required_states: &[(StateEventType, String)],
) {
if requested_required_states
.iter()
.any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
{
self.mark_encryption_state_synced();
}
}
pub fn handle_state_event(
&mut self,
raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
) -> bool {
if raw_event.event_type == StateEventType::MemberHints
&& let Some(AnySyncStateEvent::MemberHints(new_hints)) = raw_event.deserialize()
&& let (Some(current_hints), Some(new)) =
(&self.base_info.member_hints, new_hints.as_original())
&& current_hints
.content
.service_members
.as_ref()
.is_some_and(|current_members| *current_members != new.content.service_members)
{
self.summary.active_service_members = None;
}
let base_info_has_been_modified = self.base_info.handle_state_event(raw_event);
if raw_event.event_type == StateEventType::RoomEncryption && raw_event.state_key.is_empty()
{
self.mark_encryption_state_synced();
}
base_info_has_been_modified
}
pub fn handle_stripped_state_event(
&mut self,
raw_event: &mut RawStateEventWithKeys<AnyStrippedStateEvent>,
) -> bool {
self.base_info.handle_state_event(raw_event)
}
#[instrument(skip_all, fields(redacts))]
pub fn handle_redaction(
&mut self,
event: &SyncRoomRedactionEvent,
_raw: &Raw<SyncRoomRedactionEvent>,
) {
let redaction_rules = self.room_version_rules_or_default().redaction;
let Some(redacts) = event.redacts(&redaction_rules) else {
info!("Can't apply redaction, redacts field is missing");
return;
};
tracing::Span::current().record("redacts", debug(redacts));
self.base_info.handle_redaction(redacts);
}
pub fn avatar_url(&self) -> Option<&MxcUri> {
self.base_info.avatar.as_ref().and_then(|e| e.content.url.as_deref())
}
pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
self.base_info.avatar = url.map(|url| {
let mut content = PossiblyRedactedRoomAvatarEventContent::new();
content.url = Some(url);
MinimalStateEvent { content, event_id: None }
});
}
pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
self.base_info.avatar.as_ref().and_then(|e| e.content.info.as_deref())
}
pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
self.notification_counts = notification_counts;
}
pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
let mut changed = false;
if !summary.is_empty() {
if !summary.heroes.is_empty() {
self.summary.room_heroes = summary
.heroes
.iter()
.map(|hero_id| RoomHero {
user_id: hero_id.to_owned(),
display_name: None,
avatar_url: None,
})
.collect();
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;
}
}
if changed {
self.summary.active_service_members = None;
}
changed
}
pub(crate) fn update_joined_member_count(&mut self, count: u64) {
self.summary.joined_member_count = count;
}
pub(crate) fn update_invited_member_count(&mut self, count: u64) {
self.summary.invited_member_count = count;
}
pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
self.summary.room_heroes = heroes;
}
pub fn heroes(&self) -> &[RoomHero] {
&self.summary.room_heroes
}
pub fn active_members_count(&self) -> u64 {
self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
}
pub fn invited_members_count(&self) -> u64 {
self.summary.invited_member_count
}
pub fn joined_members_count(&self) -> u64 {
self.summary.joined_member_count
}
pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
self.base_info.canonical_alias.as_ref()?.content.alias.as_deref()
}
pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
self.base_info
.canonical_alias
.as_ref()
.map(|ev| ev.content.alt_aliases.as_ref())
.unwrap_or_default()
}
pub fn room_id(&self) -> &RoomId {
&self.room_id
}
pub fn room_version(&self) -> Option<&RoomVersionId> {
self.base_info.room_version()
}
pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
use std::sync::atomic::Ordering;
self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
|| {
if self
.warned_about_unknown_room_version_rules
.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
}
ROOM_VERSION_RULES_FALLBACK
},
)
}
pub fn room_type(&self) -> Option<&RoomType> {
self.base_info.create.as_ref()?.content.room_type.as_ref()
}
pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
Some(self.base_info.create.as_ref()?.content.creators())
}
pub(super) fn guest_access(&self) -> &GuestAccess {
self.base_info
.guest_access
.as_ref()
.and_then(|event| event.content.guest_access.as_ref())
.unwrap_or(&GuestAccess::Forbidden)
}
pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
Some(&self.base_info.history_visibility.as_ref()?.content.history_visibility)
}
pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
self.history_visibility().unwrap_or(&HistoryVisibility::Shared)
}
pub fn join_rule(&self) -> Option<&JoinRule> {
Some(&self.base_info.join_rules.as_ref()?.content.join_rule)
}
pub fn service_members(&self) -> Option<&BTreeSet<OwnedUserId>> {
self.base_info.member_hints.as_ref()?.content.service_members.as_ref()
}
pub fn name(&self) -> Option<&str> {
self.base_info.name.as_ref()?.content.name.as_deref().filter(|name| !name.is_empty())
}
pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
Some(&self.base_info.create.as_ref()?.content)
}
pub fn tombstone(&self) -> Option<&PossiblyRedactedRoomTombstoneEventContent> {
Some(&self.base_info.tombstone.as_ref()?.content)
}
pub fn topic(&self) -> Option<&str> {
self.base_info.topic.as_ref()?.content.topic.as_deref()
}
fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
let mut v = self
.base_info
.rtc_member_events
.iter()
.flat_map(|(state_key, ev)| {
ev.content.active_memberships(None).into_iter().map(move |m| (state_key.clone(), m))
})
.collect::<Vec<_>>();
v.sort_by_key(|(_, m)| m.created_ts());
v
}
fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
self.active_matrix_rtc_memberships()
.into_iter()
.filter(|(_user_id, m)| m.is_room_call())
.collect()
}
pub fn has_active_room_call(&self) -> bool {
!self.active_room_call_memberships().is_empty()
}
pub fn active_room_call_consensus_intent(&self) -> CallIntentConsensus {
let memberships = self.active_room_call_memberships();
let total_count: u64 = memberships.len() as u64;
if total_count == 0 {
return CallIntentConsensus::None;
}
let mut consensus_intent: Option<CallIntent> = None;
let mut agreeing_count: u64 = 0;
for (_, data) in memberships.iter() {
if let Some(intent) = data.call_intent() {
match &consensus_intent {
None => {
consensus_intent = Some(intent.clone());
agreeing_count = 1;
}
Some(current) if current == intent => {
agreeing_count += 1;
}
Some(_) => return CallIntentConsensus::None,
}
}
}
match consensus_intent {
None => CallIntentConsensus::None,
Some(intent) if agreeing_count == total_count => {
CallIntentConsensus::Full(intent)
}
Some(intent) => {
CallIntentConsensus::Partial { intent, agreeing_count, total_count }
}
}
}
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
self.active_room_call_memberships()
.iter()
.map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
.collect()
}
pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
self.latest_event_value = new_value;
}
pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
self.recency_stamp = Some(stamp);
}
pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
self.base_info.pinned_events.clone().and_then(|c| c.pinned)
}
pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
self.base_info
.pinned_events
.as_ref()
.and_then(|content| content.pinned.as_deref())
.is_some_and(|pinned| pinned.contains(&event_id.to_owned()))
}
pub fn read_receipts(&self) -> &RoomReadReceipts {
&self.read_receipts
}
pub fn set_read_receipts(&mut self, read_receipts: RoomReadReceipts) {
self.read_receipts = read_receipts;
}
#[instrument(skip_all, fields(room_id = ?self.room_id))]
pub(crate) async fn apply_migrations(&mut self, store: SaveLockedStateStore) -> bool {
let mut migrated = false;
if self.data_format_version < 1 {
info!("Migrating room info to version 1");
match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
Ok(Some(raw_event)) => match raw_event.deserialize() {
Ok(event) => {
self.base_info.handle_notable_tags(&event.content.tags);
}
Err(error) => {
warn!("Failed to deserialize room tags: {error}");
}
},
Ok(_) => {
}
Err(error) => {
warn!("Failed to load room tags: {error}");
}
}
match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
{
Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
if let Some(mut raw_event) =
RawStateEventWithKeys::try_from_raw_state_event(raw_event.cast())
{
self.handle_state_event(&mut raw_event);
}
}
Ok(_) => {
}
Err(error) => {
warn!("Failed to load room pinned events: {error}");
}
}
self.data_format_version = 1;
migrated = true;
}
migrated
}
pub fn active_service_member_count(&self) -> Option<u64> {
self.summary.active_service_members
}
pub fn update_active_service_member_count(&mut self, count: Option<u64>) {
self.summary.active_service_members = count;
}
}
#[repr(transparent)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
#[serde(transparent)]
pub struct RoomRecencyStamp(u64);
impl From<u64> for RoomRecencyStamp {
fn from(value: u64) -> Self {
Self(value)
}
}
impl From<RoomRecencyStamp> for u64 {
fn from(value: RoomRecencyStamp) -> Self {
value.0
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) enum SyncInfo {
NoState,
PartiallySynced,
FullySynced,
}
pub fn apply_redaction(
event: &Raw<AnySyncTimelineEvent>,
raw_redaction: &Raw<SyncRoomRedactionEvent>,
rules: &RedactionRules,
) -> Option<Raw<AnySyncTimelineEvent>> {
use ruma::canonical_json::{RedactedBecause, redact_in_place};
let mut event_json = match event.deserialize_as() {
Ok(json) => json,
Err(e) => {
warn!("Failed to deserialize latest event: {e}");
return None;
}
};
let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
Ok(rb) => rb,
Err(e) => {
warn!("Redaction event is not valid canonical JSON: {e}");
return None;
}
};
let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
if let Err(e) = redact_result {
warn!("Failed to redact event: {e}");
return None;
}
let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
Some(raw.cast_unchecked())
}
#[derive(Debug, Clone)]
pub struct RoomInfoNotableUpdate {
pub room_id: OwnedRoomId,
pub reasons: RoomInfoNotableUpdateReasons,
}
bitflags! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RoomInfoNotableUpdateReasons: u8 {
const RECENCY_STAMP = 0b0000_0001;
const LATEST_EVENT = 0b0000_0010;
const READ_RECEIPT = 0b0000_0100;
const UNREAD_MARKER = 0b0000_1000;
const MEMBERSHIP = 0b0001_0000;
const DISPLAY_NAME = 0b0010_0000;
const ACTIVE_SERVICE_MEMBERS = 0b0100_0000;
const NONE = 0b1000_0000;
}
}
impl Default for RoomInfoNotableUpdateReasons {
fn default() -> Self {
Self::empty()
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeSet, str::FromStr, sync::Arc, time::Duration};
use assert_matches::assert_matches;
use futures_util::future::{self, Either};
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
use gloo_timers::future::sleep;
use matrix_sdk_common::executor::spawn;
use matrix_sdk_test::{async_test, event_factory::EventFactory};
use ruma::{
assign,
events::{
AnyRoomAccountDataEvent,
room::pinned_events::RoomPinnedEventsEventContent,
tag::{TagInfo, TagName, Tags, UserTagName},
},
owned_event_id, owned_mxc_uri, owned_user_id, room_id,
serde::Raw,
user_id,
};
use serde_json::json;
use similar_asserts::assert_eq;
use tokio::sync::Mutex;
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
use tokio::time::sleep;
use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
use crate::{
RawStateEventWithKeys, Room, RoomDisplayName, RoomHero, RoomInfoNotableUpdateReasons,
RoomState, StateChanges, StateStore,
notification_settings::RoomNotificationMode,
room::{RoomNotableTags, RoomSummary},
store::{IntoStateStore, MemoryStore, RoomLoadSettings, SaveLockedStateStore},
sync::UnreadNotificationsCount,
};
#[test]
fn test_room_info_serialization() {
let info = RoomInfo {
data_format_version: 1,
room_id: room_id!("!gda78o:server.tld").into(),
room_state: RoomState::Invited,
notification_counts: UnreadNotificationsCount {
highlight_count: 1,
notification_count: 2,
},
summary: RoomSummary {
room_heroes: vec![RoomHero {
user_id: owned_user_id!("@somebody:example.org"),
display_name: None,
avatar_url: None,
}],
joined_member_count: 5,
invited_member_count: 0,
active_service_members: None,
},
members_synced: true,
last_prev_batch: Some("pb".to_owned()),
sync_info: SyncInfo::FullySynced,
encryption_state_synced: true,
latest_event_value: LatestEventValue::None,
base_info: Box::new(
assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")]).into()) }),
),
read_receipts: Default::default(),
warned_about_unknown_room_version_rules: Arc::new(false.into()),
cached_display_name: None,
cached_user_defined_notification_mode: None,
recency_stamp: Some(42.into()),
};
let info_json = json!({
"data_format_version": 1,
"room_id": "!gda78o:server.tld",
"room_state": "Invited",
"notification_counts": {
"highlight_count": 1,
"notification_count": 2,
},
"summary": {
"room_heroes": [{
"user_id": "@somebody:example.org",
"display_name": null,
"avatar_url": null
}],
"joined_member_count": 5,
"invited_member_count": 0,
},
"members_synced": true,
"last_prev_batch": "pb",
"sync_info": "FullySynced",
"encryption_state_synced": true,
"latest_event_value": "None",
"base_info": {
"avatar": null,
"canonical_alias": null,
"create": null,
"dm_targets": [],
"encryption": null,
"guest_access": null,
"history_visibility": null,
"is_marked_unread": false,
"is_marked_unread_source": "Unstable",
"join_rules": null,
"max_power_level": 100,
"member_hints": null,
"name": null,
"tombstone": null,
"topic": null,
"pinned_events": {
"pinned": ["$a"]
},
},
"read_receipts": {
"num_unread": 0,
"num_mentions": 0,
"num_notifications": 0,
"latest_active": null,
"pending": [],
},
"recency_stamp": 42,
});
assert_eq!(serde_json::to_value(info).unwrap(), info_json);
}
#[async_test]
async fn test_room_info_migration_v1() {
let store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
let room_info_json = json!({
"room_id": "!gda78o:server.tld",
"room_state": "Joined",
"notification_counts": {
"highlight_count": 1,
"notification_count": 2,
},
"summary": {
"room_heroes": [{
"user_id": "@somebody:example.org",
"display_name": null,
"avatar_url": null
}],
"joined_member_count": 5,
"invited_member_count": 0,
},
"members_synced": true,
"last_prev_batch": "pb",
"sync_info": "FullySynced",
"encryption_state_synced": true,
"latest_event": {
"event": {
"encryption_info": null,
"event": {
"sender": "@u:i.uk",
},
},
},
"base_info": {
"avatar": null,
"canonical_alias": null,
"create": null,
"dm_targets": [],
"encryption": null,
"guest_access": null,
"history_visibility": null,
"join_rules": null,
"max_power_level": 100,
"name": null,
"tombstone": null,
"topic": null,
},
"read_receipts": {
"num_unread": 0,
"num_mentions": 0,
"num_notifications": 0,
"latest_active": null,
"pending": []
},
"recency_stamp": 42,
});
let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
assert_eq!(room_info.data_format_version, 0);
assert!(room_info.base_info.notable_tags.is_empty());
assert!(room_info.base_info.pinned_events.is_none());
assert!(room_info.apply_migrations(store.clone()).await);
assert_eq!(room_info.data_format_version, 1);
assert!(room_info.base_info.notable_tags.is_empty());
assert!(room_info.base_info.pinned_events.is_none());
assert!(!room_info.apply_migrations(store.clone()).await);
assert_eq!(room_info.data_format_version, 1);
assert!(room_info.base_info.notable_tags.is_empty());
assert!(room_info.base_info.pinned_events.is_none());
let mut changes = StateChanges::default();
let f = EventFactory::new().room(&room_info.room_id).sender(user_id!("@example:localhost"));
let mut tags = Tags::new();
tags.insert(TagName::Favorite, TagInfo::new());
tags.insert(TagName::User(UserTagName::from_str("u.work").unwrap()), TagInfo::new());
let raw_tag_event: Raw<AnyRoomAccountDataEvent> = f.tag(tags).into();
let tag_event = raw_tag_event.deserialize().unwrap();
changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
let raw_pinned_events_event: Raw<_> = f
.room_pinned_events(vec![owned_event_id!("$a"), owned_event_id!("$b")])
.into_raw_sync_state();
let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
store.save_changes(&changes).await.unwrap();
room_info.data_format_version = 0;
assert!(room_info.apply_migrations(store.clone()).await);
assert_eq!(room_info.data_format_version, 1);
assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
assert!(room_info.base_info.pinned_events.is_some());
let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
assert_eq!(new_room_info.data_format_version, 1);
}
#[test]
fn test_room_info_deserialization() {
let info_json = json!({
"room_id": "!gda78o:server.tld",
"room_state": "Joined",
"notification_counts": {
"highlight_count": 1,
"notification_count": 2,
},
"summary": {
"room_heroes": [{
"user_id": "@somebody:example.org",
"display_name": "Somebody",
"avatar_url": "mxc://example.org/abc"
}],
"joined_member_count": 5,
"invited_member_count": 0,
},
"members_synced": true,
"last_prev_batch": "pb",
"sync_info": "FullySynced",
"encryption_state_synced": true,
"base_info": {
"avatar": null,
"canonical_alias": null,
"create": null,
"dm_targets": [],
"encryption": null,
"guest_access": null,
"history_visibility": null,
"join_rules": null,
"max_power_level": 100,
"member_hints": null,
"name": null,
"tombstone": null,
"topic": null,
},
"cached_display_name": { "Calculated": "lol" },
"cached_user_defined_notification_mode": "Mute",
"recency_stamp": 42,
});
let info: RoomInfo = serde_json::from_value(info_json).unwrap();
assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
assert_eq!(info.room_state, RoomState::Joined);
assert_eq!(info.notification_counts.highlight_count, 1);
assert_eq!(info.notification_counts.notification_count, 2);
assert_eq!(
info.summary.room_heroes,
vec![RoomHero {
user_id: owned_user_id!("@somebody:example.org"),
display_name: Some("Somebody".to_owned()),
avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
}]
);
assert_eq!(info.summary.joined_member_count, 5);
assert_eq!(info.summary.invited_member_count, 0);
assert!(info.members_synced);
assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
assert_eq!(info.sync_info, SyncInfo::FullySynced);
assert!(info.encryption_state_synced);
assert_matches!(info.latest_event_value, LatestEventValue::None);
assert!(info.base_info.avatar.is_none());
assert!(info.base_info.canonical_alias.is_none());
assert!(info.base_info.create.is_none());
assert_eq!(info.base_info.dm_targets.len(), 0);
assert!(info.base_info.encryption.is_none());
assert!(info.base_info.guest_access.is_none());
assert!(info.base_info.history_visibility.is_none());
assert!(info.base_info.join_rules.is_none());
assert_eq!(info.base_info.max_power_level, 100);
assert!(info.base_info.member_hints.is_none());
assert!(info.base_info.name.is_none());
assert!(info.base_info.tombstone.is_none());
assert!(info.base_info.topic.is_none());
assert_eq!(
info.cached_display_name.as_ref(),
Some(&RoomDisplayName::Calculated("lol".to_owned())),
);
assert_eq!(
info.cached_user_defined_notification_mode.as_ref(),
Some(&RoomNotificationMode::Mute)
);
assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
}
#[test]
fn test_room_info_deserialization_without_optional_items() {
let info_json = json!({
"room_id": "!gda78o:server.tld",
"room_state": "Invited",
"notification_counts": {
"highlight_count": 1,
"notification_count": 2,
},
"summary": {
"room_heroes": [{
"user_id": "@somebody:example.org",
"display_name": "Somebody",
"avatar_url": "mxc://example.org/abc"
}],
"joined_member_count": 5,
"invited_member_count": 0,
},
"members_synced": true,
"last_prev_batch": "pb",
"sync_info": "FullySynced",
"encryption_state_synced": true,
"base_info": {
"avatar": null,
"canonical_alias": null,
"create": null,
"dm_targets": [],
"encryption": null,
"guest_access": null,
"history_visibility": null,
"join_rules": null,
"max_power_level": 100,
"name": null,
"tombstone": null,
"topic": null,
},
});
let info: RoomInfo = serde_json::from_value(info_json).unwrap();
assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
assert_eq!(info.room_state, RoomState::Invited);
assert_eq!(info.notification_counts.highlight_count, 1);
assert_eq!(info.notification_counts.notification_count, 2);
assert_eq!(
info.summary.room_heroes,
vec![RoomHero {
user_id: owned_user_id!("@somebody:example.org"),
display_name: Some("Somebody".to_owned()),
avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
}]
);
assert_eq!(info.summary.joined_member_count, 5);
assert_eq!(info.summary.invited_member_count, 0);
assert!(info.members_synced);
assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
assert_eq!(info.sync_info, SyncInfo::FullySynced);
assert!(info.encryption_state_synced);
assert!(info.base_info.avatar.is_none());
assert!(info.base_info.canonical_alias.is_none());
assert!(info.base_info.create.is_none());
assert_eq!(info.base_info.dm_targets.len(), 0);
assert!(info.base_info.encryption.is_none());
assert!(info.base_info.guest_access.is_none());
assert!(info.base_info.history_visibility.is_none());
assert!(info.base_info.join_rules.is_none());
assert_eq!(info.base_info.max_power_level, 100);
assert!(info.base_info.name.is_none());
assert!(info.base_info.tombstone.is_none());
assert!(info.base_info.topic.is_none());
}
#[test]
fn test_member_hints_with_different_contents_reset_computed_value() {
let expected = BTreeSet::from_iter([
owned_user_id!("@alice:example.org"),
owned_user_id!("@bob:example.org"),
]);
let info_json = json!({
"room_id": "!gda78o:server.tld",
"room_state": "Invited",
"notification_counts": {
"highlight_count": 1,
"notification_count": 2,
},
"summary": {
"room_heroes": [{
"user_id": "@somebody:example.org",
"display_name": "Somebody",
"avatar_url": "mxc://example.org/abc"
}],
"joined_member_count": 5,
"invited_member_count": 0,
"active_service_members": 2,
},
"members_synced": true,
"last_prev_batch": "pb",
"sync_info": "FullySynced",
"encryption_state_synced": true,
"base_info": {
"avatar": null,
"canonical_alias": null,
"create": null,
"dm_targets": [],
"encryption": null,
"guest_access": null,
"history_visibility": null,
"join_rules": null,
"max_power_level": 100,
"member_hints": {
"Original": {
"content": {
"service_members": ["@alice:example.org", "@bob:example.org"]
}
}
},
"name": null,
"tombstone": null,
"topic": null,
},
});
let info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
assert_eq!(info.summary.active_service_members, Some(2));
let mut info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
EventFactory::new()
.sender(user_id!("@alice:example.org"))
.member_hints(expected.clone())
.into_raw_sync_state(),
)
.expect("Expected member hints event is created");
info.handle_state_event(&mut raw_state_event_with_keys);
assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
assert_eq!(info.summary.active_service_members, Some(2));
let mut info: RoomInfo = serde_json::from_value(info_json).unwrap();
let new_member_hints = BTreeSet::from_iter([owned_user_id!("@alice:example.org")]);
let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
EventFactory::new()
.sender(user_id!("@alice:example.org"))
.member_hints(new_member_hints.clone())
.into_raw_sync_state(),
)
.expect("New member hints event is created");
info.handle_state_event(&mut raw_state_event_with_keys);
assert_eq!(
info.base_info.member_hints.unwrap().content.service_members.unwrap(),
new_member_hints
);
assert!(info.summary.active_service_members.is_none());
}
fn make_room_and_state_store(room_state: RoomState) -> (Room, SaveLockedStateStore) {
let state_store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
let user_id = user_id!("@user:localhost");
let room_id = room_id!("!room:localhost");
let (sender, _) = tokio::sync::broadcast::channel(1);
let room = Room::new(user_id, state_store.clone(), room_id, room_state, sender);
(room, state_store)
}
#[async_test]
async fn test_update_room_info_only_updates_in_memory_room_info() {
let (room, state_store) = make_room_and_state_store(RoomState::Joined);
let before = room.clone_info();
assert_eq!(before.state(), RoomState::Joined);
room.update_room_info(|mut info| {
info.mark_as_banned();
(info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
})
.await;
let after = room.clone_info();
assert_eq!(after.state(), RoomState::Banned);
let infos = state_store
.get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
.await
.expect("get room info");
assert!(infos.is_empty());
}
#[async_test]
async fn test_update_room_info_with_store_guard_only_updates_in_memory_room_info() {
let (room, state_store) = make_room_and_state_store(RoomState::Joined);
let before = room.clone_info();
assert_eq!(before.state(), RoomState::Joined);
room.update_room_info_with_store_guard(&state_store.lock().lock().await, |mut info| {
info.mark_as_banned();
(info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
})
.expect("update room info");
let after = room.clone_info();
assert_eq!(after.state(), RoomState::Banned);
let infos = state_store
.get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
.await
.expect("get room info");
assert!(infos.is_empty());
}
#[async_test]
async fn test_update_room_info_only_accepts_guard_for_underlying_mutex() {
let (room, state_store) = make_room_and_state_store(RoomState::Joined);
room.update_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
(info, RoomInfoNotableUpdateReasons::NONE)
})
.expect("room accepts guard for underlying mutex");
let mutex = Mutex::new(());
room.update_room_info_with_store_guard(&mutex.lock().await, |info| {
(info, RoomInfoNotableUpdateReasons::NONE)
})
.expect_err("room does not accept guard for unknown mutex");
}
#[async_test]
async fn test_update_and_save_room_info_updates_room_info_in_memory_and_store() {
let (room, state_store) = make_room_and_state_store(RoomState::Joined);
let before = room.clone_info();
assert_eq!(before.state(), RoomState::Joined);
room.update_and_save_room_info(|mut info| {
info.mark_as_banned();
(info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
})
.await
.expect("update and save room info");
let after = room.clone_info();
assert_eq!(after.state(), RoomState::Banned);
let infos = state_store
.get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
.await
.expect("get room info");
assert_eq!(infos.len(), 1);
assert_matches!(infos.first(), Some(info) => {
info.state() == RoomState::Banned
});
}
#[async_test]
async fn test_update_and_save_room_info_with_store_guard_updates_room_info_in_memory_and_store()
{
let (room, state_store) = make_room_and_state_store(RoomState::Joined);
let before = room.clone_info();
assert_eq!(before.state(), RoomState::Joined);
room.update_and_save_room_info_with_store_guard(
&state_store.lock().lock().await,
|mut info| {
info.mark_as_banned();
(info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
},
)
.await
.expect("update and save room info");
let after = room.clone_info();
assert_eq!(after.state(), RoomState::Banned);
let infos = state_store
.get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
.await
.expect("get room info");
assert_eq!(infos.len(), 1);
assert_matches!(infos.first(), Some(info) => {
info.state() == RoomState::Banned
});
}
#[async_test]
async fn test_update_and_save_room_info_only_accepts_guard_for_underlying_mutex() {
let (room, state_store) = make_room_and_state_store(RoomState::Joined);
room.update_and_save_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
(info, RoomInfoNotableUpdateReasons::NONE)
})
.await
.expect("room accepts guard for underlying mutex");
let mutex = Mutex::new(());
room.update_and_save_room_info_with_store_guard(&mutex.lock().await, |info| {
(info, RoomInfoNotableUpdateReasons::NONE)
})
.await
.expect_err("room does not accept guard for unknown mutex");
}
#[derive(Debug)]
struct Elapsed;
async fn timeout<F: Future + Unpin>(duration: Duration, f: F) -> Result<F::Output, Elapsed> {
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
{
match future::select(sleep(duration), f).await {
Either::Left(_) => return Err(Elapsed),
Either::Right((output, _)) => Ok(output),
}
}
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
{
tokio::time::timeout(duration, f).await.map_err(|_| Elapsed)
}
}
#[async_test]
async fn test_update_room_info_waits_to_acquire_lock_before_updating_room_info() {
let (room, state_store) = make_room_and_state_store(RoomState::Joined);
let lock_task = spawn({
let state_store = state_store.clone();
async move {
let lock = state_store.lock();
let _guard = lock.lock().await;
sleep(Duration::from_secs(5)).await;
}
});
let save_task = spawn(async move {
room.update_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
});
assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
timeout(Duration::from_millis(100), save_task)
.await
.expect("task completes before timeout")
.expect("task completes successfully")
});
}
#[async_test]
async fn test_update_and_save_room_info_waits_to_acquire_lock_before_updating_room_info() {
let (room, state_store) = make_room_and_state_store(RoomState::Joined);
let lock_task = spawn({
let state_store = state_store.clone();
async move {
let lock = state_store.lock();
let _guard = lock.lock().await;
sleep(Duration::from_secs(5)).await;
}
});
let save_task = spawn(async move {
room.update_and_save_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
});
assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
timeout(Duration::from_millis(100), save_task)
.await
.expect("task completes before timeout")
.expect("task completes successfully")
.expect("update and save room info");
});
}
}