matrix_sdk_base/response_processors/account_data/
global.rs

1// Copyright 2024 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 std::{
16    collections::{BTreeMap, HashMap, HashSet},
17    mem,
18};
19
20use matrix_sdk_common::timer;
21use ruma::{
22    RoomId,
23    events::{
24        AnyGlobalAccountDataEvent, GlobalAccountDataEventType, direct::OwnedDirectUserIdentifier,
25    },
26    serde::Raw,
27};
28use tracing::{debug, instrument, trace, warn};
29
30use super::super::Context;
31use crate::{RoomInfo, StateChanges, store::BaseStateStore};
32
33/// Create the [`Global`] account data processor.
34pub fn global(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Global {
35    Global::process(events)
36}
37
38#[must_use]
39pub struct Global {
40    parsed_events: Vec<AnyGlobalAccountDataEvent>,
41    raw_by_type: BTreeMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>,
42}
43
44impl Global {
45    /// Creates a new processor for global account data.
46    fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
47        let _timer = timer!(tracing::Level::TRACE, "Global::process (global account data)");
48
49        let mut raw_by_type = BTreeMap::new();
50        let mut parsed_events = Vec::new();
51
52        for raw_event in events {
53            let event = match raw_event.deserialize() {
54                Ok(e) => e,
55                Err(e) => {
56                    let event_type: Option<String> = raw_event.get_field("type").ok().flatten();
57                    warn!(event_type, "Failed to deserialize a global account data event: {e}");
58                    continue;
59                }
60            };
61
62            raw_by_type.insert(event.event_type(), raw_event.clone());
63            parsed_events.push(event);
64        }
65
66        Self { raw_by_type, parsed_events }
67    }
68
69    /// Returns the push rules found by this processor.
70    pub fn push_rules(&self) -> Option<&Raw<AnyGlobalAccountDataEvent>> {
71        self.raw_by_type.get(&GlobalAccountDataEventType::PushRules)
72    }
73
74    /// Processes the direct rooms in a sync response:
75    ///
76    /// Given a [`StateChanges`] instance, processes any direct room info
77    /// from the global account data and adds it to the room infos to
78    /// save.
79    #[instrument(skip_all)]
80    fn process_direct_rooms(
81        &self,
82        events: &[AnyGlobalAccountDataEvent],
83        state_store: &BaseStateStore,
84        state_changes: &mut StateChanges,
85    ) {
86        for event in events {
87            let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue };
88
89            let mut new_dms = HashMap::<&RoomId, HashSet<OwnedDirectUserIdentifier>>::new();
90
91            for (user_identifier, rooms) in direct_event.content.iter() {
92                for room_id in rooms {
93                    new_dms.entry(room_id).or_default().insert(user_identifier.clone());
94                }
95            }
96
97            let rooms = state_store.rooms();
98            let mut old_dms = rooms
99                .iter()
100                .filter_map(|r| {
101                    let direct_targets = r.direct_targets();
102                    (!direct_targets.is_empty()).then(|| (r.room_id(), direct_targets))
103                })
104                .collect::<HashMap<_, _>>();
105
106            // Update the direct targets of rooms if they changed.
107            for (room_id, new_direct_targets) in new_dms {
108                if let Some(old_direct_targets) = old_dms.remove(&room_id)
109                    && old_direct_targets == new_direct_targets
110                {
111                    continue;
112                }
113                trace!(?room_id, targets = ?new_direct_targets, "Marking room as direct room");
114                map_info(room_id, state_changes, state_store, |info| {
115                    info.base_info.dm_targets = new_direct_targets;
116                });
117            }
118
119            // Remove the targets of old direct chats.
120            for room_id in old_dms.keys() {
121                trace!(?room_id, "Unmarking room as direct room");
122                map_info(room_id, state_changes, state_store, |info| {
123                    info.base_info.dm_targets.clear();
124                });
125            }
126        }
127    }
128
129    /// Applies the processed data to the state changes and the state store.
130    pub async fn apply(mut self, context: &mut Context, state_store: &BaseStateStore) {
131        let _timer = timer!(tracing::Level::TRACE, "Global::apply (global account data)");
132
133        // Fill in the content of `changes.account_data`.
134        mem::swap(&mut context.state_changes.account_data, &mut self.raw_by_type);
135
136        // Process direct rooms.
137        let has_new_direct_room_data = self
138            .parsed_events
139            .iter()
140            .any(|event| event.event_type() == GlobalAccountDataEventType::Direct);
141
142        if has_new_direct_room_data {
143            self.process_direct_rooms(&self.parsed_events, state_store, &mut context.state_changes);
144        } else if let Ok(Some(direct_account_data)) =
145            state_store.get_account_data_event(GlobalAccountDataEventType::Direct).await
146        {
147            debug!("Found direct room data in the Store, applying it");
148            if let Ok(direct_account_data) = direct_account_data.deserialize() {
149                self.process_direct_rooms(
150                    &[direct_account_data],
151                    state_store,
152                    &mut context.state_changes,
153                );
154            } else {
155                warn!("Failed to deserialize direct room account data");
156            }
157        }
158    }
159}
160
161/// Applies a function to an existing `RoomInfo` if present in changes, or one
162/// loaded from the database.
163fn map_info<F: FnOnce(&mut RoomInfo)>(
164    room_id: &RoomId,
165    changes: &mut StateChanges,
166    store: &BaseStateStore,
167    f: F,
168) {
169    if let Some(info) = changes.room_infos.get_mut(room_id) {
170        f(info);
171    } else if let Some(room) = store.room(room_id) {
172        let mut info = room.clone_info();
173        f(&mut info);
174        changes.add_room(info);
175    } else if store.already_logged_missing_room.lock().insert(room_id.to_owned()) {
176        debug!(room = %room_id, "couldn't find room in state changes or store");
177    }
178}