1use std::collections::BTreeMap;
13
14use arminius_lib_db::{Database, DatabaseError, ResultExt as _, TransactedDb};
15use ruma::{OwnedRoomId, RoomId, UserId, api::client::backup::KeyBackupData};
16
17#[derive(Debug)]
19pub struct DbRoomKeysBackup {
20 pub auth_data: serde_json::Value,
22 pub keys_count: u64,
24 pub etag: i64,
26 pub version: i64,
28 pub rooms: BTreeMap<OwnedRoomId, BTreeMap<String, KeyBackupData>>,
30}
31
32#[rust_analyzer::prefer_underscore_import]
35pub trait RoomKeysQueries: Database {
36 #[tracing::instrument(skip(self))]
41 async fn insert_backup(
42 &mut self,
43 user_id: &UserId,
44 auth_data: &serde_json::Value,
45 ) -> Result<String, DatabaseError> {
46 Ok(sqlx::query!(
47 "
48 WITH new_backup_version AS (
49 UPDATE users SET keys_backup_seq = keys_backup_seq + 1
50 WHERE user_id = $1
51 RETURNING nid, keys_backup_seq
52 )
53 INSERT INTO room_keys_versions (user_nid, version, auth_data)
54 SELECT nid, keys_backup_seq, $2 FROM new_backup_version
55 RETURNING version
56 ",
57 user_id.to_string(),
58 auth_data
59 )
60 .fetch_one(self.connection().await?)
61 .await?
62 .version
63 .to_string())
64 }
65
66 #[tracing::instrument(skip(self))]
70 async fn update_user_backup(
71 &mut self,
72 user_id: &UserId,
73 version: i64,
74 new_auth_data: &serde_json::Value,
75 ) -> Result<(), DatabaseError> {
76 if sqlx::query!(
77 r#"
78 UPDATE
79 room_keys_versions
80 SET
81 auth_data = $3,
82 etag = etag + 1
83 FROM
84 users
85 WHERE
86 room_keys_versions.user_nid = users.nid
87 AND users.user_id = $1
88 AND room_keys_versions.version = $2
89 "#,
90 user_id.to_string(),
91 version,
92 new_auth_data
93 )
94 .execute(self.connection().await?)
95 .await?
96 .rows_affected()
97 != 1
98 {
99 return Err(DatabaseError::SqlxError(sqlx::Error::RowNotFound));
100 }
101
102 Ok(())
103 }
104
105 #[tracing::instrument(skip(self))]
108 async fn fetch_max_user_backup_version(
109 &mut self,
110 user_id: &UserId,
111 ) -> Result<i64, DatabaseError> {
112 Ok(sqlx::query_scalar!(
113 "SELECT keys_backup_seq FROM users WHERE user_id = $1",
114 user_id.to_string()
115 )
116 .fetch_one(self.connection().await?)
117 .await?)
118 }
119
120 #[tracing::instrument(skip(self))]
122 async fn delete_user_backup(
123 &mut self,
124 user_id: &UserId,
125 version: i64,
126 ) -> Result<(), DatabaseError> {
127 sqlx::query!(
128 "
129 DELETE FROM room_keys_versions
130 USING users
131 WHERE
132 room_keys_versions.user_nid = users.nid
133 AND users.user_id = $1
134 AND room_keys_versions.version = $2
135 ",
136 user_id.to_string(),
137 version
138 )
139 .execute(self.connection().await?)
140 .await?;
141
142 Ok(())
143 }
144
145 #[tracing::instrument(skip(self))]
154 async fn delete_backup_keys(
155 &mut self,
156 user_id: &UserId,
157 version: i64,
158 room_id: Option<OwnedRoomId>,
159 session_id: Option<String>,
160 ) -> Result<Option<(i64, u64)>, DatabaseError> {
161 Ok(sqlx::query_file!(
162 "src/delete_backup_keys.sql",
163 user_id.to_string(),
164 version,
165 room_id.map(|room_id| room_id.to_string()),
166 session_id
167 )
168 .fetch_optional(self.connection().await?)
169 .await?
170 .map(|row| (row.etag, row.keys_count as u64)))
171 }
172}
173
174impl<T: Database> RoomKeysQueries for T {}
175
176pub trait TransactedRoomKeysQueries: RoomKeysQueries {
178 #[tracing::instrument(skip(self))]
186 async fn fetch_user_backup(
187 &mut self,
188 user_id: &UserId,
189 version: Option<i64>,
190 room_id: Option<OwnedRoomId>,
191 session_id: Option<String>,
192 ) -> Result<Option<DbRoomKeysBackup>, DatabaseError> {
193 let mut backup_and_nid = sqlx::query!(
194 r#"
195 SELECT
196 room_keys_versions.nid,
197 auth_data,
198 etag,
199 version
200 FROM
201 room_keys_versions
202 JOIN users ON room_keys_versions.user_nid = users.nid
203 WHERE
204 users.user_id = $1
205 AND ($2::bigint IS NULL
206 OR version = $2)
207 ORDER BY
208 version DESC
209 LIMIT 1
210 "#,
211 user_id.to_string(),
212 version
213 )
214 .fetch_optional(self.connection().await?)
215 .await?
216 .map(|row| {
217 (
218 row.nid,
219 DbRoomKeysBackup {
220 auth_data: row.auth_data,
221 keys_count: 0,
222 etag: row.etag,
223 version: row.version,
224 rooms: BTreeMap::new(),
225 },
226 )
227 });
228
229 if let Some((backup_nid, backup)) = backup_and_nid.as_mut() {
231 let rows = sqlx::query!(
232 r#"
233 SELECT
234 rooms.room_id,
235 session_id,
236 backup_data
237 FROM
238 room_keys
239 JOIN room_keys_versions ON room_keys.room_keys_version_nid = room_keys_versions.nid
240 JOIN users ON room_keys_versions.user_nid = users.nid
241 JOIN rooms ON room_keys.room_nid = rooms.nid
242 WHERE
243 users.user_id = $1
244 AND room_keys_versions.nid = $2
245 AND ($3::text IS NULL
246 OR rooms.room_id = $3)
247 AND ($4::text IS NULL
248 OR room_keys.session_id = $4)
249 "#,
250 user_id.to_string(),
251 *backup_nid,
252 room_id.map(|room_id| room_id.to_string()),
253 session_id
254 )
255 .fetch_all(self.connection().await?)
256 .await?;
257
258 backup.keys_count = rows.len() as u64;
259
260 for row in rows {
261 let room_id =
262 RoomId::parse(row.room_id).invalid_data("Invalid room ID in database")?;
263
264 let backup_data: KeyBackupData = serde_json::from_value(row.backup_data)
265 .invalid_data("Invalid room keys backup data in database")?;
266
267 let room_keys = backup.rooms.entry(room_id).or_default();
268 room_keys.insert(row.session_id, backup_data);
269 }
270 }
271
272 Ok(backup_and_nid.map(|(_nid, backup)| backup))
273 }
274
275 #[tracing::instrument(skip(self))]
277 async fn upsert_backup_data(
278 &mut self,
279 user_id: &UserId,
280 version: i64,
281 new_backup_data: BTreeMap<OwnedRoomId, BTreeMap<String, serde_json::Value>>,
282 ) -> Result<(i64, u64), DatabaseError> {
283 for (room_id, room_backups) in new_backup_data {
284 for (session_id, backup_data) in room_backups {
285 sqlx::query!(
286 r#"
287 WITH the_backup AS (
288 SELECT
289 room_keys_versions.nid,
290 etag
291 FROM
292 room_keys_versions
293 JOIN users ON room_keys_versions.user_nid = users.nid
294 WHERE
295 users.user_id = $1
296 AND room_keys_versions.version = $2)
297 INSERT INTO room_keys (room_keys_version_nid, room_nid, session_id, backup_data)
298 VALUES
299 ((
300 SELECT
301 nid
302 FROM
303 the_backup),
304 (
305 SELECT
306 nid
307 FROM
308 rooms
309 WHERE
310 room_id = $3), $4, $5)
311 ON CONFLICT (room_keys_version_nid,
312 room_nid,
313 session_id)
314 DO UPDATE SET
315 backup_data = EXCLUDED.backup_data
316 "#,
317 user_id.to_string(),
318 version,
319 room_id.to_string(),
320 session_id,
321 backup_data
322 )
323 .execute(self.connection().await?)
324 .await?;
325 }
326 }
327
328 let etag_and_keys_count = sqlx::query!(
331 r#"
332 WITH updated_backup AS (
333 UPDATE
334 room_keys_versions
335 SET
336 etag = etag + 1
337 FROM
338 users
339 WHERE
340 room_keys_versions.user_nid = users.nid
341 AND users.user_id = $1
342 AND room_keys_versions.version = $2
343 RETURNING
344 room_keys_versions.nid,
345 etag
346 )
347 SELECT
348 etag,
349 COUNT(room_keys) AS "keys_count!"
350 FROM
351 updated_backup
352 LEFT JOIN room_keys ON updated_backup.nid = room_keys.room_keys_version_nid
353 GROUP BY
354 etag
355 "#,
356 user_id.to_string(),
357 version
358 )
359 .fetch_one(self.connection().await?)
360 .await?;
361
362 Ok((etag_and_keys_count.etag, etag_and_keys_count.keys_count as u64))
363 }
364}
365
366impl<'c> TransactedRoomKeysQueries for TransactedDb<'c> {}