1use std::{
9 collections::{HashMap, HashSet},
10 fmt,
11 fs::OpenOptions,
12 path::Path,
13 sync::{Arc, Mutex},
14 time::{SystemTime, UNIX_EPOCH},
15};
16
17use futures_util::future::BoxFuture;
18use rusqlite::{Connection, OptionalExtension, Row, params, types::Type};
19use serde::{Deserialize, Serialize};
20
21use crate::{
22 AuthCredential, ChatParticipantRecord, ClientErrorCategory, ClientEvent, ClientFailure,
23 DialogFollowMode, DialogNotificationMode, DialogRecord, DialogsOrder, DialogsPage,
24 DialogsRequest, EventReliability, HistoryPage, HistoryRequest, InlineId, MessageContent,
25 MessageRecord, SpaceMemberRecord, SpaceRecord, TransactionEvent, TransactionId,
26 TransactionIdentity, TransactionState, UserRecord, UserSettingsRecord,
27};
28
29pub(crate) fn select_history_window(
33 mut messages: Vec<MessageRecord>,
34 limit: usize,
35 before_message_id: Option<InlineId>,
36 after_message_id: Option<InlineId>,
37) -> (Vec<MessageRecord>, bool) {
38 if let Some(before) = before_message_id {
39 messages.retain(|message| message.message_id.get() < before.get());
40 }
41 if let Some(after) = after_message_id {
42 messages.retain(|message| message.message_id.get() > after.get());
43 }
44
45 messages.sort_by_key(|message| message.message_id.get());
46 let limit = limit.max(1);
47 let has_more = messages.len() > limit;
48 if has_more {
49 if after_message_id.is_some() {
50 messages.truncate(limit);
51 } else {
52 messages = messages.split_off(messages.len() - limit);
53 }
54 }
55 messages.sort_by_key(|message| (message.timestamp, message.message_id.get()));
56 (messages, has_more)
57}
58
59pub type StoreResult<T> = Result<T, StoreError>;
61
62#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
64#[error("{category:?}: {message}")]
65pub struct StoreError {
66 pub category: ClientErrorCategory,
68 pub message: String,
70}
71
72impl StoreError {
73 pub fn new(category: ClientErrorCategory, message: impl Into<String>) -> Self {
75 Self {
76 category,
77 message: message.into(),
78 }
79 }
80
81 fn internal(message: impl Into<String>) -> Self {
82 Self::new(ClientErrorCategory::Internal, message)
83 }
84}
85
86#[derive(Clone, PartialEq, Eq)]
88pub struct StoredSession {
89 pub auth: AuthCredential,
91 pub account_namespace: Option<String>,
93}
94
95#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
101pub struct SyncState {
102 pub last_sync_date: i64,
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
108pub enum SyncBucketPeer {
109 User {
111 user_id: InlineId,
113 },
114 Chat {
116 chat_id: InlineId,
118 },
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
123pub enum SyncBucketKey {
124 User,
126 Space {
128 space_id: InlineId,
130 },
131 Chat {
133 peer: SyncBucketPeer,
135 },
136}
137
138#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
140pub struct SyncBucketState {
141 pub seq: i64,
143 pub date: i64,
145}
146
147#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct PendingSyncBatch {
152 pub key: SyncBucketKey,
154 pub committed_state: SyncBucketState,
156 pub payload: Vec<u8>,
158}
159
160#[derive(Clone, Debug, PartialEq, Eq)]
165pub struct ClientEventDelivery {
166 pub delivery_id: Option<u64>,
168 pub event: ClientEvent,
170}
171
172impl ClientEventDelivery {
173 pub fn ephemeral(event: ClientEvent) -> Self {
175 Self {
176 delivery_id: None,
177 event,
178 }
179 }
180}
181
182#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
184pub struct StoredReaction {
185 pub chat_id: InlineId,
187 pub message_id: InlineId,
189 pub user_id: InlineId,
191 pub reaction: String,
193}
194
195#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
197pub struct StoredReadState {
198 pub chat_id: InlineId,
200 pub read_max_id: Option<InlineId>,
202 pub unread_count: Option<u32>,
204 pub marked_unread: bool,
206}
207
208#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
211pub struct AccountStateSnapshot {
212 pub deleted_chat_ids: Vec<InlineId>,
214}
215
216#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
219pub struct ChatStateSnapshot {
220 pub chat_id: InlineId,
222 pub dialog: Option<DialogRecord>,
224 pub deleted: bool,
226 pub deleted_message_ids: Vec<InlineId>,
228 pub reactions: Vec<StoredReaction>,
230 pub reaction_snapshot_message_ids: Vec<InlineId>,
233 pub read_state: Option<StoredReadState>,
235 pub participants: Vec<ChatParticipantRecord>,
237 pub participants_complete: bool,
239}
240
241impl fmt::Debug for StoredSession {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 f.debug_struct("StoredSession")
244 .field("auth", &self.auth)
245 .field(
246 "account_namespace",
247 &self.account_namespace.as_ref().map(|_| "<redacted>"),
248 )
249 .finish()
250 }
251}
252
253pub trait ClientStore: fmt::Debug + Send + Sync + 'static {
255 fn save_session(&self, session: StoredSession) -> BoxFuture<'static, StoreResult<()>>;
257
258 fn load_session(&self) -> BoxFuture<'static, StoreResult<Option<StoredSession>>>;
260
261 fn clear_session(&self) -> BoxFuture<'static, StoreResult<()>>;
263
264 fn clear_account_data(&self) -> BoxFuture<'static, StoreResult<()>>;
267
268 fn sync_state(&self) -> BoxFuture<'static, StoreResult<SyncState>>;
270
271 fn save_sync_state(&self, state: SyncState) -> BoxFuture<'static, StoreResult<()>>;
273
274 fn clear_sync_state(&self) -> BoxFuture<'static, StoreResult<()>>;
276
277 fn sync_bucket_state(
279 &self,
280 key: SyncBucketKey,
281 ) -> BoxFuture<'static, StoreResult<SyncBucketState>>;
282
283 fn save_sync_bucket_state(
285 &self,
286 key: SyncBucketKey,
287 state: SyncBucketState,
288 ) -> BoxFuture<'static, StoreResult<()>>;
289
290 fn save_sync_bucket_states(
292 &self,
293 states: Vec<(SyncBucketKey, SyncBucketState)>,
294 ) -> BoxFuture<'static, StoreResult<()>>;
295
296 fn remove_sync_bucket_state(&self, key: SyncBucketKey) -> BoxFuture<'static, StoreResult<()>>;
298
299 fn save_pending_sync_batch(
302 &self,
303 batch: PendingSyncBatch,
304 ) -> BoxFuture<'static, StoreResult<()>>;
305
306 fn pending_sync_batches(&self) -> BoxFuture<'static, StoreResult<Vec<PendingSyncBatch>>>;
308
309 fn discard_pending_sync_batch(
313 &self,
314 _key: SyncBucketKey,
315 ) -> BoxFuture<'static, StoreResult<()>> {
316 Box::pin(async {
317 Err(StoreError::new(
318 ClientErrorCategory::Unsupported,
319 "store does not support discarding invalid sync journals",
320 ))
321 })
322 }
323
324 fn commit_pending_sync_batch(
326 &self,
327 key: SyncBucketKey,
328 state: SyncBucketState,
329 ) -> BoxFuture<'static, StoreResult<()>>;
330
331 fn commit_pending_sync_batch_with_events(
335 &self,
336 key: SyncBucketKey,
337 state: SyncBucketState,
338 events: Vec<ClientEvent>,
339 ) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
340 if events
341 .iter()
342 .any(|event| event.reliability() == EventReliability::Lossless)
343 {
344 return Box::pin(async {
345 Err(StoreError::new(
346 ClientErrorCategory::Unsupported,
347 "client store does not support a durable event outbox",
348 ))
349 });
350 }
351 let commit = self.commit_pending_sync_batch(key, state);
352 Box::pin(async move {
353 commit.await?;
354 Ok(events
355 .into_iter()
356 .map(ClientEventDelivery::ephemeral)
357 .collect())
358 })
359 }
360
361 fn append_client_events(
364 &self,
365 events: Vec<ClientEvent>,
366 ) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
367 Box::pin(async move {
368 if events
369 .iter()
370 .any(|event| event.reliability() == EventReliability::Lossless)
371 {
372 return Err(StoreError::new(
373 ClientErrorCategory::Unsupported,
374 "client store does not support a durable event outbox",
375 ));
376 }
377 Ok(events
378 .into_iter()
379 .map(ClientEventDelivery::ephemeral)
380 .collect())
381 })
382 }
383
384 fn pending_client_events(&self) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
387 Box::pin(async { Ok(Vec::new()) })
388 }
389
390 fn acknowledge_client_event(&self, _delivery_id: u64) -> BoxFuture<'static, StoreResult<()>> {
393 Box::pin(async { Ok(()) })
394 }
395
396 fn dialogs(&self, request: DialogsRequest) -> BoxFuture<'static, StoreResult<DialogsPage>>;
398
399 fn dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<Option<DialogRecord>>>;
401
402 fn record_dialog(&self, dialog: DialogRecord) -> BoxFuture<'static, StoreResult<()>>;
404
405 fn remove_dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<()>>;
407
408 fn deleted_chat_ids(&self) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
410
411 fn record_users(&self, users: Vec<UserRecord>) -> BoxFuture<'static, StoreResult<()>>;
413
414 fn user(&self, _user_id: InlineId) -> BoxFuture<'static, StoreResult<Option<UserRecord>>> {
416 Box::pin(async { Ok(None) })
417 }
418
419 fn record_space(&self, space: SpaceRecord) -> BoxFuture<'static, StoreResult<()>>;
421
422 fn space(&self, space_id: InlineId) -> BoxFuture<'static, StoreResult<Option<SpaceRecord>>>;
424
425 fn record_space_member(&self, member: SpaceMemberRecord)
427 -> BoxFuture<'static, StoreResult<()>>;
428
429 fn record_space_members(
431 &self,
432 space_id: InlineId,
433 members: Vec<SpaceMemberRecord>,
434 ) -> BoxFuture<'static, StoreResult<()>>;
435
436 fn remove_space_member(
438 &self,
439 space_id: InlineId,
440 user_id: InlineId,
441 ) -> BoxFuture<'static, StoreResult<()>>;
442
443 fn space_members(
445 &self,
446 space_id: InlineId,
447 ) -> BoxFuture<'static, StoreResult<Vec<SpaceMemberRecord>>>;
448
449 fn record_user_settings(
451 &self,
452 settings: UserSettingsRecord,
453 ) -> BoxFuture<'static, StoreResult<()>>;
454
455 fn user_settings(&self) -> BoxFuture<'static, StoreResult<Option<UserSettingsRecord>>>;
457
458 fn record_chat_participants(
460 &self,
461 chat_id: InlineId,
462 participants: Vec<ChatParticipantRecord>,
463 ) -> BoxFuture<'static, StoreResult<()>>;
464
465 fn record_chat_participant(
467 &self,
468 chat_id: InlineId,
469 participant: ChatParticipantRecord,
470 ) -> BoxFuture<'static, StoreResult<()>>;
471
472 fn remove_chat_participant(
474 &self,
475 chat_id: InlineId,
476 user_id: InlineId,
477 ) -> BoxFuture<'static, StoreResult<()>>;
478
479 fn chat_participants(
481 &self,
482 chat_id: InlineId,
483 ) -> BoxFuture<'static, StoreResult<Vec<ChatParticipantRecord>>>;
484
485 fn chat_participants_complete(
487 &self,
488 chat_id: InlineId,
489 ) -> BoxFuture<'static, StoreResult<bool>>;
490
491 fn history(&self, request: HistoryRequest) -> BoxFuture<'static, StoreResult<HistoryPage>>;
493
494 fn record_message(&self, message: MessageRecord) -> BoxFuture<'static, StoreResult<()>>;
496
497 fn record_message_deleted(
499 &self,
500 chat_id: InlineId,
501 message_id: InlineId,
502 ) -> BoxFuture<'static, StoreResult<()>>;
503
504 fn message_deleted(
506 &self,
507 chat_id: InlineId,
508 message_id: InlineId,
509 ) -> BoxFuture<'static, StoreResult<bool>>;
510
511 fn deleted_message_ids(
513 &self,
514 chat_id: InlineId,
515 ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
516
517 fn clear_chat_messages(
519 &self,
520 chat_id: InlineId,
521 before_date: Option<i64>,
522 ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
523
524 fn chat_ids_in_space(
526 &self,
527 space_id: InlineId,
528 ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
529
530 fn record_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>>;
532
533 fn remove_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>>;
535
536 fn replace_message_reactions(
538 &self,
539 chat_id: InlineId,
540 message_id: InlineId,
541 reactions: Vec<StoredReaction>,
542 ) -> BoxFuture<'static, StoreResult<()>>;
543
544 fn reactions(
546 &self,
547 chat_id: InlineId,
548 message_id: InlineId,
549 ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>>;
550
551 fn reactions_for_chat(
553 &self,
554 chat_id: InlineId,
555 ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>>;
556
557 fn reaction_snapshot_message_ids(
559 &self,
560 chat_id: InlineId,
561 ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
562
563 fn record_read_state(&self, state: StoredReadState) -> BoxFuture<'static, StoreResult<()>>;
565
566 fn read_state(
568 &self,
569 chat_id: InlineId,
570 ) -> BoxFuture<'static, StoreResult<Option<StoredReadState>>>;
571
572 fn record_transaction(
574 &self,
575 transaction: StoredTransaction,
576 ) -> BoxFuture<'static, StoreResult<()>>;
577
578 fn transaction(
580 &self,
581 transaction_id: TransactionId,
582 ) -> BoxFuture<'static, StoreResult<Option<StoredTransaction>>>;
583}
584
585#[derive(Clone, Debug, PartialEq, Eq)]
587pub struct StoredTransaction {
588 pub identity: TransactionIdentity,
590 pub state: TransactionState,
592 pub chat_id: Option<InlineId>,
594 pub message_id: Option<InlineId>,
596 pub failure: Option<ClientFailure>,
598 pub created_at: i64,
600 pub updated_at: i64,
602}
603
604impl StoredTransaction {
605 pub fn new(identity: TransactionIdentity, state: TransactionState) -> Self {
607 let now = now_seconds();
608 Self {
609 identity,
610 state,
611 chat_id: None,
612 message_id: None,
613 failure: None,
614 created_at: now,
615 updated_at: now,
616 }
617 }
618
619 pub fn with_chat_id(mut self, chat_id: InlineId) -> Self {
621 self.chat_id = Some(chat_id);
622 self
623 }
624
625 pub fn with_message_id(mut self, message_id: InlineId) -> Self {
627 self.message_id = Some(message_id);
628 self.identity.final_message_id = Some(message_id);
629 self
630 }
631
632 pub fn with_failure(mut self, failure: ClientFailure) -> Self {
634 self.failure = Some(failure);
635 self
636 }
637
638 pub fn event(&self) -> TransactionEvent {
640 TransactionEvent {
641 identity: self.identity.clone(),
642 state: self.state,
643 failure: self.failure.clone(),
644 }
645 }
646}
647
648#[derive(Clone, Debug, Default)]
650pub struct InMemoryStore {
651 state: Arc<Mutex<InMemoryStoreState>>,
652}
653
654#[derive(Debug, Default)]
655struct InMemoryStoreState {
656 session: Option<StoredSession>,
657 sync_state: SyncState,
658 sync_buckets: HashMap<SyncBucketKey, SyncBucketState>,
659 pending_sync_batches: HashMap<SyncBucketKey, PendingSyncBatch>,
660 client_event_outbox: Vec<ClientEventDelivery>,
661 next_client_event_id: u64,
662 dialogs: Vec<DialogRecord>,
663 chat_tombstones: HashSet<i64>,
664 users: HashMap<i64, UserRecord>,
665 spaces: HashMap<i64, SpaceRecord>,
666 space_members: HashMap<(i64, i64), SpaceMemberRecord>,
667 user_settings: Option<UserSettingsRecord>,
668 participants: HashMap<i64, HashMap<i64, ChatParticipantRecord>>,
669 participant_snapshots: HashSet<i64>,
670 messages: HashMap<i64, Vec<MessageRecord>>,
671 message_tombstones: HashSet<(i64, i64)>,
672 reactions: HashMap<(i64, i64, i64, String), StoredReaction>,
673 reaction_snapshots: HashSet<(i64, i64)>,
674 read_states: HashMap<i64, StoredReadState>,
675 transactions: HashMap<String, StoredTransaction>,
676}
677
678fn append_memory_client_events(
679 state: &mut InMemoryStoreState,
680 events: Vec<ClientEvent>,
681) -> Vec<ClientEventDelivery> {
682 events
683 .into_iter()
684 .map(|event| {
685 if event.reliability() == EventReliability::BestEffort {
686 return ClientEventDelivery::ephemeral(event);
687 }
688 state.next_client_event_id = state.next_client_event_id.saturating_add(1).max(1);
689 let delivery = ClientEventDelivery {
690 delivery_id: Some(state.next_client_event_id),
691 event,
692 };
693 state.client_event_outbox.push(delivery.clone());
694 delivery
695 })
696 .collect()
697}
698
699impl InMemoryStore {
700 pub fn new() -> Self {
702 Self::default()
703 }
704
705 pub fn upsert_dialog(&self, dialog: DialogRecord) {
707 let mut state = self.state.lock().expect("in-memory store poisoned");
708 state.chat_tombstones.remove(&dialog.chat_id.get());
709 upsert_dialog(&mut state.dialogs, dialog);
710 }
711
712 pub fn upsert_user(&self, user: UserRecord) {
714 let mut state = self.state.lock().expect("in-memory store poisoned");
715 state.users.insert(user.user_id.get(), user);
716 }
717
718 pub fn insert_message(&self, message: MessageRecord) {
720 let mut state = self.state.lock().expect("in-memory store poisoned");
721 state
722 .message_tombstones
723 .remove(&(message.chat_id.get(), message.message_id.get()));
724 insert_message(&mut state.messages, message);
725 }
726}
727
728impl ClientStore for InMemoryStore {
729 fn save_session(&self, session: StoredSession) -> BoxFuture<'static, StoreResult<()>> {
730 let store = self.clone();
731 Box::pin(async move {
732 store
733 .state
734 .lock()
735 .expect("in-memory store poisoned")
736 .session = Some(session);
737 Ok(())
738 })
739 }
740
741 fn load_session(&self) -> BoxFuture<'static, StoreResult<Option<StoredSession>>> {
742 let store = self.clone();
743 Box::pin(async move {
744 Ok(store
745 .state
746 .lock()
747 .expect("in-memory store poisoned")
748 .session
749 .clone())
750 })
751 }
752
753 fn clear_session(&self) -> BoxFuture<'static, StoreResult<()>> {
754 let store = self.clone();
755 Box::pin(async move {
756 store
757 .state
758 .lock()
759 .expect("in-memory store poisoned")
760 .session = None;
761 Ok(())
762 })
763 }
764
765 fn clear_account_data(&self) -> BoxFuture<'static, StoreResult<()>> {
766 let store = self.clone();
767 Box::pin(async move {
768 let mut state = store.state.lock().expect("in-memory store poisoned");
769 let session = state.session.take();
770 *state = InMemoryStoreState::default();
771 state.session = session;
772 Ok(())
773 })
774 }
775
776 fn sync_state(&self) -> BoxFuture<'static, StoreResult<SyncState>> {
777 let store = self.clone();
778 Box::pin(async move {
779 Ok(store
780 .state
781 .lock()
782 .expect("in-memory store poisoned")
783 .sync_state)
784 })
785 }
786
787 fn save_sync_state(&self, state: SyncState) -> BoxFuture<'static, StoreResult<()>> {
788 let store = self.clone();
789 Box::pin(async move {
790 store
791 .state
792 .lock()
793 .expect("in-memory store poisoned")
794 .sync_state = state;
795 Ok(())
796 })
797 }
798
799 fn clear_sync_state(&self) -> BoxFuture<'static, StoreResult<()>> {
800 let store = self.clone();
801 Box::pin(async move {
802 let mut state = store.state.lock().expect("in-memory store poisoned");
803 state.sync_state = SyncState::default();
804 state.sync_buckets.clear();
805 Ok(())
806 })
807 }
808
809 fn sync_bucket_state(
810 &self,
811 key: SyncBucketKey,
812 ) -> BoxFuture<'static, StoreResult<SyncBucketState>> {
813 let store = self.clone();
814 Box::pin(async move {
815 Ok(store
816 .state
817 .lock()
818 .expect("in-memory store poisoned")
819 .sync_buckets
820 .get(&key)
821 .copied()
822 .unwrap_or_default())
823 })
824 }
825
826 fn save_sync_bucket_state(
827 &self,
828 key: SyncBucketKey,
829 state: SyncBucketState,
830 ) -> BoxFuture<'static, StoreResult<()>> {
831 let store = self.clone();
832 Box::pin(async move {
833 store
834 .state
835 .lock()
836 .expect("in-memory store poisoned")
837 .sync_buckets
838 .insert(key, state);
839 Ok(())
840 })
841 }
842
843 fn save_sync_bucket_states(
844 &self,
845 states: Vec<(SyncBucketKey, SyncBucketState)>,
846 ) -> BoxFuture<'static, StoreResult<()>> {
847 let store = self.clone();
848 Box::pin(async move {
849 let mut stored = store.state.lock().expect("in-memory store poisoned");
850 stored.sync_buckets.extend(states);
851 Ok(())
852 })
853 }
854
855 fn remove_sync_bucket_state(&self, key: SyncBucketKey) -> BoxFuture<'static, StoreResult<()>> {
856 let store = self.clone();
857 Box::pin(async move {
858 store
859 .state
860 .lock()
861 .expect("in-memory store poisoned")
862 .sync_buckets
863 .remove(&key);
864 Ok(())
865 })
866 }
867
868 fn save_pending_sync_batch(
869 &self,
870 batch: PendingSyncBatch,
871 ) -> BoxFuture<'static, StoreResult<()>> {
872 let store = self.clone();
873 Box::pin(async move {
874 store
875 .state
876 .lock()
877 .expect("in-memory store poisoned")
878 .pending_sync_batches
879 .insert(batch.key, batch);
880 Ok(())
881 })
882 }
883
884 fn pending_sync_batches(&self) -> BoxFuture<'static, StoreResult<Vec<PendingSyncBatch>>> {
885 let store = self.clone();
886 Box::pin(async move {
887 let mut batches = store
888 .state
889 .lock()
890 .expect("in-memory store poisoned")
891 .pending_sync_batches
892 .values()
893 .cloned()
894 .collect::<Vec<_>>();
895 batches.sort_by_key(|batch| sync_bucket_sort_key(batch.key));
896 Ok(batches)
897 })
898 }
899
900 fn discard_pending_sync_batch(
901 &self,
902 key: SyncBucketKey,
903 ) -> BoxFuture<'static, StoreResult<()>> {
904 let store = self.clone();
905 Box::pin(async move {
906 store
907 .state
908 .lock()
909 .expect("in-memory store poisoned")
910 .pending_sync_batches
911 .remove(&key);
912 Ok(())
913 })
914 }
915
916 fn commit_pending_sync_batch(
917 &self,
918 key: SyncBucketKey,
919 state: SyncBucketState,
920 ) -> BoxFuture<'static, StoreResult<()>> {
921 let store = self.clone();
922 Box::pin(async move {
923 let mut stored = store.state.lock().expect("in-memory store poisoned");
924 stored.sync_buckets.insert(key, state);
925 stored.pending_sync_batches.remove(&key);
926 Ok(())
927 })
928 }
929
930 fn commit_pending_sync_batch_with_events(
931 &self,
932 key: SyncBucketKey,
933 state: SyncBucketState,
934 events: Vec<ClientEvent>,
935 ) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
936 let store = self.clone();
937 Box::pin(async move {
938 let mut stored = store.state.lock().expect("in-memory store poisoned");
939 let deliveries = append_memory_client_events(&mut stored, events);
940 stored.sync_buckets.insert(key, state);
941 stored.pending_sync_batches.remove(&key);
942 Ok(deliveries)
943 })
944 }
945
946 fn append_client_events(
947 &self,
948 events: Vec<ClientEvent>,
949 ) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
950 let store = self.clone();
951 Box::pin(async move {
952 let mut stored = store.state.lock().expect("in-memory store poisoned");
953 Ok(append_memory_client_events(&mut stored, events))
954 })
955 }
956
957 fn pending_client_events(&self) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
958 let store = self.clone();
959 Box::pin(async move {
960 Ok(store
961 .state
962 .lock()
963 .expect("in-memory store poisoned")
964 .client_event_outbox
965 .clone())
966 })
967 }
968
969 fn acknowledge_client_event(&self, delivery_id: u64) -> BoxFuture<'static, StoreResult<()>> {
970 let store = self.clone();
971 Box::pin(async move {
972 store
973 .state
974 .lock()
975 .expect("in-memory store poisoned")
976 .client_event_outbox
977 .retain(|delivery| delivery.delivery_id != Some(delivery_id));
978 Ok(())
979 })
980 }
981
982 fn dialogs(&self, request: DialogsRequest) -> BoxFuture<'static, StoreResult<DialogsPage>> {
983 let store = self.clone();
984 Box::pin(async move {
985 let state = store.state.lock().expect("in-memory store poisoned");
986 let limit = request.limit.unwrap_or(50).max(1) as usize;
987 let (dialogs, next_cursor) = match request.order {
988 DialogsOrder::RecentActivity => {
989 let start = parse_cursor(request.cursor.as_deref())?;
990 let dialogs = state
991 .dialogs
992 .iter()
993 .skip(start)
994 .take(limit)
995 .cloned()
996 .collect::<Vec<_>>();
997 let next = start + dialogs.len();
998 (
999 dialogs,
1000 (next < state.dialogs.len()).then(|| next.to_string()),
1001 )
1002 }
1003 DialogsOrder::StableChatId => {
1004 let after = parse_stable_dialog_cursor(request.cursor.as_deref())?;
1005 let mut candidates = state
1006 .dialogs
1007 .iter()
1008 .filter(|dialog| dialog.chat_id.get() > after)
1009 .cloned()
1010 .collect::<Vec<_>>();
1011 candidates.sort_by_key(|dialog| dialog.chat_id.get());
1012 let has_more = candidates.len() > limit;
1013 candidates.truncate(limit);
1014 let next = has_more
1015 .then(|| {
1016 candidates
1017 .last()
1018 .map(|dialog| dialog.chat_id.get().to_string())
1019 })
1020 .flatten();
1021 (candidates, next)
1022 }
1023 };
1024 let dialogs = dialogs
1025 .into_iter()
1026 .map(|dialog| {
1027 let chat_id = dialog.chat_id;
1028 dialog_with_synced_through(
1029 dialog,
1030 max_message_id_from_memory(&state.messages, chat_id),
1031 )
1032 })
1033 .collect::<Vec<_>>();
1034 let users = all_users_from_memory(&state.users);
1035 Ok(DialogsPage {
1036 dialogs,
1037 users,
1038 next_cursor,
1039 })
1040 })
1041 }
1042
1043 fn dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<Option<DialogRecord>>> {
1044 let store = self.clone();
1045 Box::pin(async move {
1046 let state = store.state.lock().expect("in-memory store poisoned");
1047 Ok(state
1048 .dialogs
1049 .iter()
1050 .find(|dialog| dialog.chat_id == chat_id)
1051 .cloned()
1052 .map(|dialog| {
1053 dialog_with_synced_through(
1054 dialog,
1055 max_message_id_from_memory(&state.messages, chat_id),
1056 )
1057 }))
1058 })
1059 }
1060
1061 fn record_dialog(&self, dialog: DialogRecord) -> BoxFuture<'static, StoreResult<()>> {
1062 let store = self.clone();
1063 Box::pin(async move {
1064 store.upsert_dialog(dialog);
1065 Ok(())
1066 })
1067 }
1068
1069 fn remove_dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<()>> {
1070 let store = self.clone();
1071 Box::pin(async move {
1072 let mut state = store.state.lock().expect("in-memory store poisoned");
1073 state.dialogs.retain(|dialog| dialog.chat_id != chat_id);
1074 if let Some(messages) = state.messages.remove(&chat_id.get()) {
1075 state.message_tombstones.extend(
1076 messages
1077 .into_iter()
1078 .map(|message| (chat_id.get(), message.message_id.get())),
1079 );
1080 }
1081 state.participants.remove(&chat_id.get());
1082 state.participant_snapshots.remove(&chat_id.get());
1083 state
1084 .reactions
1085 .retain(|_, reaction| reaction.chat_id != chat_id);
1086 state
1087 .reaction_snapshots
1088 .retain(|(stored_chat_id, _)| *stored_chat_id != chat_id.get());
1089 state.read_states.remove(&chat_id.get());
1090 state.chat_tombstones.insert(chat_id.get());
1091 Ok(())
1092 })
1093 }
1094
1095 fn deleted_chat_ids(&self) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1096 let store = self.clone();
1097 Box::pin(async move {
1098 let mut ids = store
1099 .state
1100 .lock()
1101 .expect("in-memory store poisoned")
1102 .chat_tombstones
1103 .iter()
1104 .copied()
1105 .map(InlineId::new)
1106 .collect::<Vec<_>>();
1107 ids.sort_by_key(|id| id.get());
1108 Ok(ids)
1109 })
1110 }
1111
1112 fn record_users(&self, users: Vec<UserRecord>) -> BoxFuture<'static, StoreResult<()>> {
1113 let store = self.clone();
1114 Box::pin(async move {
1115 let mut state = store.state.lock().expect("in-memory store poisoned");
1116 for user in users {
1117 state.users.insert(user.user_id.get(), user);
1118 }
1119 Ok(())
1120 })
1121 }
1122
1123 fn user(&self, user_id: InlineId) -> BoxFuture<'static, StoreResult<Option<UserRecord>>> {
1124 let store = self.clone();
1125 Box::pin(async move {
1126 Ok(store
1127 .state
1128 .lock()
1129 .expect("in-memory store poisoned")
1130 .users
1131 .get(&user_id.get())
1132 .cloned())
1133 })
1134 }
1135
1136 fn record_space(&self, space: SpaceRecord) -> BoxFuture<'static, StoreResult<()>> {
1137 let store = self.clone();
1138 Box::pin(async move {
1139 store
1140 .state
1141 .lock()
1142 .expect("in-memory store poisoned")
1143 .spaces
1144 .insert(space.space_id.get(), space);
1145 Ok(())
1146 })
1147 }
1148
1149 fn space(&self, space_id: InlineId) -> BoxFuture<'static, StoreResult<Option<SpaceRecord>>> {
1150 let store = self.clone();
1151 Box::pin(async move {
1152 Ok(store
1153 .state
1154 .lock()
1155 .expect("in-memory store poisoned")
1156 .spaces
1157 .get(&space_id.get())
1158 .cloned())
1159 })
1160 }
1161
1162 fn record_space_member(
1163 &self,
1164 member: SpaceMemberRecord,
1165 ) -> BoxFuture<'static, StoreResult<()>> {
1166 let store = self.clone();
1167 Box::pin(async move {
1168 store
1169 .state
1170 .lock()
1171 .expect("in-memory store poisoned")
1172 .space_members
1173 .insert((member.space_id.get(), member.user_id.get()), member);
1174 Ok(())
1175 })
1176 }
1177
1178 fn record_space_members(
1179 &self,
1180 space_id: InlineId,
1181 members: Vec<SpaceMemberRecord>,
1182 ) -> BoxFuture<'static, StoreResult<()>> {
1183 let store = self.clone();
1184 Box::pin(async move {
1185 if members.iter().any(|member| member.space_id != space_id) {
1186 return Err(StoreError::new(
1187 ClientErrorCategory::InvalidInput,
1188 "space member snapshot contains a different space id",
1189 ));
1190 }
1191 let mut state = store.state.lock().expect("in-memory store poisoned");
1192 state
1193 .space_members
1194 .retain(|(stored_space_id, _), _| *stored_space_id != space_id.get());
1195 for member in members {
1196 state
1197 .space_members
1198 .insert((space_id.get(), member.user_id.get()), member);
1199 }
1200 Ok(())
1201 })
1202 }
1203
1204 fn remove_space_member(
1205 &self,
1206 space_id: InlineId,
1207 user_id: InlineId,
1208 ) -> BoxFuture<'static, StoreResult<()>> {
1209 let store = self.clone();
1210 Box::pin(async move {
1211 store
1212 .state
1213 .lock()
1214 .expect("in-memory store poisoned")
1215 .space_members
1216 .remove(&(space_id.get(), user_id.get()));
1217 Ok(())
1218 })
1219 }
1220
1221 fn space_members(
1222 &self,
1223 space_id: InlineId,
1224 ) -> BoxFuture<'static, StoreResult<Vec<SpaceMemberRecord>>> {
1225 let store = self.clone();
1226 Box::pin(async move {
1227 let mut members = store
1228 .state
1229 .lock()
1230 .expect("in-memory store poisoned")
1231 .space_members
1232 .iter()
1233 .filter(|((stored_space_id, _), _)| *stored_space_id == space_id.get())
1234 .map(|(_, member)| member.clone())
1235 .collect::<Vec<_>>();
1236 members.sort_by_key(|member| member.user_id.get());
1237 Ok(members)
1238 })
1239 }
1240
1241 fn record_user_settings(
1242 &self,
1243 settings: UserSettingsRecord,
1244 ) -> BoxFuture<'static, StoreResult<()>> {
1245 let store = self.clone();
1246 Box::pin(async move {
1247 store
1248 .state
1249 .lock()
1250 .expect("in-memory store poisoned")
1251 .user_settings = Some(settings);
1252 Ok(())
1253 })
1254 }
1255
1256 fn user_settings(&self) -> BoxFuture<'static, StoreResult<Option<UserSettingsRecord>>> {
1257 let store = self.clone();
1258 Box::pin(async move {
1259 Ok(store
1260 .state
1261 .lock()
1262 .expect("in-memory store poisoned")
1263 .user_settings
1264 .clone())
1265 })
1266 }
1267
1268 fn record_chat_participants(
1269 &self,
1270 chat_id: InlineId,
1271 participants: Vec<ChatParticipantRecord>,
1272 ) -> BoxFuture<'static, StoreResult<()>> {
1273 let store = self.clone();
1274 Box::pin(async move {
1275 let snapshot = participants
1276 .into_iter()
1277 .map(|participant| (participant.user_id.get(), participant))
1278 .collect();
1279 let mut state = store.state.lock().expect("in-memory store poisoned");
1280 state.participants.insert(chat_id.get(), snapshot);
1281 state.participant_snapshots.insert(chat_id.get());
1282 Ok(())
1283 })
1284 }
1285
1286 fn record_chat_participant(
1287 &self,
1288 chat_id: InlineId,
1289 participant: ChatParticipantRecord,
1290 ) -> BoxFuture<'static, StoreResult<()>> {
1291 let store = self.clone();
1292 Box::pin(async move {
1293 store
1294 .state
1295 .lock()
1296 .expect("in-memory store poisoned")
1297 .participants
1298 .entry(chat_id.get())
1299 .or_default()
1300 .insert(participant.user_id.get(), participant);
1301 Ok(())
1302 })
1303 }
1304
1305 fn remove_chat_participant(
1306 &self,
1307 chat_id: InlineId,
1308 user_id: InlineId,
1309 ) -> BoxFuture<'static, StoreResult<()>> {
1310 let store = self.clone();
1311 Box::pin(async move {
1312 if let Some(participants) = store
1313 .state
1314 .lock()
1315 .expect("in-memory store poisoned")
1316 .participants
1317 .get_mut(&chat_id.get())
1318 {
1319 participants.remove(&user_id.get());
1320 }
1321 Ok(())
1322 })
1323 }
1324
1325 fn chat_participants(
1326 &self,
1327 chat_id: InlineId,
1328 ) -> BoxFuture<'static, StoreResult<Vec<ChatParticipantRecord>>> {
1329 let store = self.clone();
1330 Box::pin(async move {
1331 let mut participants = store
1332 .state
1333 .lock()
1334 .expect("in-memory store poisoned")
1335 .participants
1336 .get(&chat_id.get())
1337 .map(|participants| participants.values().cloned().collect::<Vec<_>>())
1338 .unwrap_or_default();
1339 sort_participants(&mut participants);
1340 Ok(participants)
1341 })
1342 }
1343
1344 fn chat_participants_complete(
1345 &self,
1346 chat_id: InlineId,
1347 ) -> BoxFuture<'static, StoreResult<bool>> {
1348 let store = self.clone();
1349 Box::pin(async move {
1350 Ok(store
1351 .state
1352 .lock()
1353 .expect("in-memory store poisoned")
1354 .participant_snapshots
1355 .contains(&chat_id.get()))
1356 })
1357 }
1358
1359 fn history(&self, request: HistoryRequest) -> BoxFuture<'static, StoreResult<HistoryPage>> {
1360 let store = self.clone();
1361 Box::pin(async move {
1362 let state = store.state.lock().expect("in-memory store poisoned");
1363 let messages = state
1364 .messages
1365 .get(&request.chat_id.get())
1366 .cloned()
1367 .unwrap_or_default();
1368 if request.before_message_id.is_some() && request.after_message_id.is_some() {
1369 return Err(StoreError::new(
1370 ClientErrorCategory::InvalidInput,
1371 "history request cannot specify both before_message_id and after_message_id",
1372 ));
1373 }
1374 let limit = request.limit.unwrap_or(50).max(1) as usize;
1375 let (messages, has_more) = select_history_window(
1376 messages,
1377 limit,
1378 request.before_message_id,
1379 request.after_message_id,
1380 );
1381 let users = users_for_messages_from_memory(&state.users, &messages);
1382 Ok(HistoryPage {
1383 messages,
1384 users,
1385 has_more,
1386 next_cursor: None,
1387 })
1388 })
1389 }
1390
1391 fn record_message(&self, message: MessageRecord) -> BoxFuture<'static, StoreResult<()>> {
1392 let store = self.clone();
1393 Box::pin(async move {
1394 let mut state = store.state.lock().expect("in-memory store poisoned");
1395 state
1396 .message_tombstones
1397 .remove(&(message.chat_id.get(), message.message_id.get()));
1398 insert_message(&mut state.messages, message);
1399 Ok(())
1400 })
1401 }
1402
1403 fn record_message_deleted(
1404 &self,
1405 chat_id: InlineId,
1406 message_id: InlineId,
1407 ) -> BoxFuture<'static, StoreResult<()>> {
1408 let store = self.clone();
1409 Box::pin(async move {
1410 let mut state = store.state.lock().expect("in-memory store poisoned");
1411 if let Some(messages) = state.messages.get_mut(&chat_id.get()) {
1412 messages.retain(|message| message.message_id != message_id);
1413 }
1414 state.reactions.retain(|_, reaction| {
1415 reaction.chat_id != chat_id || reaction.message_id != message_id
1416 });
1417 state
1418 .reaction_snapshots
1419 .remove(&(chat_id.get(), message_id.get()));
1420 state
1421 .message_tombstones
1422 .insert((chat_id.get(), message_id.get()));
1423 Ok(())
1424 })
1425 }
1426
1427 fn message_deleted(
1428 &self,
1429 chat_id: InlineId,
1430 message_id: InlineId,
1431 ) -> BoxFuture<'static, StoreResult<bool>> {
1432 let store = self.clone();
1433 Box::pin(async move {
1434 Ok(store
1435 .state
1436 .lock()
1437 .expect("in-memory store poisoned")
1438 .message_tombstones
1439 .contains(&(chat_id.get(), message_id.get())))
1440 })
1441 }
1442
1443 fn deleted_message_ids(
1444 &self,
1445 chat_id: InlineId,
1446 ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1447 let store = self.clone();
1448 Box::pin(async move {
1449 let mut ids = store
1450 .state
1451 .lock()
1452 .expect("in-memory store poisoned")
1453 .message_tombstones
1454 .iter()
1455 .filter(|(stored_chat_id, _)| *stored_chat_id == chat_id.get())
1456 .map(|(_, message_id)| InlineId::new(*message_id))
1457 .collect::<Vec<_>>();
1458 ids.sort_by_key(|id| id.get());
1459 Ok(ids)
1460 })
1461 }
1462
1463 fn clear_chat_messages(
1464 &self,
1465 chat_id: InlineId,
1466 before_date: Option<i64>,
1467 ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1468 let store = self.clone();
1469 Box::pin(async move {
1470 let mut state = store.state.lock().expect("in-memory store poisoned");
1471 let mut removed = Vec::new();
1472 if let Some(messages) = state.messages.get_mut(&chat_id.get()) {
1473 messages.retain(|message| {
1474 let should_remove = before_date.is_none_or(|date| message.timestamp < date);
1475 if should_remove {
1476 removed.push(message.message_id);
1477 }
1478 !should_remove
1479 });
1480 }
1481 let removed_ids = removed.iter().map(|id| id.get()).collect::<HashSet<_>>();
1482 state.reactions.retain(|_, reaction| {
1483 reaction.chat_id != chat_id || !removed_ids.contains(&reaction.message_id.get())
1484 });
1485 state
1486 .reaction_snapshots
1487 .retain(|(stored_chat_id, message_id)| {
1488 *stored_chat_id != chat_id.get() || !removed_ids.contains(message_id)
1489 });
1490 state.message_tombstones.extend(
1491 removed_ids
1492 .iter()
1493 .map(|message_id| (chat_id.get(), *message_id)),
1494 );
1495 let last_message_id = state.messages.get(&chat_id.get()).and_then(|messages| {
1496 messages
1497 .iter()
1498 .map(|message| message.message_id)
1499 .max_by_key(|id| id.get())
1500 });
1501 if let Some(dialog) = state
1502 .dialogs
1503 .iter_mut()
1504 .find(|dialog| dialog.chat_id == chat_id)
1505 {
1506 dialog.last_message_id = last_message_id;
1507 }
1508 removed.sort_by_key(|id| id.get());
1509 Ok(removed)
1510 })
1511 }
1512
1513 fn chat_ids_in_space(
1514 &self,
1515 space_id: InlineId,
1516 ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1517 let store = self.clone();
1518 Box::pin(async move {
1519 let mut chat_ids = store
1520 .state
1521 .lock()
1522 .expect("in-memory store poisoned")
1523 .dialogs
1524 .iter()
1525 .filter(|dialog| dialog.space_id == Some(space_id))
1526 .map(|dialog| dialog.chat_id)
1527 .collect::<Vec<_>>();
1528 chat_ids.sort_by_key(|id| id.get());
1529 Ok(chat_ids)
1530 })
1531 }
1532
1533 fn record_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>> {
1534 let store = self.clone();
1535 Box::pin(async move {
1536 let key = reaction_key(&reaction);
1537 store
1538 .state
1539 .lock()
1540 .expect("in-memory store poisoned")
1541 .reactions
1542 .insert(key, reaction);
1543 Ok(())
1544 })
1545 }
1546
1547 fn remove_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>> {
1548 let store = self.clone();
1549 Box::pin(async move {
1550 store
1551 .state
1552 .lock()
1553 .expect("in-memory store poisoned")
1554 .reactions
1555 .remove(&reaction_key(&reaction));
1556 Ok(())
1557 })
1558 }
1559
1560 fn replace_message_reactions(
1561 &self,
1562 chat_id: InlineId,
1563 message_id: InlineId,
1564 reactions: Vec<StoredReaction>,
1565 ) -> BoxFuture<'static, StoreResult<()>> {
1566 let store = self.clone();
1567 Box::pin(async move {
1568 let mut state = store.state.lock().expect("in-memory store poisoned");
1569 state.reactions.retain(|_, reaction| {
1570 reaction.chat_id != chat_id || reaction.message_id != message_id
1571 });
1572 for reaction in reactions {
1573 state.reactions.insert(reaction_key(&reaction), reaction);
1574 }
1575 state
1576 .reaction_snapshots
1577 .insert((chat_id.get(), message_id.get()));
1578 Ok(())
1579 })
1580 }
1581
1582 fn reactions(
1583 &self,
1584 chat_id: InlineId,
1585 message_id: InlineId,
1586 ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>> {
1587 let store = self.clone();
1588 Box::pin(async move {
1589 let mut reactions = store
1590 .state
1591 .lock()
1592 .expect("in-memory store poisoned")
1593 .reactions
1594 .values()
1595 .filter(|reaction| reaction.chat_id == chat_id && reaction.message_id == message_id)
1596 .cloned()
1597 .collect::<Vec<_>>();
1598 sort_reactions(&mut reactions);
1599 Ok(reactions)
1600 })
1601 }
1602
1603 fn reactions_for_chat(
1604 &self,
1605 chat_id: InlineId,
1606 ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>> {
1607 let store = self.clone();
1608 Box::pin(async move {
1609 let mut reactions = store
1610 .state
1611 .lock()
1612 .expect("in-memory store poisoned")
1613 .reactions
1614 .values()
1615 .filter(|reaction| reaction.chat_id == chat_id)
1616 .cloned()
1617 .collect::<Vec<_>>();
1618 reactions.sort_by(|left, right| {
1619 (
1620 left.message_id.get(),
1621 left.user_id.get(),
1622 left.reaction.as_str(),
1623 )
1624 .cmp(&(
1625 right.message_id.get(),
1626 right.user_id.get(),
1627 right.reaction.as_str(),
1628 ))
1629 });
1630 Ok(reactions)
1631 })
1632 }
1633
1634 fn reaction_snapshot_message_ids(
1635 &self,
1636 chat_id: InlineId,
1637 ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1638 let store = self.clone();
1639 Box::pin(async move {
1640 let mut ids = store
1641 .state
1642 .lock()
1643 .expect("in-memory store poisoned")
1644 .reaction_snapshots
1645 .iter()
1646 .filter(|(stored_chat_id, _)| *stored_chat_id == chat_id.get())
1647 .map(|(_, message_id)| InlineId::new(*message_id))
1648 .collect::<Vec<_>>();
1649 ids.sort_by_key(|id| id.get());
1650 Ok(ids)
1651 })
1652 }
1653
1654 fn record_read_state(&self, state: StoredReadState) -> BoxFuture<'static, StoreResult<()>> {
1655 let store = self.clone();
1656 Box::pin(async move {
1657 store
1658 .state
1659 .lock()
1660 .expect("in-memory store poisoned")
1661 .read_states
1662 .insert(state.chat_id.get(), state);
1663 Ok(())
1664 })
1665 }
1666
1667 fn read_state(
1668 &self,
1669 chat_id: InlineId,
1670 ) -> BoxFuture<'static, StoreResult<Option<StoredReadState>>> {
1671 let store = self.clone();
1672 Box::pin(async move {
1673 Ok(store
1674 .state
1675 .lock()
1676 .expect("in-memory store poisoned")
1677 .read_states
1678 .get(&chat_id.get())
1679 .copied())
1680 })
1681 }
1682
1683 fn record_transaction(
1684 &self,
1685 transaction: StoredTransaction,
1686 ) -> BoxFuture<'static, StoreResult<()>> {
1687 let store = self.clone();
1688 Box::pin(async move {
1689 let mut state = store.state.lock().expect("in-memory store poisoned");
1690 state.transactions.insert(
1691 transaction.identity.transaction_id.as_str().to_owned(),
1692 transaction,
1693 );
1694 Ok(())
1695 })
1696 }
1697
1698 fn transaction(
1699 &self,
1700 transaction_id: TransactionId,
1701 ) -> BoxFuture<'static, StoreResult<Option<StoredTransaction>>> {
1702 let store = self.clone();
1703 Box::pin(async move {
1704 Ok(store
1705 .state
1706 .lock()
1707 .expect("in-memory store poisoned")
1708 .transactions
1709 .get(transaction_id.as_str())
1710 .cloned())
1711 })
1712 }
1713}
1714
1715#[derive(Clone)]
1717pub struct SqliteStore {
1718 connection: Arc<Mutex<Connection>>,
1719}
1720
1721impl fmt::Debug for SqliteStore {
1722 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1723 f.debug_struct("SqliteStore")
1724 .field("connection", &"<sqlite>")
1725 .finish()
1726 }
1727}
1728
1729impl SqliteStore {
1730 pub fn open(path: impl AsRef<Path>) -> StoreResult<Self> {
1732 let path = path.as_ref();
1733 if let Some(parent) = path
1734 .parent()
1735 .filter(|parent| !parent.as_os_str().is_empty())
1736 {
1737 std::fs::create_dir_all(parent).map_err(|error| {
1738 StoreError::internal(format!("create store directory: {error}"))
1739 })?;
1740 }
1741 prepare_private_sqlite_file(path)?;
1742 let connection = Connection::open(path).map_err(sqlite_error)?;
1743 Self::from_connection(connection)
1744 }
1745
1746 pub fn open_in_memory() -> StoreResult<Self> {
1748 let connection = Connection::open_in_memory().map_err(sqlite_error)?;
1749 Self::from_connection(connection)
1750 }
1751
1752 fn from_connection(connection: Connection) -> StoreResult<Self> {
1753 migrate_sqlite(&connection)?;
1754 Ok(Self {
1755 connection: Arc::new(Mutex::new(connection)),
1756 })
1757 }
1758
1759 pub fn upsert_dialog(&self, dialog: DialogRecord) -> StoreResult<()> {
1761 let connection = self.connection.lock().expect("sqlite store poisoned");
1762 upsert_sqlite_dialog(&connection, dialog)
1763 }
1764
1765 pub fn upsert_user(&self, user: UserRecord) -> StoreResult<()> {
1767 let connection = self.connection.lock().expect("sqlite store poisoned");
1768 upsert_sqlite_user(&connection, user)
1769 }
1770
1771 pub fn insert_message(&self, message: MessageRecord) -> StoreResult<()> {
1773 self.record_message_sync(message)
1774 }
1775
1776 fn save_session_sync(&self, session: StoredSession) -> StoreResult<()> {
1777 let auth_json = serde_json::to_string(&session.auth)
1778 .map_err(|error| StoreError::internal(format!("encode session auth: {error}")))?;
1779 let connection = self.connection.lock().expect("sqlite store poisoned");
1780 connection
1781 .execute(
1782 "INSERT INTO sessions (id, auth_json, account_namespace, updated_at)
1783 VALUES (1, ?1, ?2, ?3)
1784 ON CONFLICT(id) DO UPDATE SET
1785 auth_json = excluded.auth_json,
1786 account_namespace = excluded.account_namespace,
1787 updated_at = excluded.updated_at",
1788 params![auth_json, session.account_namespace, now_seconds()],
1789 )
1790 .map_err(sqlite_error)?;
1791 Ok(())
1792 }
1793
1794 fn load_session_sync(&self) -> StoreResult<Option<StoredSession>> {
1795 let connection = self.connection.lock().expect("sqlite store poisoned");
1796 let row = connection
1797 .query_row(
1798 "SELECT auth_json, account_namespace FROM sessions WHERE id = 1",
1799 [],
1800 |row| Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?)),
1801 )
1802 .optional()
1803 .map_err(sqlite_error)?;
1804
1805 row.map(|(auth_json, account_namespace)| {
1806 let auth = serde_json::from_str::<AuthCredential>(&auth_json)
1807 .map_err(|error| StoreError::internal(format!("decode session auth: {error}")))?;
1808 Ok(StoredSession {
1809 auth,
1810 account_namespace,
1811 })
1812 })
1813 .transpose()
1814 }
1815
1816 fn clear_session_sync(&self) -> StoreResult<()> {
1817 let connection = self.connection.lock().expect("sqlite store poisoned");
1818 connection
1819 .execute("DELETE FROM sessions WHERE id = 1", [])
1820 .map_err(sqlite_error)?;
1821 Ok(())
1822 }
1823
1824 fn clear_account_data_sync(&self) -> StoreResult<()> {
1825 let mut connection = self.connection.lock().expect("sqlite store poisoned");
1826 let transaction = connection.transaction().map_err(sqlite_error)?;
1827 for table in [
1828 "sync_state",
1829 "sync_buckets",
1830 "pending_sync_batches",
1831 "client_event_outbox",
1832 "dialogs",
1833 "chat_tombstones",
1834 "users",
1835 "spaces",
1836 "space_members",
1837 "user_settings",
1838 "chat_participants",
1839 "chat_participant_snapshots",
1840 "messages",
1841 "message_tombstones",
1842 "reactions",
1843 "reaction_snapshots",
1844 "read_states",
1845 "transactions",
1846 ] {
1847 transaction
1848 .execute(&format!("DELETE FROM {table}"), [])
1849 .map_err(sqlite_error)?;
1850 }
1851 transaction.commit().map_err(sqlite_error)?;
1852 Ok(())
1853 }
1854
1855 fn sync_state_sync(&self) -> StoreResult<SyncState> {
1856 let connection = self.connection.lock().expect("sqlite store poisoned");
1857 let last_sync_date = connection
1858 .query_row(
1859 "SELECT last_sync_date FROM sync_state WHERE id = 1",
1860 [],
1861 |row| row.get::<_, i64>(0),
1862 )
1863 .optional()
1864 .map_err(sqlite_error)?
1865 .unwrap_or_default();
1866 Ok(SyncState { last_sync_date })
1867 }
1868
1869 fn save_sync_state_sync(&self, state: SyncState) -> StoreResult<()> {
1870 let connection = self.connection.lock().expect("sqlite store poisoned");
1871 connection
1872 .execute(
1873 "INSERT INTO sync_state (id, last_sync_date, updated_at)
1874 VALUES (1, ?1, ?2)
1875 ON CONFLICT(id) DO UPDATE SET
1876 last_sync_date = excluded.last_sync_date,
1877 updated_at = excluded.updated_at",
1878 params![state.last_sync_date, now_seconds()],
1879 )
1880 .map_err(sqlite_error)?;
1881 Ok(())
1882 }
1883
1884 fn clear_sync_state_sync(&self) -> StoreResult<()> {
1885 let mut connection = self.connection.lock().expect("sqlite store poisoned");
1886 let transaction = connection.transaction().map_err(sqlite_error)?;
1887 transaction
1888 .execute("DELETE FROM sync_state", [])
1889 .map_err(sqlite_error)?;
1890 transaction
1891 .execute("DELETE FROM sync_buckets", [])
1892 .map_err(sqlite_error)?;
1893 transaction.commit().map_err(sqlite_error)?;
1894 Ok(())
1895 }
1896
1897 fn sync_bucket_state_sync(&self, key: SyncBucketKey) -> StoreResult<SyncBucketState> {
1898 let (kind, entity_id) = sync_bucket_key_parts(key);
1899 let connection = self.connection.lock().expect("sqlite store poisoned");
1900 let state = connection
1901 .query_row(
1902 "SELECT seq, date FROM sync_buckets
1903 WHERE bucket_kind = ?1 AND entity_id = ?2",
1904 params![kind, entity_id],
1905 |row| {
1906 Ok(SyncBucketState {
1907 seq: row.get(0)?,
1908 date: row.get(1)?,
1909 })
1910 },
1911 )
1912 .optional()
1913 .map_err(sqlite_error)?
1914 .unwrap_or_default();
1915 Ok(state)
1916 }
1917
1918 fn save_sync_bucket_state_sync(
1919 &self,
1920 key: SyncBucketKey,
1921 state: SyncBucketState,
1922 ) -> StoreResult<()> {
1923 let (kind, entity_id) = sync_bucket_key_parts(key);
1924 let connection = self.connection.lock().expect("sqlite store poisoned");
1925 upsert_sync_bucket(&connection, kind, entity_id, state)
1926 }
1927
1928 fn save_sync_bucket_states_sync(
1929 &self,
1930 states: Vec<(SyncBucketKey, SyncBucketState)>,
1931 ) -> StoreResult<()> {
1932 let mut connection = self.connection.lock().expect("sqlite store poisoned");
1933 let transaction = connection.transaction().map_err(sqlite_error)?;
1934 for (key, state) in states {
1935 let (kind, entity_id) = sync_bucket_key_parts(key);
1936 upsert_sync_bucket(&transaction, kind, entity_id, state)?;
1937 }
1938 transaction.commit().map_err(sqlite_error)?;
1939 Ok(())
1940 }
1941
1942 fn remove_sync_bucket_state_sync(&self, key: SyncBucketKey) -> StoreResult<()> {
1943 let (kind, entity_id) = sync_bucket_key_parts(key);
1944 let connection = self.connection.lock().expect("sqlite store poisoned");
1945 connection
1946 .execute(
1947 "DELETE FROM sync_buckets WHERE bucket_kind = ?1 AND entity_id = ?2",
1948 params![kind, entity_id],
1949 )
1950 .map_err(sqlite_error)?;
1951 Ok(())
1952 }
1953
1954 fn save_pending_sync_batch_sync(&self, batch: PendingSyncBatch) -> StoreResult<()> {
1955 let (kind, entity_id) = sync_bucket_key_parts(batch.key);
1956 let connection = self.connection.lock().expect("sqlite store poisoned");
1957 connection
1958 .execute(
1959 "INSERT INTO pending_sync_batches (
1960 bucket_kind, entity_id, seq, date, payload, updated_at
1961 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1962 ON CONFLICT(bucket_kind, entity_id) DO UPDATE SET
1963 seq = excluded.seq,
1964 date = excluded.date,
1965 payload = excluded.payload,
1966 updated_at = excluded.updated_at",
1967 params![
1968 kind,
1969 entity_id,
1970 batch.committed_state.seq,
1971 batch.committed_state.date,
1972 batch.payload,
1973 now_seconds(),
1974 ],
1975 )
1976 .map_err(sqlite_error)?;
1977 Ok(())
1978 }
1979
1980 fn pending_sync_batches_sync(&self) -> StoreResult<Vec<PendingSyncBatch>> {
1981 let connection = self.connection.lock().expect("sqlite store poisoned");
1982 let mut statement = connection
1983 .prepare(
1984 "SELECT bucket_kind, entity_id, seq, date, payload
1985 FROM pending_sync_batches
1986 ORDER BY bucket_kind ASC, entity_id ASC",
1987 )
1988 .map_err(sqlite_error)?;
1989 statement
1990 .query_map([], |row| {
1991 let kind = row.get::<_, String>(0)?;
1992 let entity_id = row.get::<_, i64>(1)?;
1993 let key = sync_bucket_key_from_parts(&kind, entity_id).map_err(|error| {
1994 rusqlite::Error::FromSqlConversionFailure(0, Type::Text, Box::new(error))
1995 })?;
1996 Ok(PendingSyncBatch {
1997 key,
1998 committed_state: SyncBucketState {
1999 seq: row.get(2)?,
2000 date: row.get(3)?,
2001 },
2002 payload: row.get(4)?,
2003 })
2004 })
2005 .map_err(sqlite_error)?
2006 .collect::<Result<Vec<_>, _>>()
2007 .map_err(sqlite_error)
2008 }
2009
2010 fn discard_pending_sync_batch_sync(&self, key: SyncBucketKey) -> StoreResult<()> {
2011 let (kind, entity_id) = sync_bucket_key_parts(key);
2012 let connection = self.connection.lock().expect("sqlite store poisoned");
2013 connection
2014 .execute(
2015 "DELETE FROM pending_sync_batches
2016 WHERE bucket_kind = ?1 AND entity_id = ?2",
2017 params![kind, entity_id],
2018 )
2019 .map_err(sqlite_error)?;
2020 Ok(())
2021 }
2022
2023 fn commit_pending_sync_batch_sync(
2024 &self,
2025 key: SyncBucketKey,
2026 state: SyncBucketState,
2027 events: Vec<ClientEvent>,
2028 ) -> StoreResult<Vec<ClientEventDelivery>> {
2029 let (kind, entity_id) = sync_bucket_key_parts(key);
2030 let mut connection = self.connection.lock().expect("sqlite store poisoned");
2031 let transaction = connection.transaction().map_err(sqlite_error)?;
2032 let deliveries = append_sqlite_client_events(&transaction, events)?;
2033 upsert_sync_bucket(&transaction, kind, entity_id, state)?;
2034 transaction
2035 .execute(
2036 "DELETE FROM pending_sync_batches
2037 WHERE bucket_kind = ?1 AND entity_id = ?2",
2038 params![kind, entity_id],
2039 )
2040 .map_err(sqlite_error)?;
2041 transaction.commit().map_err(sqlite_error)?;
2042 Ok(deliveries)
2043 }
2044
2045 fn append_client_events_sync(
2046 &self,
2047 events: Vec<ClientEvent>,
2048 ) -> StoreResult<Vec<ClientEventDelivery>> {
2049 let mut connection = self.connection.lock().expect("sqlite store poisoned");
2050 let transaction = connection.transaction().map_err(sqlite_error)?;
2051 let deliveries = append_sqlite_client_events(&transaction, events)?;
2052 transaction.commit().map_err(sqlite_error)?;
2053 Ok(deliveries)
2054 }
2055
2056 fn pending_client_events_sync(&self) -> StoreResult<Vec<ClientEventDelivery>> {
2057 let connection = self.connection.lock().expect("sqlite store poisoned");
2058 let mut statement = connection
2059 .prepare(
2060 "SELECT delivery_id, event_json
2061 FROM client_event_outbox
2062 ORDER BY delivery_id ASC",
2063 )
2064 .map_err(sqlite_error)?;
2065 statement
2066 .query_map([], |row| {
2067 let delivery_id = row.get::<_, i64>(0)?;
2068 let event_json = row.get::<_, String>(1)?;
2069 let event = serde_json::from_str(&event_json).map_err(|error| {
2070 rusqlite::Error::FromSqlConversionFailure(1, Type::Text, Box::new(error))
2071 })?;
2072 Ok(ClientEventDelivery {
2073 delivery_id: Some(u64::try_from(delivery_id).map_err(|error| {
2074 rusqlite::Error::FromSqlConversionFailure(0, Type::Integer, Box::new(error))
2075 })?),
2076 event,
2077 })
2078 })
2079 .map_err(sqlite_error)?
2080 .collect::<Result<Vec<_>, _>>()
2081 .map_err(sqlite_error)
2082 }
2083
2084 fn acknowledge_client_event_sync(&self, delivery_id: u64) -> StoreResult<()> {
2085 let delivery_id = i64::try_from(delivery_id)
2086 .map_err(|_| StoreError::internal("client event delivery ID exceeded SQLite range"))?;
2087 let connection = self.connection.lock().expect("sqlite store poisoned");
2088 connection
2089 .execute(
2090 "DELETE FROM client_event_outbox WHERE delivery_id = ?1",
2091 params![delivery_id],
2092 )
2093 .map_err(sqlite_error)?;
2094 Ok(())
2095 }
2096
2097 fn dialogs_sync(&self, request: DialogsRequest) -> StoreResult<DialogsPage> {
2098 let limit = request.limit.unwrap_or(50).max(1) as usize;
2099 let connection = self.connection.lock().expect("sqlite store poisoned");
2100 let rows = match request.order {
2101 DialogsOrder::RecentActivity => {
2102 let start = parse_cursor(request.cursor.as_deref())?;
2103 let mut stmt = connection.prepare(
2104 "SELECT d.chat_id, d.peer_user_id, d.title, d.emoji, d.last_message_id,
2105 (SELECT MAX(m.message_id) FROM messages m WHERE m.chat_id = d.chat_id) AS synced_through_message_id,
2106 d.unread_count, d.space_id, d.is_public, d.archived, d.pinned,
2107 d.open, d.chat_list_hidden, d.list_order, d.pinned_order,
2108 d.notification_mode, d.follow_mode, d.pinned_message_ids_json
2109 FROM dialogs d
2110 ORDER BY COALESCE(last_message_id, 0) DESC, chat_id ASC
2111 LIMIT ?1 OFFSET ?2",
2112 ).map_err(sqlite_error)?;
2113 stmt.query_map(
2114 params![(limit + 1) as i64, start as i64],
2115 sqlite_dialog_from_row,
2116 )
2117 .map_err(sqlite_error)?
2118 .collect::<Result<Vec<_>, _>>()
2119 .map_err(sqlite_error)?
2120 }
2121 DialogsOrder::StableChatId => {
2122 let after = parse_stable_dialog_cursor(request.cursor.as_deref())?;
2123 let mut stmt = connection.prepare(
2124 "SELECT d.chat_id, d.peer_user_id, d.title, d.emoji, d.last_message_id,
2125 (SELECT MAX(m.message_id) FROM messages m WHERE m.chat_id = d.chat_id) AS synced_through_message_id,
2126 d.unread_count, d.space_id, d.is_public, d.archived, d.pinned,
2127 d.open, d.chat_list_hidden, d.list_order, d.pinned_order,
2128 d.notification_mode, d.follow_mode, d.pinned_message_ids_json
2129 FROM dialogs d
2130 WHERE d.chat_id > ?1
2131 ORDER BY d.chat_id ASC
2132 LIMIT ?2",
2133 ).map_err(sqlite_error)?;
2134 stmt.query_map(params![after, (limit + 1) as i64], sqlite_dialog_from_row)
2135 .map_err(sqlite_error)?
2136 .collect::<Result<Vec<_>, _>>()
2137 .map_err(sqlite_error)?
2138 }
2139 };
2140
2141 let has_more = rows.len() > limit;
2142 let dialogs = rows.into_iter().take(limit).collect::<Vec<_>>();
2143 let users = all_sqlite_users(&connection)?;
2144 let next_cursor = match request.order {
2145 DialogsOrder::RecentActivity => {
2146 let start = parse_cursor(request.cursor.as_deref())?;
2147 has_more.then(|| (start + dialogs.len()).to_string())
2148 }
2149 DialogsOrder::StableChatId => has_more
2150 .then(|| {
2151 dialogs
2152 .last()
2153 .map(|dialog| dialog.chat_id.get().to_string())
2154 })
2155 .flatten(),
2156 };
2157 Ok(DialogsPage {
2158 next_cursor,
2159 dialogs,
2160 users,
2161 })
2162 }
2163
2164 fn dialog_sync(&self, chat_id: InlineId) -> StoreResult<Option<DialogRecord>> {
2165 let connection = self.connection.lock().expect("sqlite store poisoned");
2166 connection
2167 .query_row(
2168 "SELECT d.chat_id, d.peer_user_id, d.title, d.emoji, d.last_message_id,
2169 (SELECT MAX(m.message_id) FROM messages m WHERE m.chat_id = d.chat_id),
2170 d.unread_count, d.space_id, d.is_public, d.archived, d.pinned,
2171 d.open, d.chat_list_hidden, d.list_order, d.pinned_order,
2172 d.notification_mode, d.follow_mode, d.pinned_message_ids_json
2173 FROM dialogs d WHERE d.chat_id = ?1",
2174 params![chat_id.get()],
2175 sqlite_dialog_from_row,
2176 )
2177 .optional()
2178 .map_err(sqlite_error)
2179 }
2180
2181 fn remove_dialog_sync(&self, chat_id: InlineId) -> StoreResult<()> {
2182 let mut connection = self.connection.lock().expect("sqlite store poisoned");
2183 let transaction = connection.transaction().map_err(sqlite_error)?;
2184 transaction
2185 .execute(
2186 "INSERT INTO chat_tombstones (chat_id, deleted_at)
2187 VALUES (?1, ?2)
2188 ON CONFLICT(chat_id) DO UPDATE SET deleted_at = excluded.deleted_at",
2189 params![chat_id.get(), now_seconds()],
2190 )
2191 .map_err(sqlite_error)?;
2192 transaction
2193 .execute(
2194 "INSERT INTO message_tombstones (chat_id, message_id, deleted_at)
2195 SELECT chat_id, message_id, ?2 FROM messages WHERE chat_id = ?1
2196 ON CONFLICT(chat_id, message_id) DO UPDATE SET deleted_at = excluded.deleted_at",
2197 params![chat_id.get(), now_seconds()],
2198 )
2199 .map_err(sqlite_error)?;
2200 for table in [
2201 "dialogs",
2202 "messages",
2203 "chat_participants",
2204 "chat_participant_snapshots",
2205 "reactions",
2206 "reaction_snapshots",
2207 "read_states",
2208 ] {
2209 transaction
2210 .execute(
2211 &format!("DELETE FROM {table} WHERE chat_id = ?1"),
2212 params![chat_id.get()],
2213 )
2214 .map_err(sqlite_error)?;
2215 }
2216 transaction.commit().map_err(sqlite_error)?;
2217 Ok(())
2218 }
2219
2220 fn deleted_chat_ids_sync(&self) -> StoreResult<Vec<InlineId>> {
2221 let connection = self.connection.lock().expect("sqlite store poisoned");
2222 let mut statement = connection
2223 .prepare("SELECT chat_id FROM chat_tombstones ORDER BY chat_id ASC")
2224 .map_err(sqlite_error)?;
2225 statement
2226 .query_map([], |row| row.get::<_, i64>(0).map(InlineId::new))
2227 .map_err(sqlite_error)?
2228 .collect::<Result<Vec<_>, _>>()
2229 .map_err(sqlite_error)
2230 }
2231
2232 fn history_sync(&self, request: HistoryRequest) -> StoreResult<HistoryPage> {
2233 let limit = request.limit.unwrap_or(50).max(1) as usize;
2234 let connection = self.connection.lock().expect("sqlite store poisoned");
2235 let rows = if request.before_message_id.is_some() && request.after_message_id.is_some() {
2236 return Err(StoreError::new(
2237 ClientErrorCategory::InvalidInput,
2238 "history request cannot specify both before_message_id and after_message_id",
2239 ));
2240 } else if let Some(before) = request.before_message_id {
2241 let mut stmt = connection
2242 .prepare(
2243 "SELECT chat_id, message_id, sender_id, timestamp, is_outgoing,
2244 content_json, reply_to_message_id, transaction_json
2245 FROM messages
2246 WHERE chat_id = ?1 AND message_id < ?2
2247 ORDER BY message_id DESC
2248 LIMIT ?3",
2249 )
2250 .map_err(sqlite_error)?;
2251 query_message_rows(
2252 &mut stmt,
2253 params![request.chat_id.get(), before.get(), (limit + 1) as i64],
2254 )?
2255 } else if let Some(after) = request.after_message_id {
2256 let mut stmt = connection
2257 .prepare(
2258 "SELECT chat_id, message_id, sender_id, timestamp, is_outgoing,
2259 content_json, reply_to_message_id, transaction_json
2260 FROM messages
2261 WHERE chat_id = ?1 AND message_id > ?2
2262 ORDER BY message_id ASC
2263 LIMIT ?3",
2264 )
2265 .map_err(sqlite_error)?;
2266 query_message_rows(
2267 &mut stmt,
2268 params![request.chat_id.get(), after.get(), (limit + 1) as i64],
2269 )?
2270 } else {
2271 let mut stmt = connection
2272 .prepare(
2273 "SELECT chat_id, message_id, sender_id, timestamp, is_outgoing,
2274 content_json, reply_to_message_id, transaction_json
2275 FROM messages
2276 WHERE chat_id = ?1
2277 ORDER BY message_id DESC
2278 LIMIT ?2",
2279 )
2280 .map_err(sqlite_error)?;
2281 query_message_rows(
2282 &mut stmt,
2283 params![request.chat_id.get(), (limit + 1) as i64],
2284 )?
2285 };
2286
2287 let messages = rows
2288 .into_iter()
2289 .map(raw_sqlite_message_to_record)
2290 .collect::<StoreResult<Vec<_>>>()?;
2291 let (messages, has_more) = select_history_window(
2292 messages,
2293 limit,
2294 request.before_message_id,
2295 request.after_message_id,
2296 );
2297 let users = sqlite_users_for_messages(&connection, &messages)?;
2298 Ok(HistoryPage {
2299 messages,
2300 users,
2301 has_more,
2302 next_cursor: None,
2303 })
2304 }
2305
2306 fn record_users_sync(&self, users: Vec<UserRecord>) -> StoreResult<()> {
2307 if users.is_empty() {
2308 return Ok(());
2309 }
2310 let connection = self.connection.lock().expect("sqlite store poisoned");
2311 for user in users {
2312 upsert_sqlite_user(&connection, user)?;
2313 }
2314 Ok(())
2315 }
2316
2317 fn user_sync(&self, user_id: InlineId) -> StoreResult<Option<UserRecord>> {
2318 let connection = self.connection.lock().expect("sqlite store poisoned");
2319 connection
2320 .query_row(
2321 "SELECT user_id, display_name, username, first_name, last_name, avatar_url, is_bot
2322 FROM users WHERE user_id = ?1",
2323 params![user_id.get()],
2324 sqlite_user_from_row,
2325 )
2326 .optional()
2327 .map_err(sqlite_error)
2328 }
2329
2330 fn record_space_sync(&self, space: SpaceRecord) -> StoreResult<()> {
2331 let payload = serde_json::to_string(&space)
2332 .map_err(|error| StoreError::internal(format!("encode space: {error}")))?;
2333 let connection = self.connection.lock().expect("sqlite store poisoned");
2334 connection
2335 .execute(
2336 "INSERT INTO spaces (space_id, record_json, updated_at)
2337 VALUES (?1, ?2, ?3)
2338 ON CONFLICT(space_id) DO UPDATE SET
2339 record_json = excluded.record_json,
2340 updated_at = excluded.updated_at",
2341 params![space.space_id.get(), payload, now_seconds()],
2342 )
2343 .map_err(sqlite_error)?;
2344 Ok(())
2345 }
2346
2347 fn space_sync(&self, space_id: InlineId) -> StoreResult<Option<SpaceRecord>> {
2348 let connection = self.connection.lock().expect("sqlite store poisoned");
2349 connection
2350 .query_row(
2351 "SELECT record_json FROM spaces WHERE space_id = ?1",
2352 params![space_id.get()],
2353 |row| row.get::<_, String>(0),
2354 )
2355 .optional()
2356 .map_err(sqlite_error)?
2357 .map(|payload| {
2358 serde_json::from_str(&payload)
2359 .map_err(|error| StoreError::internal(format!("decode space: {error}")))
2360 })
2361 .transpose()
2362 }
2363
2364 fn record_space_member_sync(&self, member: SpaceMemberRecord) -> StoreResult<()> {
2365 let payload = serde_json::to_string(&member)
2366 .map_err(|error| StoreError::internal(format!("encode space member: {error}")))?;
2367 let connection = self.connection.lock().expect("sqlite store poisoned");
2368 connection
2369 .execute(
2370 "INSERT INTO space_members (space_id, user_id, record_json, updated_at)
2371 VALUES (?1, ?2, ?3, ?4)
2372 ON CONFLICT(space_id, user_id) DO UPDATE SET
2373 record_json = excluded.record_json,
2374 updated_at = excluded.updated_at",
2375 params![
2376 member.space_id.get(),
2377 member.user_id.get(),
2378 payload,
2379 now_seconds()
2380 ],
2381 )
2382 .map_err(sqlite_error)?;
2383 Ok(())
2384 }
2385
2386 fn record_space_members_sync(
2387 &self,
2388 space_id: InlineId,
2389 members: Vec<SpaceMemberRecord>,
2390 ) -> StoreResult<()> {
2391 if members.iter().any(|member| member.space_id != space_id) {
2392 return Err(StoreError::new(
2393 ClientErrorCategory::InvalidInput,
2394 "space member snapshot contains a different space id",
2395 ));
2396 }
2397 let mut encoded = Vec::with_capacity(members.len());
2398 for member in members {
2399 let payload = serde_json::to_string(&member)
2400 .map_err(|error| StoreError::internal(format!("encode space member: {error}")))?;
2401 encoded.push((member, payload));
2402 }
2403 let mut connection = self.connection.lock().expect("sqlite store poisoned");
2404 let transaction = connection.transaction().map_err(sqlite_error)?;
2405 transaction
2406 .execute(
2407 "DELETE FROM space_members WHERE space_id = ?1",
2408 params![space_id.get()],
2409 )
2410 .map_err(sqlite_error)?;
2411 for (member, payload) in encoded {
2412 transaction
2413 .execute(
2414 "INSERT INTO space_members (space_id, user_id, record_json, updated_at)
2415 VALUES (?1, ?2, ?3, ?4)",
2416 params![
2417 member.space_id.get(),
2418 member.user_id.get(),
2419 payload,
2420 now_seconds()
2421 ],
2422 )
2423 .map_err(sqlite_error)?;
2424 }
2425 transaction.commit().map_err(sqlite_error)?;
2426 Ok(())
2427 }
2428
2429 fn remove_space_member_sync(&self, space_id: InlineId, user_id: InlineId) -> StoreResult<()> {
2430 let connection = self.connection.lock().expect("sqlite store poisoned");
2431 connection
2432 .execute(
2433 "DELETE FROM space_members WHERE space_id = ?1 AND user_id = ?2",
2434 params![space_id.get(), user_id.get()],
2435 )
2436 .map_err(sqlite_error)?;
2437 Ok(())
2438 }
2439
2440 fn space_members_sync(&self, space_id: InlineId) -> StoreResult<Vec<SpaceMemberRecord>> {
2441 let connection = self.connection.lock().expect("sqlite store poisoned");
2442 let mut statement = connection
2443 .prepare(
2444 "SELECT record_json FROM space_members
2445 WHERE space_id = ?1 ORDER BY user_id ASC",
2446 )
2447 .map_err(sqlite_error)?;
2448 let payloads = statement
2449 .query_map(params![space_id.get()], |row| row.get::<_, String>(0))
2450 .map_err(sqlite_error)?
2451 .collect::<Result<Vec<_>, _>>()
2452 .map_err(sqlite_error)?;
2453 payloads
2454 .into_iter()
2455 .map(|payload| {
2456 serde_json::from_str(&payload)
2457 .map_err(|error| StoreError::internal(format!("decode space member: {error}")))
2458 })
2459 .collect()
2460 }
2461
2462 fn record_user_settings_sync(&self, settings: UserSettingsRecord) -> StoreResult<()> {
2463 let payload = serde_json::to_string(&settings)
2464 .map_err(|error| StoreError::internal(format!("encode user settings: {error}")))?;
2465 let connection = self.connection.lock().expect("sqlite store poisoned");
2466 connection
2467 .execute(
2468 "INSERT INTO user_settings (id, settings_json, updated_at)
2469 VALUES (1, ?1, ?2)
2470 ON CONFLICT(id) DO UPDATE SET
2471 settings_json = excluded.settings_json,
2472 updated_at = excluded.updated_at",
2473 params![payload, now_seconds()],
2474 )
2475 .map_err(sqlite_error)?;
2476 Ok(())
2477 }
2478
2479 fn user_settings_sync(&self) -> StoreResult<Option<UserSettingsRecord>> {
2480 let connection = self.connection.lock().expect("sqlite store poisoned");
2481 connection
2482 .query_row(
2483 "SELECT settings_json FROM user_settings WHERE id = 1",
2484 [],
2485 |row| row.get::<_, String>(0),
2486 )
2487 .optional()
2488 .map_err(sqlite_error)?
2489 .map(|payload| {
2490 serde_json::from_str(&payload)
2491 .map_err(|error| StoreError::internal(format!("decode user settings: {error}")))
2492 })
2493 .transpose()
2494 }
2495
2496 fn record_chat_participants_sync(
2497 &self,
2498 chat_id: InlineId,
2499 participants: Vec<ChatParticipantRecord>,
2500 ) -> StoreResult<()> {
2501 let mut connection = self.connection.lock().expect("sqlite store poisoned");
2502 let transaction = connection.transaction().map_err(sqlite_error)?;
2503 transaction
2504 .execute(
2505 "DELETE FROM chat_participants WHERE chat_id = ?1",
2506 params![chat_id.get()],
2507 )
2508 .map_err(sqlite_error)?;
2509 for participant in participants {
2510 upsert_sqlite_participant(&transaction, chat_id, participant)?;
2511 }
2512 transaction
2513 .execute(
2514 "INSERT INTO chat_participant_snapshots (chat_id, updated_at)
2515 VALUES (?1, ?2)
2516 ON CONFLICT(chat_id) DO UPDATE SET updated_at = excluded.updated_at",
2517 params![chat_id.get(), now_seconds()],
2518 )
2519 .map_err(sqlite_error)?;
2520 transaction.commit().map_err(sqlite_error)?;
2521 Ok(())
2522 }
2523
2524 fn record_chat_participant_sync(
2525 &self,
2526 chat_id: InlineId,
2527 participant: ChatParticipantRecord,
2528 ) -> StoreResult<()> {
2529 let connection = self.connection.lock().expect("sqlite store poisoned");
2530 upsert_sqlite_participant(&connection, chat_id, participant)
2531 }
2532
2533 fn remove_chat_participant_sync(
2534 &self,
2535 chat_id: InlineId,
2536 user_id: InlineId,
2537 ) -> StoreResult<()> {
2538 let connection = self.connection.lock().expect("sqlite store poisoned");
2539 connection
2540 .execute(
2541 "DELETE FROM chat_participants WHERE chat_id = ?1 AND user_id = ?2",
2542 params![chat_id.get(), user_id.get()],
2543 )
2544 .map_err(sqlite_error)?;
2545 Ok(())
2546 }
2547
2548 fn chat_participants_sync(&self, chat_id: InlineId) -> StoreResult<Vec<ChatParticipantRecord>> {
2549 let connection = self.connection.lock().expect("sqlite store poisoned");
2550 let mut statement = connection
2551 .prepare(
2552 "SELECT user_id, date FROM chat_participants
2553 WHERE chat_id = ?1 ORDER BY user_id ASC",
2554 )
2555 .map_err(sqlite_error)?;
2556 statement
2557 .query_map(params![chat_id.get()], |row| {
2558 Ok(ChatParticipantRecord {
2559 user_id: InlineId::new(row.get(0)?),
2560 date: row.get(1)?,
2561 })
2562 })
2563 .map_err(sqlite_error)?
2564 .collect::<Result<Vec<_>, _>>()
2565 .map_err(sqlite_error)
2566 }
2567
2568 fn chat_participants_complete_sync(&self, chat_id: InlineId) -> StoreResult<bool> {
2569 let connection = self.connection.lock().expect("sqlite store poisoned");
2570 connection
2571 .query_row(
2572 "SELECT 1 FROM chat_participant_snapshots WHERE chat_id = ?1",
2573 params![chat_id.get()],
2574 |_| Ok(()),
2575 )
2576 .optional()
2577 .map(|row| row.is_some())
2578 .map_err(sqlite_error)
2579 }
2580
2581 fn record_message_sync(&self, message: MessageRecord) -> StoreResult<()> {
2582 let content_json = serde_json::to_string(&message.content)
2583 .map_err(|error| StoreError::internal(format!("encode message content: {error}")))?;
2584 let transaction_json = message
2585 .transaction
2586 .as_ref()
2587 .map(serde_json::to_string)
2588 .transpose()
2589 .map_err(|error| {
2590 StoreError::internal(format!("encode transaction identity: {error}"))
2591 })?;
2592 let connection = self.connection.lock().expect("sqlite store poisoned");
2593 connection
2594 .execute(
2595 "INSERT INTO messages (
2596 chat_id, message_id, sender_id, timestamp, is_outgoing,
2597 content_json, reply_to_message_id, transaction_json
2598 )
2599 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
2600 ON CONFLICT(chat_id, message_id) DO UPDATE SET
2601 sender_id = excluded.sender_id,
2602 timestamp = excluded.timestamp,
2603 is_outgoing = excluded.is_outgoing,
2604 content_json = excluded.content_json,
2605 reply_to_message_id = excluded.reply_to_message_id,
2606 transaction_json = excluded.transaction_json",
2607 params![
2608 message.chat_id.get(),
2609 message.message_id.get(),
2610 message.sender_id.get(),
2611 message.timestamp,
2612 i64::from(message.is_outgoing),
2613 content_json,
2614 message.reply_to_message_id.map(InlineId::get),
2615 transaction_json,
2616 ],
2617 )
2618 .map_err(sqlite_error)?;
2619 connection
2620 .execute(
2621 "DELETE FROM message_tombstones WHERE chat_id = ?1 AND message_id = ?2",
2622 params![message.chat_id.get(), message.message_id.get()],
2623 )
2624 .map_err(sqlite_error)?;
2625 upsert_sqlite_dialog_last_message(&connection, message.chat_id, message.message_id)?;
2626 Ok(())
2627 }
2628
2629 fn record_message_deleted_sync(
2630 &self,
2631 chat_id: InlineId,
2632 message_id: InlineId,
2633 ) -> StoreResult<()> {
2634 let mut connection = self.connection.lock().expect("sqlite store poisoned");
2635 let transaction = connection.transaction().map_err(sqlite_error)?;
2636 transaction
2637 .execute(
2638 "DELETE FROM messages WHERE chat_id = ?1 AND message_id = ?2",
2639 params![chat_id.get(), message_id.get()],
2640 )
2641 .map_err(sqlite_error)?;
2642 transaction
2643 .execute(
2644 "DELETE FROM reactions WHERE chat_id = ?1 AND message_id = ?2",
2645 params![chat_id.get(), message_id.get()],
2646 )
2647 .map_err(sqlite_error)?;
2648 transaction
2649 .execute(
2650 "DELETE FROM reaction_snapshots WHERE chat_id = ?1 AND message_id = ?2",
2651 params![chat_id.get(), message_id.get()],
2652 )
2653 .map_err(sqlite_error)?;
2654 transaction
2655 .execute(
2656 "INSERT INTO message_tombstones (chat_id, message_id, deleted_at)
2657 VALUES (?1, ?2, ?3)
2658 ON CONFLICT(chat_id, message_id) DO UPDATE SET
2659 deleted_at = excluded.deleted_at",
2660 params![chat_id.get(), message_id.get(), now_seconds()],
2661 )
2662 .map_err(sqlite_error)?;
2663 transaction.commit().map_err(sqlite_error)?;
2664 Ok(())
2665 }
2666
2667 fn message_deleted_sync(&self, chat_id: InlineId, message_id: InlineId) -> StoreResult<bool> {
2668 let connection = self.connection.lock().expect("sqlite store poisoned");
2669 connection
2670 .query_row(
2671 "SELECT 1 FROM message_tombstones WHERE chat_id = ?1 AND message_id = ?2",
2672 params![chat_id.get(), message_id.get()],
2673 |_| Ok(()),
2674 )
2675 .optional()
2676 .map(|row| row.is_some())
2677 .map_err(sqlite_error)
2678 }
2679
2680 fn deleted_message_ids_sync(&self, chat_id: InlineId) -> StoreResult<Vec<InlineId>> {
2681 let connection = self.connection.lock().expect("sqlite store poisoned");
2682 let mut statement = connection
2683 .prepare(
2684 "SELECT message_id FROM message_tombstones
2685 WHERE chat_id = ?1 ORDER BY message_id ASC",
2686 )
2687 .map_err(sqlite_error)?;
2688 statement
2689 .query_map(params![chat_id.get()], |row| {
2690 row.get::<_, i64>(0).map(InlineId::new)
2691 })
2692 .map_err(sqlite_error)?
2693 .collect::<Result<Vec<_>, _>>()
2694 .map_err(sqlite_error)
2695 }
2696
2697 fn clear_chat_messages_sync(
2698 &self,
2699 chat_id: InlineId,
2700 before_date: Option<i64>,
2701 ) -> StoreResult<Vec<InlineId>> {
2702 let mut connection = self.connection.lock().expect("sqlite store poisoned");
2703 let transaction = connection.transaction().map_err(sqlite_error)?;
2704 let removed = {
2705 let (query, parameters): (&str, Vec<i64>) = match before_date {
2706 Some(date) => (
2707 "SELECT message_id FROM messages
2708 WHERE chat_id = ?1 AND timestamp < ?2 ORDER BY message_id ASC",
2709 vec![chat_id.get(), date],
2710 ),
2711 None => (
2712 "SELECT message_id FROM messages
2713 WHERE chat_id = ?1 ORDER BY message_id ASC",
2714 vec![chat_id.get()],
2715 ),
2716 };
2717 let mut statement = transaction.prepare(query).map_err(sqlite_error)?;
2718 statement
2719 .query_map(rusqlite::params_from_iter(parameters), |row| {
2720 row.get::<_, i64>(0).map(InlineId::new)
2721 })
2722 .map_err(sqlite_error)?
2723 .collect::<Result<Vec<_>, _>>()
2724 .map_err(sqlite_error)?
2725 };
2726 for message_id in &removed {
2727 transaction
2728 .execute(
2729 "DELETE FROM reactions WHERE chat_id = ?1 AND message_id = ?2",
2730 params![chat_id.get(), message_id.get()],
2731 )
2732 .map_err(sqlite_error)?;
2733 transaction
2734 .execute(
2735 "DELETE FROM reaction_snapshots WHERE chat_id = ?1 AND message_id = ?2",
2736 params![chat_id.get(), message_id.get()],
2737 )
2738 .map_err(sqlite_error)?;
2739 transaction
2740 .execute(
2741 "INSERT INTO message_tombstones (chat_id, message_id, deleted_at)
2742 VALUES (?1, ?2, ?3)
2743 ON CONFLICT(chat_id, message_id) DO UPDATE SET deleted_at = excluded.deleted_at",
2744 params![chat_id.get(), message_id.get(), now_seconds()],
2745 )
2746 .map_err(sqlite_error)?;
2747 }
2748 match before_date {
2749 Some(date) => transaction
2750 .execute(
2751 "DELETE FROM messages WHERE chat_id = ?1 AND timestamp < ?2",
2752 params![chat_id.get(), date],
2753 )
2754 .map_err(sqlite_error)?,
2755 None => transaction
2756 .execute(
2757 "DELETE FROM messages WHERE chat_id = ?1",
2758 params![chat_id.get()],
2759 )
2760 .map_err(sqlite_error)?,
2761 };
2762 transaction
2763 .execute(
2764 "UPDATE dialogs SET
2765 last_message_id = (SELECT MAX(message_id) FROM messages WHERE chat_id = ?1),
2766 updated_at = ?2
2767 WHERE chat_id = ?1",
2768 params![chat_id.get(), now_seconds()],
2769 )
2770 .map_err(sqlite_error)?;
2771 transaction.commit().map_err(sqlite_error)?;
2772 Ok(removed)
2773 }
2774
2775 fn chat_ids_in_space_sync(&self, space_id: InlineId) -> StoreResult<Vec<InlineId>> {
2776 let connection = self.connection.lock().expect("sqlite store poisoned");
2777 let mut statement = connection
2778 .prepare("SELECT chat_id FROM dialogs WHERE space_id = ?1 ORDER BY chat_id ASC")
2779 .map_err(sqlite_error)?;
2780 statement
2781 .query_map(params![space_id.get()], |row| {
2782 row.get::<_, i64>(0).map(InlineId::new)
2783 })
2784 .map_err(sqlite_error)?
2785 .collect::<Result<Vec<_>, _>>()
2786 .map_err(sqlite_error)
2787 }
2788
2789 fn record_reaction_sync(&self, reaction: StoredReaction) -> StoreResult<()> {
2790 let connection = self.connection.lock().expect("sqlite store poisoned");
2791 connection
2792 .execute(
2793 "INSERT INTO reactions (chat_id, message_id, user_id, reaction, updated_at)
2794 VALUES (?1, ?2, ?3, ?4, ?5)
2795 ON CONFLICT(chat_id, message_id, user_id, reaction) DO UPDATE SET
2796 updated_at = excluded.updated_at",
2797 params![
2798 reaction.chat_id.get(),
2799 reaction.message_id.get(),
2800 reaction.user_id.get(),
2801 reaction.reaction,
2802 now_seconds(),
2803 ],
2804 )
2805 .map_err(sqlite_error)?;
2806 Ok(())
2807 }
2808
2809 fn remove_reaction_sync(&self, reaction: StoredReaction) -> StoreResult<()> {
2810 let connection = self.connection.lock().expect("sqlite store poisoned");
2811 connection
2812 .execute(
2813 "DELETE FROM reactions
2814 WHERE chat_id = ?1 AND message_id = ?2 AND user_id = ?3 AND reaction = ?4",
2815 params![
2816 reaction.chat_id.get(),
2817 reaction.message_id.get(),
2818 reaction.user_id.get(),
2819 reaction.reaction,
2820 ],
2821 )
2822 .map_err(sqlite_error)?;
2823 Ok(())
2824 }
2825
2826 fn replace_message_reactions_sync(
2827 &self,
2828 chat_id: InlineId,
2829 message_id: InlineId,
2830 reactions: Vec<StoredReaction>,
2831 ) -> StoreResult<()> {
2832 let mut connection = self.connection.lock().expect("sqlite store poisoned");
2833 let transaction = connection.transaction().map_err(sqlite_error)?;
2834 transaction
2835 .execute(
2836 "DELETE FROM reactions WHERE chat_id = ?1 AND message_id = ?2",
2837 params![chat_id.get(), message_id.get()],
2838 )
2839 .map_err(sqlite_error)?;
2840 for reaction in reactions {
2841 transaction
2842 .execute(
2843 "INSERT INTO reactions (chat_id, message_id, user_id, reaction, updated_at)
2844 VALUES (?1, ?2, ?3, ?4, ?5)",
2845 params![
2846 chat_id.get(),
2847 message_id.get(),
2848 reaction.user_id.get(),
2849 reaction.reaction,
2850 now_seconds(),
2851 ],
2852 )
2853 .map_err(sqlite_error)?;
2854 }
2855 transaction
2856 .execute(
2857 "INSERT INTO reaction_snapshots (chat_id, message_id, updated_at)
2858 VALUES (?1, ?2, ?3)
2859 ON CONFLICT(chat_id, message_id) DO UPDATE SET updated_at = excluded.updated_at",
2860 params![chat_id.get(), message_id.get(), now_seconds()],
2861 )
2862 .map_err(sqlite_error)?;
2863 transaction.commit().map_err(sqlite_error)?;
2864 Ok(())
2865 }
2866
2867 fn reactions_sync(
2868 &self,
2869 chat_id: InlineId,
2870 message_id: InlineId,
2871 ) -> StoreResult<Vec<StoredReaction>> {
2872 let connection = self.connection.lock().expect("sqlite store poisoned");
2873 let mut statement = connection
2874 .prepare(
2875 "SELECT user_id, reaction FROM reactions
2876 WHERE chat_id = ?1 AND message_id = ?2
2877 ORDER BY user_id ASC, reaction ASC",
2878 )
2879 .map_err(sqlite_error)?;
2880 statement
2881 .query_map(params![chat_id.get(), message_id.get()], |row| {
2882 Ok(StoredReaction {
2883 chat_id,
2884 message_id,
2885 user_id: InlineId::new(row.get(0)?),
2886 reaction: row.get(1)?,
2887 })
2888 })
2889 .map_err(sqlite_error)?
2890 .collect::<Result<Vec<_>, _>>()
2891 .map_err(sqlite_error)
2892 }
2893
2894 fn reactions_for_chat_sync(&self, chat_id: InlineId) -> StoreResult<Vec<StoredReaction>> {
2895 let connection = self.connection.lock().expect("sqlite store poisoned");
2896 let mut statement = connection
2897 .prepare(
2898 "SELECT message_id, user_id, reaction FROM reactions
2899 WHERE chat_id = ?1
2900 ORDER BY message_id ASC, user_id ASC, reaction ASC",
2901 )
2902 .map_err(sqlite_error)?;
2903 statement
2904 .query_map(params![chat_id.get()], |row| {
2905 Ok(StoredReaction {
2906 chat_id,
2907 message_id: InlineId::new(row.get(0)?),
2908 user_id: InlineId::new(row.get(1)?),
2909 reaction: row.get(2)?,
2910 })
2911 })
2912 .map_err(sqlite_error)?
2913 .collect::<Result<Vec<_>, _>>()
2914 .map_err(sqlite_error)
2915 }
2916
2917 fn reaction_snapshot_message_ids_sync(&self, chat_id: InlineId) -> StoreResult<Vec<InlineId>> {
2918 let connection = self.connection.lock().expect("sqlite store poisoned");
2919 let mut statement = connection
2920 .prepare(
2921 "SELECT message_id FROM reaction_snapshots
2922 WHERE chat_id = ?1 ORDER BY message_id ASC",
2923 )
2924 .map_err(sqlite_error)?;
2925 statement
2926 .query_map(params![chat_id.get()], |row| {
2927 row.get::<_, i64>(0).map(InlineId::new)
2928 })
2929 .map_err(sqlite_error)?
2930 .collect::<Result<Vec<_>, _>>()
2931 .map_err(sqlite_error)
2932 }
2933
2934 fn record_read_state_sync(&self, state: StoredReadState) -> StoreResult<()> {
2935 let connection = self.connection.lock().expect("sqlite store poisoned");
2936 connection
2937 .execute(
2938 "INSERT INTO read_states (
2939 chat_id, read_max_id, unread_count, marked_unread, updated_at
2940 ) VALUES (?1, ?2, ?3, ?4, ?5)
2941 ON CONFLICT(chat_id) DO UPDATE SET
2942 read_max_id = excluded.read_max_id,
2943 unread_count = excluded.unread_count,
2944 marked_unread = excluded.marked_unread,
2945 updated_at = excluded.updated_at",
2946 params![
2947 state.chat_id.get(),
2948 state.read_max_id.map(InlineId::get),
2949 state.unread_count.map(i64::from),
2950 i64::from(state.marked_unread),
2951 now_seconds(),
2952 ],
2953 )
2954 .map_err(sqlite_error)?;
2955 Ok(())
2956 }
2957
2958 fn read_state_sync(&self, chat_id: InlineId) -> StoreResult<Option<StoredReadState>> {
2959 let connection = self.connection.lock().expect("sqlite store poisoned");
2960 connection
2961 .query_row(
2962 "SELECT read_max_id, unread_count, marked_unread
2963 FROM read_states WHERE chat_id = ?1",
2964 params![chat_id.get()],
2965 |row| {
2966 Ok(StoredReadState {
2967 chat_id,
2968 read_max_id: row.get::<_, Option<i64>>(0)?.map(InlineId::new),
2969 unread_count: row
2970 .get::<_, Option<i64>>(1)?
2971 .and_then(|value| u32::try_from(value).ok()),
2972 marked_unread: row.get::<_, i64>(2)? != 0,
2973 })
2974 },
2975 )
2976 .optional()
2977 .map_err(sqlite_error)
2978 }
2979
2980 fn record_transaction_sync(&self, transaction: StoredTransaction) -> StoreResult<()> {
2981 let identity_json = serde_json::to_string(&transaction.identity).map_err(|error| {
2982 StoreError::internal(format!("encode transaction identity: {error}"))
2983 })?;
2984 let state_json = serde_json::to_string(&transaction.state)
2985 .map_err(|error| StoreError::internal(format!("encode transaction state: {error}")))?;
2986 let failure_json = transaction
2987 .failure
2988 .as_ref()
2989 .map(serde_json::to_string)
2990 .transpose()
2991 .map_err(|error| {
2992 StoreError::internal(format!("encode transaction failure: {error}"))
2993 })?;
2994 let connection = self.connection.lock().expect("sqlite store poisoned");
2995 connection
2996 .execute(
2997 "INSERT INTO transactions (
2998 transaction_id, identity_json, state_json, chat_id, message_id,
2999 random_id, external_source, external_id, failure_json, created_at, updated_at
3000 )
3001 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
3002 ON CONFLICT(transaction_id) DO UPDATE SET
3003 identity_json = excluded.identity_json,
3004 state_json = excluded.state_json,
3005 chat_id = excluded.chat_id,
3006 message_id = excluded.message_id,
3007 random_id = excluded.random_id,
3008 external_source = excluded.external_source,
3009 external_id = excluded.external_id,
3010 failure_json = excluded.failure_json,
3011 updated_at = excluded.updated_at",
3012 params![
3013 transaction.identity.transaction_id.as_str(),
3014 identity_json,
3015 state_json,
3016 transaction.chat_id.map(InlineId::get),
3017 transaction.message_id.map(InlineId::get),
3018 transaction.identity.random_id.get(),
3019 transaction
3020 .identity
3021 .external_id
3022 .as_ref()
3023 .map(|external| external.source()),
3024 transaction
3025 .identity
3026 .external_id
3027 .as_ref()
3028 .map(|external| external.id()),
3029 failure_json,
3030 transaction.created_at,
3031 transaction.updated_at,
3032 ],
3033 )
3034 .map_err(sqlite_error)?;
3035 Ok(())
3036 }
3037
3038 fn transaction_sync(
3039 &self,
3040 transaction_id: TransactionId,
3041 ) -> StoreResult<Option<StoredTransaction>> {
3042 let connection = self.connection.lock().expect("sqlite store poisoned");
3043 let row = connection
3044 .query_row(
3045 "SELECT identity_json, state_json, chat_id, message_id,
3046 failure_json, created_at, updated_at
3047 FROM transactions
3048 WHERE transaction_id = ?1",
3049 params![transaction_id.as_str()],
3050 |row| {
3051 Ok(RawSqliteTransaction {
3052 identity_json: row.get(0)?,
3053 state_json: row.get(1)?,
3054 chat_id: row.get(2)?,
3055 message_id: row.get(3)?,
3056 failure_json: row.get(4)?,
3057 created_at: row.get(5)?,
3058 updated_at: row.get(6)?,
3059 })
3060 },
3061 )
3062 .optional()
3063 .map_err(sqlite_error)?;
3064
3065 row.map(raw_sqlite_transaction_to_record).transpose()
3066 }
3067}
3068
3069fn append_sqlite_client_events(
3070 connection: &Connection,
3071 events: Vec<ClientEvent>,
3072) -> StoreResult<Vec<ClientEventDelivery>> {
3073 events
3074 .into_iter()
3075 .map(|event| {
3076 if event.reliability() == EventReliability::BestEffort {
3077 return Ok(ClientEventDelivery::ephemeral(event));
3078 }
3079 let event_json = serde_json::to_string(&event)
3080 .map_err(|error| StoreError::internal(format!("encode client event: {error}")))?;
3081 connection
3082 .execute(
3083 "INSERT INTO client_event_outbox (event_json, created_at) VALUES (?1, ?2)",
3084 params![event_json, now_seconds()],
3085 )
3086 .map_err(sqlite_error)?;
3087 let delivery_id = u64::try_from(connection.last_insert_rowid())
3088 .map_err(|_| StoreError::internal("stored client event delivery ID was invalid"))?;
3089 Ok(ClientEventDelivery {
3090 delivery_id: Some(delivery_id),
3091 event,
3092 })
3093 })
3094 .collect()
3095}
3096
3097fn prepare_private_sqlite_file(path: &Path) -> StoreResult<()> {
3098 let mut options = OpenOptions::new();
3099 options.create(true).truncate(false).read(true).write(true);
3100 #[cfg(unix)]
3101 {
3102 use std::os::unix::fs::OpenOptionsExt;
3103 options.mode(0o600);
3104 }
3105 let _file = options
3106 .open(path)
3107 .map_err(|error| StoreError::internal(format!("prepare SQLite store file: {error}")))?;
3108 #[cfg(unix)]
3109 {
3110 use std::os::unix::fs::PermissionsExt;
3111 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).map_err(
3112 |error| StoreError::internal(format!("secure SQLite store permissions: {error}")),
3113 )?;
3114 }
3115 Ok(())
3116}
3117
3118impl ClientStore for SqliteStore {
3119 fn save_session(&self, session: StoredSession) -> BoxFuture<'static, StoreResult<()>> {
3120 let store = self.clone();
3121 Box::pin(async move { store.save_session_sync(session) })
3122 }
3123
3124 fn load_session(&self) -> BoxFuture<'static, StoreResult<Option<StoredSession>>> {
3125 let store = self.clone();
3126 Box::pin(async move { store.load_session_sync() })
3127 }
3128
3129 fn clear_session(&self) -> BoxFuture<'static, StoreResult<()>> {
3130 let store = self.clone();
3131 Box::pin(async move { store.clear_session_sync() })
3132 }
3133
3134 fn clear_account_data(&self) -> BoxFuture<'static, StoreResult<()>> {
3135 let store = self.clone();
3136 Box::pin(async move { store.clear_account_data_sync() })
3137 }
3138
3139 fn sync_state(&self) -> BoxFuture<'static, StoreResult<SyncState>> {
3140 let store = self.clone();
3141 Box::pin(async move { store.sync_state_sync() })
3142 }
3143
3144 fn save_sync_state(&self, state: SyncState) -> BoxFuture<'static, StoreResult<()>> {
3145 let store = self.clone();
3146 Box::pin(async move { store.save_sync_state_sync(state) })
3147 }
3148
3149 fn clear_sync_state(&self) -> BoxFuture<'static, StoreResult<()>> {
3150 let store = self.clone();
3151 Box::pin(async move { store.clear_sync_state_sync() })
3152 }
3153
3154 fn sync_bucket_state(
3155 &self,
3156 key: SyncBucketKey,
3157 ) -> BoxFuture<'static, StoreResult<SyncBucketState>> {
3158 let store = self.clone();
3159 Box::pin(async move { store.sync_bucket_state_sync(key) })
3160 }
3161
3162 fn save_sync_bucket_state(
3163 &self,
3164 key: SyncBucketKey,
3165 state: SyncBucketState,
3166 ) -> BoxFuture<'static, StoreResult<()>> {
3167 let store = self.clone();
3168 Box::pin(async move { store.save_sync_bucket_state_sync(key, state) })
3169 }
3170
3171 fn save_sync_bucket_states(
3172 &self,
3173 states: Vec<(SyncBucketKey, SyncBucketState)>,
3174 ) -> BoxFuture<'static, StoreResult<()>> {
3175 let store = self.clone();
3176 Box::pin(async move { store.save_sync_bucket_states_sync(states) })
3177 }
3178
3179 fn remove_sync_bucket_state(&self, key: SyncBucketKey) -> BoxFuture<'static, StoreResult<()>> {
3180 let store = self.clone();
3181 Box::pin(async move { store.remove_sync_bucket_state_sync(key) })
3182 }
3183
3184 fn save_pending_sync_batch(
3185 &self,
3186 batch: PendingSyncBatch,
3187 ) -> BoxFuture<'static, StoreResult<()>> {
3188 let store = self.clone();
3189 Box::pin(async move { store.save_pending_sync_batch_sync(batch) })
3190 }
3191
3192 fn pending_sync_batches(&self) -> BoxFuture<'static, StoreResult<Vec<PendingSyncBatch>>> {
3193 let store = self.clone();
3194 Box::pin(async move { store.pending_sync_batches_sync() })
3195 }
3196
3197 fn discard_pending_sync_batch(
3198 &self,
3199 key: SyncBucketKey,
3200 ) -> BoxFuture<'static, StoreResult<()>> {
3201 let store = self.clone();
3202 Box::pin(async move { store.discard_pending_sync_batch_sync(key) })
3203 }
3204
3205 fn commit_pending_sync_batch(
3206 &self,
3207 key: SyncBucketKey,
3208 state: SyncBucketState,
3209 ) -> BoxFuture<'static, StoreResult<()>> {
3210 let store = self.clone();
3211 Box::pin(async move {
3212 store
3213 .commit_pending_sync_batch_sync(key, state, Vec::new())
3214 .map(|_| ())
3215 })
3216 }
3217
3218 fn commit_pending_sync_batch_with_events(
3219 &self,
3220 key: SyncBucketKey,
3221 state: SyncBucketState,
3222 events: Vec<ClientEvent>,
3223 ) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
3224 let store = self.clone();
3225 Box::pin(async move { store.commit_pending_sync_batch_sync(key, state, events) })
3226 }
3227
3228 fn append_client_events(
3229 &self,
3230 events: Vec<ClientEvent>,
3231 ) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
3232 let store = self.clone();
3233 Box::pin(async move { store.append_client_events_sync(events) })
3234 }
3235
3236 fn pending_client_events(&self) -> BoxFuture<'static, StoreResult<Vec<ClientEventDelivery>>> {
3237 let store = self.clone();
3238 Box::pin(async move { store.pending_client_events_sync() })
3239 }
3240
3241 fn acknowledge_client_event(&self, delivery_id: u64) -> BoxFuture<'static, StoreResult<()>> {
3242 let store = self.clone();
3243 Box::pin(async move { store.acknowledge_client_event_sync(delivery_id) })
3244 }
3245
3246 fn dialogs(&self, request: DialogsRequest) -> BoxFuture<'static, StoreResult<DialogsPage>> {
3247 let store = self.clone();
3248 Box::pin(async move { store.dialogs_sync(request) })
3249 }
3250
3251 fn dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<Option<DialogRecord>>> {
3252 let store = self.clone();
3253 Box::pin(async move { store.dialog_sync(chat_id) })
3254 }
3255
3256 fn record_dialog(&self, dialog: DialogRecord) -> BoxFuture<'static, StoreResult<()>> {
3257 let store = self.clone();
3258 Box::pin(async move { store.upsert_dialog(dialog) })
3259 }
3260
3261 fn remove_dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<()>> {
3262 let store = self.clone();
3263 Box::pin(async move { store.remove_dialog_sync(chat_id) })
3264 }
3265
3266 fn deleted_chat_ids(&self) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
3267 let store = self.clone();
3268 Box::pin(async move { store.deleted_chat_ids_sync() })
3269 }
3270
3271 fn record_users(&self, users: Vec<UserRecord>) -> BoxFuture<'static, StoreResult<()>> {
3272 let store = self.clone();
3273 Box::pin(async move { store.record_users_sync(users) })
3274 }
3275
3276 fn user(&self, user_id: InlineId) -> BoxFuture<'static, StoreResult<Option<UserRecord>>> {
3277 let store = self.clone();
3278 Box::pin(async move { store.user_sync(user_id) })
3279 }
3280
3281 fn record_space(&self, space: SpaceRecord) -> BoxFuture<'static, StoreResult<()>> {
3282 let store = self.clone();
3283 Box::pin(async move { store.record_space_sync(space) })
3284 }
3285
3286 fn space(&self, space_id: InlineId) -> BoxFuture<'static, StoreResult<Option<SpaceRecord>>> {
3287 let store = self.clone();
3288 Box::pin(async move { store.space_sync(space_id) })
3289 }
3290
3291 fn record_space_member(
3292 &self,
3293 member: SpaceMemberRecord,
3294 ) -> BoxFuture<'static, StoreResult<()>> {
3295 let store = self.clone();
3296 Box::pin(async move { store.record_space_member_sync(member) })
3297 }
3298
3299 fn record_space_members(
3300 &self,
3301 space_id: InlineId,
3302 members: Vec<SpaceMemberRecord>,
3303 ) -> BoxFuture<'static, StoreResult<()>> {
3304 let store = self.clone();
3305 Box::pin(async move { store.record_space_members_sync(space_id, members) })
3306 }
3307
3308 fn remove_space_member(
3309 &self,
3310 space_id: InlineId,
3311 user_id: InlineId,
3312 ) -> BoxFuture<'static, StoreResult<()>> {
3313 let store = self.clone();
3314 Box::pin(async move { store.remove_space_member_sync(space_id, user_id) })
3315 }
3316
3317 fn space_members(
3318 &self,
3319 space_id: InlineId,
3320 ) -> BoxFuture<'static, StoreResult<Vec<SpaceMemberRecord>>> {
3321 let store = self.clone();
3322 Box::pin(async move { store.space_members_sync(space_id) })
3323 }
3324
3325 fn record_user_settings(
3326 &self,
3327 settings: UserSettingsRecord,
3328 ) -> BoxFuture<'static, StoreResult<()>> {
3329 let store = self.clone();
3330 Box::pin(async move { store.record_user_settings_sync(settings) })
3331 }
3332
3333 fn user_settings(&self) -> BoxFuture<'static, StoreResult<Option<UserSettingsRecord>>> {
3334 let store = self.clone();
3335 Box::pin(async move { store.user_settings_sync() })
3336 }
3337
3338 fn record_chat_participants(
3339 &self,
3340 chat_id: InlineId,
3341 participants: Vec<ChatParticipantRecord>,
3342 ) -> BoxFuture<'static, StoreResult<()>> {
3343 let store = self.clone();
3344 Box::pin(async move { store.record_chat_participants_sync(chat_id, participants) })
3345 }
3346
3347 fn record_chat_participant(
3348 &self,
3349 chat_id: InlineId,
3350 participant: ChatParticipantRecord,
3351 ) -> BoxFuture<'static, StoreResult<()>> {
3352 let store = self.clone();
3353 Box::pin(async move { store.record_chat_participant_sync(chat_id, participant) })
3354 }
3355
3356 fn remove_chat_participant(
3357 &self,
3358 chat_id: InlineId,
3359 user_id: InlineId,
3360 ) -> BoxFuture<'static, StoreResult<()>> {
3361 let store = self.clone();
3362 Box::pin(async move { store.remove_chat_participant_sync(chat_id, user_id) })
3363 }
3364
3365 fn chat_participants(
3366 &self,
3367 chat_id: InlineId,
3368 ) -> BoxFuture<'static, StoreResult<Vec<ChatParticipantRecord>>> {
3369 let store = self.clone();
3370 Box::pin(async move { store.chat_participants_sync(chat_id) })
3371 }
3372
3373 fn chat_participants_complete(
3374 &self,
3375 chat_id: InlineId,
3376 ) -> BoxFuture<'static, StoreResult<bool>> {
3377 let store = self.clone();
3378 Box::pin(async move { store.chat_participants_complete_sync(chat_id) })
3379 }
3380
3381 fn history(&self, request: HistoryRequest) -> BoxFuture<'static, StoreResult<HistoryPage>> {
3382 let store = self.clone();
3383 Box::pin(async move { store.history_sync(request) })
3384 }
3385
3386 fn record_message(&self, message: MessageRecord) -> BoxFuture<'static, StoreResult<()>> {
3387 let store = self.clone();
3388 Box::pin(async move { store.record_message_sync(message) })
3389 }
3390
3391 fn record_message_deleted(
3392 &self,
3393 chat_id: InlineId,
3394 message_id: InlineId,
3395 ) -> BoxFuture<'static, StoreResult<()>> {
3396 let store = self.clone();
3397 Box::pin(async move { store.record_message_deleted_sync(chat_id, message_id) })
3398 }
3399
3400 fn message_deleted(
3401 &self,
3402 chat_id: InlineId,
3403 message_id: InlineId,
3404 ) -> BoxFuture<'static, StoreResult<bool>> {
3405 let store = self.clone();
3406 Box::pin(async move { store.message_deleted_sync(chat_id, message_id) })
3407 }
3408
3409 fn deleted_message_ids(
3410 &self,
3411 chat_id: InlineId,
3412 ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
3413 let store = self.clone();
3414 Box::pin(async move { store.deleted_message_ids_sync(chat_id) })
3415 }
3416
3417 fn clear_chat_messages(
3418 &self,
3419 chat_id: InlineId,
3420 before_date: Option<i64>,
3421 ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
3422 let store = self.clone();
3423 Box::pin(async move { store.clear_chat_messages_sync(chat_id, before_date) })
3424 }
3425
3426 fn chat_ids_in_space(
3427 &self,
3428 space_id: InlineId,
3429 ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
3430 let store = self.clone();
3431 Box::pin(async move { store.chat_ids_in_space_sync(space_id) })
3432 }
3433
3434 fn record_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>> {
3435 let store = self.clone();
3436 Box::pin(async move { store.record_reaction_sync(reaction) })
3437 }
3438
3439 fn remove_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>> {
3440 let store = self.clone();
3441 Box::pin(async move { store.remove_reaction_sync(reaction) })
3442 }
3443
3444 fn replace_message_reactions(
3445 &self,
3446 chat_id: InlineId,
3447 message_id: InlineId,
3448 reactions: Vec<StoredReaction>,
3449 ) -> BoxFuture<'static, StoreResult<()>> {
3450 let store = self.clone();
3451 Box::pin(
3452 async move { store.replace_message_reactions_sync(chat_id, message_id, reactions) },
3453 )
3454 }
3455
3456 fn reactions(
3457 &self,
3458 chat_id: InlineId,
3459 message_id: InlineId,
3460 ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>> {
3461 let store = self.clone();
3462 Box::pin(async move { store.reactions_sync(chat_id, message_id) })
3463 }
3464
3465 fn reactions_for_chat(
3466 &self,
3467 chat_id: InlineId,
3468 ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>> {
3469 let store = self.clone();
3470 Box::pin(async move { store.reactions_for_chat_sync(chat_id) })
3471 }
3472
3473 fn reaction_snapshot_message_ids(
3474 &self,
3475 chat_id: InlineId,
3476 ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
3477 let store = self.clone();
3478 Box::pin(async move { store.reaction_snapshot_message_ids_sync(chat_id) })
3479 }
3480
3481 fn record_read_state(&self, state: StoredReadState) -> BoxFuture<'static, StoreResult<()>> {
3482 let store = self.clone();
3483 Box::pin(async move { store.record_read_state_sync(state) })
3484 }
3485
3486 fn read_state(
3487 &self,
3488 chat_id: InlineId,
3489 ) -> BoxFuture<'static, StoreResult<Option<StoredReadState>>> {
3490 let store = self.clone();
3491 Box::pin(async move { store.read_state_sync(chat_id) })
3492 }
3493
3494 fn record_transaction(
3495 &self,
3496 transaction: StoredTransaction,
3497 ) -> BoxFuture<'static, StoreResult<()>> {
3498 let store = self.clone();
3499 Box::pin(async move { store.record_transaction_sync(transaction) })
3500 }
3501
3502 fn transaction(
3503 &self,
3504 transaction_id: TransactionId,
3505 ) -> BoxFuture<'static, StoreResult<Option<StoredTransaction>>> {
3506 let store = self.clone();
3507 Box::pin(async move { store.transaction_sync(transaction_id) })
3508 }
3509}
3510
3511pub(crate) fn parse_cursor(cursor: Option<&str>) -> StoreResult<usize> {
3512 match cursor {
3513 Some(cursor) if !cursor.trim().is_empty() => cursor.parse::<usize>().map_err(|_| {
3514 StoreError::new(
3515 ClientErrorCategory::InvalidInput,
3516 "invalid pagination cursor",
3517 )
3518 }),
3519 _ => Ok(0),
3520 }
3521}
3522
3523fn parse_stable_dialog_cursor(cursor: Option<&str>) -> StoreResult<i64> {
3524 match cursor {
3525 Some(cursor) if !cursor.trim().is_empty() => {
3526 let value = cursor.parse::<i64>().map_err(|_| {
3527 StoreError::new(
3528 ClientErrorCategory::InvalidInput,
3529 "invalid stable dialog pagination cursor",
3530 )
3531 })?;
3532 if value < 0 {
3533 return Err(StoreError::new(
3534 ClientErrorCategory::InvalidInput,
3535 "invalid stable dialog pagination cursor",
3536 ));
3537 }
3538 Ok(value)
3539 }
3540 _ => Ok(0),
3541 }
3542}
3543
3544fn migrate_sqlite(connection: &Connection) -> StoreResult<()> {
3545 connection
3546 .execute_batch(
3547 "
3548 PRAGMA foreign_keys = ON;
3549
3550 CREATE TABLE IF NOT EXISTS sessions (
3551 id INTEGER PRIMARY KEY CHECK (id = 1),
3552 auth_json TEXT NOT NULL,
3553 account_namespace TEXT,
3554 updated_at INTEGER NOT NULL
3555 );
3556
3557 CREATE TABLE IF NOT EXISTS sync_state (
3558 id INTEGER PRIMARY KEY CHECK (id = 1),
3559 last_sync_date INTEGER NOT NULL,
3560 updated_at INTEGER NOT NULL
3561 );
3562
3563 CREATE TABLE IF NOT EXISTS sync_buckets (
3564 bucket_kind TEXT NOT NULL,
3565 entity_id INTEGER NOT NULL,
3566 seq INTEGER NOT NULL,
3567 date INTEGER NOT NULL,
3568 updated_at INTEGER NOT NULL,
3569 PRIMARY KEY (bucket_kind, entity_id)
3570 );
3571
3572 CREATE TABLE IF NOT EXISTS pending_sync_batches (
3573 bucket_kind TEXT NOT NULL,
3574 entity_id INTEGER NOT NULL,
3575 seq INTEGER NOT NULL,
3576 date INTEGER NOT NULL,
3577 payload BLOB NOT NULL,
3578 updated_at INTEGER NOT NULL,
3579 PRIMARY KEY (bucket_kind, entity_id)
3580 );
3581
3582 CREATE TABLE IF NOT EXISTS client_event_outbox (
3583 delivery_id INTEGER PRIMARY KEY AUTOINCREMENT,
3584 event_json TEXT NOT NULL,
3585 created_at INTEGER NOT NULL
3586 );
3587
3588 CREATE TABLE IF NOT EXISTS dialogs (
3589 chat_id INTEGER PRIMARY KEY,
3590 peer_user_id INTEGER,
3591 title TEXT,
3592 emoji TEXT,
3593 last_message_id INTEGER,
3594 unread_count INTEGER,
3595 space_id INTEGER,
3596 is_public INTEGER,
3597 archived INTEGER,
3598 pinned INTEGER,
3599 open INTEGER,
3600 chat_list_hidden INTEGER,
3601 list_order TEXT,
3602 pinned_order TEXT,
3603 notification_mode TEXT,
3604 follow_mode TEXT,
3605 pinned_message_ids_json TEXT NOT NULL DEFAULT '[]',
3606 updated_at INTEGER NOT NULL
3607 );
3608
3609 CREATE TABLE IF NOT EXISTS chat_tombstones (
3610 chat_id INTEGER PRIMARY KEY,
3611 deleted_at INTEGER NOT NULL
3612 );
3613
3614 CREATE TABLE IF NOT EXISTS users (
3615 user_id INTEGER PRIMARY KEY,
3616 display_name TEXT,
3617 username TEXT,
3618 first_name TEXT,
3619 last_name TEXT,
3620 avatar_url TEXT,
3621 is_bot INTEGER,
3622 updated_at INTEGER NOT NULL
3623 );
3624
3625 CREATE TABLE IF NOT EXISTS spaces (
3626 space_id INTEGER PRIMARY KEY,
3627 record_json TEXT NOT NULL,
3628 updated_at INTEGER NOT NULL
3629 );
3630
3631 CREATE TABLE IF NOT EXISTS space_members (
3632 space_id INTEGER NOT NULL,
3633 user_id INTEGER NOT NULL,
3634 record_json TEXT NOT NULL,
3635 updated_at INTEGER NOT NULL,
3636 PRIMARY KEY (space_id, user_id)
3637 );
3638
3639 CREATE INDEX IF NOT EXISTS idx_space_members_space
3640 ON space_members (space_id, user_id);
3641
3642 CREATE TABLE IF NOT EXISTS user_settings (
3643 id INTEGER PRIMARY KEY CHECK (id = 1),
3644 settings_json TEXT NOT NULL,
3645 updated_at INTEGER NOT NULL
3646 );
3647
3648 CREATE TABLE IF NOT EXISTS chat_participants (
3649 chat_id INTEGER NOT NULL,
3650 user_id INTEGER NOT NULL,
3651 date INTEGER,
3652 updated_at INTEGER NOT NULL,
3653 PRIMARY KEY (chat_id, user_id)
3654 );
3655
3656 CREATE TABLE IF NOT EXISTS chat_participant_snapshots (
3657 chat_id INTEGER PRIMARY KEY,
3658 updated_at INTEGER NOT NULL
3659 );
3660
3661 CREATE TABLE IF NOT EXISTS messages (
3662 chat_id INTEGER NOT NULL,
3663 message_id INTEGER NOT NULL,
3664 sender_id INTEGER NOT NULL,
3665 timestamp INTEGER NOT NULL,
3666 is_outgoing INTEGER NOT NULL,
3667 content_json TEXT NOT NULL,
3668 reply_to_message_id INTEGER,
3669 transaction_json TEXT,
3670 PRIMARY KEY (chat_id, message_id)
3671 );
3672
3673 CREATE INDEX IF NOT EXISTS idx_messages_chat_message
3674 ON messages (chat_id, message_id DESC);
3675
3676 CREATE TABLE IF NOT EXISTS message_tombstones (
3677 chat_id INTEGER NOT NULL,
3678 message_id INTEGER NOT NULL,
3679 deleted_at INTEGER NOT NULL,
3680 PRIMARY KEY (chat_id, message_id)
3681 );
3682
3683 CREATE TABLE IF NOT EXISTS reactions (
3684 chat_id INTEGER NOT NULL,
3685 message_id INTEGER NOT NULL,
3686 user_id INTEGER NOT NULL,
3687 reaction TEXT NOT NULL,
3688 updated_at INTEGER NOT NULL,
3689 PRIMARY KEY (chat_id, message_id, user_id, reaction)
3690 );
3691
3692 CREATE INDEX IF NOT EXISTS idx_reactions_message
3693 ON reactions (chat_id, message_id);
3694
3695 CREATE TABLE IF NOT EXISTS reaction_snapshots (
3696 chat_id INTEGER NOT NULL,
3697 message_id INTEGER NOT NULL,
3698 updated_at INTEGER NOT NULL,
3699 PRIMARY KEY (chat_id, message_id)
3700 );
3701
3702 CREATE TABLE IF NOT EXISTS read_states (
3703 chat_id INTEGER PRIMARY KEY,
3704 read_max_id INTEGER,
3705 unread_count INTEGER,
3706 marked_unread INTEGER NOT NULL,
3707 updated_at INTEGER NOT NULL
3708 );
3709
3710 CREATE TABLE IF NOT EXISTS transactions (
3711 transaction_id TEXT PRIMARY KEY,
3712 identity_json TEXT NOT NULL,
3713 state_json TEXT NOT NULL,
3714 chat_id INTEGER,
3715 message_id INTEGER,
3716 random_id INTEGER NOT NULL,
3717 external_source TEXT,
3718 external_id TEXT,
3719 failure_json TEXT,
3720 created_at INTEGER NOT NULL,
3721 updated_at INTEGER NOT NULL
3722 );
3723
3724 CREATE INDEX IF NOT EXISTS idx_transactions_external
3725 ON transactions (external_source, external_id);
3726 CREATE INDEX IF NOT EXISTS idx_transactions_random
3727 ON transactions (random_id);
3728 ",
3729 )
3730 .map_err(sqlite_error)?;
3731 ensure_sqlite_column(connection, "sessions", "account_namespace", "TEXT")?;
3732 ensure_sqlite_column(connection, "dialogs", "peer_user_id", "INTEGER")?;
3733 ensure_sqlite_column(connection, "dialogs", "emoji", "TEXT")?;
3734 ensure_sqlite_column(connection, "dialogs", "space_id", "INTEGER")?;
3735 ensure_sqlite_column(connection, "dialogs", "is_public", "INTEGER")?;
3736 ensure_sqlite_column(connection, "dialogs", "archived", "INTEGER")?;
3737 ensure_sqlite_column(connection, "dialogs", "pinned", "INTEGER")?;
3738 ensure_sqlite_column(connection, "dialogs", "open", "INTEGER")?;
3739 ensure_sqlite_column(connection, "dialogs", "chat_list_hidden", "INTEGER")?;
3740 ensure_sqlite_column(connection, "dialogs", "list_order", "TEXT")?;
3741 ensure_sqlite_column(connection, "dialogs", "pinned_order", "TEXT")?;
3742 ensure_sqlite_column(connection, "dialogs", "notification_mode", "TEXT")?;
3743 ensure_sqlite_column(connection, "dialogs", "follow_mode", "TEXT")?;
3744 ensure_sqlite_column(
3745 connection,
3746 "dialogs",
3747 "pinned_message_ids_json",
3748 "TEXT NOT NULL DEFAULT '[]'",
3749 )?;
3750 Ok(())
3751}
3752
3753fn ensure_sqlite_column(
3754 connection: &Connection,
3755 table: &str,
3756 column: &str,
3757 definition: &str,
3758) -> StoreResult<()> {
3759 let pragma = format!("PRAGMA table_info({table})");
3760 let mut stmt = connection.prepare(&pragma).map_err(sqlite_error)?;
3761 let columns = stmt
3762 .query_map([], |row| row.get::<_, String>(1))
3763 .map_err(sqlite_error)?
3764 .collect::<Result<Vec<_>, _>>()
3765 .map_err(sqlite_error)?;
3766 if columns.iter().any(|name| name == column) {
3767 return Ok(());
3768 }
3769 connection
3770 .execute(
3771 &format!("ALTER TABLE {table} ADD COLUMN {column} {definition}"),
3772 [],
3773 )
3774 .map_err(sqlite_error)?;
3775 Ok(())
3776}
3777
3778fn sync_bucket_key_parts(key: SyncBucketKey) -> (&'static str, i64) {
3779 match key {
3780 SyncBucketKey::User => ("user", 0),
3781 SyncBucketKey::Space { space_id } => ("space", space_id.get()),
3782 SyncBucketKey::Chat {
3783 peer: SyncBucketPeer::User { user_id },
3784 } => ("chat_user", user_id.get()),
3785 SyncBucketKey::Chat {
3786 peer: SyncBucketPeer::Chat { chat_id },
3787 } => ("chat_chat", chat_id.get()),
3788 }
3789}
3790
3791fn sync_bucket_key_from_parts(kind: &str, entity_id: i64) -> StoreResult<SyncBucketKey> {
3792 match kind {
3793 "user" if entity_id == 0 => Ok(SyncBucketKey::User),
3794 "space" => Ok(SyncBucketKey::Space {
3795 space_id: InlineId::new(entity_id),
3796 }),
3797 "chat_user" => Ok(SyncBucketKey::Chat {
3798 peer: SyncBucketPeer::User {
3799 user_id: InlineId::new(entity_id),
3800 },
3801 }),
3802 "chat_chat" => Ok(SyncBucketKey::Chat {
3803 peer: SyncBucketPeer::Chat {
3804 chat_id: InlineId::new(entity_id),
3805 },
3806 }),
3807 _ => Err(StoreError::internal(format!(
3808 "invalid stored sync bucket {kind:?}/{entity_id}"
3809 ))),
3810 }
3811}
3812
3813fn sync_bucket_sort_key(key: SyncBucketKey) -> (u8, i64) {
3814 match key {
3815 SyncBucketKey::User => (0, 0),
3816 SyncBucketKey::Space { space_id } => (1, space_id.get()),
3817 SyncBucketKey::Chat {
3818 peer: SyncBucketPeer::User { user_id },
3819 } => (2, user_id.get()),
3820 SyncBucketKey::Chat {
3821 peer: SyncBucketPeer::Chat { chat_id },
3822 } => (3, chat_id.get()),
3823 }
3824}
3825
3826fn upsert_sync_bucket(
3827 connection: &Connection,
3828 kind: &str,
3829 entity_id: i64,
3830 state: SyncBucketState,
3831) -> StoreResult<()> {
3832 connection
3833 .execute(
3834 "INSERT INTO sync_buckets (bucket_kind, entity_id, seq, date, updated_at)
3835 VALUES (?1, ?2, ?3, ?4, ?5)
3836 ON CONFLICT(bucket_kind, entity_id) DO UPDATE SET
3837 seq = excluded.seq,
3838 date = excluded.date,
3839 updated_at = excluded.updated_at",
3840 params![kind, entity_id, state.seq, state.date, now_seconds()],
3841 )
3842 .map_err(sqlite_error)?;
3843 Ok(())
3844}
3845
3846fn upsert_sqlite_user(connection: &Connection, user: UserRecord) -> StoreResult<()> {
3847 connection
3848 .execute(
3849 "INSERT INTO users (
3850 user_id, display_name, username, first_name, last_name,
3851 avatar_url, is_bot, updated_at
3852 )
3853 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
3854 ON CONFLICT(user_id) DO UPDATE SET
3855 display_name = excluded.display_name,
3856 username = excluded.username,
3857 first_name = excluded.first_name,
3858 last_name = excluded.last_name,
3859 avatar_url = excluded.avatar_url,
3860 is_bot = excluded.is_bot,
3861 updated_at = excluded.updated_at",
3862 params![
3863 user.user_id.get(),
3864 user.display_name,
3865 user.username,
3866 user.first_name,
3867 user.last_name,
3868 user.avatar_url,
3869 user.is_bot.map(|is_bot| if is_bot { 1_i64 } else { 0_i64 }),
3870 now_seconds(),
3871 ],
3872 )
3873 .map_err(sqlite_error)?;
3874 Ok(())
3875}
3876
3877fn upsert_sqlite_participant(
3878 connection: &Connection,
3879 chat_id: InlineId,
3880 participant: ChatParticipantRecord,
3881) -> StoreResult<()> {
3882 connection
3883 .execute(
3884 "INSERT INTO chat_participants (chat_id, user_id, date, updated_at)
3885 VALUES (?1, ?2, ?3, ?4)
3886 ON CONFLICT(chat_id, user_id) DO UPDATE SET
3887 date = excluded.date,
3888 updated_at = excluded.updated_at",
3889 params![
3890 chat_id.get(),
3891 participant.user_id.get(),
3892 participant.date,
3893 now_seconds(),
3894 ],
3895 )
3896 .map_err(sqlite_error)?;
3897 Ok(())
3898}
3899
3900fn sqlite_dialog_from_row(row: &Row<'_>) -> rusqlite::Result<DialogRecord> {
3901 let pinned_json = row.get::<_, String>(17)?;
3902 let pinned_message_ids = serde_json::from_str(&pinned_json).map_err(|error| {
3903 rusqlite::Error::FromSqlConversionFailure(17, Type::Text, Box::new(error))
3904 })?;
3905 Ok(DialogRecord {
3906 chat_id: InlineId::new(row.get(0)?),
3907 peer_user_id: row.get::<_, Option<i64>>(1)?.map(InlineId::new),
3908 title: row.get(2)?,
3909 emoji: row.get(3)?,
3910 last_message_id: row.get::<_, Option<i64>>(4)?.map(InlineId::new),
3911 synced_through_message_id: row.get::<_, Option<i64>>(5)?.map(InlineId::new),
3912 unread_count: row
3913 .get::<_, Option<i64>>(6)?
3914 .and_then(|value| u32::try_from(value).ok()),
3915 space_id: row.get::<_, Option<i64>>(7)?.map(InlineId::new),
3916 is_public: row.get::<_, Option<bool>>(8)?,
3917 archived: row.get::<_, Option<bool>>(9)?,
3918 pinned: row.get::<_, Option<bool>>(10)?,
3919 open: row.get::<_, Option<bool>>(11)?,
3920 chat_list_hidden: row.get::<_, Option<bool>>(12)?,
3921 order: row.get(13)?,
3922 pinned_order: row.get(14)?,
3923 notification_mode: row
3924 .get::<_, Option<String>>(15)?
3925 .as_deref()
3926 .and_then(dialog_notification_mode_from_store),
3927 follow_mode: row
3928 .get::<_, Option<String>>(16)?
3929 .as_deref()
3930 .and_then(dialog_follow_mode_from_store),
3931 pinned_message_ids,
3932 })
3933}
3934
3935fn upsert_sqlite_dialog(connection: &Connection, dialog: DialogRecord) -> StoreResult<()> {
3936 let chat_id = dialog.chat_id;
3937 let pinned_message_ids_json = serde_json::to_string(&dialog.pinned_message_ids)
3938 .map_err(|error| StoreError::internal(format!("encode pinned message IDs: {error}")))?;
3939 connection
3940 .execute(
3941 "INSERT INTO dialogs (
3942 chat_id, peer_user_id, title, emoji, last_message_id, unread_count,
3943 space_id, is_public, archived, pinned, open, chat_list_hidden,
3944 list_order, pinned_order, notification_mode, follow_mode,
3945 pinned_message_ids_json, updated_at
3946 ) VALUES (
3947 ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13,
3948 ?14, ?15, ?16, ?17, ?18
3949 )
3950 ON CONFLICT(chat_id) DO UPDATE SET
3951 peer_user_id = excluded.peer_user_id,
3952 title = excluded.title,
3953 emoji = excluded.emoji,
3954 last_message_id = excluded.last_message_id,
3955 unread_count = excluded.unread_count,
3956 space_id = excluded.space_id,
3957 is_public = excluded.is_public,
3958 archived = excluded.archived,
3959 pinned = excluded.pinned,
3960 open = excluded.open,
3961 chat_list_hidden = excluded.chat_list_hidden,
3962 list_order = excluded.list_order,
3963 pinned_order = excluded.pinned_order,
3964 notification_mode = excluded.notification_mode,
3965 follow_mode = excluded.follow_mode,
3966 pinned_message_ids_json = excluded.pinned_message_ids_json,
3967 updated_at = excluded.updated_at",
3968 params![
3969 dialog.chat_id.get(),
3970 dialog.peer_user_id.map(InlineId::get),
3971 dialog.title,
3972 dialog.emoji,
3973 dialog.last_message_id.map(InlineId::get),
3974 dialog.unread_count.map(i64::from),
3975 dialog.space_id.map(InlineId::get),
3976 dialog.is_public.map(i64::from),
3977 dialog.archived.map(i64::from),
3978 dialog.pinned.map(i64::from),
3979 dialog.open.map(i64::from),
3980 dialog.chat_list_hidden.map(i64::from),
3981 dialog.order,
3982 dialog.pinned_order,
3983 dialog
3984 .notification_mode
3985 .map(dialog_notification_mode_for_store),
3986 dialog.follow_mode.map(dialog_follow_mode_for_store),
3987 pinned_message_ids_json,
3988 now_seconds(),
3989 ],
3990 )
3991 .map_err(sqlite_error)?;
3992 connection
3993 .execute(
3994 "DELETE FROM chat_tombstones WHERE chat_id = ?1",
3995 params![chat_id.get()],
3996 )
3997 .map_err(sqlite_error)?;
3998 Ok(())
3999}
4000
4001const fn dialog_notification_mode_for_store(mode: DialogNotificationMode) -> &'static str {
4002 match mode {
4003 DialogNotificationMode::All => "all",
4004 DialogNotificationMode::Mentions => "mentions",
4005 DialogNotificationMode::None => "none",
4006 }
4007}
4008
4009fn dialog_notification_mode_from_store(value: &str) -> Option<DialogNotificationMode> {
4010 match value {
4011 "all" => Some(DialogNotificationMode::All),
4012 "mentions" => Some(DialogNotificationMode::Mentions),
4013 "none" => Some(DialogNotificationMode::None),
4014 _ => None,
4015 }
4016}
4017
4018const fn dialog_follow_mode_for_store(mode: DialogFollowMode) -> &'static str {
4019 match mode {
4020 DialogFollowMode::Following => "following",
4021 DialogFollowMode::Unfollowed => "unfollowed",
4022 }
4023}
4024
4025fn dialog_follow_mode_from_store(value: &str) -> Option<DialogFollowMode> {
4026 match value {
4027 "following" => Some(DialogFollowMode::Following),
4028 "unfollowed" => Some(DialogFollowMode::Unfollowed),
4029 _ => None,
4030 }
4031}
4032
4033fn upsert_sqlite_dialog_last_message(
4034 connection: &Connection,
4035 chat_id: InlineId,
4036 message_id: InlineId,
4037) -> StoreResult<()> {
4038 let updated = connection
4039 .execute(
4040 "UPDATE dialogs
4041 SET last_message_id = ?1, updated_at = ?2
4042 WHERE chat_id = ?3",
4043 params![message_id.get(), now_seconds(), chat_id.get()],
4044 )
4045 .map_err(sqlite_error)?;
4046 if updated == 0 {
4047 connection
4048 .execute(
4049 "INSERT INTO dialogs (chat_id, title, last_message_id, unread_count, updated_at)
4050 VALUES (?1, NULL, ?2, 0, ?3)",
4051 params![chat_id.get(), message_id.get(), now_seconds()],
4052 )
4053 .map_err(sqlite_error)?;
4054 }
4055 connection
4056 .execute(
4057 "DELETE FROM chat_tombstones WHERE chat_id = ?1",
4058 params![chat_id.get()],
4059 )
4060 .map_err(sqlite_error)?;
4061 Ok(())
4062}
4063
4064fn all_sqlite_users(connection: &Connection) -> StoreResult<Vec<UserRecord>> {
4065 let mut stmt = connection
4066 .prepare(
4067 "SELECT user_id, display_name, username, first_name, last_name, avatar_url, is_bot
4068 FROM users
4069 ORDER BY user_id ASC",
4070 )
4071 .map_err(sqlite_error)?;
4072 query_user_rows(&mut stmt, [])
4073}
4074
4075fn sqlite_users_for_messages(
4076 connection: &Connection,
4077 messages: &[MessageRecord],
4078) -> StoreResult<Vec<UserRecord>> {
4079 let mut user_ids = messages
4080 .iter()
4081 .map(|message| message.sender_id.get())
4082 .collect::<Vec<_>>();
4083 user_ids.sort_unstable();
4084 user_ids.dedup();
4085
4086 let mut users = Vec::new();
4087 let mut stmt = connection
4088 .prepare(
4089 "SELECT user_id, display_name, username, first_name, last_name, avatar_url, is_bot
4090 FROM users
4091 WHERE user_id = ?1",
4092 )
4093 .map_err(sqlite_error)?;
4094 for user_id in user_ids {
4095 if let Some(user) = stmt
4096 .query_row(params![user_id], sqlite_user_from_row)
4097 .optional()
4098 .map_err(sqlite_error)?
4099 {
4100 users.push(user);
4101 }
4102 }
4103 Ok(users)
4104}
4105
4106fn query_user_rows<P>(stmt: &mut rusqlite::Statement<'_>, params: P) -> StoreResult<Vec<UserRecord>>
4107where
4108 P: rusqlite::Params,
4109{
4110 stmt.query_map(params, sqlite_user_from_row)
4111 .map_err(sqlite_error)?
4112 .collect::<Result<Vec<_>, _>>()
4113 .map_err(sqlite_error)
4114}
4115
4116fn sqlite_user_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<UserRecord> {
4117 Ok(UserRecord {
4118 user_id: InlineId::new(row.get::<_, i64>(0)?),
4119 display_name: row.get::<_, Option<String>>(1)?,
4120 username: row.get::<_, Option<String>>(2)?,
4121 first_name: row.get::<_, Option<String>>(3)?,
4122 last_name: row.get::<_, Option<String>>(4)?,
4123 avatar_url: row.get::<_, Option<String>>(5)?,
4124 is_bot: row.get::<_, Option<i64>>(6)?.map(|value| value != 0),
4125 })
4126}
4127
4128#[derive(Debug)]
4129struct RawSqliteMessage {
4130 chat_id: i64,
4131 message_id: i64,
4132 sender_id: i64,
4133 timestamp: i64,
4134 is_outgoing: bool,
4135 content_json: String,
4136 reply_to_message_id: Option<i64>,
4137 transaction_json: Option<String>,
4138}
4139
4140fn query_message_rows<P>(
4141 stmt: &mut rusqlite::Statement<'_>,
4142 params: P,
4143) -> StoreResult<Vec<RawSqliteMessage>>
4144where
4145 P: rusqlite::Params,
4146{
4147 stmt.query_map(params, |row| {
4148 Ok(RawSqliteMessage {
4149 chat_id: row.get(0)?,
4150 message_id: row.get(1)?,
4151 sender_id: row.get(2)?,
4152 timestamp: row.get(3)?,
4153 is_outgoing: row.get::<_, i64>(4)? != 0,
4154 content_json: row.get(5)?,
4155 reply_to_message_id: row.get(6)?,
4156 transaction_json: row.get(7)?,
4157 })
4158 })
4159 .map_err(sqlite_error)?
4160 .collect::<Result<Vec<_>, _>>()
4161 .map_err(sqlite_error)
4162}
4163
4164fn raw_sqlite_message_to_record(raw: RawSqliteMessage) -> StoreResult<MessageRecord> {
4165 let content = serde_json::from_str::<MessageContent>(&raw.content_json)
4166 .map_err(|error| StoreError::internal(format!("decode message content: {error}")))?;
4167 let transaction = raw
4168 .transaction_json
4169 .as_deref()
4170 .map(serde_json::from_str::<TransactionIdentity>)
4171 .transpose()
4172 .map_err(|error| StoreError::internal(format!("decode transaction identity: {error}")))?;
4173 Ok(MessageRecord {
4174 chat_id: InlineId::new(raw.chat_id),
4175 message_id: InlineId::new(raw.message_id),
4176 sender_id: InlineId::new(raw.sender_id),
4177 timestamp: raw.timestamp,
4178 is_outgoing: raw.is_outgoing,
4179 content,
4180 reply_to_message_id: raw.reply_to_message_id.map(InlineId::new),
4181 transaction,
4182 })
4183}
4184
4185#[derive(Debug)]
4186struct RawSqliteTransaction {
4187 identity_json: String,
4188 state_json: String,
4189 chat_id: Option<i64>,
4190 message_id: Option<i64>,
4191 failure_json: Option<String>,
4192 created_at: i64,
4193 updated_at: i64,
4194}
4195
4196fn raw_sqlite_transaction_to_record(raw: RawSqliteTransaction) -> StoreResult<StoredTransaction> {
4197 let identity = serde_json::from_str::<TransactionIdentity>(&raw.identity_json)
4198 .map_err(|error| StoreError::internal(format!("decode transaction identity: {error}")))?;
4199 let state = serde_json::from_str::<TransactionState>(&raw.state_json)
4200 .map_err(|error| StoreError::internal(format!("decode transaction state: {error}")))?;
4201 let failure = raw
4202 .failure_json
4203 .as_deref()
4204 .map(serde_json::from_str::<ClientFailure>)
4205 .transpose()
4206 .map_err(|error| StoreError::internal(format!("decode transaction failure: {error}")))?;
4207 Ok(StoredTransaction {
4208 identity,
4209 state,
4210 chat_id: raw.chat_id.map(InlineId::new),
4211 message_id: raw.message_id.map(InlineId::new),
4212 failure,
4213 created_at: raw.created_at,
4214 updated_at: raw.updated_at,
4215 })
4216}
4217
4218fn sqlite_error(error: rusqlite::Error) -> StoreError {
4219 StoreError::internal(format!("sqlite store error: {error}"))
4220}
4221
4222fn now_seconds() -> i64 {
4223 SystemTime::now()
4224 .duration_since(UNIX_EPOCH)
4225 .map(|duration| duration.as_secs() as i64)
4226 .unwrap_or_default()
4227}
4228
4229fn all_users_from_memory(users: &HashMap<i64, UserRecord>) -> Vec<UserRecord> {
4230 let mut users = users.values().cloned().collect::<Vec<_>>();
4231 users.sort_by_key(|user| user.user_id.get());
4232 users
4233}
4234
4235fn max_message_id_from_memory(
4236 messages: &HashMap<i64, Vec<MessageRecord>>,
4237 chat_id: InlineId,
4238) -> Option<InlineId> {
4239 messages
4240 .get(&chat_id.get())?
4241 .iter()
4242 .map(|message| message.message_id.get())
4243 .max()
4244 .map(InlineId::new)
4245}
4246
4247fn dialog_with_synced_through(
4248 mut dialog: DialogRecord,
4249 synced_through_message_id: Option<InlineId>,
4250) -> DialogRecord {
4251 dialog.synced_through_message_id = synced_through_message_id;
4252 dialog
4253}
4254
4255fn users_for_messages_from_memory(
4256 users: &HashMap<i64, UserRecord>,
4257 messages: &[MessageRecord],
4258) -> Vec<UserRecord> {
4259 let mut user_ids = messages
4260 .iter()
4261 .map(|message| message.sender_id.get())
4262 .collect::<Vec<_>>();
4263 user_ids.sort_unstable();
4264 user_ids.dedup();
4265 user_ids
4266 .into_iter()
4267 .filter_map(|user_id| users.get(&user_id).cloned())
4268 .collect()
4269}
4270
4271fn upsert_dialog(dialogs: &mut Vec<DialogRecord>, dialog: DialogRecord) {
4272 if let Some(existing) = dialogs
4273 .iter_mut()
4274 .find(|existing| existing.chat_id == dialog.chat_id)
4275 {
4276 *existing = dialog;
4277 return;
4278 }
4279 dialogs.push(dialog);
4280}
4281
4282fn insert_message(messages: &mut HashMap<i64, Vec<MessageRecord>>, message: MessageRecord) {
4283 let chat_id = message.chat_id.get();
4284 messages.entry(chat_id).or_default().push(message);
4285 if let Some(messages) = messages.get_mut(&chat_id) {
4286 messages.sort_by_key(|message| (message.timestamp, message.message_id.get()));
4287 }
4288}
4289
4290fn reaction_key(reaction: &StoredReaction) -> (i64, i64, i64, String) {
4291 (
4292 reaction.chat_id.get(),
4293 reaction.message_id.get(),
4294 reaction.user_id.get(),
4295 reaction.reaction.clone(),
4296 )
4297}
4298
4299fn sort_reactions(reactions: &mut [StoredReaction]) {
4300 reactions.sort_by(|left, right| {
4301 (left.user_id.get(), left.reaction.as_str())
4302 .cmp(&(right.user_id.get(), right.reaction.as_str()))
4303 });
4304}
4305
4306fn sort_participants(participants: &mut [ChatParticipantRecord]) {
4307 participants.sort_by_key(|participant| participant.user_id.get());
4308}
4309
4310pub(crate) fn upsert_dialog_last_message(
4312 dialogs: &mut Vec<DialogRecord>,
4313 chat_id: InlineId,
4314 message_id: InlineId,
4315) {
4316 if let Some(dialog) = dialogs.iter_mut().find(|dialog| dialog.chat_id == chat_id) {
4317 dialog.last_message_id = Some(message_id);
4318 return;
4319 }
4320 dialogs.push(DialogRecord {
4321 chat_id,
4322 peer_user_id: None,
4323 title: None,
4324 last_message_id: Some(message_id),
4325 synced_through_message_id: None,
4326 unread_count: Some(0),
4327 ..DialogRecord::new(chat_id)
4328 });
4329}
4330
4331#[cfg(test)]
4332mod tests {
4333 use crate::{AuthToken, ExternalId, RandomId};
4334
4335 use super::*;
4336
4337 fn session() -> StoredSession {
4338 StoredSession {
4339 auth: AuthCredential::AccessToken {
4340 token: AuthToken::try_new("secret-token").unwrap(),
4341 },
4342 account_namespace: Some("team".to_owned()),
4343 }
4344 }
4345
4346 fn test_message(message_id: i64) -> MessageRecord {
4347 MessageRecord {
4348 chat_id: InlineId::new(7),
4349 message_id: InlineId::new(message_id),
4350 sender_id: InlineId::new(2),
4351 timestamp: message_id,
4352 is_outgoing: false,
4353 content: MessageContent::Text {
4354 text: format!("message {message_id}"),
4355 },
4356 reply_to_message_id: None,
4357 transaction: None,
4358 }
4359 }
4360
4361 #[tokio::test]
4362 async fn stored_session_debug_redacts_token() {
4363 let rendered = format!("{:?}", session());
4364
4365 assert!(rendered.contains("[redacted]"));
4366 assert!(rendered.contains("<redacted>"));
4367 assert!(!rendered.contains("secret-token"));
4368 assert!(!rendered.contains("team"));
4369 }
4370
4371 #[tokio::test]
4372 async fn in_memory_store_saves_and_clears_session() {
4373 let store = InMemoryStore::new();
4374 store.save_session(session()).await.unwrap();
4375
4376 assert!(store.load_session().await.unwrap().is_some());
4377
4378 store.clear_session().await.unwrap();
4379 assert!(store.load_session().await.unwrap().is_none());
4380 }
4381
4382 #[tokio::test]
4383 async fn in_memory_store_round_trips_sync_cursors() {
4384 let store = InMemoryStore::new();
4385 let user = SyncBucketKey::User;
4386 let direct = SyncBucketKey::Chat {
4387 peer: SyncBucketPeer::User {
4388 user_id: InlineId::new(2),
4389 },
4390 };
4391 let chat = SyncBucketKey::Chat {
4392 peer: SyncBucketPeer::Chat {
4393 chat_id: InlineId::new(7),
4394 },
4395 };
4396 let space = SyncBucketKey::Space {
4397 space_id: InlineId::new(9),
4398 };
4399
4400 assert_eq!(store.sync_state().await.unwrap(), SyncState::default());
4401 assert_eq!(
4402 store.sync_bucket_state(user).await.unwrap(),
4403 SyncBucketState::default()
4404 );
4405
4406 store
4407 .save_sync_state(SyncState { last_sync_date: 40 })
4408 .await
4409 .unwrap();
4410 store
4411 .save_sync_bucket_state(user, SyncBucketState { seq: 3, date: 30 })
4412 .await
4413 .unwrap();
4414 store
4415 .save_sync_bucket_states(vec![
4416 (direct, SyncBucketState { seq: 4, date: 31 }),
4417 (chat, SyncBucketState { seq: 5, date: 32 }),
4418 (space, SyncBucketState { seq: 6, date: 33 }),
4419 ])
4420 .await
4421 .unwrap();
4422 store
4423 .save_sync_bucket_state(user, SyncBucketState { seq: 7, date: 41 })
4424 .await
4425 .unwrap();
4426
4427 assert_eq!(
4428 store.sync_state().await.unwrap(),
4429 SyncState { last_sync_date: 40 }
4430 );
4431 assert_eq!(
4432 store.sync_bucket_state(user).await.unwrap(),
4433 SyncBucketState { seq: 7, date: 41 }
4434 );
4435 assert_eq!(
4436 store.sync_bucket_state(direct).await.unwrap(),
4437 SyncBucketState { seq: 4, date: 31 }
4438 );
4439 assert_eq!(
4440 store.sync_bucket_state(chat).await.unwrap(),
4441 SyncBucketState { seq: 5, date: 32 }
4442 );
4443 assert_eq!(
4444 store.sync_bucket_state(space).await.unwrap(),
4445 SyncBucketState { seq: 6, date: 33 }
4446 );
4447
4448 store.remove_sync_bucket_state(direct).await.unwrap();
4449 assert_eq!(
4450 store.sync_bucket_state(direct).await.unwrap(),
4451 SyncBucketState::default()
4452 );
4453
4454 store.clear_sync_state().await.unwrap();
4455 assert_eq!(store.sync_state().await.unwrap(), SyncState::default());
4456 assert_eq!(
4457 store.sync_bucket_state(user).await.unwrap(),
4458 SyncBucketState::default()
4459 );
4460 assert_eq!(
4461 store.sync_bucket_state(space).await.unwrap(),
4462 SyncBucketState::default()
4463 );
4464 }
4465
4466 #[tokio::test]
4467 async fn in_memory_store_lists_dialogs_and_history() {
4468 let store = InMemoryStore::new();
4469 store.upsert_dialog(DialogRecord {
4470 chat_id: InlineId::new(7),
4471 peer_user_id: None,
4472 title: Some("general".to_owned()),
4473 last_message_id: None,
4474 synced_through_message_id: None,
4475 unread_count: Some(0),
4476 ..DialogRecord::new(InlineId::new(7))
4477 });
4478 store
4479 .record_users(vec![UserRecord {
4480 user_id: InlineId::new(2),
4481 display_name: Some("Ada Lovelace".to_owned()),
4482 username: Some("ada".to_owned()),
4483 first_name: Some("Ada".to_owned()),
4484 last_name: Some("Lovelace".to_owned()),
4485 avatar_url: None,
4486 is_bot: Some(false),
4487 }])
4488 .await
4489 .unwrap();
4490 store
4491 .record_message(MessageRecord {
4492 chat_id: InlineId::new(7),
4493 message_id: InlineId::new(1),
4494 sender_id: InlineId::new(2),
4495 timestamp: 1,
4496 is_outgoing: false,
4497 content: MessageContent::Text {
4498 text: "hello".to_owned(),
4499 },
4500 reply_to_message_id: None,
4501 transaction: None,
4502 })
4503 .await
4504 .unwrap();
4505
4506 let dialogs = store.dialogs(DialogsRequest::default()).await.unwrap();
4507 assert_eq!(dialogs.dialogs.len(), 1);
4508 assert_eq!(
4509 dialogs.dialogs[0].synced_through_message_id,
4510 Some(InlineId::new(1))
4511 );
4512 assert_eq!(dialogs.users.len(), 1);
4513 assert_eq!(
4514 dialogs.users[0].display_name.as_deref(),
4515 Some("Ada Lovelace")
4516 );
4517
4518 let history = store
4519 .history(HistoryRequest {
4520 chat_id: InlineId::new(7),
4521 limit: Some(10),
4522 before_message_id: None,
4523 after_message_id: None,
4524 })
4525 .await
4526 .unwrap();
4527 assert_eq!(history.messages.len(), 1);
4528 assert_eq!(history.users.len(), 1);
4529 assert_eq!(history.users[0].user_id, InlineId::new(2));
4530 }
4531
4532 #[tokio::test]
4533 async fn stable_dialog_cursor_does_not_duplicate_when_activity_order_changes() {
4534 let store = InMemoryStore::new();
4535 for (chat_id, last_message_id) in [(10, 100), (30, 300), (50, 500)] {
4536 store.upsert_dialog(DialogRecord {
4537 chat_id: InlineId::new(chat_id),
4538 last_message_id: Some(InlineId::new(last_message_id)),
4539 ..DialogRecord::new(InlineId::new(chat_id))
4540 });
4541 }
4542
4543 let first = store
4544 .dialogs(DialogsRequest {
4545 limit: Some(2),
4546 cursor: None,
4547 order: DialogsOrder::StableChatId,
4548 })
4549 .await
4550 .unwrap();
4551 assert_eq!(
4552 first
4553 .dialogs
4554 .iter()
4555 .map(|dialog| dialog.chat_id.get())
4556 .collect::<Vec<_>>(),
4557 vec![10, 30]
4558 );
4559 assert_eq!(first.next_cursor.as_deref(), Some("30"));
4560
4561 store.upsert_dialog(DialogRecord {
4562 chat_id: InlineId::new(50),
4563 last_message_id: Some(InlineId::new(1_000)),
4564 ..DialogRecord::new(InlineId::new(50))
4565 });
4566 store.upsert_dialog(DialogRecord::new(InlineId::new(20)));
4567 store.upsert_dialog(DialogRecord::new(InlineId::new(40)));
4568
4569 let second = store
4570 .dialogs(DialogsRequest {
4571 limit: Some(2),
4572 cursor: first.next_cursor,
4573 order: DialogsOrder::StableChatId,
4574 })
4575 .await
4576 .unwrap();
4577 assert_eq!(
4578 second
4579 .dialogs
4580 .iter()
4581 .map(|dialog| dialog.chat_id.get())
4582 .collect::<Vec<_>>(),
4583 vec![40, 50]
4584 );
4585 assert_eq!(second.next_cursor, None);
4586 }
4587
4588 #[tokio::test]
4589 async fn in_memory_store_records_transactions() {
4590 let store = InMemoryStore::new();
4591 let transaction = stored_transaction("txn-mem", TransactionState::Queued);
4592
4593 store.record_transaction(transaction.clone()).await.unwrap();
4594
4595 let loaded = store
4596 .transaction(transaction.identity.transaction_id.clone())
4597 .await
4598 .unwrap()
4599 .unwrap();
4600 assert_eq!(
4601 loaded.identity.transaction_id,
4602 transaction.identity.transaction_id
4603 );
4604 assert_eq!(loaded.state, TransactionState::Queued);
4605 }
4606
4607 #[tokio::test]
4608 async fn in_memory_store_persists_message_reaction_and_read_mutations() {
4609 let store = InMemoryStore::new();
4610 let message = test_message(10);
4611 let reaction = StoredReaction {
4612 chat_id: message.chat_id,
4613 message_id: message.message_id,
4614 user_id: InlineId::new(2),
4615 reaction: "👍".to_owned(),
4616 };
4617 let read_state = StoredReadState {
4618 chat_id: message.chat_id,
4619 read_max_id: Some(message.message_id),
4620 unread_count: Some(3),
4621 marked_unread: false,
4622 };
4623
4624 store.record_message(message.clone()).await.unwrap();
4625 store.record_reaction(reaction.clone()).await.unwrap();
4626 store.record_read_state(read_state).await.unwrap();
4627 assert_eq!(
4628 store
4629 .reactions(message.chat_id, message.message_id)
4630 .await
4631 .unwrap(),
4632 vec![reaction.clone()]
4633 );
4634 assert_eq!(
4635 store.read_state(message.chat_id).await.unwrap(),
4636 Some(read_state)
4637 );
4638
4639 store
4640 .record_message_deleted(message.chat_id, message.message_id)
4641 .await
4642 .unwrap();
4643 assert!(
4644 store
4645 .message_deleted(message.chat_id, message.message_id)
4646 .await
4647 .unwrap()
4648 );
4649 assert!(
4650 store
4651 .reactions(message.chat_id, message.message_id)
4652 .await
4653 .unwrap()
4654 .is_empty()
4655 );
4656
4657 store.record_message(message.clone()).await.unwrap();
4658 assert!(
4659 !store
4660 .message_deleted(message.chat_id, message.message_id)
4661 .await
4662 .unwrap()
4663 );
4664 }
4665
4666 #[tokio::test]
4667 async fn in_memory_store_fetches_history_after_checkpoint() {
4668 let store = InMemoryStore::new();
4669 for message_id in 1..=3 {
4670 store
4671 .record_message(test_message(message_id))
4672 .await
4673 .unwrap();
4674 }
4675
4676 let history = store
4677 .history(HistoryRequest {
4678 chat_id: InlineId::new(7),
4679 limit: Some(1),
4680 before_message_id: None,
4681 after_message_id: Some(InlineId::new(1)),
4682 })
4683 .await
4684 .unwrap();
4685
4686 assert_eq!(history.messages.len(), 1);
4687 assert_eq!(history.messages[0].message_id, InlineId::new(2));
4688 assert!(history.has_more);
4689 }
4690
4691 #[tokio::test]
4692 async fn in_memory_store_latest_history_keeps_newest_window() {
4693 let store = InMemoryStore::new();
4694 for message_id in 1..=4 {
4695 store
4696 .record_message(test_message(message_id))
4697 .await
4698 .unwrap();
4699 }
4700
4701 let history = store
4702 .history(HistoryRequest {
4703 chat_id: InlineId::new(7),
4704 limit: Some(2),
4705 before_message_id: None,
4706 after_message_id: None,
4707 })
4708 .await
4709 .unwrap();
4710
4711 assert!(history.has_more);
4712 assert_eq!(
4713 history
4714 .messages
4715 .iter()
4716 .map(|message| message.message_id)
4717 .collect::<Vec<_>>(),
4718 vec![InlineId::new(3), InlineId::new(4)]
4719 );
4720 }
4721
4722 #[tokio::test]
4723 async fn sqlite_store_persists_session_dialogs_and_history() {
4724 let path = sqlite_temp_path("persist");
4725 let store = SqliteStore::open(&path).unwrap();
4726
4727 store.save_session(session()).await.unwrap();
4728 store
4729 .upsert_dialog(DialogRecord {
4730 chat_id: InlineId::new(7),
4731 peer_user_id: Some(InlineId::new(2)),
4732 title: Some("general".to_owned()),
4733 emoji: Some("🚀".to_owned()),
4734 last_message_id: None,
4735 synced_through_message_id: None,
4736 unread_count: Some(3),
4737 space_id: Some(InlineId::new(5)),
4738 is_public: Some(true),
4739 archived: Some(false),
4740 pinned: Some(true),
4741 open: Some(true),
4742 chat_list_hidden: Some(false),
4743 order: Some("a0".to_owned()),
4744 pinned_order: Some("p0".to_owned()),
4745 notification_mode: Some(DialogNotificationMode::Mentions),
4746 follow_mode: Some(DialogFollowMode::Following),
4747 pinned_message_ids: vec![InlineId::new(99), InlineId::new(98)],
4748 })
4749 .unwrap();
4750 store
4751 .record_users(vec![UserRecord {
4752 user_id: InlineId::new(2),
4753 display_name: Some("Grace Hopper".to_owned()),
4754 username: Some("grace".to_owned()),
4755 first_name: Some("Grace".to_owned()),
4756 last_name: Some("Hopper".to_owned()),
4757 avatar_url: Some("https://cdn.inline.test/grace.jpg".to_owned()),
4758 is_bot: Some(false),
4759 }])
4760 .await
4761 .unwrap();
4762 store
4763 .record_message(MessageRecord {
4764 chat_id: InlineId::new(7),
4765 message_id: InlineId::new(10),
4766 sender_id: InlineId::new(2),
4767 timestamp: 100,
4768 is_outgoing: false,
4769 content: MessageContent::Text {
4770 text: "persisted".to_owned(),
4771 },
4772 reply_to_message_id: None,
4773 transaction: None,
4774 })
4775 .await
4776 .unwrap();
4777 drop(store);
4778
4779 let reopened = SqliteStore::open(&path).unwrap();
4780 let session = reopened.load_session().await.unwrap().unwrap();
4781 assert_eq!(session.account_namespace.as_deref(), Some("team"));
4782 assert!(!format!("{reopened:?}").contains(path.to_string_lossy().as_ref()));
4783
4784 let dialogs = reopened.dialogs(DialogsRequest::default()).await.unwrap();
4785 assert_eq!(dialogs.dialogs.len(), 1);
4786 assert_eq!(dialogs.dialogs[0].chat_id, InlineId::new(7));
4787 assert_eq!(dialogs.dialogs[0].peer_user_id, Some(InlineId::new(2)));
4788 assert_eq!(dialogs.dialogs[0].title.as_deref(), Some("general"));
4789 assert_eq!(dialogs.dialogs[0].emoji.as_deref(), Some("🚀"));
4790 assert_eq!(dialogs.dialogs[0].last_message_id, Some(InlineId::new(10)));
4791 assert_eq!(dialogs.dialogs[0].space_id, Some(InlineId::new(5)));
4792 assert_eq!(dialogs.dialogs[0].is_public, Some(true));
4793 assert_eq!(dialogs.dialogs[0].archived, Some(false));
4794 assert_eq!(dialogs.dialogs[0].pinned, Some(true));
4795 assert_eq!(dialogs.dialogs[0].open, Some(true));
4796 assert_eq!(dialogs.dialogs[0].chat_list_hidden, Some(false));
4797 assert_eq!(dialogs.dialogs[0].order.as_deref(), Some("a0"));
4798 assert_eq!(dialogs.dialogs[0].pinned_order.as_deref(), Some("p0"));
4799 assert_eq!(
4800 dialogs.dialogs[0].notification_mode,
4801 Some(DialogNotificationMode::Mentions)
4802 );
4803 assert_eq!(
4804 dialogs.dialogs[0].follow_mode,
4805 Some(DialogFollowMode::Following)
4806 );
4807 assert_eq!(
4808 dialogs.dialogs[0].pinned_message_ids,
4809 vec![InlineId::new(99), InlineId::new(98)]
4810 );
4811 assert_eq!(
4812 dialogs.dialogs[0].synced_through_message_id,
4813 Some(InlineId::new(10))
4814 );
4815 assert_eq!(dialogs.users.len(), 1);
4816 assert_eq!(
4817 dialogs.users[0].display_name.as_deref(),
4818 Some("Grace Hopper")
4819 );
4820
4821 let history = reopened
4822 .history(HistoryRequest {
4823 chat_id: InlineId::new(7),
4824 limit: Some(10),
4825 before_message_id: None,
4826 after_message_id: None,
4827 })
4828 .await
4829 .unwrap();
4830 assert_eq!(history.messages.len(), 1);
4831 assert_eq!(history.messages[0].message_id, InlineId::new(10));
4832 assert!(matches!(
4833 history.messages[0].content,
4834 MessageContent::Text { ref text } if text == "persisted"
4835 ));
4836 assert_eq!(history.users.len(), 1);
4837 assert_eq!(
4838 history.users[0].avatar_url.as_deref(),
4839 Some("https://cdn.inline.test/grace.jpg")
4840 );
4841
4842 let _ = std::fs::remove_file(path);
4843 }
4844
4845 #[cfg(unix)]
4846 #[tokio::test]
4847 async fn sqlite_store_file_permissions_are_private() {
4848 use std::os::unix::fs::PermissionsExt;
4849
4850 let path = sqlite_temp_path("permissions");
4851 let store = SqliteStore::open(&path).unwrap();
4852 store.save_session(session()).await.unwrap();
4853
4854 let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
4855 assert_eq!(mode, 0o600);
4856
4857 drop(store);
4858 let _ = std::fs::remove_file(path);
4859 }
4860
4861 async fn assert_clear_history_store(store: &dyn ClientStore) {
4862 let mut dialog = DialogRecord::new(InlineId::new(7));
4863 dialog.space_id = Some(InlineId::new(5));
4864 store.record_dialog(dialog).await.unwrap();
4865 for (message_id, timestamp) in [(10, 100), (11, 200)] {
4866 store
4867 .record_message(MessageRecord {
4868 chat_id: InlineId::new(7),
4869 message_id: InlineId::new(message_id),
4870 sender_id: InlineId::new(2),
4871 timestamp,
4872 is_outgoing: false,
4873 content: MessageContent::Text {
4874 text: format!("message {message_id}"),
4875 },
4876 reply_to_message_id: None,
4877 transaction: None,
4878 })
4879 .await
4880 .unwrap();
4881 }
4882 store
4883 .record_reaction(StoredReaction {
4884 chat_id: InlineId::new(7),
4885 message_id: InlineId::new(10),
4886 user_id: InlineId::new(2),
4887 reaction: "ok".to_owned(),
4888 })
4889 .await
4890 .unwrap();
4891
4892 assert_eq!(
4893 store.chat_ids_in_space(InlineId::new(5)).await.unwrap(),
4894 vec![InlineId::new(7)]
4895 );
4896 assert_eq!(
4897 store
4898 .clear_chat_messages(InlineId::new(7), Some(150))
4899 .await
4900 .unwrap(),
4901 vec![InlineId::new(10)]
4902 );
4903 assert!(
4904 store
4905 .message_deleted(InlineId::new(7), InlineId::new(10))
4906 .await
4907 .unwrap()
4908 );
4909 assert!(
4910 store
4911 .reactions(InlineId::new(7), InlineId::new(10))
4912 .await
4913 .unwrap()
4914 .is_empty()
4915 );
4916 let history = store
4917 .history(HistoryRequest {
4918 chat_id: InlineId::new(7),
4919 limit: Some(10),
4920 before_message_id: None,
4921 after_message_id: None,
4922 })
4923 .await
4924 .unwrap();
4925 assert_eq!(
4926 history
4927 .messages
4928 .iter()
4929 .map(|message| message.message_id)
4930 .collect::<Vec<_>>(),
4931 vec![InlineId::new(11)]
4932 );
4933 assert_eq!(
4934 store
4935 .dialog(InlineId::new(7))
4936 .await
4937 .unwrap()
4938 .unwrap()
4939 .last_message_id,
4940 Some(InlineId::new(11))
4941 );
4942 }
4943
4944 #[tokio::test]
4945 async fn stores_clear_history_with_tombstones_and_space_lookup() {
4946 let memory = InMemoryStore::new();
4947 assert_clear_history_store(&memory).await;
4948 let sqlite = SqliteStore::open_in_memory().unwrap();
4949 assert_clear_history_store(&sqlite).await;
4950 }
4951
4952 async fn assert_space_and_settings_store(store: &dyn ClientStore) {
4953 let space = SpaceRecord {
4954 space_id: InlineId::new(5),
4955 name: "Engineering".to_owned(),
4956 creator: true,
4957 date: 100,
4958 is_public: Some(false),
4959 grid_enabled: Some(true),
4960 };
4961 let member = SpaceMemberRecord {
4962 space_id: InlineId::new(5),
4963 user_id: InlineId::new(2),
4964 role: Some(crate::SpaceMemberRole::Admin),
4965 date: 101,
4966 can_access_public_chats: true,
4967 };
4968 let settings = UserSettingsRecord {
4969 notification_mode: Some(crate::NotificationMode::Mentions),
4970 silent: Some(true),
4971 zen_mode_requires_mention: Some(true),
4972 zen_mode_uses_default_rules: Some(false),
4973 zen_mode_custom_rules: Some("important".to_owned()),
4974 disable_dm_notifications: Some(false),
4975 };
4976
4977 store.record_space(space.clone()).await.unwrap();
4978 store.record_space_member(member.clone()).await.unwrap();
4979 store.record_user_settings(settings.clone()).await.unwrap();
4980
4981 assert_eq!(store.space(InlineId::new(5)).await.unwrap(), Some(space));
4982 assert_eq!(
4983 store.space_members(InlineId::new(5)).await.unwrap(),
4984 vec![member]
4985 );
4986 assert_eq!(store.user_settings().await.unwrap(), Some(settings));
4987 store
4988 .remove_space_member(InlineId::new(5), InlineId::new(2))
4989 .await
4990 .unwrap();
4991 assert!(
4992 store
4993 .space_members(InlineId::new(5))
4994 .await
4995 .unwrap()
4996 .is_empty()
4997 );
4998 }
4999
5000 #[tokio::test]
5001 async fn stores_persist_spaces_members_and_user_settings() {
5002 let memory = InMemoryStore::new();
5003 assert_space_and_settings_store(&memory).await;
5004 let sqlite = SqliteStore::open_in_memory().unwrap();
5005 assert_space_and_settings_store(&sqlite).await;
5006 }
5007
5008 #[tokio::test]
5009 async fn sqlite_store_persists_sync_cursors_across_reopen() {
5010 let path = sqlite_temp_path("sync-cursors");
5011 let store = SqliteStore::open(&path).unwrap();
5012 let user = SyncBucketKey::User;
5013 let direct = SyncBucketKey::Chat {
5014 peer: SyncBucketPeer::User {
5015 user_id: InlineId::new(2),
5016 },
5017 };
5018 let chat = SyncBucketKey::Chat {
5019 peer: SyncBucketPeer::Chat {
5020 chat_id: InlineId::new(7),
5021 },
5022 };
5023 let space = SyncBucketKey::Space {
5024 space_id: InlineId::new(9),
5025 };
5026
5027 store
5028 .save_sync_state(SyncState { last_sync_date: 80 })
5029 .await
5030 .unwrap();
5031 store
5032 .save_sync_bucket_states(vec![
5033 (user, SyncBucketState { seq: 10, date: 70 }),
5034 (direct, SyncBucketState { seq: 11, date: 71 }),
5035 (chat, SyncBucketState { seq: 12, date: 72 }),
5036 (space, SyncBucketState { seq: 13, date: 73 }),
5037 ])
5038 .await
5039 .unwrap();
5040 store
5041 .save_sync_bucket_state(direct, SyncBucketState { seq: 14, date: 74 })
5042 .await
5043 .unwrap();
5044 drop(store);
5045
5046 let reopened = SqliteStore::open(&path).unwrap();
5047 assert_eq!(
5048 reopened.sync_state().await.unwrap(),
5049 SyncState { last_sync_date: 80 }
5050 );
5051 assert_eq!(
5052 reopened.sync_bucket_state(user).await.unwrap(),
5053 SyncBucketState { seq: 10, date: 70 }
5054 );
5055 assert_eq!(
5056 reopened.sync_bucket_state(direct).await.unwrap(),
5057 SyncBucketState { seq: 14, date: 74 }
5058 );
5059 assert_eq!(
5060 reopened.sync_bucket_state(chat).await.unwrap(),
5061 SyncBucketState { seq: 12, date: 72 }
5062 );
5063 assert_eq!(
5064 reopened.sync_bucket_state(space).await.unwrap(),
5065 SyncBucketState { seq: 13, date: 73 }
5066 );
5067
5068 reopened.remove_sync_bucket_state(chat).await.unwrap();
5069 assert_eq!(
5070 reopened.sync_bucket_state(chat).await.unwrap(),
5071 SyncBucketState::default()
5072 );
5073 reopened.clear_sync_state().await.unwrap();
5074 assert_eq!(reopened.sync_state().await.unwrap(), SyncState::default());
5075 assert_eq!(
5076 reopened.sync_bucket_state(space).await.unwrap(),
5077 SyncBucketState::default()
5078 );
5079
5080 drop(reopened);
5081 let _ = std::fs::remove_file(path);
5082 }
5083
5084 #[tokio::test]
5085 async fn sqlite_store_upgrades_initial_client_schema_in_place() {
5086 let path = sqlite_temp_path("initial-client-upgrade");
5087 let connection = Connection::open(&path).unwrap();
5088 connection
5089 .execute_batch(
5090 "
5091 CREATE TABLE sessions (
5092 id INTEGER PRIMARY KEY CHECK (id = 1),
5093 auth_json TEXT NOT NULL,
5094 account_namespace TEXT,
5095 updated_at INTEGER NOT NULL
5096 );
5097 CREATE TABLE dialogs (
5098 chat_id INTEGER PRIMARY KEY,
5099 peer_user_id INTEGER,
5100 title TEXT,
5101 last_message_id INTEGER,
5102 unread_count INTEGER,
5103 updated_at INTEGER NOT NULL
5104 );
5105 CREATE TABLE users (
5106 user_id INTEGER PRIMARY KEY,
5107 display_name TEXT,
5108 username TEXT,
5109 first_name TEXT,
5110 last_name TEXT,
5111 avatar_url TEXT,
5112 is_bot INTEGER,
5113 updated_at INTEGER NOT NULL
5114 );
5115 CREATE TABLE messages (
5116 chat_id INTEGER NOT NULL,
5117 message_id INTEGER NOT NULL,
5118 sender_id INTEGER NOT NULL,
5119 timestamp INTEGER NOT NULL,
5120 is_outgoing INTEGER NOT NULL,
5121 content_json TEXT NOT NULL,
5122 reply_to_message_id INTEGER,
5123 transaction_json TEXT,
5124 PRIMARY KEY (chat_id, message_id)
5125 );
5126 CREATE TABLE transactions (
5127 transaction_id TEXT PRIMARY KEY,
5128 identity_json TEXT NOT NULL,
5129 state_json TEXT NOT NULL,
5130 chat_id INTEGER,
5131 message_id INTEGER,
5132 random_id INTEGER NOT NULL,
5133 external_source TEXT,
5134 external_id TEXT,
5135 failure_json TEXT,
5136 created_at INTEGER NOT NULL,
5137 updated_at INTEGER NOT NULL
5138 );
5139 ",
5140 )
5141 .unwrap();
5142 let initial_session = session();
5143 connection
5144 .execute(
5145 "INSERT INTO sessions (id, auth_json, account_namespace, updated_at)
5146 VALUES (1, ?1, ?2, 1)",
5147 params![
5148 serde_json::to_string(&initial_session.auth).unwrap(),
5149 initial_session.account_namespace
5150 ],
5151 )
5152 .unwrap();
5153 connection
5154 .execute(
5155 "INSERT INTO dialogs (
5156 chat_id, peer_user_id, title, last_message_id, unread_count, updated_at
5157 ) VALUES (7, 2, 'General', 10, 1, 1)",
5158 [],
5159 )
5160 .unwrap();
5161 connection
5162 .execute(
5163 "INSERT INTO messages (
5164 chat_id, message_id, sender_id, timestamp, is_outgoing,
5165 content_json, reply_to_message_id, transaction_json
5166 ) VALUES (7, 10, 2, 10, 0, ?1, NULL, NULL)",
5167 params![
5168 serde_json::to_string(&MessageContent::Text {
5169 text: "from the initial client".to_owned(),
5170 })
5171 .unwrap()
5172 ],
5173 )
5174 .unwrap();
5175 drop(connection);
5176
5177 let upgraded = SqliteStore::open(&path).unwrap();
5178 assert_eq!(
5179 upgraded
5180 .load_session()
5181 .await
5182 .unwrap()
5183 .unwrap()
5184 .account_namespace
5185 .as_deref(),
5186 Some("team")
5187 );
5188 let dialogs = upgraded.dialogs(DialogsRequest::default()).await.unwrap();
5189 assert_eq!(dialogs.dialogs.len(), 1);
5190 assert_eq!(dialogs.dialogs[0].chat_id, InlineId::new(7));
5191 assert_eq!(dialogs.dialogs[0].peer_user_id, Some(InlineId::new(2)));
5192 assert_eq!(dialogs.dialogs[0].title.as_deref(), Some("General"));
5193 let history = upgraded
5194 .history(HistoryRequest {
5195 chat_id: InlineId::new(7),
5196 limit: Some(10),
5197 before_message_id: None,
5198 after_message_id: None,
5199 })
5200 .await
5201 .unwrap();
5202 assert_eq!(history.messages.len(), 1);
5203 assert!(matches!(
5204 &history.messages[0].content,
5205 MessageContent::Text { text } if text == "from the initial client"
5206 ));
5207 assert_eq!(upgraded.sync_state().await.unwrap(), SyncState::default());
5208 upgraded
5209 .save_sync_bucket_state(SyncBucketKey::User, SyncBucketState { seq: 3, date: 30 })
5210 .await
5211 .unwrap();
5212 assert_eq!(
5213 upgraded
5214 .sync_bucket_state(SyncBucketKey::User)
5215 .await
5216 .unwrap(),
5217 SyncBucketState { seq: 3, date: 30 }
5218 );
5219
5220 drop(upgraded);
5221 let _ = std::fs::remove_file(path);
5222 }
5223
5224 #[tokio::test]
5225 async fn sqlite_store_persists_message_tombstones_reactions_and_read_state() {
5226 let path = sqlite_temp_path("message-state");
5227 let store = SqliteStore::open(&path).unwrap();
5228 let message = test_message(10);
5229 let reaction = StoredReaction {
5230 chat_id: message.chat_id,
5231 message_id: message.message_id,
5232 user_id: InlineId::new(2),
5233 reaction: "🔥".to_owned(),
5234 };
5235 let read_state = StoredReadState {
5236 chat_id: message.chat_id,
5237 read_max_id: Some(message.message_id),
5238 unread_count: Some(1),
5239 marked_unread: true,
5240 };
5241
5242 store.record_message(message.clone()).await.unwrap();
5243 store.record_reaction(reaction).await.unwrap();
5244 store.record_read_state(read_state).await.unwrap();
5245 store
5246 .record_message_deleted(message.chat_id, message.message_id)
5247 .await
5248 .unwrap();
5249 drop(store);
5250
5251 let reopened = SqliteStore::open(&path).unwrap();
5252 assert!(
5253 reopened
5254 .message_deleted(message.chat_id, message.message_id)
5255 .await
5256 .unwrap()
5257 );
5258 assert!(
5259 reopened
5260 .reactions(message.chat_id, message.message_id)
5261 .await
5262 .unwrap()
5263 .is_empty()
5264 );
5265 assert_eq!(
5266 reopened.read_state(message.chat_id).await.unwrap(),
5267 Some(read_state)
5268 );
5269
5270 reopened.record_message(message.clone()).await.unwrap();
5271 assert!(
5272 !reopened
5273 .message_deleted(message.chat_id, message.message_id)
5274 .await
5275 .unwrap()
5276 );
5277
5278 drop(reopened);
5279 let _ = std::fs::remove_file(path);
5280 }
5281
5282 #[tokio::test]
5283 async fn sqlite_store_preserves_complete_reconciliation_markers_and_chat_tombstones() {
5284 let path = sqlite_temp_path("reconciliation-state");
5285 let store = SqliteStore::open(&path).unwrap();
5286 let message = test_message(10);
5287 store
5288 .record_dialog(DialogRecord::new(message.chat_id))
5289 .await
5290 .unwrap();
5291 store.record_message(message.clone()).await.unwrap();
5292 store
5293 .record_chat_participants(message.chat_id, Vec::new())
5294 .await
5295 .unwrap();
5296 store
5297 .replace_message_reactions(
5298 message.chat_id,
5299 message.message_id,
5300 vec![StoredReaction {
5301 chat_id: message.chat_id,
5302 message_id: message.message_id,
5303 user_id: InlineId::new(2),
5304 reaction: "👍".to_owned(),
5305 }],
5306 )
5307 .await
5308 .unwrap();
5309
5310 assert!(
5311 store
5312 .chat_participants_complete(message.chat_id)
5313 .await
5314 .unwrap()
5315 );
5316 assert_eq!(
5317 store
5318 .reaction_snapshot_message_ids(message.chat_id)
5319 .await
5320 .unwrap(),
5321 vec![message.message_id]
5322 );
5323
5324 store.remove_dialog(message.chat_id).await.unwrap();
5325 drop(store);
5326 let reopened = SqliteStore::open(&path).unwrap();
5327 assert_eq!(
5328 reopened.deleted_chat_ids().await.unwrap(),
5329 vec![message.chat_id]
5330 );
5331 assert_eq!(
5332 reopened.deleted_message_ids(message.chat_id).await.unwrap(),
5333 vec![message.message_id]
5334 );
5335 assert!(
5336 !reopened
5337 .chat_participants_complete(message.chat_id)
5338 .await
5339 .unwrap()
5340 );
5341
5342 drop(reopened);
5343 let _ = std::fs::remove_file(path);
5344 }
5345
5346 #[tokio::test]
5347 async fn sqlite_store_commits_sync_cursor_and_journal_removal_atomically() {
5348 let path = sqlite_temp_path("sync-journal");
5349 let key = SyncBucketKey::Chat {
5350 peer: SyncBucketPeer::Chat {
5351 chat_id: InlineId::new(7),
5352 },
5353 };
5354 let state = SyncBucketState { seq: 9, date: 90 };
5355 let store = SqliteStore::open(&path).unwrap();
5356 store
5357 .save_pending_sync_batch(PendingSyncBatch {
5358 key,
5359 committed_state: state,
5360 payload: vec![1, 2, 3],
5361 })
5362 .await
5363 .unwrap();
5364 drop(store);
5365
5366 let reopened = SqliteStore::open(&path).unwrap();
5367 assert_eq!(
5368 reopened.pending_sync_batches().await.unwrap(),
5369 vec![PendingSyncBatch {
5370 key,
5371 committed_state: state,
5372 payload: vec![1, 2, 3],
5373 }]
5374 );
5375 let lossless = ClientEvent::MessageDeleted {
5376 chat_id: InlineId::new(7),
5377 message_id: InlineId::new(11),
5378 };
5379 let best_effort = ClientEvent::Typing {
5380 chat_id: InlineId::new(7),
5381 user_id: InlineId::new(2),
5382 is_typing: true,
5383 };
5384 let deliveries = reopened
5385 .commit_pending_sync_batch_with_events(
5386 key,
5387 state,
5388 vec![lossless.clone(), best_effort.clone()],
5389 )
5390 .await
5391 .unwrap();
5392 assert_eq!(deliveries.len(), 2);
5393 assert!(deliveries[0].delivery_id.is_some());
5394 assert_eq!(deliveries[0].event, lossless);
5395 assert_eq!(deliveries[1], ClientEventDelivery::ephemeral(best_effort));
5396 assert_eq!(reopened.sync_bucket_state(key).await.unwrap(), state);
5397 assert!(reopened.pending_sync_batches().await.unwrap().is_empty());
5398
5399 drop(reopened);
5400 let reopened = SqliteStore::open(&path).unwrap();
5401 let pending = reopened.pending_client_events().await.unwrap();
5402 assert_eq!(pending.len(), 1);
5403 assert_eq!(pending[0].event, lossless);
5404 reopened
5405 .acknowledge_client_event(pending[0].delivery_id.unwrap())
5406 .await
5407 .unwrap();
5408 assert!(reopened.pending_client_events().await.unwrap().is_empty());
5409
5410 drop(reopened);
5411 let _ = std::fs::remove_file(path);
5412 }
5413
5414 #[tokio::test]
5415 async fn sqlite_store_persists_transactions() {
5416 let path = sqlite_temp_path("transactions");
5417 let store = SqliteStore::open(&path).unwrap();
5418 let transaction = stored_transaction("txn-sqlite", TransactionState::Sent)
5419 .with_chat_id(InlineId::new(7))
5420 .with_message_id(InlineId::new(11));
5421
5422 store.record_transaction(transaction.clone()).await.unwrap();
5423 drop(store);
5424
5425 let reopened = SqliteStore::open(&path).unwrap();
5426 let loaded = reopened
5427 .transaction(transaction.identity.transaction_id.clone())
5428 .await
5429 .unwrap()
5430 .unwrap();
5431
5432 assert_eq!(
5433 loaded.identity.transaction_id,
5434 transaction.identity.transaction_id
5435 );
5436 assert_eq!(
5437 loaded.identity.external_id,
5438 transaction.identity.external_id
5439 );
5440 assert_eq!(loaded.state, TransactionState::Sent);
5441 assert_eq!(loaded.chat_id, Some(InlineId::new(7)));
5442 assert_eq!(loaded.message_id, Some(InlineId::new(11)));
5443
5444 let _ = std::fs::remove_file(path);
5445 }
5446
5447 #[tokio::test]
5448 async fn sqlite_store_replaces_messages_by_chat_and_message_id() {
5449 let store = SqliteStore::open_in_memory().unwrap();
5450
5451 store
5452 .record_message(MessageRecord {
5453 chat_id: InlineId::new(7),
5454 message_id: InlineId::new(10),
5455 sender_id: InlineId::new(2),
5456 timestamp: 100,
5457 is_outgoing: false,
5458 content: MessageContent::Text {
5459 text: "first".to_owned(),
5460 },
5461 reply_to_message_id: None,
5462 transaction: None,
5463 })
5464 .await
5465 .unwrap();
5466 store
5467 .record_message(MessageRecord {
5468 chat_id: InlineId::new(7),
5469 message_id: InlineId::new(10),
5470 sender_id: InlineId::new(2),
5471 timestamp: 101,
5472 is_outgoing: false,
5473 content: MessageContent::Text {
5474 text: "updated".to_owned(),
5475 },
5476 reply_to_message_id: None,
5477 transaction: None,
5478 })
5479 .await
5480 .unwrap();
5481
5482 let history = store
5483 .history(HistoryRequest {
5484 chat_id: InlineId::new(7),
5485 limit: Some(10),
5486 before_message_id: None,
5487 after_message_id: None,
5488 })
5489 .await
5490 .unwrap();
5491 assert_eq!(history.messages.len(), 1);
5492 assert!(matches!(
5493 history.messages[0].content,
5494 MessageContent::Text { ref text } if text == "updated"
5495 ));
5496 }
5497
5498 #[tokio::test]
5499 async fn sqlite_store_history_windows_match_client_cursors() {
5500 let store = SqliteStore::open_in_memory().unwrap();
5501 for message_id in 1..=4 {
5502 store
5503 .record_message(test_message(message_id))
5504 .await
5505 .unwrap();
5506 }
5507
5508 let latest = store
5509 .history(HistoryRequest {
5510 chat_id: InlineId::new(7),
5511 limit: Some(2),
5512 before_message_id: None,
5513 after_message_id: None,
5514 })
5515 .await
5516 .unwrap();
5517 assert!(latest.has_more);
5518 assert_eq!(
5519 latest
5520 .messages
5521 .iter()
5522 .map(|message| message.message_id)
5523 .collect::<Vec<_>>(),
5524 vec![InlineId::new(3), InlineId::new(4)]
5525 );
5526
5527 let newer = store
5528 .history(HistoryRequest {
5529 chat_id: InlineId::new(7),
5530 limit: Some(2),
5531 before_message_id: None,
5532 after_message_id: Some(InlineId::new(1)),
5533 })
5534 .await
5535 .unwrap();
5536 assert!(newer.has_more);
5537 assert_eq!(
5538 newer
5539 .messages
5540 .iter()
5541 .map(|message| message.message_id)
5542 .collect::<Vec<_>>(),
5543 vec![InlineId::new(2), InlineId::new(3)]
5544 );
5545
5546 let older = store
5547 .history(HistoryRequest {
5548 chat_id: InlineId::new(7),
5549 limit: Some(2),
5550 before_message_id: Some(InlineId::new(4)),
5551 after_message_id: None,
5552 })
5553 .await
5554 .unwrap();
5555 assert!(older.has_more);
5556 assert_eq!(
5557 older
5558 .messages
5559 .iter()
5560 .map(|message| message.message_id)
5561 .collect::<Vec<_>>(),
5562 vec![InlineId::new(2), InlineId::new(3)]
5563 );
5564 }
5565
5566 fn sqlite_temp_path(label: &str) -> std::path::PathBuf {
5567 let unique = SystemTime::now()
5568 .duration_since(UNIX_EPOCH)
5569 .unwrap()
5570 .as_nanos();
5571 std::env::temp_dir().join(format!(
5572 "inline-client-{label}-{}-{unique}.db",
5573 std::process::id()
5574 ))
5575 }
5576
5577 fn stored_transaction(id: &str, state: TransactionState) -> StoredTransaction {
5578 StoredTransaction::new(
5579 TransactionIdentity::new(
5580 TransactionId::try_new(id).unwrap(),
5581 Some(ExternalId::try_new("host-event", "event-1").unwrap()),
5582 RandomId::new(42),
5583 ),
5584 state,
5585 )
5586 }
5587}