tuwunel_api_client/sync/v5/rooms/
bump_stamp.rs1use futures::{TryStreamExt, pin_mut};
2use ruma::{
3 RoomId, UInt, UserId,
4 events::TimelineEventType::{
5 self, Beacon, CallInvite, PollStart, RoomEncrypted, RoomMessage, Sticker,
6 },
7};
8use tuwunel_core::{Result, is_equal_to, utils::TryReadyExt};
9use tuwunel_matrix::{
10 Event,
11 pdu::{PduCount, PduEvent},
12};
13use tuwunel_service::Services;
14
15static DEFAULT_BUMP_TYPES: [TimelineEventType; 6] = [
17 CallInvite, PollStart, RoomEncrypted, RoomMessage, Sticker, Beacon, ];
24
25pub(super) fn knock_bump_stamp(count: u64, next_batch: u64) -> Option<UInt> {
26 let valid = 1..=next_batch;
27
28 valid
29 .contains(&count)
30 .then(|| count.try_into().ok())
31 .flatten()
32}
33
34pub(super) async fn room_bump_stamp(
35 services: &Services,
36 sender_user: &UserId,
37 room_id: &RoomId,
38 roomsince: PduCount,
39 next_batch: PduCount,
40 last_timeline_count: PduCount,
41) -> Result<Option<UInt>> {
42 if last_timeline_count <= roomsince {
43 return Ok(None);
44 }
45
46 let bumpable_pdus = services
47 .timeline
48 .pdus_rev(Some(sender_user), room_id, Some(next_batch.saturating_add(1)))
49 .ready_try_take_while(|&(pdu_count, _)| Ok(pdu_count > roomsince))
50 .ready_try_filter_map(|(pdu_count, pdu)| {
51 Ok(is_bumpable_pdu(&pdu, sender_user)
52 .then(|| pdu_count.into_signed().try_into().ok())
53 .flatten())
54 });
55
56 pin_mut!(bumpable_pdus);
57 bumpable_pdus.try_next().await
58}
59
60fn is_bumpable_pdu(pdu: &PduEvent, sender_user: &UserId) -> bool {
61 if pdu.is_redacted() {
62 return false;
63 }
64
65 if *pdu.event_type() == TimelineEventType::RoomMember {
66 return pdu
67 .state_key()
68 .is_some_and(is_equal_to!(sender_user.as_str()));
69 }
70
71 DEFAULT_BUMP_TYPES
72 .binary_search(pdu.event_type())
73 .is_ok()
74}
75
76#[cfg_attr(debug_assertions, tuwunel_core::ctor(unsafe))]
77fn _is_sorted() {
78 debug_assert!(
79 DEFAULT_BUMP_TYPES.is_sorted(),
80 "DEFAULT_BUMP_TYPES must be sorted by the developer"
81 );
82}
83
84#[cfg(test)]
85mod tests {
86 use ruma::{
87 CanonicalJsonObject, UInt, event_id, events::TimelineEventType, room_id, serde::Raw,
88 uint, user_id,
89 };
90 use serde_json::{json, value::to_raw_value};
91 use tuwunel_matrix::{StateKey, pdu::PduEvent};
92
93 use super::{DEFAULT_BUMP_TYPES, is_bumpable_pdu, knock_bump_stamp};
94
95 #[test]
96 fn knock_baseline_is_positive_bounded_and_representable() {
97 assert_eq!(knock_bump_stamp(1, 10), Some(uint!(1)));
98 assert_eq!(knock_bump_stamp(10, 10), Some(uint!(10)));
99 assert_eq!(knock_bump_stamp(0, 10), None);
100 assert_eq!(knock_bump_stamp(1, 0), None);
101 assert_eq!(knock_bump_stamp(11, 10), None);
102 assert_eq!(knock_bump_stamp(UInt::MAX.into(), u64::MAX), Some(UInt::MAX));
103 assert_eq!(knock_bump_stamp(u64::from(UInt::MAX) + 1, u64::MAX), None);
104 assert_eq!(knock_bump_stamp(u64::MAX, u64::MAX), None);
105 }
106
107 fn pdu(kind: TimelineEventType, state_key: Option<StateKey>, redacted: bool) -> PduEvent {
108 let unsigned = redacted.then(|| {
109 to_raw_value(&json!({ "redacted_because": {} }))
110 .expect("valid unsigned")
111 .into()
112 });
113
114 PduEvent {
115 kind,
116 content: Raw::from_json(
117 to_raw_value(&CanonicalJsonObject::new()).expect("valid content"),
118 ),
119 event_id: event_id!("$event:example.com").to_owned(),
120 room_id: room_id!("!room:example.com").to_owned(),
121 sender: user_id!("@alice:example.com").to_owned(),
122 state_key,
123 redacts: None,
124 prev_events: Default::default(),
125 auth_events: Default::default(),
126 origin_server_ts: uint!(1),
127 depth: uint!(1),
128 hashes: Default::default(),
129 origin: None,
130 unsigned,
131 }
132 }
133
134 #[test]
135 fn default_bump_types_are_sorted() {
136 assert!(DEFAULT_BUMP_TYPES.is_sorted());
137 }
138
139 #[test]
140 fn default_bump_types_bump() {
141 let sender = user_id!("@alice:example.com");
142
143 for kind in DEFAULT_BUMP_TYPES.iter().cloned() {
144 assert!(is_bumpable_pdu(&pdu(kind, None, false), sender));
145 }
146 }
147
148 #[test]
149 fn non_bump_type_does_not_bump() {
150 let sender = user_id!("@alice:example.com");
151 let pdu = pdu(TimelineEventType::RoomName, Some("".into()), false);
152
153 assert!(!is_bumpable_pdu(&pdu, sender));
154 }
155
156 #[test]
157 fn own_membership_bumps() {
158 let sender = user_id!("@alice:example.com");
159 let pdu = pdu(TimelineEventType::RoomMember, Some(sender.as_str().into()), false);
160
161 assert!(is_bumpable_pdu(&pdu, sender));
162 }
163
164 #[test]
165 fn other_membership_does_not_bump() {
166 let sender = user_id!("@alice:example.com");
167 let pdu = pdu(TimelineEventType::RoomMember, Some("@bob:example.com".into()), false);
168
169 assert!(!is_bumpable_pdu(&pdu, sender));
170 }
171
172 #[test]
173 fn redacted_pdu_does_not_bump() {
174 let sender = user_id!("@alice:example.com");
175 let pdu = pdu(TimelineEventType::RoomMessage, None, true);
176
177 assert!(!is_bumpable_pdu(&pdu, sender));
178 }
179}