diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index a5ba2324a24..1b11a1ced13 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -50,8 +50,8 @@ pub use http; pub use matrix_sdk_crypto as crypto; pub use once_cell; pub use rooms::{ - DisplayName, Room, RoomCreateWithCreatorEventContent, RoomInfo, RoomInfoUpdate, RoomMember, - RoomMemberships, RoomState, RoomStateFilter, + DisplayName, RawRoomInfo, Room, RoomCreateWithCreatorEventContent, RoomInfo, RoomInfoUpdate, + RoomMember, RoomMemberships, RoomState, RoomStateFilter, }; pub use store::{StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError}; pub use utils::{ diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index bd54bef4960..9f9545f30e6 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -11,11 +11,12 @@ use std::{ use bitflags::bitflags; pub use members::RoomMember; -pub use normal::{Room, RoomInfo, RoomInfoUpdate, RoomState, RoomStateFilter}; +pub use normal::{RawRoomInfo, Room, RoomInfo, RoomInfoUpdate, RoomState, RoomStateFilter}; use ruma::{ assign, + canonical_json::redact_in_place, events::{ - call::member::CallMemberEventContent, + call::member::{CallMemberEvent, CallMemberEventContent}, macros::EventContent, room::{ avatar::RoomAvatarEventContent, @@ -32,9 +33,10 @@ use ruma::{ }, tag::{TagName, Tags}, AnyStrippedStateEvent, AnySyncStateEvent, EmptyStateKey, RedactContent, - RedactedStateEventContent, StaticStateEventContent, SyncStateEvent, + RedactedStateEventContent, StateEventType, StaticStateEventContent, SyncStateEvent, }, room::RoomType, + serde::Raw, EventId, OwnedUserId, RoomVersionId, }; use serde::{Deserialize, Serialize}; @@ -116,12 +118,163 @@ pub struct BaseRoomInfo { pub(crate) notable_tags: RoomNotableTags, } +/// This contains the same fields as BaseRoomInfo, but events are in the raw, unparsed form. +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct RawBaseRoomInfo { + /// The avatar URL of this room. + pub avatar: Option>, + /// The canonical alias of this room. + pub canonical_alias: Option>, + /// The `m.room.create` event content of this room. + pub create: Option>, + /// A list of user ids this room is considered as direct message, if this + /// room is a DM. + pub dm_targets: HashSet, + /// The `m.room.encryption` event content that enabled E2EE in this room. + pub encryption: Option>, + /// The guest access policy of this room. + pub guest_access: Option>, + /// The history visibility policy of this room. + pub history_visibility: Option>, + /// The join rule policy of this room. + pub join_rules: Option>, + /// The maximal power level that can be found in this room. + pub max_power_level: i64, + /// The `m.room.name` of this room. + pub name: Option>, + /// The `m.room.tombstone` event content of this room. + pub tombstone: Option>, + /// The topic of this room. + pub topic: Option>, + /// All minimal state events that containing one or more running matrixRTC + /// memberships. + #[serde(skip_serializing_if = "BTreeMap::is_empty", default)] + pub rtc_member: BTreeMap>, + /// Whether this room has been manually marked as unread. + #[serde(default)] + pub is_marked_unread: bool, + /// Some notable tags. + /// + /// We are not interested by all the tags. Some tags are more important than + /// others, and this field collects them. + #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)] + pub notable_tags: RoomNotableTags, +} + +impl RawBaseRoomInfo { + pub fn map_events( + &mut self, + f: &dyn Fn(&mut Option>, StateEventType, &str), + f_rtc_member: &dyn Fn(&mut BTreeMap>), + ) { + f(&mut self.avatar, StateEventType::RoomAvatar, ""); + f(&mut self.canonical_alias, StateEventType::RoomCanonicalAlias, ""); + f(&mut self.create, StateEventType::RoomCreate, ""); + f(&mut self.encryption, StateEventType::RoomEncryption, ""); + f(&mut self.guest_access, StateEventType::RoomGuestAccess, ""); + f(&mut self.history_visibility, StateEventType::RoomHistoryVisibility, ""); + f(&mut self.join_rules, StateEventType::RoomJoinRules, ""); + f(&mut self.name, StateEventType::RoomName, ""); + f(&mut self.tombstone, StateEventType::RoomTombstone, ""); + f(&mut self.topic, StateEventType::RoomTopic, ""); + f_rtc_member(&mut self.rtc_member); + } + + pub fn handle_redaction(&mut self, event_id: &EventId, room_version: &RoomVersionId) { + self.map_events( + // Clean up normal events + &|event, _event_type, _state_key| { + if event + .as_ref() + .and_then(|a| a.deserialize().ok()) + .is_some_and(|e| e.event_id() == event_id) + { + if let Ok(mut object) = event.as_ref().unwrap().deserialize_as() { + if redact_in_place(&mut object, room_version, None).is_ok() { + *event = Some(Raw::new(&object).unwrap().cast()); + } + } + } + }, + // Clean up rtc_members + &|map| { + for event in map.values_mut() { + if event.deserialize().is_ok_and(|e| e.event_id() == event_id) { + if let Ok(mut object) = event.deserialize_as() { + if redact_in_place(&mut object, room_version, None).is_ok() { + *event = Raw::new(&object).unwrap().cast(); + } + } + } + } + }, + ); + } + pub fn extend_with_changes( + &mut self, + changes: &BTreeMap>>, + ) { + self.map_events( + &|event, event_type, state_key| { + if let Some(e) = changes.get(&event_type).and_then(|c| c.get(state_key)) { + *event = Some(e.clone()); + } + }, + &|map| { + if let Some(rtc_changes) = changes.get(&StateEventType::CallMember) { + map.extend( + rtc_changes + .clone() + .into_iter() + .filter_map(|(u, r)| OwnedUserId::try_from(u).ok().map(|u| (u, r))), + ); + // Remove all events that don't contain any memberships anymore. + map.retain(|_, ev| { + ev.deserialize_as::().is_ok_and(|c| { + c.as_original() + .is_some_and(|o| !o.content.active_memberships(None).is_empty()) + }) + }); + } + }, + ); + } +} + impl BaseRoomInfo { /// Create a new, empty base room info. pub fn new() -> Self { Self::default() } + /// Converts BaseRoomInfo into RawBaseRoomInfo, potentially losing information about some events. + pub fn into_raw_lossy(self) -> RawBaseRoomInfo { + RawBaseRoomInfo { + avatar: self.avatar.and_then(|e| Raw::new(&e).ok()).map(|r| r.cast()), + canonical_alias: self.canonical_alias.and_then(|e| Raw::new(&e).ok()).map(|r| r.cast()), + create: self.create.and_then(|e| Raw::new(&e).ok()).map(|r| r.cast()), + dm_targets: self.dm_targets, + encryption: self.encryption.and_then(|e| Raw::new(&e).ok()).map(|r| r.cast()), + guest_access: self.guest_access.and_then(|e| Raw::new(&e).ok()).map(|r| r.cast()), + history_visibility: self + .history_visibility + .and_then(|e| Raw::new(&e).ok()) + .map(|r| r.cast()), + join_rules: self.join_rules.and_then(|e| Raw::new(&e).ok()).map(|r| r.cast()), + max_power_level: self.max_power_level, + name: self.name.and_then(|e| Raw::new(&e).ok()).map(|r| r.cast()), + tombstone: self.tombstone.and_then(|e| Raw::new(&e).ok()).map(|r| r.cast()), + topic: self.topic.and_then(|e| Raw::new(&e).ok()).map(|r| r.cast()), + rtc_member: self + .rtc_member + .into_iter() + .filter_map(|(u, e)| Raw::new(&e).ok().map(|r| (u, r.cast()))) + .collect(), + is_marked_unread: self.is_marked_unread, + notable_tags: self.notable_tags, + } + } + pub(crate) fn calculate_room_name( &self, joined_member_count: u64, @@ -190,7 +343,7 @@ impl BaseRoomInfo { return false; }; - // we modify the event so that `origin_sever_ts` gets copied into + // we modify the event so that `origin_server_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); @@ -318,7 +471,7 @@ bitflags! { /// others, and this struct describes them. #[repr(transparent)] #[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)] - pub(crate) struct RoomNotableTags: u8 { + pub struct RoomNotableTags: u8 { /// The `m.favourite` tag. const FAVOURITE = 0b0000_0001; diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index d95a823de75..08f892deaec 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -59,7 +59,8 @@ use tracing::{debug, field::debug, info, instrument, trace, warn}; use super::{ members::{MemberInfo, MemberRoomInfo}, - BaseRoomInfo, DisplayName, RoomCreateWithCreatorEventContent, RoomMember, RoomNotableTags, + BaseRoomInfo, DisplayName, RawBaseRoomInfo, RoomCreateWithCreatorEventContent, RoomMember, + RoomNotableTags, }; #[cfg(feature = "experimental-sliding-sync")] use crate::latest_event::LatestEvent; @@ -808,7 +809,7 @@ impl Room { /// 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)] +#[derive(Clone, Debug, Deserialize)] pub struct RoomInfo { /// The unique room id of the room. pub(crate) room_id: OwnedRoomId, @@ -850,8 +851,51 @@ pub struct RoomInfo { pub(crate) base_info: Box, } +/// This is the same as RoomInfo, except it contains RawBaseRoomInfo and implements Serialize. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RawRoomInfo { + /// The unique room id of the room. + pub room_id: OwnedRoomId, + + /// The state of the room. + pub room_state: RoomState, + + /// The unread notifications counts, as returned by the server. + /// + /// These might be incorrect for encrypted rooms, since the server doesn't + /// have access to the content of the encrypted events. + 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, + + /// How much we know about this room. + pub sync_info: SyncInfo, + + /// Whether or not the encryption info was been synced. + pub encryption_state_synced: bool, + + /// The last event send by sliding sync + #[cfg(feature = "experimental-sliding-sync")] + pub latest_event: Option>, + + /// Information about read receipts for this room. + #[serde(default)] + pub read_receipts: RoomReadReceipts, + + /// Base room info which holds some basic event contents important for the + /// room state. + pub base_info: Box, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub(crate) enum SyncInfo { +pub enum SyncInfo { /// We only know the room exists and whether it is in invite / joined / left /// state. /// @@ -886,6 +930,23 @@ impl RoomInfo { } } + /// Converts RoomInfo into RawRoomInfo, potentially losing information about some events. + pub fn into_raw_lossy(self) -> RawRoomInfo { + RawRoomInfo { + room_id: self.room_id, + room_state: self.room_state, + notification_counts: self.notification_counts, + summary: self.summary, + members_synced: self.members_synced, + last_prev_batch: self.last_prev_batch, + sync_info: self.sync_info, + encryption_state_synced: self.encryption_state_synced, + latest_event: self.latest_event, + read_receipts: self.read_receipts, + base_info: Box::new(self.base_info.into_raw_lossy()), + } + } + /// Mark this Room as joined. pub fn mark_as_joined(&mut self) { self.room_state = RoomState::Joined; @@ -1127,21 +1188,24 @@ impl RoomInfo { fn guest_access(&self) -> &GuestAccess { match &self.base_info.guest_access { Some(MinimalStateEvent::Original(ev)) => &ev.content.guest_access, - _ => &GuestAccess::Forbidden, + Some(MinimalStateEvent::Redacted(_)) => &GuestAccess::Forbidden, /* Redaction does not keep field */ + None => &GuestAccess::Forbidden, } } fn history_visibility(&self) -> &HistoryVisibility { match &self.base_info.history_visibility { Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility, - _ => &HistoryVisibility::WorldReadable, + Some(MinimalStateEvent::Redacted(ev)) => &ev.content.history_visibility, + None => &HistoryVisibility::WorldReadable, } } fn join_rule(&self) -> &JoinRule { match &self.base_info.join_rules { Some(MinimalStateEvent::Original(ev)) => &ev.content.join_rule, - _ => &JoinRule::Public, + Some(MinimalStateEvent::Redacted(ev)) => &ev.content.join_rule, + None => &JoinRule::Public, } } @@ -1313,6 +1377,7 @@ mod tests { use matrix_sdk_test::{async_test, ALICE, BOB, CAROL}; use ruma::{ api::client::sync::sync_events::v3::RoomSummary as RumaSummary, + event_id, events::{ call::member::{ Application, CallApplicationContent, CallMemberEventContent, Focus, LivekitFocus, @@ -1320,6 +1385,7 @@ mod tests { }, room::{ canonical_alias::RoomCanonicalAliasEventContent, + join_rules::{RoomJoinRulesEvent, RoomJoinRulesEventContent}, member::{ MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent, SyncRoomMemberEvent, @@ -1332,7 +1398,7 @@ mod tests { serde::Raw, user_id, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, UserId, }; - use serde_json::json; + use serde_json::{json, value::to_raw_value}; use stream_assert::{assert_pending, assert_ready}; #[cfg(feature = "experimental-sliding-sync")] @@ -1342,6 +1408,7 @@ mod tests { use crate::latest_event::LatestEvent; use crate::{ store::{MemoryStore, StateChanges, StateStore}, + sync::UnreadNotificationsCount, BaseClient, DisplayName, MinimalStateEvent, OriginalMinimalStateEvent, SessionMeta, }; @@ -1352,9 +1419,13 @@ mod tests { // serialized format for `RoomInfo`. use super::RoomSummary; - use crate::{rooms::BaseRoomInfo, sync::UnreadNotificationsCount}; + use crate::{ + rooms::{BaseRoomInfo, RawBaseRoomInfo}, + sync::UnreadNotificationsCount, + RawRoomInfo, + }; - let info = RoomInfo { + let info = RawRoomInfo { room_id: room_id!("!gda78o:server.tld").into(), room_state: RoomState::Invited, notification_counts: UnreadNotificationsCount { @@ -1373,7 +1444,7 @@ mod tests { latest_event: Some(Box::new(LatestEvent::new( Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap().into(), ))), - base_info: Box::new(BaseRoomInfo::new()), + base_info: Box::new(RawBaseRoomInfo::default()), read_receipts: Default::default(), }; @@ -1778,6 +1849,65 @@ mod tests { ); } + #[async_test] + async fn test_save_bad_joinrules() { + let (store, room) = make_room(RoomState::Invited); + let room_id = room_id!("!test:localhost"); + let matthew = user_id!("@matthew:example.org"); + let me = user_id!("@me:example.org"); + let mut changes = StateChanges::new("".to_owned()); + let summary = assign!(RumaSummary::new(), { + heroes: vec![me.to_string(), matthew.to_string()], + }); + + let raw_join_rules_content = Raw::>::from_json( + to_raw_value(&json!({ "join_rule": "test!" })).unwrap(), + ); + let raw_join_rules = Raw::from_json( + to_raw_value(&json!({ + "event_id": "$test", + "type": "m.room.join_rules", + "content": raw_join_rules_content, + "origin_server_ts": 0, + "sender": matthew, + "state_key": "", + })) + .unwrap(), + ); + let join_rules_content = + raw_join_rules_content.deserialize_as::().unwrap(); + assert_eq!( + to_raw_value(&join_rules_content).unwrap_err().to_string(), + "the enum variant JoinRule::_Custom cannot be serialized" + ); + + let mut room_info = RoomInfo::new(room_id, RoomState::Joined); + room_info.base_info.join_rules = + Some(MinimalStateEvent::Original(raw_join_rules.deserialize().unwrap())); + changes.add_room(room_info); + + changes.add_state_event( + room_id, + raw_join_rules.deserialize_as().unwrap(), + raw_join_rules.clone().cast(), + ); + store.save_changes(&changes).await.unwrap(); + + let read_room_info = &store.get_room_infos().await.unwrap()[0]; + assert_eq!( + read_room_info + .base_info + .join_rules + .as_ref() + .unwrap() + .as_original() + .unwrap() + .content + .join_rule, + raw_join_rules.deserialize().unwrap().content.join_rule + ); + } + #[async_test] async fn test_display_name_dm_invited_no_heroes() { let (store, room) = make_room(RoomState::Invited); diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index c92cbc71237..3c9d36a3616 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -718,7 +718,7 @@ async fn migrate_to_v8(db: IdbDatabase, store_cipher: Option<&StoreCipher>) -> R let room_info = room_info_v1.migrate(create.as_ref()); room_infos_store.put_key_val( &encode_key(store_cipher, keys::ROOM_INFOS, room_info.room_id()), - &serialize_event(store_cipher, &room_info)?, + &serialize_event(store_cipher, &room_info.into_raw_lossy())?, )?; } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 9b0b5594045..df131f6106a 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -25,13 +25,14 @@ use matrix_sdk_base::{ deserialized_responses::RawAnySyncOrStrippedState, media::{MediaRequest, UniqueKey}, store::{StateChanges, StateStore, StoreError}, - MinimalRoomMemberEvent, RoomInfo, RoomMemberships, RoomState, StateStoreDataKey, + MinimalRoomMemberEvent, RawRoomInfo, RoomInfo, RoomMemberships, RoomState, StateStoreDataKey, StateStoreDataValue, }; use matrix_sdk_store_encryption::{Error as EncryptionError, StoreCipher}; use ruma::{ - canonical_json::{redact, RedactedBecause}, + canonical_json::{redact, redact_in_place, RedactedBecause}, events::{ + call::member::CallMemberEvent, presence::PresenceEvent, receipt::{Receipt, ReceiptThread, ReceiptType}, room::member::{ @@ -623,10 +624,23 @@ impl_state_store!({ if !changes.room_infos.is_empty() { let room_infos = tx.object_store(keys::ROOM_INFOS)?; + for (room_id, room_info) in &changes.room_infos { + // Load raw room info from db, add our updates and then save the raw version again. + + let mut raw_room_info = room_infos + .get(&self.encode_key(keys::ROOM_INFOS, room_id))? + .await? + .and_then(|v| self.deserialize_event::(&v).ok()) + .unwrap_or_else(|| room_info.clone().into_raw_lossy()); + + if let Some(room_state_changes) = changes.state.get(room_id) { + raw_room_info.base_info.extend_with_changes(&room_state_changes); + } + room_infos.put_key_val( &self.encode_key(keys::ROOM_INFOS, room_id), - &self.serialize_event(&room_info)?, + &self.serialize_event(&raw_room_info)?, )?; } } @@ -740,38 +754,55 @@ impl_state_store!({ if !changes.redactions.is_empty() { let state = tx.object_store(keys::ROOM_STATE)?; - let room_info = tx.object_store(keys::ROOM_INFOS)?; + let room_infos = tx.object_store(keys::ROOM_INFOS)?; for (room_id, redactions) in &changes.redactions { + // TODO: Load room version lazily, only if it is necessary + let room_version = room_infos + .get(&self.encode_key(keys::ROOM_INFOS, room_id))? + .await? + .and_then(|f| self.deserialize_event::(&f).ok()) + .and_then(|info| info.room_version().cloned()) + .unwrap_or_else(|| { + warn!(?room_id, "Unable to find the room version, assume version 9"); + RoomVersionId::V9 + }); + + // Clean up raw events we saved for roominfo + if changes.room_infos.get(room_id).is_some() { + // This roominfo has changed, so let's update the raw events + + let Some(mut raw_room_info) = room_infos + .get(&self.encode_key(keys::ROOM_INFOS, room_id))? + .await? + .and_then(|v| self.deserialize_event::(&v).ok()) + else { + continue; + }; + + for (event_id, _redaction_raw) in redactions { + raw_room_info.base_info.handle_redaction(event_id, &room_version); + } + + room_infos.put_key_val( + &self.encode_key(keys::ROOM_INFOS, room_id), + &self.serialize_event(&raw_room_info)?, + )?; + } + + // Cleanup events in room state. TODO: Make this more efficient + let range = self.encode_to_range(keys::ROOM_STATE, room_id)?; let Some(cursor) = state.open_cursor_with_range(&range)?.await? else { continue }; - let mut room_version = None; - while let Some(key) = cursor.key() { let raw_evt = self.deserialize_event::>(&cursor.value())?; if let Ok(Some(event_id)) = raw_evt.get_field::("event_id") { if let Some(redaction) = redactions.get(&event_id) { - let version = { - if room_version.is_none() { - room_version.replace(room_info - .get(&self.encode_key(keys::ROOM_INFOS, room_id))? - .await? - .and_then(|f| self.deserialize_event::(&f).ok()) - .and_then(|info| info.room_version().cloned()) - .unwrap_or_else(|| { - warn!(?room_id, "Unable to find the room version, assume version 9"); - RoomVersionId::V9 - }) - ); - } - room_version.as_ref().unwrap() - }; - let redacted = redact( raw_evt.deserialize_as::()?, - version, + &room_version, Some(RedactedBecause::from_raw_event(redaction)?), ) .map_err(StoreError::Redaction)?; @@ -977,7 +1008,7 @@ impl_state_store!({ } async fn get_room_infos(&self) -> Result> { - let entries: Vec<_> = self + Ok(self .inner .transaction_on_one_with_mode(keys::ROOM_INFOS, IdbTransactionMode::Readonly)? .object_store(keys::ROOM_INFOS)? @@ -985,9 +1016,7 @@ impl_state_store!({ .await? .iter() .filter_map(|f| self.deserialize_event::(&f).ok()) - .collect(); - - Ok(entries) + .collect::>()) } async fn get_stripped_room_infos(&self) -> Result> { diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 5ad856a0858..e08fd54ca28 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -12,8 +12,8 @@ use matrix_sdk_base::{ deserialized_responses::{RawAnySyncOrStrippedState, SyncOrStrippedState}, media::{MediaRequest, UniqueKey}, store::migration_helpers::RoomInfoV1, - MinimalRoomMemberEvent, RoomInfo, RoomMemberships, RoomState, StateChanges, StateStore, - StateStoreDataKey, StateStoreDataValue, + MinimalRoomMemberEvent, RawRoomInfo, RoomInfo, RoomMemberships, RoomState, StateChanges, + StateStore, StateStoreDataKey, StateStoreDataValue, }; use matrix_sdk_store_encryption::StoreCipher; use ruma::{ @@ -202,8 +202,8 @@ impl SqliteStateStore { let migrated_room_info = room_info_v1.migrate(create.as_ref()); - let data = this.serialize_json(&migrated_room_info)?; let room_id = this.encode_key(keys::ROOM_INFO, migrated_room_info.room_id()); + let data = this.serialize_json(&migrated_room_info.into_raw_lossy())?; txn.prepare_cached("UPDATE room_info SET data = ? WHERE room_id = ?")? .execute((data, room_id))?; } @@ -959,21 +959,37 @@ impl StateStore for SqliteStateStore { txn.set_kv_blob(&key, &value)?; } - for (room_id, room_info) in room_infos { + for (room_id, room_info) in &room_infos { let stripped = room_info.state() == RoomState::Invited; // Remove non-stripped data for stripped rooms and vice-versa. - this.remove_maybe_stripped_room_data(txn, &room_id, !stripped)?; + this.remove_maybe_stripped_room_data(txn, room_id, !stripped)?; - let room_id = this.encode_key(keys::ROOM_INFO, room_id); - let state = this - .encode_key(keys::ROOM_INFO, serde_json::to_string(&room_info.state())?); - let data = this.serialize_json(&room_info)?; - txn.set_room_info(&room_id, &state, &data)?; + // Load raw room info from db, add our updates and then save the raw version again. + + let mut raw_room_info = txn + .get_room_info(&this.encode_key(keys::ROOM_INFO, room_id))? + .and_then(|v| this.deserialize_json::(&v).ok()) + .unwrap_or_else(|| room_info.clone().into_raw_lossy()); + + if let Some(room_state_changes) = state.get(room_id) { + raw_room_info.base_info.extend_with_changes(&room_state_changes); + } + + if let Ok(serialized) = this.serialize_json(&raw_room_info) { + txn.set_room_info( + &this.encode_key(keys::ROOM_INFO, room_id), + &this.encode_key( + keys::ROOM_INFO, + serde_json::to_string(&room_info.state())?, + ), + &serialized, + )?; + } } - for (room_id, state_event_types) in state { - let profiles = profiles.get(&room_id); - let encoded_room_id = this.encode_key(keys::STATE_EVENT, &room_id); + for (room_id, state_event_types) in &state { + let profiles = profiles.get(room_id); + let encoded_room_id = this.encode_key(keys::STATE_EVENT, room_id); for (event_type, state_events) in state_event_types { let encoded_event_type = @@ -997,7 +1013,7 @@ impl StateStore for SqliteStateStore { &data, )?; - if event_type == StateEventType::RoomMember { + if event_type == &StateEventType::RoomMember { let member_event = match raw_state_event .deserialize_as::() { @@ -1008,8 +1024,8 @@ impl StateStore for SqliteStateStore { } }; - let encoded_room_id = this.encode_key(keys::MEMBER, &room_id); - let user_id = this.encode_key(keys::MEMBER, &state_key); + let encoded_room_id = this.encode_key(keys::MEMBER, room_id); + let user_id = this.encode_key(keys::MEMBER, state_key); let membership = this .encode_key(keys::MEMBER, member_event.membership().as_str()); let data = this.serialize_value(&state_key)?; @@ -1025,8 +1041,8 @@ impl StateStore for SqliteStateStore { if let Some(profile) = profiles.and_then(|p| p.get(member_event.state_key())) { - let room_id = this.encode_key(keys::PROFILE, &room_id); - let user_id = this.encode_key(keys::PROFILE, &state_key); + let room_id = this.encode_key(keys::PROFILE, room_id); + let user_id = this.encode_key(keys::PROFILE, state_key); let data = this.serialize_json(&profile)?; txn.set_profile(&room_id, &user_id, &data)?; } @@ -1116,25 +1132,49 @@ impl StateStore for SqliteStateStore { } } - for (room_id, redactions) in redactions { - let make_room_version = || { - let encoded_room_id = this.encode_key(keys::ROOM_INFO, &room_id); - txn.get_room_info(&encoded_room_id) - .ok() - .flatten() - .and_then(|v| this.deserialize_json::(&v).ok()) - .and_then(|info| info.room_version().cloned()) - .unwrap_or_else(|| { - warn!( - ?room_id, - "Unable to find the room version, assume version 9" - ); - RoomVersionId::V9 - }) - }; + for (room_id, redactions) in &redactions { + let encoded_room_id = this.encode_key(keys::ROOM_INFO, room_id); + let room_version = txn + .get_room_info(&encoded_room_id) + .ok() + .flatten() + .and_then(|v| this.deserialize_json::(&v).ok()) + .and_then(|info| info.room_version().cloned()) + .unwrap_or_else(|| { + warn!(?room_id, "Unable to find the room version, assume version 9"); + RoomVersionId::V9 + }); + + // Clean up raw events we saved for roominfo + if let Some(room_info) = room_infos.get(room_id) { + // This roominfo has changed, so let's update the raw events + + let Some(mut raw_room_info) = txn + .get_room_info(&this.encode_key(keys::ROOM_INFO, room_id))? + .and_then(|v| this.deserialize_json::(&v).ok()) + else { + continue; + }; - let encoded_room_id = this.encode_key(keys::STATE_EVENT, &room_id); - let mut room_version = None; + for (event_id, _redaction_raw) in redactions { + raw_room_info.base_info.handle_redaction(event_id, &room_version); + } + + if let Ok(serialized) = this.serialize_json(&raw_room_info) { + txn.set_room_info( + &this.encode_key(keys::ROOM_INFO, room_id), + &this.encode_key( + keys::ROOM_INFO, + serde_json::to_string(&room_info.state())?, + ), + &serialized, + )?; + } + } + + // Cleanup events in room state. + + let encoded_room_id = this.encode_key(keys::STATE_EVENT, room_id); for (event_id, redaction) in redactions { let event_id = this.encode_key(keys::STATE_EVENT, event_id); @@ -1146,7 +1186,7 @@ impl StateStore for SqliteStateStore { let event = raw_event.deserialize()?; let redacted = redact( raw_event.deserialize_as::()?, - room_version.get_or_insert_with(make_room_version), + &room_version, Some(RedactedBecause::from_raw_event(&redaction)?), ) .map_err(Error::Redaction)?; @@ -1368,8 +1408,8 @@ impl StateStore for SqliteStateStore { .get_room_infos(Vec::new()) .await? .into_iter() - .map(|data| self.deserialize_json(&data)) - .collect() + .map(|data| self.deserialize_json::(&data)) + .collect::>>() } async fn get_stripped_room_infos(&self) -> Result> { diff --git a/testing/matrix-sdk-integration-testing/src/tests.rs b/testing/matrix-sdk-integration-testing/src/tests.rs index ee80945ad2f..42ce04cee54 100644 --- a/testing/matrix-sdk-integration-testing/src/tests.rs +++ b/testing/matrix-sdk-integration-testing/src/tests.rs @@ -5,3 +5,4 @@ mod reactions; mod redaction; mod repeated_join; mod sliding_sync; +mod state; diff --git a/testing/matrix-sdk-integration-testing/src/tests/state.rs b/testing/matrix-sdk-integration-testing/src/tests/state.rs new file mode 100644 index 00000000000..6d6597e9ede --- /dev/null +++ b/testing/matrix-sdk-integration-testing/src/tests/state.rs @@ -0,0 +1,94 @@ +use std::time::Duration; + +use anyhow::Result; +use matrix_sdk::{ + ruma::{ + api::client::{ + room::create_room::v3::Request as CreateRoomRequest, state::send_state_event, + }, + assign, + events::{room::join_rules::RoomJoinRulesEventContent, StateEventType}, + serde::Raw, + }, + RoomState, +}; +use serde_json::{json, value::to_raw_value}; +use tokio::{spawn, time::sleep}; + +use crate::helpers::TestClientBuilder; + +/// This makes sure the sync worker does not panic. However this does not check +/// if the raw event values are recovered when the roominfo is loaded from the +/// database +#[tokio::test] +async fn test_send_bad_join_rules() -> Result<()> { + let alice = TestClientBuilder::new("alice".to_owned()) + .randomize_username() + .use_sqlite() + .build() + .await?; + let bob = + TestClientBuilder::new("bob".to_owned()).randomize_username().use_sqlite().build().await?; + + // The sync tasks will panic when there is a problem. At the end of this test, + // we will check if they are still running + let a = alice.clone(); + let alice_handle = spawn(async move { + if let Err(err) = a.sync(Default::default()).await { + panic!("alice sync errored: {err}"); + } + }); + + let b = bob.clone(); + let bob_handle = spawn(async move { + if let Err(err) = b.sync(Default::default()).await { + panic!("bob sync errored: {err}"); + } + }); + + // Alice creates a room. + let alice_room = alice + .create_room(assign!(CreateRoomRequest::new(), { + invite: vec![], + is_direct: false, + })) + .await?; + + let alice_room = alice.get_room(alice_room.room_id()).unwrap(); + assert_eq!(alice_room.state(), RoomState::Joined); + + // This join rule cannot be serialized: + let content = RoomJoinRulesEventContent::new( + serde_json::from_value(json!({ "join_rule": "test!"})).unwrap(), + ); + assert_eq!( + to_raw_value(&content).unwrap_err().to_string(), + "the enum variant JoinRule::_Custom cannot be serialized" + ); + + // This will lead to a sync response for alice with a stateevent that fails to + // serialize. + let request = send_state_event::v3::Request::new_raw( + alice_room.room_id().to_owned(), + StateEventType::RoomJoinRules, + "".to_owned(), + Raw::from_json(to_raw_value(&json!({ "join_rule": "test!"})).unwrap()), + ); + let response = alice.send(request, None).await?; + dbg!(response); + + // This will lead to a sync response for bob with a strippedstateevent that + // fails to serialize + alice_room.invite_user_by_id(bob.user_id().unwrap()).await?; + + sleep(Duration::from_secs(1)).await; + + // The sync handlers should not have crashed + assert!(!alice_handle.is_finished()); + assert!(!bob_handle.is_finished()); + + alice_handle.abort(); + bob_handle.abort(); + + Ok(()) +}