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::{
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
19pub(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 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 if body.thread.as_str() == Some("") {
40 return Err!(Request(InvalidParam("thread_id must be a non-empty string")));
41 }
42
43 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 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}