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