tuwunel_service/rooms/threads/
mod.rs1use std::{collections::BTreeMap, pin::pin, sync::Arc};
2
3use futures::{Stream, StreamExt, TryFutureExt, future::join3};
4use ruma::{
5 CanonicalJsonValue, EventId, OwnedEventId, OwnedUserId, RoomId, UserId,
6 api::{Direction, client::threads::get_threads::v1::IncludeThreads},
7 events::{
8 TimelineEventType,
9 relation::{BundledThread, RelationType},
10 },
11 uint,
12};
13use serde::Deserialize;
14use serde_json::json;
15use tuwunel_core::{
16 Event, Result, err,
17 matrix::pdu::{PduCount, PduEvent, PduId, RawPduId},
18 utils::{
19 ReadyExt,
20 stream::{TryIgnore, WidebandExt, automatic_width},
21 },
22};
23use tuwunel_database::{Deserialized, Map, Txn};
24
25#[cfg(test)]
26mod tests;
27
28const MAX_THREAD_HOPS: usize = 3;
31
32#[derive(Deserialize)]
33struct ExtractThreadRelation {
34 #[serde(rename = "m.relates_to")]
35 relates_to: ThreadRelation,
36}
37
38#[derive(Deserialize)]
39struct ThreadRelation {
40 rel_type: RelationType,
41 event_id: OwnedEventId,
42}
43
44pub struct Service {
45 db: Data,
46 services: Arc<crate::services::OnceServices>,
47}
48
49pub(super) struct Data {
50 threadid_userids: Arc<Map>,
51 threadactivityid_rootid: Arc<Map>,
52 threadrootid_latestcount: Arc<Map>,
53}
54
55impl crate::Service for Service {
56 fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
57 Ok(Arc::new(Self {
58 db: Data {
59 threadid_userids: args.db["threadid_userids"].clone(),
60 threadactivityid_rootid: args.db["threadactivityid_rootid"].clone(),
61 threadrootid_latestcount: args.db["threadrootid_latestcount"].clone(),
62 },
63 services: args.services.clone(),
64 }))
65 }
66
67 fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
68}
69
70impl Service {
71 pub async fn get_thread_id<E>(&self, event: &E) -> Option<OwnedEventId>
77 where
78 E: Event,
79 {
80 let initial = match event.get_content::<ExtractThreadRelation>() {
81 | Ok(t) => Some(t.relates_to),
82 | Err(_) => self.relates_to_via_redaction_target(event).await,
83 };
84
85 let mut relates_to = initial?;
86
87 for _ in 0..MAX_THREAD_HOPS {
88 if relates_to.rel_type == RelationType::Thread {
89 return Some(relates_to.event_id);
90 }
91
92 relates_to = self
93 .services
94 .timeline
95 .get_pdu(&relates_to.event_id)
96 .await
97 .ok()?
98 .get_content::<ExtractThreadRelation>()
99 .ok()?
100 .relates_to;
101 }
102
103 None
104 }
105
106 async fn relates_to_via_redaction_target<E>(&self, event: &E) -> Option<ThreadRelation>
110 where
111 E: Event,
112 {
113 if *event.kind() != TimelineEventType::RoomRedaction {
114 return None;
115 }
116
117 let room_rules = self
118 .services
119 .state
120 .get_room_version_rules(event.room_id())
121 .await
122 .ok()?;
123
124 let target_id = event.redacts_id(&room_rules)?;
125
126 self.services
127 .timeline
128 .get_pdu(&target_id)
129 .await
130 .ok()?
131 .get_content::<ExtractThreadRelation>()
132 .ok()
133 .map(|t| t.relates_to)
134 }
135
136 pub async fn get_thread_id_for_event(&self, event_id: &EventId) -> Option<OwnedEventId> {
139 let pdu = self
140 .services
141 .timeline
142 .get_pdu(event_id)
143 .await
144 .ok()?;
145
146 self.get_thread_id(&pdu).await
147 }
148
149 pub async fn add_to_thread<E>(
150 &self,
151 root_event_id: &EventId,
152 pdu_id: RawPduId,
153 event: &E,
154 ) -> Result
155 where
156 E: Event,
157 {
158 let root_id = self
159 .services
160 .timeline
161 .get_pdu_id(root_event_id)
162 .await
163 .map_err(|e| {
164 err!(Request(InvalidParam("Invalid event_id in thread message: {e:?}")))
165 })?;
166
167 let root_pdu = self
168 .services
169 .timeline
170 .get_pdu_from_id(&root_id)
171 .await
172 .map_err(|e| err!(Request(InvalidParam("Thread root not found: {e:?}"))))?;
173
174 let mut root_pdu_json = self
175 .services
176 .timeline
177 .get_pdu_json_from_id(&root_id)
178 .await
179 .map_err(|e| err!(Request(InvalidParam("Thread root pdu not found: {e:?}"))))?;
180
181 let mut users = self
182 .get_participants(&root_id)
183 .await
184 .unwrap_or_else(|_| vec![root_pdu.sender().to_owned()]);
185
186 users.push(event.sender().to_owned());
187
188 let mut txn = self.services.db.txn();
191
192 self.update_participants(&mut txn, &root_id, &users);
193
194 let count = pdu_id.pdu_count();
195
196 if matches!(count, PduCount::Normal(_)) {
197 txn.insert_raw(&self.db.threadactivityid_rootid, pdu_id, root_id);
198 txn.insert_raw(&self.db.threadrootid_latestcount, root_id, count.to_be_bytes());
199 }
200
201 txn.execute();
202
203 if let CanonicalJsonValue::Object(unsigned) = root_pdu_json
204 .entry("unsigned".into())
205 .or_insert_with(|| CanonicalJsonValue::Object(BTreeMap::default()))
206 {
207 if let Some(mut relations) = unsigned
208 .get("m.relations")
209 .and_then(|r| r.as_object())
210 .and_then(|r| r.get("m.thread"))
211 .and_then(|relations| {
212 serde_json::from_value::<BundledThread>(relations.clone().into()).ok()
213 }) {
214 relations.count = relations.count.saturating_add(uint!(1));
216 relations.latest_event = event.to_format();
217
218 let content = serde_json::to_value(relations).expect("to_value always works");
219
220 unsigned.insert(
221 "m.relations".into(),
222 json!({ "m.thread": content })
223 .try_into()
224 .expect("thread is valid json"),
225 );
226 } else {
227 let relations = BundledThread {
229 latest_event: event.to_format(),
230 count: uint!(1),
231 current_user_participated: true,
232 };
233
234 let content = serde_json::to_value(relations).expect("to_value always works");
235
236 unsigned.insert(
237 "m.relations".into(),
238 json!({ "m.thread": content })
239 .try_into()
240 .expect("thread is valid json"),
241 );
242 }
243
244 self.services
245 .timeline
246 .replace_pdu(&root_id, &root_pdu_json)
247 .await?;
248 }
249
250 Ok(())
251 }
252
253 pub fn threads_until<'a>(
254 &'a self,
255 user_id: &'a UserId,
256 room_id: &'a RoomId,
257 count: PduCount,
258 include: &'a IncludeThreads,
259 ) -> impl Stream<Item = Result<(PduCount, PduEvent)>> + Send {
260 let participated = matches!(include, IncludeThreads::Participated);
261
262 self.services
263 .short
264 .get_shortroomid(room_id)
265 .map_ok(move |shortroomid| PduId {
266 shortroomid,
267 count: count.saturating_sub(1),
268 })
269 .map_ok(Into::into)
270 .map_ok(move |current: RawPduId| {
271 self.db
272 .threadactivityid_rootid
273 .rev_raw_stream_from(¤t)
274 .ignore_err()
275 .map(|(key, root_id)| (RawPduId::from(key), RawPduId::from(root_id)))
276 .ready_take_while(move |(activity_id, _)| {
277 activity_id.shortroomid() == current.shortroomid()
278 })
279 .map(move |(activity_id, root_id)| {
280 (activity_id, root_id, user_id, participated)
281 })
282 .wide_filter_map(async |(activity_id, root_id, user_id, participated)| {
283 self.live_thread(user_id, participated, activity_id, root_id)
284 .await
285 })
286 .map(Ok)
287 })
288 .try_flatten_stream()
289 }
290
291 async fn live_thread(
294 &self,
295 user_id: &UserId,
296 participated: bool,
297 activity_id: RawPduId,
298 root_id: RawPduId,
299 ) -> Option<(PduCount, PduEvent)> {
300 let count = activity_id.pdu_count();
301
302 let pointer = self
303 .db
304 .threadrootid_latestcount
305 .get(&root_id)
306 .await
307 .deserialized()
308 .map(PduCount::from_unsigned)
309 .ok()?;
310
311 if count != pointer {
312 if count < pointer {
315 self.db
316 .threadactivityid_rootid
317 .remove(&activity_id);
318 }
319
320 return None;
321 }
322
323 if participated && !self.is_participant(&root_id, user_id).await {
324 return None;
325 }
326
327 let mut pdu = self
328 .services
329 .timeline
330 .get_pdu_from_id(&root_id)
331 .await
332 .ok()?;
333
334 if pdu.sender() != user_id {
335 pdu.as_mut_pdu().remove_transaction_id().ok();
336 }
337
338 Some((count, pdu))
339 }
340
341 async fn is_participant(&self, root_id: &RawPduId, user_id: &UserId) -> bool {
342 self.db
343 .threadid_userids
344 .get(root_id)
345 .await
346 .is_ok_and(|participants| {
347 participants
348 .split(|&byte| byte == 0xFF)
349 .any(|user| user == user_id.as_bytes())
350 })
351 }
352
353 pub(super) fn update_participants(
354 &self,
355 txn: &mut Txn,
356 root_id: &RawPduId,
357 participants: &[OwnedUserId],
358 ) {
359 let users = participants
360 .iter()
361 .map(|user| user.as_bytes())
362 .collect::<Vec<_>>()
363 .join(&[0xFF][..]);
364
365 txn.insert_raw(&self.db.threadid_userids, root_id, &users);
366 }
367
368 pub(super) async fn get_participants(&self, root_id: &RawPduId) -> Result<Vec<OwnedUserId>> {
369 self.db
370 .threadid_userids
371 .get(root_id)
372 .await
373 .deserialized()
374 }
375
376 pub async fn user_participated(&self, root_event_id: &EventId, user_id: &UserId) -> bool {
379 let Ok(root_id) = self
380 .services
381 .timeline
382 .get_pdu_id(root_event_id)
383 .await
384 else {
385 return false;
386 };
387
388 self.is_participant(&root_id, user_id).await
389 }
390
391 #[tracing::instrument(skip(self), level = "debug")]
392 pub(super) async fn delete_all_rooms_threads(&self, room_id: &RoomId) -> Result {
393 let Ok(shortroomid) = self.services.short.get_shortroomid(room_id).await else {
394 return Ok(());
395 };
396
397 join3(
398 self.db.threadid_userids.del_prefix(&shortroomid),
399 self.db
400 .threadactivityid_rootid
401 .del_prefix(&shortroomid),
402 self.db
403 .threadrootid_latestcount
404 .del_prefix(&shortroomid),
405 )
406 .await;
407
408 Ok(())
409 }
410
411 pub async fn rebuild_thread_activity(&self) -> Result {
415 self.db.threadactivityid_rootid.clear().await;
416 self.db.threadrootid_latestcount.clear().await;
417
418 self.db
419 .threadid_userids
420 .raw_keys()
421 .ignore_err()
422 .map(RawPduId::from)
423 .for_each_concurrent(automatic_width(), async |root_id| {
424 self.index_thread_activity(root_id).await;
425 })
426 .await;
427
428 Ok(())
429 }
430
431 async fn index_thread_activity(&self, root_id: RawPduId) {
432 let root: PduId = root_id.into();
433
434 let replies = self
435 .services
436 .pdu_metadata
437 .get_relations(root.shortroomid, root.count, None, Direction::Backward, None)
438 .ready_filter_map(|(count, pdu)| {
439 pdu.get_content()
440 .is_ok_and(|content: ExtractThreadRelation| {
441 content.relates_to.rel_type == RelationType::Thread
442 })
443 .then_some(count)
444 });
445
446 let mut replies = pin!(replies);
447
448 let latest = replies.next().await.unwrap_or(root.count);
449
450 let activity_id: RawPduId = PduId {
451 shortroomid: root.shortroomid,
452 count: latest,
453 }
454 .into();
455
456 let mut txn = self.services.db.txn();
457
458 txn.insert_raw(&self.db.threadactivityid_rootid, activity_id, root_id);
459 txn.insert_raw(&self.db.threadrootid_latestcount, root_id, latest.to_be_bytes());
460 txn.execute();
461 }
462}