use std::{
collections::{HashMap, HashSet},
fmt::Write,
io::Read,
};
use chrono::{DateTime, offset::Local};
use crabstep::TypedStreamDeserializer;
use plist::Value;
use rusqlite::{CachedStatement, Connection, Result, Row};
use crate::{
error::{message::MessageError, table::TableError},
message_types::{
edited::{EditStatus, EditedMessage},
expressives::{BubbleEffect, Expressive, ScreenEffect},
polls::Poll,
text_effects::TextEffect,
translation::Translation,
variants::{Announcement, BalloonProvider, CustomBalloon, Tapback, TapbackAction, Variant},
},
tables::{
diagnostic::MessageDiagnostic,
messages::{
body::{parse_body_legacy, parse_body_typedstream},
models::{BubbleComponent, GroupAction, Service, TextAttributes},
query_parts::{ios_13_older_query, ios_14_15_query, ios_16_newer_query},
},
table::{
ATTRIBUTED_BODY, CHAT_MESSAGE_JOIN, Cacheable, MESSAGE, MESSAGE_ATTACHMENT_JOIN,
MESSAGE_PAYLOAD, MESSAGE_SUMMARY_INFO, RECENTLY_DELETED, Table,
},
},
util::{
bundle_id::parse_balloon_bundle_id,
dates::{get_local_time, readable_diff},
query_context::QueryContext,
streamtyped,
},
};
pub(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";
#[derive(Debug)]
#[allow(non_snake_case)]
pub struct Message {
pub rowid: i32,
pub guid: String,
pub text: Option<String>,
pub service: Option<String>,
pub handle_id: Option<i32>,
pub destination_caller_id: Option<String>,
pub subject: Option<String>,
pub date: i64,
pub date_read: i64,
pub date_delivered: i64,
pub is_from_me: bool,
pub is_read: bool,
pub item_type: i32,
pub other_handle: Option<i32>,
pub share_status: bool,
pub share_direction: Option<bool>,
pub group_title: Option<String>,
pub group_action_type: i32,
pub associated_message_guid: Option<String>,
pub associated_message_type: Option<i32>,
pub balloon_bundle_id: Option<String>,
pub expressive_send_style_id: Option<String>,
pub thread_originator_guid: Option<String>,
pub thread_originator_part: Option<String>,
pub date_edited: i64,
pub associated_message_emoji: Option<String>,
pub chat_id: Option<i32>,
pub num_attachments: i32,
pub deleted_from: Option<i32>,
pub num_replies: i32,
pub components: Vec<BubbleComponent>,
pub edited_parts: Option<EditedMessage>,
}
#[derive(Debug)]
#[must_use]
pub struct ParsedBody {
pub text: Option<String>,
pub components: Vec<BubbleComponent>,
pub edited_parts: Option<EditedMessage>,
pub balloon_bundle_id: Option<String>,
}
impl Table for Message {
fn from_row(row: &Row) -> Result<Message> {
Self::from_row_idx(row).or_else(|_| Self::from_row_named(row))
}
fn get(db: &'_ Connection) -> Result<CachedStatement<'_>, TableError> {
Ok(db
.prepare_cached(&ios_16_newer_query(None))
.or_else(|_| db.prepare_cached(&ios_14_15_query(None)))
.or_else(|_| db.prepare_cached(&ios_13_older_query(None)))?)
}
}
impl Message {
pub fn run_diagnostic(db: &Connection) -> Result<MessageDiagnostic, TableError> {
let mut messages_without_chat = db.prepare(&format!(
"
SELECT
COUNT(m.rowid)
FROM
{MESSAGE} as m
LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.rowid = c.message_id
WHERE
c.chat_id is NULL
ORDER BY
m.date
"
))?;
let messages_without_chat = messages_without_chat
.query_row([], |r| r.get::<_, i64>(0))
.ok()
.and_then(|count| usize::try_from(count).ok())
.unwrap_or(0);
let mut messages_in_more_than_one_chat_q = db.prepare(&format!(
"
SELECT
COUNT(*)
FROM (
SELECT DISTINCT
message_id
, COUNT(chat_id) AS c
FROM {CHAT_MESSAGE_JOIN}
GROUP BY
message_id
HAVING c > 1);
"
))?;
let messages_in_multiple_chats = messages_in_more_than_one_chat_q
.query_row([], |r| r.get::<_, i64>(0))
.ok()
.and_then(|count| usize::try_from(count).ok())
.unwrap_or(0);
let mut messages_count = db.prepare(&format!(
"
SELECT
COUNT(rowid)
FROM
{MESSAGE}
"
))?;
let total_messages = messages_count
.query_row([], |r| r.get::<_, i64>(0))
.ok()
.and_then(|count| usize::try_from(count).ok())
.unwrap_or(0);
let recoverable_messages = db
.prepare(&format!("SELECT COUNT(*) FROM {RECENTLY_DELETED}"))
.and_then(|mut s| s.query_row([], |r| r.get::<_, i64>(0)))
.ok()
.and_then(|count| usize::try_from(count).ok())
.unwrap_or(0);
let mut date_range = db.prepare(&format!("SELECT MIN(date), MAX(date) FROM {MESSAGE}"))?;
let (first_message_date, last_message_date): (Option<i64>, Option<i64>) = date_range
.query_row([], |r| Ok((r.get(0).ok(), r.get(1).ok())))
.unwrap_or((None, None));
Ok(MessageDiagnostic {
total_messages,
messages_without_chat,
messages_in_multiple_chats,
recoverable_messages,
first_message_date,
last_message_date,
})
}
}
impl Cacheable for Message {
type K = String;
type V = HashMap<usize, Vec<Self>>;
fn cache(db: &Connection) -> Result<HashMap<Self::K, Self::V>, TableError> {
let mut map: HashMap<Self::K, Self::V> = HashMap::new();
let statement = db.prepare(&format!(
"SELECT
{COLS},
c.chat_id,
(SELECT COUNT(*) FROM {MESSAGE_ATTACHMENT_JOIN} a WHERE m.ROWID = a.message_id) as num_attachments,
NULL as deleted_from,
0 as num_replies
FROM
{MESSAGE} as m
LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
WHERE m.associated_message_guid IS NOT NULL
"
)).or_else(|_| db.prepare(&format!(
"SELECT
*,
c.chat_id,
(SELECT COUNT(*) FROM {MESSAGE_ATTACHMENT_JOIN} a WHERE m.ROWID = a.message_id) as num_attachments,
NULL as deleted_from,
0 as num_replies
FROM
{MESSAGE} as m
LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
WHERE m.associated_message_guid IS NOT NULL
"
)));
if let Ok(mut statement) = statement {
let messages = statement.query_map([], |row| Ok(Message::from_row(row)))?;
for message in messages {
let message = Self::extract(message)?;
if message.is_tapback()
&& let Some((idx, tapback_target_guid)) = message.clean_associated_guid()
{
map.entry(tapback_target_guid.to_string())
.or_insert_with(HashMap::new)
.entry(idx)
.or_insert_with(Vec::new)
.push(message);
}
}
}
Ok(map)
}
}
impl Message {
fn from_row_idx(row: &Row) -> Result<Message> {
Ok(Message {
rowid: row.get(0)?,
guid: row.get(1)?,
text: row.get(2).unwrap_or(None),
service: row.get(3).unwrap_or(None),
handle_id: row.get(4).unwrap_or(None),
destination_caller_id: row.get(5).unwrap_or(None),
subject: row.get(6).unwrap_or(None),
date: row.get(7)?,
date_read: row.get(8).unwrap_or(0),
date_delivered: row.get(9).unwrap_or(0),
is_from_me: row.get(10)?,
is_read: row.get(11).unwrap_or(false),
item_type: row.get(12).unwrap_or_default(),
other_handle: row.get(13).unwrap_or(None),
share_status: row.get(14).unwrap_or(false),
share_direction: row.get(15).unwrap_or(None),
group_title: row.get(16).unwrap_or(None),
group_action_type: row.get(17).unwrap_or(0),
associated_message_guid: row.get(18).unwrap_or(None),
associated_message_type: row.get(19).unwrap_or(None),
balloon_bundle_id: row.get(20).unwrap_or(None),
expressive_send_style_id: row.get(21).unwrap_or(None),
thread_originator_guid: row.get(22).unwrap_or(None),
thread_originator_part: row.get(23).unwrap_or(None),
date_edited: row.get(24).unwrap_or(0),
associated_message_emoji: row.get(25).unwrap_or(None),
chat_id: row.get(26).unwrap_or(None),
num_attachments: row.get(27)?,
deleted_from: row.get(28).unwrap_or(None),
num_replies: row.get(29)?,
components: vec![],
edited_parts: None,
})
}
fn from_row_named(row: &Row) -> Result<Message> {
Ok(Message {
rowid: row.get("rowid")?,
guid: row.get("guid")?,
text: row.get("text").unwrap_or(None),
service: row.get("service").unwrap_or(None),
handle_id: row.get("handle_id").unwrap_or(None),
destination_caller_id: row.get("destination_caller_id").unwrap_or(None),
subject: row.get("subject").unwrap_or(None),
date: row.get("date")?,
date_read: row.get("date_read").unwrap_or(0),
date_delivered: row.get("date_delivered").unwrap_or(0),
is_from_me: row.get("is_from_me")?,
is_read: row.get("is_read").unwrap_or(false),
item_type: row.get("item_type").unwrap_or_default(),
other_handle: row.get("other_handle").unwrap_or(None),
share_status: row.get("share_status").unwrap_or(false),
share_direction: row.get("share_direction").unwrap_or(None),
group_title: row.get("group_title").unwrap_or(None),
group_action_type: row.get("group_action_type").unwrap_or(0),
associated_message_guid: row.get("associated_message_guid").unwrap_or(None),
associated_message_type: row.get("associated_message_type").unwrap_or(None),
balloon_bundle_id: row.get("balloon_bundle_id").unwrap_or(None),
expressive_send_style_id: row.get("expressive_send_style_id").unwrap_or(None),
thread_originator_guid: row.get("thread_originator_guid").unwrap_or(None),
thread_originator_part: row.get("thread_originator_part").unwrap_or(None),
date_edited: row.get("date_edited").unwrap_or(0),
associated_message_emoji: row.get("associated_message_emoji").unwrap_or(None),
chat_id: row.get("chat_id").unwrap_or(None),
num_attachments: row.get("num_attachments")?,
deleted_from: row.get("deleted_from").unwrap_or(None),
num_replies: row.get("num_replies")?,
components: vec![],
edited_parts: None,
})
}
pub fn parse_body(&self, db: &Connection) -> Result<ParsedBody, MessageError> {
let edited_parts = self
.is_edited()
.then(|| self.message_summary_info(db))
.flatten()
.as_ref()
.and_then(|payload| EditedMessage::from_map(payload).ok());
let mut text = None;
let mut components = vec![];
let mut balloon_bundle_id = None;
if let Some(body) = self.attributed_body(db) {
let mut typedstream = TypedStreamDeserializer::new(&body);
match parse_body_typedstream(typedstream.iter_root().ok(), edited_parts.as_ref()) {
Some(parsed) => {
text = parsed.text;
let is_single_url = match &parsed.components[..] {
[BubbleComponent::Text(text_attrs)] => match &text_attrs[..] {
[TextAttributes { effects, .. }] => {
matches!(&effects[..], [TextEffect::Link(_)])
}
_ => false,
},
_ => false,
};
if self.balloon_bundle_id.is_some() {
components = vec![BubbleComponent::App];
} else if is_single_url
&& self.has_blob(db, MESSAGE, MESSAGE_PAYLOAD, self.rowid.into())
{
balloon_bundle_id =
Some("com.apple.messages.URLBalloonProvider".to_string());
components = vec![BubbleComponent::App];
} else {
components = parsed.components;
}
}
None => {
text = self.text.clone();
}
}
if text.is_none() {
text = Some(streamtyped::parse(body)?);
}
}
let text = text.or_else(|| self.text.clone());
let balloon_bundle_id = balloon_bundle_id.or_else(|| self.balloon_bundle_id.clone());
if components.is_empty() && text.is_some() {
components = parse_body_legacy(&text);
}
if text.is_some() || !components.is_empty() || edited_parts.is_some() {
Ok(ParsedBody {
text,
components,
edited_parts,
balloon_bundle_id,
})
} else {
Err(MessageError::NoText)
}
}
pub fn apply_body(&mut self, body: ParsedBody) {
self.text = body.text;
self.components = body.components;
self.edited_parts = body.edited_parts;
self.balloon_bundle_id = body.balloon_bundle_id;
}
pub fn generate_text_legacy<'a>(
&'a mut self,
db: &'a Connection,
) -> Result<&'a str, MessageError> {
if self.text.is_none()
&& let Some(body) = self.attributed_body(db)
{
self.text = Some(streamtyped::parse(body)?);
}
if self.components.is_empty() {
self.components = parse_body_legacy(&self.text);
}
self.text.as_deref().ok_or(MessageError::NoText)
}
pub fn date(&self, offset: i64) -> Result<DateTime<Local>, MessageError> {
get_local_time(self.date, offset)
}
pub fn date_delivered(&self, offset: i64) -> Result<DateTime<Local>, MessageError> {
get_local_time(self.date_delivered, offset)
}
pub fn date_read(&self, offset: i64) -> Result<DateTime<Local>, MessageError> {
get_local_time(self.date_read, offset)
}
pub fn date_edited(&self, offset: i64) -> Result<DateTime<Local>, MessageError> {
get_local_time(self.date_edited, offset)
}
#[must_use]
pub fn time_until_read(&self, offset: i64) -> Option<String> {
if !self.is_from_me && self.date_read != 0 && self.date != 0 {
return readable_diff(&self.date(offset).ok()?, &self.date_read(offset).ok()?);
}
else if self.is_from_me && self.date_delivered != 0 && self.date != 0 {
return readable_diff(&self.date(offset).ok()?, &self.date_delivered(offset).ok()?);
}
None
}
#[must_use]
pub fn is_reply(&self) -> bool {
self.thread_originator_guid.is_some()
}
#[must_use]
pub fn is_announcement(&self) -> bool {
self.get_announcement().is_some()
}
#[must_use]
pub fn is_tapback(&self) -> bool {
matches!(self.variant(), Variant::Tapback(..))
}
#[must_use]
pub fn is_expressive(&self) -> bool {
self.expressive_send_style_id.is_some()
}
#[must_use]
pub fn is_url(&self) -> bool {
matches!(self.variant(), Variant::App(CustomBalloon::URL))
}
#[must_use]
pub fn is_handwriting(&self) -> bool {
matches!(self.variant(), Variant::App(CustomBalloon::Handwriting))
}
#[must_use]
pub fn is_digital_touch(&self) -> bool {
matches!(self.variant(), Variant::App(CustomBalloon::DigitalTouch))
}
#[must_use]
pub fn is_poll(&self) -> bool {
matches!(self.variant(), Variant::App(CustomBalloon::Polls))
}
#[must_use]
pub fn is_poll_vote(&self) -> bool {
self.associated_message_type == Some(4000)
}
#[must_use]
pub fn is_poll_update(&self) -> bool {
matches!(self.variant(), Variant::PollUpdate)
}
#[must_use]
pub fn is_edited(&self) -> bool {
self.date_edited != 0
}
#[must_use]
pub fn is_part_edited(&self, index: usize) -> bool {
if let Some(edited_parts) = &self.edited_parts
&& let Some(part) = edited_parts.part(index)
{
return matches!(part.status, EditStatus::Edited);
}
false
}
#[must_use]
pub fn is_fully_unsent(&self) -> bool {
self.edited_parts.as_ref().is_some_and(|ep| {
ep.parts
.iter()
.all(|part| matches!(part.status, EditStatus::Unsent))
})
}
#[must_use]
pub fn has_attachments(&self) -> bool {
self.num_attachments > 0
}
#[must_use]
pub fn has_replies(&self) -> bool {
self.num_replies > 0
}
#[must_use]
pub fn is_kept_audio_message(&self) -> bool {
self.item_type == 5
}
#[must_use]
pub fn is_shareplay(&self) -> bool {
self.item_type == 6
}
#[must_use]
pub fn is_from_me(&self) -> bool {
if self.item_type == 4
&& let (Some(other_handle), Some(share_direction)) =
(self.other_handle, self.share_direction)
{
self.is_from_me || other_handle != 0 && !share_direction
} else {
self.is_from_me
}
}
#[must_use]
pub fn started_sharing_location(&self) -> bool {
self.item_type == 4 && self.group_action_type == 0 && !self.share_status
}
#[must_use]
pub fn stopped_sharing_location(&self) -> bool {
self.item_type == 4 && self.group_action_type == 0 && self.share_status
}
#[must_use]
pub fn is_deleted(&self) -> bool {
self.deleted_from.is_some()
}
pub fn has_translation(&self, db: &Connection) -> bool {
let query = format!(
"SELECT ROWID FROM {MESSAGE}
WHERE message_summary_info IS NOT NULL
AND length(message_summary_info) > 61
AND instr(message_summary_info, X'7472616E736C6174696F6E4C616E6775616765') > 0
AND instr(message_summary_info, X'7472616E736C6174656454657874') > 0
AND ROWID = ?"
);
if let Ok(mut statement) = db.prepare_cached(&query) {
let result: Result<i32, _> = statement.query_row([self.rowid], |row| row.get(0));
result.is_ok()
} else {
false
}
}
pub fn get_translation(&self, db: &Connection) -> Result<Option<Translation>, MessageError> {
if let Some(payload) = self.message_summary_info(db) {
return Ok(Some(Translation::from_payload(&payload)?));
}
Ok(None)
}
pub fn cache_translations(db: &Connection) -> Result<HashSet<String>, TableError> {
let query = format!(
"SELECT guid FROM {MESSAGE}
WHERE message_summary_info IS NOT NULL
AND length(message_summary_info) > 61
AND instr(message_summary_info, X'7472616E736C6174696F6E4C616E6775616765') > 0
AND instr(message_summary_info, X'7472616E736C6174656454657874') > 0"
);
let mut statement = db.prepare(&query)?;
let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
let mut guids = HashSet::new();
for guid_result in rows {
guids.insert(guid_result?);
}
Ok(guids)
}
#[must_use]
pub fn group_action(&'_ self) -> Option<GroupAction<'_>> {
GroupAction::from_message(self)
}
fn get_reply_index(&self) -> usize {
if let Some(parts) = &self.thread_originator_part {
return match parts.split(':').next() {
Some(part) => str::parse::<usize>(part).unwrap_or(0),
None => 0,
};
}
0
}
pub(crate) fn generate_filter_statement(
context: &QueryContext,
include_recoverable: bool,
) -> String {
let mut filters = String::with_capacity(128);
if let Some(start) = context.start {
let _ = write!(filters, " m.date >= {start}");
}
if let Some(end) = context.end {
if !filters.is_empty() {
filters.push_str(" AND ");
}
let _ = write!(filters, " m.date <= {end}");
}
if let Some(chat_ids) = &context.selected_chat_ids {
if !filters.is_empty() {
filters.push_str(" AND ");
}
let ids = chat_ids
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>()
.join(", ");
if include_recoverable {
let _ = write!(filters, " (c.chat_id IN ({ids}) OR d.chat_id IN ({ids}))");
} else {
let _ = write!(filters, " c.chat_id IN ({ids})");
}
}
if !filters.is_empty() {
return format!("WHERE {filters}");
}
filters
}
pub fn get_count(db: &Connection, context: &QueryContext) -> Result<i64, TableError> {
let mut statement = if context.has_filters() {
db.prepare_cached(&format!(
"SELECT
COUNT(*)
FROM {MESSAGE} as m
LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
LEFT JOIN {RECENTLY_DELETED} as d ON m.ROWID = d.message_id
{}",
Self::generate_filter_statement(context, true)
))
.or_else(|_| {
db.prepare_cached(&format!(
"SELECT
COUNT(*)
FROM {MESSAGE} as m
LEFT JOIN {CHAT_MESSAGE_JOIN} as c ON m.ROWID = c.message_id
{}",
Self::generate_filter_statement(context, false)
))
})?
} else {
db.prepare_cached(&format!("SELECT COUNT(*) FROM {MESSAGE}"))?
};
let count: i64 = statement.query_row([], |r| r.get(0)).unwrap_or(0);
Ok(count)
}
pub fn stream_rows<'a>(
db: &'a Connection,
context: &'a QueryContext,
) -> Result<CachedStatement<'a>, TableError> {
if !context.has_filters() {
return Self::get(db);
}
Ok(db
.prepare_cached(&ios_16_newer_query(Some(&Self::generate_filter_statement(
context, true,
))))
.or_else(|_| {
db.prepare_cached(&ios_14_15_query(Some(&Self::generate_filter_statement(
context, false,
))))
})
.or_else(|_| {
db.prepare_cached(&ios_13_older_query(Some(&Self::generate_filter_statement(
context, false,
))))
})?)
}
#[must_use]
pub fn clean_associated_guid(&self) -> Option<(usize, &str)> {
if let Some(guid) = &self.associated_message_guid {
if guid.starts_with("p:") {
let mut split = guid.split('/');
let index_str = split.next()?;
let message_id = split.next()?;
let index = str::parse::<usize>(&index_str.replace("p:", "")).unwrap_or(0);
return Some((index, message_id.get(0..36)?));
} else if guid.starts_with("bp:") {
return Some((0, guid.get(3..39)?));
}
return Some((0, guid.get(0..36)?));
}
None
}
fn tapback_index(&self) -> usize {
match self.clean_associated_guid() {
Some((x, _)) => x,
None => 0,
}
}
pub fn get_replies(&self, db: &Connection) -> Result<HashMap<usize, Vec<Self>>, TableError> {
let mut out_h: HashMap<usize, Vec<Self>> = HashMap::new();
if self.has_replies() {
let filters = "WHERE m.thread_originator_guid = ?1";
let mut statement = db
.prepare_cached(&ios_16_newer_query(Some(filters)))
.or_else(|_| db.prepare_cached(&ios_14_15_query(Some(filters))))?;
let iter =
statement.query_map([self.guid.as_str()], |row| Ok(Message::from_row(row)))?;
for message in iter {
let m = Message::extract(message)?;
let idx = m.get_reply_index();
match out_h.get_mut(&idx) {
Some(body_part) => body_part.push(m),
None => {
out_h.insert(idx, vec![m]);
}
}
}
}
Ok(out_h)
}
pub fn get_votes(&self, db: &Connection) -> Result<Vec<Self>, TableError> {
let mut out_v: Vec<Self> = Vec::new();
if self.is_poll() {
let filters = "WHERE m.associated_message_guid = ?1";
let mut statement = db
.prepare_cached(&ios_16_newer_query(Some(filters)))
.or_else(|_| db.prepare_cached(&ios_14_15_query(Some(filters))))?;
let iter =
statement.query_map([self.guid.as_str()], |row| Ok(Message::from_row(row)))?;
for message in iter {
let m = Message::extract(message)?;
out_v.push(m);
}
}
Ok(out_v)
}
pub fn as_poll(&self, db: &Connection) -> Result<Option<Poll>, MessageError> {
if self.is_poll()
&& let Some(payload) = self.payload_data(db)
{
let mut poll = Poll::from_payload(&payload)?;
let votes = self.get_votes(db).unwrap_or_default();
for vote in votes.iter().rev() {
if !vote.is_poll_vote()
&& let Some(vote_payload) = vote.payload_data(db)
&& let Ok(update) = Poll::from_payload(&vote_payload)
{
poll = update;
break;
}
}
for vote in &votes {
if vote.is_poll_vote()
&& let Some(vote_payload) = vote.payload_data(db)
{
poll.count_votes(&vote_payload)?;
}
}
return Ok(Some(poll));
}
Ok(None)
}
#[must_use]
pub fn variant(&'_ self) -> Variant<'_> {
if self.is_edited() {
return Variant::Edited;
}
if let Some(associated_message_type) = self.associated_message_type {
match associated_message_type {
0 | 2 | 3 => return self.get_app_variant().unwrap_or(Variant::Normal),
1000 | 2000..=2007 | 3000..=3007 => {
if let Some((action, tapback)) = self.get_tapback() {
return Variant::Tapback(self.tapback_index(), action, tapback);
}
}
4000 => return Variant::Vote,
x => return Variant::Unknown(x),
}
}
if self.is_shareplay() {
return Variant::SharePlay;
}
Variant::Normal
}
#[must_use]
fn get_app_variant(&self) -> Option<Variant<'_>> {
let bundle_id = parse_balloon_bundle_id(self.balloon_bundle_id.as_deref())?;
let custom = match bundle_id {
"com.apple.messages.URLBalloonProvider" => CustomBalloon::URL,
"com.apple.Handwriting.HandwritingProvider" => CustomBalloon::Handwriting,
"com.apple.DigitalTouchBalloonProvider" => CustomBalloon::DigitalTouch,
"com.apple.PassbookUIService.PeerPaymentMessagesExtension" => CustomBalloon::ApplePay,
"com.apple.ActivityMessagesApp.MessagesExtension" => CustomBalloon::Fitness,
"com.apple.mobileslideshow.PhotosMessagesApp" => CustomBalloon::Slideshow,
"com.apple.SafetyMonitorApp.SafetyMonitorMessages" => CustomBalloon::CheckIn,
"com.apple.findmy.FindMyMessagesApp" => CustomBalloon::FindMy,
"com.apple.messages.Polls" => {
if self
.associated_message_guid
.as_ref()
.is_none_or(|id| id == &self.guid)
{
CustomBalloon::Polls
} else {
return Some(Variant::PollUpdate);
}
}
_ => CustomBalloon::Application(bundle_id),
};
Some(Variant::App(custom))
}
#[must_use]
fn get_tapback(&self) -> Option<(TapbackAction, Tapback<'_>)> {
match self.associated_message_type? {
1000 => Some((TapbackAction::Added, Tapback::Sticker)),
2000 => Some((TapbackAction::Added, Tapback::Loved)),
2001 => Some((TapbackAction::Added, Tapback::Liked)),
2002 => Some((TapbackAction::Added, Tapback::Disliked)),
2003 => Some((TapbackAction::Added, Tapback::Laughed)),
2004 => Some((TapbackAction::Added, Tapback::Emphasized)),
2005 => Some((TapbackAction::Added, Tapback::Questioned)),
2006 => Some((
TapbackAction::Added,
Tapback::Emoji(self.associated_message_emoji.as_deref()),
)),
2007 => Some((TapbackAction::Added, Tapback::Sticker)),
3000 => Some((TapbackAction::Removed, Tapback::Loved)),
3001 => Some((TapbackAction::Removed, Tapback::Liked)),
3002 => Some((TapbackAction::Removed, Tapback::Disliked)),
3003 => Some((TapbackAction::Removed, Tapback::Laughed)),
3004 => Some((TapbackAction::Removed, Tapback::Emphasized)),
3005 => Some((TapbackAction::Removed, Tapback::Questioned)),
3006 => Some((
TapbackAction::Removed,
Tapback::Emoji(self.associated_message_emoji.as_deref()),
)),
3007 => Some((TapbackAction::Removed, Tapback::Sticker)),
_ => None,
}
}
#[must_use]
pub fn get_announcement(&'_ self) -> Option<Announcement<'_>> {
if let Some(action) = self.group_action() {
return Some(Announcement::GroupAction(action));
}
if self.is_fully_unsent() {
return Some(Announcement::FullyUnsent);
}
if self.is_kept_audio_message() {
return Some(Announcement::AudioMessageKept);
}
None
}
#[must_use]
pub fn service(&'_ self) -> Service<'_> {
Service::from_name(self.service.as_deref())
}
pub fn payload_data(&self, db: &Connection) -> Option<Value> {
Value::from_reader(self.get_blob(db, MESSAGE, MESSAGE_PAYLOAD, self.rowid.into())?).ok()
}
pub fn raw_payload_data(&self, db: &Connection) -> Option<Vec<u8>> {
let mut buf = Vec::new();
self.get_blob(db, MESSAGE, MESSAGE_PAYLOAD, self.rowid.into())?
.read_to_end(&mut buf)
.ok()?;
Some(buf)
}
pub fn message_summary_info(&self, db: &Connection) -> Option<Value> {
Value::from_reader(self.get_blob(db, MESSAGE, MESSAGE_SUMMARY_INFO, self.rowid.into())?)
.ok()
}
pub fn attributed_body(&self, db: &Connection) -> Option<Vec<u8>> {
let mut body = vec![];
self.get_blob(db, MESSAGE, ATTRIBUTED_BODY, self.rowid.into())?
.read_to_end(&mut body)
.ok();
Some(body)
}
#[must_use]
pub fn get_expressive(&'_ self) -> Expressive<'_> {
match &self.expressive_send_style_id {
Some(content) => match content.as_str() {
"com.apple.MobileSMS.expressivesend.gentle" => {
Expressive::Bubble(BubbleEffect::Gentle)
}
"com.apple.MobileSMS.expressivesend.impact" => {
Expressive::Bubble(BubbleEffect::Slam)
}
"com.apple.MobileSMS.expressivesend.invisibleink" => {
Expressive::Bubble(BubbleEffect::InvisibleInk)
}
"com.apple.MobileSMS.expressivesend.loud" => Expressive::Bubble(BubbleEffect::Loud),
"com.apple.messages.effect.CKConfettiEffect" => {
Expressive::Screen(ScreenEffect::Confetti)
}
"com.apple.messages.effect.CKEchoEffect" => Expressive::Screen(ScreenEffect::Echo),
"com.apple.messages.effect.CKFireworksEffect" => {
Expressive::Screen(ScreenEffect::Fireworks)
}
"com.apple.messages.effect.CKHappyBirthdayEffect" => {
Expressive::Screen(ScreenEffect::Balloons)
}
"com.apple.messages.effect.CKHeartEffect" => {
Expressive::Screen(ScreenEffect::Heart)
}
"com.apple.messages.effect.CKLasersEffect" => {
Expressive::Screen(ScreenEffect::Lasers)
}
"com.apple.messages.effect.CKShootingStarEffect" => {
Expressive::Screen(ScreenEffect::ShootingStar)
}
"com.apple.messages.effect.CKSparklesEffect" => {
Expressive::Screen(ScreenEffect::Sparkles)
}
"com.apple.messages.effect.CKSpotlightEffect" => {
Expressive::Screen(ScreenEffect::Spotlight)
}
_ => Expressive::Unknown(content),
},
None => Expressive::None,
}
}
pub fn from_guid(guid: &str, db: &Connection) -> Result<Self, TableError> {
let mut statement = db
.prepare_cached(&ios_16_newer_query(Some("WHERE m.guid = ?1")))
.or_else(|_| db.prepare_cached(&ios_14_15_query(Some("WHERE m.guid = ?1"))))
.or_else(|_| db.prepare_cached(&ios_13_older_query(Some("WHERE m.guid = ?1"))))?;
Message::extract(statement.query_row([guid], |row| Ok(Message::from_row(row))))
}
}
#[cfg(test)]
impl Message {
#[must_use]
pub fn blank() -> Message {
use std::vec;
Message {
rowid: i32::default(),
guid: String::default(),
text: None,
service: Some("iMessage".to_string()),
handle_id: Some(i32::default()),
destination_caller_id: None,
subject: None,
date: i64::default(),
date_read: i64::default(),
date_delivered: i64::default(),
is_from_me: false,
is_read: false,
item_type: 0,
other_handle: None,
share_status: false,
share_direction: None,
group_title: None,
group_action_type: 0,
associated_message_guid: None,
associated_message_type: None,
balloon_bundle_id: None,
expressive_send_style_id: None,
thread_originator_guid: None,
thread_originator_part: None,
date_edited: 0,
associated_message_emoji: None,
chat_id: None,
num_attachments: 0,
deleted_from: None,
num_replies: 0,
components: vec![],
edited_parts: None,
}
}
}