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