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(unsafe))]
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.then(|| {
93			to_raw_value(&json!({ "redacted_because": {} }))
94				.expect("valid unsigned")
95				.into()
96		});
97
98		PduEvent {
99			kind,
100			content: Raw::from_json(
101				to_raw_value(&CanonicalJsonObject::new()).expect("valid content"),
102			),
103			event_id: event_id!("$event:example.com").to_owned(),
104			room_id: room_id!("!room:example.com").to_owned(),
105			sender: user_id!("@alice:example.com").to_owned(),
106			state_key,
107			redacts: None,
108			prev_events: Default::default(),
109			auth_events: Default::default(),
110			origin_server_ts: uint!(1),
111			depth: uint!(1),
112			hashes: Default::default(),
113			origin: None,
114			unsigned,
115		}
116	}
117
118	#[test]
119	fn default_bump_types_are_sorted() {
120		assert!(DEFAULT_BUMP_TYPES.is_sorted());
121	}
122
123	#[test]
124	fn default_bump_types_bump() {
125		let sender = user_id!("@alice:example.com");
126
127		for kind in DEFAULT_BUMP_TYPES.iter().cloned() {
128			assert!(is_bumpable_pdu(&pdu(kind, None, false), sender));
129		}
130	}
131
132	#[test]
133	fn non_bump_type_does_not_bump() {
134		let sender = user_id!("@alice:example.com");
135		let pdu = pdu(TimelineEventType::RoomName, Some("".into()), false);
136
137		assert!(!is_bumpable_pdu(&pdu, sender));
138	}
139
140	#[test]
141	fn own_membership_bumps() {
142		let sender = user_id!("@alice:example.com");
143		let pdu = pdu(TimelineEventType::RoomMember, Some(sender.as_str().into()), false);
144
145		assert!(is_bumpable_pdu(&pdu, sender));
146	}
147
148	#[test]
149	fn other_membership_does_not_bump() {
150		let sender = user_id!("@alice:example.com");
151		let pdu = pdu(TimelineEventType::RoomMember, Some("@bob:example.com".into()), false);
152
153		assert!(!is_bumpable_pdu(&pdu, sender));
154	}
155
156	#[test]
157	fn redacted_pdu_does_not_bump() {
158		let sender = user_id!("@alice:example.com");
159		let pdu = pdu(TimelineEventType::RoomMessage, None, true);
160
161		assert!(!is_bumpable_pdu(&pdu, sender));
162	}
163}