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