diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 5781b49d6..dd7c68f33 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -69,7 +69,8 @@ pub use store::{ ThreadSubscriptionCatchupToken, }; pub use utils::{ - MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent, + MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, + RawSyncStateEventWithKeys, RedactedMinimalStateEvent, }; #[cfg(test)] diff --git a/crates/matrix-sdk-base/src/response_processors/room/msc4186/mod.rs b/crates/matrix-sdk-base/src/response_processors/room/msc4186/mod.rs index c1d406dc2..e26f3a761 100644 --- a/crates/matrix-sdk-base/src/response_processors/room/msc4186/mod.rs +++ b/crates/matrix-sdk-base/src/response_processors/room/msc4186/mod.rs @@ -18,6 +18,7 @@ use std::collections::BTreeMap; #[cfg(feature = "e2e-encryption")] use std::collections::BTreeSet; +use as_variant::as_variant; use matrix_sdk_common::timer; use ruma::{ JsOption, OwnedRoomId, RoomId, UserId, @@ -27,7 +28,7 @@ use ruma::{ }, assign, events::{ - AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, + AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, StateEventType, room::member::{MembershipState, RoomMemberEventContent}, }, serde::Raw, @@ -45,6 +46,7 @@ use crate::{ RoomState, store::BaseStateStore, sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate, State}, + utils::RawSyncStateEventWithKeys, }; /// Represent any kind of room updates. @@ -79,7 +81,7 @@ pub async fn update_any_room( // incomplete or staled already. We must only read state events from // `required_state`. let state = State::from_msc4186(room_response.required_state.clone()); - let (raw_state_events, state_events) = state.collect(&[]); + let mut raw_state_events = state.collect(&[]); let state_store = notification.state_store; @@ -92,7 +94,7 @@ pub async fn update_any_room( #[allow(unused_mut)] // Required for some feature flag combinations let (mut room, mut room_info, maybe_room_update_kind) = membership( context, - &state_events, + &mut raw_state_events, &invite_state_events, state_store, user_id, @@ -111,7 +113,7 @@ pub async fn update_any_room( state_events::sync::dispatch( context, - (&raw_state_events, &state_events), + raw_state_events, &mut room_info, ambiguity_cache, &mut new_user_ids, @@ -213,7 +215,7 @@ pub async fn update_any_room( /// or knocked room, depending of the membership event (if any). fn membership( context: &mut Context, - state_events: &[AnySyncStateEvent], + state_events: &mut [RawSyncStateEventWithKeys], invite_state_events: &Option<(Vec>, Vec)>, store: &BaseStateStore, user_id: &UserId, @@ -304,31 +306,33 @@ fn membership( fn own_membership( context: &mut Context, user_id: &UserId, - state_events: &[AnySyncStateEvent], + state_events: &mut [RawSyncStateEventWithKeys], room_info: &mut RoomInfo, ) { // Start from the last event; the first membership event we see in that order is // the last in the regular order, so that's the only one we need to // consider. - for event in state_events.iter().rev() { - if let AnySyncStateEvent::RoomMember(member) = &event { - // If this event updates the current user's membership, record that in the - // room_info. - if member.state_key() == user_id.as_str() { - let new_state: RoomState = member.membership().into(); + for event in state_events.iter_mut().rev() { + // If this event updates the current user's membership, record that in the + // room_info. + if event.event_type == StateEventType::RoomMember + && event.state_key.as_str() == user_id + && let Some(member) = event + .deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomMember)) + { + let new_state: RoomState = member.membership().into(); - if new_state != room_info.state() { - room_info.set_state(new_state); - // Update an existing notable update entry or create a new one - context - .room_info_notable_updates - .entry(room_info.room_id.to_owned()) - .or_default() - .insert(RoomInfoNotableUpdateReasons::MEMBERSHIP); - } - - break; + if new_state != room_info.state() { + room_info.set_state(new_state); + // Update an existing notable update entry or create a new one + context + .room_info_notable_updates + .entry(room_info.room_id.to_owned()) + .or_default() + .insert(RoomInfoNotableUpdateReasons::MEMBERSHIP); } + + break; } } } diff --git a/crates/matrix-sdk-base/src/response_processors/room/sync_v2.rs b/crates/matrix-sdk-base/src/response_processors/room/sync_v2.rs index d7c2eb428..5ea4fe577 100644 --- a/crates/matrix-sdk-base/src/response_processors/room/sync_v2.rs +++ b/crates/matrix-sdk-base/src/response_processors/room/sync_v2.rs @@ -67,11 +67,11 @@ pub async fn update_joined_room( let mut new_user_ids = BTreeSet::new(); let state = State::from_sync_v2(joined_room.state); - let (raw_state_events, state_events) = state.collect(&joined_room.timeline.events); + let raw_state_events = state.collect(&joined_room.timeline.events); state_events::sync::dispatch( context, - (&raw_state_events, &state_events), + raw_state_events, &mut room_info, ambiguity_cache, &mut new_user_ids, @@ -173,11 +173,11 @@ pub async fn update_left_room( room_info.handle_encryption_state(requested_required_states.for_room(room_id)); let state = State::from_sync_v2(left_room.state); - let (raw_state_events, state_events) = state.collect(&left_room.timeline.events); + let raw_state_events = state.collect(&left_room.timeline.events); state_events::sync::dispatch( context, - (&raw_state_events, &state_events), + raw_state_events, &mut room_info, ambiguity_cache, &mut (), diff --git a/crates/matrix-sdk-base/src/response_processors/state_events.rs b/crates/matrix-sdk-base/src/response_processors/state_events.rs index 50dbb1d01..d6caba00e 100644 --- a/crates/matrix-sdk-base/src/response_processors/state_events.rs +++ b/crates/matrix-sdk-base/src/response_processors/state_events.rs @@ -14,40 +14,41 @@ use std::collections::BTreeSet; +use as_variant::as_variant; use ruma::{ RoomId, - events::{ - AnySyncStateEvent, SyncStateEvent, - room::{create::RoomCreateEventContent, tombstone::RoomTombstoneEventContent}, - }, + events::{AnySyncStateEvent, SyncStateEvent}, serde::Raw, }; use serde::Deserialize; -use tracing::warn; +use tracing::{error, warn}; use super::Context; -use crate::store::BaseStateStore; +#[cfg(feature = "experimental-encrypted-state-events")] +use super::e2ee; +use crate::{store::BaseStateStore, utils::RawSyncStateEventWithKeys}; /// Collect [`AnySyncStateEvent`]. pub mod sync { - use std::{collections::BTreeSet, iter}; + use std::collections::BTreeSet; + use as_variant::as_variant; use ruma::{ OwnedUserId, RoomId, UserId, events::{ - AnySyncTimelineEvent, SyncStateEvent, - room::member::{MembershipState, RoomMemberEventContent}, + AnySyncStateEvent, AnySyncTimelineEvent, StateEventType, room::member::MembershipState, }, }; - use tracing::{error, instrument}; + use tracing::instrument; - use super::{super::profiles, AnySyncStateEvent, Context, Raw}; + use super::{super::profiles, Context, Raw}; #[cfg(feature = "experimental-encrypted-state-events")] use crate::response_processors::e2ee; use crate::{ RoomInfo, store::{BaseStateStore, Result as StoreResult, ambiguity_map::AmbiguityCache}, sync::State, + utils::RawSyncStateEventWithKeys, }; impl State { @@ -58,18 +59,23 @@ pub mod sync { pub(crate) fn collect( &self, timeline: &[Raw], - ) -> (Vec>, Vec) { + ) -> Vec { match self { - Self::Before(events) => { - super::collect(events.iter().chain(timeline.iter().filter_map(|raw_event| { - // Only state events have a `state_key` field. - match raw_event.get_field::<&str>("state_key") { - Ok(Some(_)) => Some(raw_event.cast_ref_unchecked()), - _ => None, - } - }))) - } - Self::After(events) => super::collect(events), + Self::Before(events) => events + .iter() + .cloned() + .filter_map(RawSyncStateEventWithKeys::try_from_raw_state_event) + .chain( + timeline + .iter() + .filter_map(RawSyncStateEventWithKeys::try_from_raw_timeline_event), + ) + .collect(), + Self::After(events) => events + .iter() + .cloned() + .filter_map(RawSyncStateEventWithKeys::try_from_raw_state_event) + .collect(), } } } @@ -86,7 +92,7 @@ pub mod sync { #[instrument(skip_all, fields(room_id = ?room_info.room_id))] pub async fn dispatch( context: &mut Context, - (raw_events, events): (&[Raw], &[AnySyncStateEvent]), + raw_events: Vec, room_info: &mut RoomInfo, ambiguity_cache: &mut AmbiguityCache, new_users: &mut U, @@ -96,49 +102,41 @@ pub mod sync { where U: NewUsers, { - for (raw_event, event) in iter::zip(raw_events, events) { - match event { - AnySyncStateEvent::RoomMember(member) => { - room_info.handle_state_event(event); + for mut raw_event in raw_events { + match (&raw_event.event_type, raw_event.state_key.as_str()) { + (StateEventType::RoomMember, _) => { + room_info.handle_state_event(&mut raw_event); dispatch_room_member( context, &room_info.room_id, - member, + &mut raw_event, ambiguity_cache, new_users, ) .await?; } - AnySyncStateEvent::RoomCreate(create) => { - let edited_create = super::validate_create_event_predecessor( + (StateEventType::RoomCreate, "") => { + super::validate_create_event_predecessor( context, - room_info.room_id(), - create, + &room_info.room_id, + &mut raw_event, state_store, ); - room_info.handle_state_event( - edited_create.map(Into::into).as_ref().unwrap_or(event), - ); + room_info.handle_state_event(&mut raw_event); } - AnySyncStateEvent::RoomTombstone(tombstone) => { + (StateEventType::RoomTombstone, "") => { if super::is_tombstone_event_valid( context, - room_info.room_id(), - tombstone, + &room_info.room_id, + &mut raw_event, state_store, ) { - room_info.handle_state_event(event); + room_info.handle_state_event(&mut raw_event); } else { - error!( - room_id = ?room_info.room_id(), - ?tombstone, - "`m.room.tombstone` event is invalid, it creates a loop" - ); - // Do not add the event to `room_info`. // Do not add the event to `context.state_changes.state`. continue; @@ -146,23 +144,18 @@ pub mod sync { } #[cfg(feature = "experimental-encrypted-state-events")] - AnySyncStateEvent::RoomEncrypted(SyncStateEvent::Original(outer)) => { - let Some(event) = super::decrypt_state_event( - raw_event, - &outer.event_id, - &room_info.room_id, - &e2ee, - ) - .await + (StateEventType::RoomEncrypted, _) => { + let Some(mut raw_event) = + super::decrypt_state_event(&mut raw_event, &room_info.room_id, &e2ee).await else { continue; }; - room_info.handle_state_event(&event); + room_info.handle_state_event(&mut raw_event); } _ => { - room_info.handle_state_event(event); + room_info.handle_state_event(&mut raw_event); } } @@ -171,9 +164,9 @@ pub mod sync { .state .entry(room_info.room_id.to_owned()) .or_default() - .entry(event.event_type()) + .entry(raw_event.event_type) .or_default() - .insert(event.state_key().to_owned(), raw_event.clone()); + .insert(raw_event.state_key, raw_event.raw); } Ok(()) @@ -183,13 +176,19 @@ pub mod sync { async fn dispatch_room_member( context: &mut Context, room_id: &RoomId, - event: &SyncStateEvent, + raw_event: &mut RawSyncStateEventWithKeys, ambiguity_cache: &mut AmbiguityCache, new_users: &mut U, ) -> StoreResult<()> where U: NewUsers, { + let Some(event) = raw_event + .deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomMember)) + else { + return Ok(()); + }; + ambiguity_cache.handle_event(&context.state_changes, room_id, event).await?; match event.membership() { @@ -321,16 +320,22 @@ where /// Check if the `predecessor` in `m.room.create` isn't creating a loop of /// rooms. /// -/// If it is, we return a clone of the event with the predecessor removed. +/// If it is, we edit the cached event in `raw_event` to remove the predecessor. pub fn validate_create_event_predecessor( context: &mut Context, room_id: &RoomId, - event: &SyncStateEvent, + raw_event: &mut RawSyncStateEventWithKeys, state_store: &BaseStateStore, -) -> Option> { +) { let mut already_seen = BTreeSet::new(); already_seen.insert(room_id.to_owned()); + let Some(event) = + raw_event.deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomCreate)) + else { + return; + }; + // Redacted and non-redacted create events use the same content type. let content = match event { SyncStateEvent::Original(event) => &event.content, @@ -341,7 +346,7 @@ pub fn validate_create_event_predecessor( content.predecessor.as_ref().map(|predecessor| predecessor.room_id.clone()) else { // No predecessor = no problem here. - return None; + return; }; loop { @@ -358,7 +363,9 @@ pub fn validate_create_event_predecessor( SyncStateEvent::Redacted(event) => event.content.predecessor.take(), }; - return Some(event); + raw_event.set_cached_event(event.into()); + + return; } already_seen.insert(predecessor_room_id.clone()); @@ -382,32 +389,34 @@ pub fn validate_create_event_predecessor( predecessor_room_id = next_predecessor_room_id; } - - None } /// Check if `m.room.tombstone` isn't creating a loop of rooms. pub fn is_tombstone_event_valid( context: &mut Context, room_id: &RoomId, - event: &SyncStateEvent, + raw_event: &mut RawSyncStateEventWithKeys, state_store: &BaseStateStore, ) -> bool { let mut already_seen = BTreeSet::new(); already_seen.insert(room_id.to_owned()); - let Some(mut successor_room_id) = - event.as_original().map(|event| event.content.replacement_room.clone()) + let Some(tombstone) = raw_event + .deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomTombstone)) + .and_then(|event| Some(&event.as_original()?.content)) else { // `true` means no problem. No successor = no problem here. return true; }; + let mut successor_room_id = tombstone.replacement_room.clone(); + loop { // We must check immediately if the `successor_room_id` is in `already_seen` in // case of a room is created and tombstones itself in a single sync. if already_seen.contains(AsRef::::as_ref(&successor_room_id)) { // Ahhh, there is a loop with `m.room.tombstone` events! + error!(?room_id, ?tombstone, "`m.room.tombstone` event is invalid, it creates a loop"); return false; } @@ -438,23 +447,39 @@ pub fn is_tombstone_event_valid( /// Attempt to decrypt the given state event. /// /// Returns `Some(_)` if the state event was successfully decrypted and -/// deserialized. +/// its keys were deserialized. #[cfg(feature = "experimental-encrypted-state-events")] async fn decrypt_state_event( - raw_event: &Raw, - event_id: &ruma::EventId, + raw_event: &mut RawSyncStateEventWithKeys, room_id: &RoomId, - e2ee: &super::e2ee::E2EE<'_>, -) -> Option { + e2ee: &e2ee::E2EE<'_>, +) -> Option { use matrix_sdk_crypto::RoomEventDecryptionResult; + use ruma::OwnedEventId; use tracing::{trace, warn}; + let event_id = match raw_event.raw.get_field::("event_id") { + Ok(Some(event_id)) => event_id, + Ok(None) => { + warn!("Couldn't deserialize encrypted state event's ID: missing `event_id` field"); + return None; + } + Err(error) => { + warn!("Couldn't deserialize encrypted state event's ID: {error}"); + return None; + } + }; + trace!(?event_id, "Received encrypted state event, attempting decryption..."); let olm_machine = e2ee.olm_machine?; let decrypted_event = olm_machine - .try_decrypt_room_event(raw_event.cast_ref_unchecked(), room_id, e2ee.decryption_settings) + .try_decrypt_room_event( + raw_event.raw.cast_ref_unchecked(), + room_id, + e2ee.decryption_settings, + ) .await .expect("OlmMachine was not started"); @@ -466,13 +491,15 @@ async fn decrypt_state_event( // Cast to `AnySync*Event`, safe since this is a supertype of // `AnyTimelineEvent`. - match decrypted_event.event.deserialize_as_unchecked::() { - Ok(event) => { + match RawSyncStateEventWithKeys::try_from_raw_state_event( + decrypted_event.event.cast_unchecked(), + ) { + Some(event) => { trace!(?event_id, "Decrypted state event successfully."); Some(event) } - Err(err) => { - warn!(?event_id, "Failed to decrypt state event: {err}"); + None => { + warn!(?event_id, "Failed to decrypt state event: decrypted state event is invalid"); None } } diff --git a/crates/matrix-sdk-base/src/room/call.rs b/crates/matrix-sdk-base/src/room/call.rs index 7d3600010..93a57d55e 100644 --- a/crates/matrix-sdk-base/src/room/call.rs +++ b/crates/matrix-sdk-base/src/room/call.rs @@ -53,13 +53,14 @@ mod tests { }, }, room_id, + serde::Raw, time::SystemTime, user_id, }; use similar_asserts::assert_eq; use super::super::{Room, RoomState}; - use crate::store::MemoryStore; + use crate::{store::MemoryStore, utils::RawSyncStateEventWithKeys}; fn make_room_test_helper(room_type: RoomState) -> (Arc, Room) { let store = Arc::new(MemoryStore::new()); @@ -99,7 +100,7 @@ mod tests { memberships: Vec, ev_id: &EventId, user_id: &UserId, - ) -> AnySyncStateEvent { + ) -> Raw { let content = CallMemberEventContent::new_legacy(memberships); EventFactory::new() .sender(user_id) @@ -121,7 +122,7 @@ mod tests { ev_id: &EventId, user_id: &UserId, init_data: Option>, - ) -> AnySyncStateEvent { + ) -> Raw { let application = Application::Call(CallApplicationContent::new( "my_call_id_1".to_owned(), ruma::events::call::member::CallScope::Room, @@ -178,11 +179,14 @@ mod tests { ) } - fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) { + fn receive_state_events(room: &Room, events: Vec>) { room.info.update_if(|info| { let mut res = false; for ev in events { - res |= info.handle_state_event(ev); + res |= info.handle_state_event( + &mut RawSyncStateEventWithKeys::try_from_raw_state_event(ev) + .expect("generated state event should be valid"), + ); } res }); @@ -207,7 +211,7 @@ mod tests { let c_two = legacy_member_state_event(vec![m_init_c1, m_init_c2], event_id!("$123456"), c); // Intentionally use a non time sorted receive order. - receive_state_events(&room, vec![&c_two, &a_empty, &b_one]); + receive_state_events(&room, vec![c_two, a_empty, b_one]); room } @@ -238,7 +242,7 @@ mod tests { Some(InitData { device_id: "DEVICE_1".into(), minutes_ago: 20 }), ); // Intentionally use a non time sorted receive order1 - receive_state_events(&room, vec![&m_c1, &m_c2, &a_empty, &b_one]); + receive_state_events(&room, vec![m_c1, m_c2, a_empty, b_one]); room } @@ -272,7 +276,7 @@ mod tests { let c_empty_membership = legacy_member_state_event(Vec::new(), event_id!("$12345_1"), &CAROL); - receive_state_events(&room, vec![&b_empty_membership, &c_empty_membership]); + receive_state_events(&room, vec![b_empty_membership, c_empty_membership]); // We have no active call anymore after emptying the memberships assert_eq!(Vec::::new(), room.active_room_call_participants()); diff --git a/crates/matrix-sdk-base/src/room/encryption.rs b/crates/matrix-sdk-base/src/room/encryption.rs index 704546188..5522df5d7 100644 --- a/crates/matrix-sdk-base/src/room/encryption.rs +++ b/crates/matrix-sdk-base/src/room/encryption.rs @@ -90,12 +90,13 @@ mod tests { EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, event_id, events::{AnySyncStateEvent, room::encryption::RoomEncryptionEventContent}, room_id, + serde::Raw, time::SystemTime, user_id, }; use super::{EncryptionState, Room}; - use crate::{RoomState, store::MemoryStore}; + use crate::{RoomState, store::MemoryStore, utils::RawSyncStateEventWithKeys}; fn make_room_test_helper(room_type: RoomState) -> (Arc, Room) { let store = Arc::new(MemoryStore::new()); @@ -113,11 +114,14 @@ mod tests { .expect("date out of range") } - fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) { + fn receive_state_events(room: &Room, events: Vec>) { room.info.update_if(|info| { let mut res = false; for ev in events { - res |= info.handle_state_event(ev); + res |= info.handle_state_event( + &mut RawSyncStateEventWithKeys::try_from_raw_state_event(ev) + .expect("generated state event should be valid"), + ); } res }); @@ -140,7 +144,7 @@ mod tests { // in the roomInfo .server_ts(timestamp(0)) .into(); - receive_state_events(&room, vec![&encryption_event]); + receive_state_events(&room, vec![encryption_event]); assert_matches!(room.encryption_state(), EncryptionState::Encrypted); } diff --git a/crates/matrix-sdk-base/src/room/room_info.rs b/crates/matrix-sdk-base/src/room/room_info.rs index eebb238c7..69e86526d 100644 --- a/crates/matrix-sdk-base/src/room/room_info.rs +++ b/crates/matrix-sdk-base/src/room/room_info.rs @@ -17,6 +17,7 @@ use std::{ 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}; @@ -64,8 +65,12 @@ use crate::{ read_receipts::RoomReadReceipts, store::{DynStateStore, StateStoreExt}, sync::UnreadNotificationsCount, + utils::RawSyncStateEventWithKeys, }; +/// The default value of the maximum power level. +const DEFAULT_MAX_POWER_LEVEL: i64 = 100; + /// A struct remembering details of an invite and if the invite has been /// accepted on this particular client. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -186,76 +191,198 @@ impl BaseRoomInfo { /// Handle a state event for this room and update our info accordingly. /// /// Returns true if the event modified the info, false otherwise. - pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool { - match ev { - // No redacted branch - enabling encryption cannot be undone. - AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => { - self.encryption = Some(encryption.content.clone()); + pub fn handle_state_event(&mut self, raw_event: &mut RawSyncStateEventWithKeys) -> bool { + match (&raw_event.event_type, raw_event.state_key.as_str()) { + (StateEventType::RoomEncryption, "") => { + // No redacted or failed deserialization branch - enabling encryption cannot be + // undone. + if let Some(SyncStateEvent::Original(event)) = + raw_event.deserialize_as(|any_event| { + as_variant!(any_event, AnySyncStateEvent::RoomEncryption) + }) + { + self.encryption = Some(event.content.clone()); + true + } else { + false + } } - AnySyncStateEvent::RoomAvatar(a) => { - self.avatar = Some(a.into()); + (StateEventType::RoomAvatar, "") => { + if let Some(event) = raw_event.deserialize_as(|any_event| { + as_variant!(any_event, AnySyncStateEvent::RoomAvatar) + }) { + self.avatar = Some(event.into()); + true + } else { + // Remove the previous content if the new content is unknown. + self.avatar.take().is_some() + } } - AnySyncStateEvent::RoomName(n) => { - self.name = Some(n.into()); + (StateEventType::RoomName, "") => { + if let Some(event) = raw_event + .deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomName)) + { + self.name = Some(event.into()); + true + } else { + // Remove the previous content if the new content is unknown. + self.name.take().is_some() + } } - // `m.room.create` can NOT be overwritten. - AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => { - self.create = Some(c.into()); + // `m.room.create` CANNOT be overwritten. + (StateEventType::RoomCreate, "") if self.create.is_none() => { + if let Some(event) = raw_event.deserialize_as(|any_event| { + as_variant!(any_event, AnySyncStateEvent::RoomCreate) + }) { + self.create = Some(event.into()); + true + } else { + false + } } - AnySyncStateEvent::RoomHistoryVisibility(h) => { - self.history_visibility = Some(h.into()); + (StateEventType::RoomHistoryVisibility, "") => { + if let Some(event) = raw_event.deserialize_as(|any_event| { + as_variant!(any_event, AnySyncStateEvent::RoomHistoryVisibility) + }) { + self.history_visibility = Some(event.into()); + true + } else { + // Remove the previous content if the new content is unknown. + self.history_visibility.take().is_some() + } } - AnySyncStateEvent::RoomGuestAccess(g) => { - self.guest_access = Some(g.into()); + (StateEventType::RoomGuestAccess, "") => { + if let Some(event) = raw_event.deserialize_as(|any_event| { + as_variant!(any_event, AnySyncStateEvent::RoomGuestAccess) + }) { + self.guest_access = Some(event.into()); + true + } else { + // Remove the previous content if the new content is unknown. + self.guest_access.take().is_some() + } } - AnySyncStateEvent::RoomJoinRules(c) => match c.join_rule() { - JoinRule::Invite - | JoinRule::Knock - | JoinRule::Private - | JoinRule::Restricted(_) - | JoinRule::KnockRestricted(_) - | JoinRule::Public => self.join_rules = Some(c.into()), - r => warn!("Encountered a custom join rule {}, skipping", r.as_str()), - }, - AnySyncStateEvent::RoomCanonicalAlias(a) => { - self.canonical_alias = Some(a.into()); + (StateEventType::RoomJoinRules, "") => { + if let Some(event) = raw_event.deserialize_as(|any_event| { + as_variant!(any_event, AnySyncStateEvent::RoomJoinRules) + }) { + match event.join_rule() { + JoinRule::Invite + | JoinRule::Knock + | JoinRule::Private + | JoinRule::Restricted(_) + | JoinRule::KnockRestricted(_) + | JoinRule::Public => { + self.join_rules = Some(event.into()); + true + } + r => { + warn!("Encountered a custom join rule {}, skipping", r.as_str()); + // Remove the previous content if the new content is unsupported. + self.join_rules.take().is_some() + } + } + } else { + // Remove the previous content if the new content is unknown. + self.join_rules.take().is_some() + } } - AnySyncStateEvent::RoomTopic(t) => { - self.topic = Some(t.into()); + (StateEventType::RoomCanonicalAlias, "") => { + if let Some(event) = raw_event.deserialize_as(|any_event| { + as_variant!(any_event, AnySyncStateEvent::RoomCanonicalAlias) + }) { + self.canonical_alias = Some(event.into()); + true + } else { + // Remove the previous content if the new content is unknown. + self.canonical_alias.take().is_some() + } } - AnySyncStateEvent::RoomTombstone(t) => { - self.tombstone = Some(t.into()); + (StateEventType::RoomTopic, "") => { + if let Some(event) = raw_event.deserialize_as(|any_event| { + as_variant!(any_event, AnySyncStateEvent::RoomTopic) + }) { + self.topic = Some(event.into()); + true + } else { + // Remove the previous content if the new content is unknown. + self.topic.take().is_some() + } } - AnySyncStateEvent::RoomPowerLevels(p) => { - // The rules and creators do not affect the max power level. - self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into(); + (StateEventType::RoomTombstone, "") => { + if let Some(event) = raw_event.deserialize_as(|any_event| { + as_variant!(any_event, AnySyncStateEvent::RoomTombstone) + }) { + self.tombstone = Some(event.into()); + true + } else { + // Remove the previous content if the new content is unknown. + self.tombstone.take().is_some() + } } - AnySyncStateEvent::CallMember(m) => { - let Some(o_ev) = m.as_original() else { - return false; - }; + (StateEventType::RoomPowerLevels, "") => { + if let Some(event) = raw_event.deserialize_as(|any_event| { + as_variant!(any_event, AnySyncStateEvent::RoomPowerLevels) + }) { + // The rules and creators do not affect the max power level. + self.max_power_level = + event.power_levels(&AuthorizationRules::V1, vec![]).max().into(); + true + } else if self.max_power_level != DEFAULT_MAX_POWER_LEVEL { + // Reset the previous value if the new value is unknown. + self.max_power_level = DEFAULT_MAX_POWER_LEVEL; + true + } else { + false + } + } + (StateEventType::CallMember, _) => { + if let Some(SyncStateEvent::Original(event)) = + raw_event.deserialize_as(|any_event| { + as_variant!(any_event, AnySyncStateEvent::CallMember) + }) + { + // we modify the event so that `origin_server_ts` gets copied into + // `content.created_ts` + let mut event = event.clone(); + event.content.set_created_ts_if_none(event.origin_server_ts); - // we modify the event so that `origin_sever_ts` gets copied into - // `content.created_ts` - let mut o_ev = o_ev.clone(); - o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts); + // Add the new event. + self.rtc_member_events + .insert(event.state_key.clone(), SyncStateEvent::Original(event).into()); - // Add the new event. - self.rtc_member_events - .insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into()); + // Remove all events that don't contain any memberships anymore. + self.rtc_member_events.retain(|_, ev| { + ev.as_original() + .is_some_and(|o| !o.content.active_memberships(None).is_empty()) + }); - // Remove all events that don't contain any memberships anymore. - self.rtc_member_events.retain(|_, ev| { - ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty()) - }); + true + } else if let Ok(call_member_key) = + raw_event.state_key.parse::() + { + // Remove the previous content with the same state key if the new content is + // unknown. + self.rtc_member_events.remove(&call_member_key).is_some() + } else { + false + } } - AnySyncStateEvent::RoomPinnedEvents(p) => { - self.pinned_events = p.as_original().map(|p| p.content.clone()); + (StateEventType::RoomPinnedEvents, "") => { + if let Some(SyncStateEvent::Original(event)) = + raw_event.deserialize_as(|any_event| { + as_variant!(any_event, AnySyncStateEvent::RoomPinnedEvents) + }) + { + self.pinned_events = Some(event.content.clone()); + true + } else { + // Remove the previous content if the new content is unknown. + self.pinned_events.take().is_some() + } } - _ => return false, + _ => false, } - - true } /// Handle a stripped state event for this room and update our info @@ -404,7 +531,7 @@ impl Default for BaseRoomInfo { guest_access: None, history_visibility: None, join_rules: None, - max_power_level: 100, + max_power_level: DEFAULT_MAX_POWER_LEVEL, name: None, tombstone: None, topic: None, @@ -686,11 +813,12 @@ impl RoomInfo { /// Handle the given state event. /// /// Returns true if the event modified the info, false otherwise. - pub fn handle_state_event(&mut self, event: &AnySyncStateEvent) -> bool { + pub fn handle_state_event(&mut self, raw_event: &mut RawSyncStateEventWithKeys) -> bool { // Store the state event in the `BaseRoomInfo` first. - let base_info_has_been_modified = self.base_info.handle_state_event(event); + let base_info_has_been_modified = self.base_info.handle_state_event(raw_event); - if let AnySyncStateEvent::RoomEncryption(_) = event { + if raw_event.event_type == StateEventType::RoomEncryption && raw_event.state_key.is_empty() + { // The `m.room.encryption` event was or wasn't explicitly requested, we don't // know here (see `Self::handle_encryption_state`) but we got one in // return! In this case, we can deduce the room _is_ encrypted, but we cannot @@ -1091,13 +1219,10 @@ impl RoomInfo { { // Pinned events are never in stripped state. Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => { - match raw_event.deserialize() { - Ok(event) => { - self.handle_state_event(&event.into()); - } - Err(error) => { - warn!("Failed to deserialize room pinned events: {error}"); - } + if let Some(mut raw_event) = + RawSyncStateEventWithKeys::try_from_raw_state_event(raw_event.cast()) + { + self.handle_state_event(&mut raw_event); } } Ok(_) => { diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index bdbcff068..2cc066e81 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -49,6 +49,7 @@ use crate::{ ChildTransactionId, QueueWedgeError, SerializableEventContent, StateStoreExt, StoredThreadSubscription, ThreadSubscriptionStatus, }, + utils::RawSyncStateEventWithKeys, }; /// `StateStore` integration tests. @@ -146,13 +147,19 @@ impl StateStoreIntegrationTests for DynStateStore { let name_json: &JsonValue = &test_json::NAME; let name_raw = serde_json::from_value::>(name_json.clone())?; let name_event = name_raw.deserialize()?; - room.handle_state_event(&name_event); + room.handle_state_event( + &mut RawSyncStateEventWithKeys::try_from_raw_state_event(name_raw.clone()) + .expect("generated state event should be valid"), + ); changes.add_state_event(room_id, name_event, name_raw); let topic_json: &JsonValue = &test_json::TOPIC; let topic_raw = serde_json::from_value::>(topic_json.clone())?; let topic_event = topic_raw.deserialize()?; - room.handle_state_event(&topic_event); + room.handle_state_event( + &mut RawSyncStateEventWithKeys::try_from_raw_state_event(topic_raw.clone()) + .expect("generated state event should be valid"), + ); changes.add_state_event(room_id, topic_event, topic_raw); let mut room_ambiguity_map = HashMap::new(); diff --git a/crates/matrix-sdk-base/src/utils.rs b/crates/matrix-sdk-base/src/utils.rs index 13e736795..e241ecea7 100644 --- a/crates/matrix-sdk-base/src/utils.rs +++ b/crates/matrix-sdk-base/src/utils.rs @@ -1,7 +1,8 @@ use ruma::{ EventId, OwnedEventId, assign, events::{ - RedactContent, RedactedStateEventContent, StateEventContent, StaticStateEventContent, + AnySyncStateEvent, AnySyncTimelineEvent, RedactContent, RedactedStateEventContent, + StateEventContent, StateEventType, StaticEventContent, StaticStateEventContent, SyncStateEvent, room::{ avatar::{RoomAvatarEventContent, StrippedRoomAvatarEvent}, @@ -25,8 +26,10 @@ use ruma::{ }, }, room_version_rules::RedactionRules, + serde::Raw, }; use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use tracing::{error, warn}; use crate::room::RoomCreateWithCreatorEventContent; @@ -313,3 +316,138 @@ impl From<&StrippedRoomTombstoneEvent> for MinimalStateEvent, + /// The type of the state event. + pub event_type: StateEventType, + /// The state key of the state event. + pub state_key: String, + /// The cached deserialized event. + cached_event: Option>, +} + +impl RawSyncStateEventWithKeys { + /// Try to construct a `RawSyncStateEventWithKeys` from the given raw state + /// event. + /// + /// Returns `None` if extracting the `type` or `state_key` fails. + pub fn try_from_raw_state_event(raw: Raw) -> Option { + let StateEventWithKeysDeHelper { event_type, state_key } = + match raw.deserialize_as_unchecked() { + Ok(fields) => fields, + Err(error) => { + warn!("Couldn't deserialize type and state key of state event: {error}"); + return None; + } + }; + + // It should be a state event, so log if there is no state key. + let Some(state_key) = state_key else { + warn!( + ?event_type, + "Couldn't deserialize type and state key of state event: missing state key" + ); + return None; + }; + + Some(Self { raw, event_type, state_key, cached_event: None }) + } + + /// Try to construct a `RawSyncStateEventWithKeys` from the given raw + /// timeline event. + /// + /// Returns `None` if deserializing the `type` or `state_key` fails, or if + /// the event is not a state event. + pub fn try_from_raw_timeline_event(raw: &Raw) -> Option { + let StateEventWithKeysDeHelper { event_type, state_key } = match raw + .deserialize_as_unchecked() + { + Ok(fields) => fields, + Err(error) => { + warn!( + "Couldn't deserialize type and optional state key of timeline event: {error}" + ); + return None; + } + }; + + // If the state key is missing, it is not a state event according to the spec. + Some(Self { + event_type, + state_key: state_key?, + raw: raw.clone().cast_unchecked(), + cached_event: None, + }) + } + + /// Try to deserialize the raw event and return the selected variant of + /// `AnySyncStateEvent`. + /// + /// This method should only be called if the variant is already known. It is + /// considered a developer error for `as_variant_fn` to return `None`, but + /// this API was chosen to simplify closures that use the + /// [`as_variant!`](as_variant::as_variant) macro. + /// + /// The result of the event deserialization is cached for future calls to + /// this method. + /// + /// Returns `None` if the deserialization failed or if `as_variant_fn` + /// returns `None`. + pub fn deserialize_as(&mut self, as_variant_fn: F) -> Option<&SyncStateEvent> + where + F: FnOnce(&AnySyncStateEvent) -> Option<&SyncStateEvent>, + C: StaticEventContent + StaticStateEventContent + RedactContent, + C::Redacted: RedactedStateEventContent, + { + let any_event = self + .cached_event + .get_or_insert_with(|| { + self.raw.deserialize().map_err(|error| { + warn!("Couldn't deserialize `{}` state event: {error}", C::TYPE); + }) + }) + .as_ref() + .ok()?; + + let event = as_variant_fn(any_event); + + if event.is_none() { + // This should be a developer error, or an upstream error. + error!( + "Couldn't deserialize `{}` state event: got `{}` event type", + C::TYPE, + any_event.event_type() + ); + } + + event + } + + /// Override the event cached by + /// [`RawSyncStateEventWithKeys::deserialize_as()`]. + /// + /// When validating the content of the deserialized event, this can be used + /// to edit the parts that fail validation and pass the edited event down + /// the chain. + pub(crate) fn set_cached_event(&mut self, event: AnySyncStateEvent) { + self.cached_event = Some(Ok(event)); + } +} + +/// Helper type to deserialize a [`RawSyncStateEventWithKeys`]. +#[derive(Deserialize)] +struct StateEventWithKeysDeHelper { + #[serde(rename = "type")] + event_type: StateEventType, + /// The state key is optional to be able to differentiate state events from + /// other messages in the timeline. + state_key: Option, +} diff --git a/crates/matrix-sdk/src/room_preview.rs b/crates/matrix-sdk/src/room_preview.rs index ab4f4e891..401fb7c05 100644 --- a/crates/matrix-sdk/src/room_preview.rs +++ b/crates/matrix-sdk/src/room_preview.rs @@ -19,7 +19,7 @@ //! well. use futures_util::future::join_all; -use matrix_sdk_base::{RoomHero, RoomInfo, RoomState}; +use matrix_sdk_base::{RawSyncStateEventWithKeys, RoomHero, RoomInfo, RoomState}; use ruma::{ OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName, RoomId, RoomOrAliasId, ServerName, api::client::{membership::joined_members, state::get_state_events}, @@ -310,14 +310,11 @@ impl RoomPreview { 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()); + if let Some(mut raw_event) = + RawSyncStateEventWithKeys::try_from_raw_state_event(ev.cast()) + { + room_info.handle_state_event(&mut raw_event); + } } let room = client.get_room(room_id);