Skip to main content

tuwunel_api/client/sync/v5/rooms/
bump_stamp.rs

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