fix(base): Handle sync state events that fail to deserialize

If the homeserver provides a state event to a client, it means that it
considers the event to be valid. If a state event is valid, it always
updates the state map of the room. So ignoring events that fail to
deserialize means that the local state map is different than the one
from the server.

In some cases the Matrix spec even explicitly says that if a required
field is missing from the content of a state event, it should be treated
as if the event is missing from the state map. And if a required field
is missing, the event will fail to deserialize.

So this handles state events very closely to how a server would we only
deserialize the event type and the state key first to make sure that a
valid state event always updates the local state map. Then we only
deserialize the events lazily when we encounter an event type that
updates the `RoomInfo`. Because we deserialize the event lazily and some
methods might edit parts of an event before passing it to `RoomInfo`,
the (possibly edited) deserialized event is cached alongside the raw
event and its keys to be able to pass it further down the chain.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
This commit is contained in:
Kévin Commaille
2026-01-18 12:19:50 +01:00
committed by Ivan Enderlin
parent 9590b3c683
commit e77e2ae18f
10 changed files with 502 additions and 195 deletions
+2 -1
View File
@@ -69,7 +69,8 @@ pub use store::{
ThreadSubscriptionCatchupToken,
};
pub use utils::{
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent,
RawSyncStateEventWithKeys, RedactedMinimalStateEvent,
};
#[cfg(test)]
@@ -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<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>)>,
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;
}
}
}
@@ -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 (),
@@ -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<AnySyncTimelineEvent>],
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
) -> Vec<RawSyncStateEventWithKeys> {
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<U>(
context: &mut Context,
(raw_events, events): (&[Raw<AnySyncStateEvent>], &[AnySyncStateEvent]),
raw_events: Vec<RawSyncStateEventWithKeys>,
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<U>(
context: &mut Context,
room_id: &RoomId,
event: &SyncStateEvent<RoomMemberEventContent>,
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<RoomCreateEventContent>,
raw_event: &mut RawSyncStateEventWithKeys,
state_store: &BaseStateStore,
) -> Option<SyncStateEvent<RoomCreateEventContent>> {
) {
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<RoomTombstoneEventContent>,
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::<RoomId>::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<AnySyncStateEvent>,
event_id: &ruma::EventId,
raw_event: &mut RawSyncStateEventWithKeys,
room_id: &RoomId,
e2ee: &super::e2ee::E2EE<'_>,
) -> Option<AnySyncStateEvent> {
e2ee: &e2ee::E2EE<'_>,
) -> Option<RawSyncStateEventWithKeys> {
use matrix_sdk_crypto::RoomEventDecryptionResult;
use ruma::OwnedEventId;
use tracing::{trace, warn};
let event_id = match raw_event.raw.get_field::<OwnedEventId>("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::<AnySyncStateEvent>() {
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
}
}
+12 -8
View File
@@ -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<MemoryStore>, Room) {
let store = Arc::new(MemoryStore::new());
@@ -99,7 +100,7 @@ mod tests {
memberships: Vec<LegacyMembershipData>,
ev_id: &EventId,
user_id: &UserId,
) -> AnySyncStateEvent {
) -> Raw<AnySyncStateEvent> {
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<InitData<'_>>,
) -> AnySyncStateEvent {
) -> Raw<AnySyncStateEvent> {
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<Raw<AnySyncStateEvent>>) {
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::<OwnedUserId>::new(), room.active_room_call_participants());
@@ -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<MemoryStore>, 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<Raw<AnySyncStateEvent>>) {
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);
}
+190 -65
View File
@@ -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::<CallMemberStateKey>()
{
// 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(_) => {
@@ -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::<Raw<AnySyncStateEvent>>(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::<Raw<AnySyncStateEvent>>(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();
+139 -1
View File
@@ -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<RoomTombstoneEventC
}
}
}
/// A raw sync state event and its `(type, state_key)` tuple that identifies it
/// in the state map of the room.
///
/// This type can also cache the deserialized event lazily when using
/// [`RawSyncStateEventWithKeys::deserialize_as()`].
#[derive(Debug, Clone)]
pub struct RawSyncStateEventWithKeys {
/// The raw state event.
pub raw: Raw<AnySyncStateEvent>,
/// 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<Result<AnySyncStateEvent, ()>>,
}
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<AnySyncStateEvent>) -> Option<Self> {
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<AnySyncTimelineEvent>) -> Option<Self> {
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<F, C>(&mut self, as_variant_fn: F) -> Option<&SyncStateEvent<C>>
where
F: FnOnce(&AnySyncStateEvent) -> Option<&SyncStateEvent<C>>,
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<String>,
}
+6 -9
View File
@@ -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);