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