Skip to main content

imessage_database/message_types/
variants.rs

1/*!
2 Message classification helpers for rows from the `message` table.
3*/
4
5use std::fmt::Display;
6
7use plist::Value;
8
9use crate::{
10    error::plist::PlistParseError,
11    message_types::{
12        app_store::AppStoreMessage, collaboration::CollaborationMessage, music::MusicMessage,
13        placemark::PlacemarkMessage, url::URLMessage,
14    },
15    tables::messages::models::GroupAction,
16};
17
18/// # Tapbacks
19///
20/// Tapbacks look like normal messages in the database. Only the latest tapback
21/// state is stored. For example:
22///
23/// - user receives message -> user likes message
24///   - This creates a message and a like message.
25/// - user receives message -> user likes message -> user unlikes message
26///   - This creates a message and a like message.
27///   - The like message is removed when the unlike message arrives.
28///   - Removed rows leave gaps in `ROWID`; the row ID is not reused.
29///   - The database keeps the latest tapback state, not the full tapback history.
30///
31/// ## Technical detail
32///
33/// The index specified by the prefix identifies a parsed message body part;
34/// see [`Message::components`](crate::tables::messages::Message::components).
35///
36/// - `bp:` GUID prefix for bubble message tapbacks (url previews, apps, etc).
37/// - `p:0/` GUID prefix for normal messages (body text, attachments).
38///
39/// If a message has 3 attachments followed by some text:
40/// - 0 is the first image
41/// - 1 is the second image
42/// - 2 is the third image
43/// - 3 is the text of the message
44///
45/// In this example, a Like on `p:2/` is a like on the third image.
46#[derive(Debug, PartialEq, Eq)]
47pub enum Tapback<'a> {
48    /// Heart
49    Loved,
50    /// Thumbs up
51    Liked,
52    /// Thumbs down
53    Disliked,
54    /// Laughing face
55    Laughed,
56    /// Exclamation points
57    Emphasized,
58    /// Question marks
59    Questioned,
60    /// Custom emoji tapbacks
61    Emoji(Option<&'a str>),
62    /// Custom sticker tapbacks
63    Sticker,
64}
65
66impl Display for Tapback<'_> {
67    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Tapback::Emoji(emoji) => match emoji {
70                Some(e) => write!(fmt, "{e}"),
71                None => write!(fmt, "Unknown emoji!"),
72            },
73            _ => write!(fmt, "{self:?}"),
74        }
75    }
76}
77
78/// iMessage app balloon kind.
79///
80/// App integrations use custom balloons instead of the normal text bubble. This
81/// enum identifies the supported balloon families.
82#[derive(Debug, PartialEq, Eq)]
83pub enum CustomBalloon<'a> {
84    /// Generic third-party [application](crate::message_types::app).
85    Application(&'a str),
86    /// [URL](crate::message_types::url) preview.
87    URL,
88    /// Handwritten animated message.
89    Handwriting,
90    /// Digital Touch message.
91    DigitalTouch,
92    /// Apple Pay message (one of Sent, Requested, Received)
93    ApplePay,
94    /// Fitness.app message.
95    Fitness,
96    /// Photos.app slideshow message.
97    Slideshow,
98    /// [Check In](https://support.apple.com/guide/iphone/use-check-in-iphc143bb7e9/ios) message.
99    CheckIn,
100    /// Find My message.
101    FindMy,
102    /// Poll message.
103    Polls,
104    /// Apple [Business Chat](crate::message_types::business_chat) message.
105    Business,
106}
107
108/// Specialized payload carried by a URL balloon.
109///
110/// Apple reuses `com.apple.messages.URLBalloonProvider` for link previews and a
111/// few richer payloads. This enum stores the parsed result.
112#[derive(Debug, PartialEq)]
113pub enum URLOverride<'a> {
114    /// Standard [`URL`](crate::message_types::url) preview.
115    Normal(URLMessage<'a>),
116    /// [`Apple Music`](crate::message_types::music) message.
117    AppleMusic(MusicMessage<'a>),
118    /// [`App Store`](crate::message_types::app_store) message.
119    AppStore(AppStoreMessage<'a>),
120    /// [`Collaboration`](crate::message_types::collaboration) message.
121    Collaboration(CollaborationMessage<'a>),
122    /// [`Placemark`](crate::message_types::placemark) message.
123    SharedPlacemark(PlacemarkMessage<'a>),
124}
125
126/// Non-balloon announcement represented by a message row.
127///
128/// Announcements cover thread-level events such as group changes and fully
129/// unsent messages.
130#[derive(Debug, PartialEq, Eq)]
131pub enum Announcement<'a> {
132    /// All parts of the message were unsent.
133    FullyUnsent,
134    /// Group action.
135    GroupAction(GroupAction<'a>),
136    /// User kept an audio message.
137    AudioMessageKept,
138}
139
140/// Whether a tapback was added or removed.
141///
142#[derive(Debug, PartialEq, Eq)]
143pub enum TapbackAction {
144    /// Tapback was added to the message.
145    Added,
146    /// Tapback was removed from the message.
147    Removed,
148}
149
150/// High-level classification for a message row.
151#[derive(Debug, PartialEq, Eq)]
152pub enum Variant<'a> {
153    /// Standard message body, possibly with attachments.
154    Normal,
155    /// Message with edited or unsent parts.
156    Edited,
157    /// A [tapback](https://support.apple.com/guide/messages/react-with-tapbacks-icht504f698a/mac)
158    ///
159    /// The `usize` is the body component index the tapback applies to.
160    Tapback(usize, TapbackAction, Tapback<'a>),
161    /// Message generated by an iMessage app integration.
162    App(CustomBalloon<'a>),
163    /// SharePlay message.
164    SharePlay,
165    /// Vote cast on a poll.
166    Vote,
167    /// New option sent to a poll.
168    PollUpdate,
169    /// Unmapped `item_type`.
170    Unknown(i32),
171}
172
173/// Parser for custom balloon payloads stored in message plist data.
174pub trait BalloonProvider<'a> {
175    /// Parse the type from a plist payload.
176    fn from_map(payload: &'a Value) -> Result<Self, PlistParseError>
177    where
178        Self: Sized;
179}
180
181/// URL fields shared by payloads that store both final and original URLs.
182pub trait HasUrl {
183    /// The URL that ended up serving content, after redirects.
184    fn url(&self) -> Option<&str>;
185
186    /// The original URL before redirects.
187    fn original_url(&self) -> Option<&str>;
188
189    /// Return the final URL, falling back to the original URL.
190    #[must_use]
191    fn get_url(&self) -> Option<&str> {
192        self.url().or(self.original_url())
193    }
194}