1use std::collections::HashMap;
8
9use arminius_lib_db::{Database, DatabaseError, ResultExt as _, TransactedDb};
10use arminius_lib_events::{RoomEvent, Verified};
11use arminius_lib_model::{event::EventVerifiedRow, syncv4::StreamUpperBound, types::StreamId};
12use itertools::Itertools as _;
13use ruma::{
14 DeviceId, OwnedRoomId, RoomId, UserId,
15 events::{AnyStrippedStateEvent, StateEventType},
16 serde::Raw,
17};
18
19pub const BUMP_EVENTS_TYPES: [&str; 7] = [
43 "m.room.create",
44 "m.room.message",
45 "m.room.encrypted",
46 "m.sticker",
47 "m.call.invite",
48 "m.poll.start",
49 "m.beacon_info",
50];
51
52#[derive(Debug, Clone, sqlx::Type, PartialEq, Eq, Hash)]
54#[sqlx(type_name = "STREAM_NAME", rename_all = "snake_case")]
55pub enum DbStreamName {
56 Events,
58}
59
60#[derive(Debug, Clone, sqlx::Type, PartialEq, Eq, Hash)]
62#[sqlx(type_name = "STREAM_STATUS", rename_all = "UPPERCASE")]
63pub enum DbStreamStatus {
64 Live,
66 Previously,
68}
69
70#[derive(Debug, Clone)]
74pub struct DbSyncV4Connection {
75 }
77
78#[derive(Debug)]
82pub struct DbMaxStreamIds {
83 pub max_event_stream_id: StreamId,
85 pub max_todevice_stream_id: StreamId,
87 pub max_account_data_stream_id: StreamId,
90 pub max_user_keys_stream_id: StreamId,
92 pub max_read_receipts_stream_id: StreamId,
94 pub max_federated_invites_stream_id: StreamId,
96 pub max_federated_knocks_stream_id: StreamId,
98}
99
100#[derive(Debug, Clone)]
102pub struct RoomInfo {
103 pub was_ever_joined: bool,
105 pub max_event_stream_id: StreamId,
107 pub bump_event_stream_id: StreamId,
110}
111
112#[derive(Debug, Default)]
115pub struct SyncedRoomTimeline {
116 pub timeline: Vec<RoomEvent<Verified>>,
118 pub limited: bool,
121 pub num_live: usize,
123}
124
125#[derive(Debug)]
127pub struct RoomState {
128 pub events_stream_upper_bound: StreamUpperBound,
132}
133
134#[rust_analyzer::prefer_underscore_import]
137pub trait TransactedSyncV4Queries: Database {
138 #[tracing::instrument(skip(self))]
140 async fn insert_syncv4_connection(
141 &mut self,
142 user_id: &UserId,
143 device_id: &DeviceId,
144 conn_id: Option<&str>,
145 ) -> Result<(), DatabaseError> {
146 sqlx::query!(
147 r#"
148 INSERT INTO syncv4_connections (device_nid, conn_id)
149 VALUES ((
150 SELECT
151 devices.nid
152 FROM
153 devices
154 JOIN users ON devices.user_nid = users.nid
155 WHERE
156 users.user_id = $1
157 AND devices.device_id = $2),
158 $3)
159 "#,
160 user_id.to_string(),
161 device_id.to_string(),
162 conn_id
163 )
164 .execute(self.connection().await?)
165 .await?;
166
167 Ok(())
168 }
169
170 #[tracing::instrument(skip(self))]
172 async fn reset_connection(
173 &mut self,
174 user_id: &UserId,
175 device_id: &DeviceId,
176 conn_id: Option<&str>,
177 ) -> Result<(), DatabaseError> {
178 sqlx::query!(
179 r#"
180 DELETE FROM syncv4_positions USING syncv4_connections, devices, users
181 WHERE syncv4_positions.connection_nid = syncv4_connections.nid
182 AND syncv4_connections.device_nid = devices.nid
183 AND devices.user_nid = users.nid
184 AND users.user_id = $1
185 AND devices.device_id = $2
186 AND syncv4_connections.conn_id IS NOT DISTINCT FROM $3
187 "#,
188 user_id.as_str(),
189 device_id.as_str(),
190 conn_id
191 )
192 .execute(self.connection().await?)
193 .await?;
194
195 Ok(())
196 }
197
198 #[tracing::instrument(skip(self))]
201 async fn fetch_syncv4_connection(
202 &mut self,
203 user_id: &UserId,
204 device_id: &DeviceId,
205 conn_id: Option<&str>,
206 ) -> Result<Option<DbSyncV4Connection>, DatabaseError> {
207 Ok(sqlx::query_scalar!(
209 r#"
210 SELECT EXISTS (
211 SELECT 1
212 FROM syncv4_connections
213 JOIN devices ON syncv4_connections.device_nid = devices.nid
214 JOIN users ON devices.user_nid = users.nid
215 WHERE
216 users.user_id = $1
217 AND devices.device_id = $2
218 AND syncv4_connections.conn_id IS NOT DISTINCT FROM $3
219 ) AS "exists!"
220 "#,
221 user_id.to_string(),
222 device_id.to_string(),
223 conn_id,
224 )
225 .fetch_one(self.connection().await?)
226 .await?
227 .then_some(DbSyncV4Connection {}))
228 }
229
230 #[tracing::instrument(skip(self))]
234 async fn fetch_syncv4_position(
235 &mut self,
236 user_id: &UserId,
237 device_id: &DeviceId,
238 conn_id: Option<&str>,
239 position: i64,
240 ) -> Result<(), DatabaseError> {
241 sqlx::query!(
242 r#"
243 SELECT
244 1 AS "exists"
245 FROM
246 syncv4_positions
247 JOIN syncv4_connections ON syncv4_positions.connection_nid = syncv4_connections.nid
248 JOIN devices ON syncv4_connections.device_nid = devices.nid
249 JOIN users ON devices.user_nid = users.nid
250 WHERE
251 users.user_id = $1
252 AND devices.device_id = $2
253 AND syncv4_connections.conn_id IS NOT DISTINCT FROM $3
254 AND syncv4_positions.position = $4
255 "#,
256 user_id.to_string(),
257 device_id.to_string(),
258 conn_id,
259 position
260 )
261 .fetch_one(self.connection().await?)
262 .await?;
263
264 Ok(())
265 }
266
267 #[tracing::instrument(skip(self))]
271 async fn new_syncv4_position(
272 &mut self,
273 user_id: &UserId,
274 device_id: &DeviceId,
275 conn_id: Option<&str>,
276 prev_position: Option<i64>,
277 ) -> Result<StreamId, DatabaseError> {
278 sqlx::query!(
280 "
281 DELETE FROM syncv4_positions USING syncv4_connections, users, devices
282 WHERE syncv4_connections.nid = syncv4_positions.connection_nid
283 AND syncv4_connections.device_nid = devices.nid
284 AND devices.user_nid = users.nid
285 AND users.user_id = $1
286 AND devices.device_id = $2
287 AND conn_id IS NOT DISTINCT FROM $3
288 AND prev_position IS DISTINCT FROM $4
289 AND position IS DISTINCT FROM $4
290 ",
291 user_id.to_string(),
292 device_id.to_string(),
293 conn_id,
294 prev_position
295 )
296 .execute(self.connection().await?)
297 .await?;
298
299 let new_position = sqlx::query_scalar!(
300 r#"
301 INSERT INTO syncv4_positions (connection_nid, prev_position)
302 VALUES (
303 (
304 SELECT syncv4_connections.nid
305 FROM syncv4_connections
306 JOIN devices ON syncv4_connections.device_nid = devices.nid
307 JOIN users ON devices.user_nid = users.nid
308 WHERE
309 users.user_id = $1
310 AND devices.device_id = $2
311 AND syncv4_connections.conn_id IS NOT DISTINCT FROM $3
312 ),
313 $4
314 )
315 RETURNING position
316 "#,
317 user_id.to_string(),
318 device_id.to_string(),
319 conn_id,
320 prev_position
321 )
322 .fetch_one(self.connection().await?)
323 .await?;
324
325 Ok(new_position)
326 }
327
328 #[tracing::instrument(skip(self))]
332 async fn fetch_max_stream_ids(&mut self) -> Result<DbMaxStreamIds, DatabaseError> {
333 let row = sqlx::query!(
334 r#"
335 SELECT
336 (SELECT MAX(events.stream_id) FROM events) AS max_events_stream_id,
337 (SELECT MAX(to_device_messages.stream_id) FROM to_device_messages) AS max_todevice_stream_id,
338 (SELECT GREATEST(
339 (SELECT MAX(global_account_data.stream_id) FROM global_account_data),
340 (SELECT MAX(room_account_data.stream_id) FROM room_account_data)
341 )) AS max_account_data_stream_id,
342 (SELECT MAX(user_keys.stream_id) FROM user_keys) AS max_user_keys_stream_id,
343 (SELECT MAX(read_receipts.stream_id) FROM read_receipts) AS max_read_receipts_stream_id,
344 (SELECT MAX(federated_invites.stream_id) FROM federated_invites) AS max_federated_invites_stream_id,
345 (SELECT MAX(federated_knocks.stream_id) FROM federated_knocks) AS max_federated_knocks_stream_id
346 "#
347 )
348 .fetch_one(self.connection().await?)
349 .await?;
350
351 Ok(DbMaxStreamIds {
352 max_event_stream_id: row.max_events_stream_id.unwrap_or(0),
353 max_todevice_stream_id: row.max_todevice_stream_id.unwrap_or(0),
354 max_account_data_stream_id: row.max_account_data_stream_id.unwrap_or(0),
355 max_user_keys_stream_id: row.max_user_keys_stream_id.unwrap_or(0),
356 max_read_receipts_stream_id: row.max_read_receipts_stream_id.unwrap_or(0),
357 max_federated_invites_stream_id: row.max_federated_invites_stream_id.unwrap_or(0),
358 max_federated_knocks_stream_id: row.max_federated_knocks_stream_id.unwrap_or(0),
359 })
360 }
361
362 #[tracing::instrument(skip(self))]
365 async fn fetch_user_room_info(
366 &mut self,
367 user_id: &UserId,
368 ) -> Result<HashMap<OwnedRoomId, RoomInfo>, DatabaseError> {
369 sqlx::query!(
372 r#"
373 SELECT
374 room_membership_state.room_id AS "room_id!",
375 (room_membership_state.membership = 'join'
376 OR bool_or((events.event_json ->> 'type') = 'm.room.member'
377 AND (events.event_json ->> 'state_key') = $1
378 AND (events.event_json #>> '{content,membership}' = 'join'))) AS "has_ever_joined!",
379 MAX(events.stream_id) AS "max_event_stream_id!",
380 MAX(bump_events.stream_id) AS "bump_stream_id"
381 FROM
382 room_membership_state ($1)
383 JOIN events ON room_membership_state.room_nid = events.room_nid
384 -- PERF: This might need a pre-populated table to be less costly to fetch.
385 LEFT JOIN events bump_events ON room_membership_state.room_nid = bump_events.room_nid
386 AND (bump_events.event_json ->> 'type' = ANY ($2))
387 GROUP BY
388 room_membership_state.room_id,
389 room_membership_state.membership
390 "#,
391 user_id.to_string(),
392 &BUMP_EVENTS_TYPES as _
393 )
394 .fetch_all(self.connection().await?)
395 .await?
396 .into_iter()
397 .map(|row| {
398 let room_id = RoomId::parse(row.room_id).invalid_data("Invalid room ID in database")?;
399
400 Ok((
401 room_id,
402 RoomInfo {
403 was_ever_joined: row.has_ever_joined,
404 max_event_stream_id: row.max_event_stream_id,
405 bump_event_stream_id: row.bump_stream_id.unwrap_or(0),
406 },
407 ))
408 })
409 .collect::<Result<_, DatabaseError>>()
410 }
411
412 #[tracing::instrument(skip(self))]
425 async fn fetch_syncv4_joined_rooms_timelines(
426 &mut self,
427 user_id: &UserId,
428 rooms: &HashMap<OwnedRoomId, (StreamId, i64)>,
429 last_sync_stream_id: StreamId,
430 to_stream_id: StreamId,
431 ) -> Result<HashMap<OwnedRoomId, SyncedRoomTimeline>, DatabaseError> {
432 let (rooms_ids, rooms_from, rooms_limits): (Vec<String>, Vec<i64>, Vec<i64>) = rooms
433 .iter()
434 .map(|(room_id, (from_stream_id, timeline_limit))| {
437 (room_id.to_string(), from_stream_id, timeline_limit + 1)
438 })
439 .multiunzip();
440
441 let mut room_timelines = sqlx::query!(
442 r#"
443 SELECT
444 events.room_id AS "room_id!",
445 events.room_limit AS "room_limit!",
446 events.stream_id AS "stream_id!",
447 events.event_json::text AS "event_json!: Box<sqlx::types::JsonRawValue>",
448 events.room_version,
449 events.attempted_at,
450 events.signature_verification,
451 events.soft_failed
452 FROM
453 UNNEST($2::text[], $3::bigint[], $4::bigint[]) AS room_params (room_id,
454 room_from,
455 room_limit)
456 CROSS JOIN LATERAL (
457 SELECT
458 rooms.room_id,
459 room_params.room_limit,
460 events.stream_id,
461 events.event_json,
462 rooms.room_version,
463 verification_metadata.attempted_at,
464 verification_metadata.signature_verification,
465 verification_metadata.soft_failed
466 FROM
467 visible_timeline_events ($1, room_params.room_id) events
468 JOIN verification_metadata ON verification_metadata.event_nid = events.nid
469 JOIN rooms ON rooms.room_id = room_params.room_id
470 WHERE
471 events.stream_id > room_params.room_from
472 AND events.stream_id <= max_safe_seq ('eventids')
473 AND events.stream_id <= $5
474 ORDER BY
475 events.stream_id DESC
476 LIMIT room_params.room_limit) events
477 ORDER BY
478 events.stream_id DESC
479 "#,
480 user_id.to_string(),
481 &rooms_ids,
482 &rooms_from,
483 &rooms_limits,
484 to_stream_id
485 )
486 .fetch_all(self.connection().await?)
487 .await?
488 .into_iter()
489 .try_fold(
490 HashMap::new(),
491 |mut result: HashMap<OwnedRoomId, SyncedRoomTimeline>, row| {
492 let room_id =
493 RoomId::parse(row.room_id).invalid_data("Invalid room ID in database")?;
494
495 let room_timeline = result.entry(room_id).or_default();
496
497 if room_timeline.timeline.len() == (row.room_limit - 1) as usize {
500 room_timeline.limited = true;
501 return Ok(result);
502 }
503
504 room_timeline.timeline.push(
505 EventVerifiedRow {
506 event_json: row.event_json,
507 room_version: row.room_version,
508 attempted_at: row.attempted_at,
509 signature_verification: row.signature_verification,
510 soft_failed: row.soft_failed,
511 }
512 .try_into_event()?,
513 );
514
515 if row.stream_id > last_sync_stream_id {
516 room_timeline.num_live += 1;
517 }
518
519 Ok::<_, DatabaseError>(result)
520 },
521 )?;
522
523 for timeline in room_timelines.values_mut() {
526 timeline.timeline.reverse();
527 }
528
529 Ok(room_timelines)
530 }
531
532 #[tracing::instrument(skip(self))]
534 async fn fetch_room_membership_counts(
535 &mut self,
536 room_id: &RoomId,
537 ) -> Result<(u64, u64), DatabaseError> {
538 let counts = sqlx::query!(
539 r#"
540 SELECT
541 COUNT(*) FILTER (WHERE events.event_json #>> '{content,membership}' = 'join') AS "joined_count!",
542 COUNT(*) FILTER (WHERE events.event_json #>> '{content,membership}' = 'invite') AS "invited_count!"
543 FROM
544 rooms
545 JOIN room_state_snapshots ON room_state_snapshots.room_nid = rooms.nid
546 JOIN state_snapshot_state_events ON state_snapshot_state_events.state_snapshot_nid = room_state_snapshots.state_snapshot_nid
547 JOIN events ON events.nid = state_snapshot_state_events.state_event_nid
548 WHERE
549 events.event_json ->> 'type' = 'm.room.member'
550 AND rooms.room_id = $1
551 AND events.event_json #>> '{content, membership}' IN ('join', 'invite')
552 "#,
553 room_id.to_string()
554 ).fetch_one(self.connection().await?).await?;
555
556 Ok((counts.joined_count as u64, counts.invited_count as u64))
557 }
558
559 #[tracing::instrument(skip(self))]
562 async fn fetch_rooms_stripped_state(
563 &mut self,
564 rooms: &[OwnedRoomId],
565 ) -> Result<HashMap<OwnedRoomId, Vec<Raw<AnyStrippedStateEvent>>>, DatabaseError> {
566 sqlx::query!(
567 r#"
568 SELECT
569 rooms.room_id,
570 rooms.room_version,
571 events.event_json::text AS "event_json!: Box<sqlx::types::JsonRawValue>",
572 verification_metadata.attempted_at,
573 verification_metadata.signature_verification,
574 verification_metadata.soft_failed
575 FROM
576 room_state_snapshots
577 JOIN state_snapshot_state_events ON state_snapshot_state_events.state_snapshot_nid = room_state_snapshots.state_snapshot_nid
578 JOIN events ON state_snapshot_state_events.state_event_nid = events.nid
579 JOIN verification_metadata ON verification_metadata.event_nid = events.nid
580 JOIN rooms ON events.room_nid = rooms.nid
581 WHERE
582 rooms.room_id = ANY ($1::text[])
583 "#,
584 &rooms.into_iter().map(ToString::to_string).collect::<Vec<_>>()
585 ).fetch_all(self.connection().await?)
586 .await?
587 .into_iter()
588 .try_fold(HashMap::new(), |mut result, row| -> Result<_, DatabaseError> {
589 let room_id = RoomId::parse(row.room_id).invalid_data("Invalid room ID in database")?;
590
591 let verified_event = EventVerifiedRow {
592 event_json: row.event_json,
593 room_version: row.room_version,
594 attempted_at: row.attempted_at,
595 signature_verification: row.signature_verification,
596 soft_failed: row.soft_failed
597 }.try_into_event()?;
598
599
600 let stripped_state_event = verified_event.to_stripped_state_event().invalid_data("Non state event stored as one in the database")?;
601
602 result.entry(room_id)
603 .and_modify(|room_events: &mut Vec<_>| room_events.push(stripped_state_event.clone()))
604 .or_insert_with(|| vec![stripped_state_event]);
605
606 Ok(result)
607 })
608 }
609
610 #[tracing::instrument(skip(self))]
612 async fn fetch_rooms_required_states(
613 &mut self,
614 room_id: &RoomId,
615 required_state_config: &Vec<(StateEventType, String)>,
616 from_stream_id: StreamId,
617 to_stream_id: StreamId,
618 ) -> Result<Vec<RoomEvent<Verified>>, DatabaseError> {
619 let (event_types, state_keys): (Vec<String>, Vec<String>) = required_state_config
620 .iter()
621 .map(|(event_type, state_key)| (event_type.to_string(), state_key.to_owned()))
622 .unzip();
623
624 sqlx::query_as!(
625 EventVerifiedRow,
626 r#"
627 WITH wanted_events AS (
628 SELECT
629 *
630 FROM
631 unnest($2::text[], $3::text[]) AS t (event_type,
632 state_key))
633 SELECT DISTINCT ON (events.nid)
634 events.event_json AS "event_json: Box<sqlx::types::JsonRawValue>",
635 rooms.room_version,
636 verification_metadata.attempted_at,
637 verification_metadata.signature_verification,
638 verification_metadata.soft_failed
639 FROM
640 room_state_snapshots
641 JOIN state_snapshot_state_events s ON room_state_snapshots.state_snapshot_nid = s.state_snapshot_nid
642 JOIN events ON s.state_event_nid = events.nid
643 JOIN verification_metadata ON verification_metadata.event_nid = events.nid
644 JOIN rooms ON events.room_nid = rooms.nid
645 JOIN wanted_events ON (wanted_events.event_type = '*'
646 OR wanted_events.event_type = event_json ->> 'type')
647 AND (wanted_events.state_key = '*'
648 OR wanted_events.state_key = '$LAZY'
649 OR wanted_events.state_key = event_json ->> 'state_key')
650 WHERE
651 rooms.room_id = $1
652 AND events.stream_id > $4
653 AND events.stream_id <= max_safe_seq ('eventids')
654 AND events.stream_id <= $5
655 "#,
656 room_id.as_str(),
657 &event_types,
658 &state_keys,
659 from_stream_id,
660 to_stream_id
661 )
662 .fetch_all(self.connection().await?)
663 .await?
664 .into_iter()
665 .map(EventVerifiedRow::try_into_event)
666 .collect()
667 }
668
669 #[tracing::instrument(skip(self))]
672 async fn fetch_events_room_streams(
673 &mut self,
674 position: i64,
675 ) -> Result<HashMap<OwnedRoomId, StreamUpperBound>, DatabaseError> {
676 sqlx::query!(
677 r#"
678 SELECT
679 room_id,
680 stream_status AS "stream_status:DbStreamStatus",
681 up_to
682 FROM
683 syncv4_room_streams
684 JOIN rooms ON syncv4_room_streams.room_nid = rooms.nid
685 JOIN syncv4_positions ON syncv4_room_streams.position = syncv4_positions.position
686 WHERE
687 syncv4_positions.position = $1
688 "#,
689 position
690 )
691 .fetch_all(self.connection().await?)
692 .await?
693 .into_iter()
694 .map(|row| {
695 let room_id = RoomId::parse(row.room_id).invalid_data("Invalid room ID in database")?;
696
697 let event_stream_status = if row.stream_status == DbStreamStatus::Live {
698 StreamUpperBound::Live
699 } else {
700 let up_to = row.up_to.ok_or(DatabaseError::InvalidData(
701 "Entry with status 'previously' without its `up_to` set".to_owned(),
702 ))?;
703 StreamUpperBound::Previously(up_to)
704 };
705
706 Ok((room_id, event_stream_status))
707 })
708 .collect::<Result<_, DatabaseError>>()
709 }
710
711 #[tracing::instrument(skip(self))]
714 async fn update_rooms_events_streams_to_previously(
715 &mut self,
716 position: i64,
717 rooms_to_update: Vec<OwnedRoomId>,
718 new_up_to: StreamId,
719 ) -> Result<(), DatabaseError> {
720 let room_ids: Vec<String> =
721 rooms_to_update.into_iter().map(|room_id| room_id.to_string()).collect();
722
723 sqlx::query!(
724 r#"
725 UPDATE
726 syncv4_room_streams
727 SET
728 stream_status = $4,
729 up_to = $5
730 FROM
731 rooms
732 WHERE
733 syncv4_room_streams.position = $1
734 AND syncv4_room_streams.stream_name = $2
735 AND rooms.room_id = ANY ($3::text[])
736 "#,
737 position,
738 DbStreamName::Events as _,
739 &room_ids,
740 DbStreamStatus::Previously as _,
741 new_up_to
742 )
743 .execute(self.connection().await?)
744 .await?;
745
746 Ok(())
747 }
748
749 #[tracing::instrument(skip(self))]
751 async fn insert_rooms_events_streams(
752 &mut self,
753 new_position: i64,
754 rooms_streams: HashMap<OwnedRoomId, StreamUpperBound>,
755 ) -> Result<(), DatabaseError> {
756 let (room_ids, stream_statuses, up_tos): (
757 Vec<String>,
758 Vec<DbStreamStatus>,
759 Vec<Option<i64>>,
760 ) = rooms_streams
761 .into_iter()
762 .map(|(room_id, event_stream_status)| {
763 let (stream_status, up_to) = match event_stream_status {
764 StreamUpperBound::Previously(up_to) => {
765 (DbStreamStatus::Previously, Some(up_to))
766 }
767 StreamUpperBound::Live => (DbStreamStatus::Live, None),
768 };
769
770 (room_id.to_string(), stream_status, up_to)
771 })
772 .multiunzip();
773
774 sqlx::query!(
775 r#"
776 INSERT INTO syncv4_room_streams (position, stream_name, room_nid, stream_status, up_to)
777 SELECT
778 $1,
779 $2,
780 rooms.nid,
781 params.stream_status,
782 params.up_to
783 FROM
784 UNNEST($3::text[], $4::STREAM_STATUS[], $5::bigint[]) AS params (room_id,
785 stream_status,
786 up_to)
787 JOIN rooms ON params.room_id = rooms.room_id
788 "#,
789 new_position,
790 DbStreamName::Events as _,
791 &room_ids,
792 stream_statuses as _,
793 up_tos as _
794 )
795 .execute(self.connection().await?)
796 .await?;
797
798 Ok(())
799 }
800}
801
802impl<'c> TransactedSyncV4Queries for TransactedDb<'c> {}