tuwunel_service/rooms/pdu_metadata/
bundling.rs1use std::collections::BTreeSet;
2
3use futures::{Stream, StreamExt, TryFutureExt, pin_mut};
4use ruma::{OwnedUserId, UserId, api::Direction, events::room::encrypted::Relation};
5use tuwunel_core::{
6 PduId,
7 arrayvec::ArrayVec,
8 implement,
9 matrix::{Event, Pdu, PduCount, RawPduId},
10 result::LogErr,
11 utils::{
12 BoolExt,
13 stream::{ReadyExt, TryIgnore},
14 u64_from_u8,
15 },
16};
17
18use super::{
19 ExtractRelatesTo, IgnoredThreadView,
20 IgnoredThreadView::{Adjusted, Omitted, Unchanged},
21 Service,
22 typed_relations::{CHILD_COUNT_OFFSET, KEY_LEN, Tag, prefix},
23};
24
25type Seek = ArrayVec<u8, KEY_LEN>;
26
27#[implement(Service)]
38#[tracing::instrument(skip_all, level = "trace")]
39pub async fn bundle_aggregations(&self, sender_user: &UserId, mut pdu: Pdu) -> Pdu {
40 if let Some(pruned) = self
43 .services
44 .state_accessor
45 .erased_view(sender_user, &pdu)
46 .await
47 {
48 return pruned;
49 }
50
51 let has_thread = pdu
52 .unsigned()
53 .is_some_and(|unsigned| unsigned.get().contains("m.thread"));
54
55 if has_thread {
56 let participated = self
57 .services
58 .threads
59 .user_participated(pdu.event_id(), sender_user)
60 .await;
61
62 pdu.set_thread_participated(participated)
63 .log_err()
64 .ok();
65
66 self.erase_thread_latest(sender_user, &mut pdu)
67 .await;
68
69 if self.services.server.config.bundle_edit_relations {
70 self.bundle_thread_latest_edit(sender_user, &mut pdu)
71 .await;
72 }
73 }
74
75 let replacement = self
76 .services
77 .server
78 .config
79 .bundle_edit_relations
80 .then_async(|| self.newest_replacement(&pdu))
81 .await
82 .flatten();
83
84 if let Some(replacement) = replacement
85 && !self
86 .services
87 .state_accessor
88 .erased_for(sender_user, &replacement)
89 .await
90 {
91 pdu.set_replacement_bundle(&replacement.into_format())
92 .log_err()
93 .ok();
94 }
95
96 let references = self
97 .services
98 .server
99 .config
100 .bundle_reference_relations
101 .then_async(|| self.references(&pdu))
102 .await
103 .unwrap_or_default();
104
105 if !references.is_empty() {
106 pdu.set_reference_bundle(&references)
107 .log_err()
108 .ok();
109 }
110
111 pdu
112}
113
114#[implement(Service)]
118#[tracing::instrument(skip_all, level = "trace")]
119async fn erase_thread_latest(&self, sender_user: &UserId, pdu: &mut Pdu) {
120 let Some((event_id, sender)) = pdu.thread_latest_event() else {
121 return;
122 };
123
124 if !self.services.users.is_erased(&sender).await {
125 return;
126 }
127
128 let Ok(latest) = self.services.timeline.get_pdu(&event_id).await else {
129 return;
130 };
131
132 if let Some(pruned) = self
133 .services
134 .state_accessor
135 .erased_view(sender_user, &latest)
136 .await
137 {
138 pdu.set_thread_latest_event(&pruned.into_format())
139 .log_err()
140 .ok();
141 }
142}
143
144#[implement(Service)]
149#[tracing::instrument(skip_all, level = "trace")]
150async fn bundle_thread_latest_edit(&self, sender_user: &UserId, pdu: &mut Pdu) {
151 let Some((event_id, _)) = pdu.thread_latest_event() else {
152 return;
153 };
154
155 let Ok(mut latest) = self.services.timeline.get_pdu(&event_id).await else {
156 return;
157 };
158
159 if self
160 .services
161 .state_accessor
162 .erased_for(sender_user, &latest)
163 .await
164 {
165 return;
166 }
167
168 let Some(replacement) = self.newest_replacement(&latest).await else {
169 return;
170 };
171
172 if self
173 .services
174 .state_accessor
175 .erased_for(sender_user, &replacement)
176 .await
177 {
178 return;
179 }
180
181 if latest
182 .set_replacement_bundle(&replacement.into_format())
183 .log_err()
184 .is_err()
185 {
186 return;
187 }
188
189 pdu.set_thread_latest_event(&latest.into_format())
190 .log_err()
191 .ok();
192}
193
194#[implement(Service)]
199#[tracing::instrument(skip_all, level = "trace")]
200async fn newest_replacement(&self, parent: &Pdu) -> Option<Pdu> {
201 if parent.is_redacted() {
202 return None;
203 }
204
205 let parent_id: PduId = self
206 .services
207 .timeline
208 .get_pdu_id(parent.event_id())
209 .map_ok(Into::into)
210 .await
211 .ok()?;
212
213 let replacements = self.replacement_children(parent, parent_id);
214
215 pin_mut!(replacements);
216 replacements.next().await
217}
218
219#[implement(Service)]
223fn replacement_children<'a>(
224 &'a self,
225 parent: &'a Pdu,
226 parent_id: PduId,
227) -> impl Stream<Item = Pdu> + Send + 'a {
228 let shortroomid = parent_id.shortroomid;
229 let prefix = prefix(shortroomid, parent_id.count, Tag::Replace);
230
231 let mut seek = Seek::new();
232
233 seek.extend(prefix.iter().copied());
234 seek.extend([u8::MAX; size_of::<u64>() * 2]);
235
236 self.db
237 .relatesto_typed
238 .rev_raw_keys_from(seek.as_slice())
239 .ignore_err()
240 .ready_take_while(move |key| key.starts_with(&prefix))
241 .map(|key| u64_from_u8(&key[CHILD_COUNT_OFFSET..KEY_LEN]))
242 .map(PduCount::from_unsigned)
243 .map(move |count| (shortroomid, count))
244 .filter_map(async |(shortroomid, count)| {
245 let child_id: RawPduId = PduId { shortroomid, count }.into();
246 self.services
247 .timeline
248 .get_pdu_from_id(&child_id)
249 .await
250 .ok()
251 .filter(|child| !child.is_redacted())
252 .filter(|child| child.sender() == parent.sender())
253 .filter(|child| child.kind() == parent.kind())
254 })
255}
256
257#[implement(Service)]
263#[tracing::instrument(skip_all, level = "trace")]
264pub async fn ignored_thread_view(
265 &self,
266 sender_user: &UserId,
267 ignored: &BTreeSet<OwnedUserId>,
268 root: &Pdu,
269) -> IgnoredThreadView {
270 let Ok(root_id) = self
271 .services
272 .timeline
273 .get_pdu_id(root.event_id())
274 .await
275 else {
276 return Unchanged;
277 };
278
279 let participants = self
280 .services
281 .threads
282 .get_participants(&root_id)
283 .await
284 .unwrap_or_default();
285
286 if !participants
287 .iter()
288 .any(|user| ignored.contains(user))
289 {
290 return Unchanged;
291 }
292
293 let root_pid: PduId = root_id.into();
294 let replies = self
295 .get_relations(
296 root_pid.shortroomid,
297 root_pid.count,
298 None,
299 Direction::Backward,
300 Some(sender_user),
301 )
302 .ready_filter_map(|(_, pdu)| {
303 pdu.get_content()
304 .is_ok_and(|content: ExtractRelatesTo| {
305 matches!(content.relates_to, Relation::Thread(_))
306 })
307 .then_some(pdu)
308 });
309
310 let fold = |(total, unignored, latest): (usize, usize, Option<Pdu>), pdu: Pdu| match ignored
311 .contains(pdu.sender())
312 {
313 | true => (total.saturating_add(1), unignored, latest),
314 | false => (total.saturating_add(1), unignored.saturating_add(1), latest.or(Some(pdu))),
315 };
316
317 let (total, unignored, latest) = replies.ready_fold((0, 0, None), fold).await;
318
319 if total == 0 {
320 return match self.redacted_root(ignored, root).await {
321 | None => Unchanged,
322 | root => Adjusted { root, count: None, latest: None },
323 };
324 }
325
326 if unignored == 0 {
327 return Omitted;
328 }
329
330 let swap = root
331 .thread_latest_event()
332 .is_some_and(|(_, sender)| ignored.contains(&sender));
333
334 let latest = match swap.then_some(latest).flatten() {
335 | None => None,
336 | Some(reply) => {
337 let reply = self
340 .services
341 .state_accessor
342 .erased_view(sender_user, &reply)
343 .await
344 .unwrap_or(reply);
345
346 Some(reply.into_format())
347 },
348 };
349
350 let count = unignored.ne(&total).then_some(unignored);
351
352 let root = self.redacted_root(ignored, root).await;
353
354 if root.is_none() && count.is_none() && latest.is_none() {
355 return Unchanged;
356 }
357
358 Adjusted { root, count, latest }
359}
360
361#[implement(Service)]
365#[tracing::instrument(skip_all, level = "trace")]
366async fn redacted_root(&self, ignored: &BTreeSet<OwnedUserId>, root: &Pdu) -> Option<Box<Pdu>> {
367 ignored
368 .contains(root.sender())
369 .then_async(async || {
370 self.services
371 .state
372 .get_room_version_rules(root.room_id())
373 .await
374 .log_err()
375 .ok()
376 .and_then(|rules| root.redacted(&rules.redaction).log_err().ok())
377 .map(Box::new)
378 })
379 .await
380 .flatten()
381}