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