1mod prune;
2
3use std::{collections::HashMap, fmt::Write, iter::once, sync::Arc};
4
5use async_trait::async_trait;
6use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt, future::join_all};
7pub(crate) use prune::prune_goal;
8pub use prune::{PruneSummary, Trigger};
9use ruma::{
10 CanonicalJsonObject, EventId, OwnedEventId, OwnedRoomId, RoomId, RoomVersionId, UserId,
11 events::{AnyStrippedStateEvent, StateEventType, TimelineEventType},
12 room_version_rules::AuthorizationRules,
13 serde::Raw,
14};
15use serde_json::value::RawValue as RawJsonValue;
16use tuwunel_core::{
17 Event, PduEvent, Result, err,
18 error::inspect_debug_log,
19 implement,
20 matrix::{PduCount, RoomVersionRules, StateKey, TypeStateKey, room_version},
21 result::{AndThenRef, FlatOk},
22 smallvec::SmallVec,
23 trace,
24 utils::{
25 IterStream, MutexMap, MutexMapGuard, ReadyExt, calculate_hash,
26 mutex_map::Guard,
27 stream::{BroadbandExt, TryIgnore, WidebandExt},
28 },
29 warn,
30};
31use tuwunel_database::{Deserialized, Ignore, Interfix, Map, Txn};
32
33use crate::{
34 rooms::{
35 short::{ShortEventId, ShortStateHash, ShortStateKey},
36 state_cache::MembershipUpdate,
37 state_compressor::{CompressedState, parse_compressed_state_event},
38 state_res::{StateMap, auth_types_for_event},
39 },
40 services::OnceServices,
41};
42
43pub struct Service {
44 pub mutex: RoomMutexMap,
50 services: Arc<OnceServices>,
51 db: Data,
52}
53
54struct Data {
55 shorteventid_shortstatehash: Arc<Map>,
56 roomid_shortstatehash: Arc<Map>,
57 roomid_pduleaves: Arc<Map>,
58}
59
60type RoomMutexMap = MutexMap<OwnedRoomId, ()>;
61pub type RoomMutexGuard = MutexMapGuard<OwnedRoomId, ()>;
62type ForwardExtremities = SmallVec<[OwnedEventId; 1]>;
63
64#[async_trait]
65impl crate::Service for Service {
66 fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
67 Ok(Arc::new(Self {
68 mutex: RoomMutexMap::new(),
69 services: args.services.clone(),
70 db: Data {
71 shorteventid_shortstatehash: args.db["shorteventid_shortstatehash"].clone(),
72 roomid_shortstatehash: args.db["roomid_shortstatehash"].clone(),
73 roomid_pduleaves: args.db["roomid_pduleaves"].clone(),
74 },
75 }))
76 }
77
78 async fn memory_usage(&self, out: &mut (dyn Write + Send)) -> Result {
79 let mutex = self.mutex.len();
80 writeln!(out, "- state_mutex: {mutex}")?;
81
82 Ok(())
83 }
84
85 fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
86}
87
88#[implement(Service)]
90#[tracing::instrument(
91 name = "force",
92 level = "debug",
93 skip_all,
94 fields(
95 count = ?self.services.globals.pending_count(),
96 %shortstatehash,
97 )
98)]
99pub async fn force_state(
100 &self,
101 room_id: &RoomId,
102 shortstatehash: u64,
103 statediffnew: Arc<CompressedState>,
104 _statediffremoved: Arc<CompressedState>,
105 state_lock: &RoomMutexGuard,
106) -> Result {
107 statediffnew
108 .iter()
109 .stream()
110 .map(|&new| parse_compressed_state_event(new).1)
111 .wide_filter_map(async |shorteventid| {
112 let event_id: OwnedEventId = self
113 .services
114 .short
115 .get_eventid_from_short(shorteventid)
116 .inspect_err(inspect_debug_log)
117 .await
118 .ok()?;
119
120 self.services
121 .timeline
122 .get_pdu(&event_id)
123 .await
124 .ok()
125 })
126 .map(Ok)
127 .try_for_each(async |pdu| match pdu.kind {
128 | TimelineEventType::RoomMember => {
129 let Some(user_id) = pdu
130 .state_key
131 .as_ref()
132 .map(UserId::parse)
133 .flat_ok()
134 else {
135 return Ok(());
136 };
137
138 let Ok(membership_event) = pdu.get_content() else {
139 return Ok(());
140 };
141
142 let count = self.services.globals.next_count();
143 self.services
144 .state_cache
145 .update_membership(MembershipUpdate {
146 room_id,
147 user_id: &user_id,
148 membership_event,
149 sender: &pdu.sender,
150 last_state: None,
151 invite_via: None,
152 update_joined_count: false,
153 count: PduCount::Normal(*count),
154 })
155 .await
156 },
157 | _ => Ok(()),
158 })
159 .boxed()
160 .await?;
161
162 self.services
163 .state_cache
164 .update_joined_count(room_id)
165 .await;
166
167 self.set_room_state(room_id, shortstatehash, state_lock);
168
169 self.services.spaces.cache_evict(room_id);
171
172 Ok(())
173}
174
175#[implement(Service)]
180#[tracing::instrument(
181 name = "set",
182 level = "debug",
183 skip(self, state_ids_compressed),
184 fields(
185 count = ?self.services.globals.pending_count(),
186 )
187)]
188pub async fn set_event_state(
189 &self,
190 event_id: &EventId,
191 room_id: &RoomId,
192 state_ids_compressed: Arc<CompressedState>,
193) -> Result<ShortStateHash> {
194 const KEY_LEN: usize = size_of::<ShortEventId>();
195 const VAL_LEN: usize = size_of::<ShortStateHash>();
196
197 let shorteventid = self
198 .services
199 .short
200 .get_or_create_shorteventid(event_id)
201 .await;
202
203 let state_hash = calculate_hash(state_ids_compressed.iter().map(|s| &s[..]));
204
205 if let Ok(shortstatehash) = self
206 .services
207 .short
208 .get_shortstatehash(&state_hash)
209 .await
210 {
211 self.db
212 .shorteventid_shortstatehash
213 .aput::<KEY_LEN, VAL_LEN, _, _>(shorteventid, shortstatehash);
214
215 return Ok(shortstatehash);
216 }
217
218 let previous_shortstatehash = self.get_room_shortstatehash(room_id).await;
219 let states_parents = match previous_shortstatehash {
220 | Ok(p) =>
221 self.services
222 .state_compressor
223 .load_shortstatehash_info(p)
224 .await?,
225 | _ => Vec::new(),
226 };
227
228 let (statediffnew, statediffremoved) = if let Some(parent_stateinfo) = states_parents.last() {
229 let statediffnew: CompressedState = state_ids_compressed
230 .difference(&parent_stateinfo.full_state)
231 .copied()
232 .collect();
233
234 let statediffremoved: CompressedState = parent_stateinfo
235 .full_state
236 .difference(&state_ids_compressed)
237 .copied()
238 .collect();
239
240 (Arc::new(statediffnew), Arc::new(statediffremoved))
241 } else {
242 (state_ids_compressed, Arc::new(CompressedState::new()))
243 };
244
245 let save_statediff = |txn: &mut Txn, shortstatehash| {
246 self.services
247 .state_compressor
248 .save_state_from_diff(
249 txn,
250 shortstatehash,
251 statediffnew,
252 statediffremoved,
253 1_000_000, states_parents,
255 )
256 };
257
258 let (shortstatehash, _) = self
259 .services
260 .short
261 .get_or_create_shortstatehash(&state_hash, save_statediff)
262 .await?;
263
264 self.db
265 .shorteventid_shortstatehash
266 .aput::<KEY_LEN, VAL_LEN, _, _>(shorteventid, shortstatehash);
267
268 Ok(shortstatehash)
269}
270
271#[implement(Service)]
278#[tracing::instrument(
279 name = "set",
280 level = "debug",
281 skip(self, new_pdu),
282 fields(
283 count = ?self.services.globals.pending_count(),
284 )
285)]
286pub async fn append_to_state(&self, new_pdu: &PduEvent) -> Result<u64> {
287 const KEY_LEN: usize = size_of::<ShortEventId>();
288 const VAL_LEN: usize = size_of::<ShortStateHash>();
289
290 let shorteventid = self
291 .services
292 .short
293 .get_or_create_shorteventid(&new_pdu.event_id)
294 .await;
295
296 let previous_shortstatehash = self
297 .get_room_shortstatehash(&new_pdu.room_id)
298 .await;
299
300 if let Ok(p) = previous_shortstatehash {
301 self.db
302 .shorteventid_shortstatehash
303 .aput::<KEY_LEN, VAL_LEN, _, _>(shorteventid, p);
304 }
305
306 match &new_pdu.state_key {
307 | Some(state_key) => {
308 let states_parents = match previous_shortstatehash {
309 | Ok(p) =>
310 self.services
311 .state_compressor
312 .load_shortstatehash_info(p)
313 .await?,
314 | _ => Vec::new(),
315 };
316
317 let shortstatekey = self
318 .services
319 .short
320 .get_or_create_shortstatekey(&new_pdu.kind.to_string().into(), state_key)
321 .await;
322
323 let new = self
324 .services
325 .state_compressor
326 .compress_state_event(shortstatekey, &new_pdu.event_id)
327 .await;
328
329 let replaces = states_parents
330 .last()
331 .map(|info| {
332 info.full_state
333 .iter()
334 .find(|bytes| bytes.starts_with(&shortstatekey.to_be_bytes()))
335 })
336 .unwrap_or_default();
337
338 if Some(&new) == replaces {
339 return Ok(previous_shortstatehash.expect("must exist"));
340 }
341
342 let shortstatehash = self.services.globals.next_count();
344 let mut txn = self.services.db.txn();
345
346 let mut statediffnew = CompressedState::new();
347 statediffnew.insert(new);
348
349 let mut statediffremoved = CompressedState::new();
350 if let Some(replaces) = replaces {
351 statediffremoved.insert(*replaces);
352 }
353
354 self.services
355 .state_compressor
356 .save_state_from_diff(
357 &mut txn,
358 *shortstatehash,
359 Arc::new(statediffnew),
360 Arc::new(statediffremoved),
361 2,
362 states_parents,
363 )?;
364
365 txn.execute();
366
367 Ok(*shortstatehash)
368 },
369 | _ => Ok(previous_shortstatehash.expect("first event in room must be a state event")),
370 }
371}
372
373#[implement(Service)]
375#[tracing::instrument(skip(self, _mutex_lock), level = "debug")]
376pub fn set_room_state(
377 &self,
378 room_id: &RoomId,
379 shortstatehash: u64,
380 _mutex_lock: &RoomMutexGuard,
382) {
383 const BUFSIZE: usize = size_of::<u64>();
384
385 self.db
386 .roomid_shortstatehash
387 .raw_aput::<BUFSIZE, _, _>(room_id, shortstatehash);
388}
389
390#[implement(Service)]
392#[expect(clippy::too_many_arguments)]
393#[tracing::instrument(skip(self, content), level = "debug")]
394pub async fn get_auth_events(
395 &self,
396 room_id: &RoomId,
397 kind: &TimelineEventType,
398 sender: &UserId,
399 state_key: Option<&str>,
400 content: &serde_json::value::RawValue,
401 auth_rules: &AuthorizationRules,
402 include_create: bool,
403) -> Result<StateMap<PduEvent>>
404where
405 StateEventType: Send + Sync,
406 StateKey: Send + Sync,
407{
408 let Ok(shortstatehash) = self.get_room_shortstatehash(room_id).await else {
409 return Ok(StateMap::new());
410 };
411
412 let sauthevents: HashMap<ShortStateKey, TypeStateKey> =
413 auth_types_for_event(kind, sender, state_key, content, auth_rules, include_create)?
414 .into_iter()
415 .stream()
416 .broad_filter_map(async |(event_type, state_key): TypeStateKey| {
417 self.services
418 .short
419 .get_shortstatekey(&event_type, &state_key)
420 .await
421 .map(move |sstatekey| (sstatekey, (event_type, state_key)))
422 .ok()
423 })
424 .collect()
425 .await;
426
427 let (state_keys, event_ids): (Vec<_>, Vec<_>) = self
428 .services
429 .state_accessor
430 .state_full_shortids(shortstatehash)
431 .ready_filter_map(Result::ok)
432 .ready_filter_map(|(shortstatekey, shorteventid)| {
433 sauthevents
434 .get(&shortstatekey)
435 .map(move |(ty, sk)| ((ty, sk), shorteventid))
436 })
437 .unzip()
438 .await;
439
440 self.services
441 .short
442 .multi_get_eventid_from_short(event_ids.into_iter().stream())
443 .zip(state_keys.into_iter().stream())
444 .ready_filter_map(|(event_id, (ty, sk))| Some(((ty, sk), event_id.ok()?)))
445 .broad_filter_map(async |((ty, sk), event_id): ((&_, &_), OwnedEventId)| {
446 let pdu = self.services.timeline.get_pdu(&event_id).await;
447
448 Some(((ty.clone(), sk.clone()), pdu.ok()?))
449 })
450 .collect()
451 .map(Ok)
452 .await
453}
454
455#[implement(Service)]
456#[tracing::instrument(skip_all, level = "debug")]
457pub async fn summary_stripped<Pdu: Event>(&self, event: &Pdu) -> Vec<Raw<AnyStrippedStateEvent>> {
458 let cells = [
459 (&StateEventType::RoomCreate, ""),
460 (&StateEventType::RoomJoinRules, ""),
461 (&StateEventType::RoomCanonicalAlias, ""),
462 (&StateEventType::RoomName, ""),
463 (&StateEventType::RoomAvatar, ""),
464 (&StateEventType::RoomMember, event.sender().as_str()), (&StateEventType::RoomEncryption, ""),
466 (&StateEventType::RoomTopic, ""),
467 ];
468
469 let fetches = cells.into_iter().map(|(event_type, state_key)| {
470 self.services
471 .state_accessor
472 .room_state_get(event.room_id(), event_type, state_key)
473 });
474
475 join_all(fetches)
476 .await
477 .into_iter()
478 .filter_map(Result::ok)
479 .map(Event::into_format)
480 .chain(once(event.to_format()))
481 .collect()
482}
483
484#[implement(Service)]
489#[tracing::instrument(skip_all, level = "debug")]
490pub async fn summary_pdus<Pdu: Event>(
491 &self,
492 event: &Pdu,
493 event_json: &CanonicalJsonObject,
494 room_version: &RoomVersionId,
495) -> Vec<Box<RawJsonValue>> {
496 let cells = [
497 (&StateEventType::RoomCreate, ""),
498 (&StateEventType::RoomJoinRules, ""),
499 (&StateEventType::RoomCanonicalAlias, ""),
500 (&StateEventType::RoomName, ""),
501 (&StateEventType::RoomAvatar, ""),
502 (&StateEventType::RoomMember, event.sender().as_str()),
503 (&StateEventType::RoomEncryption, ""),
504 (&StateEventType::RoomTopic, ""),
505 ];
506
507 let membership = self
508 .services
509 .federation
510 .format_pdu_into(event_json.clone(), Some(room_version))
511 .boxed() .await;
513
514 cells
515 .into_iter()
516 .stream()
517 .wide_filter_map(async |(event_type, state_key)| {
518 let pdu = self
519 .services
520 .state_accessor
521 .room_state_get(event.room_id(), event_type, state_key)
522 .await
523 .ok()?;
524
525 let pdu_json = self
526 .services
527 .timeline
528 .get_pdu_json(pdu.event_id())
529 .await
530 .ok()?;
531
532 Some(
533 self.services
534 .federation
535 .format_pdu_into(pdu_json, Some(room_version))
536 .await,
537 )
538 })
539 .chain(once(membership).stream())
540 .collect()
541 .await
542}
543
544#[implement(Service)]
546#[inline]
547pub async fn get_room_version_rules(&self, room_id: &RoomId) -> Result<RoomVersionRules> {
548 self.get_room_version(room_id)
549 .await
550 .and_then_ref(room_version::rules)
551}
552
553#[implement(Service)]
555#[tracing::instrument(
556 level = "debug"
557 skip(self),
558 ret(level = "trace"),
559)]
560pub async fn get_room_version(&self, room_id: &RoomId) -> Result<RoomVersionId> {
561 self.services
562 .state_accessor
563 .room_state_get_content(room_id, &StateEventType::RoomCreate, "")
564 .await
565 .as_ref()
566 .map(room_version::from_create_content)
567 .cloned()
568 .map_err(|e| err!(Request(NotFound("No create event found: {e:?}"))))
569}
570
571#[implement(Service)]
572#[tracing::instrument(
573 level = "debug"
574 skip(self),
575 ret(level = "trace"),
576)]
577pub async fn get_room_shortstatehash(&self, room_id: &RoomId) -> Result<ShortStateHash> {
578 self.db
579 .roomid_shortstatehash
580 .get(room_id)
581 .await
582 .deserialized()
583}
584
585#[implement(Service)]
587pub async fn pdu_shortstatehash(&self, event_id: &EventId) -> Result<ShortStateHash> {
588 self.services
589 .short
590 .get_shorteventid(event_id)
591 .and_then(|shorteventid| self.get_shortstatehash(shorteventid))
592 .await
593}
594
595#[implement(Service)]
597#[tracing::instrument(
598 level = "debug"
599 skip(self),
600 ret(level = "trace"),
601)]
602pub async fn get_shortstatehash(&self, shorteventid: ShortEventId) -> Result<ShortStateHash> {
603 const BUFSIZE: usize = size_of::<ShortEventId>();
604
605 self.db
606 .shorteventid_shortstatehash
607 .aqry::<BUFSIZE, _>(&shorteventid)
608 .await
609 .deserialized()
610}
611
612#[implement(Service)]
613pub(super) fn delete_room_shortstatehash(
614 &self,
615 room_id: &RoomId,
616 _mutex_lock: &Guard<OwnedRoomId, ()>,
617) -> Result {
618 self.db.roomid_shortstatehash.remove(room_id);
619
620 Ok(())
621}
622
623#[implement(Service)]
626#[tracing::instrument(
627 level = "debug"
628 skip_all,
629 fields(%room_id),
630)]
631pub async fn collapse_forward_extremities(
632 &self,
633 room_id: &RoomId,
634 state_lock: &RoomMutexGuard,
635) -> usize {
636 let extremities: ForwardExtremities = self
637 .get_forward_extremities(room_id)
638 .map(ToOwned::to_owned)
639 .collect()
640 .await;
641
642 if extremities.len() <= 1 {
643 return 0;
644 }
645
646 let survivor = join_all(extremities.iter().map(async |event_id| {
647 self.services
648 .timeline
649 .get_pdu_count(event_id)
650 .await
651 .ok()
652 .map(|count| (count, event_id))
653 }))
654 .await
655 .into_iter()
656 .flatten()
657 .max_by_key(|(count, _)| *count)
658 .map(|(_, event_id)| event_id);
659
660 let Some(survivor) = survivor else {
661 return 0;
662 };
663
664 self.set_forward_extremities(room_id, once(&**survivor), state_lock)
665 .await;
666
667 extremities.len().saturating_sub(1)
668}
669
670#[implement(Service)]
671#[tracing::instrument(
672 level = "trace"
673 skip(self),
674)]
675pub fn get_forward_extremities<'a>(
676 &'a self,
677 room_id: &'a RoomId,
678) -> impl Stream<Item = &EventId> + Send + '_ {
679 let prefix = (room_id, Interfix);
680
681 self.db
682 .roomid_pduleaves
683 .keys_prefix(&prefix)
684 .map_ok(|(_, event_id): (Ignore, &EventId)| event_id)
685 .ignore_err()
686}
687
688#[implement(Service)]
689#[tracing::instrument(
690 level = "debug"
691 skip_all,
692 fields(%room_id),
693)]
694pub async fn set_forward_extremities<'a, I>(
695 &'a self,
696 room_id: &'a RoomId,
697 event_ids: I,
698 _state_lock: &'a RoomMutexGuard,
699) where
700 I: Iterator<Item = &'a EventId> + Send + 'a,
701{
702 let prefix = (room_id, Interfix);
703 self.db
704 .roomid_pduleaves
705 .keys_prefix_raw(&prefix)
706 .ignore_err()
707 .ready_for_each(|key| self.db.roomid_pduleaves.remove(key))
708 .await;
709
710 for event_id in event_ids {
711 let key = (room_id, event_id);
712 self.db.roomid_pduleaves.put_raw(key, event_id);
713 }
714}
715
716#[implement(Service)]
717pub(super) async fn delete_all_rooms_forward_extremities(&self, room_id: &RoomId) -> Result {
718 let prefix = (room_id, Interfix);
719
720 self.db
721 .roomid_pduleaves
722 .keys_prefix_raw(&prefix)
723 .ignore_err()
724 .ready_for_each(|key| {
725 trace!("Removing key: {key:?}");
726 self.db.roomid_pduleaves.remove(key);
727 })
728 .await;
729
730 Ok(())
731}