Skip to content

Commit a4434d7

Browse files
committed
feat(event cache): strip bundled relations before persisting events
1 parent e0b1b5d commit a4434d7

File tree

1 file changed

+145
-3
lines changed
  • crates/matrix-sdk/src/event_cache/room

1 file changed

+145
-3
lines changed

crates/matrix-sdk/src/event_cache/room/mod.rs

Lines changed: 145 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,13 @@ impl RoomEventCacheInner {
539539
mod private {
540540
use std::sync::Arc;
541541

542-
use matrix_sdk_base::event_cache::store::EventCacheStoreLock;
542+
use matrix_sdk_base::{
543+
deserialized_responses::{SyncTimelineEvent, TimelineEventKind},
544+
event_cache::store::EventCacheStoreLock,
545+
linked_chunk::Update,
546+
};
543547
use once_cell::sync::OnceCell;
544-
use ruma::OwnedRoomId;
548+
use ruma::{serde::Raw, OwnedRoomId};
545549

546550
use super::events::RoomEvents;
547551
use crate::event_cache::EventCacheError;
@@ -587,13 +591,72 @@ mod private {
587591
Ok(Self { room, store, events, waited_for_initial_prev_token: false })
588592
}
589593

594+
/// Removes the bundled relations from an event, if they were present.
595+
///
596+
/// Only replaces the present if it contained bundled relations.
597+
fn strip_relations_if_present<T>(event: &mut Raw<T>) {
598+
// We're going to get rid of the `unsigned`/`m.relations` field, if it's
599+
// present.
600+
// Use a closure that returns an option so we can quickly short-circuit.
601+
let mut closure = || -> Option<()> {
602+
let mut val: serde_json::Value = event.deserialize_as().ok()?;
603+
let unsigned = val.get_mut("unsigned")?;
604+
let unsigned_obj = unsigned.as_object_mut()?;
605+
if unsigned_obj.remove("m.relations").is_some() {
606+
*event = Raw::new(&val).ok()?.cast();
607+
}
608+
None
609+
};
610+
let _ = closure();
611+
}
612+
613+
/// Strips the bundled relations from a collection of events.
614+
fn strip_relations_from_events(items: &mut [SyncTimelineEvent]) {
615+
for ev in items.iter_mut() {
616+
match &mut ev.kind {
617+
TimelineEventKind::Decrypted(decrypted) => {
618+
// Remove all information about encryption info for
619+
// the bundled events.
620+
decrypted.unsigned_encryption_info = None;
621+
622+
// Remove the `unsigned`/`m.relations` field, if needs be.
623+
Self::strip_relations_if_present(&mut decrypted.event);
624+
}
625+
626+
TimelineEventKind::UnableToDecrypt { event, .. }
627+
| TimelineEventKind::PlainText { event } => {
628+
Self::strip_relations_if_present(event);
629+
}
630+
}
631+
}
632+
}
633+
590634
/// Propagate changes to the underlying storage.
591635
async fn propagate_changes(&mut self) -> Result<(), EventCacheError> {
592-
let updates = self.events.updates().take();
636+
let mut updates = self.events.updates().take();
593637

594638
if !updates.is_empty() {
595639
if let Some(store) = self.store.get() {
596640
let locked = store.lock().await?;
641+
642+
// Strip relations from the `PushItems` updates.
643+
for up in updates.iter_mut() {
644+
match up {
645+
Update::PushItems { items, .. } => {
646+
Self::strip_relations_from_events(items)
647+
}
648+
// Other update kinds don't involve adding new events.
649+
Update::NewItemsChunk { .. }
650+
| Update::NewGapChunk { .. }
651+
| Update::RemoveChunk(_)
652+
| Update::RemoveItem { .. }
653+
| Update::DetachLastItems { .. }
654+
| Update::StartReattachItems
655+
| Update::EndReattachItems
656+
| Update::Clear => {}
657+
}
658+
}
659+
597660
locked.handle_linked_chunk_updates(&self.room, updates).await?;
598661
}
599662
}
@@ -959,6 +1022,85 @@ mod tests {
9591022
assert!(chunks.next().is_none());
9601023
}
9611024

1025+
#[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support.
1026+
#[async_test]
1027+
async fn test_write_to_storage_strips_bundled_relations() {
1028+
use ruma::events::BundledMessageLikeRelations;
1029+
1030+
let room_id = room_id!("!galette:saucisse.bzh");
1031+
let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh"));
1032+
1033+
let event_cache_store = Arc::new(MemoryStore::new());
1034+
1035+
let client = MockClientBuilder::new("http://localhost".to_owned())
1036+
.store_config(
1037+
StoreConfig::new("hodlor".to_owned()).event_cache_store(event_cache_store.clone()),
1038+
)
1039+
.build()
1040+
.await;
1041+
1042+
let event_cache = client.event_cache();
1043+
1044+
// Don't forget to subscribe and like^W enable storage!
1045+
event_cache.subscribe().unwrap();
1046+
event_cache.enable_storage().unwrap();
1047+
1048+
client.base_client().get_or_create_room(room_id, matrix_sdk_base::RoomState::Joined);
1049+
let room = client.get_room(room_id).unwrap();
1050+
1051+
let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap();
1052+
1053+
// Propagate an update for a message with bundled relations.
1054+
let mut relations = BundledMessageLikeRelations::new();
1055+
relations.replace =
1056+
Some(Box::new(f.text_msg("Hello, Kind Sir").sender(*ALICE).into_raw_sync()));
1057+
let ev = f.text_msg("hey yo").sender(*ALICE).bundled_relations(relations).into_sync();
1058+
1059+
let timeline = Timeline { limited: false, prev_batch: None, events: vec![ev] };
1060+
1061+
room_event_cache
1062+
.inner
1063+
.handle_joined_room_update(JoinedRoomUpdate { timeline, ..Default::default() })
1064+
.await
1065+
.unwrap();
1066+
1067+
// The in-memory linked chunk keeps the bundled relation.
1068+
{
1069+
let (events, _) = room_event_cache.subscribe().await.unwrap();
1070+
1071+
assert_eq!(events.len(), 1);
1072+
1073+
let ev = events[0].raw().deserialize().unwrap();
1074+
assert_let!(
1075+
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(msg)) = ev
1076+
);
1077+
1078+
let original = msg.as_original().unwrap();
1079+
assert_eq!(original.content.body(), "hey yo");
1080+
assert!(original.unsigned.relations.replace.is_some());
1081+
}
1082+
1083+
// The one in storage does not.
1084+
let linked_chunk = event_cache_store.reload_linked_chunk(room_id).await.unwrap().unwrap();
1085+
1086+
assert_eq!(linked_chunk.chunks().count(), 1);
1087+
1088+
let mut chunks = linked_chunk.chunks();
1089+
assert_matches!(chunks.next().unwrap().content(), ChunkContent::Items(events) => {
1090+
assert_eq!(events.len(), 1);
1091+
1092+
let ev = events[0].raw().deserialize().unwrap();
1093+
assert_let!(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(msg)) = ev);
1094+
1095+
let original = msg.as_original().unwrap();
1096+
assert_eq!(original.content.body(), "hey yo");
1097+
assert!(original.unsigned.relations.replace.is_none());
1098+
});
1099+
1100+
// That's all, folks!
1101+
assert!(chunks.next().is_none());
1102+
}
1103+
9621104
#[cfg(not(target_arch = "wasm32"))] // This uses the cross-process lock, so needs time support.
9631105
#[async_test]
9641106
async fn test_load_from_storage() {

0 commit comments

Comments
 (0)