arminius_lib_db_read_receipts/
lib.rs1use std::{collections::BTreeMap, time::SystemTime};
8
9use arminius_lib_db::{Database, DatabaseError, ResultExt as _};
10use arminius_lib_model::{
11 read_receipts::{DbReceiptType, SyncReceiptsMap},
12 types::StreamId,
13};
14use ruma::{
15 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId,
16 api::client::filter::RoomEventFilter,
17 events::receipt::{Receipt, ReceiptThread},
18};
19
20#[derive(Debug)]
22pub struct ReadReceiptRow {
23 pub nid: i32,
26 pub event_id: OwnedEventId,
28 pub receipt_type: DbReceiptType,
30 pub thread: ReceiptThread,
32 pub event_ordering: Vec<i64>,
34 pub ts: chrono::DateTime<chrono::Utc>,
36}
37
38#[derive(Debug)]
40pub struct SyncRoomReceipts {
41 pub max_stream_id: StreamId,
43 pub receipts: SyncReceiptsMap,
45}
46
47#[rust_analyzer::prefer_underscore_import]
50pub trait ReadReceiptsQueries: Database {
51 #[tracing::instrument(skip(self))]
54 async fn fetch_read_receipts(
55 &mut self,
56 room_id: &RoomId,
57 user_id: &UserId,
58 only_private_receipts: bool,
59 ) -> Result<Vec<ReadReceiptRow>, DatabaseError> {
60 sqlx::query!(
61 r#"
62 SELECT
63 read_receipts.nid,
64 events.event_id,
65 read_receipts.receipt_type AS "receipt_type:DbReceiptType",
66 read_receipts.thread,
67 event_ordering.ordering,
68 read_receipts.ts
69 FROM
70 read_receipts
71 JOIN rooms ON read_receipts.room_nid = rooms.nid
72 JOIN events ON read_receipts.event_nid = events.nid
73 JOIN event_ordering ON event_ordering.event_nid = events.nid
74 WHERE
75 rooms.room_id = $1
76 AND read_receipts.user_id = $2
77 AND (
78 CASE WHEN $3::bool IS TRUE THEN
79 read_receipts.receipt_type = 'm.read.private'
80 ELSE
81 TRUE
82 END)
83 ORDER BY
84 event_ordering.ordering DESC
85 "#,
86 room_id.to_string(),
87 user_id.to_string(),
88 only_private_receipts
89 )
90 .fetch_all(self.connection().await?)
91 .await?
92 .into_iter()
93 .map(|row| {
94 let event_id =
95 EventId::parse(row.event_id).invalid_data("Invalid event ID in database")?;
96
97 let thread = ReceiptThread::try_from(row.thread)
98 .invalid_data("Invalid read receipt thread in database")?;
99
100 Ok(ReadReceiptRow {
101 nid: row.nid,
102 event_id,
103 receipt_type: row.receipt_type,
104 thread,
105 event_ordering: row.ordering,
106 ts: row.ts,
107 })
108 })
109 .collect::<Result<Vec<_>, _>>()
110 }
111
112 #[expect(
114 trivial_casts,
115 reason = "The casts in `sqlx` queries are not regular rust casts, but used to infer custom
116 types from SQL queries. See
117 <https://docs.rs/sqlx/latest/sqlx/macro.query.html#type-overrides-bind-parameters-postgres-only>"
118 )]
119 #[tracing::instrument(skip(self))]
120 async fn insert_read_receipt(
121 &mut self,
122 room_id: &RoomId,
123 user_id: &UserId,
124 event_id: &EventId,
125 receipt_type: DbReceiptType,
126 thread: &ReceiptThread,
127 ts: chrono::DateTime<chrono::Utc>,
128 ) -> Result<(), DatabaseError> {
129 sqlx::query!(
130 "
131 INSERT INTO read_receipts
132 (room_nid, user_id, event_nid, receipt_type, thread, ts)
133 VALUES (
134 (SELECT nid FROM rooms WHERE room_id = $1),
135 $2,
136 (SELECT nid FROM events WHERE event_id = $3),
137 $4,
138 $5,
139 $6
140 )
141 ",
142 room_id.to_string(),
143 user_id.to_string(),
144 event_id.to_string(),
145 receipt_type as DbReceiptType,
146 thread.as_str(),
147 ts
148 )
149 .execute(self.connection().await?)
150 .await?;
151
152 Ok(())
153 }
154
155 #[tracing::instrument(skip(self))]
157 async fn delete_read_receipts(&mut self, nids: &[i32]) -> Result<(), DatabaseError> {
158 if sqlx::query!("DELETE FROM read_receipts WHERE nid = ANY($1::int[])", nids)
159 .execute(self.connection().await?)
160 .await?
161 .rows_affected() as usize
162 != nids.len()
163 {
164 return Err(DatabaseError::SqlxError(sqlx::Error::RowNotFound));
165 }
166
167 Ok(())
168 }
169
170 #[tracing::instrument(skip(self))]
175 async fn fetch_sync_read_receipts(
176 &mut self,
177 user_id: &UserId,
178 from_stream_id: StreamId,
179 filter: Option<&RoomEventFilter>,
180 ) -> Result<SyncRoomReceipts, DatabaseError> {
181 let types = filter.and_then(|filter| filter.types.clone());
182 let not_types = filter.map(|filter| filter.not_types.clone()).unwrap_or_default();
183 let should_have_receipts = !(types
184 .is_some_and(|types| !types.contains(&"m.receipt".to_owned()))
185 || not_types.contains(&"m.receipt".to_owned()));
186
187 let rows = sqlx::query!(
188 r#"
189 SELECT
190 rooms.room_id,
191 read_receipts.user_id,
192 events.event_id,
193 read_receipts.stream_id,
194 read_receipts.receipt_type AS "receipt_type:DbReceiptType",
195 read_receipts.thread,
196 read_receipts.ts
197 FROM
198 read_receipts
199 JOIN room_membership_state ($2) ON read_receipts.room_nid = room_membership_state.room_nid
200 JOIN rooms ON read_receipts.room_nid = rooms.nid
201 JOIN events ON read_receipts.event_nid = events.nid
202 WHERE
203 -- Filter in rooms the user is joined in
204 room_membership_state.membership = 'join'
205 -- Filter in receipts that were stored after the given stream ID
206 AND (read_receipts.stream_id > $1
207 AND read_receipts.stream_id <= max_safe_seq ('read_receipts_streamids'))
208 -- Users are allowed to see all of their own read receipts, and only the public
209 -- ones of other users
210 AND (read_receipts.user_id = $2
211 OR read_receipts.receipt_type = 'm.read')
212 AND $3
213 "#,
214 from_stream_id,
215 user_id.to_string(),
216 should_have_receipts
217 )
218 .fetch_all(self.connection().await?)
219 .await?;
220
221 let mut max_stream_id = StreamId::MIN;
222 let mut receipts: SyncReceiptsMap = BTreeMap::new();
223
224 for row in rows {
225 max_stream_id = max_stream_id.max(row.stream_id);
226
227 let room_id = RoomId::parse(row.room_id).invalid_data("Invalid room ID in database")?;
228 let user_id = UserId::parse(row.user_id).invalid_data("Invalid user ID in database")?;
229 let event_id =
230 EventId::parse(row.event_id).invalid_data("Invalid event ID in database")?;
231 let receipt_type = row.receipt_type.into();
232 let receipt_thread = ReceiptThread::try_from(row.thread)
233 .invalid_data("Invalid receipt thread in database")?;
234
235 let ts = MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::from(row.ts)).ok_or(
236 DatabaseError::InvalidData("Invalid read receipt timestamp in database".to_owned()),
237 )?;
238
239 let mut receipt = Receipt::new(ts);
240 receipt.thread = receipt_thread;
241
242 receipts
243 .entry(room_id)
244 .or_default()
245 .entry(event_id)
246 .or_default()
247 .entry(receipt_type)
248 .or_default()
249 .entry(user_id)
250 .or_insert(receipt);
251 }
252
253 Ok(SyncRoomReceipts { max_stream_id, receipts })
254 }
255}
256
257impl<T> ReadReceiptsQueries for T where T: Database {}