Skip to main content

tuwunel_service/rooms/timeline/
append.rs

1//! Appends authenticated events to room timelines and applies their side effects.
2//!
3//! Incoming events first receive a state snapshot, while accepted events are
4//! assigned a normal stream count and committed to the timeline. Subsequent
5//! cache, indexing, notification, and membership effects are coordinated here.
6
7use std::{collections::BTreeMap, sync::Arc};
8
9use ruma::{
10	CanonicalJsonObject, CanonicalJsonValue, EventId, UserId,
11	events::{
12		TimelineEventType,
13		receipt::ReceiptThread,
14		relation::RelationType,
15		room::{
16			encrypted::Relation,
17			member::{MembershipState, RoomMemberEventContent},
18		},
19	},
20};
21use tuwunel_core::{
22	Result, debug_warn, err, error, implement,
23	matrix::{
24		event::Event,
25		pdu::{PduCount, PduEvent, PduId, RawPduId},
26		room_version,
27	},
28	smallvec::SmallVec,
29	utils::result::{LogErr, NotFound},
30};
31use tuwunel_database::Json;
32
33use super::{ExtractBody, ExtractRelatesTo, ExtractRelatesToEventId, RoomMutexGuard, bias_count};
34use crate::{
35	admin::CommandInput,
36	rooms::{
37		read_receipt::PrivateRead, short::ShortRoomId, state_accessor::plain_text_topic,
38		state_cache::MembershipUpdate, state_compressor::CompressedState,
39	},
40};
41
42type Band<'a> = SmallVec<[&'a EventId; 1]>;
43
44/// Appends an incoming event with its locally resolved state snapshot.
45///
46/// The snapshot is recorded even when the event is soft-failed. A soft-failed
47/// event is not inserted into the accepted timeline, but its predecessors are
48/// marked referenced. Only a nonempty replacement extremity band is stored; an
49/// empty calculation preserves the prior band.
50#[implement(super::Service)]
51#[tracing::instrument(
52	name = "append_incoming",
53	level = "debug",
54	skip_all,
55	ret(Debug)
56)]
57pub(crate) async fn append_incoming_pdu<'a, Leafs>(
58	&'a self,
59	pdu: &'a PduEvent,
60	pdu_json: CanonicalJsonObject,
61	new_room_leafs: Leafs,
62	state_ids_compressed: Arc<CompressedState>,
63	soft_fail: bool,
64	state_lock: &'a RoomMutexGuard,
65) -> Result<Option<RawPduId>>
66where
67	Leafs: Iterator<Item = &'a EventId> + Send + 'a,
68{
69	// We append to state before appending the pdu, so we don't have a moment in
70	// time with the pdu without it's state. This is okay because append_pdu can't
71	// fail.
72	self.services
73		.state
74		.set_event_state(&pdu.event_id, &pdu.room_id, state_ids_compressed)
75		.await?;
76
77	if soft_fail {
78		self.services
79			.pdu_metadata
80			.mark_as_referenced(&pdu.room_id, pdu.prev_events.iter().map(AsRef::as_ref));
81
82		// Keep the previous band rather than let a soft-failed event empty it; a
83		// later accepted event self-chains and heals it.
84		if let Some(new_room_leafs) = nonempty_band(new_room_leafs) {
85			self.services
86				.state
87				.set_forward_extremities(&pdu.room_id, new_room_leafs.into_iter(), state_lock)
88				.await;
89		}
90
91		return Ok(None);
92	}
93
94	let pdu_id = self
95		.append_pdu(pdu, pdu_json, new_room_leafs, state_lock)
96		.await?;
97
98	Ok(Some(pdu_id))
99}
100
101fn nonempty_band<'a, Leafs>(leafs: Leafs) -> Option<Band<'a>>
102where
103	Leafs: Iterator<Item = &'a EventId>,
104{
105	let leafs: Band<'_> = leafs.collect();
106
107	(!leafs.is_empty()).then_some(leafs)
108}
109
110/// Persists an authenticated event and applies its timeline side effects.
111///
112/// This method performs no authentication. The accepted row, event mapping,
113/// outlier removal, and timestamp index are committed together, but later
114/// cache, indexing, notification, and membership work is not part of that
115/// transaction, so an error can be returned after the event is stored.
116#[implement(super::Service)]
117#[tracing::instrument(name = "append", level = "debug", skip_all, ret(Debug))]
118pub async fn append_pdu<'a, Leafs>(
119	&'a self,
120	pdu: &'a PduEvent,
121	mut pdu_json: CanonicalJsonObject,
122	leafs: Leafs,
123	state_lock: &'a RoomMutexGuard,
124) -> Result<RawPduId>
125where
126	Leafs: Iterator<Item = &'a EventId> + Send + 'a,
127{
128	// Coalesce database writes for the remainder of this scope.
129	let _cork = self.db.db.cork_and_flush();
130
131	let shortroomid = self
132		.services
133		.short
134		.get_shortroomid(pdu.room_id())
135		.await
136		.map_err(|_| err!(Database("Room does not exist")))?;
137
138	// Make unsigned fields correct. This is not properly documented in the spec,
139	// but state events need to have previous content in the unsigned field, so
140	// clients can easily interpret things like membership changes
141	if let Some(state_key) = pdu.state_key() {
142		if let CanonicalJsonValue::Object(unsigned) = pdu_json
143			.entry("unsigned".into())
144			.or_insert_with(|| CanonicalJsonValue::Object(BTreeMap::default()))
145		{
146			if let Some(prev_state) = self.prev_state(pdu, state_key).await {
147				unsigned.extend(prev_state_unsigned(&prev_state)?);
148			}
149		} else {
150			error!("Invalid unsigned type in pdu.");
151		}
152	}
153
154	// We must keep track of all events that have been referenced.
155	self.services
156		.pdu_metadata
157		.mark_as_referenced(pdu.room_id(), pdu.prev_events().map(AsRef::as_ref));
158
159	self.services
160		.state
161		.set_forward_extremities(pdu.room_id(), leafs, state_lock)
162		.await;
163
164	let insert_lock = self.mutex_insert.lock(pdu.room_id()).await;
165	let next_count = self.services.globals.next_count();
166
167	// Mark as read first so the sending client doesn't get a notification even if
168	// appending fails. Route through the dispatcher so per-thread counts are
169	// also cleared; the sender's own send subsumes any thread receipt.
170	self.services
171		.read_receipt
172		.private_read_set(PrivateRead {
173			room_id: pdu.room_id(),
174			user_id: pdu.sender(),
175			count: *next_count,
176			ts: pdu.origin_server_ts(),
177			thread: &ReceiptThread::Unthreaded,
178			announce: false,
179		})
180		.await;
181
182	self.services
183		.pusher
184		.reset_notification_counts_for_thread(
185			pdu.sender(),
186			pdu.room_id(),
187			None,
188			&ReceiptThread::Unthreaded,
189		)
190		.await;
191
192	let count = PduCount::Normal(*next_count);
193	let pdu_id: RawPduId = PduId { shortroomid, count }.into();
194
195	// Insert pdu
196	self.append_pdu_json(&pdu_id, pdu, &pdu_json);
197
198	drop(insert_lock);
199
200	// Only local senders can own pushers.
201	if self.services.globals.user_is_local(pdu.sender()) {
202		self.services
203			.sending
204			.refresh_push_badge(pdu.sender())
205			.await
206			.log_err()
207			.ok();
208	}
209
210	self.services
211		.pusher
212		.append_pdu(pdu_id, pdu)
213		.await
214		.log_err()
215		.ok();
216
217	self.append_pdu_effects(pdu_id, pdu, shortroomid, count, state_lock)
218		.await?;
219
220	drop(next_count);
221
222	self.services
223		.appservice
224		.append_pdu(pdu_id, pdu)
225		.await
226		.log_err()
227		.ok();
228
229	Ok(pdu_id)
230}
231
232#[implement(super::Service)]
233async fn prev_state(&self, pdu: &PduEvent, state_key: &str) -> Option<PduEvent> {
234	let event_id = pdu.event_id();
235	let shortstatehash = self
236		.services
237		.state
238		.pdu_shortstatehash(event_id)
239		.await
240		.optional()
241		.inspect_err(|error| debug_warn!(%event_id, %error, "State snapshot read failed."))
242		.ok()
243		.flatten()?;
244
245	let event_type = pdu.kind().to_cow_str().into();
246
247	self.services
248		.state_accessor
249		.state_get(shortstatehash, &event_type, state_key)
250		.await
251		.optional()
252		.inspect_err(|error| debug_warn!(%event_id, %error, "Replaced state read failed."))
253		.ok()
254		.flatten()
255}
256
257fn prev_state_unsigned(prev_state: &PduEvent) -> Result<CanonicalJsonObject> {
258	let prev_content = prev_state
259		.get_content::<CanonicalJsonObject>()
260		.map_err(|e| {
261			err!(Database(error!("Failed to convert prev_state to canonical JSON: {e}")))
262		})?;
263
264	let unsigned = [
265		("prev_content".into(), CanonicalJsonValue::Object(prev_content)),
266		(
267			"prev_sender".into(),
268			CanonicalJsonValue::String(prev_state.sender().to_string()),
269		),
270		(
271			"replaces_state".into(),
272			CanonicalJsonValue::String(prev_state.event_id().to_string()),
273		),
274	]
275	.into();
276
277	Ok(unsigned)
278}
279
280#[implement(super::Service)]
281async fn append_pdu_effects(
282	&self,
283	pdu_id: RawPduId,
284	pdu: &PduEvent,
285	shortroomid: ShortRoomId,
286	count: PduCount,
287	state_lock: &RoomMutexGuard,
288) -> Result {
289	match *pdu.kind() {
290		| TimelineEventType::RoomRedaction => {
291			let room_version = self
292				.services
293				.state
294				.get_room_version(pdu.room_id())
295				.await?;
296
297			let room_rules = room_version::rules(&room_version)?;
298
299			let redacts_id = pdu.redacts_id(&room_rules);
300
301			if let Some(redacts_id) = &redacts_id
302				&& self
303					.services
304					.state_accessor
305					.user_can_redact(redacts_id, pdu.sender(), pdu.room_id(), false)
306					.await?
307			{
308				self.redact_pdu(redacts_id, pdu, shortroomid, state_lock)
309					.await?;
310			}
311		},
312		| TimelineEventType::RoomMember => self.append_member_effects(pdu, count).await?,
313		| TimelineEventType::RoomMessage =>
314			self.append_message_effects(&pdu_id, pdu, shortroomid)
315				.await?,
316		| TimelineEventType::RoomTopic =>
317			if let Some(topic) = pdu.get_content().ok().and_then(plain_text_topic) {
318				self.services
319					.search
320					.index_pdu(shortroomid, &pdu_id, &topic);
321			},
322		| _ => {},
323	}
324
325	// The cached hierarchy summary projects room state; evict on any state change.
326	if pdu.state_key().is_some() {
327		self.services.spaces.cache_evict(pdu.room_id());
328	}
329
330	if let Ok(content) = pdu.get_content::<ExtractRelatesToEventId>()
331		&& let Ok(related_pducount) = self
332			.get_pdu_count(&content.relates_to.event_id)
333			.await
334	{
335		self.services
336			.pdu_metadata
337			.add_relation(count, related_pducount);
338	}
339
340	if let Ok(content) = pdu.get_content::<ExtractRelatesTo>() {
341		match content.relates_to {
342			| Relation::Reply(ruma::events::relation::Reply { in_reply_to }) => {
343				// We need to do it again here, because replies don't have
344				// event_id as a top level field
345				if let Ok(related_pducount) = self.get_pdu_count(&in_reply_to.event_id).await {
346					self.services
347						.pdu_metadata
348						.add_relation(count, related_pducount);
349				}
350			},
351			| Relation::Thread(thread) => {
352				self.services
353					.threads
354					.add_to_thread(&thread.event_id, pdu_id, pdu)
355					.await?;
356			},
357			| Relation::Replacement(replacement) => {
358				self.services
359					.pdu_metadata
360					.add_typed_relation(
361						shortroomid,
362						count,
363						&replacement.event_id,
364						pdu,
365						RelationType::Replacement,
366					)
367					.await;
368			},
369			| Relation::Reference(reference) => {
370				self.services
371					.pdu_metadata
372					.add_typed_relation(
373						shortroomid,
374						count,
375						&reference.event_id,
376						pdu,
377						RelationType::Reference,
378					)
379					.await;
380			},
381			| _ => {}, // TODO: Aggregate other types
382		}
383	}
384
385	Ok(())
386}
387
388/// Record the membership transition an `m.room.member` event carries.
389///
390/// The cache is written here rather than off the resolved state so that a
391/// user who is invited or knocked and leaves immediately still leaves the
392/// earlier event on record for auth.
393#[implement(super::Service)]
394async fn append_member_effects(&self, pdu: &PduEvent, count: PduCount) -> Result {
395	let Some(state_key) = pdu.state_key() else {
396		return Ok(());
397	};
398
399	let user_id = UserId::parse(state_key).expect("This state_key was previously validated");
400	let content: RoomMemberEventContent = pdu.get_content()?;
401	let is_invite = content.membership == MembershipState::Invite;
402	let is_direct = content.is_direct;
403
404	let stripped_state = match content.membership {
405		| MembershipState::Invite | MembershipState::Knock => self
406			.services
407			.state
408			.summary_stripped(pdu)
409			.await
410			.into(),
411		| _ => None,
412	};
413
414	self.services
415		.state_cache
416		.update_membership(MembershipUpdate {
417			room_id: pdu.room_id(),
418			user_id: &user_id,
419			membership_event: content,
420			sender: pdu.sender(),
421			last_state: stripped_state,
422			invite_via: None,
423			update_joined_count: true,
424			count,
425		})
426		.await?;
427
428	if is_invite {
429		self.services
430			.membership
431			.auto_accept(pdu.room_id(), &user_id, pdu.sender(), is_direct);
432	}
433
434	Ok(())
435}
436
437/// Index an `m.room.message` event's body, and queue it when it is an admin
438/// command.
439///
440/// The queued command carries the event's sender, so a handler can tell who
441/// issued it, and the event's id, which its response replies to.
442#[implement(super::Service)]
443async fn append_message_effects(
444	&self,
445	pdu_id: &RawPduId,
446	pdu: &PduEvent,
447	shortroomid: ShortRoomId,
448) -> Result {
449	let content: ExtractBody = pdu.get_content()?;
450	let Some(body) = content.body else {
451		return Ok(());
452	};
453
454	self.services
455		.search
456		.index_pdu(shortroomid, pdu_id, &body);
457
458	if self
459		.services
460		.admin
461		.is_admin_command(pdu, &body)
462		.await
463	{
464		self.services
465			.admin
466			.command(CommandInput {
467				command: body,
468				reply_id: Some(pdu.event_id().into()),
469				sender: Some(pdu.sender().into()),
470			})
471			.await?;
472	}
473
474	Ok(())
475}
476
477#[implement(super::Service)]
478fn append_pdu_json(&self, pdu_id: &RawPduId, pdu: &PduEvent, json: &CanonicalJsonObject) {
479	debug_assert!(matches!(pdu_id.pdu_count(), PduCount::Normal(_)), "PduCount not Normal");
480
481	let mut txn = self.db.db.txn();
482
483	txn.raw_put(&self.db.pduid_pdu, pdu_id, Json(json));
484	txn.insert_raw(&self.db.eventid_pduid, pdu.event_id.as_bytes(), pdu_id);
485	txn.del_raw(&self.db.eventid_outlierpdu, pdu.event_id.as_bytes());
486
487	let count_key = bias_count(pdu_id.count());
488	let ts = u64::from(pdu.origin_server_ts);
489	let key = (pdu.room_id(), ts, count_key);
490	txn.put_raw(&self.db.roomid_tscount_pducount, key, pdu_id.count());
491
492	txn.execute();
493}
494
495#[cfg(test)]
496mod tests {
497	use std::iter::empty;
498
499	use ruma::event_id;
500
501	use super::*;
502
503	#[test]
504	fn empty_band_is_skipped() {
505		assert!(nonempty_band(empty::<&EventId>()).is_none());
506	}
507
508	#[test]
509	fn nonempty_band_preserves_all_leaves() {
510		let leaves = [event_id!("$a:test.local"), event_id!("$b:test.local")];
511
512		let kept: Vec<&EventId> = nonempty_band(leaves.iter().copied())
513			.expect("non-empty band retained")
514			.into_iter()
515			.collect();
516
517		assert_eq!(kept, leaves);
518	}
519}