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::DatabaseName::Main,
439 MESSAGE,
440 column,
441 self.rowid as i64,
442 true,
443 ).ok()
444 }
445}
446
447impl AttributedBody for Message {
448 /// 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.
449 ///
450 /// For more detail see the trait documentation [here](crate::tables::table::AttributedBody).
451 fn body(&self) -> Vec<BubbleComponent> {
452 // If the message is an app, it will be rendered differently, so just escape there
453 if self.balloon_bundle_id.is_some() {
454 return vec![BubbleComponent::App];
455 }
456
457 if let Some(body) = parse_body_typedstream(
458 self.components.as_ref(),
459 self.text.as_deref(),
460 self.edited_parts.as_ref(),
461 ) {
462 return body;
463 }
464
465 // Naive logic for when `typedstream` component parsing fails
466 parse_body_legacy(&self.text)
467 }
468}
469
470impl Message {
471 /// Generate the text of a message, deserializing it as [`typedstream`](crate::util::typedstream) (and falling back to [`streamtyped`]) data if necessary.
472 pub fn generate_text<'a>(&'a mut self, db: &'a Connection) -> Result<&'a str, MessageError> {
473 // Grab the body data from the table
474 if let Some(body) = self.attributed_body(db) {
475 // Attempt to deserialize the typedstream data
476 let mut typedstream = TypedStreamReader::from(&body);
477 self.components = typedstream.parse().ok();
478
479 // If we deserialize the typedstream, use that data
480 self.text = self
481 .components
482 .as_ref()
483 .and_then(|items| items.first())
484 .and_then(|item| item.as_nsstring())
485 .map(String::from);
486
487 // If the above parsing failed, fall back to the legacy parser instead
488 if self.text.is_none() {
489 self.text =
490 Some(streamtyped::parse(body).map_err(MessageError::StreamTypedParseError)?);
491 }
492 }
493
494 // Generate the edited message data
495 self.edited_parts = self
496 .is_edited()
497 .then(|| self.message_summary_info(db))
498 .flatten()
499 .as_ref()
500 .and_then(|payload| EditedMessage::from_map(payload).ok());
501
502 if let Some(t) = &self.text {
503 Ok(t)
504 } else {
505 Err(MessageError::NoText)
506 }
507 }
508
509 /// Calculates the date a message was written to the database.
510 ///
511 /// This field is stored as a unix timestamp with an epoch of `2001-01-01 00:00:00` in the local time zone
512 ///
513 /// `offset` can be provided by [`get_offset`](crate::util::dates::get_offset) or manually.
514 pub fn date(&self, offset: &i64) -> Result<DateTime<Local>, MessageError> {
515 get_local_time(&self.date, offset)
516 }
517
518 /// Calculates the date a message was marked as delivered.
519 ///
520 /// This field is stored as a unix timestamp with an epoch of `2001-01-01 00:00:00` in the local time zone
521 ///
522 /// `offset` can be provided by [`get_offset`](crate::util::dates::get_offset) or manually.
523 pub fn date_delivered(&self, offset: &i64) -> Result<DateTime<Local>, MessageError> {
524 get_local_time(&self.date_delivered, offset)
525 }
526
527 /// Calculates the date a message was marked as read.
528 ///
529 /// This field is stored as a unix timestamp with an epoch of `2001-01-01 00:00:00` in the local time zone
530 ///
531 /// `offset` can be provided by [`get_offset`](crate::util::dates::get_offset) or manually.
532 pub fn date_read(&self, offset: &i64) -> Result<DateTime<Local>, MessageError> {
533 get_local_time(&self.date_read, offset)
534 }
535
536 /// Calculates the date a message was most recently edited.
537 ///
538 /// This field is stored as a unix timestamp with an epoch of `2001-01-01 00:00:00` in the local time zone
539 ///
540 /// `offset` can be provided by [`get_offset`](crate::util::dates::get_offset) or manually.
541 pub fn date_edited(&self, offset: &i64) -> Result<DateTime<Local>, MessageError> {
542 get_local_time(&self.date_edited, offset)
543 }
544
545 /// Gets the time until the message was read. This can happen in two ways:
546 ///
547 /// - You received a message, then waited to read it
548 /// - You sent a message, and the recipient waited to read it
549 ///
550 /// In the former case, this subtracts the date read column (`date_read`) from the date received column (`date`).
551 /// In the latter case, this subtracts the date delivered column (`date_delivered`) from the date received column (`date`).
552 ///
553 /// Not all messages get tagged with the read properties.
554 /// If more than one message has been sent in a thread before getting read,
555 /// only the most recent message will get the tag.
556 ///
557 /// `offset` can be provided by [`get_offset`](crate::util::dates::get_offset) or manually.
558 pub fn time_until_read(&self, offset: &i64) -> Option<String> {
559 // Message we received
560 if !self.is_from_me && self.date_read != 0 && self.date != 0 {
561 return readable_diff(self.date(offset), self.date_read(offset));
562 }
563 // Message we sent
564 else if self.is_from_me && self.date_delivered != 0 && self.date != 0 {
565 return readable_diff(self.date(offset), self.date_delivered(offset));
566 }
567 None
568 }
569
570 /// `true` if the message is a response to a thread, else `false`
571 pub fn is_reply(&self) -> bool {
572 self.thread_originator_guid.is_some()
573 }
574
575 /// `true` if the message is an [`Announcement`], else `false`
576 pub fn is_announcement(&self) -> bool {
577 self.get_announcement().is_some()
578 }
579
580 /// `true` if the message is a [`Tapback`] to another message, else `false`
581 pub fn is_tapback(&self) -> bool {
582 matches!(self.variant(), Variant::Tapback(..))
583 }
584
585 /// `true` if the message has an [`Expressive`], else `false`
586 pub fn is_expressive(&self) -> bool {
587 self.expressive_send_style_id.is_some()
588 }
589
590 /// `true` if the message has a [URL preview](crate::message_types::url), else `false`
591 pub fn is_url(&self) -> bool {
592 matches!(self.variant(), Variant::App(CustomBalloon::URL))
593 }
594
595 /// `true` if the message is a [`HandwrittenMessage`](crate::message_types::handwriting::models::HandwrittenMessage), else `false`
596 pub fn is_handwriting(&self) -> bool {
597 matches!(self.variant(), Variant::App(CustomBalloon::Handwriting))
598 }
599
600 /// `true` if the message is a [`Digital Touch`](crate::message_types::digital_touch::models), else `false`
601 pub fn is_digital_touch(&self) -> bool {
602 matches!(self.variant(), Variant::App(CustomBalloon::DigitalTouch))
603 }
604
605 /// `true` if the message was [`Edited`](crate::message_types::edited), else `false`
606 pub fn is_edited(&self) -> bool {
607 self.date_edited != 0
608 }
609
610 /// `true` if the specified message component was [edited](crate::message_types::edited::EditStatus::Edited), else `false`
611 pub fn is_part_edited(&self, index: usize) -> bool {
612 if let Some(edited_parts) = &self.edited_parts {
613 if let Some(part) = edited_parts.part(index) {
614 return matches!(part.status, EditStatus::Edited);
615 }
616 }
617 false
618 }
619
620 /// `true` if all message components were [unsent](crate::message_types::edited::EditStatus::Unsent), else `false`
621 pub fn is_fully_unsent(&self) -> bool {
622 self.edited_parts.as_ref().is_some_and(|ep| {
623 ep.parts
624 .iter()
625 .all(|part| matches!(part.status, EditStatus::Unsent))
626 })
627 }
628
629 /// `true` if the message contains [`Attachment`](crate::tables::attachment::Attachment)s, else `false`
630 ///
631 /// Attachments can be queried with [`Attachment::from_message()`](crate::tables::attachment::Attachment::from_message).
632 pub fn has_attachments(&self) -> bool {
633 self.num_attachments > 0
634 }
635
636 /// `true` if the message begins a thread, else `false`
637 pub fn has_replies(&self) -> bool {
638 self.num_replies > 0
639 }
640
641 /// `true` if the message is a [SharePlay/FaceTime](crate::message_types::variants::Variant::SharePlay) message, else `false`
642 pub fn is_shareplay(&self) -> bool {
643 self.item_type == 6
644 }
645
646 /// `true` if the message was sent by the database owner, else `false`
647 pub fn is_from_me(&self) -> bool {
648 if let (Some(other_handle), Some(share_direction)) =
649 (self.other_handle, self.share_direction)
650 {
651 self.is_from_me || other_handle != 0 && !share_direction
652 } else {
653 self.is_from_me
654 }
655 }
656
657 /// Get the group action for the current message
658 pub fn group_action(&self) -> Option<GroupAction> {
659 GroupAction::from_message(self)
660 }
661
662 /// `true` if the message indicates a sender started sharing their location, else `false`
663 pub fn started_sharing_location(&self) -> bool {
664 self.item_type == 4 && self.group_action_type == 0 && !self.share_status
665 }
666
667 /// `true` if the message indicates a sender stopped sharing their location, else `false`
668 pub fn stopped_sharing_location(&self) -> bool {
669 self.item_type == 4 && self.group_action_type == 0 && self.share_status
670 }
671
672 /// `true` if the message was deleted and is recoverable, else `false`
673 ///
674 /// Messages removed by deleting an entire conversation or by deleting a single message
675 /// from a conversation are moved to a separate collection for up to 30 days. Messages
676 /// present in this collection are restored to the conversations they belong to. Apple
677 /// details this process [here](https://support.apple.com/en-us/HT202549#delete).
678 ///
679 /// Messages that have expired from this restoration process are permanently deleted and
680 /// cannot be recovered.
681 ///
682 /// Note: This is not the same as an [`Unsent`](crate::message_types::edited::EditStatus::Unsent) message.
683 pub fn is_deleted(&self) -> bool {
684 self.deleted_from.is_some()
685 }
686
687 /// Get the index of the part of a message a reply is pointing to
688 fn get_reply_index(&self) -> usize {
689 if let Some(parts) = &self.thread_originator_part {
690 return match parts.split(':').next() {
691 Some(part) => str::parse::<usize>(part).unwrap_or(0),
692 None => 0,
693 };
694 }
695 0
696 }
697
698 /// Generate the SQL `WHERE` clause described by a [`QueryContext`].
699 ///
700 /// If `include_recoverable` is `true`, the filter includes messages from the recently deleted messages
701 /// table that match the chat IDs. This allows recovery of deleted messages that are still
702 /// present in the database but no longer visible in the Messages app.
703 pub(crate) fn generate_filter_statement(
704 context: &QueryContext,
705 include_recoverable: bool,
706 ) -> String {
707 let mut filters = String::new();
708
709 // Start date filter
710 if let Some(start) = context.start {
711 filters.push_str(&format!(" m.date >= {start}"));
712 }
713
714 // End date filter
715 if let Some(end) = context.end {
716 if !filters.is_empty() {
717 filters.push_str(" AND ");
718 }
719 filters.push_str(&format!(" m.date <= {end}"));
720 }
721
722 // Chat ID filter, optionally including recoverable messages
723 if let Some(chat_ids) = &context.selected_chat_ids {
724 if !filters.is_empty() {
725 filters.push_str(" AND ");
726 }
727
728 // Allocate the filter string for interpolation
729 let ids = chat_ids
730 .iter()
731 .map(|x| x.to_string())
732 .collect::<Vec<String>>()
733 .join(", ");
734
735 if include_recoverable {
736 filters.push_str(&format!(" (c.chat_id IN ({ids}) OR d.chat_id IN ({ids}))"));
737 } else {
738 filters.push_str(&format!(" c.chat_id IN ({ids})"));
739 }
740 }
741
742 if !filters.is_empty() {
743 return format!("WHERE {filters}");
744 }
745 filters
746 }
747
748 /// Get the number of messages in the database
749 ///
750 /// # Example:
751 ///
752 /// ```
753 /// use imessage_database::util::dirs::default_db_path;
754 /// use imessage_database::tables::table::{Diagnostic, get_connection};
755 /// use imessage_database::tables::messages::Message;
756 /// use imessage_database::util::query_context::QueryContext;
757 ///
758 /// let db_path = default_db_path();
759 /// let conn = get_connection(&db_path).unwrap();
760 /// let context = QueryContext::default();
761 /// Message::get_count(&conn, &context);
762 /// ```
763 pub fn get_count(db: &Connection, context: &QueryContext) -> Result<u64, TableError> {
764 let mut statement = if context.has_filters() {
765 db.prepare(&format!(
766 "SELECT
767 COUNT(*)
768 FROM {MESSAGE} as m
769 LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
770 LEFT JOIN {RECENTLY_DELETED} as d ON m.ROWID = d.message_id
771 {}",
772 Self::generate_filter_statement(context, true)
773 ))
774 .or_else(|_| {
775 db.prepare(&format!(
776 "SELECT
777 COUNT(*)
778 FROM {MESSAGE} as m
779 LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
780 {}",
781 Self::generate_filter_statement(context, false)
782 ))
783 })
784 .map_err(TableError::Messages)?
785 } else {
786 db.prepare(&format!("SELECT COUNT(*) FROM {MESSAGE}"))
787 .map_err(TableError::Messages)?
788 };
789 // Execute query, defaulting to zero if it fails
790 let count: u64 = statement.query_row([], |r| r.get(0)).unwrap_or(0);
791
792 Ok(count)
793 }
794
795 /// Stream messages from the database with optional filters.
796 ///
797 /// # Example:
798 ///
799 /// ```
800 /// use imessage_database::util::dirs::default_db_path;
801 /// use imessage_database::tables::table::{Diagnostic, get_connection};
802 /// use imessage_database::tables::{messages::Message, table::Table};
803 /// use imessage_database::util::query_context::QueryContext;
804 ///
805 /// let db_path = default_db_path();
806 /// let conn = get_connection(&db_path).unwrap();
807 /// let context = QueryContext::default();
808 ///
809 /// let mut statement = Message::stream_rows(&conn, &context).unwrap();
810 ///
811 /// let messages = statement.query_map([], |row| Ok(Message::from_row(row))).unwrap();
812 ///
813 /// messages.map(|msg| println!("{:#?}", Message::extract(msg)));
814 /// ```
815 pub fn stream_rows<'a>(
816 db: &'a Connection,
817 context: &'a QueryContext,
818 ) -> Result<Statement<'a>, TableError> {
819 if !context.has_filters() {
820 return Self::get(db);
821 }
822 db.prepare(&ios_16_newer_query(Some(&Self::generate_filter_statement(
823 context, true,
824 ))))
825 .or_else(|_| {
826 db.prepare(&ios_14_15_query(Some(&Self::generate_filter_statement(
827 context, false,
828 ))))
829 })
830 .or_else(|_| {
831 db.prepare(&ios_13_older_query(Some(&Self::generate_filter_statement(
832 context, false,
833 ))))
834 })
835 .map_err(TableError::Messages)
836 }
837
838 /// See [`Tapback`] for details on this data.
839 pub fn clean_associated_guid(&self) -> Option<(usize, &str)> {
840 if let Some(guid) = &self.associated_message_guid {
841 if guid.starts_with("p:") {
842 let mut split = guid.split('/');
843 let index_str = split.next()?;
844 let message_id = split.next()?;
845 let index = str::parse::<usize>(&index_str.replace("p:", "")).unwrap_or(0);
846 return Some((index, message_id.get(0..36)?));
847 } else if guid.starts_with("bp:") {
848 return Some((0, guid.get(3..39)?));
849 }
850
851 return Some((0, guid.get(0..36)?));
852 }
853 None
854 }
855
856 /// Parse the index of a tapback from it's associated GUID field
857 fn tapback_index(&self) -> usize {
858 match self.clean_associated_guid() {
859 Some((x, _)) => x,
860 None => 0,
861 }
862 }
863
864 /// Build a `HashMap` of message component index to messages that reply to that component
865 pub fn get_replies(&self, db: &Connection) -> Result<HashMap<usize, Vec<Self>>, TableError> {
866 let mut out_h: HashMap<usize, Vec<Self>> = HashMap::new();
867
868 // No need to hit the DB if we know we don't have replies
869 if self.has_replies() {
870 let filters = format!("WHERE m.thread_originator_guid = \"{}\"", self.guid);
871
872 // No iOS 13 and prior used here because `thread_originator_guid` is not present in that schema
873 let mut statement = db
874 .prepare(&ios_16_newer_query(Some(&filters)))
875 .or_else(|_| db.prepare(&ios_14_15_query(Some(&filters))))
876 .map_err(TableError::Messages)?;
877
878 let iter = statement
879 .query_map([], |row| Ok(Message::from_row(row)))
880 .map_err(TableError::Messages)?;
881
882 for message in iter {
883 let m = Message::extract(message)?;
884 let idx = m.get_reply_index();
885 match out_h.get_mut(&idx) {
886 Some(body_part) => body_part.push(m),
887 None => {
888 out_h.insert(idx, vec![m]);
889 }
890 }
891 }
892 }
893
894 Ok(out_h)
895 }
896
897 /// Get the variant of a message, see [`variants`](crate::message_types::variants) for detail.
898 pub fn variant(&self) -> Variant {
899 // Check if a message was edited first as those have special properties
900 if self.is_edited() {
901 return Variant::Edited;
902 }
903
904 // Handle different types of bundle IDs next, as those are most common
905 if let Some(associated_message_type) = self.associated_message_type {
906 return match associated_message_type {
907 // Standard iMessages with either text or a message payload
908 0 | 2 | 3 => match parse_balloon_bundle_id(self.balloon_bundle_id.as_deref()) {
909 Some(bundle_id) => match bundle_id {
910 "com.apple.messages.URLBalloonProvider" => Variant::App(CustomBalloon::URL),
911 "com.apple.Handwriting.HandwritingProvider" => {
912 Variant::App(CustomBalloon::Handwriting)
913 }
914 "com.apple.DigitalTouchBalloonProvider" => {
915 Variant::App(CustomBalloon::DigitalTouch)
916 }
917 "com.apple.PassbookUIService.PeerPaymentMessagesExtension" => {
918 Variant::App(CustomBalloon::ApplePay)
919 }
920 "com.apple.ActivityMessagesApp.MessagesExtension" => {
921 Variant::App(CustomBalloon::Fitness)
922 }
923 "com.apple.mobileslideshow.PhotosMessagesApp" => {
924 Variant::App(CustomBalloon::Slideshow)
925 }
926 "com.apple.SafetyMonitorApp.SafetyMonitorMessages" => {
927 Variant::App(CustomBalloon::CheckIn)
928 }
929 "com.apple.findmy.FindMyMessagesApp" => Variant::App(CustomBalloon::FindMy),
930 _ => Variant::App(CustomBalloon::Application(bundle_id)),
931 },
932 // This is the most common case
933 None => Variant::Normal,
934 },
935
936 // Stickers overlaid on messages
937 1000 => {
938 Variant::Tapback(self.tapback_index(), TapbackAction::Added, Tapback::Sticker)
939 }
940
941 // Tapbacks
942 2000 => {
943 Variant::Tapback(self.tapback_index(), TapbackAction::Added, Tapback::Loved)
944 }
945 2001 => {
946 Variant::Tapback(self.tapback_index(), TapbackAction::Added, Tapback::Liked)
947 }
948 2002 => Variant::Tapback(
949 self.tapback_index(),
950 TapbackAction::Added,
951 Tapback::Disliked,
952 ),
953 2003 => {
954 Variant::Tapback(self.tapback_index(), TapbackAction::Added, Tapback::Laughed)
955 }
956 2004 => Variant::Tapback(
957 self.tapback_index(),
958 TapbackAction::Added,
959 Tapback::Emphasized,
960 ),
961 2005 => Variant::Tapback(
962 self.tapback_index(),
963 TapbackAction::Added,
964 Tapback::Questioned,
965 ),
966 2006 => Variant::Tapback(
967 self.tapback_index(),
968 TapbackAction::Added,
969 Tapback::Emoji(self.associated_message_emoji.as_deref()),
970 ),
971 2007 => {
972 Variant::Tapback(self.tapback_index(), TapbackAction::Added, Tapback::Sticker)
973 }
974 3000 => {
975 Variant::Tapback(self.tapback_index(), TapbackAction::Removed, Tapback::Loved)
976 }
977 3001 => {
978 Variant::Tapback(self.tapback_index(), TapbackAction::Removed, Tapback::Liked)
979 }
980 3002 => Variant::Tapback(
981 self.tapback_index(),
982 TapbackAction::Removed,
983 Tapback::Disliked,
984 ),
985 3003 => Variant::Tapback(
986 self.tapback_index(),
987 TapbackAction::Removed,
988 Tapback::Laughed,
989 ),
990 3004 => Variant::Tapback(
991 self.tapback_index(),
992 TapbackAction::Removed,
993 Tapback::Emphasized,
994 ),
995 3005 => Variant::Tapback(
996 self.tapback_index(),
997 TapbackAction::Removed,
998 Tapback::Questioned,
999 ),
1000 3006 => Variant::Tapback(
1001 self.tapback_index(),
1002 TapbackAction::Removed,
1003 Tapback::Emoji(self.associated_message_emoji.as_deref()),
1004 ),
1005 3007 => Variant::Tapback(
1006 self.tapback_index(),
1007 TapbackAction::Removed,
1008 Tapback::Sticker,
1009 ),
1010
1011 // Unknown
1012 x => Variant::Unknown(x),
1013 };
1014 }
1015
1016 // Any other rarer cases belong here
1017 if self.is_shareplay() {
1018 return Variant::SharePlay;
1019 }
1020
1021 Variant::Normal
1022 }
1023
1024 /// Determine the type of announcement a message contains, if it contains one
1025 pub fn get_announcement(&self) -> Option<Announcement> {
1026 if let Some(action) = self.group_action() {
1027 return Some(Announcement::GroupAction(action));
1028 }
1029
1030 if self.is_fully_unsent() {
1031 return Some(Announcement::FullyUnsent);
1032 }
1033
1034 None
1035 }
1036
1037 /// Determine the service the message was sent from, i.e. iMessage, SMS, IRC, etc.
1038 pub fn service(&self) -> Service {
1039 Service::from(self.service.as_deref())
1040 }
1041
1042 /// Get a message's plist from the [`MESSAGE_PAYLOAD`] BLOB column
1043 ///
1044 /// Calling this hits the database, so it is expensive and should
1045 /// only get invoked when needed.
1046 ///
1047 /// This column contains data used by iMessage app balloons and can be parsed with
1048 /// [`parse_ns_keyed_archiver()`](crate::util::plist::parse_ns_keyed_archiver).
1049 pub fn payload_data(&self, db: &Connection) -> Option<Value> {
1050 Value::from_reader(self.get_blob(db, MESSAGE_PAYLOAD)?).ok()
1051 }
1052
1053 /// Get a message's raw data from the [`MESSAGE_PAYLOAD`] BLOB column
1054 ///
1055 /// Calling this hits the database, so it is expensive and should
1056 /// only get invoked when needed.
1057 ///
1058 /// This column contains data used by [`HandwrittenMessage`](crate::message_types::handwriting::HandwrittenMessage)s.
1059 pub fn raw_payload_data(&self, db: &Connection) -> Option<Vec<u8>> {
1060 let mut buf = Vec::new();
1061 self.get_blob(db, MESSAGE_PAYLOAD)?
1062 .read_to_end(&mut buf)
1063 .ok()?;
1064 Some(buf)
1065 }
1066
1067 /// Get a message's plist from the [`MESSAGE_SUMMARY_INFO`] BLOB column
1068 ///
1069 /// Calling this hits the database, so it is expensive and should
1070 /// only get invoked when needed.
1071 ///
1072 /// This column contains data used by [`edited`](crate::message_types::edited) iMessages.
1073 pub fn message_summary_info(&self, db: &Connection) -> Option<Value> {
1074 Value::from_reader(self.get_blob(db, MESSAGE_SUMMARY_INFO)?).ok()
1075 }
1076
1077 /// Get a message's [typedstream](crate::util::typedstream) from the [`ATTRIBUTED_BODY`] BLOB column
1078 ///
1079 /// Calling this hits the database, so it is expensive and should
1080 /// only get invoked when needed.
1081 ///
1082 /// This column contains the message's body text with any other attributes.
1083 pub fn attributed_body(&self, db: &Connection) -> Option<Vec<u8>> {
1084 let mut body = vec![];
1085 self.get_blob(db, ATTRIBUTED_BODY)?
1086 .read_to_end(&mut body)
1087 .ok();
1088 Some(body)
1089 }
1090
1091 /// Determine which [`Expressive`] the message was sent with
1092 pub fn get_expressive(&self) -> Expressive {
1093 match &self.expressive_send_style_id {
1094 Some(content) => match content.as_str() {
1095 "com.apple.MobileSMS.expressivesend.gentle" => {
1096 Expressive::Bubble(BubbleEffect::Gentle)
1097 }
1098 "com.apple.MobileSMS.expressivesend.impact" => {
1099 Expressive::Bubble(BubbleEffect::Slam)
1100 }
1101 "com.apple.MobileSMS.expressivesend.invisibleink" => {
1102 Expressive::Bubble(BubbleEffect::InvisibleInk)
1103 }
1104 "com.apple.MobileSMS.expressivesend.loud" => Expressive::Bubble(BubbleEffect::Loud),
1105 "com.apple.messages.effect.CKConfettiEffect" => {
1106 Expressive::Screen(ScreenEffect::Confetti)
1107 }
1108 "com.apple.messages.effect.CKEchoEffect" => Expressive::Screen(ScreenEffect::Echo),
1109 "com.apple.messages.effect.CKFireworksEffect" => {
1110 Expressive::Screen(ScreenEffect::Fireworks)
1111 }
1112 "com.apple.messages.effect.CKHappyBirthdayEffect" => {
1113 Expressive::Screen(ScreenEffect::Balloons)
1114 }
1115 "com.apple.messages.effect.CKHeartEffect" => {
1116 Expressive::Screen(ScreenEffect::Heart)
1117 }
1118 "com.apple.messages.effect.CKLasersEffect" => {
1119 Expressive::Screen(ScreenEffect::Lasers)
1120 }
1121 "com.apple.messages.effect.CKShootingStarEffect" => {
1122 Expressive::Screen(ScreenEffect::ShootingStar)
1123 }
1124 "com.apple.messages.effect.CKSparklesEffect" => {
1125 Expressive::Screen(ScreenEffect::Sparkles)
1126 }
1127 "com.apple.messages.effect.CKSpotlightEffect" => {
1128 Expressive::Screen(ScreenEffect::Spotlight)
1129 }
1130 _ => Expressive::Unknown(content),
1131 },
1132 None => Expressive::None,
1133 }
1134 }
1135
1136 /// Create a message from a given GUID; useful for debugging
1137 ///
1138 /// # Example
1139 /// ```rust
1140 /// use imessage_database::{
1141 /// tables::{
1142 /// messages::Message,
1143 /// table::get_connection,
1144 /// },
1145 /// util::dirs::default_db_path,
1146 /// };
1147 ///
1148 /// let db_path = default_db_path();
1149 /// let conn = get_connection(&db_path).unwrap();
1150 ///
1151 /// if let Ok(mut message) = Message::from_guid("example-guid", &conn) {
1152 /// let _ = message.generate_text(&conn);
1153 /// println!("{:#?}", message)
1154 /// }
1155 ///```
1156 pub fn from_guid(guid: &str, db: &Connection) -> Result<Self, TableError> {
1157 // If the database has `chat_recoverable_message_join`, we can restore some deleted messages.
1158 // If database has `thread_originator_guid`, we can parse replies, otherwise default to 0
1159 let filters = format!("WHERE m.guid = \"{guid}\"");
1160
1161 let mut statement = db
1162 .prepare(&ios_16_newer_query(Some(&filters)))
1163 .or_else(|_| db.prepare(&ios_14_15_query(Some(&filters))))
1164 .or_else(|_| db.prepare(&ios_13_older_query(Some(&filters))))
1165 .map_err(TableError::Messages)?;
1166
1167 Message::extract(statement.query_row([], |row| Ok(Message::from_row(row))))
1168 }
1169}
1170
1171#[cfg(test)]
1172impl Message {
1173 pub fn blank() -> Message {
1174 Message {
1175 rowid: i32::default(),
1176 guid: String::default(),
1177 text: None,
1178 service: Some("iMessage".to_string()),
1179 handle_id: Some(i32::default()),
1180 destination_caller_id: None,
1181 subject: None,
1182 date: i64::default(),
1183 date_read: i64::default(),
1184 date_delivered: i64::default(),
1185 is_from_me: false,
1186 is_read: false,
1187 item_type: 0,
1188 other_handle: None,
1189 share_status: false,
1190 share_direction: None,
1191 group_title: None,
1192 group_action_type: 0,
1193 associated_message_guid: None,
1194 associated_message_type: None,
1195 balloon_bundle_id: None,
1196 expressive_send_style_id: None,
1197 thread_originator_guid: None,
1198 thread_originator_part: None,
1199 date_edited: 0,
1200 associated_message_emoji: None,
1201 chat_id: None,
1202 num_attachments: 0,
1203 deleted_from: None,
1204 num_replies: 0,
1205 components: None,
1206 edited_parts: None,
1207 }
1208 }
1209}