Skip to main content

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        // Share direction and other handle are only populated for shared location messages,
763        // so this check is only necessary for those
764        if self.item_type == 4
765            && let (Some(other_handle), Some(share_direction)) =
766                (self.other_handle, self.share_direction)
767        {
768            self.is_from_me || other_handle != 0 && !share_direction
769        } else {
770            self.is_from_me
771        }
772    }
773
774    /// `true` if the message indicates a sender started sharing their location, else `false`
775    #[must_use]
776    pub fn started_sharing_location(&self) -> bool {
777        self.item_type == 4 && self.group_action_type == 0 && !self.share_status
778    }
779
780    /// `true` if the message indicates a sender stopped sharing their location, else `false`
781    #[must_use]
782    pub fn stopped_sharing_location(&self) -> bool {
783        self.item_type == 4 && self.group_action_type == 0 && self.share_status
784    }
785
786    /// `true` if the message was deleted and is recoverable, else `false`
787    ///
788    /// Messages removed by deleting an entire conversation or by deleting a single message
789    /// from a conversation are moved to a separate collection for up to 30 days. Messages
790    /// present in this collection are restored to the conversations they belong to. Apple
791    /// details this process [here](https://support.apple.com/en-us/HT202549#delete).
792    ///
793    /// Messages that have expired from this restoration process are permanently deleted and
794    /// cannot be recovered.
795    ///
796    /// Note: This is not the same as an [`Unsent`](crate::message_types::edited::EditStatus::Unsent) message.
797    #[must_use]
798    pub fn is_deleted(&self) -> bool {
799        self.deleted_from.is_some()
800    }
801
802    /// `true` if the message was translated, else `false`. Only works on iOS 16+ databases.
803    pub fn has_translation(&self, db: &Connection) -> bool {
804        // `7472616E736C6174696F6E4C616E6775616765` -> "translationLanguage"
805        // `7472616E736C6174656454657874` -> "translatedText"
806        let query = format!(
807            "SELECT ROWID FROM {MESSAGE} 
808                WHERE message_summary_info IS NOT NULL 
809                AND length(message_summary_info) > 61 
810                AND instr(message_summary_info, X'7472616E736C6174696F6E4C616E6775616765') > 0 
811                AND instr(message_summary_info, X'7472616E736C6174656454657874') > 0 
812                AND ROWID = ?"
813        );
814        if let Ok(mut statement) = db.prepare_cached(&query) {
815            let result: Result<i32, _> = statement.query_row([self.rowid], |row| row.get(0));
816            result.is_ok()
817        } else {
818            false
819        }
820    }
821
822    /// Generates the [`Translation`] for the current message
823    pub fn get_translation(&self, db: &Connection) -> Result<Option<Translation>, MessageError> {
824        if let Some(payload) = self.message_summary_info(db) {
825            return Ok(Some(Translation::from_payload(&payload)?));
826        }
827        Ok(None)
828    }
829
830    /// Cache all message GUIDs that contain translation data. Only works on iOS 16+ databases.
831    pub fn cache_translations(db: &Connection) -> Result<HashSet<String>, TableError> {
832        // `7472616E736C6174696F6E4C616E6775616765` -> "translationLanguage"
833        // `7472616E736C6174656454657874` -> "translatedText"
834        let query = format!(
835            "SELECT guid FROM {MESSAGE} 
836                WHERE message_summary_info IS NOT NULL 
837                AND length(message_summary_info) > 61 
838                AND instr(message_summary_info, X'7472616E736C6174696F6E4C616E6775616765') > 0 
839                AND instr(message_summary_info, X'7472616E736C6174656454657874') > 0"
840        );
841
842        let mut statement = db.prepare(&query)?;
843        let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
844
845        let mut guids = HashSet::new();
846        for guid_result in rows {
847            guids.insert(guid_result?);
848        }
849
850        Ok(guids)
851    }
852
853    /// Get the group action for the current message
854    #[must_use]
855    pub fn group_action(&'_ self) -> Option<GroupAction<'_>> {
856        GroupAction::from_message(self)
857    }
858
859    /// Get the index of the part of a message a reply is pointing to
860    fn get_reply_index(&self) -> usize {
861        if let Some(parts) = &self.thread_originator_part {
862            return match parts.split(':').next() {
863                Some(part) => str::parse::<usize>(part).unwrap_or(0),
864                None => 0,
865            };
866        }
867        0
868    }
869
870    // MARK: SQL
871    /// Generate the SQL `WHERE` clause described by a [`QueryContext`].
872    ///
873    /// If `include_recoverable` is `true`, the filter includes messages from the recently deleted messages
874    /// table that match the chat IDs. This allows recovery of deleted messages that are still
875    /// present in the database but no longer visible in the Messages app.
876    pub(crate) fn generate_filter_statement(
877        context: &QueryContext,
878        include_recoverable: bool,
879    ) -> String {
880        let mut filters = String::with_capacity(128);
881
882        // Start date filter
883        if let Some(start) = context.start {
884            let _ = write!(filters, " m.date >= {start}");
885        }
886
887        // End date filter
888        if let Some(end) = context.end {
889            if !filters.is_empty() {
890                filters.push_str(" AND ");
891            }
892            let _ = write!(filters, " m.date <= {end}");
893        }
894
895        // Chat ID filter, optionally including recoverable messages
896        if let Some(chat_ids) = &context.selected_chat_ids {
897            if !filters.is_empty() {
898                filters.push_str(" AND ");
899            }
900
901            // Allocate the filter string for interpolation
902            let ids = chat_ids
903                .iter()
904                .map(std::string::ToString::to_string)
905                .collect::<Vec<String>>()
906                .join(", ");
907
908            if include_recoverable {
909                let _ = write!(filters, " (c.chat_id IN ({ids}) OR d.chat_id IN ({ids}))");
910            } else {
911                let _ = write!(filters, " c.chat_id IN ({ids})");
912            }
913        }
914
915        if !filters.is_empty() {
916            return format!("WHERE {filters}");
917        }
918        filters
919    }
920
921    /// Get the number of messages in the database
922    ///
923    /// # Example
924    ///
925    /// ```
926    /// use imessage_database::util::dirs::default_db_path;
927    /// use imessage_database::tables::table::{Diagnostic, get_connection};
928    /// use imessage_database::tables::messages::Message;
929    /// use imessage_database::util::query_context::QueryContext;
930    ///
931    /// let db_path = default_db_path();
932    /// let conn = get_connection(&db_path).unwrap();
933    /// let context = QueryContext::default();
934    /// Message::get_count(&conn, &context);
935    /// ```
936    pub fn get_count(db: &Connection, context: &QueryContext) -> Result<i64, TableError> {
937        let mut statement = if context.has_filters() {
938            db.prepare_cached(&format!(
939                "SELECT
940                     COUNT(*)
941                 FROM {MESSAGE} as m
942                 LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
943                 LEFT JOIN {RECENTLY_DELETED} as d ON m.ROWID = d.message_id
944                 {}",
945                Self::generate_filter_statement(context, true)
946            ))
947            .or_else(|_| {
948                db.prepare_cached(&format!(
949                    "SELECT
950                         COUNT(*)
951                     FROM {MESSAGE} as m
952                     LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
953                    {}",
954                    Self::generate_filter_statement(context, false)
955                ))
956            })?
957        } else {
958            db.prepare_cached(&format!("SELECT COUNT(*) FROM {MESSAGE}"))?
959        };
960        // Execute query, defaulting to zero if it fails
961        let count: i64 = statement.query_row([], |r| r.get(0)).unwrap_or(0);
962
963        Ok(count)
964    }
965
966    /// Stream messages from the database with optional filters.
967    ///
968    /// # Example
969    ///
970    /// ```
971    /// use imessage_database::util::dirs::default_db_path;
972    /// use imessage_database::tables::table::{Diagnostic, get_connection};
973    /// use imessage_database::tables::{messages::Message, table::Table};
974    /// use imessage_database::util::query_context::QueryContext;
975    ///
976    /// let db_path = default_db_path();
977    /// let conn = get_connection(&db_path).unwrap();
978    /// let context = QueryContext::default();
979    ///
980    /// let mut statement = Message::stream_rows(&conn, &context).unwrap();
981    ///
982    /// let messages = statement.query_map([], |row| Ok(Message::from_row(row))).unwrap();
983    ///
984    /// messages.map(|msg| println!("{:#?}", Message::extract(msg)));
985    /// ```
986    pub fn stream_rows<'a>(
987        db: &'a Connection,
988        context: &'a QueryContext,
989    ) -> Result<CachedStatement<'a>, TableError> {
990        if !context.has_filters() {
991            return Self::get(db);
992        }
993        Ok(db
994            .prepare_cached(&ios_16_newer_query(Some(&Self::generate_filter_statement(
995                context, true,
996            ))))
997            .or_else(|_| {
998                db.prepare_cached(&ios_14_15_query(Some(&Self::generate_filter_statement(
999                    context, false,
1000                ))))
1001            })
1002            .or_else(|_| {
1003                db.prepare_cached(&ios_13_older_query(Some(&Self::generate_filter_statement(
1004                    context, false,
1005                ))))
1006            })?)
1007    }
1008
1009    /// Clean and parse the associated message GUID for tapbacks and replies.
1010    ///
1011    /// Returns a tuple of (component index, message GUID) if present.
1012    #[must_use]
1013    pub fn clean_associated_guid(&self) -> Option<(usize, &str)> {
1014        if let Some(guid) = &self.associated_message_guid {
1015            if guid.starts_with("p:") {
1016                let mut split = guid.split('/');
1017                let index_str = split.next()?;
1018                let message_id = split.next()?;
1019                let index = str::parse::<usize>(&index_str.replace("p:", "")).unwrap_or(0);
1020                return Some((index, message_id.get(0..36)?));
1021            } else if guid.starts_with("bp:") {
1022                return Some((0, guid.get(3..39)?));
1023            }
1024
1025            return Some((0, guid.get(0..36)?));
1026        }
1027        None
1028    }
1029
1030    /// Parse the index of a tapback from it's associated GUID field
1031    fn tapback_index(&self) -> usize {
1032        match self.clean_associated_guid() {
1033            Some((x, _)) => x,
1034            None => 0,
1035        }
1036    }
1037
1038    /// Build a `HashMap` of message component index to messages that reply to that component
1039    pub fn get_replies(&self, db: &Connection) -> Result<HashMap<usize, Vec<Self>>, TableError> {
1040        let mut out_h: HashMap<usize, Vec<Self>> = HashMap::new();
1041
1042        // No need to hit the DB if we know we don't have replies
1043        if self.has_replies() {
1044            // Use a parameterized filter so the prepared statement can be cached/reused
1045            let filters = "WHERE m.thread_originator_guid = ?1";
1046
1047            // No iOS 13 and prior used here because `thread_originator_guid` is not present in that schema
1048            let mut statement = db
1049                .prepare_cached(&ios_16_newer_query(Some(filters)))
1050                .or_else(|_| db.prepare_cached(&ios_14_15_query(Some(filters))))?;
1051
1052            let iter =
1053                statement.query_map([self.guid.as_str()], |row| Ok(Message::from_row(row)))?;
1054
1055            for message in iter {
1056                let m = Message::extract(message)?;
1057                let idx = m.get_reply_index();
1058                match out_h.get_mut(&idx) {
1059                    Some(body_part) => body_part.push(m),
1060                    None => {
1061                        out_h.insert(idx, vec![m]);
1062                    }
1063                }
1064            }
1065        }
1066
1067        Ok(out_h)
1068    }
1069
1070    // MARK: Polls
1071    /// Build a `Vec` of messages that vote on the parent poll
1072    pub fn get_votes(&self, db: &Connection) -> Result<Vec<Self>, TableError> {
1073        let mut out_v: Vec<Self> = Vec::new();
1074
1075        // No need to hit the DB if we know we don't have a poll
1076        if self.is_poll() {
1077            // Use a parameterized filter so the prepared statement can be cached/reused
1078            let filters = "WHERE m.associated_message_guid = ?1";
1079
1080            // No iOS 13 and prior used here because `associated_message_guid` is not present in that schema
1081            let mut statement = db
1082                .prepare_cached(&ios_16_newer_query(Some(filters)))
1083                .or_else(|_| db.prepare_cached(&ios_14_15_query(Some(filters))))?;
1084
1085            let iter =
1086                statement.query_map([self.guid.as_str()], |row| Ok(Message::from_row(row)))?;
1087
1088            for message in iter {
1089                let m = Message::extract(message)?;
1090                out_v.push(m);
1091            }
1092        }
1093
1094        Ok(out_v)
1095    }
1096
1097    /// If the message is a poll, attempt to parse and return it
1098    pub fn as_poll(&self, db: &Connection) -> Result<Option<Poll>, MessageError> {
1099        if self.is_poll()
1100            && let Some(payload) = self.payload_data(db)
1101        {
1102            let mut poll = Poll::from_payload(&payload)?;
1103
1104            // Get all votes associated with this poll
1105            let votes = self.get_votes(db).unwrap_or_default();
1106
1107            // Subsequent updates to the poll are stored as messages that reference the original poll message
1108            // so we need to find the latest message in the vector of votes and determine if it is an update
1109            for vote in votes.iter().rev() {
1110                // The most recent non-vote message is the latest poll update
1111                // and contains all of the possible options
1112                if !vote.is_poll_vote()
1113                    && let Some(vote_payload) = vote.payload_data(db)
1114                    && let Ok(update) = Poll::from_payload(&vote_payload)
1115                {
1116                    poll = update;
1117                    break;
1118                }
1119            }
1120
1121            // Count all votes associated with this poll, ignoring any poll update messages in the process
1122            for vote in &votes {
1123                if vote.is_poll_vote()
1124                    && let Some(vote_payload) = vote.payload_data(db)
1125                {
1126                    poll.count_votes(&vote_payload)?;
1127                }
1128            }
1129
1130            // Return the final poll object
1131            return Ok(Some(poll));
1132        }
1133
1134        Ok(None)
1135    }
1136
1137    // MARK: Variant
1138    /// Get the variant of a message, see [`variants`](crate::message_types::variants) for detail.
1139    #[must_use]
1140    pub fn variant(&'_ self) -> Variant<'_> {
1141        // Check if a message was edited first as those have special properties
1142        if self.is_edited() {
1143            return Variant::Edited;
1144        }
1145
1146        // Handle different types of associated message types
1147        if let Some(associated_message_type) = self.associated_message_type {
1148            match associated_message_type {
1149                // Standard iMessages with either text or a message payload
1150                0 | 2 | 3 => return self.get_app_variant().unwrap_or(Variant::Normal),
1151                // Tapbacks (added or removed)
1152                1000 | 2000..=2007 | 3000..=3007 => {
1153                    if let Some((action, tapback)) = self.get_tapback() {
1154                        return Variant::Tapback(self.tapback_index(), action, tapback);
1155                    }
1156                }
1157                // A vote was cast on a poll
1158                4000 => return Variant::Vote,
1159                // Unknown
1160                x => return Variant::Unknown(x),
1161            }
1162        }
1163
1164        // Any other rarer cases belong here
1165        if self.is_shareplay() {
1166            return Variant::SharePlay;
1167        }
1168
1169        Variant::Normal
1170    }
1171
1172    /// Helper to determine app variants based on balloon bundle ID.
1173    #[must_use]
1174    fn get_app_variant(&self) -> Option<Variant<'_>> {
1175        let bundle_id = parse_balloon_bundle_id(self.balloon_bundle_id.as_deref())?;
1176        let custom = match bundle_id {
1177            "com.apple.messages.URLBalloonProvider" => CustomBalloon::URL,
1178            "com.apple.Handwriting.HandwritingProvider" => CustomBalloon::Handwriting,
1179            "com.apple.DigitalTouchBalloonProvider" => CustomBalloon::DigitalTouch,
1180            "com.apple.PassbookUIService.PeerPaymentMessagesExtension" => CustomBalloon::ApplePay,
1181            "com.apple.ActivityMessagesApp.MessagesExtension" => CustomBalloon::Fitness,
1182            "com.apple.mobileslideshow.PhotosMessagesApp" => CustomBalloon::Slideshow,
1183            "com.apple.SafetyMonitorApp.SafetyMonitorMessages" => CustomBalloon::CheckIn,
1184            "com.apple.findmy.FindMyMessagesApp" => CustomBalloon::FindMy,
1185            "com.apple.messages.Polls" => {
1186                // Special case: Check if this is the original poll or an update
1187                if self.associated_message_guid.as_ref() == Some(&self.guid) {
1188                    CustomBalloon::Polls
1189                } else {
1190                    return Some(Variant::PollUpdate);
1191                }
1192            }
1193            _ => CustomBalloon::Application(bundle_id),
1194        };
1195        Some(Variant::App(custom))
1196    }
1197
1198    /// Helper to determine tapback variants based on associated message type.
1199    #[must_use]
1200    fn get_tapback(&self) -> Option<(TapbackAction, Tapback<'_>)> {
1201        match self.associated_message_type? {
1202            1000 => Some((TapbackAction::Added, Tapback::Sticker)),
1203            2000 => Some((TapbackAction::Added, Tapback::Loved)),
1204            2001 => Some((TapbackAction::Added, Tapback::Liked)),
1205            2002 => Some((TapbackAction::Added, Tapback::Disliked)),
1206            2003 => Some((TapbackAction::Added, Tapback::Laughed)),
1207            2004 => Some((TapbackAction::Added, Tapback::Emphasized)),
1208            2005 => Some((TapbackAction::Added, Tapback::Questioned)),
1209            2006 => Some((
1210                TapbackAction::Added,
1211                Tapback::Emoji(self.associated_message_emoji.as_deref()),
1212            )),
1213            2007 => Some((TapbackAction::Added, Tapback::Sticker)),
1214            3000 => Some((TapbackAction::Removed, Tapback::Loved)),
1215            3001 => Some((TapbackAction::Removed, Tapback::Liked)),
1216            3002 => Some((TapbackAction::Removed, Tapback::Disliked)),
1217            3003 => Some((TapbackAction::Removed, Tapback::Laughed)),
1218            3004 => Some((TapbackAction::Removed, Tapback::Emphasized)),
1219            3005 => Some((TapbackAction::Removed, Tapback::Questioned)),
1220            3006 => Some((
1221                TapbackAction::Removed,
1222                Tapback::Emoji(self.associated_message_emoji.as_deref()),
1223            )),
1224            3007 => Some((TapbackAction::Removed, Tapback::Sticker)),
1225            _ => None,
1226        }
1227    }
1228
1229    /// Determine the type of announcement a message contains, if it contains one
1230    #[must_use]
1231    pub fn get_announcement(&'_ self) -> Option<Announcement<'_>> {
1232        if let Some(action) = self.group_action() {
1233            return Some(Announcement::GroupAction(action));
1234        }
1235
1236        if self.is_fully_unsent() {
1237            return Some(Announcement::FullyUnsent);
1238        }
1239
1240        if self.is_kept_audio_message() {
1241            return Some(Announcement::AudioMessageKept);
1242        }
1243
1244        None
1245    }
1246
1247    /// Determine the service the message was sent from, i.e. iMessage, SMS, IRC, etc.
1248    #[must_use]
1249    pub fn service(&'_ self) -> Service<'_> {
1250        Service::from(self.service.as_deref())
1251    }
1252
1253    // MARK: BLOBs
1254    /// Get a message's plist from the [`MESSAGE_PAYLOAD`] BLOB column
1255    ///
1256    /// Calling this hits the database, so it is expensive and should
1257    /// only get invoked when needed.
1258    ///
1259    /// This column contains data used by iMessage app balloons and can be parsed with
1260    /// [`parse_ns_keyed_archiver()`](crate::util::plist::parse_ns_keyed_archiver).
1261    pub fn payload_data(&self, db: &Connection) -> Option<Value> {
1262        Value::from_reader(self.get_blob(db, MESSAGE, MESSAGE_PAYLOAD, self.rowid.into())?).ok()
1263    }
1264
1265    /// Get a message's raw data from the [`MESSAGE_PAYLOAD`] BLOB column
1266    ///
1267    /// Calling this hits the database, so it is expensive and should
1268    /// only get invoked when needed.
1269    ///
1270    /// This column contains data used by [`HandwrittenMessage`](crate::message_types::handwriting::HandwrittenMessage)s.
1271    pub fn raw_payload_data(&self, db: &Connection) -> Option<Vec<u8>> {
1272        let mut buf = Vec::new();
1273        self.get_blob(db, MESSAGE, MESSAGE_PAYLOAD, self.rowid.into())?
1274            .read_to_end(&mut buf)
1275            .ok()?;
1276        Some(buf)
1277    }
1278
1279    /// Get a message's plist from the [`MESSAGE_SUMMARY_INFO`] BLOB column
1280    ///
1281    /// Calling this hits the database, so it is expensive and should
1282    /// only get invoked when needed.
1283    ///
1284    /// This column contains data used by [`edited`](crate::message_types::edited) iMessages.
1285    pub fn message_summary_info(&self, db: &Connection) -> Option<Value> {
1286        Value::from_reader(self.get_blob(db, MESSAGE, MESSAGE_SUMMARY_INFO, self.rowid.into())?)
1287            .ok()
1288    }
1289
1290    /// Get a message's [typedstream](crate::util::typedstream) from the [`ATTRIBUTED_BODY`] BLOB column
1291    ///
1292    /// Calling this hits the database, so it is expensive and should
1293    /// only get invoked when needed.
1294    ///
1295    /// This column contains the message's body text with any other attributes.
1296    pub fn attributed_body(&self, db: &Connection) -> Option<Vec<u8>> {
1297        let mut body = vec![];
1298        self.get_blob(db, MESSAGE, ATTRIBUTED_BODY, self.rowid.into())?
1299            .read_to_end(&mut body)
1300            .ok();
1301        Some(body)
1302    }
1303
1304    // MARK: Expressive
1305    /// Determine which [`Expressive`] the message was sent with
1306    #[must_use]
1307    pub fn get_expressive(&'_ self) -> Expressive<'_> {
1308        match &self.expressive_send_style_id {
1309            Some(content) => match content.as_str() {
1310                "com.apple.MobileSMS.expressivesend.gentle" => {
1311                    Expressive::Bubble(BubbleEffect::Gentle)
1312                }
1313                "com.apple.MobileSMS.expressivesend.impact" => {
1314                    Expressive::Bubble(BubbleEffect::Slam)
1315                }
1316                "com.apple.MobileSMS.expressivesend.invisibleink" => {
1317                    Expressive::Bubble(BubbleEffect::InvisibleInk)
1318                }
1319                "com.apple.MobileSMS.expressivesend.loud" => Expressive::Bubble(BubbleEffect::Loud),
1320                "com.apple.messages.effect.CKConfettiEffect" => {
1321                    Expressive::Screen(ScreenEffect::Confetti)
1322                }
1323                "com.apple.messages.effect.CKEchoEffect" => Expressive::Screen(ScreenEffect::Echo),
1324                "com.apple.messages.effect.CKFireworksEffect" => {
1325                    Expressive::Screen(ScreenEffect::Fireworks)
1326                }
1327                "com.apple.messages.effect.CKHappyBirthdayEffect" => {
1328                    Expressive::Screen(ScreenEffect::Balloons)
1329                }
1330                "com.apple.messages.effect.CKHeartEffect" => {
1331                    Expressive::Screen(ScreenEffect::Heart)
1332                }
1333                "com.apple.messages.effect.CKLasersEffect" => {
1334                    Expressive::Screen(ScreenEffect::Lasers)
1335                }
1336                "com.apple.messages.effect.CKShootingStarEffect" => {
1337                    Expressive::Screen(ScreenEffect::ShootingStar)
1338                }
1339                "com.apple.messages.effect.CKSparklesEffect" => {
1340                    Expressive::Screen(ScreenEffect::Sparkles)
1341                }
1342                "com.apple.messages.effect.CKSpotlightEffect" => {
1343                    Expressive::Screen(ScreenEffect::Spotlight)
1344                }
1345                _ => Expressive::Unknown(content),
1346            },
1347            None => Expressive::None,
1348        }
1349    }
1350
1351    /// Create a message from a given GUID; useful for debugging
1352    ///
1353    /// # Example
1354    /// ```rust
1355    /// use imessage_database::{
1356    ///     tables::{
1357    ///         messages::Message,
1358    ///         table::get_connection,
1359    ///     },
1360    ///     util::dirs::default_db_path,
1361    /// };
1362    ///
1363    /// let db_path = default_db_path();
1364    /// let conn = get_connection(&db_path).unwrap();
1365    ///
1366    /// if let Ok(mut message) = Message::from_guid("example-guid", &conn) {
1367    ///     let _ = message.generate_text(&conn);
1368    ///     println!("{:#?}", message)
1369    /// }
1370    ///```
1371    pub fn from_guid(guid: &str, db: &Connection) -> Result<Self, TableError> {
1372        // If the database has `chat_recoverable_message_join`, we can restore some deleted messages.
1373        // If database has `thread_originator_guid`, we can parse replies, otherwise default to 0
1374        let filters = format!("WHERE m.guid = \"{guid}\"");
1375
1376        let mut statement = db
1377            .prepare(&ios_16_newer_query(Some(&filters)))
1378            .or_else(|_| db.prepare(&ios_14_15_query(Some(&filters))))
1379            .or_else(|_| db.prepare(&ios_13_older_query(Some(&filters))))?;
1380
1381        Message::extract(statement.query_row([], |row| Ok(Message::from_row(row))))
1382    }
1383}
1384
1385// MARK: Fixture
1386#[cfg(test)]
1387impl Message {
1388    #[must_use]
1389    /// Create a blank test message with default values
1390    pub fn blank() -> Message {
1391        use std::vec;
1392
1393        Message {
1394            rowid: i32::default(),
1395            guid: String::default(),
1396            text: None,
1397            service: Some("iMessage".to_string()),
1398            handle_id: Some(i32::default()),
1399            destination_caller_id: None,
1400            subject: None,
1401            date: i64::default(),
1402            date_read: i64::default(),
1403            date_delivered: i64::default(),
1404            is_from_me: false,
1405            is_read: false,
1406            item_type: 0,
1407            other_handle: None,
1408            share_status: false,
1409            share_direction: None,
1410            group_title: None,
1411            group_action_type: 0,
1412            associated_message_guid: None,
1413            associated_message_type: None,
1414            balloon_bundle_id: None,
1415            expressive_send_style_id: None,
1416            thread_originator_guid: None,
1417            thread_originator_part: None,
1418            date_edited: 0,
1419            associated_message_emoji: None,
1420            chat_id: None,
1421            num_attachments: 0,
1422            deleted_from: None,
1423            num_replies: 0,
1424            components: vec![],
1425            edited_parts: None,
1426        }
1427    }
1428}