Skip to main content

tuwunel_api_client/
context.rs

1use axum::extract::State;
2use futures::{
3	FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt,
4	future::{OptionFuture, join, join3, try_join},
5};
6use ruma::{
7	DeviceId, EventId, OwnedEventId, RoomId, UInt, UserId,
8	api::client::{context::get_context, filter::RoomEventFilter},
9	events::{AnyStateEvent, StateEventType},
10	serde::Raw,
11};
12use tuwunel_core::{
13	Err, Result, at, debug_warn, err, ref_at,
14	utils::{
15		BoolExt, IterStream,
16		future::TryExtExt,
17		math::usize_from_ruma_bounded,
18		stream::{BroadbandExt, ReadyExt, TryIgnore, WidebandExt},
19	},
20};
21use tuwunel_matrix::{
22	Event,
23	pdu::{PduEvent, RawPduId},
24};
25use tuwunel_service::{
26	Services,
27	rooms::{
28		lazy_loading,
29		lazy_loading::{Options, Witness},
30		short::{ShortRoomId, ShortStateHash, ShortStateKey},
31		timeline::PdusIterItem,
32	},
33};
34
35use crate::{
36	Ruma, is_ignored_pdu,
37	message::{
38		add_membership_unsigned, event_filter, event_filters, ignored_filter,
39		lazy_loading_witness, related_by_filter, with_membership,
40	},
41};
42
43const LIMIT_MAX: usize = 100;
44const LIMIT_DEFAULT: usize = 10;
45
46/// # `GET /_matrix/client/r0/rooms/{roomId}/context/{eventId}`
47///
48/// Allows loading room history around an event.
49///
50/// - Only works if the user is joined (TODO: always allow, but only show events
51///   if the user was joined, depending on history_visibility)
52pub(crate) async fn get_context_route(
53	State(services): State<crate::State>,
54	body: Ruma<get_context::v3::Request>,
55) -> Result<get_context::v3::Response> {
56	event_context(&services, ContextArgs {
57		room_id: &body.room_id,
58		event_id: &body.event_id,
59		sender_user: body.sender_user(),
60		sender_device: body.sender_device.as_deref(),
61		filter: &body.filter,
62		limit: Some(body.limit),
63		bypass_visibility: false,
64	})
65	.await
66}
67
68/// Shared inputs for [`event_context`], the core behind both the client-server
69/// `/context` route and the admin room-context endpoint.
70pub struct ContextArgs<'a> {
71	/// Room containing the requested event.
72	pub room_id: &'a RoomId,
73
74	/// Event at the center of the timeline window.
75	pub event_id: &'a EventId,
76
77	/// User requesting the event context.
78	pub sender_user: &'a UserId,
79
80	/// Requesting device used for lazy-loading membership state.
81	pub sender_device: Option<&'a DeviceId>,
82
83	/// Filter for timeline events and membership state.
84	pub filter: &'a RoomEventFilter,
85
86	/// Requested maximum number of surrounding timeline events.
87	pub limit: Option<UInt>,
88
89	/// Skip the visibility and ignore checks on the base event and both halves,
90	/// and the room-state check on an anchor without state, for admin callers.
91	pub bypass_visibility: bool,
92}
93
94/// Loads the timeline window around an event with its state and aggregations,
95/// applying (unless `bypass_visibility`) the per-user visibility and ignore
96/// checks.
97///
98/// Powers the client-server `/context` route and its admin bypass twin.
99pub async fn event_context(
100	services: &Services,
101	args: ContextArgs<'_>,
102) -> Result<get_context::v3::Response> {
103	let ContextArgs {
104		room_id,
105		event_id,
106		sender_user,
107		sender_device,
108		filter,
109		limit,
110		bypass_visibility,
111	} = args;
112
113	if !services.metadata.exists(room_id).await {
114		return Err!(Request(Forbidden("Room does not exist to this server")));
115	}
116
117	let limit = limit
118		.map_or(LIMIT_DEFAULT, |limit| usize_from_ruma_bounded(limit, LIMIT_DEFAULT, LIMIT_MAX));
119
120	let (base_id, base_pdu) =
121		resolve_base_event(services, room_id, event_id, sender_user, bypass_visibility).await?;
122
123	let base_count = base_id.pdu_count();
124
125	let encrypted = services
126		.state_accessor
127		.is_encrypted_room(room_id)
128		.await;
129
130	let shortroomid = services.short.get_shortroomid(room_id).await?;
131
132	let base_event = async {
133		let item = if bypass_visibility {
134			(base_count, base_pdu)
135		} else {
136			ignored_filter(services, (base_count, base_pdu), sender_user).await?
137		};
138
139		Some(add_membership_unsigned(services, item, sender_user, encrypted).await)
140	};
141
142	let half = TimelineHalf {
143		services,
144		filter,
145		shortroomid,
146		sender_user,
147		encrypted,
148		bypass_visibility,
149	};
150
151	let events_before = collect_timeline_half(
152		half,
153		services
154			.timeline
155			.pdus_rev(Some(sender_user), room_id, Some(base_count)),
156		limit / 2,
157	);
158
159	let events_after = collect_timeline_half(
160		half,
161		services
162			.timeline
163			.pdus(Some(sender_user), room_id, Some(base_count)),
164		limit.div_ceil(2),
165	);
166
167	let (base_event, events_before, events_after): (_, Vec<_>, Vec<_>) =
168		join3(base_event, events_before, events_after)
169			.boxed() // size firewall
170			.await;
171
172	let lazy_loading_context = lazy_loading::Context {
173		user_id: sender_user,
174		device_id: sender_device,
175		room_id,
176		token: Some(base_count.into_unsigned()),
177		options: Some(&filter.lazy_load_options),
178		mode: lazy_loading::Mode::Update,
179	};
180
181	let lazy_loading_witnessed = filter
182		.lazy_load_options
183		.is_enabled()
184		.then_async(|| {
185			let witnessed = base_event
186				.iter()
187				.chain(events_before.iter())
188				.chain(events_after.iter());
189
190			lazy_loading_witness(services, &lazy_loading_context, witnessed)
191		});
192
193	let state_at = events_after
194		.last()
195		.map(ref_at!(1))
196		.map_or(event_id, Event::event_id);
197
198	let state_ids = load_state_ids(services, room_id, state_at, sender_user, bypass_visibility);
199
200	let (lazy_loading_witnessed, state_ids) = join(lazy_loading_witnessed, state_ids).await;
201
202	let state = build_state_response(
203		services,
204		state_ids?,
205		lazy_loading_witnessed.unwrap_or_default(),
206		filter,
207		sender_user,
208		encrypted,
209	)
210	.await;
211
212	let event = OptionFuture::from(base_event.map(at!(1)).map(|pdu| {
213		services
214			.pdu_metadata
215			.bundle_aggregations(sender_user, pdu)
216	}))
217	.await
218	.map(Event::into_format);
219
220	Ok(get_context::v3::Response {
221		event,
222
223		start: events_before
224			.last()
225			.map(at!(0))
226			.or(Some(base_count))
227			.as_ref()
228			.map(ToString::to_string),
229
230		// `end` is one past the base so a backward page from it still yields the base;
231		// `start` stays at `base_count` (a bare count can't suit both directions).
232		end: events_after
233			.last()
234			.map(at!(0))
235			.or_else(|| Some(base_count.saturating_add(1)))
236			.as_ref()
237			.map(ToString::to_string),
238
239		events_before: events_before
240			.into_iter()
241			.map(at!(1))
242			.map(Event::into_format)
243			.collect(),
244
245		events_after: events_after
246			.into_iter()
247			.map(at!(1))
248			.map(Event::into_format)
249			.collect(),
250
251		state,
252	})
253}
254
255async fn resolve_base_event(
256	services: &Services,
257	room_id: &RoomId,
258	event_id: &EventId,
259	sender_user: &UserId,
260	bypass_visibility: bool,
261) -> Result<(RawPduId, PduEvent)> {
262	let lookup = || {
263		let base_id = services
264			.timeline
265			.get_pdu_id(event_id)
266			.map_err(|_| err!(Request(NotFound("Event not found."))));
267
268		let base_pdu = services
269			.timeline
270			.get_pdu(event_id)
271			.map_err(|_| err!(Request(NotFound("Base event not found."))));
272
273		try_join(base_id, base_pdu)
274	};
275
276	let resolve_remote = services
277		.config
278		.federation
279		.history
280		.fetch_unreceived_contexts
281		&& services.config.federation.enable;
282
283	let (base_id, base_pdu) = match lookup().await {
284		| Ok(found) => found,
285		| Err(e) if !resolve_remote => return Err(e),
286		| Err(_) => {
287			services
288				.timeline
289				.fetch_remote_event(room_id, event_id)
290				.await
291				.ok();
292
293			lookup().await?
294		},
295	};
296
297	if base_pdu.room_id != *room_id || base_pdu.event_id != *event_id {
298		return Err!(Request(NotFound("Base event not found.")));
299	}
300
301	if !bypass_visibility
302		&& !services
303			.state_accessor
304			.user_can_see_event(sender_user, &base_pdu)
305			.await
306	{
307		debug_warn!(
308			req_evt = ?event_id, ?base_id, ?room_id, %sender_user,
309			"Event requested but the requester is not allowed to see it."
310		);
311
312		return Err!(Request(NotFound("Event not found.")));
313	}
314
315	if !bypass_visibility && is_ignored_pdu(services, &base_pdu, sender_user).await {
316		return Err!(HttpJson(NOT_FOUND, {
317			"errcode": "M_SENDER_IGNORED",
318			"error": "You have ignored the user that sent this event",
319			"sender": base_pdu.sender().as_str(),
320		}));
321	}
322
323	Ok((base_id, base_pdu.without_private_unless_sender(Some(sender_user))))
324}
325
326/// Shared inputs for the two [`collect_timeline_half`] calls assembling the
327/// before and after windows; only the stream and take count differ per call.
328#[derive(Clone, Copy)]
329struct TimelineHalf<'a> {
330	services: &'a Services,
331	filter: &'a RoomEventFilter,
332	shortroomid: ShortRoomId,
333	sender_user: &'a UserId,
334	encrypted: bool,
335	bypass_visibility: bool,
336}
337
338async fn collect_timeline_half<'a, S>(
339	half: TimelineHalf<'a>,
340	pdus: S,
341	take: usize,
342) -> Vec<PdusIterItem>
343where
344	S: Stream<Item = Result<PdusIterItem>> + Send + 'a,
345{
346	let TimelineHalf {
347		services,
348		filter,
349		shortroomid,
350		sender_user,
351		encrypted,
352		bypass_visibility,
353	} = half;
354
355	pdus.ignore_err()
356		.ready_filter_map(|item| event_filter(item, filter))
357		.wide_filter_map(|item| related_by_filter(services, shortroomid, filter, item))
358		.wide_filter_map(|item| event_filters(services, sender_user, item, bypass_visibility))
359		.take(take)
360		.wide_then(|item| add_membership_unsigned(services, item, sender_user, encrypted))
361		.wide_then(async |(count, pdu)| {
362			let pdu = services
363				.pdu_metadata
364				.bundle_aggregations(sender_user, pdu)
365				.await;
366
367			(count, pdu)
368		})
369		.collect()
370		.await
371}
372
373/// Loads the room state at the event that anchors the response's state.
374///
375/// An anchor without recorded state, such as a room's create event or a
376/// backfilled event, falls back to the state `fallback_shortstatehash` selects
377/// for the requester, which may be none.
378async fn load_state_ids(
379	services: &Services,
380	room_id: &RoomId,
381	state_at: &EventId,
382	sender_user: &UserId,
383	bypass_visibility: bool,
384) -> Result<Vec<(ShortStateKey, OwnedEventId)>> {
385	let state_full_ids = |shortstatehash| {
386		services
387			.state_accessor
388			.state_full_ids(shortstatehash)
389	};
390
391	services
392		.state
393		.pdu_shortstatehash(state_at)
394		.map_ok(Some)
395		.or_else(|_| fallback_shortstatehash(services, room_id, sender_user, bypass_visibility))
396		.map_ok(|shortstatehash| {
397			shortstatehash
398				.into_iter()
399				.stream()
400				.flat_map(state_full_ids)
401				.map(Ok)
402		})
403		.map_err(|e| err!(Database("State not found: {e}")))
404		.try_flatten_stream()
405		.try_collect()
406		.boxed() // Send generality
407		.await
408}
409
410/// Selects the state a requester reads in place of an event's missing state.
411///
412/// Admitted requesters reach the selection, which checks access again; departed
413/// members read their departure snapshot, admins included. Only an admin may
414/// read current state when the selection admits none.
415async fn fallback_shortstatehash(
416	services: &Services,
417	room_id: &RoomId,
418	sender_user: &UserId,
419	bypass_visibility: bool,
420) -> Result<Option<ShortStateHash>> {
421	let admitted = bypass_visibility
422		|| services
423			.state_accessor
424			.user_can_see_state_events(sender_user, room_id)
425			.await;
426
427	match admitted
428		.then_async(|| {
429			services
430				.state_accessor
431				.user_visible_shortstatehash(sender_user, room_id, None)
432		})
433		.await
434		.transpose()?
435		.flatten()
436	{
437		| None if bypass_visibility =>
438			services
439				.state
440				.get_room_shortstatehash(room_id)
441				.map_ok(Some)
442				.await,
443		| shortstatehash => Ok(shortstatehash),
444	}
445}
446
447async fn build_state_response(
448	services: &Services,
449	state_ids: Vec<(ShortStateKey, OwnedEventId)>,
450	lazy_loading_witnessed: Witness,
451	filter: &RoomEventFilter,
452	sender_user: &UserId,
453	encrypted: bool,
454) -> Vec<Raw<AnyStateEvent>> {
455	let shortstatekeys = state_ids.iter().map(at!(0)).stream();
456	let shorteventids = state_ids.iter().map(ref_at!(1)).stream();
457
458	services
459		.short
460		.multi_get_statekey_from_short(shortstatekeys)
461		.zip(shorteventids)
462		.ready_filter_map(|item| Some((item.0.ok()?, item.1)))
463		.ready_filter_map(|((event_type, state_key), event_id)| {
464			if filter.lazy_load_options.is_enabled()
465				&& event_type == StateEventType::RoomMember
466				&& state_key
467					.as_str()
468					.try_into()
469					.is_ok_and(|user_id: &UserId| !lazy_loading_witnessed.contains(user_id))
470			{
471				return None;
472			}
473
474			Some(event_id)
475		})
476		.broad_filter_map(|event_id: &OwnedEventId| {
477			services.timeline.get_pdu(event_id.as_ref()).ok()
478		})
479		.map(|pdu| pdu.without_private_unless_sender(Some(sender_user)))
480		.broad_then(|pdu| with_membership(services, pdu, sender_user, encrypted))
481		.map(Event::into_format)
482		.collect()
483		.await
484}