Skip to main content

tuwunel_api_client/sync/v5/rooms/
bump_stamp.rs

1use 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
15/// MUST be sorted by `TimelineEventType::event_type_str()` for `binary_search`.
16static DEFAULT_BUMP_TYPES: [TimelineEventType; 6] = [
17	CallInvite,    // m.call.invite
18	PollStart,     // m.poll.start
19	RoomEncrypted, // m.room.encrypted
20	RoomMessage,   // m.room.message
21	Sticker,       // m.sticker
22	Beacon,        // org.matrix.msc3672.beacon
23];
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}