Skip to main content

tuwunel_service/rooms/timeline/
append.rs

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