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