Skip to main content

xmtp_db/encrypted_store/
group_message.rs

1use super::ConnectionExt;
2use super::group::ConversationType;
3use super::schema::groups;
4use super::{
5    Sqlite,
6    db_connection::DbConnection,
7    schema::{
8        group_messages::{self, dsl},
9        groups::dsl as groups_dsl,
10    },
11};
12use crate::impl_fetch;
13use derive_builder::Builder;
14use diesel::{
15    backend::Backend,
16    deserialize::{self, FromSql, FromSqlRow},
17    dsl::sql as diesel_sql,
18    expression::AsExpression,
19    prelude::*,
20    serialize::{self, IsNull, Output, ToSql},
21    sql_types::Integer,
22};
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25use xmtp_common::{NS_IN_DAY, time::now_ns};
26use xmtp_content_types::{
27    actions, attachment, delete_message, group_updated, intent, leave_request, markdown,
28    membership_change, multi_remote_attachment, reaction, read_receipt, remote_attachment, reply,
29    text, transaction_reference, wallet_send_calls,
30};
31use xmtp_proto::types::{Cursor, GroupId};
32
33mod convert;
34#[cfg(test)]
35pub mod messages_newer_than_tests;
36#[cfg(test)]
37pub mod tests;
38
39#[derive(
40    Debug, Clone, Serialize, Deserialize, Queryable, Selectable, Identifiable, Eq, PartialEq,
41)]
42#[diesel(table_name = group_messages)]
43#[diesel(primary_key(id))]
44#[diesel(check_for_backend(Sqlite))]
45/// Successfully processed messages to be returned to the User.
46pub struct StoredGroupMessage {
47    /// Id of the message.
48    pub id: Vec<u8>,
49    /// Id of the group this message is tied to.
50    pub group_id: GroupId,
51    /// Contents of message after decryption.
52    pub decrypted_message_bytes: Vec<u8>,
53    /// Time in nanoseconds the message was sent.
54    pub sent_at_ns: i64,
55    /// Group Message Kind Enum: 1 = Application, 2 = MembershipChange
56    pub kind: GroupMessageKind,
57    /// The ID of the App Installation this message was sent from.
58    pub sender_installation_id: Vec<u8>,
59    /// The Inbox ID of the Sender
60    pub sender_inbox_id: String,
61    /// We optimistically store messages before sending.
62    pub delivery_status: DeliveryStatus,
63    /// The Content Type of the message
64    pub content_type: ContentType,
65    /// The content type version major
66    pub version_major: i32,
67    /// The content type version minor
68    pub version_minor: i32,
69    /// The ID of the authority defining the content type
70    pub authority_id: String,
71    /// The ID of a referenced message
72    pub reference_id: Option<Vec<u8>>,
73    /// The Message SequenceId
74    pub sequence_id: i64,
75    /// Canonical envelope hash assigned by the backend.
76    pub envelope_hash: Option<Vec<u8>>,
77    /// Backend retention metadata. This does not control message deletion.
78    pub expiry_ns: Option<i64>,
79    /// Time in nanoseconds the message was inserted into the database
80    /// This field is automatically set by the database
81    pub inserted_at_ns: i64,
82    /// Timestamp (in NS) after which the message must be deleted
83    pub expire_at_ns: Option<i64>,
84    /// Whether to send a push notification when publishing this message
85    pub should_push: bool,
86    /// The idempotency key the message id is derived from. Defaults to the send
87    /// timestamp, but callers may supply their own to make retries idempotent.
88    pub idempotency_key: String,
89}
90
91impl StoredGroupMessage {
92    pub fn cursor(&self) -> Cursor {
93        Cursor(self.sequence_id as u64)
94    }
95}
96
97// Separate Insertable struct that excludes inserted_at_ns to let the database set it
98#[derive(Debug, Clone, Insertable)]
99#[diesel(table_name = group_messages)]
100struct NewStoredGroupMessage {
101    pub id: Vec<u8>,
102    pub group_id: GroupId,
103    pub decrypted_message_bytes: Vec<u8>,
104    pub sent_at_ns: i64,
105    pub kind: GroupMessageKind,
106    pub sender_installation_id: Vec<u8>,
107    pub sender_inbox_id: String,
108    pub delivery_status: DeliveryStatus,
109    pub content_type: ContentType,
110    pub version_major: i32,
111    pub version_minor: i32,
112    pub authority_id: String,
113    pub reference_id: Option<Vec<u8>>,
114    pub sequence_id: i64,
115    /// Canonical envelope hash assigned by the backend.
116    pub envelope_hash: Option<Vec<u8>>,
117    /// Backend retention metadata. This does not control message deletion.
118    pub expiry_ns: Option<i64>,
119    // inserted_at_ns is NOT included - let database set it
120    pub expire_at_ns: Option<i64>,
121    pub should_push: bool,
122    pub idempotency_key: String,
123}
124
125impl From<&StoredGroupMessage> for NewStoredGroupMessage {
126    fn from(msg: &StoredGroupMessage) -> Self {
127        Self {
128            id: msg.id.clone(),
129            group_id: msg.group_id,
130            decrypted_message_bytes: msg.decrypted_message_bytes.clone(),
131            sent_at_ns: msg.sent_at_ns,
132            kind: msg.kind,
133            sender_installation_id: msg.sender_installation_id.clone(),
134            sender_inbox_id: msg.sender_inbox_id.clone(),
135            delivery_status: msg.delivery_status,
136            content_type: msg.content_type,
137            version_major: msg.version_major,
138            version_minor: msg.version_minor,
139            authority_id: msg.authority_id.clone(),
140            reference_id: msg.reference_id.clone(),
141            sequence_id: msg.sequence_id,
142            envelope_hash: msg.envelope_hash.clone(),
143            expiry_ns: msg.expiry_ns,
144            expire_at_ns: msg.expire_at_ns,
145            should_push: msg.should_push,
146            idempotency_key: msg.idempotency_key.clone(),
147        }
148    }
149}
150
151pub struct StoredGroupMessageWithReactions {
152    pub message: StoredGroupMessage,
153    // Messages who's reference_id matches this message's id
154    pub reactions: Vec<StoredGroupMessage>,
155}
156
157#[derive(Clone, Debug, PartialEq, Default)]
158pub enum SortDirection {
159    #[default]
160    Ascending,
161    Descending,
162}
163
164#[derive(Clone, Debug, PartialEq, Default)]
165pub enum SortBy {
166    #[default]
167    SentAt,
168    InsertedAt,
169}
170
171#[repr(i32)]
172#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq, AsExpression, FromSqlRow)]
173#[diesel(sql_type = Integer)]
174pub enum GroupMessageKind {
175    Application = 1,
176    MembershipChange = 2,
177}
178
179impl ToSql<Integer, Sqlite> for GroupMessageKind
180where
181    i32: ToSql<Integer, Sqlite>,
182{
183    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
184        out.set_value(*self as i32);
185        Ok(IsNull::No)
186    }
187}
188
189impl FromSql<Integer, Sqlite> for GroupMessageKind
190where
191    i32: FromSql<Integer, Sqlite>,
192{
193    fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
194        match i32::from_sql(bytes)? {
195            1 => Ok(GroupMessageKind::Application),
196            2 => Ok(GroupMessageKind::MembershipChange),
197            x => Err(format!("Unrecognized variant {}", x).into()),
198        }
199    }
200}
201
202/// Trait for determining if a message can be deleted by users.
203pub trait Deletable {
204    /// Returns whether this message can be deleted by users.
205    fn is_deletable(&self) -> bool;
206}
207
208impl Deletable for GroupMessageKind {
209    fn is_deletable(&self) -> bool {
210        match self {
211            // Application messages are deletable
212            GroupMessageKind::Application => true,
213            // Membership changes are transcript messages - not deletable
214            GroupMessageKind::MembershipChange => false,
215        }
216    }
217}
218
219//Legacy content types found at https://github.com/xmtp/xmtp-js/tree/main/content-types
220#[repr(i32)]
221#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, FromSqlRow, AsExpression)]
222#[diesel(sql_type = diesel::sql_types::Integer)]
223pub enum ContentType {
224    Unknown = 0,
225    Text = 1,
226    GroupMembershipChange = 2,
227    GroupUpdated = 3,
228    Reaction = 4,
229    ReadReceipt = 5,
230    Reply = 6,
231    Attachment = 7,
232    RemoteAttachment = 8,
233    TransactionReference = 9,
234    WalletSendCalls = 10,
235    LeaveRequest = 11,
236    Markdown = 12,
237    Actions = 13,
238    Intent = 14,
239    MultiRemoteAttachment = 15,
240    DeleteMessage = 16,
241}
242
243impl ContentType {
244    pub fn all() -> Vec<ContentType> {
245        vec![
246            ContentType::Unknown,
247            ContentType::Text,
248            ContentType::GroupMembershipChange,
249            ContentType::GroupUpdated,
250            ContentType::Reaction,
251            ContentType::ReadReceipt,
252            ContentType::Reply,
253            ContentType::Attachment,
254            ContentType::RemoteAttachment,
255            ContentType::TransactionReference,
256            ContentType::WalletSendCalls,
257            ContentType::LeaveRequest,
258            ContentType::Markdown,
259            ContentType::Actions,
260            ContentType::Intent,
261            ContentType::MultiRemoteAttachment,
262            ContentType::DeleteMessage,
263        ]
264    }
265}
266
267impl Deletable for ContentType {
268    fn is_deletable(&self) -> bool {
269        match self {
270            ContentType::GroupMembershipChange
271            | ContentType::GroupUpdated
272            | ContentType::LeaveRequest
273            | ContentType::Reaction
274            | ContentType::ReadReceipt
275            | ContentType::Actions
276            | ContentType::Intent
277            | ContentType::DeleteMessage
278            // Unknown content types default to non-deletable for safety
279            |ContentType::Unknown => false,
280
281            ContentType::Text
282            | ContentType::Markdown
283            | ContentType::Reply
284            | ContentType::Attachment
285            | ContentType::RemoteAttachment
286            | ContentType::TransactionReference
287            | ContentType::MultiRemoteAttachment
288            | ContentType::WalletSendCalls => true,
289        }
290    }
291}
292
293impl std::fmt::Display for ContentType {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        let as_string = match self {
296            Self::Unknown => "unknown",
297            Self::Text => text::TextCodec::TYPE_ID,
298            Self::Markdown => markdown::MarkdownCodec::TYPE_ID,
299            Self::GroupMembershipChange => membership_change::GroupMembershipChangeCodec::TYPE_ID,
300            Self::GroupUpdated => group_updated::GroupUpdatedCodec::TYPE_ID,
301            Self::Reaction => reaction::ReactionCodec::TYPE_ID,
302            Self::ReadReceipt => read_receipt::ReadReceiptCodec::TYPE_ID,
303            Self::Attachment => attachment::AttachmentCodec::TYPE_ID,
304            Self::RemoteAttachment => remote_attachment::RemoteAttachmentCodec::TYPE_ID,
305            Self::Reply => reply::ReplyCodec::TYPE_ID,
306            Self::TransactionReference => transaction_reference::TransactionReferenceCodec::TYPE_ID,
307            Self::WalletSendCalls => wallet_send_calls::WalletSendCallsCodec::TYPE_ID,
308            Self::LeaveRequest => leave_request::LeaveRequestCodec::TYPE_ID,
309            Self::Actions => actions::ActionsCodec::TYPE_ID,
310            Self::Intent => intent::IntentCodec::TYPE_ID,
311            Self::MultiRemoteAttachment => {
312                multi_remote_attachment::MultiRemoteAttachmentCodec::TYPE_ID
313            }
314            Self::DeleteMessage => delete_message::DeleteMessageCodec::TYPE_ID,
315        };
316
317        write!(f, "{}", as_string)
318    }
319}
320
321impl From<String> for ContentType {
322    fn from(type_id: String) -> Self {
323        match type_id.as_str() {
324            text::TextCodec::TYPE_ID => Self::Text,
325            markdown::MarkdownCodec::TYPE_ID => Self::Markdown,
326            membership_change::GroupMembershipChangeCodec::TYPE_ID => Self::GroupMembershipChange,
327            group_updated::GroupUpdatedCodec::TYPE_ID => Self::GroupUpdated,
328            reaction::ReactionCodec::TYPE_ID => Self::Reaction,
329            read_receipt::ReadReceiptCodec::TYPE_ID => Self::ReadReceipt,
330            reply::ReplyCodec::TYPE_ID => Self::Reply,
331            attachment::AttachmentCodec::TYPE_ID => Self::Attachment,
332            remote_attachment::RemoteAttachmentCodec::TYPE_ID => Self::RemoteAttachment,
333            transaction_reference::TransactionReferenceCodec::TYPE_ID => Self::TransactionReference,
334            wallet_send_calls::WalletSendCallsCodec::TYPE_ID => Self::WalletSendCalls,
335            leave_request::LeaveRequestCodec::TYPE_ID => Self::LeaveRequest,
336            actions::ActionsCodec::TYPE_ID => Self::Actions,
337            intent::IntentCodec::TYPE_ID => Self::Intent,
338            multi_remote_attachment::MultiRemoteAttachmentCodec::TYPE_ID => {
339                Self::MultiRemoteAttachment
340            }
341            delete_message::DeleteMessageCodec::TYPE_ID => Self::DeleteMessage,
342            _ => Self::Unknown,
343        }
344    }
345}
346
347impl ToSql<Integer, Sqlite> for ContentType
348where
349    i32: ToSql<Integer, Sqlite>,
350{
351    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
352        out.set_value(*self as i32);
353        Ok(IsNull::No)
354    }
355}
356
357impl FromSql<Integer, Sqlite> for ContentType
358where
359    i32: FromSql<Integer, Sqlite>,
360{
361    fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
362        match i32::from_sql(bytes)? {
363            0 => Ok(ContentType::Unknown),
364            1 => Ok(ContentType::Text),
365            2 => Ok(ContentType::GroupMembershipChange),
366            3 => Ok(ContentType::GroupUpdated),
367            4 => Ok(ContentType::Reaction),
368            5 => Ok(ContentType::ReadReceipt),
369            6 => Ok(ContentType::Reply),
370            7 => Ok(ContentType::Attachment),
371            8 => Ok(ContentType::RemoteAttachment),
372            9 => Ok(ContentType::TransactionReference),
373            10 => Ok(ContentType::WalletSendCalls),
374            11 => Ok(ContentType::LeaveRequest),
375            12 => Ok(ContentType::Markdown),
376            13 => Ok(ContentType::Actions),
377            14 => Ok(ContentType::Intent),
378            15 => Ok(ContentType::MultiRemoteAttachment),
379            16 => Ok(ContentType::DeleteMessage),
380            x => Err(format!("Unrecognized variant {}", x).into()),
381        }
382    }
383}
384
385#[repr(i32)]
386#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq, FromSqlRow, AsExpression)]
387#[diesel(sql_type = Integer)]
388pub enum DeliveryStatus {
389    Unpublished = 1,
390    Published = 2,
391    Failed = 3,
392}
393
394impl ToSql<Integer, Sqlite> for DeliveryStatus
395where
396    i32: ToSql<Integer, Sqlite>,
397{
398    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
399        out.set_value(*self as i32);
400        Ok(IsNull::No)
401    }
402}
403
404impl FromSql<Integer, Sqlite> for DeliveryStatus
405where
406    i32: FromSql<Integer, Sqlite>,
407{
408    fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
409        match i32::from_sql(bytes)? {
410            1 => Ok(DeliveryStatus::Unpublished),
411            2 => Ok(DeliveryStatus::Published),
412            3 => Ok(DeliveryStatus::Failed),
413            x => Err(format!("Unrecognized variant {}", x).into()),
414        }
415    }
416}
417
418impl_fetch!(StoredGroupMessage, group_messages, Vec<u8>, select);
419
420// Custom store implementation that uses NewStoredGroupMessage to exclude inserted_at_ns
421impl<C> crate::Store<C> for StoredGroupMessage
422where
423    C: crate::ConnectionExt,
424{
425    type Output = ();
426    fn store(&self, into: &C) -> Result<(), crate::StorageError> {
427        let new_msg = NewStoredGroupMessage::from(self);
428        super::stream_storage::stream_transaction(into, |conn| {
429            diesel::insert_into(group_messages::table)
430                .values(&new_msg)
431                .execute(conn)?;
432            super::delivery::assign_sequence(conn, &self.id)?;
433            Ok(())
434        })
435    }
436}
437
438// Custom store_or_ignore implementation that uses NewStoredGroupMessage
439impl<C> crate::StoreOrIgnore<C> for StoredGroupMessage
440where
441    C: crate::ConnectionExt,
442{
443    type Output = ();
444
445    fn store_or_ignore(&self, into: &C) -> Result<(), crate::StorageError> {
446        let new_msg = NewStoredGroupMessage::from(self);
447        super::stream_storage::stream_transaction(into, |conn| {
448            diesel::insert_or_ignore_into(group_messages::table)
449                .values(&new_msg)
450                .execute(conn)?;
451            super::delivery::assign_sequence(conn, &self.id)?;
452            Ok(())
453        })
454    }
455}
456
457#[derive(Default, Clone, Builder, Debug)]
458#[builder(setter(into))]
459pub struct MsgQueryArgs {
460    #[builder(default = None)]
461    pub sent_after_ns: Option<i64>,
462    #[builder(default = None)]
463    pub sent_before_ns: Option<i64>,
464    #[builder(default = None)]
465    pub kind: Option<GroupMessageKind>,
466    #[builder(default = None)]
467    pub delivery_status: Option<DeliveryStatus>,
468    #[builder(default = None)]
469    pub limit: Option<i64>,
470    #[builder(default = None)]
471    pub direction: Option<SortDirection>,
472    #[builder(default = None)]
473    pub content_types: Option<Vec<ContentType>>,
474    #[builder(default = None)]
475    pub exclude_content_types: Option<Vec<ContentType>>,
476    #[builder(default = None)]
477    pub exclude_sender_inbox_ids: Option<Vec<String>>,
478    #[builder(default = None)]
479    pub sort_by: Option<SortBy>,
480    #[builder(default = None)]
481    pub inserted_after_ns: Option<i64>,
482    #[builder(default = None)]
483    pub inserted_before_ns: Option<i64>,
484    #[builder(default = false)]
485    pub exclude_disappearing: bool,
486}
487
488impl MsgQueryArgs {
489    pub fn builder() -> MsgQueryArgsBuilder {
490        MsgQueryArgsBuilder::default()
491    }
492}
493
494#[derive(Default, Clone, Builder)]
495pub struct RelationQuery {
496    #[builder(default = None)]
497    pub content_types: Option<Vec<ContentType>>,
498    #[builder(default = None)]
499    pub limit: Option<i64>,
500    #[builder(default = SortDirection::Ascending)]
501    pub direction: SortDirection,
502}
503
504impl RelationQuery {
505    pub fn builder() -> RelationQueryBuilder {
506        RelationQueryBuilder::default()
507    }
508}
509
510pub type InboundRelations = HashMap<Vec<u8>, Vec<StoredGroupMessage>>;
511pub type OutboundRelations = HashMap<Vec<u8>, StoredGroupMessage>;
512pub type RelationCounts = HashMap<Vec<u8>, usize>;
513
514pub struct MessagesWithRelations {
515    pub messages: Vec<StoredGroupMessage>,
516    /// Messages referenced by any item in the `messages` vector, keyed by their ID
517    pub outbound_relations: HashMap<Vec<u8>, StoredGroupMessage>,
518    /// Messages that reference any item in the `messages` vector, grouped by the reference_id
519    pub inbound_relations: HashMap<Vec<u8>, Vec<StoredGroupMessage>>,
520}
521
522pub type LatestMessageTimeBySender = HashMap<String, i64>;
523
524pub trait QueryGroupMessage {
525    /// Query for group messages
526    fn get_group_messages(
527        &self,
528        group_id: &GroupId,
529        args: &MsgQueryArgs,
530    ) -> Result<Vec<StoredGroupMessage>, crate::ConnectionError>;
531
532    /// Count group messages matching the given criteria
533    fn count_group_messages(
534        &self,
535        group_id: &GroupId,
536        args: &MsgQueryArgs,
537    ) -> Result<i64, crate::ConnectionError>;
538
539    /// Return all `Application`-kind messages stored locally for `group_id`
540    /// whose `sequence_id` is NOT in the provided list. Used by tools that
541    /// compare local state against an authoritative set of sequence ids
542    /// (e.g. xdbg's healthcheck validator).
543    fn missing_messages(
544        &self,
545        group_id: &GroupId,
546        sequence_ids: &[u64],
547    ) -> Result<Vec<StoredGroupMessage>, crate::ConnectionError>;
548
549    fn group_messages_paged(
550        &self,
551        args: &MsgQueryArgs,
552        offset: i64,
553    ) -> Result<Vec<StoredGroupMessage>, crate::ConnectionError>;
554
555    /// Query for group messages with their reactions
556    fn get_group_messages_with_reactions(
557        &self,
558        group_id: &GroupId,
559        args: &MsgQueryArgs,
560    ) -> Result<Vec<StoredGroupMessageWithReactions>, crate::ConnectionError>;
561
562    fn get_inbound_relations(
563        &self,
564        group_id: &GroupId,
565        message_ids: &[&[u8]],
566        relation_query: RelationQuery,
567    ) -> Result<InboundRelations, crate::ConnectionError>;
568
569    fn get_outbound_relations(
570        &self,
571        group_id: &GroupId,
572        message_ids: &[&[u8]],
573    ) -> Result<OutboundRelations, crate::ConnectionError>;
574
575    fn get_inbound_relation_counts(
576        &self,
577        group_id: &GroupId,
578        message_ids: &[&[u8]],
579        relation_query: RelationQuery,
580    ) -> Result<RelationCounts, crate::ConnectionError>;
581
582    /// Get a particular group message
583    fn get_group_message<MessageId: AsRef<[u8]>>(
584        &self,
585        id: MessageId,
586    ) -> Result<Option<StoredGroupMessage>, crate::ConnectionError>;
587
588    fn get_latest_message_times_by_sender<Id: AsRef<[u8]>>(
589        &self,
590        group_id: Id,
591        allowed_content_types: &[ContentType],
592    ) -> Result<LatestMessageTimeBySender, crate::ConnectionError>;
593
594    /// Get a particular group message using the write connection
595    fn write_conn_get_group_message<MessageId: AsRef<[u8]>>(
596        &self,
597        id: MessageId,
598    ) -> Result<Option<StoredGroupMessage>, crate::ConnectionError>;
599
600    fn get_group_message_by_timestamp<Id: AsRef<[u8]>>(
601        &self,
602        group_id: Id,
603        timestamp: i64,
604    ) -> Result<Option<StoredGroupMessage>, crate::ConnectionError>;
605
606    fn get_group_message_by_cursor<Id: AsRef<[u8]>>(
607        &self,
608        group_id: Id,
609        sequence_id: Cursor,
610    ) -> Result<Option<StoredGroupMessage>, crate::ConnectionError>;
611
612    fn set_delivery_status_to_published<MessageId: AsRef<[u8]>>(
613        &self,
614        msg_id: &MessageId,
615        timestamp: u64,
616        cursor: Cursor,
617        message_expire_at_ns: Option<i64>,
618    ) -> Result<usize, crate::StorageError>;
619
620    fn set_delivery_status_to_failed<MessageId: AsRef<[u8]>>(
621        &self,
622        msg_id: &MessageId,
623    ) -> Result<usize, crate::ConnectionError>;
624
625    fn delete_expired_messages(&self) -> Result<Vec<StoredGroupMessage>, crate::ConnectionError>;
626
627    /// The soonest `expire_at_ns` among published Application messages that have
628    /// an expiry set, or `None` if no disappearing messages exist. Note this can
629    /// return a timestamp already in the past (an expiry that elapsed while the
630    /// worker was asleep) — the caller clamps the resulting sleep to `>= 0` and
631    /// deletes on the next wake. Same filters as `delete_expired_messages`
632    /// without its `expire_at_ns <= now` bound.
633    fn min_expire_at_ns(&self) -> Result<Option<i64>, crate::ConnectionError>;
634
635    fn delete_message_by_id<MessageId: AsRef<[u8]>>(
636        &self,
637        message_id: MessageId,
638    ) -> Result<usize, crate::ConnectionError>;
639
640    /// Stored messages above each group's cursor, attributed to their group.
641    /// The attribution matters: sequence ids are not scoped per group, so a
642    /// caller folding these into per-group state must never mix groups.
643    fn messages_newer_than(
644        &self,
645        cursors_by_group: &HashMap<Vec<u8>, xmtp_proto::types::Cursor>,
646    ) -> Result<Vec<(GroupId, Cursor)>, crate::ConnectionError>;
647
648    /// Clear messages from the database with optional filtering.
649    ///
650    /// # Arguments
651    /// * `group_ids` - If provided, only delete messages in these groups. If None, delete from all groups.
652    /// * `retention_days` - If provided, only delete messages older than this many days. If None, delete all matching messages.
653    ///
654    /// # Returns
655    /// The number of messages deleted.
656    fn clear_messages(
657        &self,
658        group_ids: Option<&[GroupId]>,
659        retention_days: Option<u32>,
660    ) -> Result<usize, crate::ConnectionError>;
661}
662
663impl<T> QueryGroupMessage for &T
664where
665    T: QueryGroupMessage,
666{
667    /// Query for group messages
668    fn get_group_messages(
669        &self,
670        group_id: &GroupId,
671        args: &MsgQueryArgs,
672    ) -> Result<Vec<StoredGroupMessage>, crate::ConnectionError> {
673        (**self).get_group_messages(group_id, args)
674    }
675
676    /// Count group messages matching the given criteria
677    fn count_group_messages(
678        &self,
679        group_id: &GroupId,
680        args: &MsgQueryArgs,
681    ) -> Result<i64, crate::ConnectionError> {
682        (**self).count_group_messages(group_id, args)
683    }
684
685    fn missing_messages(
686        &self,
687        group_id: &GroupId,
688        sequence_ids: &[u64],
689    ) -> Result<Vec<StoredGroupMessage>, crate::ConnectionError> {
690        (**self).missing_messages(group_id, sequence_ids)
691    }
692
693    fn group_messages_paged(
694        &self,
695        args: &MsgQueryArgs,
696        offset: i64,
697    ) -> Result<Vec<StoredGroupMessage>, crate::ConnectionError> {
698        (**self).group_messages_paged(args, offset)
699    }
700
701    /// Query for group messages with their reactions
702    fn get_group_messages_with_reactions(
703        &self,
704        group_id: &GroupId,
705        args: &MsgQueryArgs,
706    ) -> Result<Vec<StoredGroupMessageWithReactions>, crate::ConnectionError> {
707        (**self).get_group_messages_with_reactions(group_id, args)
708    }
709
710    fn get_inbound_relations(
711        &self,
712        group_id: &GroupId,
713        message_ids: &[&[u8]],
714        relation_query: RelationQuery,
715    ) -> Result<InboundRelations, crate::ConnectionError> {
716        (**self).get_inbound_relations(group_id, message_ids, relation_query)
717    }
718
719    fn get_outbound_relations(
720        &self,
721        group_id: &GroupId,
722        message_ids: &[&[u8]],
723    ) -> Result<OutboundRelations, crate::ConnectionError> {
724        (**self).get_outbound_relations(group_id, message_ids)
725    }
726
727    fn get_inbound_relation_counts(
728        &self,
729        group_id: &GroupId,
730        message_ids: &[&[u8]],
731        relation_query: RelationQuery,
732    ) -> Result<RelationCounts, crate::ConnectionError> {
733        (**self).get_inbound_relation_counts(group_id, message_ids, relation_query)
734    }
735
736    fn get_latest_message_times_by_sender<Id: AsRef<[u8]>>(
737        &self,
738        group_id: Id,
739        allowed_content_types: &[ContentType],
740    ) -> Result<LatestMessageTimeBySender, crate::ConnectionError> {
741        (**self).get_latest_message_times_by_sender(group_id, allowed_content_types)
742    }
743
744    /// Get a particular group message
745    fn get_group_message<MessageId: AsRef<[u8]>>(
746        &self,
747        id: MessageId,
748    ) -> Result<Option<StoredGroupMessage>, crate::ConnectionError> {
749        (**self).get_group_message(id)
750    }
751
752    /// Get a particular group message using the write connection
753    fn write_conn_get_group_message<MessageId: AsRef<[u8]>>(
754        &self,
755        id: MessageId,
756    ) -> Result<Option<StoredGroupMessage>, crate::ConnectionError> {
757        (**self).write_conn_get_group_message(id)
758    }
759
760    fn get_group_message_by_timestamp<Id: AsRef<[u8]>>(
761        &self,
762        group_id: Id,
763        timestamp: i64,
764    ) -> Result<Option<StoredGroupMessage>, crate::ConnectionError> {
765        (**self).get_group_message_by_timestamp(group_id, timestamp)
766    }
767
768    fn get_group_message_by_cursor<Id: AsRef<[u8]>>(
769        &self,
770        group_id: Id,
771        cursor: Cursor,
772    ) -> Result<Option<StoredGroupMessage>, crate::ConnectionError> {
773        (**self).get_group_message_by_cursor(group_id, cursor)
774    }
775
776    fn set_delivery_status_to_published<MessageId: AsRef<[u8]>>(
777        &self,
778        msg_id: &MessageId,
779        timestamp: u64,
780        cursor: Cursor,
781        message_expire_at_ns: Option<i64>,
782    ) -> Result<usize, crate::StorageError> {
783        (**self).set_delivery_status_to_published(msg_id, timestamp, cursor, message_expire_at_ns)
784    }
785
786    fn set_delivery_status_to_failed<MessageId: AsRef<[u8]>>(
787        &self,
788        msg_id: &MessageId,
789    ) -> Result<usize, crate::ConnectionError> {
790        (**self).set_delivery_status_to_failed(msg_id)
791    }
792
793    fn delete_expired_messages(&self) -> Result<Vec<StoredGroupMessage>, crate::ConnectionError> {
794        (**self).delete_expired_messages()
795    }
796
797    fn min_expire_at_ns(&self) -> Result<Option<i64>, crate::ConnectionError> {
798        (**self).min_expire_at_ns()
799    }
800
801    fn delete_message_by_id<MessageId: AsRef<[u8]>>(
802        &self,
803        message_id: MessageId,
804    ) -> Result<usize, crate::ConnectionError> {
805        (**self).delete_message_by_id(message_id)
806    }
807
808    fn messages_newer_than(
809        &self,
810        cursors_by_group: &HashMap<Vec<u8>, xmtp_proto::types::Cursor>,
811    ) -> Result<Vec<(GroupId, Cursor)>, crate::ConnectionError> {
812        (**self).messages_newer_than(cursors_by_group)
813    }
814
815    fn clear_messages(
816        &self,
817        group_ids: Option<&[GroupId]>,
818        retention_days: Option<u32>,
819    ) -> Result<usize, crate::ConnectionError> {
820        (**self).clear_messages(group_ids, retention_days)
821    }
822}
823
824// Macro to apply common message filters to any boxed query
825macro_rules! apply_message_filters {
826    ($query:expr, $args:expr) => {{
827        let mut query = $query;
828
829        if let Some(sent_after) = $args.sent_after_ns {
830            query = query.filter(dsl::sent_at_ns.gt(sent_after));
831        }
832
833        if let Some(sent_before) = $args.sent_before_ns {
834            query = query.filter(dsl::sent_at_ns.lt(sent_before));
835        }
836
837        if let Some(kind) = $args.kind {
838            query = query.filter(dsl::kind.eq(kind));
839        }
840
841        if let Some(status) = $args.delivery_status {
842            query = query.filter(dsl::delivery_status.eq(status));
843        }
844
845        if let Some(content_types) = &$args.content_types {
846            query = query.filter(dsl::content_type.eq_any(content_types));
847        }
848
849        if let Some(exclude_content_types) = &$args.exclude_content_types {
850            query = query.filter(dsl::content_type.ne_all(exclude_content_types));
851        }
852
853        if let Some(exclude_sender_inbox_ids) = &$args.exclude_sender_inbox_ids {
854            query = query.filter(dsl::sender_inbox_id.ne_all(exclude_sender_inbox_ids));
855        }
856
857        if let Some(inserted_after_ns) = $args.inserted_after_ns {
858            query = query.filter(dsl::inserted_at_ns.gt(inserted_after_ns));
859        }
860
861        if let Some(inserted_before_ns) = $args.inserted_before_ns {
862            query = query.filter(dsl::inserted_at_ns.lt(inserted_before_ns));
863        }
864
865        // Always exclude expired messages (expire_at_ns < now)
866        let current_time = now_ns();
867        query = query.filter(
868            dsl::expire_at_ns
869                .is_null()
870                .or(dsl::expire_at_ns.gt(current_time)),
871        );
872
873        query
874    }};
875}
876
877impl<C: ConnectionExt> QueryGroupMessage for DbConnection<C> {
878    /// Query for group messages
879    #[xmtp_common::db_span]
880    fn get_group_messages(
881        &self,
882        group_id: &GroupId,
883        args: &MsgQueryArgs,
884    ) -> Result<Vec<StoredGroupMessage>, crate::ConnectionError> {
885        use crate::schema::group_messages::dsl;
886
887        // Start with base query
888        let mut query = dsl::group_messages
889            .filter(group_id_filter(group_id.as_ref()))
890            .into_boxed();
891
892        // Apply common filters using macro
893        query = apply_message_filters!(query, args);
894
895        // Apply ordering with a rowid tie-break to ensure indexes get used when sorting.
896        query = match (
897            args.sort_by.clone().unwrap_or_default(),
898            args.direction.clone().unwrap_or_default(),
899        ) {
900            (SortBy::SentAt, SortDirection::Ascending) => {
901                query.order((dsl::sent_at_ns.asc(), diesel_sql::<Integer>("rowid").asc()))
902            }
903            (SortBy::SentAt, SortDirection::Descending) => query.order((
904                dsl::sent_at_ns.desc(),
905                diesel_sql::<Integer>("rowid").desc(),
906            )),
907            (SortBy::InsertedAt, SortDirection::Ascending) => query.order((
908                dsl::inserted_at_ns.asc(),
909                diesel_sql::<Integer>("rowid").asc(),
910            )),
911            (SortBy::InsertedAt, SortDirection::Descending) => query.order((
912                dsl::inserted_at_ns.desc(),
913                diesel_sql::<Integer>("rowid").desc(),
914            )),
915        };
916
917        if let Some(limit) = args.limit {
918            query = query.limit(limit);
919        }
920
921        self.raw_query(|conn| {
922            query
923                .select(StoredGroupMessage::as_select())
924                .load::<StoredGroupMessage>(conn)
925        })
926    }
927
928    /// Count group messages matching the given criteria
929    #[xmtp_common::db_span]
930    fn count_group_messages(
931        &self,
932        group_id: &GroupId,
933        args: &MsgQueryArgs,
934    ) -> Result<i64, crate::ConnectionError> {
935        use crate::schema::{group_messages::dsl, groups::dsl as groups_dsl};
936
937        // Check if this is a DM group
938        let is_dm = self.raw_query(|conn| {
939            groups_dsl::groups
940                .filter(groups_dsl::id.eq(group_id))
941                .select(groups_dsl::conversation_type)
942                .first::<ConversationType>(conn)
943        })? == ConversationType::Dm;
944
945        let include_group_updated = args
946            .content_types
947            .as_ref()
948            .map(|types| types.contains(&ContentType::GroupUpdated))
949            .unwrap_or(false);
950
951        // Start with base query
952        let mut query = dsl::group_messages
953            .filter(group_id_filter(group_id.as_ref()))
954            .into_boxed();
955
956        // For DM groups, exclude GroupUpdated messages unless specifically requested
957        // In find_group_messages we do some post-query deduplication to return the first GroupUpdated
958        // message but not the subsequent ones. That's not really an option here, so instead we are excluding
959        // them altogether.
960        //
961        // Ideally we would prevent the duplicate GroupUpdated messages from being inserted in the first place.
962        if is_dm && !include_group_updated {
963            query = query.filter(dsl::content_type.ne(ContentType::GroupUpdated));
964        }
965
966        // Apply common filters using macro
967        query = apply_message_filters!(query, args);
968
969        let count =
970            self.raw_query(|conn| query.select(diesel::dsl::count_star()).first::<i64>(conn))?;
971
972        Ok(count)
973    }
974
975    #[xmtp_common::db_span]
976    fn missing_messages(
977        &self,
978        group_id: &GroupId,
979        sequence_ids: &[u64],
980    ) -> Result<Vec<StoredGroupMessage>, crate::ConnectionError> {
981        use crate::schema::group_messages::{self, dsl};
982        use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
983
984        let sequence_ids: Vec<i64> = sequence_ids.iter().copied().map(|id| id as i64).collect();
985        let query = dsl::group_messages
986            .filter(dsl::group_id.eq(group_id))
987            .filter(dsl::sequence_id.is_not_null())
988            .filter(group_messages::sequence_id.ne_all(sequence_ids))
989            .filter(group_messages::kind.eq(GroupMessageKind::Application))
990            .order(group_messages::sequence_id.asc());
991
992        self.raw_query(|conn| query.select(StoredGroupMessage::as_select()).load(conn))
993    }
994
995    #[xmtp_common::db_span]
996    fn group_messages_paged(
997        &self,
998        args: &MsgQueryArgs,
999        offset: i64,
1000    ) -> Result<Vec<StoredGroupMessage>, crate::ConnectionError> {
1001        let MsgQueryArgs {
1002            sent_after_ns,
1003            sent_before_ns,
1004            limit,
1005            exclude_disappearing,
1006            ..
1007        } = args;
1008
1009        let mut query = group_messages::table
1010            .left_join(groups::table)
1011            .filter(groups::conversation_type.ne_all(ConversationType::virtual_types()))
1012            .filter(group_messages::kind.eq(GroupMessageKind::Application))
1013            .order_by(group_messages::id)
1014            .into_boxed();
1015
1016        if let Some(start_ns) = sent_after_ns {
1017            query = query.filter(group_messages::sent_at_ns.gt(start_ns));
1018        }
1019        if let Some(end_ns) = sent_before_ns {
1020            query = query.filter(group_messages::sent_at_ns.le(end_ns));
1021        }
1022        if *exclude_disappearing {
1023            query = query.filter(group_messages::expire_at_ns.is_null());
1024        } else {
1025            // Always exclude expired messages (expire_at_ns < now)
1026            let current_time = now_ns();
1027            query = query.filter(
1028                group_messages::expire_at_ns
1029                    .is_null()
1030                    .or(group_messages::expire_at_ns.gt(current_time)),
1031            );
1032        }
1033
1034        query = query.limit(limit.unwrap_or(100)).offset(offset);
1035
1036        self.raw_query(|conn| {
1037            query
1038                .select(StoredGroupMessage::as_select())
1039                .load::<StoredGroupMessage>(conn)
1040        })
1041    }
1042
1043    /// Query for group messages with their reactions
1044    #[xmtp_common::db_span]
1045    fn get_group_messages_with_reactions(
1046        &self,
1047        group_id: &GroupId,
1048        args: &MsgQueryArgs,
1049    ) -> Result<Vec<StoredGroupMessageWithReactions>, crate::ConnectionError> {
1050        // First get all the main messages
1051        let mut modified_args = args.clone();
1052        // filter out reactions from the main query so we don't get them twice
1053        let content_types = match modified_args.content_types.clone() {
1054            Some(content_types) => {
1055                let mut content_types = content_types.clone();
1056                content_types.retain(|content_type| *content_type != ContentType::Reaction);
1057                Some(content_types)
1058            }
1059            None => Some(vec![
1060                ContentType::Text,
1061                ContentType::GroupMembershipChange,
1062                ContentType::GroupUpdated,
1063                ContentType::ReadReceipt,
1064                ContentType::Reply,
1065                ContentType::Attachment,
1066                ContentType::RemoteAttachment,
1067                ContentType::TransactionReference,
1068                ContentType::Unknown,
1069            ]),
1070        };
1071
1072        modified_args.content_types = content_types;
1073        let messages = self.get_group_messages(group_id, &modified_args)?;
1074
1075        // Then get all reactions for these messages in a single query
1076        let message_ids: Vec<&[u8]> = messages.iter().map(|m| m.id.as_slice()).collect();
1077
1078        let mut reactions_query = dsl::group_messages
1079            .filter(group_id_filter(group_id.as_ref()))
1080            .filter(dsl::reference_id.is_not_null())
1081            .filter(dsl::reference_id.eq_any(message_ids))
1082            .into_boxed();
1083
1084        // Apply the same sorting as the main messages
1085        reactions_query = match args.direction.as_ref().unwrap_or(&SortDirection::Ascending) {
1086            SortDirection::Ascending => reactions_query.order(dsl::sent_at_ns.asc()),
1087            SortDirection::Descending => reactions_query.order(dsl::sent_at_ns.desc()),
1088        };
1089
1090        let reactions: Vec<StoredGroupMessage> = self.raw_query(|conn| {
1091            reactions_query
1092                .select(StoredGroupMessage::as_select())
1093                .load::<StoredGroupMessage>(conn)
1094        })?;
1095
1096        // Group reactions by parent message id
1097        let mut reactions_by_reference: HashMap<Vec<u8>, Vec<StoredGroupMessage>> = HashMap::new();
1098
1099        for reaction in reactions {
1100            if let Some(reference_id) = &reaction.reference_id {
1101                reactions_by_reference
1102                    .entry(reference_id.clone())
1103                    .or_default()
1104                    .push(reaction);
1105            }
1106        }
1107
1108        // Combine messages with their reactions
1109        let messages_with_reactions: Vec<StoredGroupMessageWithReactions> = messages
1110            .into_iter()
1111            .map(|message| {
1112                let message_clone = message.clone();
1113                StoredGroupMessageWithReactions {
1114                    message,
1115                    reactions: reactions_by_reference
1116                        .remove(&message_clone.id)
1117                        .unwrap_or_default(),
1118                }
1119            })
1120            .collect();
1121
1122        Ok(messages_with_reactions)
1123    }
1124
1125    #[xmtp_common::db_span]
1126    fn get_inbound_relations(
1127        &self,
1128        group_id: &GroupId,
1129        message_ids: &[&[u8]],
1130        relation_query: RelationQuery,
1131    ) -> Result<InboundRelations, crate::ConnectionError> {
1132        let mut inbound_relations: HashMap<Vec<u8>, Vec<StoredGroupMessage>> = HashMap::new();
1133
1134        let mut inbound_relations_query = dsl::group_messages
1135            .filter(group_id_filter(group_id.as_ref()))
1136            .filter(dsl::reference_id.is_not_null())
1137            .filter(dsl::reference_id.eq_any(message_ids))
1138            .into_boxed();
1139
1140        if relation_query.direction == SortDirection::Descending {
1141            inbound_relations_query = inbound_relations_query.order(dsl::sent_at_ns.desc());
1142        } else {
1143            inbound_relations_query = inbound_relations_query.order(dsl::sent_at_ns.asc());
1144        }
1145
1146        if let Some(content_types) = relation_query.content_types {
1147            inbound_relations_query =
1148                inbound_relations_query.filter(dsl::content_type.eq_any(content_types));
1149        }
1150
1151        if let Some(limit) = relation_query.limit {
1152            inbound_relations_query = inbound_relations_query.limit(limit);
1153        }
1154
1155        let raw_inbound_relations: Vec<StoredGroupMessage> = self.raw_query(|conn| {
1156            inbound_relations_query
1157                .select(StoredGroupMessage::as_select())
1158                .load::<StoredGroupMessage>(conn)
1159        })?;
1160
1161        for inbound_reference in raw_inbound_relations {
1162            if let Some(reference_id) = &inbound_reference.reference_id {
1163                inbound_relations
1164                    .entry(reference_id.clone())
1165                    .or_default()
1166                    .push(inbound_reference);
1167            }
1168        }
1169
1170        Ok(inbound_relations)
1171    }
1172
1173    #[xmtp_common::db_span]
1174    fn get_outbound_relations(
1175        &self,
1176        group_id: &GroupId,
1177        reference_ids: &[&[u8]],
1178    ) -> Result<OutboundRelations, crate::ConnectionError> {
1179        let outbound_references_query = dsl::group_messages
1180            .filter(group_id_filter(group_id.as_ref()))
1181            .filter(dsl::id.eq_any(reference_ids))
1182            .into_boxed();
1183
1184        let raw_outbound_references: Vec<StoredGroupMessage> = self.raw_query(|conn| {
1185            outbound_references_query
1186                .select(StoredGroupMessage::as_select())
1187                .load::<StoredGroupMessage>(conn)
1188        })?;
1189
1190        Ok(raw_outbound_references
1191            .into_iter()
1192            .map(|outbound| (outbound.id.clone(), outbound))
1193            .collect())
1194    }
1195
1196    #[xmtp_common::db_span]
1197    fn get_inbound_relation_counts(
1198        &self,
1199        group_id: &GroupId,
1200        message_ids: &[&[u8]],
1201        relation_query: RelationQuery,
1202    ) -> Result<RelationCounts, crate::ConnectionError> {
1203        let mut count_query = dsl::group_messages
1204            .filter(group_id_filter(group_id.as_ref()))
1205            .filter(dsl::reference_id.is_not_null())
1206            .filter(dsl::reference_id.eq_any(message_ids))
1207            .group_by(dsl::reference_id)
1208            .select((dsl::reference_id, diesel::dsl::count_star()))
1209            .into_boxed();
1210
1211        if let Some(content_types) = relation_query.content_types {
1212            count_query = count_query.filter(dsl::content_type.eq_any(content_types));
1213        }
1214
1215        let raw_counts: Vec<(Option<Vec<u8>>, i64)> =
1216            self.raw_query(|conn| count_query.load(conn))?;
1217
1218        Ok(raw_counts
1219            .into_iter()
1220            .filter_map(|(reference_id, count)| reference_id.map(|id| (id, count as usize)))
1221            .collect())
1222    }
1223
1224    #[xmtp_common::db_span]
1225    fn get_latest_message_times_by_sender<Id: AsRef<[u8]>>(
1226        &self,
1227        group_id: Id,
1228        allowed_content_types: &[ContentType],
1229    ) -> Result<LatestMessageTimeBySender, crate::ConnectionError> {
1230        let query = dsl::group_messages
1231            .filter(group_id_filter(group_id.as_ref()))
1232            .filter(dsl::content_type.eq_any(allowed_content_types))
1233            .group_by(dsl::sender_inbox_id)
1234            .select((dsl::sender_inbox_id, diesel::dsl::max(dsl::sent_at_ns)))
1235            .into_boxed();
1236
1237        let raw_results: Vec<(String, Option<i64>)> = self.raw_query(|conn| query.load(conn))?;
1238
1239        Ok(raw_results
1240            .into_iter()
1241            .filter_map(|(sender_inbox_id, max_sent_at_ns)| {
1242                max_sent_at_ns.map(|sent_at_ns| (sender_inbox_id, sent_at_ns))
1243            })
1244            .collect())
1245    }
1246
1247    /// Get a particular group message
1248    fn get_group_message<MessageId: AsRef<[u8]>>(
1249        &self,
1250        id: MessageId,
1251    ) -> Result<Option<StoredGroupMessage>, crate::ConnectionError> {
1252        self.raw_query(|conn| {
1253            dsl::group_messages
1254                .filter(dsl::id.eq(id.as_ref()))
1255                .select(StoredGroupMessage::as_select())
1256                .first::<StoredGroupMessage>(conn)
1257                .optional()
1258        })
1259    }
1260
1261    /// Get a particular group message using the write connection
1262    fn write_conn_get_group_message<MessageId: AsRef<[u8]>>(
1263        &self,
1264        id: MessageId,
1265    ) -> Result<Option<StoredGroupMessage>, crate::ConnectionError> {
1266        self.raw_query(|conn| {
1267            dsl::group_messages
1268                .filter(dsl::id.eq(id.as_ref()))
1269                .select(StoredGroupMessage::as_select())
1270                .first::<StoredGroupMessage>(conn)
1271                .optional()
1272        })
1273    }
1274
1275    fn get_group_message_by_timestamp<Id: AsRef<[u8]>>(
1276        &self,
1277        group_id: Id,
1278        timestamp: i64,
1279    ) -> Result<Option<StoredGroupMessage>, crate::ConnectionError> {
1280        self.raw_query(|conn| {
1281            dsl::group_messages
1282                .filter(dsl::group_id.eq(group_id.as_ref()))
1283                .filter(dsl::sent_at_ns.eq(&timestamp))
1284                .select(StoredGroupMessage::as_select())
1285                .first::<StoredGroupMessage>(conn)
1286                .optional()
1287        })
1288    }
1289
1290    fn get_group_message_by_cursor<Id: AsRef<[u8]>>(
1291        &self,
1292        group_id: Id,
1293        cursor: Cursor,
1294    ) -> Result<Option<StoredGroupMessage>, crate::ConnectionError> {
1295        self.raw_query(|conn| {
1296            dsl::group_messages
1297                .filter(dsl::group_id.eq(group_id.as_ref()))
1298                .filter(dsl::sequence_id.eq(cursor.0 as i64))
1299                .select(StoredGroupMessage::as_select())
1300                .first::<StoredGroupMessage>(conn)
1301                .optional()
1302        })
1303    }
1304
1305    fn set_delivery_status_to_published<MessageId: AsRef<[u8]>>(
1306        &self,
1307        msg_id: &MessageId,
1308        timestamp: u64,
1309        cursor: Cursor,
1310        message_expire_at_ns: Option<i64>,
1311    ) -> Result<usize, crate::StorageError> {
1312        tracing::info!(
1313            "Message [{}] published with cursor = {}",
1314            hex::encode(msg_id),
1315            cursor
1316        );
1317        super::stream_storage::stream_transaction(self, |conn| {
1318            let Some((group_id, previous_sent_at_ns)) = dsl::group_messages
1319                .filter(dsl::id.eq(msg_id.as_ref()))
1320                .select((dsl::group_id, dsl::sent_at_ns))
1321                .first::<(GroupId, i64)>(conn)
1322                .optional()?
1323            else {
1324                return Ok(0);
1325            };
1326            let changed = diesel::update(dsl::group_messages)
1327                .filter(dsl::id.eq(msg_id.as_ref()))
1328                .set((
1329                    dsl::delivery_status.eq(DeliveryStatus::Published),
1330                    dsl::sent_at_ns.eq(timestamp as i64),
1331                    dsl::sequence_id.eq(cursor.0 as i64),
1332                    dsl::expire_at_ns.eq(message_expire_at_ns),
1333                ))
1334                .execute(conn)?;
1335            if changed > 0 {
1336                let latest_sent_at_ns = dsl::group_messages
1337                    .filter(dsl::group_id.eq(group_id))
1338                    .order(dsl::sent_at_ns.desc())
1339                    .select(dsl::sent_at_ns)
1340                    .first::<i64>(conn)?;
1341                // Correct the replaced timestamp in either direction. Keep a newer message
1342                // or independent cached activity that does not match the replaced timestamp.
1343                diesel::update(groups_dsl::groups)
1344                    .filter(groups_dsl::id.eq(group_id))
1345                    .filter(
1346                        groups_dsl::last_message_ns
1347                            .is_null()
1348                            .or(groups_dsl::last_message_ns.eq(previous_sent_at_ns))
1349                            .or(groups_dsl::last_message_ns.lt(latest_sent_at_ns)),
1350                    )
1351                    .set(groups_dsl::last_message_ns.eq(latest_sent_at_ns))
1352                    .execute(conn)?;
1353                super::delivery::assign_sequence(conn, msg_id.as_ref())?;
1354            }
1355            Ok(changed)
1356        })
1357    }
1358
1359    fn set_delivery_status_to_failed<MessageId: AsRef<[u8]>>(
1360        &self,
1361        msg_id: &MessageId,
1362    ) -> Result<usize, crate::ConnectionError> {
1363        self.raw_query(|conn| {
1364            diesel::update(dsl::group_messages)
1365                .filter(dsl::id.eq(msg_id.as_ref()))
1366                .set((dsl::delivery_status.eq(DeliveryStatus::Failed),))
1367                .execute(conn)
1368        })
1369    }
1370
1371    #[xmtp_common::db_span]
1372    fn delete_expired_messages(&self) -> Result<Vec<StoredGroupMessage>, crate::ConnectionError> {
1373        self.raw_query(|conn| {
1374            use diesel::prelude::*;
1375            let now = now_ns();
1376
1377            diesel::delete(
1378                dsl::group_messages
1379                    .filter(dsl::delivery_status.eq(DeliveryStatus::Published))
1380                    .filter(dsl::kind.eq(GroupMessageKind::Application))
1381                    .filter(dsl::expire_at_ns.is_not_null())
1382                    .filter(dsl::expire_at_ns.le(now)),
1383            )
1384            .returning(StoredGroupMessage::as_returning())
1385            .load::<StoredGroupMessage>(conn)
1386        })
1387    }
1388
1389    #[xmtp_common::db_span]
1390    fn min_expire_at_ns(&self) -> Result<Option<i64>, crate::ConnectionError> {
1391        self.raw_query(|conn| {
1392            use diesel::dsl::min;
1393            use diesel::prelude::*;
1394            dsl::group_messages
1395                .filter(dsl::delivery_status.eq(DeliveryStatus::Published))
1396                .filter(dsl::kind.eq(GroupMessageKind::Application))
1397                .filter(dsl::expire_at_ns.is_not_null())
1398                .select(min(dsl::expire_at_ns))
1399                .first::<Option<i64>>(conn)
1400        })
1401    }
1402
1403    fn delete_message_by_id<MessageId: AsRef<[u8]>>(
1404        &self,
1405        message_id: MessageId,
1406    ) -> Result<usize, crate::ConnectionError> {
1407        self.raw_query(|conn| {
1408            use diesel::prelude::*;
1409            diesel::delete(dsl::group_messages.filter(dsl::id.eq(message_id.as_ref())))
1410                .execute(conn)
1411        })
1412    }
1413
1414    #[xmtp_common::db_span]
1415    fn messages_newer_than(
1416        &self,
1417        cursors_by_group: &HashMap<Vec<u8>, xmtp_proto::types::Cursor>,
1418    ) -> Result<Vec<(GroupId, Cursor)>, crate::ConnectionError> {
1419        // Each group contributes two bind parameters and one OR branch.
1420        const GROUPS_PER_QUERY: usize = 100;
1421        let groups: Vec<_> = cursors_by_group.iter().collect();
1422        let mut result = Vec::new();
1423        for batch in groups.chunks(GROUPS_PER_QUERY) {
1424            let mut filter = Box::new(dsl::group_id.eq(&[] as &[u8]))
1425                as Box<
1426                    dyn BoxableExpression<
1427                            group_messages::table,
1428                            Sqlite,
1429                            SqlType = diesel::sql_types::Bool,
1430                        >,
1431                >;
1432            for (group, cursor) in batch {
1433                filter = Box::new(
1434                    filter.or(dsl::group_id
1435                        .eq(group)
1436                        .and(dsl::sequence_id.gt(cursor.0 as i64))),
1437                );
1438            }
1439            let rows: Vec<(GroupId, i64)> = self.raw_query(|conn| {
1440                dsl::group_messages
1441                    .select((dsl::group_id, dsl::sequence_id))
1442                    .filter(filter)
1443                    .load(conn)
1444            })?;
1445            result.extend(
1446                rows.into_iter()
1447                    .map(|(group, sequence)| (group, Cursor(sequence as u64))),
1448            );
1449        }
1450        Ok(result)
1451    }
1452
1453    #[xmtp_common::db_span]
1454    fn clear_messages(
1455        &self,
1456        group_ids: Option<&[GroupId]>,
1457        retention_days: Option<u32>,
1458    ) -> Result<usize, crate::ConnectionError> {
1459        let mut query = diesel::delete(dsl::group_messages).into_boxed();
1460
1461        if let Some(group_ids) = group_ids {
1462            query = query.filter(dsl::group_id.eq_any(group_ids));
1463        }
1464
1465        if let Some(days) = retention_days {
1466            let limit = now_ns().saturating_sub(NS_IN_DAY.saturating_mul(i64::from(days)));
1467            query = query.filter(dsl::sent_at_ns.lt(limit));
1468        }
1469
1470        self.raw_query(|conn| query.execute(conn))
1471    }
1472}
1473
1474fn group_id_filter(
1475    group_id: &[u8],
1476) -> impl diesel::expression::BoxableExpression<
1477    group_messages::table,
1478    diesel::sqlite::Sqlite,
1479    SqlType = diesel::sql_types::Bool,
1480> + diesel::expression::NonAggregate {
1481    dsl::group_id.eq_any(
1482        groups_dsl::groups
1483            .filter(
1484                groups_dsl::id.eq(group_id).or(groups_dsl::dm_id.eq_any(
1485                    groups_dsl::groups
1486                        .select(groups_dsl::dm_id)
1487                        .filter(groups_dsl::id.eq(group_id))
1488                        .into_boxed(),
1489                )),
1490            )
1491            .select(groups_dsl::id),
1492    )
1493}