1use std::{
119 collections::{HashMap, HashSet},
120 fmt::Write,
121 io::Read,
122};
123
124use chrono::{DateTime, offset::Local};
125use crabstep::TypedStreamDeserializer;
126use plist::Value;
127use rusqlite::{CachedStatement, Connection, Error, Result, Row};
128
129use crate::{
130 error::{message::MessageError, table::TableError},
131 message_types::{
132 edited::{EditStatus, EditedMessage},
133 expressives::{BubbleEffect, Expressive, ScreenEffect},
134 polls::Poll,
135 text_effects::TextEffect,
136 translation::Translation,
137 variants::{Announcement, BalloonProvider, CustomBalloon, Tapback, TapbackAction, Variant},
138 },
139 tables::{
140 messages::{
141 body::{parse_body_legacy, parse_body_typedstream},
142 models::{BubbleComponent, GroupAction, Service, TextAttributes},
143 query_parts::{ios_13_older_query, ios_14_15_query, ios_16_newer_query},
144 },
145 table::{
146 ATTRIBUTED_BODY, CHAT_MESSAGE_JOIN, Cacheable, Diagnostic, MESSAGE,
147 MESSAGE_ATTACHMENT_JOIN, MESSAGE_PAYLOAD, MESSAGE_SUMMARY_INFO, RECENTLY_DELETED,
148 Table,
149 },
150 },
151 util::{
152 bundle_id::parse_balloon_bundle_id,
153 dates::{get_local_time, readable_diff},
154 output::{done_processing, processing},
155 query_context::QueryContext,
156 streamtyped,
157 },
158};
159
160pub(crate) const COLS: &str = "rowid, guid, text, service, handle_id, destination_caller_id, subject, date, date_read, date_delivered, is_from_me, is_read, item_type, other_handle, share_status, share_direction, group_title, group_action_type, associated_message_guid, associated_message_type, balloon_bundle_id, expressive_send_style_id, thread_originator_guid, thread_originator_part, date_edited, associated_message_emoji";
163
164#[derive(Debug)]
168#[allow(non_snake_case)]
169pub struct Message {
170 pub rowid: i32,
172 pub guid: String,
174 pub text: Option<String>,
176 pub service: Option<String>,
178 pub handle_id: Option<i32>,
180 pub destination_caller_id: Option<String>,
182 pub subject: Option<String>,
184 pub date: i64,
186 pub date_read: i64,
188 pub date_delivered: i64,
190 pub is_from_me: bool,
192 pub is_read: bool,
194 pub item_type: i32,
196 pub other_handle: Option<i32>,
198 pub share_status: bool,
200 pub share_direction: Option<bool>,
202 pub group_title: Option<String>,
204 pub group_action_type: i32,
206 pub associated_message_guid: Option<String>,
208 pub associated_message_type: Option<i32>,
210 pub balloon_bundle_id: Option<String>,
212 pub expressive_send_style_id: Option<String>,
214 pub thread_originator_guid: Option<String>,
216 pub thread_originator_part: Option<String>,
218 pub date_edited: i64,
220 pub associated_message_emoji: Option<String>,
222 pub chat_id: Option<i32>,
224 pub num_attachments: i32,
226 pub deleted_from: Option<i32>,
228 pub num_replies: i32,
230 pub components: Vec<BubbleComponent>,
232 pub edited_parts: Option<EditedMessage>,
234}
235
236impl Table for Message {
238 fn from_row(row: &Row) -> Result<Message> {
239 Self::from_row_idx(row).or_else(|_| Self::from_row_named(row))
240 }
241
242 fn get(db: &'_ Connection) -> Result<CachedStatement<'_>, TableError> {
245 Ok(db
246 .prepare_cached(&ios_16_newer_query(None))
247 .or_else(|_| db.prepare_cached(&ios_14_15_query(None)))
248 .or_else(|_| db.prepare_cached(&ios_13_older_query(None)))?)
249 }
250
251 fn extract(message: Result<Result<Self, Error>, Error>) -> Result<Self, TableError> {
252 match message {
253 Ok(Ok(message)) => Ok(message),
254 Err(why) | Ok(Err(why)) => Err(TableError::QueryError(why)),
255 }
256 }
257}
258
259impl Diagnostic for Message {
261 fn run_diagnostic(db: &Connection) -> Result<(), TableError> {
275 processing();
276 let mut messages_without_chat = db.prepare(&format!(
277 "
278 SELECT
279 COUNT(m.rowid)
280 FROM
281 {MESSAGE} as m
282 LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.rowid = c.message_id
283 WHERE
284 c.chat_id is NULL
285 ORDER BY
286 m.date
287 "
288 ))?;
289
290 let num_dangling: i32 = messages_without_chat
291 .query_row([], |r| r.get(0))
292 .unwrap_or(0);
293
294 let mut messages_in_more_than_one_chat_q = db.prepare(&format!(
295 "
296 SELECT
297 COUNT(*)
298 FROM (
299 SELECT DISTINCT
300 message_id
301 , COUNT(chat_id) AS c
302 FROM {CHAT_MESSAGE_JOIN}
303 GROUP BY
304 message_id
305 HAVING c > 1);
306 "
307 ))?;
308
309 let messages_in_more_than_one_chat: i32 = messages_in_more_than_one_chat_q
310 .query_row([], |r| r.get(0))
311 .unwrap_or(0);
312
313 let mut messages_count = db.prepare(&format!(
314 "
315 SELECT
316 COUNT(rowid)
317 FROM
318 {MESSAGE}
319 "
320 ))?;
321
322 let total_messages: i64 = messages_count.query_row([], |r| r.get(0)).unwrap_or(0);
323
324 done_processing();
325
326 println!("Message diagnostic data:");
327 println!(" Total messages: {total_messages}");
328 if num_dangling > 0 {
329 println!(" Messages not associated with a chat: {num_dangling}");
330 }
331 if messages_in_more_than_one_chat > 0 {
332 println!(
333 " Messages belonging to more than one chat: {messages_in_more_than_one_chat}"
334 );
335 }
336 Ok(())
337 }
338}
339
340impl Cacheable for Message {
342 type K = String;
343 type V = HashMap<usize, Vec<Self>>;
344 fn cache(db: &Connection) -> Result<HashMap<Self::K, Self::V>, TableError> {
359 let mut map: HashMap<Self::K, Self::V> = HashMap::new();
361
362 let statement = db.prepare(&format!(
364 "SELECT
365 {COLS},
366 c.chat_id,
367 (SELECT COUNT(*) FROM {MESSAGE_ATTACHMENT_JOIN} a WHERE m.ROWID = a.message_id) as num_attachments,
368 NULL as deleted_from,
369 0 as num_replies
370 FROM
371 {MESSAGE} as m
372 LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
373 WHERE m.associated_message_guid IS NOT NULL
374 "
375 )).or_else(|_| db.prepare(&format!(
376 "SELECT
377 *,
378 c.chat_id,
379 (SELECT COUNT(*) FROM {MESSAGE_ATTACHMENT_JOIN} a WHERE m.ROWID = a.message_id) as num_attachments,
380 NULL as deleted_from,
381 0 as num_replies
382 FROM
383 {MESSAGE} as m
384 LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
385 WHERE m.associated_message_guid IS NOT NULL
386 "
387 )));
388
389 if let Ok(mut statement) = statement {
390 let messages = statement.query_map([], |row| Ok(Message::from_row(row)))?;
392
393 for message in messages {
395 let message = Self::extract(message)?;
396 if message.is_tapback()
397 && let Some((idx, tapback_target_guid)) = message.clean_associated_guid()
398 {
399 map.entry(tapback_target_guid.to_string())
400 .or_insert_with(HashMap::new)
401 .entry(idx)
402 .or_insert_with(Vec::new)
403 .push(message);
404 }
405 }
406 }
407
408 Ok(map)
409 }
410}
411
412impl Message {
414 fn from_row_idx(row: &Row) -> Result<Message> {
416 Ok(Message {
417 rowid: row.get(0)?,
418 guid: row.get(1)?,
419 text: row.get(2).unwrap_or(None),
420 service: row.get(3).unwrap_or(None),
421 handle_id: row.get(4).unwrap_or(None),
422 destination_caller_id: row.get(5).unwrap_or(None),
423 subject: row.get(6).unwrap_or(None),
424 date: row.get(7)?,
425 date_read: row.get(8).unwrap_or(0),
426 date_delivered: row.get(9).unwrap_or(0),
427 is_from_me: row.get(10)?,
428 is_read: row.get(11).unwrap_or(false),
429 item_type: row.get(12).unwrap_or_default(),
430 other_handle: row.get(13).unwrap_or(None),
431 share_status: row.get(14).unwrap_or(false),
432 share_direction: row.get(15).unwrap_or(None),
433 group_title: row.get(16).unwrap_or(None),
434 group_action_type: row.get(17).unwrap_or(0),
435 associated_message_guid: row.get(18).unwrap_or(None),
436 associated_message_type: row.get(19).unwrap_or(None),
437 balloon_bundle_id: row.get(20).unwrap_or(None),
438 expressive_send_style_id: row.get(21).unwrap_or(None),
439 thread_originator_guid: row.get(22).unwrap_or(None),
440 thread_originator_part: row.get(23).unwrap_or(None),
441 date_edited: row.get(24).unwrap_or(0),
442 associated_message_emoji: row.get(25).unwrap_or(None),
443 chat_id: row.get(26).unwrap_or(None),
444 num_attachments: row.get(27)?,
445 deleted_from: row.get(28).unwrap_or(None),
446 num_replies: row.get(29)?,
447 components: vec![],
448 edited_parts: None,
449 })
450 }
451
452 fn from_row_named(row: &Row) -> Result<Message> {
454 Ok(Message {
455 rowid: row.get("rowid")?,
456 guid: row.get("guid")?,
457 text: row.get("text").unwrap_or(None),
458 service: row.get("service").unwrap_or(None),
459 handle_id: row.get("handle_id").unwrap_or(None),
460 destination_caller_id: row.get("destination_caller_id").unwrap_or(None),
461 subject: row.get("subject").unwrap_or(None),
462 date: row.get("date")?,
463 date_read: row.get("date_read").unwrap_or(0),
464 date_delivered: row.get("date_delivered").unwrap_or(0),
465 is_from_me: row.get("is_from_me")?,
466 is_read: row.get("is_read").unwrap_or(false),
467 item_type: row.get("item_type").unwrap_or_default(),
468 other_handle: row.get("other_handle").unwrap_or(None),
469 share_status: row.get("share_status").unwrap_or(false),
470 share_direction: row.get("share_direction").unwrap_or(None),
471 group_title: row.get("group_title").unwrap_or(None),
472 group_action_type: row.get("group_action_type").unwrap_or(0),
473 associated_message_guid: row.get("associated_message_guid").unwrap_or(None),
474 associated_message_type: row.get("associated_message_type").unwrap_or(None),
475 balloon_bundle_id: row.get("balloon_bundle_id").unwrap_or(None),
476 expressive_send_style_id: row.get("expressive_send_style_id").unwrap_or(None),
477 thread_originator_guid: row.get("thread_originator_guid").unwrap_or(None),
478 thread_originator_part: row.get("thread_originator_part").unwrap_or(None),
479 date_edited: row.get("date_edited").unwrap_or(0),
480 associated_message_emoji: row.get("associated_message_emoji").unwrap_or(None),
481 chat_id: row.get("chat_id").unwrap_or(None),
482 num_attachments: row.get("num_attachments")?,
483 deleted_from: row.get("deleted_from").unwrap_or(None),
484 num_replies: row.get("num_replies")?,
485 components: vec![],
486 edited_parts: None,
487 })
488 }
489
490 pub fn generate_text<'a>(&'a mut self, db: &'a Connection) -> Result<&'a str, MessageError> {
493 self.edited_parts = self
495 .is_edited()
496 .then(|| self.message_summary_info(db))
497 .flatten()
498 .as_ref()
499 .and_then(|payload| EditedMessage::from_map(payload).ok());
500
501 if let Some(body) = self.attributed_body(db) {
503 let mut typedstream = TypedStreamDeserializer::new(&body);
505 let parsed =
506 parse_body_typedstream(typedstream.iter_root().ok(), self.edited_parts.as_ref());
507
508 if let Some(parsed) = parsed {
509 let is_single_url = match &parsed.components[..] {
511 [BubbleComponent::Text(text_attrs)] => match &text_attrs[..] {
512 [TextAttributes { effects, .. }] => {
513 matches!(&effects[..], [TextEffect::Link(_)])
514 }
515 _ => false,
516 },
517 _ => false,
518 };
519
520 self.text = parsed.text;
521
522 if self.balloon_bundle_id.is_some() {
525 self.components = vec![BubbleComponent::App];
526 } else if is_single_url
527 && self.has_blob(db, MESSAGE, MESSAGE_PAYLOAD, self.rowid.into())
528 {
529 self.balloon_bundle_id =
534 Some("com.apple.messages.URLBalloonProvider".to_string());
535 self.components = vec![BubbleComponent::App];
536 } else {
537 self.components = parsed.components;
538 }
539 }
540
541 if self.text.is_none() {
543 self.text = Some(streamtyped::parse(body)?);
544
545 if self.components.is_empty() {
547 self.components = parse_body_legacy(&self.text);
548 }
549 }
550 }
551
552 if let Some(t) = &self.text {
553 Ok(t)
554 } else {
555 Err(MessageError::NoText)
556 }
557 }
558
559 pub fn generate_text_legacy<'a>(
564 &'a mut self,
565 db: &'a Connection,
566 ) -> Result<&'a str, MessageError> {
567 if self.text.is_none()
569 && let Some(body) = self.attributed_body(db)
570 {
571 self.text = Some(streamtyped::parse(body)?);
572 }
573
574 if self.components.is_empty() {
576 self.components = parse_body_legacy(&self.text);
577 }
578
579 self.text.as_deref().ok_or(MessageError::NoText)
580 }
581
582 pub fn date(&self, offset: &i64) -> Result<DateTime<Local>, MessageError> {
589 get_local_time(&self.date, offset)
590 }
591
592 pub fn date_delivered(&self, offset: &i64) -> Result<DateTime<Local>, MessageError> {
598 get_local_time(&self.date_delivered, offset)
599 }
600
601 pub fn date_read(&self, offset: &i64) -> Result<DateTime<Local>, MessageError> {
607 get_local_time(&self.date_read, offset)
608 }
609
610 pub fn date_edited(&self, offset: &i64) -> Result<DateTime<Local>, MessageError> {
616 get_local_time(&self.date_edited, offset)
617 }
618
619 #[must_use]
633 pub fn time_until_read(&self, offset: &i64) -> Option<String> {
634 if !self.is_from_me && self.date_read != 0 && self.date != 0 {
636 return readable_diff(self.date(offset), self.date_read(offset));
637 }
638 else if self.is_from_me && self.date_delivered != 0 && self.date != 0 {
640 return readable_diff(self.date(offset), self.date_delivered(offset));
641 }
642 None
643 }
644
645 #[must_use]
648 pub fn is_reply(&self) -> bool {
649 self.thread_originator_guid.is_some()
650 }
651
652 #[must_use]
654 pub fn is_announcement(&self) -> bool {
655 self.get_announcement().is_some()
656 }
657
658 #[must_use]
660 pub fn is_tapback(&self) -> bool {
661 matches!(self.variant(), Variant::Tapback(..))
662 }
663
664 #[must_use]
666 pub fn is_expressive(&self) -> bool {
667 self.expressive_send_style_id.is_some()
668 }
669
670 #[must_use]
672 pub fn is_url(&self) -> bool {
673 matches!(self.variant(), Variant::App(CustomBalloon::URL))
674 }
675
676 #[must_use]
678 pub fn is_handwriting(&self) -> bool {
679 matches!(self.variant(), Variant::App(CustomBalloon::Handwriting))
680 }
681
682 #[must_use]
684 pub fn is_digital_touch(&self) -> bool {
685 matches!(self.variant(), Variant::App(CustomBalloon::DigitalTouch))
686 }
687
688 #[must_use]
690 pub fn is_poll(&self) -> bool {
691 matches!(self.variant(), Variant::App(CustomBalloon::Polls))
692 }
693
694 #[must_use]
696 pub fn is_poll_vote(&self) -> bool {
697 self.associated_message_type == Some(4000)
698 }
699
700 #[must_use]
702 pub fn is_poll_update(&self) -> bool {
703 matches!(self.variant(), Variant::PollUpdate)
704 }
705
706 #[must_use]
708 pub fn is_edited(&self) -> bool {
709 self.date_edited != 0
710 }
711
712 #[must_use]
714 pub fn is_part_edited(&self, index: usize) -> bool {
715 if let Some(edited_parts) = &self.edited_parts
716 && let Some(part) = edited_parts.part(index)
717 {
718 return matches!(part.status, EditStatus::Edited);
719 }
720 false
721 }
722
723 #[must_use]
725 pub fn is_fully_unsent(&self) -> bool {
726 self.edited_parts.as_ref().is_some_and(|ep| {
727 ep.parts
728 .iter()
729 .all(|part| matches!(part.status, EditStatus::Unsent))
730 })
731 }
732
733 #[must_use]
737 pub fn has_attachments(&self) -> bool {
738 self.num_attachments > 0
739 }
740
741 #[must_use]
743 pub fn has_replies(&self) -> bool {
744 self.num_replies > 0
745 }
746
747 #[must_use]
749 pub fn is_kept_audio_message(&self) -> bool {
750 self.item_type == 5
751 }
752
753 #[must_use]
755 pub fn is_shareplay(&self) -> bool {
756 self.item_type == 6
757 }
758
759 #[must_use]
761 pub fn is_from_me(&self) -> bool {
762 if self.item_type == 4
765 && let (Some(other_handle), Some(share_direction)) =
766 (self.other_handle, self.share_direction)
767 {
768 self.is_from_me || other_handle != 0 && !share_direction
769 } else {
770 self.is_from_me
771 }
772 }
773
774 #[must_use]
776 pub fn started_sharing_location(&self) -> bool {
777 self.item_type == 4 && self.group_action_type == 0 && !self.share_status
778 }
779
780 #[must_use]
782 pub fn stopped_sharing_location(&self) -> bool {
783 self.item_type == 4 && self.group_action_type == 0 && self.share_status
784 }
785
786 #[must_use]
798 pub fn is_deleted(&self) -> bool {
799 self.deleted_from.is_some()
800 }
801
802 pub fn has_translation(&self, db: &Connection) -> bool {
804 let query = format!(
807 "SELECT ROWID FROM {MESSAGE}
808 WHERE message_summary_info IS NOT NULL
809 AND length(message_summary_info) > 61
810 AND instr(message_summary_info, X'7472616E736C6174696F6E4C616E6775616765') > 0
811 AND instr(message_summary_info, X'7472616E736C6174656454657874') > 0
812 AND ROWID = ?"
813 );
814 if let Ok(mut statement) = db.prepare_cached(&query) {
815 let result: Result<i32, _> = statement.query_row([self.rowid], |row| row.get(0));
816 result.is_ok()
817 } else {
818 false
819 }
820 }
821
822 pub fn get_translation(&self, db: &Connection) -> Result<Option<Translation>, MessageError> {
824 if let Some(payload) = self.message_summary_info(db) {
825 return Ok(Some(Translation::from_payload(&payload)?));
826 }
827 Ok(None)
828 }
829
830 pub fn cache_translations(db: &Connection) -> Result<HashSet<String>, TableError> {
832 let query = format!(
835 "SELECT guid FROM {MESSAGE}
836 WHERE message_summary_info IS NOT NULL
837 AND length(message_summary_info) > 61
838 AND instr(message_summary_info, X'7472616E736C6174696F6E4C616E6775616765') > 0
839 AND instr(message_summary_info, X'7472616E736C6174656454657874') > 0"
840 );
841
842 let mut statement = db.prepare(&query)?;
843 let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
844
845 let mut guids = HashSet::new();
846 for guid_result in rows {
847 guids.insert(guid_result?);
848 }
849
850 Ok(guids)
851 }
852
853 #[must_use]
855 pub fn group_action(&'_ self) -> Option<GroupAction<'_>> {
856 GroupAction::from_message(self)
857 }
858
859 fn get_reply_index(&self) -> usize {
861 if let Some(parts) = &self.thread_originator_part {
862 return match parts.split(':').next() {
863 Some(part) => str::parse::<usize>(part).unwrap_or(0),
864 None => 0,
865 };
866 }
867 0
868 }
869
870 pub(crate) fn generate_filter_statement(
877 context: &QueryContext,
878 include_recoverable: bool,
879 ) -> String {
880 let mut filters = String::with_capacity(128);
881
882 if let Some(start) = context.start {
884 let _ = write!(filters, " m.date >= {start}");
885 }
886
887 if let Some(end) = context.end {
889 if !filters.is_empty() {
890 filters.push_str(" AND ");
891 }
892 let _ = write!(filters, " m.date <= {end}");
893 }
894
895 if let Some(chat_ids) = &context.selected_chat_ids {
897 if !filters.is_empty() {
898 filters.push_str(" AND ");
899 }
900
901 let ids = chat_ids
903 .iter()
904 .map(std::string::ToString::to_string)
905 .collect::<Vec<String>>()
906 .join(", ");
907
908 if include_recoverable {
909 let _ = write!(filters, " (c.chat_id IN ({ids}) OR d.chat_id IN ({ids}))");
910 } else {
911 let _ = write!(filters, " c.chat_id IN ({ids})");
912 }
913 }
914
915 if !filters.is_empty() {
916 return format!("WHERE {filters}");
917 }
918 filters
919 }
920
921 pub fn get_count(db: &Connection, context: &QueryContext) -> Result<i64, TableError> {
937 let mut statement = if context.has_filters() {
938 db.prepare_cached(&format!(
939 "SELECT
940 COUNT(*)
941 FROM {MESSAGE} as m
942 LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
943 LEFT JOIN {RECENTLY_DELETED} as d ON m.ROWID = d.message_id
944 {}",
945 Self::generate_filter_statement(context, true)
946 ))
947 .or_else(|_| {
948 db.prepare_cached(&format!(
949 "SELECT
950 COUNT(*)
951 FROM {MESSAGE} as m
952 LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
953 {}",
954 Self::generate_filter_statement(context, false)
955 ))
956 })?
957 } else {
958 db.prepare_cached(&format!("SELECT COUNT(*) FROM {MESSAGE}"))?
959 };
960 let count: i64 = statement.query_row([], |r| r.get(0)).unwrap_or(0);
962
963 Ok(count)
964 }
965
966 pub fn stream_rows<'a>(
987 db: &'a Connection,
988 context: &'a QueryContext,
989 ) -> Result<CachedStatement<'a>, TableError> {
990 if !context.has_filters() {
991 return Self::get(db);
992 }
993 Ok(db
994 .prepare_cached(&ios_16_newer_query(Some(&Self::generate_filter_statement(
995 context, true,
996 ))))
997 .or_else(|_| {
998 db.prepare_cached(&ios_14_15_query(Some(&Self::generate_filter_statement(
999 context, false,
1000 ))))
1001 })
1002 .or_else(|_| {
1003 db.prepare_cached(&ios_13_older_query(Some(&Self::generate_filter_statement(
1004 context, false,
1005 ))))
1006 })?)
1007 }
1008
1009 #[must_use]
1013 pub fn clean_associated_guid(&self) -> Option<(usize, &str)> {
1014 if let Some(guid) = &self.associated_message_guid {
1015 if guid.starts_with("p:") {
1016 let mut split = guid.split('/');
1017 let index_str = split.next()?;
1018 let message_id = split.next()?;
1019 let index = str::parse::<usize>(&index_str.replace("p:", "")).unwrap_or(0);
1020 return Some((index, message_id.get(0..36)?));
1021 } else if guid.starts_with("bp:") {
1022 return Some((0, guid.get(3..39)?));
1023 }
1024
1025 return Some((0, guid.get(0..36)?));
1026 }
1027 None
1028 }
1029
1030 fn tapback_index(&self) -> usize {
1032 match self.clean_associated_guid() {
1033 Some((x, _)) => x,
1034 None => 0,
1035 }
1036 }
1037
1038 pub fn get_replies(&self, db: &Connection) -> Result<HashMap<usize, Vec<Self>>, TableError> {
1040 let mut out_h: HashMap<usize, Vec<Self>> = HashMap::new();
1041
1042 if self.has_replies() {
1044 let filters = "WHERE m.thread_originator_guid = ?1";
1046
1047 let mut statement = db
1049 .prepare_cached(&ios_16_newer_query(Some(filters)))
1050 .or_else(|_| db.prepare_cached(&ios_14_15_query(Some(filters))))?;
1051
1052 let iter =
1053 statement.query_map([self.guid.as_str()], |row| Ok(Message::from_row(row)))?;
1054
1055 for message in iter {
1056 let m = Message::extract(message)?;
1057 let idx = m.get_reply_index();
1058 match out_h.get_mut(&idx) {
1059 Some(body_part) => body_part.push(m),
1060 None => {
1061 out_h.insert(idx, vec![m]);
1062 }
1063 }
1064 }
1065 }
1066
1067 Ok(out_h)
1068 }
1069
1070 pub fn get_votes(&self, db: &Connection) -> Result<Vec<Self>, TableError> {
1073 let mut out_v: Vec<Self> = Vec::new();
1074
1075 if self.is_poll() {
1077 let filters = "WHERE m.associated_message_guid = ?1";
1079
1080 let mut statement = db
1082 .prepare_cached(&ios_16_newer_query(Some(filters)))
1083 .or_else(|_| db.prepare_cached(&ios_14_15_query(Some(filters))))?;
1084
1085 let iter =
1086 statement.query_map([self.guid.as_str()], |row| Ok(Message::from_row(row)))?;
1087
1088 for message in iter {
1089 let m = Message::extract(message)?;
1090 out_v.push(m);
1091 }
1092 }
1093
1094 Ok(out_v)
1095 }
1096
1097 pub fn as_poll(&self, db: &Connection) -> Result<Option<Poll>, MessageError> {
1099 if self.is_poll()
1100 && let Some(payload) = self.payload_data(db)
1101 {
1102 let mut poll = Poll::from_payload(&payload)?;
1103
1104 let votes = self.get_votes(db).unwrap_or_default();
1106
1107 for vote in votes.iter().rev() {
1110 if !vote.is_poll_vote()
1113 && let Some(vote_payload) = vote.payload_data(db)
1114 && let Ok(update) = Poll::from_payload(&vote_payload)
1115 {
1116 poll = update;
1117 break;
1118 }
1119 }
1120
1121 for vote in &votes {
1123 if vote.is_poll_vote()
1124 && let Some(vote_payload) = vote.payload_data(db)
1125 {
1126 poll.count_votes(&vote_payload)?;
1127 }
1128 }
1129
1130 return Ok(Some(poll));
1132 }
1133
1134 Ok(None)
1135 }
1136
1137 #[must_use]
1140 pub fn variant(&'_ self) -> Variant<'_> {
1141 if self.is_edited() {
1143 return Variant::Edited;
1144 }
1145
1146 if let Some(associated_message_type) = self.associated_message_type {
1148 match associated_message_type {
1149 0 | 2 | 3 => return self.get_app_variant().unwrap_or(Variant::Normal),
1151 1000 | 2000..=2007 | 3000..=3007 => {
1153 if let Some((action, tapback)) = self.get_tapback() {
1154 return Variant::Tapback(self.tapback_index(), action, tapback);
1155 }
1156 }
1157 4000 => return Variant::Vote,
1159 x => return Variant::Unknown(x),
1161 }
1162 }
1163
1164 if self.is_shareplay() {
1166 return Variant::SharePlay;
1167 }
1168
1169 Variant::Normal
1170 }
1171
1172 #[must_use]
1174 fn get_app_variant(&self) -> Option<Variant<'_>> {
1175 let bundle_id = parse_balloon_bundle_id(self.balloon_bundle_id.as_deref())?;
1176 let custom = match bundle_id {
1177 "com.apple.messages.URLBalloonProvider" => CustomBalloon::URL,
1178 "com.apple.Handwriting.HandwritingProvider" => CustomBalloon::Handwriting,
1179 "com.apple.DigitalTouchBalloonProvider" => CustomBalloon::DigitalTouch,
1180 "com.apple.PassbookUIService.PeerPaymentMessagesExtension" => CustomBalloon::ApplePay,
1181 "com.apple.ActivityMessagesApp.MessagesExtension" => CustomBalloon::Fitness,
1182 "com.apple.mobileslideshow.PhotosMessagesApp" => CustomBalloon::Slideshow,
1183 "com.apple.SafetyMonitorApp.SafetyMonitorMessages" => CustomBalloon::CheckIn,
1184 "com.apple.findmy.FindMyMessagesApp" => CustomBalloon::FindMy,
1185 "com.apple.messages.Polls" => {
1186 if self.associated_message_guid.as_ref() == Some(&self.guid) {
1188 CustomBalloon::Polls
1189 } else {
1190 return Some(Variant::PollUpdate);
1191 }
1192 }
1193 _ => CustomBalloon::Application(bundle_id),
1194 };
1195 Some(Variant::App(custom))
1196 }
1197
1198 #[must_use]
1200 fn get_tapback(&self) -> Option<(TapbackAction, Tapback<'_>)> {
1201 match self.associated_message_type? {
1202 1000 => Some((TapbackAction::Added, Tapback::Sticker)),
1203 2000 => Some((TapbackAction::Added, Tapback::Loved)),
1204 2001 => Some((TapbackAction::Added, Tapback::Liked)),
1205 2002 => Some((TapbackAction::Added, Tapback::Disliked)),
1206 2003 => Some((TapbackAction::Added, Tapback::Laughed)),
1207 2004 => Some((TapbackAction::Added, Tapback::Emphasized)),
1208 2005 => Some((TapbackAction::Added, Tapback::Questioned)),
1209 2006 => Some((
1210 TapbackAction::Added,
1211 Tapback::Emoji(self.associated_message_emoji.as_deref()),
1212 )),
1213 2007 => Some((TapbackAction::Added, Tapback::Sticker)),
1214 3000 => Some((TapbackAction::Removed, Tapback::Loved)),
1215 3001 => Some((TapbackAction::Removed, Tapback::Liked)),
1216 3002 => Some((TapbackAction::Removed, Tapback::Disliked)),
1217 3003 => Some((TapbackAction::Removed, Tapback::Laughed)),
1218 3004 => Some((TapbackAction::Removed, Tapback::Emphasized)),
1219 3005 => Some((TapbackAction::Removed, Tapback::Questioned)),
1220 3006 => Some((
1221 TapbackAction::Removed,
1222 Tapback::Emoji(self.associated_message_emoji.as_deref()),
1223 )),
1224 3007 => Some((TapbackAction::Removed, Tapback::Sticker)),
1225 _ => None,
1226 }
1227 }
1228
1229 #[must_use]
1231 pub fn get_announcement(&'_ self) -> Option<Announcement<'_>> {
1232 if let Some(action) = self.group_action() {
1233 return Some(Announcement::GroupAction(action));
1234 }
1235
1236 if self.is_fully_unsent() {
1237 return Some(Announcement::FullyUnsent);
1238 }
1239
1240 if self.is_kept_audio_message() {
1241 return Some(Announcement::AudioMessageKept);
1242 }
1243
1244 None
1245 }
1246
1247 #[must_use]
1249 pub fn service(&'_ self) -> Service<'_> {
1250 Service::from(self.service.as_deref())
1251 }
1252
1253 pub fn payload_data(&self, db: &Connection) -> Option<Value> {
1262 Value::from_reader(self.get_blob(db, MESSAGE, MESSAGE_PAYLOAD, self.rowid.into())?).ok()
1263 }
1264
1265 pub fn raw_payload_data(&self, db: &Connection) -> Option<Vec<u8>> {
1272 let mut buf = Vec::new();
1273 self.get_blob(db, MESSAGE, MESSAGE_PAYLOAD, self.rowid.into())?
1274 .read_to_end(&mut buf)
1275 .ok()?;
1276 Some(buf)
1277 }
1278
1279 pub fn message_summary_info(&self, db: &Connection) -> Option<Value> {
1286 Value::from_reader(self.get_blob(db, MESSAGE, MESSAGE_SUMMARY_INFO, self.rowid.into())?)
1287 .ok()
1288 }
1289
1290 pub fn attributed_body(&self, db: &Connection) -> Option<Vec<u8>> {
1297 let mut body = vec![];
1298 self.get_blob(db, MESSAGE, ATTRIBUTED_BODY, self.rowid.into())?
1299 .read_to_end(&mut body)
1300 .ok();
1301 Some(body)
1302 }
1303
1304 #[must_use]
1307 pub fn get_expressive(&'_ self) -> Expressive<'_> {
1308 match &self.expressive_send_style_id {
1309 Some(content) => match content.as_str() {
1310 "com.apple.MobileSMS.expressivesend.gentle" => {
1311 Expressive::Bubble(BubbleEffect::Gentle)
1312 }
1313 "com.apple.MobileSMS.expressivesend.impact" => {
1314 Expressive::Bubble(BubbleEffect::Slam)
1315 }
1316 "com.apple.MobileSMS.expressivesend.invisibleink" => {
1317 Expressive::Bubble(BubbleEffect::InvisibleInk)
1318 }
1319 "com.apple.MobileSMS.expressivesend.loud" => Expressive::Bubble(BubbleEffect::Loud),
1320 "com.apple.messages.effect.CKConfettiEffect" => {
1321 Expressive::Screen(ScreenEffect::Confetti)
1322 }
1323 "com.apple.messages.effect.CKEchoEffect" => Expressive::Screen(ScreenEffect::Echo),
1324 "com.apple.messages.effect.CKFireworksEffect" => {
1325 Expressive::Screen(ScreenEffect::Fireworks)
1326 }
1327 "com.apple.messages.effect.CKHappyBirthdayEffect" => {
1328 Expressive::Screen(ScreenEffect::Balloons)
1329 }
1330 "com.apple.messages.effect.CKHeartEffect" => {
1331 Expressive::Screen(ScreenEffect::Heart)
1332 }
1333 "com.apple.messages.effect.CKLasersEffect" => {
1334 Expressive::Screen(ScreenEffect::Lasers)
1335 }
1336 "com.apple.messages.effect.CKShootingStarEffect" => {
1337 Expressive::Screen(ScreenEffect::ShootingStar)
1338 }
1339 "com.apple.messages.effect.CKSparklesEffect" => {
1340 Expressive::Screen(ScreenEffect::Sparkles)
1341 }
1342 "com.apple.messages.effect.CKSpotlightEffect" => {
1343 Expressive::Screen(ScreenEffect::Spotlight)
1344 }
1345 _ => Expressive::Unknown(content),
1346 },
1347 None => Expressive::None,
1348 }
1349 }
1350
1351 pub fn from_guid(guid: &str, db: &Connection) -> Result<Self, TableError> {
1372 let filters = format!("WHERE m.guid = \"{guid}\"");
1375
1376 let mut statement = db
1377 .prepare(&ios_16_newer_query(Some(&filters)))
1378 .or_else(|_| db.prepare(&ios_14_15_query(Some(&filters))))
1379 .or_else(|_| db.prepare(&ios_13_older_query(Some(&filters))))?;
1380
1381 Message::extract(statement.query_row([], |row| Ok(Message::from_row(row))))
1382 }
1383}
1384
1385#[cfg(test)]
1387impl Message {
1388 #[must_use]
1389 pub fn blank() -> Message {
1391 use std::vec;
1392
1393 Message {
1394 rowid: i32::default(),
1395 guid: String::default(),
1396 text: None,
1397 service: Some("iMessage".to_string()),
1398 handle_id: Some(i32::default()),
1399 destination_caller_id: None,
1400 subject: None,
1401 date: i64::default(),
1402 date_read: i64::default(),
1403 date_delivered: i64::default(),
1404 is_from_me: false,
1405 is_read: false,
1406 item_type: 0,
1407 other_handle: None,
1408 share_status: false,
1409 share_direction: None,
1410 group_title: None,
1411 group_action_type: 0,
1412 associated_message_guid: None,
1413 associated_message_type: None,
1414 balloon_bundle_id: None,
1415 expressive_send_style_id: None,
1416 thread_originator_guid: None,
1417 thread_originator_part: None,
1418 date_edited: 0,
1419 associated_message_emoji: None,
1420 chat_id: None,
1421 num_attachments: 0,
1422 deleted_from: None,
1423 num_replies: 0,
1424 components: vec![],
1425 edited_parts: None,
1426 }
1427 }
1428}