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