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.
1415use std::{
16 collections::{BTreeMap, HashMap, HashSet},
17 mem,
18};
1920use ruma::{
21 events::{
22 direct::OwnedDirectUserIdentifier, AnyGlobalAccountDataEvent, GlobalAccountDataEventType,
23 },
24 serde::Raw,
25 RoomId,
26};
27use tracing::{debug, instrument, trace, warn};
2829use crate::{store::BaseStateStore, RoomInfo, StateChanges};
3031/// Applies a function to an existing `RoomInfo` if present in changes, or one
32/// loaded from the database.
33fn map_info<F: FnOnce(&mut RoomInfo)>(
34 room_id: &RoomId,
35 changes: &mut StateChanges,
36 store: &BaseStateStore,
37 f: F,
38) {
39if let Some(info) = changes.room_infos.get_mut(room_id) {
40 f(info);
41 } else if let Some(room) = store.room(room_id) {
42let mut info = room.clone_info();
43 f(&mut info);
44 changes.add_room(info);
45 } else {
46debug!(room = %room_id, "couldn't find room in state changes or store");
47 }
48}
4950#[must_use]
51pub(crate) struct AccountDataProcessor {
52 parsed_events: Vec<AnyGlobalAccountDataEvent>,
53 raw_by_type: BTreeMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>,
54}
5556impl AccountDataProcessor {
57/// Creates a new processor for global account data.
58pub fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
59let mut raw_by_type = BTreeMap::new();
60let mut parsed_events = Vec::new();
6162for raw_event in events {
63let event = match raw_event.deserialize() {
64Ok(e) => e,
65Err(e) => {
66let event_type: Option<String> = raw_event.get_field("type").ok().flatten();
67warn!(event_type, "Failed to deserialize a global account data event: {e}");
68continue;
69 }
70 };
7172 raw_by_type.insert(event.event_type(), raw_event.clone());
73 parsed_events.push(event);
74 }
7576Self { raw_by_type, parsed_events }
77 }
7879/// Returns the push rules found by this processor.
80pub fn push_rules(&self) -> Option<&Raw<AnyGlobalAccountDataEvent>> {
81self.raw_by_type.get(&GlobalAccountDataEventType::PushRules)
82 }
8384/// Processes the direct rooms in a sync response:
85 ///
86 /// Given a [`StateChanges`] instance, processes any direct room info
87 /// from the global account data and adds it to the room infos to
88 /// save.
89#[instrument(skip_all)]
90pub(crate) fn process_direct_rooms(
91&self,
92 events: &[AnyGlobalAccountDataEvent],
93 store: &BaseStateStore,
94 changes: &mut StateChanges,
95 ) {
96for event in events {
97let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue };
9899let mut new_dms = HashMap::<&RoomId, HashSet<OwnedDirectUserIdentifier>>::new();
100for (user_identifier, rooms) in direct_event.content.iter() {
101for room_id in rooms {
102 new_dms.entry(room_id).or_default().insert(user_identifier.clone());
103 }
104 }
105106let rooms = store.rooms();
107let mut old_dms = rooms
108 .iter()
109 .filter_map(|r| {
110let direct_targets = r.direct_targets();
111 (!direct_targets.is_empty()).then(|| (r.room_id(), direct_targets))
112 })
113 .collect::<HashMap<_, _>>();
114115// Update the direct targets of rooms if they changed.
116for (room_id, new_direct_targets) in new_dms {
117if let Some(old_direct_targets) = old_dms.remove(&room_id) {
118if old_direct_targets == new_direct_targets {
119continue;
120 }
121 }
122trace!(?room_id, targets = ?new_direct_targets, "Marking room as direct room");
123 map_info(room_id, changes, store, |info| {
124 info.base_info.dm_targets = new_direct_targets;
125 });
126 }
127128// Remove the targets of old direct chats.
129for room_id in old_dms.keys() {
130trace!(?room_id, "Unmarking room as direct room");
131 map_info(room_id, changes, store, |info| {
132 info.base_info.dm_targets.clear();
133 });
134 }
135 }
136 }
137138/// Applies the processed data to the state changes.
139pub async fn apply(mut self, changes: &mut StateChanges, store: &BaseStateStore) {
140// Fill in the content of `changes.account_data`.
141mem::swap(&mut changes.account_data, &mut self.raw_by_type);
142143// Process direct rooms.
144let has_new_direct_room_data = self
145.parsed_events
146 .iter()
147 .any(|event| event.event_type() == GlobalAccountDataEventType::Direct);
148149if has_new_direct_room_data {
150self.process_direct_rooms(&self.parsed_events, store, changes);
151 } else if let Ok(Some(direct_account_data)) =
152 store.get_account_data_event(GlobalAccountDataEventType::Direct).await
153{
154debug!("Found direct room data in the Store, applying it");
155if let Ok(direct_account_data) = direct_account_data.deserialize() {
156self.process_direct_rooms(&[direct_account_data], store, changes);
157 } else {
158warn!("Failed to deserialize direct room account data");
159 }
160 }
161 }
162}