tuwunel_api/client/read_marker/
receipt.rs1use 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
18pub(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 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 if body.thread.as_str() == Some("") {
39 return Err!(Request(InvalidParam("thread_id must be a non-empty string")));
40 }
41
42 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 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}