imessage_database/tables/messages/
message.rs

1/*!
2 This module represents common (but not all) columns in the `message` table.
3
4 # Iterating over Message Data
5
6 Generally, use [`Message::stream()`] to iterate over message rows.
7
8 ## Example
9 ```no_run
10 use imessage_database::{
11     tables::{
12         messages::Message,
13         table::{get_connection, Diagnostic, Table},
14     },
15     util::dirs::default_db_path,
16 };
17
18 // Your custom error type
19 struct ProgramError;
20
21 // Get the default database path and connect to it
22 let db_path = default_db_path();
23 let conn = get_connection(&db_path).unwrap();
24
25 Message::stream(&conn, |message_result| {
26    match message_result {
27        Ok(message) => println!("Message: {:#?}", message),
28        Err(e) => eprintln!("Error: {:?}", e),
29    }
30    Ok::<(), ProgramError>(())
31 }).unwrap();
32 ```
33
34 # Making Custom Message Queries
35
36 In addition columns from the `messages` table, there are several additional fields represented
37 by [`Message`]  that are not present in the database:
38
39 - [`Message::chat_id`]
40 - [`Message::num_attachments`]
41 - [`Message::deleted_from`]
42 - [`Message::num_replies`]
43
44 ## Sample Queries
45
46 To provide a custom query, ensure inclusion of the foregoing columns:
47
48 ```sql
49 SELECT
50     *,
51     c.chat_id,
52     (SELECT COUNT(*) FROM message_attachment_join a WHERE m.ROWID = a.message_id) as num_attachments,
53     d.chat_id as deleted_from,
54     (SELECT COUNT(*) FROM message m2 WHERE m2.thread_originator_guid = m.guid) as num_replies
55 FROM
56     message as m
57 LEFT JOIN chat_message_join as c ON m.ROWID = c.message_id
58 LEFT JOIN chat_recoverable_message_join as d ON m.ROWID = d.message_id
59 ORDER BY
60     m.date;
61 ```
62
63 If the source database does not include these required columns, include them as so:
64
65 ```sql
66 SELECT
67     *,
68     c.chat_id,
69     (SELECT COUNT(*) FROM message_attachment_join a WHERE m.ROWID = a.message_id) as num_attachments,
70     NULL as deleted_from,
71     0 as num_replies
72 FROM
73     message as m
74 LEFT JOIN chat_message_join as c ON m.ROWID = c.message_id
75 ORDER BY
76     m.date;
77 ```
78
79 ## Custom Query Example
80
81 The following will return an iterator over messages that have an associated emoji:
82
83
84 ```no_run
85 use imessage_database::{
86     tables::{
87         messages::Message,
88         table::{get_connection, Diagnostic, Table},
89     },
90     util::dirs::default_db_path
91 };
92
93 let db_path = default_db_path();
94 let db = get_connection(&db_path).unwrap();
95
96 let mut statement = db.prepare("
97 SELECT
98     *,
99     c.chat_id,
100     (SELECT COUNT(*) FROM message_attachment_join a WHERE m.ROWID = a.message_id) as num_attachments,
101     d.chat_id as deleted_from,
102     (SELECT COUNT(*) FROM message m2 WHERE m2.thread_originator_guid = m.guid) as num_replies
103 FROM
104     message as m
105 LEFT JOIN chat_message_join as c ON m.ROWID = c.message_id
106 LEFT JOIN chat_recoverable_message_join as d ON m.ROWID = d.message_id
107 WHERE m.associated_message_emoji IS NOT NULL
108 ORDER BY
109     m.date;
110 ").unwrap();
111
112 let messages = statement.query_map([], |row| Ok(Message::from_row(row))).unwrap();
113
114 messages.for_each(|msg| println!("{:#?}", Message::extract(msg)));
115 ```
116*/
117
118use std::{collections::HashMap, io::Read};
119
120use chrono::{DateTime, offset::Local};
121use crabstep::TypedStreamDeserializer;
122use plist::Value;
123use rusqlite::{CachedStatement, Connection, Error, Result, Row};
124
125use crate::{
126    error::{message::MessageError, table::TableError},
127    message_types::{
128        edited::{EditStatus, EditedMessage},
129        expressives::{BubbleEffect, Expressive, ScreenEffect},
130        text_effects::TextEffect,
131        variants::{Announcement, BalloonProvider, CustomBalloon, Tapback, TapbackAction, Variant},
132    },
133    tables::{
134        messages::{
135            body::{parse_body_legacy, parse_body_typedstream},
136            models::{BubbleComponent, GroupAction, Service, TextAttributes},
137            query_parts::{ios_13_older_query, ios_14_15_query, ios_16_newer_query},
138        },
139        table::{
140            ATTRIBUTED_BODY, CHAT_MESSAGE_JOIN, Cacheable, Diagnostic, MESSAGE,
141            MESSAGE_ATTACHMENT_JOIN, MESSAGE_PAYLOAD, MESSAGE_SUMMARY_INFO, RECENTLY_DELETED,
142            Table,
143        },
144    },
145    util::{
146        bundle_id::parse_balloon_bundle_id,
147        dates::{get_local_time, readable_diff},
148        output::{done_processing, processing},
149        query_context::QueryContext,
150        streamtyped,
151    },
152};
153
154/// The required columns, interpolated into the most recent schema due to performance considerations
155pub(crate) const COLS: &str = "rowid, guid, text, service, handle_id, destination_caller_id, subject, date, date_read, date_delivered, is_from_me, is_read, item_type, other_handle, share_status, share_direction, group_title, group_action_type, associated_message_guid, associated_message_type, balloon_bundle_id, expressive_send_style_id, thread_originator_guid, thread_originator_part, date_edited, associated_message_emoji";
156
157/// Represents a single row in the `message` table.
158///
159/// Additional information is available in the [parent](crate::tables::messages::message) module.
160#[derive(Debug)]
161#[allow(non_snake_case)]
162pub struct Message {
163    /// The unique identifier for the message in the database
164    pub rowid: i32,
165    /// The globally unique identifier for the message
166    pub guid: String,
167    /// The text of the message, which may require calling [`Self::generate_text()`] to populate
168    pub text: Option<String>,
169    /// The service the message was sent from
170    pub service: Option<String>,
171    /// The ID of the person who sent the message
172    pub handle_id: Option<i32>,
173    /// The address the database owner received the message at, i.e. a phone number or email
174    pub destination_caller_id: Option<String>,
175    /// The content of the Subject field
176    pub subject: Option<String>,
177    /// The date the message was written to the database
178    pub date: i64,
179    /// The date the message was read
180    pub date_read: i64,
181    /// The date a message was delivered
182    pub date_delivered: i64,
183    /// `true` if the database owner sent the message, else `false`
184    pub is_from_me: bool,
185    /// `true` if the message was read by the recipient, else `false`
186    pub is_read: bool,
187    /// Intermediate data for determining the [`Variant`] of a message
188    pub item_type: i32,
189    /// Optional handle for the recipient of a message that includes shared content
190    pub other_handle: Option<i32>,
191    /// Boolean determining whether some shared data is active or inactive, i.e. shared location being enabled or disabled
192    pub share_status: bool,
193    /// Boolean determining the direction shared data was sent; `false` indicates it was sent from the database owner, `true` indicates it was sent to the database owner
194    pub share_direction: Option<bool>,
195    /// If the message updates the [`display_name`](crate::tables::chat::Chat::display_name) of the chat, this field will be populated
196    pub group_title: Option<String>,
197    /// If the message modified for a group, this will be nonzero
198    pub group_action_type: i32,
199    /// The message GUID of a message associated with this one
200    pub associated_message_guid: Option<String>,
201    /// The numeric type code for the associated message, used to determine message variant
202    pub associated_message_type: Option<i32>,
203    /// The [bundle ID](https://developer.apple.com/help/app-store-connect/reference/app-bundle-information) of the app that generated the [`AppMessage`](crate::message_types::app::AppMessage)
204    pub balloon_bundle_id: Option<String>,
205    /// Intermediate data for determining the [`expressive`](crate::message_types::expressives) of a message
206    pub expressive_send_style_id: Option<String>,
207    /// Indicates the first message in a thread of replies in [`get_replies()`](crate::tables::messages::Message::get_replies)
208    pub thread_originator_guid: Option<String>,
209    /// Indicates the part of a message a reply is pointing to
210    pub thread_originator_part: Option<String>,
211    /// The date the message was most recently edited
212    pub date_edited: i64,
213    /// If present, this is the emoji associated with a custom emoji tapback
214    pub associated_message_emoji: Option<String>,
215    /// The [`identifier`](crate::tables::chat::Chat::chat_identifier) of the chat the message belongs to
216    pub chat_id: Option<i32>,
217    /// The number of attached files included in the message
218    pub num_attachments: i32,
219    /// The [`identifier`](crate::tables::chat::Chat::chat_identifier) of the chat the message was deleted from
220    pub deleted_from: Option<i32>,
221    /// The number of replies to the message
222    pub num_replies: i32,
223    /// The components of the message body, parsed by a [`TypedStreamDeserializer`] or [`streamtyped::parse()`]
224    pub components: Vec<BubbleComponent>,
225    /// The components of the message that may or may not have been edited or unsent
226    pub edited_parts: Option<EditedMessage>,
227}
228
229impl Table for Message {
230    fn from_row(row: &Row) -> Result<Message> {
231        Self::from_row_idx(row).or_else(|_| Self::from_row_named(row))
232    }
233
234    /// Convert data from the messages table to native Rust data structures, falling back to
235    /// more compatible queries to ensure compatibility with older database schemas
236    fn get(db: &Connection) -> Result<CachedStatement, TableError> {
237        Ok(db
238            .prepare_cached(&ios_16_newer_query(None))
239            .or_else(|_| db.prepare_cached(&ios_14_15_query(None)))
240            .or_else(|_| db.prepare_cached(&ios_13_older_query(None)))?)
241    }
242
243    fn extract(message: Result<Result<Self, Error>, Error>) -> Result<Self, TableError> {
244        match message {
245            Ok(Ok(message)) => Ok(message),
246            Err(why) | Ok(Err(why)) => Err(TableError::QueryError(why)),
247        }
248    }
249}
250
251impl Diagnostic for Message {
252    /// Emit diagnostic data for the Messages table
253    ///
254    /// # Example
255    ///
256    /// ```
257    /// use imessage_database::util::dirs::default_db_path;
258    /// use imessage_database::tables::table::{Diagnostic, get_connection};
259    /// use imessage_database::tables::messages::Message;
260    ///
261    /// let db_path = default_db_path();
262    /// let conn = get_connection(&db_path).unwrap();
263    /// Message::run_diagnostic(&conn);
264    /// ```
265    fn run_diagnostic(db: &Connection) -> Result<(), TableError> {
266        processing();
267        let mut messages_without_chat = db.prepare(&format!(
268            "
269            SELECT
270                COUNT(m.rowid)
271            FROM
272            {MESSAGE} as m
273            LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.rowid = c.message_id
274            WHERE
275                c.chat_id is NULL
276            ORDER BY
277                m.date
278            "
279        ))?;
280
281        let num_dangling: i32 = messages_without_chat
282            .query_row([], |r| r.get(0))
283            .unwrap_or(0);
284
285        let mut messages_in_more_than_one_chat_q = db.prepare(&format!(
286            "
287            SELECT
288                COUNT(*)
289            FROM (
290            SELECT DISTINCT
291                message_id
292              , COUNT(chat_id) AS c
293            FROM {CHAT_MESSAGE_JOIN}
294            GROUP BY
295                message_id
296            HAVING c > 1);
297            "
298        ))?;
299
300        let messages_in_more_than_one_chat: i32 = messages_in_more_than_one_chat_q
301            .query_row([], |r| r.get(0))
302            .unwrap_or(0);
303
304        let mut messages_count = db.prepare(&format!(
305            "
306            SELECT
307                COUNT(rowid)
308            FROM
309                {MESSAGE}
310            "
311        ))?;
312
313        let total_messages: i64 = messages_count.query_row([], |r| r.get(0)).unwrap_or(0);
314
315        done_processing();
316
317        println!("Message diagnostic data:");
318        println!("    Total messages: {total_messages}");
319        if num_dangling > 0 {
320            println!("    Messages not associated with a chat: {num_dangling}");
321        }
322        if messages_in_more_than_one_chat > 0 {
323            println!(
324                "    Messages belonging to more than one chat: {messages_in_more_than_one_chat}"
325            );
326        }
327        Ok(())
328    }
329}
330
331impl Cacheable for Message {
332    type K = String;
333    type V = HashMap<usize, Vec<Self>>;
334    /// Used for tapbacks that do not exist in a foreign key table
335    ///
336    /// Builds a map like:
337    ///
338    /// ```json
339    /// {
340    ///     "message_guid": {
341    ///         0: [Message, Message],
342    ///         1: [Message]
343    ///     }
344    /// }
345    /// ```
346    ///
347    /// Where the `0` and `1` are the tapback indexes in the body of the message mapped by `message_guid`
348    fn cache(db: &Connection) -> Result<HashMap<Self::K, Self::V>, TableError> {
349        // Create cache for user IDs
350        let mut map: HashMap<Self::K, Self::V> = HashMap::new();
351
352        // Create query
353        let statement = db.prepare(&format!(
354            "SELECT
355                 {COLS},
356                 c.chat_id,
357                 (SELECT COUNT(*) FROM {MESSAGE_ATTACHMENT_JOIN} a WHERE m.ROWID = a.message_id) as num_attachments,
358                 NULL as deleted_from,
359                 0 as num_replies
360             FROM
361                 {MESSAGE} as m
362             LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
363             WHERE m.associated_message_guid IS NOT NULL
364            "
365        )).or_else(|_| db.prepare(&format!(
366            "SELECT
367                 *,
368                 c.chat_id,
369                 (SELECT COUNT(*) FROM {MESSAGE_ATTACHMENT_JOIN} a WHERE m.ROWID = a.message_id) as num_attachments,
370                 NULL as deleted_from,
371                 0 as num_replies
372             FROM
373                 {MESSAGE} as m
374             LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
375             WHERE m.associated_message_guid IS NOT NULL
376            "
377        )));
378
379        if let Ok(mut statement) = statement {
380            // Execute query to build the message tapback map
381            let messages = statement.query_map([], |row| Ok(Message::from_row(row)))?;
382
383            // Iterate over the messages and update the map
384            for message in messages {
385                let message = Self::extract(message)?;
386                if message.is_tapback() {
387                    if let Some((idx, tapback_target_guid)) = message.clean_associated_guid() {
388                        map.entry(tapback_target_guid.to_string())
389                            .or_insert_with(HashMap::new)
390                            .entry(idx)
391                            .or_insert_with(Vec::new)
392                            .push(message);
393                    }
394                }
395            }
396        }
397
398        Ok(map)
399    }
400}
401
402impl Message {
403    /// Create a new [`Message`] from a [`Row`], using the fast indexed access method.
404    fn from_row_idx(row: &Row) -> Result<Message> {
405        Ok(Message {
406            rowid: row.get(0)?,
407            guid: row.get(1)?,
408            text: row.get(2).unwrap_or(None),
409            service: row.get(3).unwrap_or(None),
410            handle_id: row.get(4).unwrap_or(None),
411            destination_caller_id: row.get(5).unwrap_or(None),
412            subject: row.get(6).unwrap_or(None),
413            date: row.get(7)?,
414            date_read: row.get(8).unwrap_or(0),
415            date_delivered: row.get(9).unwrap_or(0),
416            is_from_me: row.get(10)?,
417            is_read: row.get(11).unwrap_or(false),
418            item_type: row.get(12).unwrap_or_default(),
419            other_handle: row.get(13).unwrap_or(None),
420            share_status: row.get(14).unwrap_or(false),
421            share_direction: row.get(15).unwrap_or(None),
422            group_title: row.get(16).unwrap_or(None),
423            group_action_type: row.get(17).unwrap_or(0),
424            associated_message_guid: row.get(18).unwrap_or(None),
425            associated_message_type: row.get(19).unwrap_or(None),
426            balloon_bundle_id: row.get(20).unwrap_or(None),
427            expressive_send_style_id: row.get(21).unwrap_or(None),
428            thread_originator_guid: row.get(22).unwrap_or(None),
429            thread_originator_part: row.get(23).unwrap_or(None),
430            date_edited: row.get(24).unwrap_or(0),
431            associated_message_emoji: row.get(25).unwrap_or(None),
432            chat_id: row.get(26).unwrap_or(None),
433            num_attachments: row.get(27)?,
434            deleted_from: row.get(28).unwrap_or(None),
435            num_replies: row.get(29)?,
436            components: vec![],
437            edited_parts: None,
438        })
439    }
440
441    /// Create a new [`Message`] from a [`Row`], using the slower, but more compatible, named access method.
442    fn from_row_named(row: &Row) -> Result<Message> {
443        Ok(Message {
444            rowid: row.get("rowid")?,
445            guid: row.get("guid")?,
446            text: row.get("text").unwrap_or(None),
447            service: row.get("service").unwrap_or(None),
448            handle_id: row.get("handle_id").unwrap_or(None),
449            destination_caller_id: row.get("destination_caller_id").unwrap_or(None),
450            subject: row.get("subject").unwrap_or(None),
451            date: row.get("date")?,
452            date_read: row.get("date_read").unwrap_or(0),
453            date_delivered: row.get("date_delivered").unwrap_or(0),
454            is_from_me: row.get("is_from_me")?,
455            is_read: row.get("is_read").unwrap_or(false),
456            item_type: row.get("item_type").unwrap_or_default(),
457            other_handle: row.get("other_handle").unwrap_or(None),
458            share_status: row.get("share_status").unwrap_or(false),
459            share_direction: row.get("share_direction").unwrap_or(None),
460            group_title: row.get("group_title").unwrap_or(None),
461            group_action_type: row.get("group_action_type").unwrap_or(0),
462            associated_message_guid: row.get("associated_message_guid").unwrap_or(None),
463            associated_message_type: row.get("associated_message_type").unwrap_or(None),
464            balloon_bundle_id: row.get("balloon_bundle_id").unwrap_or(None),
465            expressive_send_style_id: row.get("expressive_send_style_id").unwrap_or(None),
466            thread_originator_guid: row.get("thread_originator_guid").unwrap_or(None),
467            thread_originator_part: row.get("thread_originator_part").unwrap_or(None),
468            date_edited: row.get("date_edited").unwrap_or(0),
469            associated_message_emoji: row.get("associated_message_emoji").unwrap_or(None),
470            chat_id: row.get("chat_id").unwrap_or(None),
471            num_attachments: row.get("num_attachments")?,
472            deleted_from: row.get("deleted_from").unwrap_or(None),
473            num_replies: row.get("num_replies")?,
474            components: vec![],
475            edited_parts: None,
476        })
477    }
478
479    /// Generate the text of a message, deserializing it as [`typedstream`](crate::util::typedstream) (and falling back to [`streamtyped`]) data if necessary.
480    pub fn generate_text<'a>(&'a mut self, db: &'a Connection) -> Result<&'a str, MessageError> {
481        // Generate the edited message data
482        self.edited_parts = self
483            .is_edited()
484            .then(|| self.message_summary_info(db))
485            .flatten()
486            .as_ref()
487            .and_then(|payload| EditedMessage::from_map(payload).ok());
488
489        // Grab the body data from the table
490        if let Some(body) = self.attributed_body(db) {
491            // Attempt to deserialize the typedstream data
492            let mut typedstream = TypedStreamDeserializer::new(&body);
493            let parsed =
494                parse_body_typedstream(typedstream.iter_root().ok(), self.edited_parts.as_ref());
495
496            if let Some(parsed) = parsed {
497                // Determine if the message is a single URL
498                let is_single_url = match &parsed.components[..] {
499                    [BubbleComponent::Text(text_attrs)] => match &text_attrs[..] {
500                        [TextAttributes { effects, .. }] => {
501                            matches!(&effects[..], [TextEffect::Link(_)])
502                        }
503                        _ => false,
504                    },
505                    _ => false,
506                };
507
508                self.text = parsed.text;
509
510                // If the message is a single URL or has a balloon bundle ID
511                // set the components to just the app component
512                if self.balloon_bundle_id.is_some() {
513                    self.components = vec![BubbleComponent::App];
514                } else if is_single_url
515                    && self.has_blob(db, MESSAGE, MESSAGE_PAYLOAD, self.rowid.into())
516                {
517                    // This patch is to handle the case where a message is a single URL
518                    // but the `balloon_bundle_id` is not set.
519                    // This case can only hit if there was payload data provided for the preview,
520                    // but no `balloon_bundle_id` was set.
521                    self.balloon_bundle_id =
522                        Some("com.apple.messages.URLBalloonProvider".to_string());
523                    self.components = vec![BubbleComponent::App];
524                } else {
525                    self.components = parsed.components;
526                }
527            }
528
529            // If the above parsing failed, fall back to the legacy parser instead
530            if self.text.is_none() {
531                self.text = Some(streamtyped::parse(body)?);
532
533                // Fallback component parser as well
534                if self.components.is_empty() {
535                    self.components = parse_body_legacy(&self.text);
536                }
537            }
538        }
539
540        if let Some(t) = &self.text {
541            Ok(t)
542        } else {
543            Err(MessageError::NoText)
544        }
545    }
546
547    /// Generates the text using the legacy parser only, ignoring any typedstream data.
548    /// This is useful for messages that do not have typedstream data, such as those from older iOS versions.
549    ///
550    /// Warning: This method does not handle typedstream data and will not parse all message types correctly.
551    pub fn generate_text_legacy<'a>(
552        &'a mut self,
553        db: &'a Connection,
554    ) -> Result<&'a str, MessageError> {
555        // If the text is missing, try and query for it
556        if self.text.is_none() {
557            if let Some(body) = self.attributed_body(db) {
558                self.text = Some(streamtyped::parse(body)?);
559            }
560        }
561
562        // Fallback component parser as well
563        if self.components.is_empty() {
564            self.components = parse_body_legacy(&self.text);
565        }
566
567        self.text.as_deref().ok_or(MessageError::NoText)
568    }
569
570    /// Calculates the date a message was written to the database.
571    ///
572    /// This field is stored as a unix timestamp with an epoch of `2001-01-01 00:00:00` in the local time zone
573    ///
574    /// `offset` can be provided by [`get_offset`](crate::util::dates::get_offset) or manually.
575    pub fn date(&self, offset: &i64) -> Result<DateTime<Local>, MessageError> {
576        get_local_time(&self.date, offset)
577    }
578
579    /// Calculates the date a message was marked as delivered.
580    ///
581    /// This field is stored as a unix timestamp with an epoch of `2001-01-01 00:00:00` in the local time zone
582    ///
583    /// `offset` can be provided by [`get_offset`](crate::util::dates::get_offset) or manually.
584    pub fn date_delivered(&self, offset: &i64) -> Result<DateTime<Local>, MessageError> {
585        get_local_time(&self.date_delivered, offset)
586    }
587
588    /// Calculates the date a message was marked as read.
589    ///
590    /// This field is stored as a unix timestamp with an epoch of `2001-01-01 00:00:00` in the local time zone
591    ///
592    /// `offset` can be provided by [`get_offset`](crate::util::dates::get_offset) or manually.
593    pub fn date_read(&self, offset: &i64) -> Result<DateTime<Local>, MessageError> {
594        get_local_time(&self.date_read, offset)
595    }
596
597    /// Calculates the date a message was most recently edited.
598    ///
599    /// This field is stored as a unix timestamp with an epoch of `2001-01-01 00:00:00` in the local time zone
600    ///
601    /// `offset` can be provided by [`get_offset`](crate::util::dates::get_offset) or manually.
602    pub fn date_edited(&self, offset: &i64) -> Result<DateTime<Local>, MessageError> {
603        get_local_time(&self.date_edited, offset)
604    }
605
606    /// Gets the time until the message was read. This can happen in two ways:
607    ///
608    /// - You received a message, then waited to read it
609    /// - You sent a message, and the recipient waited to read it
610    ///
611    /// In the former case, this subtracts the date read column (`date_read`) from the date received column (`date`).
612    /// In the latter case, this subtracts the date delivered column (`date_delivered`) from the date received column (`date`).
613    ///
614    /// Not all messages get tagged with the read properties.
615    /// If more than one message has been sent in a thread before getting read,
616    /// only the most recent message will get the tag.
617    ///
618    /// `offset` can be provided by [`get_offset`](crate::util::dates::get_offset) or manually.
619    #[must_use]
620    pub fn time_until_read(&self, offset: &i64) -> Option<String> {
621        // Message we received
622        if !self.is_from_me && self.date_read != 0 && self.date != 0 {
623            return readable_diff(self.date(offset), self.date_read(offset));
624        }
625        // Message we sent
626        else if self.is_from_me && self.date_delivered != 0 && self.date != 0 {
627            return readable_diff(self.date(offset), self.date_delivered(offset));
628        }
629        None
630    }
631
632    /// `true` if the message is a response to a thread, else `false`
633    #[must_use]
634    pub fn is_reply(&self) -> bool {
635        self.thread_originator_guid.is_some()
636    }
637
638    /// `true` if the message is an [`Announcement`], else `false`
639    #[must_use]
640    pub fn is_announcement(&self) -> bool {
641        self.get_announcement().is_some()
642    }
643
644    /// `true` if the message is a [`Tapback`] to another message, else `false`
645    #[must_use]
646    pub fn is_tapback(&self) -> bool {
647        matches!(self.variant(), Variant::Tapback(..))
648    }
649
650    /// `true` if the message has an [`Expressive`], else `false`
651    #[must_use]
652    pub fn is_expressive(&self) -> bool {
653        self.expressive_send_style_id.is_some()
654    }
655
656    /// `true` if the message has a [URL preview](crate::message_types::url), else `false`
657    #[must_use]
658    pub fn is_url(&self) -> bool {
659        matches!(self.variant(), Variant::App(CustomBalloon::URL))
660    }
661
662    /// `true` if the message is a [`HandwrittenMessage`](crate::message_types::handwriting::models::HandwrittenMessage), else `false`
663    #[must_use]
664    pub fn is_handwriting(&self) -> bool {
665        matches!(self.variant(), Variant::App(CustomBalloon::Handwriting))
666    }
667
668    /// `true` if the message is a [`Digital Touch`](crate::message_types::digital_touch::models), else `false`
669    #[must_use]
670    pub fn is_digital_touch(&self) -> bool {
671        matches!(self.variant(), Variant::App(CustomBalloon::DigitalTouch))
672    }
673
674    /// `true` if the message was [`Edited`](crate::message_types::edited), else `false`
675    #[must_use]
676    pub fn is_edited(&self) -> bool {
677        self.date_edited != 0
678    }
679
680    /// `true` if the specified message component was [edited](crate::message_types::edited::EditStatus::Edited), else `false`
681    #[must_use]
682    pub fn is_part_edited(&self, index: usize) -> bool {
683        if let Some(edited_parts) = &self.edited_parts {
684            if let Some(part) = edited_parts.part(index) {
685                return matches!(part.status, EditStatus::Edited);
686            }
687        }
688        false
689    }
690
691    /// `true` if all message components were [unsent](crate::message_types::edited::EditStatus::Unsent), else `false`
692    #[must_use]
693    pub fn is_fully_unsent(&self) -> bool {
694        self.edited_parts.as_ref().is_some_and(|ep| {
695            ep.parts
696                .iter()
697                .all(|part| matches!(part.status, EditStatus::Unsent))
698        })
699    }
700
701    /// `true` if the message contains [`Attachment`](crate::tables::attachment::Attachment)s, else `false`
702    ///
703    /// Attachments can be queried with [`Attachment::from_message()`](crate::tables::attachment::Attachment::from_message).
704    #[must_use]
705    pub fn has_attachments(&self) -> bool {
706        self.num_attachments > 0
707    }
708
709    /// `true` if the message begins a thread, else `false`
710    #[must_use]
711    pub fn has_replies(&self) -> bool {
712        self.num_replies > 0
713    }
714
715    /// `true` if the message indicates a sent audio message was kept, else `false`
716    #[must_use]
717    pub fn is_kept_audio_message(&self) -> bool {
718        self.item_type == 5
719    }
720
721    /// `true` if the message is a [SharePlay/FaceTime](crate::message_types::variants::Variant::SharePlay) message, else `false`
722    #[must_use]
723    pub fn is_shareplay(&self) -> bool {
724        self.item_type == 6
725    }
726
727    /// `true` if the message was sent by the database owner, else `false`
728    #[must_use]
729    pub fn is_from_me(&self) -> bool {
730        if let (Some(other_handle), Some(share_direction)) =
731            (self.other_handle, self.share_direction)
732        {
733            self.is_from_me || other_handle != 0 && !share_direction
734        } else {
735            self.is_from_me
736        }
737    }
738
739    /// Get the group action for the current message
740    #[must_use]
741    pub fn group_action(&self) -> Option<GroupAction> {
742        GroupAction::from_message(self)
743    }
744
745    /// `true` if the message indicates a sender started sharing their location, else `false`
746    #[must_use]
747    pub fn started_sharing_location(&self) -> bool {
748        self.item_type == 4 && self.group_action_type == 0 && !self.share_status
749    }
750
751    /// `true` if the message indicates a sender stopped sharing their location, else `false`
752    #[must_use]
753    pub fn stopped_sharing_location(&self) -> bool {
754        self.item_type == 4 && self.group_action_type == 0 && self.share_status
755    }
756
757    /// `true` if the message was deleted and is recoverable, else `false`
758    ///
759    /// Messages removed by deleting an entire conversation or by deleting a single message
760    /// from a conversation are moved to a separate collection for up to 30 days. Messages
761    /// present in this collection are restored to the conversations they belong to. Apple
762    /// details this process [here](https://support.apple.com/en-us/HT202549#delete).
763    ///
764    /// Messages that have expired from this restoration process are permanently deleted and
765    /// cannot be recovered.
766    ///
767    /// Note: This is not the same as an [`Unsent`](crate::message_types::edited::EditStatus::Unsent) message.
768    #[must_use]
769    pub fn is_deleted(&self) -> bool {
770        self.deleted_from.is_some()
771    }
772
773    /// Get the index of the part of a message a reply is pointing to
774    fn get_reply_index(&self) -> usize {
775        if let Some(parts) = &self.thread_originator_part {
776            return match parts.split(':').next() {
777                Some(part) => str::parse::<usize>(part).unwrap_or(0),
778                None => 0,
779            };
780        }
781        0
782    }
783
784    /// Generate the SQL `WHERE` clause described by a [`QueryContext`].
785    ///
786    /// If `include_recoverable` is `true`, the filter includes messages from the recently deleted messages
787    /// table that match the chat IDs. This allows recovery of deleted messages that are still
788    /// present in the database but no longer visible in the Messages app.
789    pub(crate) fn generate_filter_statement(
790        context: &QueryContext,
791        include_recoverable: bool,
792    ) -> String {
793        let mut filters = String::new();
794
795        // Start date filter
796        if let Some(start) = context.start {
797            filters.push_str(&format!(" m.date >= {start}"));
798        }
799
800        // End date filter
801        if let Some(end) = context.end {
802            if !filters.is_empty() {
803                filters.push_str(" AND ");
804            }
805            filters.push_str(&format!(" m.date <= {end}"));
806        }
807
808        // Chat ID filter, optionally including recoverable messages
809        if let Some(chat_ids) = &context.selected_chat_ids {
810            if !filters.is_empty() {
811                filters.push_str(" AND ");
812            }
813
814            // Allocate the filter string for interpolation
815            let ids = chat_ids
816                .iter()
817                .map(std::string::ToString::to_string)
818                .collect::<Vec<String>>()
819                .join(", ");
820
821            if include_recoverable {
822                filters.push_str(&format!(" (c.chat_id IN ({ids}) OR d.chat_id IN ({ids}))"));
823            } else {
824                filters.push_str(&format!(" c.chat_id IN ({ids})"));
825            }
826        }
827
828        if !filters.is_empty() {
829            return format!("WHERE {filters}");
830        }
831        filters
832    }
833
834    /// Get the number of messages in the database
835    ///
836    /// # Example
837    ///
838    /// ```
839    /// use imessage_database::util::dirs::default_db_path;
840    /// use imessage_database::tables::table::{Diagnostic, get_connection};
841    /// use imessage_database::tables::messages::Message;
842    /// use imessage_database::util::query_context::QueryContext;
843    ///
844    /// let db_path = default_db_path();
845    /// let conn = get_connection(&db_path).unwrap();
846    /// let context = QueryContext::default();
847    /// Message::get_count(&conn, &context);
848    /// ```
849    pub fn get_count(db: &Connection, context: &QueryContext) -> Result<u64, TableError> {
850        let mut statement = if context.has_filters() {
851            db.prepare_cached(&format!(
852                "SELECT
853                     COUNT(*)
854                 FROM {MESSAGE} as m
855                 LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
856                 LEFT JOIN {RECENTLY_DELETED} as d ON m.ROWID = d.message_id
857                 {}",
858                Self::generate_filter_statement(context, true)
859            ))
860            .or_else(|_| {
861                db.prepare_cached(&format!(
862                    "SELECT
863                         COUNT(*)
864                     FROM {MESSAGE} as m
865                     LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
866                    {}",
867                    Self::generate_filter_statement(context, false)
868                ))
869            })?
870        } else {
871            db.prepare_cached(&format!("SELECT COUNT(*) FROM {MESSAGE}"))?
872        };
873        // Execute query, defaulting to zero if it fails
874        let count: u64 = statement.query_row([], |r| r.get(0)).unwrap_or(0);
875
876        Ok(count)
877    }
878
879    /// Stream messages from the database with optional filters.
880    ///
881    /// # Example
882    ///
883    /// ```
884    /// use imessage_database::util::dirs::default_db_path;
885    /// use imessage_database::tables::table::{Diagnostic, get_connection};
886    /// use imessage_database::tables::{messages::Message, table::Table};
887    /// use imessage_database::util::query_context::QueryContext;
888    ///
889    /// let db_path = default_db_path();
890    /// let conn = get_connection(&db_path).unwrap();
891    /// let context = QueryContext::default();
892    ///
893    /// let mut statement = Message::stream_rows(&conn, &context).unwrap();
894    ///
895    /// let messages = statement.query_map([], |row| Ok(Message::from_row(row))).unwrap();
896    ///
897    /// messages.map(|msg| println!("{:#?}", Message::extract(msg)));
898    /// ```
899    pub fn stream_rows<'a>(
900        db: &'a Connection,
901        context: &'a QueryContext,
902    ) -> Result<CachedStatement<'a>, TableError> {
903        if !context.has_filters() {
904            return Self::get(db);
905        }
906        Ok(db
907            .prepare_cached(&ios_16_newer_query(Some(&Self::generate_filter_statement(
908                context, true,
909            ))))
910            .or_else(|_| {
911                db.prepare_cached(&ios_14_15_query(Some(&Self::generate_filter_statement(
912                    context, false,
913                ))))
914            })
915            .or_else(|_| {
916                db.prepare_cached(&ios_13_older_query(Some(&Self::generate_filter_statement(
917                    context, false,
918                ))))
919            })?)
920    }
921
922    /// Clean and parse the associated message GUID for tapbacks and replies.
923    ///
924    /// Returns a tuple of (component index, message GUID) if present.
925    #[must_use]
926    pub fn clean_associated_guid(&self) -> Option<(usize, &str)> {
927        if let Some(guid) = &self.associated_message_guid {
928            if guid.starts_with("p:") {
929                let mut split = guid.split('/');
930                let index_str = split.next()?;
931                let message_id = split.next()?;
932                let index = str::parse::<usize>(&index_str.replace("p:", "")).unwrap_or(0);
933                return Some((index, message_id.get(0..36)?));
934            } else if guid.starts_with("bp:") {
935                return Some((0, guid.get(3..39)?));
936            }
937
938            return Some((0, guid.get(0..36)?));
939        }
940        None
941    }
942
943    /// Parse the index of a tapback from it's associated GUID field
944    fn tapback_index(&self) -> usize {
945        match self.clean_associated_guid() {
946            Some((x, _)) => x,
947            None => 0,
948        }
949    }
950
951    /// Build a `HashMap` of message component index to messages that reply to that component
952    pub fn get_replies(&self, db: &Connection) -> Result<HashMap<usize, Vec<Self>>, TableError> {
953        let mut out_h: HashMap<usize, Vec<Self>> = HashMap::new();
954
955        // No need to hit the DB if we know we don't have replies
956        if self.has_replies() {
957            let filters = format!("WHERE m.thread_originator_guid = \"{}\"", self.guid);
958
959            // No iOS 13 and prior used here because `thread_originator_guid` is not present in that schema
960            let mut statement = db
961                .prepare(&ios_16_newer_query(Some(&filters)))
962                .or_else(|_| db.prepare(&ios_14_15_query(Some(&filters))))?;
963
964            let iter = statement.query_map([], |row| Ok(Message::from_row(row)))?;
965
966            for message in iter {
967                let m = Message::extract(message)?;
968                let idx = m.get_reply_index();
969                match out_h.get_mut(&idx) {
970                    Some(body_part) => body_part.push(m),
971                    None => {
972                        out_h.insert(idx, vec![m]);
973                    }
974                }
975            }
976        }
977
978        Ok(out_h)
979    }
980
981    /// Get the variant of a message, see [`variants`](crate::message_types::variants) for detail.
982    #[must_use]
983    pub fn variant(&self) -> Variant {
984        // Check if a message was edited first as those have special properties
985        if self.is_edited() {
986            return Variant::Edited;
987        }
988
989        // Handle different types of bundle IDs next, as those are most common
990        if let Some(associated_message_type) = self.associated_message_type {
991            return match associated_message_type {
992                // Standard iMessages with either text or a message payload
993                0 | 2 | 3 => match parse_balloon_bundle_id(self.balloon_bundle_id.as_deref()) {
994                    // This is the most common case
995                    None => Variant::Normal,
996                    Some(bundle_id) => match bundle_id {
997                        "com.apple.messages.URLBalloonProvider" => Variant::App(CustomBalloon::URL),
998                        "com.apple.Handwriting.HandwritingProvider" => {
999                            Variant::App(CustomBalloon::Handwriting)
1000                        }
1001                        "com.apple.DigitalTouchBalloonProvider" => {
1002                            Variant::App(CustomBalloon::DigitalTouch)
1003                        }
1004                        "com.apple.PassbookUIService.PeerPaymentMessagesExtension" => {
1005                            Variant::App(CustomBalloon::ApplePay)
1006                        }
1007                        "com.apple.ActivityMessagesApp.MessagesExtension" => {
1008                            Variant::App(CustomBalloon::Fitness)
1009                        }
1010                        "com.apple.mobileslideshow.PhotosMessagesApp" => {
1011                            Variant::App(CustomBalloon::Slideshow)
1012                        }
1013                        "com.apple.SafetyMonitorApp.SafetyMonitorMessages" => {
1014                            Variant::App(CustomBalloon::CheckIn)
1015                        }
1016                        "com.apple.findmy.FindMyMessagesApp" => Variant::App(CustomBalloon::FindMy),
1017                        _ => Variant::App(CustomBalloon::Application(bundle_id)),
1018                    },
1019                },
1020
1021                // Stickers overlaid on messages
1022                1000 => {
1023                    Variant::Tapback(self.tapback_index(), TapbackAction::Added, Tapback::Sticker)
1024                }
1025
1026                // Tapbacks
1027                2000 => {
1028                    Variant::Tapback(self.tapback_index(), TapbackAction::Added, Tapback::Loved)
1029                }
1030                2001 => {
1031                    Variant::Tapback(self.tapback_index(), TapbackAction::Added, Tapback::Liked)
1032                }
1033                2002 => Variant::Tapback(
1034                    self.tapback_index(),
1035                    TapbackAction::Added,
1036                    Tapback::Disliked,
1037                ),
1038                2003 => {
1039                    Variant::Tapback(self.tapback_index(), TapbackAction::Added, Tapback::Laughed)
1040                }
1041                2004 => Variant::Tapback(
1042                    self.tapback_index(),
1043                    TapbackAction::Added,
1044                    Tapback::Emphasized,
1045                ),
1046                2005 => Variant::Tapback(
1047                    self.tapback_index(),
1048                    TapbackAction::Added,
1049                    Tapback::Questioned,
1050                ),
1051                2006 => Variant::Tapback(
1052                    self.tapback_index(),
1053                    TapbackAction::Added,
1054                    Tapback::Emoji(self.associated_message_emoji.as_deref()),
1055                ),
1056                2007 => {
1057                    Variant::Tapback(self.tapback_index(), TapbackAction::Added, Tapback::Sticker)
1058                }
1059                3000 => {
1060                    Variant::Tapback(self.tapback_index(), TapbackAction::Removed, Tapback::Loved)
1061                }
1062                3001 => {
1063                    Variant::Tapback(self.tapback_index(), TapbackAction::Removed, Tapback::Liked)
1064                }
1065                3002 => Variant::Tapback(
1066                    self.tapback_index(),
1067                    TapbackAction::Removed,
1068                    Tapback::Disliked,
1069                ),
1070                3003 => Variant::Tapback(
1071                    self.tapback_index(),
1072                    TapbackAction::Removed,
1073                    Tapback::Laughed,
1074                ),
1075                3004 => Variant::Tapback(
1076                    self.tapback_index(),
1077                    TapbackAction::Removed,
1078                    Tapback::Emphasized,
1079                ),
1080                3005 => Variant::Tapback(
1081                    self.tapback_index(),
1082                    TapbackAction::Removed,
1083                    Tapback::Questioned,
1084                ),
1085                3006 => Variant::Tapback(
1086                    self.tapback_index(),
1087                    TapbackAction::Removed,
1088                    Tapback::Emoji(self.associated_message_emoji.as_deref()),
1089                ),
1090                3007 => Variant::Tapback(
1091                    self.tapback_index(),
1092                    TapbackAction::Removed,
1093                    Tapback::Sticker,
1094                ),
1095
1096                // Unknown
1097                x => Variant::Unknown(x),
1098            };
1099        }
1100
1101        // Any other rarer cases belong here
1102        if self.is_shareplay() {
1103            return Variant::SharePlay;
1104        }
1105
1106        Variant::Normal
1107    }
1108
1109    /// Determine the type of announcement a message contains, if it contains one
1110    #[must_use]
1111    pub fn get_announcement(&self) -> Option<Announcement> {
1112        if let Some(action) = self.group_action() {
1113            return Some(Announcement::GroupAction(action));
1114        }
1115
1116        if self.is_fully_unsent() {
1117            return Some(Announcement::FullyUnsent);
1118        }
1119
1120        if self.is_kept_audio_message() {
1121            return Some(Announcement::AudioMessageKept);
1122        }
1123
1124        None
1125    }
1126
1127    /// Determine the service the message was sent from, i.e. iMessage, SMS, IRC, etc.
1128    #[must_use]
1129    pub fn service(&self) -> Service {
1130        Service::from(self.service.as_deref())
1131    }
1132
1133    /// Get a message's plist from the [`MESSAGE_PAYLOAD`] BLOB column
1134    ///
1135    /// Calling this hits the database, so it is expensive and should
1136    /// only get invoked when needed.
1137    ///
1138    /// This column contains data used by iMessage app balloons and can be parsed with
1139    /// [`parse_ns_keyed_archiver()`](crate::util::plist::parse_ns_keyed_archiver).
1140    pub fn payload_data(&self, db: &Connection) -> Option<Value> {
1141        Value::from_reader(self.get_blob(db, MESSAGE, MESSAGE_PAYLOAD, self.rowid.into())?).ok()
1142    }
1143
1144    /// Get a message's raw data from the [`MESSAGE_PAYLOAD`] BLOB column
1145    ///
1146    /// Calling this hits the database, so it is expensive and should
1147    /// only get invoked when needed.
1148    ///
1149    /// This column contains data used by [`HandwrittenMessage`](crate::message_types::handwriting::HandwrittenMessage)s.
1150    pub fn raw_payload_data(&self, db: &Connection) -> Option<Vec<u8>> {
1151        let mut buf = Vec::new();
1152        self.get_blob(db, MESSAGE, MESSAGE_PAYLOAD, self.rowid.into())?
1153            .read_to_end(&mut buf)
1154            .ok()?;
1155        Some(buf)
1156    }
1157
1158    /// Get a message's plist from the [`MESSAGE_SUMMARY_INFO`] BLOB column
1159    ///
1160    /// Calling this hits the database, so it is expensive and should
1161    /// only get invoked when needed.
1162    ///
1163    /// This column contains data used by [`edited`](crate::message_types::edited) iMessages.
1164    pub fn message_summary_info(&self, db: &Connection) -> Option<Value> {
1165        Value::from_reader(self.get_blob(db, MESSAGE, MESSAGE_SUMMARY_INFO, self.rowid.into())?)
1166            .ok()
1167    }
1168
1169    /// Get a message's [typedstream](crate::util::typedstream) from the [`ATTRIBUTED_BODY`] BLOB column
1170    ///
1171    /// Calling this hits the database, so it is expensive and should
1172    /// only get invoked when needed.
1173    ///
1174    /// This column contains the message's body text with any other attributes.
1175    pub fn attributed_body(&self, db: &Connection) -> Option<Vec<u8>> {
1176        let mut body = vec![];
1177        self.get_blob(db, MESSAGE, ATTRIBUTED_BODY, self.rowid.into())?
1178            .read_to_end(&mut body)
1179            .ok();
1180        Some(body)
1181    }
1182
1183    /// Determine which [`Expressive`] the message was sent with
1184    #[must_use]
1185    pub fn get_expressive(&self) -> Expressive {
1186        match &self.expressive_send_style_id {
1187            Some(content) => match content.as_str() {
1188                "com.apple.MobileSMS.expressivesend.gentle" => {
1189                    Expressive::Bubble(BubbleEffect::Gentle)
1190                }
1191                "com.apple.MobileSMS.expressivesend.impact" => {
1192                    Expressive::Bubble(BubbleEffect::Slam)
1193                }
1194                "com.apple.MobileSMS.expressivesend.invisibleink" => {
1195                    Expressive::Bubble(BubbleEffect::InvisibleInk)
1196                }
1197                "com.apple.MobileSMS.expressivesend.loud" => Expressive::Bubble(BubbleEffect::Loud),
1198                "com.apple.messages.effect.CKConfettiEffect" => {
1199                    Expressive::Screen(ScreenEffect::Confetti)
1200                }
1201                "com.apple.messages.effect.CKEchoEffect" => Expressive::Screen(ScreenEffect::Echo),
1202                "com.apple.messages.effect.CKFireworksEffect" => {
1203                    Expressive::Screen(ScreenEffect::Fireworks)
1204                }
1205                "com.apple.messages.effect.CKHappyBirthdayEffect" => {
1206                    Expressive::Screen(ScreenEffect::Balloons)
1207                }
1208                "com.apple.messages.effect.CKHeartEffect" => {
1209                    Expressive::Screen(ScreenEffect::Heart)
1210                }
1211                "com.apple.messages.effect.CKLasersEffect" => {
1212                    Expressive::Screen(ScreenEffect::Lasers)
1213                }
1214                "com.apple.messages.effect.CKShootingStarEffect" => {
1215                    Expressive::Screen(ScreenEffect::ShootingStar)
1216                }
1217                "com.apple.messages.effect.CKSparklesEffect" => {
1218                    Expressive::Screen(ScreenEffect::Sparkles)
1219                }
1220                "com.apple.messages.effect.CKSpotlightEffect" => {
1221                    Expressive::Screen(ScreenEffect::Spotlight)
1222                }
1223                _ => Expressive::Unknown(content),
1224            },
1225            None => Expressive::None,
1226        }
1227    }
1228
1229    /// Create a message from a given GUID; useful for debugging
1230    ///
1231    /// # Example
1232    /// ```rust
1233    /// use imessage_database::{
1234    ///     tables::{
1235    ///         messages::Message,
1236    ///         table::get_connection,
1237    ///     },
1238    ///     util::dirs::default_db_path,
1239    /// };
1240    ///
1241    /// let db_path = default_db_path();
1242    /// let conn = get_connection(&db_path).unwrap();
1243    ///
1244    /// if let Ok(mut message) = Message::from_guid("example-guid", &conn) {
1245    ///     let _ = message.generate_text(&conn);
1246    ///     println!("{:#?}", message)
1247    /// }
1248    ///```
1249    pub fn from_guid(guid: &str, db: &Connection) -> Result<Self, TableError> {
1250        // If the database has `chat_recoverable_message_join`, we can restore some deleted messages.
1251        // If database has `thread_originator_guid`, we can parse replies, otherwise default to 0
1252        let filters = format!("WHERE m.guid = \"{guid}\"");
1253
1254        let mut statement = db
1255            .prepare(&ios_16_newer_query(Some(&filters)))
1256            .or_else(|_| db.prepare(&ios_14_15_query(Some(&filters))))
1257            .or_else(|_| db.prepare(&ios_13_older_query(Some(&filters))))?;
1258
1259        Message::extract(statement.query_row([], |row| Ok(Message::from_row(row))))
1260    }
1261}
1262
1263#[cfg(test)]
1264impl Message {
1265    #[must_use]
1266    /// Create a blank test message with default values
1267    pub fn blank() -> Message {
1268        use std::vec;
1269
1270        Message {
1271            rowid: i32::default(),
1272            guid: String::default(),
1273            text: None,
1274            service: Some("iMessage".to_string()),
1275            handle_id: Some(i32::default()),
1276            destination_caller_id: None,
1277            subject: None,
1278            date: i64::default(),
1279            date_read: i64::default(),
1280            date_delivered: i64::default(),
1281            is_from_me: false,
1282            is_read: false,
1283            item_type: 0,
1284            other_handle: None,
1285            share_status: false,
1286            share_direction: None,
1287            group_title: None,
1288            group_action_type: 0,
1289            associated_message_guid: None,
1290            associated_message_type: None,
1291            balloon_bundle_id: None,
1292            expressive_send_style_id: None,
1293            thread_originator_guid: None,
1294            thread_originator_part: None,
1295            date_edited: 0,
1296            associated_message_emoji: None,
1297            chat_id: None,
1298            num_attachments: 0,
1299            deleted_from: None,
1300            num_replies: 0,
1301            components: vec![],
1302            edited_parts: None,
1303        }
1304    }
1305}