Skip to main content

tuwunel_api_client/
message.rs

1use axum::extract::State;
2use futures::{FutureExt, StreamExt, TryFutureExt, pin_mut};
3use ruma::{
4	DeviceId, RoomId, UInt, UserId,
5	api::{
6		Direction,
7		client::{filter::RoomEventFilter, message::get_message_events},
8	},
9	events::{
10		AnyStateEvent, StateEventType, TimelineEventType, TimelineEventType::*,
11		relation::RelationType,
12	},
13	serde::Raw,
14};
15use tuwunel_core::{
16	Err, Result, at, err, ref_at,
17	smallvec::SmallVec,
18	utils::{
19		BoolExt, IterStream, ReadyExt,
20		math::usize_from_ruma_bounded,
21		result::LogErr,
22		stream::{BroadbandExt, TryIgnore, WidebandExt},
23	},
24};
25use tuwunel_matrix::{
26	PduId,
27	event::{Event, Matches},
28	pdu::{PduCount, PduEvent},
29};
30use tuwunel_service::{
31	Services,
32	rooms::{
33		lazy_loading,
34		lazy_loading::{Options, Witness},
35		short::ShortRoomId,
36		timeline::PdusIterItem,
37	},
38};
39
40use super::visibility_filter;
41use crate::Ruma;
42
43/// Shared inputs for [`get_messages`], the pagination core behind both the
44/// client-server `/messages` route and the admin room-messages endpoint.
45pub struct MessagesArgs<'a> {
46	/// Room whose timeline is paginated.
47	pub room_id: &'a RoomId,
48
49	/// User requesting the timeline page.
50	pub sender_user: &'a UserId,
51
52	/// Requesting device used for lazy-loading membership state.
53	pub sender_device: Option<&'a DeviceId>,
54
55	/// Optional starting pagination token.
56	pub from: Option<&'a str>,
57
58	/// Optional stopping pagination token.
59	pub to: Option<&'a str>,
60
61	/// Direction in which to paginate the timeline.
62	pub dir: Direction,
63
64	/// Requested maximum number of returned timeline events.
65	pub limit: Option<UInt>,
66
67	/// Filter for timeline events and membership state.
68	pub filter: &'a RoomEventFilter,
69
70	/// Skip the room-visibility gate and the per-event visibility and ignore
71	/// filters, for admin callers that see all history.
72	pub bypass_visibility: bool,
73}
74
75/// list of safe and common non-state events to ignore if the user is ignored.
76/// MUST be sorted by `TimelineEventType::event_type_str()` for `binary_search`.
77const IGNORED_MESSAGE_TYPES: &[TimelineEventType] = &[
78	CallInvite,           // m.call.invite
79	KeyVerificationStart, // m.key.verification.start
80	Location,             // m.location
81	PollStart,            // m.poll.start
82	Reaction,             // m.reaction
83	RoomEncrypted,        // m.room.encrypted
84	RoomMessage,          // m.room.message
85	Sticker,              // m.sticker
86	Audio,                // org.matrix.msc1767.audio
87	Emote,                // org.matrix.msc1767.emote
88	File,                 // org.matrix.msc1767.file
89	Image,                // org.matrix.msc1767.image
90	Video,                // org.matrix.msc1767.video
91	Voice,                // org.matrix.msc3245.voice.v2
92	UnstablePollStart,    // org.matrix.msc3381.poll.start
93	Beacon,               // org.matrix.msc3672.beacon
94	CallNotify,           // org.matrix.msc4075.call.notify
95];
96
97/// MSC3440 `related_by_rel_types` entries, typed at the compare boundary.
98type RelTypes = SmallVec<[RelationType; 1]>;
99
100const LIMIT_MAX: usize = 1000;
101const LIMIT_DEFAULT: usize = 10;
102
103/// # `GET /_matrix/client/r0/rooms/{roomId}/messages`
104///
105/// Allows paginating through room history.
106///
107/// - Only works if the user is joined (TODO: always allow, but only show events
108///   where the user was joined, depending on `history_visibility`)
109pub(crate) async fn get_message_events_route(
110	State(services): State<crate::State>,
111	body: Ruma<get_message_events::v3::Request>,
112) -> Result<get_message_events::v3::Response> {
113	get_messages(&services, MessagesArgs {
114		room_id: &body.room_id,
115		sender_user: body.sender_user(),
116		sender_device: body.sender_device.as_deref(),
117		from: body.from.as_deref(),
118		to: body.to.as_deref(),
119		dir: body.dir,
120		limit: Some(body.limit),
121		filter: &body.filter,
122		bypass_visibility: false,
123	})
124	.await
125}
126
127/// Paginates a room's timeline, applying the request filter and (unless
128/// `bypass_visibility`) the per-user visibility and ignore filters.
129///
130/// Powers the client-server `/messages` route and its admin bypass twin.
131pub async fn get_messages(
132	services: &Services,
133	args: MessagesArgs<'_>,
134) -> Result<get_message_events::v3::Response> {
135	let MessagesArgs {
136		room_id,
137		sender_user,
138		sender_device,
139		from,
140		to,
141		dir,
142		limit,
143		filter,
144		bypass_visibility,
145	} = args;
146
147	if !services.metadata.exists(room_id).await {
148		return Err!(Request(Forbidden("Room does not exist to this server")));
149	}
150
151	if !bypass_visibility
152		&& !services
153			.state_accessor
154			.user_can_see_room(sender_user, room_id)
155			.await
156	{
157		return Err!(Request(Forbidden("You don't have permission to view this room.")));
158	}
159
160	let from: PduCount = from
161		.map(str::parse)
162		.transpose()
163		.map_err(|_| err!(Request(InvalidParam("Invalid `from` token."))))?
164		.unwrap_or_else(|| match dir {
165			| Direction::Forward => PduCount::min(),
166			| Direction::Backward => PduCount::max(),
167		});
168
169	let to: Option<PduCount> = to
170		.map(str::parse)
171		.transpose()
172		.map_err(|_| err!(Request(InvalidParam("Invalid `to` token."))))?;
173
174	let limit = limit
175		.map_or(LIMIT_DEFAULT, |limit| usize_from_ruma_bounded(limit, LIMIT_DEFAULT, LIMIT_MAX));
176
177	if matches!(dir, Direction::Backward) {
178		services
179			.timeline
180			.backfill_if_required(room_id, from)
181			.await
182			.log_err()
183			.ok();
184	}
185
186	let it = match dir {
187		| Direction::Forward => services
188			.timeline
189			.pdus(Some(sender_user), room_id, Some(from))
190			.ignore_err()
191			.left_stream(),
192
193		| Direction::Backward => services
194			.timeline
195			.pdus_rev(Some(sender_user), room_id, Some(from))
196			.ignore_err()
197			.right_stream(),
198	};
199
200	let encrypted = services
201		.state_accessor
202		.is_encrypted_room(room_id)
203		.await;
204
205	let shortroomid = services.short.get_shortroomid(room_id).await?;
206	let mut scanned = None;
207	let reached_to = |count: PduCount| {
208		to.is_some_and(|to| match dir {
209			| Direction::Forward => count >= to,
210			| Direction::Backward => count <= to,
211		})
212	};
213
214	let events: Vec<_> = it
215		.inspect(|(count, _)| scanned = Some(*count))
216		.ready_take_while(|(count, _)| !reached_to(*count))
217		.ready_filter_map(|item| event_filter(item, filter))
218		.wide_filter_map(|item| related_by_filter(services, shortroomid, filter, item))
219		.wide_filter_map(|item| event_filters(services, sender_user, item, bypass_visibility))
220		.take(limit)
221		.wide_then(|item| add_membership_unsigned(services, item, sender_user, encrypted))
222		.wide_then(async |(count, pdu)| {
223			let pdu = services
224				.pdu_metadata
225				.bundle_aggregations(sender_user, pdu)
226				.await;
227
228			(count, pdu)
229		})
230		.collect()
231		.await;
232
233	let lazy_loading_context = lazy_loading::Context {
234		user_id: sender_user,
235		device_id: sender_device,
236		room_id,
237		token: Some(from.into_unsigned()),
238		options: Some(&filter.lazy_load_options),
239		mode: lazy_loading::Mode::Update,
240	};
241
242	let witness = filter
243		.lazy_load_options
244		.is_enabled()
245		.then_async(|| lazy_loading_witness(services, &lazy_loading_context, events.iter()));
246
247	let state = witness
248		.map(Option::into_iter)
249		.map(|option| option.flat_map(Witness::into_iter))
250		.map(IterStream::stream)
251		.into_stream()
252		.flatten()
253		.broad_filter_map(async |user_id| {
254			get_member_event(services, sender_user, room_id, &user_id).await
255		})
256		.collect()
257		.await;
258
259	// `inspect` records the rejected boundary item, distinguishing a `to` stop
260	// from stream exhaustion.
261	let stopped_at_to = scanned.is_some_and(reached_to);
262	let exhausted = matches!(dir, Direction::Backward) && events.len() < limit && !stopped_at_to;
263	let next_token = if exhausted { scanned } else { events.last().map(at!(0)) };
264
265	let chunk = events
266		.into_iter()
267		.map(at!(1))
268		.map(Event::into_format)
269		.collect();
270
271	Ok(get_message_events::v3::Response {
272		start: from.to_string(),
273		end: next_token.as_ref().map(ToString::to_string),
274		chunk,
275		state,
276	})
277}
278
279pub(crate) async fn lazy_loading_witness<'a, I>(
280	services: &Services,
281	lazy_loading_context: &lazy_loading::Context<'_>,
282	events: I,
283) -> Witness
284where
285	I: Iterator<Item = &'a PdusIterItem> + Clone + Send,
286{
287	let oldest = events
288		.clone()
289		.map(|(count, _)| count)
290		.copied()
291		.min()
292		.unwrap_or_else(PduCount::max);
293
294	let newest = events
295		.clone()
296		.map(|(count, _)| count)
297		.copied()
298		.max()
299		.unwrap_or_else(PduCount::max);
300
301	let receipts = services.read_receipt.readreceipts_since(
302		lazy_loading_context.room_id,
303		oldest.into_unsigned(),
304		Some(newest.into_unsigned()),
305	);
306
307	pin_mut!(receipts);
308	let witness: Witness = events
309		.stream()
310		.map(ref_at!(1))
311		.map(Event::sender)
312		.map(ToOwned::to_owned)
313		.chain(
314			receipts
315				.ready_take_while(|(_, c, _)| *c <= newest.into_unsigned())
316				.map(|(user_id, ..)| user_id.to_owned()),
317		)
318		.collect()
319		.await;
320
321	services
322		.lazy_loading
323		.witness_retain(witness, lazy_loading_context)
324		.await
325}
326
327async fn get_member_event(
328	services: &Services,
329	sender_user: &UserId,
330	room_id: &RoomId,
331	user_id: &UserId,
332) -> Option<Raw<AnyStateEvent>> {
333	services
334		.state_accessor
335		.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())
336		.map_ok(|event| event.without_private_unless_sender(Some(sender_user)))
337		.map_ok(Event::into_format)
338		.await
339		.ok()
340}
341
342pub(crate) async fn event_filters(
343	services: &Services,
344	user_id: &UserId,
345	item: PdusIterItem,
346	bypass_visibility: bool,
347) -> Option<PdusIterItem> {
348	if bypass_visibility {
349		return Some(item);
350	}
351
352	let item = ignored_filter(services, item, user_id).await?;
353	let item = visibility_filter(services, item, user_id).await?;
354
355	Some(item)
356}
357
358/// MSC3440 `related_by_*`: include an event only when another event relates
359/// to it matching the filter's reverse-relation criteria. A no-op stage when
360/// the filter carries neither field.
361pub(crate) async fn related_by_filter(
362	services: &Services,
363	shortroomid: ShortRoomId,
364	filter: &RoomEventFilter,
365	item: PdusIterItem,
366) -> Option<PdusIterItem> {
367	if filter.related_by_senders.is_empty() && filter.related_by_rel_types.is_empty() {
368		return Some(item);
369	}
370
371	let rel_types: RelTypes = filter
372		.related_by_rel_types
373		.iter()
374		.map(String::as_str)
375		.map(RelationType::from)
376		.collect();
377
378	let (count, _) = &item;
379	let target = PduId { shortroomid, count: *count };
380
381	services
382		.pdu_metadata
383		.has_incoming_relation(target, &filter.related_by_senders, &rel_types)
384		.await
385		.then_some(item)
386}
387
388#[inline]
389pub(crate) async fn ignored_filter(
390	services: &Services,
391	item: PdusIterItem,
392	user_id: &UserId,
393) -> Option<PdusIterItem> {
394	let (_, ref pdu) = item;
395
396	is_ignored_pdu(services, pdu, user_id)
397		.await
398		.is_false()
399		.then_some(item)
400}
401
402#[inline]
403pub(crate) async fn is_ignored_pdu<Pdu>(
404	services: &Services,
405	event: &Pdu,
406	user_id: &UserId,
407) -> bool
408where
409	Pdu: Event,
410{
411	// exclude Synapse's dummy events from bloating up response bodies. clients
412	// don't need to see this.
413	if event.kind().to_cow_str() == "org.matrix.dummy_event" {
414		return true;
415	}
416
417	if IGNORED_MESSAGE_TYPES
418		.binary_search(event.kind())
419		.is_err()
420	{
421		return false;
422	}
423
424	let ignored_server = services
425		.config
426		.is_forbidden_remote_server_name(event.sender().server_name());
427
428	ignored_server
429		|| services
430			.users
431			.user_is_ignored(event.sender(), user_id)
432			.await
433}
434
435#[inline]
436pub(crate) fn event_filter(item: PdusIterItem, filter: &RoomEventFilter) -> Option<PdusIterItem> {
437	let (_, pdu) = &item;
438	filter.matches(pdu).then_some(item)
439}
440
441/// MSC4115: stamp `unsigned.membership` on a served PDU with the requesting
442/// user's membership at the time of the event. The MSC permits omitting the
443/// property when calculating it is expensive, so the project restricts it to
444/// encrypted rooms where membership-vs-event ordering matters for key share.
445#[inline]
446pub(crate) async fn annotate_membership(
447	services: &Services,
448	pdu: &mut PduEvent,
449	user_id: &UserId,
450	encrypted: bool,
451) {
452	if !encrypted {
453		return;
454	}
455
456	let membership = services
457		.state_accessor
458		.user_membership_at_pdu(user_id, pdu)
459		.await;
460
461	pdu.add_membership(&membership).log_err().ok();
462}
463
464/// `annotate_membership` consume-and-return adapter for stream chains.
465#[inline]
466pub async fn with_membership(
467	services: &Services,
468	mut pdu: PduEvent,
469	user_id: &UserId,
470	encrypted: bool,
471) -> PduEvent {
472	annotate_membership(services, &mut pdu, user_id, encrypted).await;
473	pdu
474}
475
476/// `with_membership` adapter for timeline-iterator items.
477#[inline]
478pub(crate) async fn add_membership_unsigned(
479	services: &Services,
480	(count, pdu): PdusIterItem,
481	user_id: &UserId,
482	encrypted: bool,
483) -> PdusIterItem {
484	(count, with_membership(services, pdu, user_id, encrypted).await)
485}
486
487#[cfg_attr(debug_assertions, tuwunel_core::ctor(unsafe))]
488fn _is_sorted() {
489	debug_assert!(
490		IGNORED_MESSAGE_TYPES.is_sorted(),
491		"IGNORED_MESSAGE_TYPES must be sorted by the developer"
492	);
493}