Skip to main content

tuwunel_api/client/read_marker/
receipt.rs

1use std::collections::BTreeMap;
2
3use axum::extract::State;
4use ruma::{
5	MilliSecondsSinceUnixEpoch,
6	api::client::receipt::create_receipt,
7	events::{
8		RoomAccountDataEventType,
9		fully_read::{FullyReadEvent, FullyReadEventContent},
10		receipt::{Receipt, ReceiptEvent, ReceiptEventContent, ReceiptThread, ReceiptType},
11	},
12	presence::PresenceState,
13};
14use tuwunel_core::{Err, PduCount, Result, err};
15
16use crate::{ClientIp, Ruma};
17
18/// # `POST /_matrix/client/r0/rooms/{roomId}/receipt/{receiptType}/{eventId}`
19///
20/// Sets private read marker and public read receipt EDU.
21pub(crate) async fn create_receipt_route(
22	State(services): State<crate::State>,
23	ClientIp(client): ClientIp,
24	body: Ruma<create_receipt::v3::Request>,
25) -> Result<create_receipt::v3::Response> {
26	let sender_user = body.sender_user();
27
28	// MSC3771: thread_id MUST NOT be provided with `m.fully_read`.
29	if matches!(&body.receipt_type, create_receipt::v3::ReceiptType::FullyRead)
30		&& !matches!(body.thread, ReceiptThread::Unthreaded)
31	{
32		return Err!(Request(InvalidParam(
33			"thread_id must not be set for m.fully_read receipts"
34		)));
35	}
36
37	// MSC3771: a present thread_id must be a non-empty string.
38	if body.thread.as_str() == Some("") {
39		return Err!(Request(InvalidParam("thread_id must be a non-empty string")));
40	}
41
42	// MSC3771: thread_id is either `"main"` or a thread root event id (which
43	// starts with `$`).
44	if !matches!(
45		&body.thread,
46		ReceiptThread::Unthreaded | ReceiptThread::Main | ReceiptThread::Thread(_)
47	) {
48		return Err!(Request(InvalidParam(
49			"thread_id must be either \"main\" or a thread root event id"
50		)));
51	}
52
53	// MSC3771: event_id must belong to the thread the receipt targets.
54	if matches!(&body.thread, ReceiptThread::Main | ReceiptThread::Thread(_)) {
55		let resolved = services
56			.threads
57			.get_thread_id_for_event(&body.event_id)
58			.await;
59
60		let in_thread = match (&body.thread, resolved.as_deref()) {
61			| (ReceiptThread::Main, None) => true,
62			| (ReceiptThread::Thread(root), Some(parent)) => &**root == parent,
63			| (ReceiptThread::Thread(root), None) => **root == *body.event_id,
64			| _ => false,
65		};
66
67		if !in_thread {
68			return Err!(Request(InvalidParam("event_id is not related to the given thread_id")));
69		}
70	}
71
72	if matches!(
73		&body.receipt_type,
74		create_receipt::v3::ReceiptType::Read | create_receipt::v3::ReceiptType::ReadPrivate
75	) {
76		services
77			.pusher
78			.reset_notification_counts_for_thread(sender_user, &body.room_id, &body.thread)
79			.await;
80	}
81
82	match body.receipt_type {
83		| create_receipt::v3::ReceiptType::FullyRead => {
84			let fully_read_event = FullyReadEvent {
85				content: FullyReadEventContent { event_id: body.event_id.clone() },
86			};
87			services
88				.account_data
89				.update(
90					Some(&body.room_id),
91					sender_user,
92					RoomAccountDataEventType::FullyRead,
93					&serde_json::to_value(fully_read_event)?,
94				)
95				.await?;
96		},
97		| create_receipt::v3::ReceiptType::Read => {
98			let receipt_content = BTreeMap::from_iter([(
99				body.event_id.clone(),
100				BTreeMap::from_iter([(
101					ReceiptType::Read,
102					BTreeMap::from_iter([(sender_user.to_owned(), Receipt {
103						ts: Some(MilliSecondsSinceUnixEpoch::now()),
104						thread: body.thread.clone(),
105					})]),
106				)]),
107			)]);
108
109			services
110				.read_receipt
111				.readreceipt_update(sender_user, &body.room_id, &ReceiptEvent {
112					content: ReceiptEventContent(receipt_content),
113					room_id: body.room_id.clone(),
114				})
115				.await;
116
117			services
118				.presence
119				.maybe_ping_presence(
120					sender_user,
121					body.sender_device.as_deref(),
122					Some(client),
123					&PresenceState::Online,
124				)
125				.await
126				.ok();
127		},
128		| create_receipt::v3::ReceiptType::ReadPrivate => {
129			let count = services
130				.timeline
131				.get_pdu_count(&body.event_id)
132				.await
133				.map_err(|_| err!(Request(NotFound("Event not found."))))?;
134
135			let PduCount::Normal(count) = count else {
136				return Err!(Request(InvalidParam(
137					"Event is a backfilled PDU and cannot be marked as read."
138				)));
139			};
140
141			services
142				.read_receipt
143				.private_read_set(&body.room_id, sender_user, count, &body.thread)
144				.await;
145		},
146		| _ => {
147			return Err!(Request(InvalidParam(warn!(
148				"Received unknown read receipt type: {}",
149				&body.receipt_type
150			))));
151		},
152	}
153
154	Ok(create_receipt::v3::Response {})
155}