matrix_sdk_base/response_processors/
latest_event.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use matrix_sdk_common::deserialized_responses::TimelineEvent;
16use matrix_sdk_crypto::{DecryptionSettings, RoomEventDecryptionResult};
17use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
18
19use super::{e2ee::E2EE, verification, Context};
20use crate::{
21    latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
22    Result, Room,
23};
24
25/// Decrypt any [`Room::latest_encrypted_events`] for a particular set of
26/// [`Room`]s.
27///
28/// If we can decrypt them, change [`Room::latest_event`] to reflect what we
29/// found, and remove any older encrypted events from
30/// [`Room::latest_encrypted_events`].
31pub async fn decrypt_from_rooms(
32    context: &mut Context,
33    rooms: Vec<Room>,
34    e2ee: E2EE<'_>,
35) -> Result<()> {
36    // All functions used by this one expect an `OlmMachine`. Return if there is
37    // none.
38    if e2ee.olm_machine.is_none() {
39        return Ok(());
40    }
41
42    for room in rooms {
43        // Try to find a message we can decrypt and is suitable for using as the latest
44        // event. If we found one, set it as the latest and delete any older
45        // encrypted events
46        if let Some((found, found_index)) = find_suitable_and_decrypt(&room, &e2ee).await {
47            room.on_latest_event_decrypted(
48                found,
49                found_index,
50                &mut context.state_changes,
51                &mut context.room_info_notable_updates,
52            );
53        }
54    }
55
56    Ok(())
57}
58
59async fn find_suitable_and_decrypt(
60    room: &Room,
61    e2ee: &E2EE<'_>,
62) -> Option<(Box<LatestEvent>, usize)> {
63    let enc_events = room.latest_encrypted_events();
64    let power_levels = room.power_levels().await.ok();
65    let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
66
67    // Walk backwards through the encrypted events, looking for one we can decrypt
68    for (i, event) in enc_events.iter().enumerate().rev() {
69        // Size of the `decrypt_sync_room_event` future should not impact this
70        // async fn since it is likely that there aren't even any encrypted
71        // events when calling it.
72        let decrypt_sync_room_event =
73            Box::pin(decrypt_sync_room_event(event, e2ee, room.room_id()));
74
75        if let Ok(decrypted) = decrypt_sync_room_event.await {
76            // We found an event we can decrypt
77            if let Ok(any_sync_event) = decrypted.raw().deserialize() {
78                // We can deserialize it to find its type
79                match is_suitable_for_latest_event(&any_sync_event, power_levels_info) {
80                    PossibleLatestEvent::YesRoomMessage(_)
81                    | PossibleLatestEvent::YesPoll(_)
82                    | PossibleLatestEvent::YesCallInvite(_)
83                    | PossibleLatestEvent::YesCallNotify(_)
84                    | PossibleLatestEvent::YesSticker(_)
85                    | PossibleLatestEvent::YesKnockedStateEvent(_) => {
86                        return Some((Box::new(LatestEvent::new(decrypted)), i));
87                    }
88                    _ => (),
89                }
90            }
91        }
92    }
93
94    None
95}
96
97/// Attempt to decrypt the given raw event into a [`TimelineEvent`].
98///
99/// In the case of a decryption error, returns a [`TimelineEvent`]
100/// representing the decryption error; in the case of problems with our
101/// application, returns `Err`.
102///
103/// # Panics
104///
105/// Panics if there is no [`OlmMachine`] in [`E2EE`].
106async fn decrypt_sync_room_event(
107    event: &Raw<AnySyncTimelineEvent>,
108    e2ee: &E2EE<'_>,
109    room_id: &RoomId,
110) -> Result<TimelineEvent> {
111    let decryption_settings =
112        DecryptionSettings { sender_device_trust_requirement: e2ee.decryption_trust_requirement };
113
114    let event = match e2ee
115        .olm_machine
116        .expect("An `OlmMachine` is expected")
117        .try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings)
118        .await?
119    {
120        RoomEventDecryptionResult::Decrypted(decrypted) => {
121            // We're fine not setting the push actions for the latest event.
122            let event = TimelineEvent::from_decrypted(decrypted, None);
123
124            if let Ok(sync_timeline_event) = event.raw().deserialize() {
125                verification::process_if_relevant(&sync_timeline_event, e2ee.clone(), room_id)
126                    .await?;
127            }
128
129            event
130        }
131
132        RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
133            TimelineEvent::from_utd(event.clone(), utd_info)
134        }
135    };
136
137    Ok(event)
138}
139
140#[cfg(test)]
141mod tests {
142    use matrix_sdk_test::{
143        async_test, event_factory::EventFactory, JoinedRoomBuilder, SyncResponseBuilder,
144    };
145    use ruma::{event_id, events::room::member::MembershipState, room_id, user_id};
146
147    use super::{decrypt_from_rooms, Context, E2EE};
148    use crate::{room::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client};
149
150    #[async_test]
151    async fn test_when_there_are_no_latest_encrypted_events_decrypting_them_does_nothing() {
152        // Given a room
153        let user_id = user_id!("@u:u.to");
154        let room_id = room_id!("!r:u.to");
155
156        let client = logged_in_base_client(Some(user_id)).await;
157
158        let mut sync_builder = SyncResponseBuilder::new();
159
160        let response = sync_builder
161            .add_joined_room(
162                JoinedRoomBuilder::new(room_id).add_timeline_event(
163                    EventFactory::new()
164                        .member(user_id)
165                        .display_name("Alice")
166                        .membership(MembershipState::Join)
167                        .event_id(event_id!("$1")),
168                ),
169            )
170            .build_sync_response();
171        client.receive_sync_response(response).await.unwrap();
172
173        let room = client.get_room(room_id).expect("Just-created room not found!");
174
175        // Sanity: it has no latest_encrypted_events or latest_event
176        assert!(room.latest_encrypted_events().is_empty());
177        assert!(room.latest_event().is_none());
178
179        // When I tell it to do some decryption
180        let mut context = Context::default();
181
182        decrypt_from_rooms(
183            &mut context,
184            vec![room.clone()],
185            E2EE::new(
186                client.olm_machine().await.as_ref(),
187                client.decryption_trust_requirement,
188                client.handle_verification_events,
189            ),
190        )
191        .await
192        .unwrap();
193
194        // Then nothing changed
195        assert!(room.latest_encrypted_events().is_empty());
196        assert!(room.latest_event().is_none());
197        assert!(context.state_changes.room_infos.is_empty());
198        assert!(!context
199            .room_info_notable_updates
200            .get(room_id)
201            .copied()
202            .unwrap_or_default()
203            .contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
204    }
205}