tuwunel_service/sending/sender/select/
receipts.rs1use std::{
2 collections::BTreeMap,
3 sync::atomic::{AtomicU64, AtomicUsize, Ordering},
4};
5
6use futures::{Stream, StreamExt, pin_mut};
7use ruma::{
8 OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId,
9 api::federation::transactions::edu::{Edu, ReceiptContent, ReceiptData, ReceiptMap},
10 events::{
11 AnySyncEphemeralRoomEvent,
12 receipt::{Receipt, ReceiptEventContent, ReceiptType},
13 },
14 serde::Raw,
15};
16use tuwunel_core::{
17 error, implement,
18 smallvec::SmallVec,
19 utils::{BoolExt, ReadyExt, stream::BroadbandExt},
20};
21
22use super::{Selected, edu_buf};
23use crate::sending::{EduBuf, Service};
24
25#[cfg(test)]
26mod tests;
27
28type Receipts = BTreeMap<OwnedRoomId, ReceiptMap>;
32
33type RankedReceipts = SmallVec<[ReceiptMap; 1]>;
39
40type RankReceipts = SmallVec<[Receipts; 1]>;
44
45const USER_LIMIT: usize = 256;
46
47#[implement(Service)]
57#[tracing::instrument(
58 name = "receipts",
59 level = "trace",
60 skip(self, server_name, max_edu_count, events_len)
61)]
62pub(super) async fn select_edus_receipts(
63 &self,
64 server_name: &ServerName,
65 since: (u64, u64),
66 max_edu_count: &AtomicU64,
67 events_len: &AtomicUsize,
68) -> Selected {
69 let num = AtomicUsize::new(0);
70 let by_rank = self
71 .services
72 .state_cache
73 .server_rooms(server_name)
74 .map(ToOwned::to_owned)
75 .broad_filter_map(async |room_id| {
76 let ranked = self
77 .select_edus_receipts_room(&room_id, since, max_edu_count, &num)
78 .await;
79
80 ranked
81 .is_empty()
82 .is_false()
83 .then_some((room_id, ranked))
84 })
85 .ready_fold(RankReceipts::new(), |mut by_rank, (room_id, ranked)| {
86 for (rank, map) in ranked.into_iter().enumerate() {
87 if rank >= by_rank.len() {
88 by_rank.push(Receipts::new());
89 }
90
91 by_rank[rank].insert(room_id.clone(), map);
92 }
93
94 by_rank
95 })
96 .await;
97
98 by_rank
101 .into_iter()
102 .map(serialize_edu)
103 .fold(Selected::default(), |mut selected, edu| {
104 selected.push(edu, events_len);
105 selected
106 })
107}
108
109#[implement(Service)]
114#[tracing::instrument(
115 name = "receipts",
116 level = "trace",
117 skip(self, since, max_edu_count)
118)]
119async fn select_edus_receipts_room(
120 &self,
121 room_id: &RoomId,
122 since: (u64, u64),
123 max_edu_count: &AtomicU64,
124 num: &AtomicUsize,
125) -> RankedReceipts {
126 let receipts = self
127 .services
128 .read_receipt
129 .readreceipts_since(room_id, since.0, Some(since.1))
130 .ready_filter_map(|(user_id, count, raw)| {
131 debug_assert!(count <= since.1, "exceeds upper-bound");
132 max_edu_count.fetch_max(count, Ordering::Relaxed);
133
134 if !self.services.globals.user_is_local(user_id) {
135 return None;
136 }
137
138 parse_receipt(user_id, count, &raw).map(|receipt| (user_id.to_owned(), receipt))
139 });
140
141 rank_receipts(receipts, num).await
142}
143
144async fn rank_receipts(
151 receipts: impl Stream<Item = (OwnedUserId, ReceiptData)>,
152 num: &AtomicUsize,
153) -> RankedReceipts {
154 pin_mut!(receipts);
155 let mut ranked = RankedReceipts::new();
156
157 while let Some((user_id, receipt)) = receipts.next().await {
158 let rank = ranked.partition_point(|map| map.read.contains_key(&user_id));
159
160 if rank == ranked.len() {
161 ranked.push(ReceiptMap { read: BTreeMap::new() });
162 }
163
164 let prior = ranked[rank].read.insert(user_id, receipt);
165 debug_assert!(prior.is_none(), "rank already holds this user");
166
167 if rank == 0 && num.fetch_add(1, Ordering::Relaxed) >= USER_LIMIT {
169 break;
170 }
171 }
172
173 ranked
174}
175
176fn serialize_edu(receipts: Receipts) -> EduBuf {
177 edu_buf(&Edu::Receipt(ReceiptContent { receipts }))
178}
179
180fn parse_receipt(
181 user_id: &UserId,
182 count: u64,
183 raw: &Raw<AnySyncEphemeralRoomEvent>,
184) -> Option<ReceiptData> {
185 let Ok(event) = raw.deserialize() else {
186 error!(?user_id, ?count, ?raw, "Invalid edu event in read_receipts.");
187 return None;
188 };
189
190 let AnySyncEphemeralRoomEvent::Receipt(receipt) = event else {
191 error!(?user_id, ?count, ?event, "Invalid event type in read_receipts");
192 return None;
193 };
194
195 let Some((event_id, data)) = own_read_receipt(receipt.content, user_id) else {
196 error!(?user_id, ?count, "Read receipt event lacks the user's own receipt.");
197 return None;
198 };
199
200 Some(ReceiptData { data, event_ids: vec![event_id] })
201}
202
203fn own_read_receipt(
208 content: ReceiptEventContent,
209 user_id: &UserId,
210) -> Option<(OwnedEventId, Receipt)> {
211 let (event_id, receipts) = content.0.into_iter().next()?;
212 let data = receipts
213 .into_iter()
214 .find_map(|(kind, users)| (kind == ReceiptType::Read).then_some(users))?
215 .into_iter()
216 .find_map(|(id, data)| (id == user_id).then_some(data))?;
217
218 Some((event_id, data))
219}