1use std::{
127 collections::{HashMap, HashSet},
128 fmt::Write,
129 io::{Cursor, Read},
130};
131
132use chrono::{DateTime, offset::Local};
133use crabstep::TypedStreamDeserializer;
134use plist::Value;
135use rusqlite::{CachedStatement, Connection, Result, Row};
136
137use crate::{
138 error::{message::MessageError, table::TableError},
139 message_types::{
140 edited::{EditStatus, EditedMessage},
141 expressives::{BubbleEffect, Expressive, ScreenEffect},
142 polls::Poll,
143 text_effects::text_effect::TextEffect,
144 translation::Translation,
145 variants::{Announcement, BalloonProvider, CustomBalloon, Tapback, TapbackAction, Variant},
146 },
147 tables::{
148 diagnostic::{MessageDiagnostic, count_query, table_exists},
149 messages::{
150 body::{parse_body_legacy, parse_body_typedstream},
151 models::{BubbleComponent, GroupAction, Service, SharedLocation},
152 query_parts::{ios_13_older_query, ios_14_15_query, ios_16_newer_query},
153 },
154 table::{
155 ATTRIBUTED_BODY, CHAT_MESSAGE_JOIN, Cacheable, MESSAGE, MESSAGE_ATTACHMENT_JOIN,
156 MESSAGE_PAYLOAD, MESSAGE_SUMMARY_INFO, RECENTLY_DELETED, Table,
157 },
158 },
159 util::{
160 bundle_id::parse_balloon_bundle_id,
161 dates::{get_local_time, readable_diff},
162 query_context::QueryContext,
163 streamtyped,
164 },
165};
166
167pub(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";
170
171#[derive(Debug)]
173#[allow(non_snake_case)]
174pub struct Message {
175 pub rowid: i32,
177 pub guid: String,
179 pub text: Option<String>,
181 pub service: Option<String>,
183 pub handle_id: Option<i32>,
185 pub destination_caller_id: Option<String>,
187 pub subject: Option<String>,
189 pub date: i64,
191 pub date_read: i64,
193 pub date_delivered: i64,
195 pub is_from_me: bool,
197 pub is_read: bool,
199 pub item_type: i32,
201 pub other_handle: Option<i32>,
203 pub share_status: bool,
205 pub share_direction: Option<bool>,
207 pub group_title: Option<String>,
209 pub group_action_type: i32,
211 pub associated_message_guid: Option<String>,
213 pub associated_message_type: Option<i32>,
215 pub balloon_bundle_id: Option<String>,
217 pub expressive_send_style_id: Option<String>,
219 pub thread_originator_guid: Option<String>,
221 pub thread_originator_part: Option<String>,
223 pub date_edited: i64,
225 pub associated_message_emoji: Option<String>,
227 pub chat_id: Option<i32>,
229 pub num_attachments: i32,
231 pub deleted_from: Option<i32>,
233 pub num_replies: i32,
235 pub components: Vec<BubbleComponent>,
237 pub edited_parts: Option<EditedMessage>,
239}
240
241#[derive(Debug)]
255#[must_use]
256pub struct ParsedBody {
257 pub text: Option<String>,
259 pub components: Vec<BubbleComponent>,
261 pub edited_parts: Option<EditedMessage>,
263 pub balloon_bundle_id: Option<String>,
265}
266
267impl Table for Message {
269 fn from_row(row: &Row) -> Result<Message> {
270 Self::from_row_idx(row).or_else(|_| Self::from_row_named(row))
271 }
272
273 fn get(db: &'_ Connection) -> Result<CachedStatement<'_>, TableError> {
275 Ok(db
276 .prepare_cached(&ios_16_newer_query(None))
277 .or_else(|_| db.prepare_cached(&ios_14_15_query(None)))
278 .or_else(|_| db.prepare_cached(&ios_13_older_query(None)))?)
279 }
280}
281
282impl Message {
284 pub fn run_diagnostic(db: &Connection) -> Result<MessageDiagnostic, TableError> {
298 let messages_without_chat = count_query(
299 db,
300 &format!(
301 "
302 SELECT
303 COUNT(m.rowid)
304 FROM
305 {MESSAGE} as m
306 LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.rowid = c.message_id
307 WHERE
308 c.chat_id is NULL
309 ORDER BY
310 m.date
311 "
312 ),
313 )?;
314
315 let messages_in_multiple_chats = count_query(
316 db,
317 &format!(
318 "
319 SELECT
320 COUNT(*)
321 FROM (
322 SELECT DISTINCT
323 message_id
324 , COUNT(chat_id) AS c
325 FROM {CHAT_MESSAGE_JOIN}
326 GROUP BY
327 message_id
328 HAVING c > 1);
329 "
330 ),
331 )?;
332
333 let total_messages = count_query(
334 db,
335 &format!(
336 "
337 SELECT
338 COUNT(rowid)
339 FROM
340 {MESSAGE}
341 "
342 ),
343 )?;
344
345 let recoverable_messages = if table_exists(db, RECENTLY_DELETED)? {
347 Some(count_query(
348 db,
349 &format!("SELECT COUNT(*) FROM {RECENTLY_DELETED}"),
350 )?)
351 } else {
352 None
353 };
354
355 let mut date_range = db.prepare(&format!("SELECT MIN(date), MAX(date) FROM {MESSAGE}"))?;
357 let (first_message_date, last_message_date): (Option<i64>, Option<i64>) = date_range
358 .query_row([], |r| Ok((r.get(0).ok(), r.get(1).ok())))
359 .unwrap_or((None, None));
360
361 Ok(MessageDiagnostic {
362 total_messages,
363 messages_without_chat,
364 messages_in_multiple_chats,
365 recoverable_messages,
366 first_message_date,
367 last_message_date,
368 })
369 }
370}
371
372impl Cacheable for Message {
374 type K = String;
375 type V = HashMap<usize, Vec<Self>>;
376 fn cache(db: &Connection) -> Result<HashMap<Self::K, Self::V>, TableError> {
391 let mut map: HashMap<Self::K, Self::V> = HashMap::new();
393
394 let statement = db.prepare(&format!(
396 "SELECT
397 {COLS},
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 )).or_else(|_| db.prepare(&format!(
408 "SELECT
409 *,
410 c.chat_id,
411 (SELECT COUNT(*) FROM {MESSAGE_ATTACHMENT_JOIN} a WHERE m.ROWID = a.message_id) as num_attachments,
412 NULL as deleted_from,
413 0 as num_replies
414 FROM
415 {MESSAGE} as m
416 LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
417 WHERE m.associated_message_guid IS NOT NULL
418 "
419 )));
420
421 if let Ok(mut statement) = statement {
422 for message in Self::rows(&mut statement, [])? {
423 let message = message?;
424 if message.is_tapback()
425 && let Some((idx, tapback_target_guid)) = message.clean_associated_guid()
426 {
427 map.entry(tapback_target_guid.to_string())
428 .or_insert_with(HashMap::new)
429 .entry(idx)
430 .or_insert_with(Vec::new)
431 .push(message);
432 }
433 }
434 }
435
436 Ok(map)
437 }
438}
439
440impl Message {
442 fn from_row_idx(row: &Row) -> Result<Message> {
444 Ok(Message {
445 rowid: row.get(0)?,
446 guid: row.get(1)?,
447 text: row.get(2).unwrap_or(None),
448 service: row.get(3).unwrap_or(None),
449 handle_id: row.get(4).unwrap_or(None),
450 destination_caller_id: row.get(5).unwrap_or(None),
451 subject: row.get(6).unwrap_or(None),
452 date: row.get(7)?,
453 date_read: row.get(8).unwrap_or(0),
454 date_delivered: row.get(9).unwrap_or(0),
455 is_from_me: row.get(10)?,
456 is_read: row.get(11).unwrap_or(false),
457 item_type: row.get(12).unwrap_or_default(),
458 other_handle: row.get(13).unwrap_or(None),
459 share_status: row.get(14).unwrap_or(false),
460 share_direction: row.get(15).unwrap_or(None),
461 group_title: row.get(16).unwrap_or(None),
462 group_action_type: row.get(17).unwrap_or(0),
463 associated_message_guid: row.get(18).unwrap_or(None),
464 associated_message_type: row.get(19).unwrap_or(None),
465 balloon_bundle_id: row.get(20).unwrap_or(None),
466 expressive_send_style_id: row.get(21).unwrap_or(None),
467 thread_originator_guid: row.get(22).unwrap_or(None),
468 thread_originator_part: row.get(23).unwrap_or(None),
469 date_edited: row.get(24).unwrap_or(0),
470 associated_message_emoji: row.get(25).unwrap_or(None),
471 chat_id: row.get(26).unwrap_or(None),
472 num_attachments: row.get(27)?,
473 deleted_from: row.get(28).unwrap_or(None),
474 num_replies: row.get(29)?,
475 components: vec![],
476 edited_parts: None,
477 })
478 }
479
480 fn from_row_named(row: &Row) -> Result<Message> {
482 Ok(Message {
483 rowid: row.get("rowid")?,
484 guid: row.get("guid")?,
485 text: row.get("text").unwrap_or(None),
486 service: row.get("service").unwrap_or(None),
487 handle_id: row.get("handle_id").unwrap_or(None),
488 destination_caller_id: row.get("destination_caller_id").unwrap_or(None),
489 subject: row.get("subject").unwrap_or(None),
490 date: row.get("date")?,
491 date_read: row.get("date_read").unwrap_or(0),
492 date_delivered: row.get("date_delivered").unwrap_or(0),
493 is_from_me: row.get("is_from_me")?,
494 is_read: row.get("is_read").unwrap_or(false),
495 item_type: row.get("item_type").unwrap_or_default(),
496 other_handle: row.get("other_handle").unwrap_or(None),
497 share_status: row.get("share_status").unwrap_or(false),
498 share_direction: row.get("share_direction").unwrap_or(None),
499 group_title: row.get("group_title").unwrap_or(None),
500 group_action_type: row.get("group_action_type").unwrap_or(0),
501 associated_message_guid: row.get("associated_message_guid").unwrap_or(None),
502 associated_message_type: row.get("associated_message_type").unwrap_or(None),
503 balloon_bundle_id: row.get("balloon_bundle_id").unwrap_or(None),
504 expressive_send_style_id: row.get("expressive_send_style_id").unwrap_or(None),
505 thread_originator_guid: row.get("thread_originator_guid").unwrap_or(None),
506 thread_originator_part: row.get("thread_originator_part").unwrap_or(None),
507 date_edited: row.get("date_edited").unwrap_or(0),
508 associated_message_emoji: row.get("associated_message_emoji").unwrap_or(None),
509 chat_id: row.get("chat_id").unwrap_or(None),
510 num_attachments: row.get("num_attachments")?,
511 deleted_from: row.get("deleted_from").unwrap_or(None),
512 num_replies: row.get("num_replies")?,
513 components: vec![],
514 edited_parts: None,
515 })
516 }
517
518 pub fn parse_body(&self, db: &Connection) -> Result<ParsedBody, MessageError> {
537 let edited_parts = self
539 .is_edited()
540 .then(|| self.message_summary_info(db))
541 .flatten()
542 .as_ref()
543 .and_then(|payload| EditedMessage::from_map(payload).ok());
544
545 let mut text = None;
547 let mut components = vec![];
548 let mut balloon_bundle_id = None;
549
550 if let Some(body) = self.attributed_body(db) {
552 let mut typedstream = TypedStreamDeserializer::new(&body);
554 match parse_body_typedstream(typedstream.iter_root().ok(), edited_parts.as_ref()) {
555 Some(parsed) => {
556 text = parsed.text;
557
558 let is_single_url = match &parsed.components[..] {
561 [BubbleComponent::Run(ranges)] => match &ranges[..] {
562 [range] if range.attachment.is_none() => {
563 matches!(&range.effects[..], [TextEffect::Link(_)])
564 }
565 _ => false,
566 },
567 _ => false,
568 };
569
570 if self.balloon_bundle_id.is_some() {
572 components = vec![BubbleComponent::App];
573 } else if is_single_url
574 && self.has_blob(db, MESSAGE, MESSAGE_PAYLOAD, self.rowid.into())
575 {
576 balloon_bundle_id =
579 Some("com.apple.messages.URLBalloonProvider".to_string());
580 components = vec![BubbleComponent::App];
581 } else {
582 components = parsed.components;
583 }
584 }
585 None => {
586 text = self.text.clone();
588 }
589 }
590
591 if text.is_none() {
593 text = Some(streamtyped::parse(body)?);
594 }
595 }
596
597 let text = text.or_else(|| self.text.clone());
599
600 let balloon_bundle_id = balloon_bundle_id.or_else(|| self.balloon_bundle_id.clone());
602
603 if components.is_empty() && text.is_some() {
605 components = parse_body_legacy(&text);
606 }
607
608 if text.is_some() || !components.is_empty() || edited_parts.is_some() {
610 Ok(ParsedBody {
611 text,
612 components,
613 edited_parts,
614 balloon_bundle_id,
615 })
616 } else {
617 Err(MessageError::NoText)
618 }
619 }
620
621 pub fn apply_body(&mut self, body: ParsedBody) {
624 self.text = body.text;
625 self.components = body.components;
626 self.edited_parts = body.edited_parts;
627 self.balloon_bundle_id = body.balloon_bundle_id;
628 }
629
630 pub fn generate_text_legacy<'a>(
634 &'a mut self,
635 db: &'a Connection,
636 ) -> Result<&'a str, MessageError> {
637 if self.text.is_none()
639 && let Some(body) = self.attributed_body(db)
640 {
641 self.text = Some(streamtyped::parse(body)?);
642 }
643
644 if self.components.is_empty() {
646 self.components = parse_body_legacy(&self.text);
647 }
648
649 self.text.as_deref().ok_or(MessageError::NoText)
650 }
651
652 pub fn date(&self, offset: i64) -> Result<DateTime<Local>, MessageError> {
659 get_local_time(self.date, offset)
660 }
661
662 pub fn date_delivered(&self, offset: i64) -> Result<DateTime<Local>, MessageError> {
668 get_local_time(self.date_delivered, offset)
669 }
670
671 pub fn date_read(&self, offset: i64) -> Result<DateTime<Local>, MessageError> {
677 get_local_time(self.date_read, offset)
678 }
679
680 pub fn date_edited(&self, offset: i64) -> Result<DateTime<Local>, MessageError> {
686 get_local_time(self.date_edited, offset)
687 }
688
689 #[must_use]
705 pub fn time_until_read(&self, offset: i64) -> Option<String> {
706 if !self.is_from_me && self.date_read != 0 && self.date != 0 {
708 return readable_diff(&self.date(offset).ok()?, &self.date_read(offset).ok()?);
709 }
710 else if self.is_from_me && self.date_delivered != 0 && self.date != 0 {
712 return readable_diff(&self.date(offset).ok()?, &self.date_delivered(offset).ok()?);
713 }
714 None
715 }
716
717 #[must_use]
720 pub fn is_reply(&self) -> bool {
721 self.thread_originator_guid.is_some()
722 }
723
724 #[must_use]
726 pub fn is_announcement(&self) -> bool {
727 self.get_announcement().is_some()
728 }
729
730 #[must_use]
732 pub fn is_tapback(&self) -> bool {
733 matches!(self.variant(), Variant::Tapback(..))
734 }
735
736 #[must_use]
738 pub fn is_expressive(&self) -> bool {
739 self.expressive_send_style_id.is_some()
740 }
741
742 #[must_use]
744 pub fn is_url(&self) -> bool {
745 matches!(self.variant(), Variant::App(CustomBalloon::URL))
746 }
747
748 #[must_use]
750 pub fn is_handwriting(&self) -> bool {
751 matches!(self.variant(), Variant::App(CustomBalloon::Handwriting))
752 }
753
754 #[must_use]
756 pub fn is_digital_touch(&self) -> bool {
757 matches!(self.variant(), Variant::App(CustomBalloon::DigitalTouch))
758 }
759
760 #[must_use]
762 pub fn is_poll(&self) -> bool {
763 matches!(self.variant(), Variant::App(CustomBalloon::Polls))
764 }
765
766 #[must_use]
768 pub fn is_poll_vote(&self) -> bool {
769 self.associated_message_type == Some(4000)
770 }
771
772 #[must_use]
774 pub fn is_poll_update(&self) -> bool {
775 matches!(self.variant(), Variant::PollUpdate)
776 }
777
778 #[must_use]
780 pub fn is_edited(&self) -> bool {
781 self.date_edited != 0
782 }
783
784 #[must_use]
786 pub fn is_part_edited(&self, index: usize) -> bool {
787 if let Some(edited_parts) = &self.edited_parts
788 && let Some(part) = edited_parts.part(index)
789 {
790 return matches!(part.status, EditStatus::Edited);
791 }
792 false
793 }
794
795 #[must_use]
797 pub fn is_fully_unsent(&self) -> bool {
798 self.edited_parts.as_ref().is_some_and(|ep| {
799 ep.parts
800 .iter()
801 .all(|part| matches!(part.status, EditStatus::Unsent))
802 })
803 }
804
805 #[must_use]
809 pub fn has_attachments(&self) -> bool {
810 self.num_attachments > 0
811 }
812
813 #[must_use]
815 pub fn has_replies(&self) -> bool {
816 self.num_replies > 0
817 }
818
819 #[must_use]
821 pub fn is_kept_audio_message(&self) -> bool {
822 self.item_type == 5
823 }
824
825 #[must_use]
827 pub fn is_shareplay(&self) -> bool {
828 self.item_type == 6
829 }
830
831 #[must_use]
833 pub fn is_from_me(&self) -> bool {
834 if self.item_type == 4
837 && let (Some(other_handle), Some(share_direction)) =
838 (self.other_handle, self.share_direction)
839 {
840 self.is_from_me || other_handle != 0 && !share_direction
841 } else {
842 self.is_from_me
843 }
844 }
845
846 #[must_use]
849 pub fn shared_location_kind(&self) -> Option<SharedLocation> {
850 if self.item_type == 4 && self.group_action_type == 0 {
851 Some(if self.share_status {
852 SharedLocation::Stopped
853 } else {
854 SharedLocation::Started
855 })
856 } else {
857 None
858 }
859 }
860
861 #[must_use]
873 pub fn is_deleted(&self) -> bool {
874 self.deleted_from.is_some()
875 }
876
877 pub fn has_translation(&self, db: &Connection) -> bool {
879 let query = format!(
882 "SELECT ROWID FROM {MESSAGE}
883 WHERE message_summary_info IS NOT NULL
884 AND length(message_summary_info) > 61
885 AND instr(message_summary_info, X'7472616E736C6174696F6E4C616E6775616765') > 0
886 AND instr(message_summary_info, X'7472616E736C6174656454657874') > 0
887 AND ROWID = ?"
888 );
889 if let Ok(mut statement) = db.prepare_cached(&query) {
890 let result: Result<i32, _> = statement.query_row([self.rowid], |row| row.get(0));
891 result.is_ok()
892 } else {
893 false
894 }
895 }
896
897 pub fn get_translation(&self, db: &Connection) -> Result<Option<Translation>, MessageError> {
899 if let Some(payload) = self.message_summary_info(db) {
900 return Ok(Some(Translation::from_payload(&payload)?));
901 }
902 Ok(None)
903 }
904
905 pub fn cache_translations(db: &Connection) -> Result<HashSet<String>, TableError> {
907 let query = format!(
910 "SELECT guid FROM {MESSAGE}
911 WHERE message_summary_info IS NOT NULL
912 AND length(message_summary_info) > 61
913 AND instr(message_summary_info, X'7472616E736C6174696F6E4C616E6775616765') > 0
914 AND instr(message_summary_info, X'7472616E736C6174656454657874') > 0"
915 );
916
917 let mut statement = db.prepare(&query)?;
918 let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
919
920 let mut guids = HashSet::new();
921 for guid_result in rows {
922 guids.insert(guid_result?);
923 }
924
925 Ok(guids)
926 }
927
928 #[must_use]
930 pub fn group_action(&'_ self) -> Option<GroupAction<'_>> {
931 GroupAction::from_message(self)
932 }
933
934 fn get_reply_index(&self) -> usize {
936 if let Some(parts) = &self.thread_originator_part {
937 return match parts.split(':').next() {
938 Some(part) => str::parse::<usize>(part).unwrap_or(0),
939 None => 0,
940 };
941 }
942 0
943 }
944
945 pub(crate) fn generate_filter_statement(
952 context: &QueryContext,
953 include_recoverable: bool,
954 ) -> String {
955 let mut filters = String::with_capacity(128);
956
957 if let Some(start) = context.start {
959 let _ = write!(filters, " m.date >= {start}");
960 }
961
962 if let Some(end) = context.end {
964 if !filters.is_empty() {
965 filters.push_str(" AND ");
966 }
967 let _ = write!(filters, " m.date <= {end}");
968 }
969
970 if let Some(chat_ids) = &context.selected_chat_ids {
972 if !filters.is_empty() {
973 filters.push_str(" AND ");
974 }
975
976 let ids = chat_ids
978 .iter()
979 .map(std::string::ToString::to_string)
980 .collect::<Vec<String>>()
981 .join(", ");
982
983 if include_recoverable {
984 let _ = write!(filters, " (c.chat_id IN ({ids}) OR d.chat_id IN ({ids}))");
985 } else {
986 let _ = write!(filters, " c.chat_id IN ({ids})");
987 }
988 }
989
990 if !filters.is_empty() {
991 return format!("WHERE {filters}");
992 }
993 filters
994 }
995
996 pub fn get_count(db: &Connection, context: &QueryContext) -> Result<i64, TableError> {
1012 let mut statement = if context.has_filters() {
1013 db.prepare_cached(&format!(
1014 "SELECT
1015 COUNT(*)
1016 FROM {MESSAGE} as m
1017 LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
1018 LEFT JOIN {RECENTLY_DELETED} as d ON m.ROWID = d.message_id
1019 {}",
1020 Self::generate_filter_statement(context, true)
1021 ))
1022 .or_else(|_| {
1023 db.prepare_cached(&format!(
1024 "SELECT
1025 COUNT(*)
1026 FROM {MESSAGE} as m
1027 LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
1028 {}",
1029 Self::generate_filter_statement(context, false)
1030 ))
1031 })?
1032 } else {
1033 db.prepare_cached(&format!("SELECT COUNT(*) FROM {MESSAGE}"))?
1034 };
1035 let count: i64 = statement.query_row([], |r| r.get(0)).unwrap_or(0);
1037
1038 Ok(count)
1039 }
1040
1041 pub fn stream_rows<'a>(
1062 db: &'a Connection,
1063 context: &'a QueryContext,
1064 ) -> Result<CachedStatement<'a>, TableError> {
1065 if !context.has_filters() {
1066 return Self::get(db);
1067 }
1068 Ok(db
1069 .prepare_cached(&ios_16_newer_query(Some(&Self::generate_filter_statement(
1070 context, true,
1071 ))))
1072 .or_else(|_| {
1073 db.prepare_cached(&ios_14_15_query(Some(&Self::generate_filter_statement(
1074 context, false,
1075 ))))
1076 })
1077 .or_else(|_| {
1078 db.prepare_cached(&ios_13_older_query(Some(&Self::generate_filter_statement(
1079 context, false,
1080 ))))
1081 })?)
1082 }
1083
1084 #[must_use]
1088 pub fn clean_associated_guid(&self) -> Option<(usize, &str)> {
1089 if let Some(guid) = &self.associated_message_guid {
1090 if guid.starts_with("p:") {
1091 let mut split = guid.split('/');
1092 let index_str = split.next()?;
1093 let message_id = split.next()?;
1094 let index = str::parse::<usize>(&index_str.replace("p:", "")).unwrap_or(0);
1095 return Some((index, message_id.get(0..36)?));
1096 } else if guid.starts_with("bp:") {
1097 return Some((0, guid.get(3..39)?));
1098 }
1099
1100 return Some((0, guid.get(0..36)?));
1101 }
1102 None
1103 }
1104
1105 fn tapback_index(&self) -> usize {
1107 match self.clean_associated_guid() {
1108 Some((x, _)) => x,
1109 None => 0,
1110 }
1111 }
1112
1113 pub fn get_replies(&self, db: &Connection) -> Result<HashMap<usize, Vec<Self>>, TableError> {
1115 let mut out_h: HashMap<usize, Vec<Self>> = HashMap::new();
1116
1117 if self.has_replies() {
1119 let filters = "WHERE m.thread_originator_guid = ?1";
1121
1122 let mut statement = db
1124 .prepare_cached(&ios_16_newer_query(Some(filters)))
1125 .or_else(|_| db.prepare_cached(&ios_14_15_query(Some(filters))))?;
1126
1127 for message in Message::rows(&mut statement, [self.guid.as_str()])? {
1128 let m = message?;
1129 let idx = m.get_reply_index();
1130 match out_h.get_mut(&idx) {
1131 Some(body_part) => body_part.push(m),
1132 None => {
1133 out_h.insert(idx, vec![m]);
1134 }
1135 }
1136 }
1137 }
1138
1139 Ok(out_h)
1140 }
1141
1142 pub fn get_votes(&self, db: &Connection) -> Result<Vec<Self>, TableError> {
1145 let mut out_v: Vec<Self> = Vec::new();
1146
1147 if self.is_poll() {
1149 let filters = "WHERE m.associated_message_guid = ?1";
1151
1152 let mut statement = db
1154 .prepare_cached(&ios_16_newer_query(Some(filters)))
1155 .or_else(|_| db.prepare_cached(&ios_14_15_query(Some(filters))))?;
1156
1157 for message in Message::rows(&mut statement, [self.guid.as_str()])? {
1158 out_v.push(message?);
1159 }
1160 }
1161
1162 Ok(out_v)
1163 }
1164
1165 pub fn as_poll(&self, db: &Connection) -> Result<Option<Poll>, MessageError> {
1167 if self.is_poll()
1168 && let Some(payload) = self.payload_data(db)
1169 {
1170 let mut poll = Poll::from_payload(&payload)?;
1171
1172 let votes = self.get_votes(db).unwrap_or_default();
1174
1175 for vote in votes.iter().rev() {
1177 if !vote.is_poll_vote()
1180 && let Some(vote_payload) = vote.payload_data(db)
1181 && let Ok(update) = Poll::from_payload(&vote_payload)
1182 {
1183 poll = update;
1184 break;
1185 }
1186 }
1187
1188 for vote in &votes {
1190 if vote.is_poll_vote()
1191 && let Some(vote_payload) = vote.payload_data(db)
1192 {
1193 poll.count_votes(&vote_payload)?;
1194 }
1195 }
1196
1197 return Ok(Some(poll));
1198 }
1199
1200 Ok(None)
1201 }
1202
1203 #[must_use]
1206 pub fn variant(&'_ self) -> Variant<'_> {
1207 if self.is_edited() {
1209 return Variant::Edited;
1210 }
1211
1212 if let Some(associated_message_type) = self.associated_message_type {
1214 match associated_message_type {
1215 0 | 2 | 3 => return self.get_app_variant().unwrap_or(Variant::Normal),
1217 1000 | 2000..=2007 | 3000..=3007 => {
1219 if let Some((action, tapback)) = self.get_tapback() {
1220 return Variant::Tapback(self.tapback_index(), action, tapback);
1221 }
1222 }
1223 4000 => return Variant::Vote,
1225 x => return Variant::Unknown(x),
1226 }
1227 }
1228
1229 if self.is_shareplay() {
1231 return Variant::SharePlay;
1232 }
1233
1234 Variant::Normal
1235 }
1236
1237 #[must_use]
1239 fn get_app_variant(&self) -> Option<Variant<'_>> {
1240 let bundle_id = parse_balloon_bundle_id(self.balloon_bundle_id.as_deref())?;
1241 let custom = match bundle_id {
1242 "com.apple.messages.URLBalloonProvider" => CustomBalloon::URL,
1243 "com.apple.Handwriting.HandwritingProvider" => CustomBalloon::Handwriting,
1244 "com.apple.DigitalTouchBalloonProvider" => CustomBalloon::DigitalTouch,
1245 "com.apple.PassbookUIService.PeerPaymentMessagesExtension" => CustomBalloon::ApplePay,
1246 "com.apple.ActivityMessagesApp.MessagesExtension" => CustomBalloon::Fitness,
1247 "com.apple.mobileslideshow.PhotosMessagesApp" => CustomBalloon::Slideshow,
1248 "com.apple.SafetyMonitorApp.SafetyMonitorMessages" => CustomBalloon::CheckIn,
1249 "com.apple.findmy.FindMyMessagesApp" => CustomBalloon::FindMy,
1250 "com.apple.icloud.apps.messages.business.extension" => CustomBalloon::Business,
1251 "com.apple.messages.Polls" => {
1252 if self
1254 .associated_message_guid
1255 .as_ref()
1256 .is_none_or(|id| id == &self.guid)
1257 {
1258 CustomBalloon::Polls
1259 } else {
1260 return Some(Variant::PollUpdate);
1261 }
1262 }
1263 _ => CustomBalloon::Application(bundle_id),
1264 };
1265 Some(Variant::App(custom))
1266 }
1267
1268 #[must_use]
1270 fn get_tapback(&self) -> Option<(TapbackAction, Tapback<'_>)> {
1271 match self.associated_message_type? {
1272 1000 => Some((TapbackAction::Added, Tapback::Sticker)),
1273 2000 => Some((TapbackAction::Added, Tapback::Loved)),
1274 2001 => Some((TapbackAction::Added, Tapback::Liked)),
1275 2002 => Some((TapbackAction::Added, Tapback::Disliked)),
1276 2003 => Some((TapbackAction::Added, Tapback::Laughed)),
1277 2004 => Some((TapbackAction::Added, Tapback::Emphasized)),
1278 2005 => Some((TapbackAction::Added, Tapback::Questioned)),
1279 2006 => Some((
1280 TapbackAction::Added,
1281 Tapback::Emoji(self.associated_message_emoji.as_deref()),
1282 )),
1283 2007 => Some((TapbackAction::Added, Tapback::Sticker)),
1284 3000 => Some((TapbackAction::Removed, Tapback::Loved)),
1285 3001 => Some((TapbackAction::Removed, Tapback::Liked)),
1286 3002 => Some((TapbackAction::Removed, Tapback::Disliked)),
1287 3003 => Some((TapbackAction::Removed, Tapback::Laughed)),
1288 3004 => Some((TapbackAction::Removed, Tapback::Emphasized)),
1289 3005 => Some((TapbackAction::Removed, Tapback::Questioned)),
1290 3006 => Some((
1291 TapbackAction::Removed,
1292 Tapback::Emoji(self.associated_message_emoji.as_deref()),
1293 )),
1294 3007 => Some((TapbackAction::Removed, Tapback::Sticker)),
1295 _ => None,
1296 }
1297 }
1298
1299 #[must_use]
1301 pub fn get_announcement(&'_ self) -> Option<Announcement<'_>> {
1302 if let Some(action) = self.group_action() {
1303 return Some(Announcement::GroupAction(action));
1304 }
1305
1306 if self.is_fully_unsent() {
1307 return Some(Announcement::FullyUnsent);
1308 }
1309
1310 if self.is_kept_audio_message() {
1311 return Some(Announcement::AudioMessageKept);
1312 }
1313
1314 None
1315 }
1316
1317 #[must_use]
1319 pub fn service(&'_ self) -> Service<'_> {
1320 Service::from_name(self.service.as_deref())
1321 }
1322
1323 pub fn payload_data(&self, db: &Connection) -> Option<Value> {
1331 Value::from_reader(Cursor::new(self.raw_payload_data(db)?)).ok()
1333 }
1334
1335 pub fn raw_payload_data(&self, db: &Connection) -> Option<Vec<u8>> {
1341 let mut buf = Vec::new();
1342 self.get_blob(db, MESSAGE, MESSAGE_PAYLOAD, self.rowid.into())?
1343 .read_to_end(&mut buf)
1344 .ok()?;
1345 Some(buf)
1346 }
1347
1348 pub fn message_summary_info(&self, db: &Connection) -> Option<Value> {
1354 let mut buf = Vec::new();
1356 self.get_blob(db, MESSAGE, MESSAGE_SUMMARY_INFO, self.rowid.into())?
1357 .read_to_end(&mut buf)
1358 .ok()?;
1359 Value::from_reader(Cursor::new(buf)).ok()
1360 }
1361
1362 pub fn attributed_body(&self, db: &Connection) -> Option<Vec<u8>> {
1368 let mut body = vec![];
1369 self.get_blob(db, MESSAGE, ATTRIBUTED_BODY, self.rowid.into())?
1370 .read_to_end(&mut body)
1371 .ok();
1372 Some(body)
1373 }
1374
1375 #[must_use]
1378 pub fn get_expressive(&'_ self) -> Expressive<'_> {
1379 match &self.expressive_send_style_id {
1380 Some(content) => match content.as_str() {
1381 "com.apple.MobileSMS.expressivesend.gentle" => {
1382 Expressive::Bubble(BubbleEffect::Gentle)
1383 }
1384 "com.apple.MobileSMS.expressivesend.impact" => {
1385 Expressive::Bubble(BubbleEffect::Slam)
1386 }
1387 "com.apple.MobileSMS.expressivesend.invisibleink" => {
1388 Expressive::Bubble(BubbleEffect::InvisibleInk)
1389 }
1390 "com.apple.MobileSMS.expressivesend.loud" => Expressive::Bubble(BubbleEffect::Loud),
1391 "com.apple.messages.effect.CKConfettiEffect" => {
1392 Expressive::Screen(ScreenEffect::Confetti)
1393 }
1394 "com.apple.messages.effect.CKEchoEffect" => Expressive::Screen(ScreenEffect::Echo),
1395 "com.apple.messages.effect.CKFireworksEffect" => {
1396 Expressive::Screen(ScreenEffect::Fireworks)
1397 }
1398 "com.apple.messages.effect.CKHappyBirthdayEffect" => {
1399 Expressive::Screen(ScreenEffect::Balloons)
1400 }
1401 "com.apple.messages.effect.CKHeartEffect" => {
1402 Expressive::Screen(ScreenEffect::Heart)
1403 }
1404 "com.apple.messages.effect.CKLasersEffect" => {
1405 Expressive::Screen(ScreenEffect::Lasers)
1406 }
1407 "com.apple.messages.effect.CKShootingStarEffect" => {
1408 Expressive::Screen(ScreenEffect::ShootingStar)
1409 }
1410 "com.apple.messages.effect.CKSparklesEffect" => {
1411 Expressive::Screen(ScreenEffect::Sparkles)
1412 }
1413 "com.apple.messages.effect.CKSpotlightEffect" => {
1414 Expressive::Screen(ScreenEffect::Spotlight)
1415 }
1416 _ => Expressive::Unknown(content),
1417 },
1418 None => Expressive::None,
1419 }
1420 }
1421
1422 pub fn from_guid(guid: &str, db: &Connection) -> Result<Self, TableError> {
1445 let mut statement = db
1446 .prepare_cached(&ios_16_newer_query(Some("WHERE m.guid = ?1")))
1447 .or_else(|_| db.prepare_cached(&ios_14_15_query(Some("WHERE m.guid = ?1"))))
1448 .or_else(|_| db.prepare_cached(&ios_13_older_query(Some("WHERE m.guid = ?1"))))?;
1449
1450 Message::row(&mut statement, [guid])
1451 }
1452}
1453
1454#[cfg(test)]
1456impl Message {
1457 #[must_use]
1458 pub fn blank() -> Message {
1460 use std::vec;
1461
1462 Message {
1463 rowid: i32::default(),
1464 guid: String::default(),
1465 text: None,
1466 service: Some("iMessage".to_string()),
1467 handle_id: Some(i32::default()),
1468 destination_caller_id: None,
1469 subject: None,
1470 date: i64::default(),
1471 date_read: i64::default(),
1472 date_delivered: i64::default(),
1473 is_from_me: false,
1474 is_read: false,
1475 item_type: 0,
1476 other_handle: None,
1477 share_status: false,
1478 share_direction: None,
1479 group_title: None,
1480 group_action_type: 0,
1481 associated_message_guid: None,
1482 associated_message_type: None,
1483 balloon_bundle_id: None,
1484 expressive_send_style_id: None,
1485 thread_originator_guid: None,
1486 thread_originator_part: None,
1487 date_edited: 0,
1488 associated_message_emoji: None,
1489 chat_id: None,
1490 num_attachments: 0,
1491 deleted_from: None,
1492 num_replies: 0,
1493 components: vec![],
1494 edited_parts: None,
1495 }
1496 }
1497}
1498
1499#[cfg(test)]
1500mod diagnostic_tests {
1501 use rusqlite::Connection;
1502
1503 use crate::tables::messages::Message;
1504
1505 fn diagnostic_db() -> Connection {
1506 let db = Connection::open_in_memory().unwrap();
1507 db.execute_batch(
1508 "
1509 CREATE TABLE message (
1510 ROWID INTEGER PRIMARY KEY,
1511 date INTEGER
1512 );
1513 CREATE TABLE chat_message_join (
1514 chat_id INTEGER,
1515 message_id INTEGER
1516 );
1517 INSERT INTO message (ROWID, date) VALUES (1, 10), (2, 20);
1518 INSERT INTO chat_message_join (chat_id, message_id) VALUES (1, 1);
1519 ",
1520 )
1521 .unwrap();
1522 db
1523 }
1524
1525 #[test]
1526 fn diagnostic_omits_recoverable_count_when_table_is_missing() {
1527 let db = diagnostic_db();
1528
1529 let diagnostic = Message::run_diagnostic(&db).unwrap();
1530
1531 assert_eq!(diagnostic.total_messages, 2);
1532 assert_eq!(diagnostic.messages_without_chat, 1);
1533 assert_eq!(diagnostic.recoverable_messages, None);
1534 }
1535
1536 #[test]
1537 fn diagnostic_counts_recoverable_messages_when_table_exists() {
1538 let db = diagnostic_db();
1539 db.execute_batch(
1540 "
1541 CREATE TABLE chat_recoverable_message_join (
1542 chat_id INTEGER,
1543 message_id INTEGER
1544 );
1545 INSERT INTO chat_recoverable_message_join (chat_id, message_id) VALUES (1, 2);
1546 ",
1547 )
1548 .unwrap();
1549
1550 let diagnostic = Message::run_diagnostic(&db).unwrap();
1551
1552 assert_eq!(diagnostic.recoverable_messages, Some(1));
1553 }
1554}