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