1use super::{
3 ConnectionExt, Sqlite,
4 consent_record::ConsentState,
5 db_connection::DbConnection,
6 schema::groups::{self, dsl},
7};
8use crate::NotFound;
9use crate::{DuplicateItem, StorageError, impl_fetch, impl_store, impl_store_or_ignore};
10use derive_builder::Builder;
11use diesel::{
12 backend::Backend,
13 deserialize::{self, FromSql, FromSqlRow},
14 dsl::sql,
15 expression::AsExpression,
16 prelude::*,
17 serialize::{self, IsNull, Output, ToSql},
18 sql_types::Integer,
19};
20use serde::{Deserialize, Serialize};
21mod convert;
22mod dms;
23mod version;
24
25pub use dms::QueryDms;
26pub use version::QueryGroupVersion;
27use xmtp_proto::types::{Cursor, GroupId};
28
29#[derive(
30 Debug,
31 Clone,
32 Serialize,
33 Deserialize,
34 PartialEq,
35 Insertable,
36 Identifiable,
37 Queryable,
38 Builder,
39 Selectable,
40 QueryableByName,
41)]
42#[diesel(table_name = groups)]
43#[diesel(primary_key(id))]
44#[diesel(check_for_backend(Sqlite))]
45#[builder(setter(into), build_fn(error = "StorageError"))]
46#[derive(AsChangeset)]
47pub struct StoredGroup {
49 pub id: GroupId,
51 pub created_at_ns: i64,
53 pub membership_state: GroupMembershipState,
55 #[builder(default = "0")]
57 pub installations_last_checked: i64,
58 pub added_by_inbox_id: String,
60 #[builder(default = None)]
62 pub sequence_id: Option<i64>,
63 #[builder(default = "0")]
65 pub rotated_at_ns: i64,
66 #[builder(default = "self.default_conversation_type()")]
68 pub conversation_type: ConversationType,
69 #[builder(default = None)]
71 pub dm_id: Option<String>,
72 #[builder(default = None)]
74 pub last_message_ns: Option<i64>,
75 #[builder(default = None)]
77 pub message_disappear_from_ns: Option<i64>,
78 #[builder(default = None)]
80 pub message_disappear_in_ns: Option<i64>,
81 #[builder(default = None)]
83 pub paused_for_version: Option<String>,
84 #[builder(default = false)]
85 pub maybe_forked: bool,
86 #[builder(default = "String::new()")]
87 pub fork_details: String,
88 #[builder(default = false)]
90 pub should_publish_commit_log: bool,
91 #[builder(default = None)]
94 pub commit_log_public_key: Option<Vec<u8>>,
95 #[builder(default = None)]
98 pub is_commit_log_forked: Option<bool>,
99 #[builder(default = None)]
102 pub has_pending_leave_request: Option<bool>,
103 #[builder(default = None)]
105 #[serde(default)]
106 pub push_override: Option<i32>,
107 }
109
110impl StoredGroup {
111 pub fn cursor(&self) -> Option<Cursor> {
112 self.sequence_id.map(|sequence| Cursor(sequence as u64))
113 }
114}
115
116impl StoredGroupBuilder {
117 pub fn cursor(&mut self, cursor: Cursor) -> &mut Self {
118 self.sequence_id = Some(Some(cursor.0 as i64));
119 self
120 }
121}
122
123#[derive(Queryable)]
125#[diesel(table_name = groups)]
126pub struct StoredGroupCommitLogPublicKey {
127 pub id: GroupId,
128 pub commit_log_public_key: Option<Vec<u8>>,
129}
130
131#[derive(Debug, Clone, Queryable, QueryableByName)]
133pub struct StoredGroupForReaddRequest {
134 #[diesel(sql_type = diesel::sql_types::Binary)]
135 pub group_id: GroupId,
136 #[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::BigInt>)]
137 pub latest_commit_sequence_id: Option<i64>,
138}
139
140#[derive(Debug, Clone, Queryable, QueryableByName)]
142pub struct StoredGroupForRespondingReadds {
143 #[diesel(sql_type = diesel::sql_types::Binary)]
144 pub group_id: GroupId,
145 #[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
146 pub dm_id: Option<String>,
147 #[diesel(sql_type = diesel::sql_types::Integer)]
148 pub conversation_type: ConversationType,
149 #[diesel(sql_type = diesel::sql_types::BigInt)]
150 pub created_at_ns: i64,
151}
152
153impl_fetch!(StoredGroup, groups, GroupId);
155impl_store!(StoredGroup, groups);
156impl_store_or_ignore!(StoredGroup, groups);
157
158impl StoredGroupBuilder {
159 fn default_conversation_type(&self) -> ConversationType {
160 if self.dm_id.is_some() {
161 ConversationType::Dm
162 } else {
163 ConversationType::Group
164 }
165 }
166}
167
168impl StoredGroup {
169 pub fn builder() -> StoredGroupBuilder {
170 StoredGroupBuilder::default()
171 }
172}
173
174#[derive(Debug, Clone, Default)]
175pub enum GroupQueryOrderBy {
176 #[default]
177 CreatedAt,
178 LastActivity,
179}
180
181#[derive(Debug, Default, Clone)]
182pub struct GroupQueryArgs {
183 pub allowed_states: Option<Vec<GroupMembershipState>>,
184 pub created_after_ns: Option<i64>,
185 pub created_before_ns: Option<i64>,
186 pub last_activity_after_ns: Option<i64>,
187 pub last_activity_before_ns: Option<i64>,
188 pub limit: Option<i64>,
189 pub conversation_type: Option<ConversationType>,
190 pub consent_states: Option<Vec<ConsentState>>,
191 pub include_sync_groups: bool,
192 pub include_duplicate_dms: bool,
193 pub should_publish_commit_log: Option<bool>,
194 pub order_by: Option<GroupQueryOrderBy>,
195}
196
197impl AsRef<GroupQueryArgs> for GroupQueryArgs {
198 fn as_ref(&self) -> &GroupQueryArgs {
199 self
200 }
201}
202
203impl GroupQueryArgs {
204 pub fn validate(&self) -> Result<(), crate::ConnectionError> {
205 if self.last_activity_after_ns.is_some() && self.created_after_ns.is_some() {
206 return Err(crate::ConnectionError::InvalidQuery(
207 "last_activity_after_ns and created_after_ns cannot be used together".to_string(),
208 ));
209 }
210
211 if self.last_activity_before_ns.is_some() && self.created_before_ns.is_some() {
212 return Err(crate::ConnectionError::InvalidQuery(
213 "last_activity_before_ns and created_before_ns cannot be used together".to_string(),
214 ));
215 }
216
217 Ok(())
218 }
219}
220
221pub trait QueryGroup {
222 fn find_groups<A: AsRef<GroupQueryArgs>>(
224 &self,
225 args: A,
226 ) -> Result<Vec<StoredGroup>, crate::ConnectionError>;
227
228 fn find_groups_by_id_paged<A: AsRef<GroupQueryArgs>>(
229 &self,
230 args: A,
231 offset: i64,
232 ) -> Result<Vec<StoredGroup>, crate::ConnectionError>;
233
234 fn update_group_membership<Id: AsRef<[u8]>>(
236 &self,
237 group_id: Id,
238 state: GroupMembershipState,
239 ) -> Result<(), crate::ConnectionError>;
240
241 fn all_sync_groups(&self) -> Result<Vec<StoredGroup>, crate::ConnectionError>;
242
243 fn find_sync_group(&self, id: &GroupId) -> Result<Option<StoredGroup>, crate::ConnectionError>;
244
245 fn primary_sync_group(&self) -> Result<Option<StoredGroup>, crate::ConnectionError>;
246
247 fn find_group(&self, id: &GroupId) -> Result<Option<StoredGroup>, crate::ConnectionError>;
249
250 fn find_group_by_sequence_id(
252 &self,
253 cursor: Cursor,
254 ) -> Result<Option<StoredGroup>, crate::ConnectionError>;
255
256 fn get_rotated_at_ns(&self, group_id: &GroupId) -> Result<i64, StorageError>;
257
258 fn update_rotated_at_ns(&self, group_id: &GroupId) -> Result<(), StorageError>;
260
261 fn get_installations_time_checked(&self, group_id: &GroupId) -> Result<i64, StorageError>;
262
263 fn update_installations_time_checked(&self, group_id: &GroupId) -> Result<(), StorageError>;
265
266 fn update_message_disappearing_from_ns(
267 &self,
268 group_id: &GroupId,
269 from_ns: Option<i64>,
270 ) -> Result<(), StorageError>;
271
272 fn update_message_disappearing_in_ns(
273 &self,
274 group_id: &GroupId,
275 in_ns: Option<i64>,
276 ) -> Result<(), StorageError>;
277
278 fn insert_or_replace_group(&self, group: StoredGroup) -> Result<StoredGroup, StorageError>;
279
280 fn group_cursors(&self) -> Result<Vec<Cursor>, crate::ConnectionError>;
282
283 fn mark_group_as_maybe_forked(
284 &self,
285 group_id: &GroupId,
286 fork_details: String,
287 ) -> Result<(), StorageError>;
288
289 fn clear_fork_flag_for_group(&self, group_id: &GroupId) -> Result<(), crate::ConnectionError>;
290
291 fn has_duplicate_dm(&self, group_id: &GroupId) -> Result<bool, crate::ConnectionError>;
292
293 fn get_conversation_ids_for_remote_log_publish(
295 &self,
296 ) -> Result<Vec<StoredGroupCommitLogPublicKey>, crate::ConnectionError>;
297
298 fn get_conversation_ids_for_remote_log_download(
300 &self,
301 ) -> Result<Vec<StoredGroupCommitLogPublicKey>, crate::ConnectionError>;
302
303 fn get_conversation_ids_for_fork_check(&self) -> Result<Vec<Vec<u8>>, crate::ConnectionError>;
305
306 fn get_conversation_ids_for_requesting_readds(
308 &self,
309 ) -> Result<Vec<StoredGroupForReaddRequest>, crate::ConnectionError>;
310
311 fn get_conversation_ids_for_responding_readds(
313 &self,
314 ) -> Result<Vec<StoredGroupForRespondingReadds>, crate::ConnectionError>;
315
316 fn get_conversation_type(
317 &self,
318 group_id: &GroupId,
319 ) -> Result<ConversationType, crate::ConnectionError>;
320
321 fn set_group_commit_log_public_key(
323 &self,
324 group_id: &GroupId,
325 public_key: &[u8],
326 ) -> Result<(), StorageError>;
327
328 fn set_group_commit_log_forked_status(
330 &self,
331 group_id: &GroupId,
332 is_forked: Option<bool>,
333 ) -> Result<(), StorageError>;
334
335 fn get_group_commit_log_forked_status(
337 &self,
338 group_id: &GroupId,
339 ) -> Result<Option<bool>, StorageError>;
340
341 fn set_group_has_pending_leave_request_status(
343 &self,
344 group_id: &GroupId,
345 has_pending_leave_request: Option<bool>,
346 ) -> Result<(), StorageError>;
347
348 fn get_groups_have_pending_leave_request(&self)
349 -> Result<Vec<Vec<u8>>, crate::ConnectionError>;
350}
351
352impl<T> QueryGroup for &T
353where
354 T: QueryGroup,
355{
356 fn find_groups<A: AsRef<GroupQueryArgs>>(
358 &self,
359 args: A,
360 ) -> Result<Vec<StoredGroup>, crate::ConnectionError> {
361 (**self).find_groups(args)
362 }
363
364 fn find_groups_by_id_paged<A: AsRef<GroupQueryArgs>>(
365 &self,
366 args: A,
367 offset: i64,
368 ) -> Result<Vec<StoredGroup>, crate::ConnectionError> {
369 (**self).find_groups_by_id_paged(args, offset)
370 }
371
372 fn update_group_membership<Id: AsRef<[u8]>>(
374 &self,
375 group_id: Id,
376 state: GroupMembershipState,
377 ) -> Result<(), crate::ConnectionError> {
378 (**self).update_group_membership(group_id, state)
379 }
380
381 fn all_sync_groups(&self) -> Result<Vec<StoredGroup>, crate::ConnectionError> {
382 (**self).all_sync_groups()
383 }
384
385 fn find_sync_group(&self, id: &GroupId) -> Result<Option<StoredGroup>, crate::ConnectionError> {
386 (**self).find_sync_group(id)
387 }
388
389 fn primary_sync_group(&self) -> Result<Option<StoredGroup>, crate::ConnectionError> {
390 (**self).primary_sync_group()
391 }
392
393 fn find_group(&self, id: &GroupId) -> Result<Option<StoredGroup>, crate::ConnectionError> {
395 (**self).find_group(id)
396 }
397
398 fn find_group_by_sequence_id(
400 &self,
401 cursor: Cursor,
402 ) -> Result<Option<StoredGroup>, crate::ConnectionError> {
403 (**self).find_group_by_sequence_id(cursor)
404 }
405
406 fn get_rotated_at_ns(&self, group_id: &GroupId) -> Result<i64, StorageError> {
407 (**self).get_rotated_at_ns(group_id)
408 }
409
410 fn update_rotated_at_ns(&self, group_id: &GroupId) -> Result<(), StorageError> {
412 (**self).update_rotated_at_ns(group_id)
413 }
414
415 fn get_installations_time_checked(&self, group_id: &GroupId) -> Result<i64, StorageError> {
416 (**self).get_installations_time_checked(group_id)
417 }
418
419 fn update_installations_time_checked(&self, group_id: &GroupId) -> Result<(), StorageError> {
421 (**self).update_installations_time_checked(group_id)
422 }
423
424 fn update_message_disappearing_from_ns(
425 &self,
426 group_id: &GroupId,
427 from_ns: Option<i64>,
428 ) -> Result<(), StorageError> {
429 (**self).update_message_disappearing_from_ns(group_id, from_ns)
430 }
431
432 fn update_message_disappearing_in_ns(
433 &self,
434 group_id: &GroupId,
435 in_ns: Option<i64>,
436 ) -> Result<(), StorageError> {
437 (**self).update_message_disappearing_in_ns(group_id, in_ns)
438 }
439
440 fn insert_or_replace_group(&self, group: StoredGroup) -> Result<StoredGroup, StorageError> {
441 (**self).insert_or_replace_group(group)
442 }
443
444 fn group_cursors(&self) -> Result<Vec<Cursor>, crate::ConnectionError> {
446 (**self).group_cursors()
447 }
448
449 fn mark_group_as_maybe_forked(
450 &self,
451 group_id: &GroupId,
452 fork_details: String,
453 ) -> Result<(), StorageError> {
454 (**self).mark_group_as_maybe_forked(group_id, fork_details)
455 }
456
457 fn clear_fork_flag_for_group(&self, group_id: &GroupId) -> Result<(), crate::ConnectionError> {
458 (**self).clear_fork_flag_for_group(group_id)
459 }
460
461 fn has_duplicate_dm(&self, group_id: &GroupId) -> Result<bool, crate::ConnectionError> {
462 (**self).has_duplicate_dm(group_id)
463 }
464
465 fn get_conversation_ids_for_remote_log_publish(
467 &self,
468 ) -> Result<Vec<StoredGroupCommitLogPublicKey>, crate::ConnectionError> {
469 (**self).get_conversation_ids_for_remote_log_publish()
470 }
471
472 fn get_conversation_ids_for_remote_log_download(
473 &self,
474 ) -> Result<Vec<StoredGroupCommitLogPublicKey>, crate::ConnectionError> {
475 (**self).get_conversation_ids_for_remote_log_download()
476 }
477
478 fn get_conversation_ids_for_fork_check(&self) -> Result<Vec<Vec<u8>>, crate::ConnectionError> {
479 (**self).get_conversation_ids_for_fork_check()
480 }
481
482 fn get_conversation_ids_for_requesting_readds(
483 &self,
484 ) -> Result<Vec<StoredGroupForReaddRequest>, crate::ConnectionError> {
485 (**self).get_conversation_ids_for_requesting_readds()
486 }
487
488 fn get_conversation_ids_for_responding_readds(
489 &self,
490 ) -> Result<Vec<StoredGroupForRespondingReadds>, crate::ConnectionError> {
491 (**self).get_conversation_ids_for_responding_readds()
492 }
493
494 fn get_conversation_type(
495 &self,
496 group_id: &GroupId,
497 ) -> Result<ConversationType, crate::ConnectionError> {
498 (**self).get_conversation_type(group_id)
499 }
500
501 fn set_group_commit_log_public_key(
502 &self,
503 group_id: &GroupId,
504 public_key: &[u8],
505 ) -> Result<(), StorageError> {
506 (**self).set_group_commit_log_public_key(group_id, public_key)
507 }
508
509 fn set_group_commit_log_forked_status(
510 &self,
511 group_id: &GroupId,
512 is_forked: Option<bool>,
513 ) -> Result<(), StorageError> {
514 (**self).set_group_commit_log_forked_status(group_id, is_forked)
515 }
516
517 fn get_group_commit_log_forked_status(
518 &self,
519 group_id: &GroupId,
520 ) -> Result<Option<bool>, StorageError> {
521 (**self).get_group_commit_log_forked_status(group_id)
522 }
523
524 fn set_group_has_pending_leave_request_status(
525 &self,
526 group_id: &GroupId,
527 has_pending_leave_request: Option<bool>,
528 ) -> Result<(), StorageError> {
529 (**self).set_group_has_pending_leave_request_status(group_id, has_pending_leave_request)
530 }
531
532 fn get_groups_have_pending_leave_request(
533 &self,
534 ) -> Result<Vec<Vec<u8>>, crate::ConnectionError> {
535 (**self).get_groups_have_pending_leave_request()
536 }
537}
538
539impl<C: ConnectionExt> QueryGroup for DbConnection<C> {
540 #[xmtp_common::db_span]
542 fn find_groups<A: AsRef<GroupQueryArgs>>(
543 &self,
544 args: A,
545 ) -> Result<Vec<StoredGroup>, crate::ConnectionError> {
546 use crate::schema::consent_records::dsl as consent_dsl;
547
548 args.as_ref().validate()?;
549
550 let GroupQueryArgs {
551 allowed_states,
552 created_after_ns,
553 created_before_ns,
554 limit,
555 conversation_type,
556 consent_states,
557 include_sync_groups,
558 include_duplicate_dms,
559 last_activity_after_ns,
560 last_activity_before_ns,
561 should_publish_commit_log,
562 order_by,
563 } = args.as_ref();
564
565 let order_expression = match order_by.clone().unwrap_or_default() {
566 GroupQueryOrderBy::CreatedAt => {
567 diesel::dsl::sql::<diesel::sql_types::BigInt>("created_at_ns ASC")
568 }
569 GroupQueryOrderBy::LastActivity => diesel::dsl::sql::<diesel::sql_types::BigInt>(
570 "COALESCE(last_message_ns, created_at_ns) DESC",
571 ),
572 };
573
574 let mut query = dsl::groups
575 .filter(dsl::conversation_type.ne_all(ConversationType::virtual_types()))
576 .order(order_expression)
577 .into_boxed();
578
579 if !include_duplicate_dms {
580 query = query.filter(sql::<diesel::sql_types::Bool>(
583 "NOT EXISTS (
584 SELECT 1 FROM groups g2
585 WHERE COALESCE(g2.dm_id, g2.id) = COALESCE(groups.dm_id, groups.id)
586 AND (COALESCE(g2.last_message_ns, 0), g2.id) > (COALESCE(groups.last_message_ns, 0), groups.id)
587 )",
588 ));
589 }
590
591 if let Some(limit) = limit {
592 query = query.limit(*limit);
593 }
594
595 if let Some(allowed_states) = allowed_states {
596 query = query.filter(dsl::membership_state.eq_any(allowed_states));
597 }
598
599 if let Some(last_activity_after_ns) = last_activity_after_ns {
601 query = query.filter(
604 diesel::dsl::sql::<diesel::sql_types::BigInt>(
605 "COALESCE(last_message_ns, created_at_ns)",
606 )
607 .gt(last_activity_after_ns),
608 );
609 }
610
611 if let Some(created_after_ns) = created_after_ns {
612 query = query.filter(dsl::created_at_ns.gt(created_after_ns));
613 }
614
615 if let Some(last_activity_before_ns) = last_activity_before_ns {
616 query = query.filter(
617 diesel::dsl::sql::<diesel::sql_types::BigInt>(
618 "COALESCE(last_message_ns, created_at_ns)",
619 )
620 .lt(last_activity_before_ns),
621 );
622 }
623
624 if let Some(created_before_ns) = created_before_ns {
625 query = query.filter(dsl::created_at_ns.lt(created_before_ns));
626 }
627
628 if let Some(conversation_type) = conversation_type {
629 query = query.filter(dsl::conversation_type.eq(conversation_type));
630 }
631
632 let effective_consent_states = match &consent_states {
633 Some(states) if !states.is_empty() => states.clone(),
634 _ => vec![ConsentState::Allowed, ConsentState::Unknown],
635 };
636
637 let includes_unknown = effective_consent_states.contains(&ConsentState::Unknown);
638 let includes_all = effective_consent_states.len() == 3;
639
640 if let Some(should_publish_commit_log) = should_publish_commit_log {
641 query = query.filter(dsl::should_publish_commit_log.eq(should_publish_commit_log));
642 }
643
644 let filtered_states: Vec<_> = effective_consent_states
645 .iter()
646 .filter(|state| **state != ConsentState::Unknown)
647 .cloned()
648 .collect();
649
650 let mut groups = if includes_all {
651 self.raw_query(|conn| query.load::<StoredGroup>(conn))?
653 } else if includes_unknown {
654 let left_joined_query = query
656 .left_join(consent_dsl::consent_records.on(
657 sql::<diesel::sql_types::Text>("lower(hex(groups.id))").eq(consent_dsl::entity),
658 ))
659 .filter(
660 consent_dsl::state
661 .is_null()
662 .or(consent_dsl::state.eq(ConsentState::Unknown))
663 .or(consent_dsl::state.eq_any(filtered_states.clone())),
664 )
665 .select(dsl::groups::all_columns());
666
667 self.raw_query(|conn| left_joined_query.load::<StoredGroup>(conn))?
668 } else {
669 let inner_joined_query = query
671 .inner_join(consent_dsl::consent_records.on(
672 sql::<diesel::sql_types::Text>("lower(hex(groups.id))").eq(consent_dsl::entity),
673 ))
674 .filter(consent_dsl::state.eq_any(filtered_states.clone()))
675 .select(dsl::groups::all_columns());
676
677 self.raw_query(|conn| inner_joined_query.load::<StoredGroup>(conn))?
678 };
679
680 if matches!(conversation_type, Some(ConversationType::Sync)) || *include_sync_groups {
683 let query = dsl::groups.filter(dsl::conversation_type.eq(ConversationType::Sync));
684 let mut sync_groups = self.raw_query(|conn| query.load(conn))?;
685 groups.append(&mut sync_groups);
686 }
687
688 Ok(groups)
689 }
690
691 #[xmtp_common::db_span]
692 fn find_groups_by_id_paged<A: AsRef<GroupQueryArgs>>(
693 &self,
694 args: A,
695 offset: i64,
696 ) -> Result<Vec<StoredGroup>, crate::ConnectionError> {
697 let GroupQueryArgs {
698 created_after_ns,
699 created_before_ns,
700 limit,
701 ..
702 } = args.as_ref();
703
704 let mut query = groups::table
705 .filter(groups::conversation_type.ne_all(ConversationType::virtual_types()))
706 .order(groups::id)
707 .into_boxed();
708
709 if let Some(start_ns) = created_after_ns {
710 query = query.filter(groups::created_at_ns.gt(start_ns));
711 }
712 if let Some(end_ns) = created_before_ns {
713 query = query.filter(groups::created_at_ns.le(end_ns));
714 }
715
716 query = query.limit(limit.unwrap_or(100)).offset(offset);
717
718 self.raw_query(|conn| query.load::<StoredGroup>(conn))
719 }
720
721 #[xmtp_common::db_span]
723 fn update_group_membership<Id: AsRef<[u8]>>(
724 &self,
725 group_id: Id,
726 state: GroupMembershipState,
727 ) -> Result<(), crate::ConnectionError> {
728 self.raw_query(|conn| {
729 diesel::update(dsl::groups.find(group_id.as_ref()))
730 .set(dsl::membership_state.eq(state))
731 .execute(conn)
732 })?;
733
734 Ok(())
735 }
736
737 #[xmtp_common::db_span]
738 fn all_sync_groups(&self) -> Result<Vec<StoredGroup>, crate::ConnectionError> {
739 let query = dsl::groups
740 .order(dsl::created_at_ns.desc())
741 .filter(dsl::conversation_type.eq(ConversationType::Sync));
742
743 self.raw_query(|conn| query.load(conn))
744 }
745
746 #[xmtp_common::db_span]
747 fn find_sync_group(&self, id: &GroupId) -> Result<Option<StoredGroup>, crate::ConnectionError> {
748 let query = dsl::groups
749 .filter(dsl::conversation_type.eq(ConversationType::Sync))
750 .filter(dsl::id.eq(id));
751
752 self.raw_query(|conn| query.first(conn).optional())
753 }
754
755 #[xmtp_common::db_span]
756 fn primary_sync_group(&self) -> Result<Option<StoredGroup>, crate::ConnectionError> {
757 let query = dsl::groups
758 .order(dsl::created_at_ns.desc())
759 .filter(dsl::conversation_type.eq(ConversationType::Sync));
760
761 self.raw_query(|conn| query.first(conn).optional())
762 }
763
764 #[xmtp_common::db_span]
766 fn find_group(&self, id: &GroupId) -> Result<Option<StoredGroup>, crate::ConnectionError> {
767 let query = dsl::groups
768 .order(dsl::created_at_ns.asc())
769 .limit(1)
770 .filter(dsl::id.eq(id));
771 let groups = self.raw_query(|conn| query.load(conn))?;
772
773 Ok(groups.into_iter().next())
774 }
775
776 #[xmtp_common::db_span]
778 fn find_group_by_sequence_id(
779 &self,
780 cursor: Cursor,
781 ) -> Result<Option<StoredGroup>, crate::ConnectionError> {
782 let query = dsl::groups
783 .order(dsl::created_at_ns.asc())
784 .filter(dsl::sequence_id.eq(cursor.0 as i64));
785
786 let groups = self.raw_query(|conn| query.load(conn))?;
787
788 if groups.len() > 1 {
789 tracing::warn!(
790 welcome_id = cursor.0,
791 "More than one group found for welcome_id {}",
792 cursor.0
793 );
794 }
795 Ok(groups.into_iter().next())
796 }
797
798 fn get_rotated_at_ns(&self, group_id: &GroupId) -> Result<i64, StorageError> {
799 let last_ts: Option<i64> = self.raw_query(|conn| {
800 dsl::groups
801 .find(&group_id)
802 .select(dsl::rotated_at_ns)
803 .first(conn)
804 .optional()
805 })?;
806
807 last_ts.ok_or(StorageError::NotFound(NotFound::InstallationTimeForGroup(
808 *group_id,
809 )))
810 }
811
812 fn update_rotated_at_ns(&self, group_id: &GroupId) -> Result<(), StorageError> {
814 self.raw_query(|conn| {
815 let now = xmtp_common::time::now_ns();
816 diesel::update(dsl::groups.find(group_id))
817 .set(dsl::rotated_at_ns.eq(now))
818 .execute(conn)
819 })?;
820
821 Ok(())
822 }
823
824 fn get_installations_time_checked(&self, group_id: &GroupId) -> Result<i64, StorageError> {
825 let last_ts = self.raw_query(|conn| {
826 dsl::groups
827 .find(&group_id)
828 .select(dsl::installations_last_checked)
829 .first(conn)
830 .optional()
831 })?;
832
833 last_ts.ok_or(NotFound::InstallationTimeForGroup(*group_id).into())
834 }
835
836 fn update_installations_time_checked(&self, group_id: &GroupId) -> Result<(), StorageError> {
838 self.raw_query(|conn| {
839 let now = xmtp_common::time::now_ns();
840 diesel::update(dsl::groups.find(group_id))
841 .set(dsl::installations_last_checked.eq(now))
842 .execute(conn)
843 })?;
844
845 Ok(())
846 }
847
848 fn update_message_disappearing_from_ns(
849 &self,
850 group_id: &GroupId,
851 from_ns: Option<i64>,
852 ) -> Result<(), StorageError> {
853 self.raw_query(|conn| {
854 diesel::update(dsl::groups.find(group_id))
855 .set(dsl::message_disappear_from_ns.eq(from_ns))
856 .execute(conn)
857 })?;
858
859 Ok(())
860 }
861
862 fn update_message_disappearing_in_ns(
863 &self,
864 group_id: &GroupId,
865 in_ns: Option<i64>,
866 ) -> Result<(), StorageError> {
867 self.raw_query(|conn| {
868 diesel::update(dsl::groups.find(group_id))
869 .set(dsl::message_disappear_in_ns.eq(in_ns))
870 .execute(conn)
871 })?;
872
873 Ok(())
874 }
875
876 fn insert_or_replace_group(&self, group: StoredGroup) -> Result<StoredGroup, StorageError> {
877 let maybe_inserted_group: Option<StoredGroup> = self.raw_query(|conn| {
878 diesel::insert_into(dsl::groups)
879 .values(&group)
880 .on_conflict_do_nothing()
881 .get_result(conn)
882 .optional()
883 })?;
884
885 if maybe_inserted_group.is_none() {
886 let mut existing_group: StoredGroup =
887 self.raw_query(|conn| dsl::groups.find(&group.id).first(conn))?;
888 if matches!(
890 existing_group.membership_state,
891 GroupMembershipState::Restored
892 ) {
893 self.raw_query(|c| {
894 diesel::update(dsl::groups.find(&group.id))
895 .set(&group)
896 .execute(c)
897 })?;
898 }
899
900 if existing_group.sequence_id == group.sequence_id {
901 tracing::info!("Group welcome id already exists");
902 Err(StorageError::Duplicate(DuplicateItem::WelcomeId(
904 existing_group.cursor(),
905 )))
906 } else {
907 tracing::info!("Group already exists");
908 if group.sequence_id.is_some()
911 && (existing_group.sequence_id.is_none()
912 || group.sequence_id > existing_group.sequence_id)
913 {
914 self.raw_query(|c| {
915 diesel::update(dsl::groups.find(&group.id))
916 .set((dsl::sequence_id.eq(group.sequence_id),))
917 .execute(c)
918 })?;
919 existing_group.sequence_id = group.sequence_id;
920 }
921 Ok(existing_group)
922 }
923 } else {
924 Ok(self.raw_query(|c| dsl::groups.find(group.id).first(c))?)
925 }
926 }
927
928 fn group_cursors(&self) -> Result<Vec<Cursor>, crate::ConnectionError> {
930 self.raw_query(|conn| {
931 Ok(dsl::groups
932 .filter(dsl::sequence_id.is_not_null())
933 .select(dsl::sequence_id)
934 .load::<Option<i64>>(conn)?
935 .into_iter()
936 .flatten()
937 .map(|sequence| Cursor(sequence as u64))
938 .collect())
939 })
940 }
941
942 fn mark_group_as_maybe_forked(
943 &self,
944 group_id: &GroupId,
945 fork_details: String,
946 ) -> Result<(), StorageError> {
947 self.raw_query(|conn| {
948 diesel::update(dsl::groups.find(group_id))
949 .set((
950 dsl::maybe_forked.eq(true),
951 dsl::fork_details.eq(fork_details),
952 ))
953 .execute(conn)
954 })?;
955
956 Ok(())
957 }
958
959 fn clear_fork_flag_for_group(&self, group_id: &GroupId) -> Result<(), crate::ConnectionError> {
960 self.raw_query(|conn| {
961 diesel::update(dsl::groups.find(group_id))
962 .set((dsl::maybe_forked.eq(false), dsl::fork_details.eq("")))
963 .execute(conn)
964 })?;
965 Ok(())
966 }
967
968 fn has_duplicate_dm(&self, group_id: &GroupId) -> Result<bool, crate::ConnectionError> {
969 self.raw_query(|conn| {
970 let dm_id: Option<String> = dsl::groups
971 .filter(dsl::id.eq(group_id))
972 .select(dsl::dm_id)
973 .first::<Option<String>>(conn)
974 .optional()?
975 .flatten();
976
977 if let Some(dm_id) = dm_id {
978 let count: i64 = dsl::groups
979 .filter(dsl::conversation_type.eq(ConversationType::Dm))
980 .filter(dsl::dm_id.eq(dm_id))
981 .count()
982 .get_result(conn)?;
983
984 Ok(count > 1)
985 } else {
986 Ok(false)
987 }
988 })
989 }
990
991 #[xmtp_common::db_span]
994 fn get_conversation_ids_for_remote_log_publish(
995 &self,
996 ) -> Result<Vec<StoredGroupCommitLogPublicKey>, crate::ConnectionError> {
997 use crate::schema::consent_records::dsl as consent_dsl;
998
999 let query = dsl::groups
1000 .filter(
1001 dsl::conversation_type
1002 .eq(ConversationType::Dm)
1003 .or(dsl::conversation_type
1004 .eq(ConversationType::Group)
1005 .and(dsl::should_publish_commit_log.eq(true))),
1006 )
1007 .inner_join(consent_dsl::consent_records.on(
1008 sql::<diesel::sql_types::Text>("lower(hex(groups.id))").eq(consent_dsl::entity),
1009 ))
1010 .filter(consent_dsl::state.eq(ConsentState::Allowed))
1011 .select((dsl::id, dsl::commit_log_public_key))
1012 .order(dsl::created_at_ns.asc());
1013
1014 self.raw_query(|conn| query.load::<StoredGroupCommitLogPublicKey>(conn))
1015 }
1016
1017 #[xmtp_common::db_span]
1019 fn get_conversation_ids_for_remote_log_download(
1020 &self,
1021 ) -> Result<Vec<StoredGroupCommitLogPublicKey>, crate::ConnectionError> {
1022 use crate::schema::consent_records::dsl as consent_dsl;
1023
1024 let query = dsl::groups
1025 .filter(dsl::conversation_type.ne_all(ConversationType::virtual_types()))
1026 .inner_join(consent_dsl::consent_records.on(
1027 sql::<diesel::sql_types::Text>("lower(hex(groups.id))").eq(consent_dsl::entity),
1028 ))
1029 .filter(consent_dsl::state.eq(ConsentState::Allowed))
1030 .select((dsl::id, dsl::commit_log_public_key));
1031
1032 self.raw_query(|conn| query.load::<StoredGroupCommitLogPublicKey>(conn))
1033 }
1034
1035 #[xmtp_common::db_span]
1037 fn get_conversation_ids_for_fork_check(&self) -> Result<Vec<Vec<u8>>, crate::ConnectionError> {
1038 let query = dsl::groups
1039 .filter(
1040 dsl::conversation_type
1041 .ne_all(ConversationType::virtual_types())
1042 .and(
1043 dsl::is_commit_log_forked
1044 .is_null()
1045 .or(dsl::is_commit_log_forked.ne(Some(true))),
1046 ),
1047 )
1048 .select(dsl::id);
1049
1050 self.raw_query(|conn| query.load::<Vec<u8>>(conn))
1051 }
1052
1053 #[xmtp_common::db_span]
1054 fn get_conversation_ids_for_requesting_readds(
1055 &self,
1056 ) -> Result<Vec<StoredGroupForReaddRequest>, crate::ConnectionError> {
1057 use super::schema::{groups::dsl as groups_dsl, remote_commit_log::dsl as rcl_dsl};
1058 use diesel::dsl::max;
1059
1060 self.raw_query(|conn| {
1061 groups_dsl::groups
1062 .left_join(rcl_dsl::remote_commit_log.on(groups_dsl::id.eq(rcl_dsl::group_id)))
1063 .filter(
1064 groups_dsl::conversation_type
1065 .ne_all(ConversationType::virtual_types())
1066 .and(groups_dsl::is_commit_log_forked.eq(true)),
1067 )
1068 .group_by(groups_dsl::id)
1069 .select((groups_dsl::id, max(rcl_dsl::commit_sequence_id).nullable()))
1070 .load::<StoredGroupForReaddRequest>(conn)
1071 })
1072 }
1073
1074 #[xmtp_common::db_span]
1075 fn get_conversation_ids_for_responding_readds(
1076 &self,
1077 ) -> Result<Vec<StoredGroupForRespondingReadds>, crate::ConnectionError> {
1078 use super::schema::{groups::dsl as groups_dsl, readd_status::dsl as readd_dsl};
1079 use diesel::{ExpressionMethods, JoinOnDsl, QueryDsl};
1080
1081 self.raw_query(|conn| {
1082 readd_dsl::readd_status
1083 .inner_join(groups_dsl::groups.on(readd_dsl::group_id.eq(groups_dsl::id)))
1084 .filter(readd_dsl::requested_at_sequence_id.is_not_null())
1085 .filter(
1086 readd_dsl::requested_at_sequence_id
1087 .ge(readd_dsl::responded_at_sequence_id)
1088 .or(readd_dsl::responded_at_sequence_id.is_null()),
1089 )
1090 .select((
1091 groups_dsl::id,
1092 groups_dsl::dm_id,
1093 groups_dsl::conversation_type,
1094 groups_dsl::created_at_ns,
1095 ))
1096 .distinct()
1097 .load::<StoredGroupForRespondingReadds>(conn)
1098 })
1099 }
1100
1101 #[xmtp_common::db_span]
1102 fn get_conversation_type(
1103 &self,
1104 group_id: &GroupId,
1105 ) -> Result<ConversationType, crate::ConnectionError> {
1106 let query = dsl::groups
1107 .filter(dsl::id.eq(group_id))
1108 .select(dsl::conversation_type);
1109 let conversation_type = self.raw_query(|conn| query.first(conn))?;
1110 Ok(conversation_type)
1111 }
1112
1113 fn set_group_commit_log_public_key(
1114 &self,
1115 group_id: &GroupId,
1116 public_key: &[u8],
1117 ) -> Result<(), StorageError> {
1118 use crate::schema::groups::dsl;
1119 let num_updated = self.raw_query(|conn| {
1120 diesel::update(dsl::groups)
1121 .filter(
1122 dsl::id
1123 .eq(group_id)
1124 .and(dsl::commit_log_public_key.is_null()),
1125 )
1126 .set(dsl::commit_log_public_key.eq(public_key))
1127 .execute(conn)
1128 })?;
1129 if num_updated == 0 {
1130 return Err(StorageError::Duplicate(DuplicateItem::CommitLogPublicKey(
1131 group_id.as_ref().to_vec(),
1132 )));
1133 }
1134 Ok(())
1135 }
1136
1137 fn set_group_commit_log_forked_status(
1138 &self,
1139 group_id: &GroupId,
1140 is_forked: Option<bool>,
1141 ) -> Result<(), StorageError> {
1142 use crate::schema::groups::dsl;
1143 self.raw_query(|conn| {
1144 diesel::update(dsl::groups.find(group_id))
1145 .set(dsl::is_commit_log_forked.eq(is_forked))
1146 .execute(conn)
1147 })?;
1148 Ok(())
1149 }
1150
1151 fn get_group_commit_log_forked_status(
1152 &self,
1153 group_id: &GroupId,
1154 ) -> Result<Option<bool>, StorageError> {
1155 use crate::schema::groups::dsl;
1156 self.raw_query(|conn| {
1157 dsl::groups
1158 .find(group_id)
1159 .select(dsl::is_commit_log_forked)
1160 .first::<Option<bool>>(conn)
1161 })
1162 .map_err(StorageError::from)
1163 }
1164
1165 fn set_group_has_pending_leave_request_status(
1166 &self,
1167 group_id: &GroupId,
1168 has_pending_leave_request: Option<bool>,
1169 ) -> Result<(), StorageError> {
1170 use crate::schema::groups::dsl;
1171 self.raw_query(|conn| {
1172 diesel::update(dsl::groups.find(group_id))
1173 .set(dsl::has_pending_leave_request.eq(has_pending_leave_request))
1174 .execute(conn)
1175 })?;
1176 Ok(())
1177 }
1178
1179 #[xmtp_common::db_span]
1180 fn get_groups_have_pending_leave_request(
1181 &self,
1182 ) -> Result<Vec<Vec<u8>>, crate::ConnectionError> {
1183 let query = dsl::groups
1184 .filter(
1185 dsl::conversation_type
1186 .ne(ConversationType::Sync)
1187 .and(dsl::has_pending_leave_request.eq(Some(true))),
1188 )
1189 .select(dsl::id);
1190
1191 self.raw_query(|conn| query.load::<Vec<u8>>(conn))
1192 }
1193}
1194
1195#[repr(i32)]
1196#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq, AsExpression, FromSqlRow)]
1197#[diesel(sql_type = Integer)]
1198pub enum GroupMembershipState {
1200 Allowed = 1,
1202 Rejected = 2,
1204 Pending = 3,
1206 Restored = 4,
1208 PendingRemove = 5,
1210}
1211
1212impl ToSql<Integer, Sqlite> for GroupMembershipState
1213where
1214 i32: ToSql<Integer, Sqlite>,
1215{
1216 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
1217 out.set_value(*self as i32);
1218 Ok(IsNull::No)
1219 }
1220}
1221
1222impl FromSql<Integer, Sqlite> for GroupMembershipState
1223where
1224 i32: FromSql<Integer, Sqlite>,
1225{
1226 fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
1227 match i32::from_sql(bytes)? {
1228 1 => Ok(GroupMembershipState::Allowed),
1229 2 => Ok(GroupMembershipState::Rejected),
1230 3 => Ok(GroupMembershipState::Pending),
1231 4 => Ok(GroupMembershipState::Restored),
1232 5 => Ok(GroupMembershipState::PendingRemove),
1233 x => Err(format!("Unrecognized variant {}", x).into()),
1234 }
1235 }
1236}
1237
1238pub use xmtp_proto::types::ConversationType;
1239
1240pub trait DmIdExt {
1241 fn other_inbox_id(&self, id: &str) -> String;
1242}
1243
1244impl DmIdExt for String {
1245 fn other_inbox_id(&self, id: &str) -> String {
1246 let dm_id = &self[3..];
1248
1249 let target_inbox = if dm_id[..id.len()] == *id {
1251 &dm_id[(id.len() + 1)..]
1253 } else {
1254 &dm_id[..id.len()]
1255 };
1256
1257 target_inbox.to_string()
1258 }
1259}
1260
1261#[cfg(test)]
1262pub(crate) mod tests {
1263 pub use super::dms::tests::*;
1264 use super::*;
1265
1266 use crate::{
1267 Fetch, Store,
1268 consent_record::{ConsentType, StoredConsentRecord},
1269 readd_status::ReaddStatus,
1270 schema::groups::dsl::groups,
1271 test_utils::{with_connection, with_connection_async},
1272 };
1273 use xmtp_common::{Generate, assert_ok, rand_vec, time::now_ns};
1274
1275 pub fn generate_group(state: Option<GroupMembershipState>) -> StoredGroup {
1277 generate_group_with_created_at(state, now_ns())
1279 }
1280
1281 pub fn generate_group_with_created_at(
1282 state: Option<GroupMembershipState>,
1283 created_at_ns: i64,
1284 ) -> StoredGroup {
1285 let id = GroupId::generate();
1286 let membership_state = state.unwrap_or(GroupMembershipState::Allowed);
1287 StoredGroup::builder()
1288 .id(id)
1289 .created_at_ns(created_at_ns)
1290 .membership_state(membership_state)
1291 .added_by_inbox_id("placeholder_address")
1292 .build()
1293 .unwrap()
1294 }
1295
1296 pub fn generate_group_with_welcome(
1298 state: Option<GroupMembershipState>,
1299 welcome_id: Option<i64>,
1300 ) -> StoredGroup {
1301 let id = GroupId::generate();
1302 let created_at_ns = now_ns();
1303 let membership_state = state.unwrap_or(GroupMembershipState::Allowed);
1304 StoredGroup::builder()
1305 .id(id)
1306 .created_at_ns(created_at_ns)
1307 .membership_state(membership_state)
1308 .added_by_inbox_id("placeholder_address")
1309 .sequence_id(welcome_id.unwrap_or(xmtp_common::rand_i64()))
1310 .conversation_type(ConversationType::Group)
1311 .build()
1312 .unwrap()
1313 }
1314
1315 pub fn generate_consent_record(
1317 entity_type: ConsentType,
1318 state: ConsentState,
1319 entity: String,
1320 ) -> StoredConsentRecord {
1321 StoredConsentRecord {
1322 entity_type,
1323 state,
1324 entity,
1325 consented_at_ns: now_ns(),
1326 }
1327 }
1328
1329 #[xmtp_common::test]
1330 fn test_it_stores_group() {
1331 with_connection(|conn| {
1332 let test_group = generate_group(None);
1333
1334 test_group.store(conn).unwrap();
1335 assert_eq!(
1336 conn.raw_query(|raw_conn| groups.first::<StoredGroup>(raw_conn))
1337 .unwrap(),
1338 test_group
1339 );
1340 })
1341 }
1342
1343 #[xmtp_common::test]
1344 fn test_it_fetches_group() {
1345 with_connection(|conn| {
1346 let test_group = generate_group(None);
1347
1348 conn.raw_query(|raw_conn| {
1349 diesel::insert_into(groups)
1350 .values(test_group.clone())
1351 .execute(raw_conn)
1352 })
1353 .unwrap();
1354
1355 let fetched_group: Option<StoredGroup> = conn.fetch(&test_group.id).unwrap();
1356 assert_eq!(fetched_group, Some(test_group));
1357 })
1358 }
1359
1360 #[xmtp_common::test]
1361 fn test_it_updates_group_membership_state() {
1362 with_connection(|conn| {
1363 let test_group = generate_group(Some(GroupMembershipState::Pending));
1364
1365 test_group.store(conn).unwrap();
1366 conn.update_group_membership(test_group.id, GroupMembershipState::Rejected)
1367 .unwrap();
1368
1369 let updated_group: StoredGroup = conn.fetch(&test_group.id).ok().flatten().unwrap();
1370 assert_eq!(
1371 updated_group,
1372 StoredGroup {
1373 membership_state: GroupMembershipState::Rejected,
1374 ..test_group
1375 }
1376 );
1377 })
1378 }
1379
1380 #[xmtp_common::test]
1381 async fn test_find_groups() {
1382 let wait_in_wasm = async || {
1383 if cfg!(target_arch = "wasm32") {
1388 xmtp_common::time::sleep(std::time::Duration::from_millis(1)).await;
1389 }
1390 };
1391 with_connection_async(|conn| async move {
1392 let test_group_1 = generate_group(Some(GroupMembershipState::Pending));
1393 test_group_1.store(&conn).unwrap();
1394 wait_in_wasm().await;
1395 let test_group_2 = generate_group(Some(GroupMembershipState::Allowed));
1396 test_group_2.store(&conn).unwrap();
1397 wait_in_wasm().await;
1398 let test_group_3 = generate_dm(Some(GroupMembershipState::Allowed));
1399 test_group_3.store(&conn).unwrap();
1400
1401 let other_inbox_id = test_group_3
1402 .dm_id
1403 .unwrap()
1404 .other_inbox_id("placeholder_inbox_id_1");
1405
1406 let all_results = conn
1407 .find_groups(GroupQueryArgs {
1408 conversation_type: Some(ConversationType::Group),
1409 ..Default::default()
1410 })
1411 .unwrap();
1412 assert_eq!(all_results.len(), 2);
1413
1414 let pending_results = conn
1415 .find_groups(GroupQueryArgs {
1416 allowed_states: Some(vec![GroupMembershipState::Pending]),
1417 conversation_type: Some(ConversationType::Group),
1418 ..Default::default()
1419 })
1420 .unwrap();
1421 assert_eq!(pending_results[0].id, test_group_1.id);
1422 assert_eq!(pending_results.len(), 1);
1423
1424 let results_with_limit = conn
1426 .find_groups(GroupQueryArgs {
1427 conversation_type: Some(ConversationType::Group),
1428 limit: Some(1),
1429 ..Default::default()
1430 })
1431 .unwrap();
1432 assert_eq!(results_with_limit.len(), 1);
1433 assert_eq!(results_with_limit[0].id, test_group_1.id);
1434
1435 let results_with_created_at_ns_after = conn
1436 .find_groups(GroupQueryArgs {
1437 conversation_type: Some(ConversationType::Group),
1438 limit: Some(1),
1439 created_after_ns: Some(test_group_1.created_at_ns),
1440 ..Default::default()
1441 })
1442 .unwrap();
1443 assert_eq!(results_with_created_at_ns_after.len(), 1);
1444 assert_eq!(results_with_created_at_ns_after[0].id, test_group_2.id);
1445
1446 let synced_groups = conn.primary_sync_group().unwrap();
1448 assert!(synced_groups.is_none());
1449
1450 let dm_results = conn.find_groups(GroupQueryArgs::default()).unwrap();
1452 assert_eq!(dm_results.len(), 3);
1453 assert_eq!(dm_results[2].id, test_group_3.id);
1454
1455 let dm_result = conn
1457 .find_active_dm_group(format!("dm:placeholder_inbox_id_1:{}", other_inbox_id))
1458 .unwrap();
1459 assert!(dm_result.is_some());
1460
1461 let dm_results = conn
1463 .find_groups(GroupQueryArgs {
1464 conversation_type: Some(ConversationType::Dm),
1465 ..Default::default()
1466 })
1467 .unwrap();
1468 assert_eq!(dm_results.len(), 1);
1469 assert_eq!(dm_results[0].id, test_group_3.id);
1470 })
1471 .await
1472 }
1473
1474 #[xmtp_common::test]
1475 async fn test_installations_last_checked_is_updated() {
1476 with_connection_async(|conn| async move {
1477 let test_group = generate_group(None);
1478 test_group.store(&conn).unwrap();
1479
1480 assert_eq!(test_group.installations_last_checked, 0);
1482
1483 if cfg!(target_arch = "wasm32") {
1484 xmtp_common::time::sleep(std::time::Duration::from_millis(1)).await;
1487 }
1488 let result = conn.update_installations_time_checked(&test_group.id);
1491 assert_ok!(result);
1492
1493 let fetched_group: StoredGroup = conn.fetch(&test_group.id).ok().flatten().unwrap();
1495 assert_ne!(fetched_group.installations_last_checked, 0);
1496 assert!(fetched_group.created_at_ns < fetched_group.installations_last_checked);
1497 })
1498 .await
1499 }
1500
1501 #[xmtp_common::test]
1502 fn test_new_group_has_correct_purpose() {
1503 with_connection(|conn| {
1504 let test_group = generate_group(None);
1505
1506 conn.raw_query(|raw_conn| {
1507 diesel::insert_into(groups)
1508 .values(test_group.clone())
1509 .execute(raw_conn)
1510 })
1511 .unwrap();
1512
1513 let fetched_group: Option<StoredGroup> = conn.fetch(&test_group.id).unwrap();
1514 assert_eq!(fetched_group, Some(test_group));
1515 let conversation_type = fetched_group.unwrap().conversation_type;
1516 assert_eq!(conversation_type, ConversationType::Group);
1517 })
1518 }
1519
1520 #[xmtp_common::test]
1521 fn test_find_groups_by_consent_state() {
1522 with_connection(|conn| {
1523 let test_group_1 = generate_group(Some(GroupMembershipState::Allowed));
1524 test_group_1.store(conn).unwrap();
1525 let test_group_2 = generate_group(Some(GroupMembershipState::Allowed));
1526 test_group_2.store(conn).unwrap();
1527 let test_group_3 = generate_dm(Some(GroupMembershipState::Allowed));
1528 test_group_3.store(conn).unwrap();
1529 let test_group_4 = generate_dm(Some(GroupMembershipState::Allowed));
1530 test_group_4.store(conn).unwrap();
1531
1532 let test_group_1_consent = generate_consent_record(
1533 ConsentType::ConversationId,
1534 ConsentState::Allowed,
1535 hex::encode(test_group_1.id),
1536 );
1537 test_group_1_consent.store(conn).unwrap();
1538 let test_group_2_consent = generate_consent_record(
1539 ConsentType::ConversationId,
1540 ConsentState::Denied,
1541 hex::encode(test_group_2.id),
1542 );
1543 test_group_2_consent.store(conn).unwrap();
1544 let test_group_3_consent = generate_consent_record(
1545 ConsentType::ConversationId,
1546 ConsentState::Allowed,
1547 hex::encode(test_group_3.id),
1548 );
1549 test_group_3_consent.store(conn).unwrap();
1550
1551 let all_results = conn
1552 .find_groups(GroupQueryArgs {
1553 consent_states: Some(vec![
1554 ConsentState::Allowed,
1555 ConsentState::Unknown,
1556 ConsentState::Denied,
1557 ]),
1558 ..Default::default()
1559 })
1560 .unwrap();
1561 assert_eq!(all_results.len(), 4);
1562
1563 let default_results = conn.find_groups(GroupQueryArgs::default()).unwrap();
1564 assert_eq!(default_results.len(), 3);
1565
1566 let allowed_results = conn
1567 .find_groups(GroupQueryArgs {
1568 consent_states: Some(vec![ConsentState::Allowed]),
1569 ..Default::default()
1570 })
1571 .unwrap();
1572 assert_eq!(allowed_results.len(), 2);
1573
1574 let allowed_unknown_results = conn
1575 .find_groups(GroupQueryArgs {
1576 consent_states: Some(vec![ConsentState::Allowed, ConsentState::Unknown]),
1577 ..Default::default()
1578 })
1579 .unwrap();
1580 assert_eq!(allowed_unknown_results.len(), 3);
1581
1582 let denied_results = conn
1583 .find_groups(GroupQueryArgs {
1584 consent_states: Some(vec![ConsentState::Denied]),
1585 ..Default::default()
1586 })
1587 .unwrap();
1588 assert_eq!(denied_results.len(), 1);
1589 assert_eq!(denied_results[0].id, test_group_2.id);
1590
1591 let unknown_results = conn
1592 .find_groups(GroupQueryArgs {
1593 consent_states: Some(vec![ConsentState::Unknown]),
1594 ..Default::default()
1595 })
1596 .unwrap();
1597 assert_eq!(unknown_results.len(), 1);
1598 assert_eq!(unknown_results[0].id, test_group_4.id);
1599
1600 let empty_array_results = conn
1601 .find_groups(GroupQueryArgs {
1602 consent_states: Some(vec![]),
1603 ..Default::default()
1604 })
1605 .unwrap();
1606 assert_eq!(empty_array_results.len(), 3);
1607 })
1608 }
1609
1610 #[xmtp_common::test]
1611 fn test_get_sequence_ids() {
1612 with_connection(|conn| {
1613 let mls_groups = [
1614 generate_group_with_welcome(None, Some(30)),
1615 generate_group(None),
1616 generate_group(None),
1617 generate_group_with_welcome(None, Some(10)),
1618 ];
1619 for g in mls_groups.iter() {
1620 g.store(conn).unwrap();
1621 }
1622 assert_eq!(
1623 vec![30, 10],
1624 conn.group_cursors()
1625 .unwrap()
1626 .into_iter()
1627 .map(|c| c.0)
1628 .collect::<Vec<u64>>()
1629 );
1630 })
1631 }
1632
1633 #[xmtp_common::test(unwrap_try = true)]
1634 fn test_insert_or_replace_group_update_preserves_cursor() {
1635 with_connection(|conn| {
1636 let group = generate_group(None);
1637 assert!(group.sequence_id.is_none());
1638 group.store(conn).unwrap();
1639 conn.insert_or_replace_group(StoredGroup {
1640 sequence_id: Some(5),
1641 ..group.clone()
1642 })
1643 .unwrap();
1644 let stored: StoredGroup = conn.fetch(&group.id).unwrap().unwrap();
1645 assert_eq!(stored.sequence_id, Some(5));
1646 assert_eq!(conn.group_cursors().unwrap(), vec![Cursor(5)]);
1647 });
1648 }
1649
1650 #[xmtp_common::test]
1651 fn test_find_group_default_excludes_denied() {
1652 with_connection(|conn| {
1653 let allowed_group = generate_group(Some(GroupMembershipState::Allowed));
1655 allowed_group.store(conn).unwrap();
1656
1657 let denied_group = generate_group(Some(GroupMembershipState::Allowed));
1658 denied_group.store(conn).unwrap();
1659
1660 let unknown_group = generate_group(Some(GroupMembershipState::Allowed));
1661 unknown_group.store(conn).unwrap();
1662
1663 let allowed_consent = generate_consent_record(
1665 ConsentType::ConversationId,
1666 ConsentState::Allowed,
1667 hex::encode(allowed_group.id),
1668 );
1669 allowed_consent.store(conn).unwrap();
1670
1671 let denied_consent = generate_consent_record(
1672 ConsentType::ConversationId,
1673 ConsentState::Denied,
1674 hex::encode(denied_group.id),
1675 );
1676 denied_consent.store(conn).unwrap();
1677
1678 let default_results = conn.find_groups(GroupQueryArgs::default()).unwrap();
1680
1681 assert_eq!(default_results.len(), 2);
1683 let returned_ids: Vec<_> = default_results.iter().map(|g| &g.id).collect();
1684 assert!(returned_ids.contains(&&allowed_group.id));
1685 assert!(returned_ids.contains(&&unknown_group.id));
1686 assert!(!returned_ids.contains(&&denied_group.id));
1687 })
1688 }
1689
1690 #[xmtp_common::test(unwrap_try = true)]
1691 fn test_get_conversation_ids_for_remote_log_publish() {
1692 with_connection(|conn| {
1693 let mut group1 = generate_group(None);
1694 let mut group2 = generate_group(None);
1695 let mut group3 = generate_group(None);
1696 let mut group4 = generate_group(None);
1697 group1.should_publish_commit_log = true;
1698 group1.commit_log_public_key = None;
1699 generate_consent_record(
1700 ConsentType::ConversationId,
1701 ConsentState::Allowed,
1702 hex::encode(group1.id),
1703 )
1704 .store(conn)?;
1705 group2.should_publish_commit_log = true;
1706 group2.commit_log_public_key = Some(rand_vec::<32>());
1707
1708 group3.should_publish_commit_log = true;
1709 group3.commit_log_public_key = Some(rand_vec::<32>());
1710 generate_consent_record(
1711 ConsentType::ConversationId,
1712 ConsentState::Allowed,
1713 hex::encode(group3.id),
1714 )
1715 .store(conn)?;
1716 group4.should_publish_commit_log = false;
1717 group1.store(conn)?;
1718 group2.store(conn)?;
1719 group3.store(conn)?;
1720 group4.store(conn)?;
1721
1722 let commit_log_keys = conn.get_conversation_ids_for_remote_log_publish().unwrap();
1723 assert_eq!(commit_log_keys.len(), 2);
1724 assert_eq!(commit_log_keys[0].id, group1.id);
1725 assert_eq!(commit_log_keys[1].id, group3.id);
1726 assert_eq!(commit_log_keys[0].commit_log_public_key, None);
1727 assert_eq!(
1728 commit_log_keys[1].commit_log_public_key,
1729 group3.commit_log_public_key
1730 );
1731 })
1732 }
1733
1734 #[xmtp_common::test]
1735 fn test_get_conversation_ids_for_remote_log_publish_with_consent() {
1736 with_connection(|conn| {
1737 let mut allowed_group = generate_group(None);
1739 allowed_group.should_publish_commit_log = true;
1740 allowed_group.store(conn).unwrap();
1741
1742 let mut denied_group = generate_group(None);
1743 denied_group.should_publish_commit_log = true;
1744 denied_group.store(conn).unwrap();
1745
1746 let mut no_consent_group = generate_group(None);
1747 no_consent_group.should_publish_commit_log = true;
1748 no_consent_group.store(conn).unwrap();
1749
1750 let allowed_consent = generate_consent_record(
1752 ConsentType::ConversationId,
1753 ConsentState::Allowed,
1754 hex::encode(allowed_group.id),
1755 );
1756 allowed_consent.store(conn).unwrap();
1757
1758 let denied_consent = generate_consent_record(
1759 ConsentType::ConversationId,
1760 ConsentState::Denied,
1761 hex::encode(denied_group.id),
1762 );
1763 denied_consent.store(conn).unwrap();
1764
1765 let commit_log_keys = conn.get_conversation_ids_for_remote_log_publish().unwrap();
1767 assert_eq!(commit_log_keys.len(), 1);
1768 assert_eq!(commit_log_keys[0].id, allowed_group.id);
1769 })
1770 }
1771
1772 #[xmtp_common::test]
1773 fn test_get_conversation_ids_for_remote_log_download_with_consent() {
1774 with_connection(|conn| {
1775 let allowed_group = generate_group(None);
1777 allowed_group.store(conn).unwrap();
1778
1779 let denied_group = generate_group(None);
1780 denied_group.store(conn).unwrap();
1781
1782 let no_consent_group = generate_group(None);
1783 no_consent_group.store(conn).unwrap();
1784
1785 let mut sync_group = generate_group(None);
1787 sync_group.conversation_type = ConversationType::Sync;
1788 sync_group.store(conn).unwrap();
1789 let sync_consent = generate_consent_record(
1790 ConsentType::ConversationId,
1791 ConsentState::Allowed,
1792 hex::encode(sync_group.id),
1793 );
1794 sync_consent.store(conn).unwrap();
1795
1796 let allowed_consent = generate_consent_record(
1798 ConsentType::ConversationId,
1799 ConsentState::Allowed,
1800 hex::encode(allowed_group.id),
1801 );
1802 allowed_consent.store(conn).unwrap();
1803
1804 let denied_consent = generate_consent_record(
1805 ConsentType::ConversationId,
1806 ConsentState::Denied,
1807 hex::encode(denied_group.id),
1808 );
1809 denied_consent.store(conn).unwrap();
1810
1811 let conversation_ids = conn.get_conversation_ids_for_remote_log_download().unwrap();
1813 assert_eq!(conversation_ids.len(), 1);
1814 assert_eq!(conversation_ids[0].id, allowed_group.id);
1815 })
1816 }
1817
1818 #[xmtp_common::test]
1819 fn test_get_conversation_ids_for_responding_readds() {
1820 with_connection(|conn| {
1821 let group_id_1 = GroupId::ONE;
1823 let group_id_2 = GroupId::TWO;
1824 let group_id_3 = GroupId::THREE;
1825
1826 let group1 = StoredGroup::builder()
1827 .id(group_id_1)
1828 .created_at_ns(1000)
1829 .membership_state(GroupMembershipState::Allowed)
1830 .added_by_inbox_id("placeholder_address")
1831 .build()
1832 .unwrap();
1833 group1.store(conn).unwrap();
1834
1835 let group2 = StoredGroup::builder()
1836 .id(group_id_2)
1837 .created_at_ns(2000)
1838 .membership_state(GroupMembershipState::Allowed)
1839 .added_by_inbox_id("placeholder_address")
1840 .build()
1841 .unwrap();
1842 group2.store(conn).unwrap();
1843
1844 let group3 = StoredGroup::builder()
1845 .id(group_id_3)
1846 .created_at_ns(3000)
1847 .membership_state(GroupMembershipState::Allowed)
1848 .added_by_inbox_id("placeholder_address")
1849 .build()
1850 .unwrap();
1851 group3.store(conn).unwrap();
1852
1853 let test_cases = vec![
1855 ReaddStatus {
1857 group_id: group_id_1,
1858 installation_id: vec![1],
1859 requested_at_sequence_id: Some(10),
1860 responded_at_sequence_id: Some(5),
1861 },
1862 ReaddStatus {
1864 group_id: group_id_1,
1865 installation_id: vec![2],
1866 requested_at_sequence_id: Some(8),
1867 responded_at_sequence_id: None,
1868 },
1869 ReaddStatus {
1871 group_id: group_id_2,
1872 installation_id: vec![4],
1873 requested_at_sequence_id: Some(12),
1874 responded_at_sequence_id: Some(15),
1875 },
1876 ReaddStatus {
1878 group_id: group_id_2,
1879 installation_id: vec![5],
1880 requested_at_sequence_id: None,
1881 responded_at_sequence_id: Some(20),
1882 },
1883 ReaddStatus {
1885 group_id: group_id_3,
1886 installation_id: vec![6],
1887 requested_at_sequence_id: Some(25),
1888 responded_at_sequence_id: Some(25),
1889 },
1890 ];
1891
1892 for status in test_cases {
1894 status.store(conn).unwrap();
1895 }
1896
1897 let result = conn.get_conversation_ids_for_responding_readds().unwrap();
1899
1900 assert_eq!(result.len(), 2);
1903
1904 let mut result_group_ids: Vec<GroupId> = result.iter().map(|r| r.group_id).collect();
1906 result_group_ids.sort();
1907
1908 assert_eq!(result_group_ids[0], group_id_1);
1909 assert_eq!(result_group_ids[1], group_id_3);
1910
1911 let group1_result = result.iter().find(|r| r.group_id == group_id_1).unwrap();
1913 assert_eq!(group1_result.dm_id, None);
1914 assert_eq!(group1_result.conversation_type, ConversationType::Group);
1915 assert_eq!(group1_result.created_at_ns, 1000);
1916
1917 let group3_result = result.iter().find(|r| r.group_id == group_id_3).unwrap();
1918 assert_eq!(group3_result.dm_id, None);
1919 assert_eq!(group3_result.conversation_type, ConversationType::Group);
1920 assert_eq!(group3_result.created_at_ns, 3000);
1921 })
1922 }
1923
1924 #[test]
1946 fn test_find_group_span_emits_operation_and_skips_group_id() {
1947 use std::sync::{
1948 Arc,
1949 atomic::{AtomicU64, Ordering},
1950 };
1951 use tracing::field::{Field, Visit};
1952
1953 #[derive(Clone, Default)]
1956 struct CaptureSubscriber {
1957 buf: Arc<parking_lot::Mutex<String>>,
1958 next_id: Arc<AtomicU64>,
1959 }
1960
1961 struct FieldVisitor<'a>(&'a mut String);
1962 impl Visit for FieldVisitor<'_> {
1963 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
1964 self.0.push_str(field.name());
1965 self.0.push('=');
1966 self.0.push_str(&format!("{value:?}"));
1967 self.0.push(' ');
1968 }
1969 }
1970
1971 impl tracing::Subscriber for CaptureSubscriber {
1972 fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
1973 true
1974 }
1975
1976 fn new_span(&self, attrs: &tracing::span::Attributes<'_>) -> tracing::span::Id {
1977 let mut line = String::new();
1978 line.push_str("SPAN ");
1979 line.push_str(attrs.metadata().name());
1980 line.push_str(" {");
1981 let mut visitor = FieldVisitor(&mut line);
1982 attrs.record(&mut visitor);
1983 line.push_str("}\n");
1984 self.buf.lock().push_str(&line);
1985
1986 let id = self.next_id.fetch_add(1, Ordering::Relaxed) + 1;
1988 tracing::span::Id::from_u64(id)
1989 }
1990
1991 fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {}
1992 fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {
1993 }
1994 fn event(&self, _event: &tracing::Event<'_>) {}
1995 fn enter(&self, _span: &tracing::span::Id) {}
1996 fn exit(&self, _span: &tracing::span::Id) {}
1997 }
1998
1999 with_connection(|conn| {
2000 let test_group = generate_group(None);
2002 conn.raw_query(|raw_conn| {
2003 diesel::insert_into(groups)
2004 .values(test_group.clone())
2005 .execute(raw_conn)
2006 })
2007 .unwrap();
2008
2009 let capture = CaptureSubscriber::default();
2010
2011 tracing::subscriber::with_default(capture.clone(), || {
2014 let _ = conn.find_group(&test_group.id);
2016 });
2017
2018 let logged = capture.buf.lock().clone();
2019
2020 assert!(
2022 logged.contains("operation=\"db.find_group\""),
2023 "expected find_group span to carry operation=\"db.find_group\", got:\n{logged}"
2024 );
2025
2026 assert!(
2032 !logged.contains("id="),
2033 "skip_all contract violated: the `id` arg leaked into the find_group \
2034 span as a field (cardinality risk), got:\n{logged}"
2035 );
2036 let fields = logged
2041 .split_once('{')
2042 .and_then(|(_, rest)| rest.split_once('}'))
2043 .map(|(inner, _)| inner.trim())
2044 .unwrap_or("");
2045 assert_eq!(
2046 fields,
2047 "operation=\"db.find_group\" sentry.op=\"db\" sentry.name=\"db.find_group\" \
2048 otel.name=\"db.find_group\"",
2049 "find_group span must carry only operation + static sentry.*/otel.* fields, \
2050 got: {fields:?}"
2051 );
2052 })
2053 }
2054}