Skip to main content

tuwunel_api/client/
message.rs

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