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::{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
17pub(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 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 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 if body.thread.as_str() == Some("") {
44 return Err!(Request(InvalidParam("thread_id must be a non-empty string")));
45 }
46
47 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 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}