imessage_database/tables/messages/
models.rs

1/*!
2 This module contains Data structures and models that represent message data.
3*/
4
5use std::fmt::{Display, Formatter, Result};
6
7use crate::{
8    message_types::text_effects::TextEffect, tables::messages::message::Message,
9    util::typedstream::models::Archivable,
10};
11
12/// Defines the parts of a message bubble, i.e. the content that can exist in a single message.
13///
14/// # Component Types
15///
16/// A single iMessage contains data that may be represented across multiple bubbles.
17///
18/// iMessage bubbles can only contain data of one variant of this enum at a time.
19#[derive(Debug, PartialEq)]
20pub enum BubbleComponent<'a> {
21    /// A text message with associated formatting, generally representing ranges present in a `NSAttributedString`
22    Text(Vec<TextAttributes<'a>>),
23    /// An attachment
24    Attachment(AttachmentMeta<'a>),
25    /// An [app integration](crate::message_types::app)
26    App,
27    /// A component that was retracted, found by parsing the [`EditedMessage`](crate::message_types::edited::EditedMessage)
28    Retracted,
29}
30
31/// Defines different types of [services](https://support.apple.com/en-us/104972) we can receive messages from.
32#[derive(Debug)]
33pub enum Service<'a> {
34    /// An iMessage
35    #[allow(non_camel_case_types)]
36    iMessage,
37    /// A message sent as SMS
38    SMS,
39    /// A message sent as RCS
40    RCS,
41    /// A message sent via [satellite](https://support.apple.com/en-us/120930)
42    Satellite,
43    /// Any other type of message
44    Other(&'a str),
45    /// Used when service field is not set
46    Unknown,
47}
48
49impl<'a> Service<'a> {
50    /// Creates a [`Service`] enum variant based on the provided service name string
51    #[must_use]
52    pub fn from(service: Option<&'a str>) -> Self {
53        if let Some(service_name) = service {
54            return match service_name.trim() {
55                "iMessage" => Service::iMessage,
56                "iMessageLite" => Service::Satellite,
57                "SMS" => Service::SMS,
58                "rcs" | "RCS" => Service::RCS,
59                service_name => Service::Other(service_name),
60            };
61        }
62        Service::Unknown
63    }
64}
65
66impl Display for Service<'_> {
67    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result {
68        match self {
69            Service::iMessage => write!(fmt, "iMessage"),
70            Service::SMS => write!(fmt, "SMS"),
71            Service::RCS => write!(fmt, "RCS"),
72            Service::Satellite => write!(fmt, "Satellite"),
73            Service::Other(other) => write!(fmt, "{other}"),
74            Service::Unknown => write!(fmt, "Unknown"),
75        }
76    }
77}
78
79/// Defines ranges of text and associated attributes parsed from [`typedstream`](crate::util::typedstream) `attributedBody` data.
80///
81/// Ranges specify locations where attributes are applied to specific portions of a [`Message`]'s [`text`](crate::tables::messages::Message::text). For example, given message text with a [`Mention`](TextEffect::Mention) like:
82///
83/// ```
84/// let message_text = "What's up, Christopher?";
85/// ```
86///
87/// There will be 3 ranges:
88///
89/// ```
90/// use imessage_database::message_types::text_effects::TextEffect;
91/// use imessage_database::tables::messages::models::{TextAttributes, BubbleComponent};
92///  
93/// let result = vec![BubbleComponent::Text(vec![
94///     TextAttributes::new(0, 11, TextEffect::Default),  // `What's up, `
95///     TextAttributes::new(11, 22, TextEffect::Mention("+5558675309")), // `Christopher`
96///     TextAttributes::new(22, 23, TextEffect::Default)  // `?`
97/// ])];
98/// ```
99#[derive(Debug, PartialEq, Eq)]
100pub struct TextAttributes<'a> {
101    /// The start index of the affected range of message text
102    pub start: usize,
103    /// The end index of the affected range of message text
104    pub end: usize,
105    /// The effects applied to the specified range
106    pub effect: TextEffect<'a>,
107}
108
109impl<'a> TextAttributes<'a> {
110    /// Creates a new [`TextAttributes`] with the specified start index, end index, and text effect.
111    #[must_use]
112    pub fn new(start: usize, end: usize, effect: TextEffect<'a>) -> Self {
113        Self { start, end, effect }
114    }
115}
116
117/// Representation of attachment metadata used for rendering message body in a conversation feed.
118#[derive(Debug, PartialEq, Default)]
119pub struct AttachmentMeta<'a> {
120    /// GUID of the attachment in the `attachment` table
121    pub guid: Option<&'a str>,
122    /// The transcription, if the attachment was an [audio message](https://support.apple.com/guide/iphone/send-and-receive-audio-messages-iph2e42d3117/ios) sent from or received on a [supported platform](https://www.apple.com/ios/feature-availability/#messages-audio-message-transcription).
123    pub transcription: Option<&'a str>,
124    /// The height of the attachment in points
125    pub height: Option<&'a f64>,
126    /// The width of the attachment in points
127    pub width: Option<&'a f64>,
128    /// The attachment's original filename
129    pub name: Option<&'a str>,
130}
131
132impl<'a> AttachmentMeta<'a> {
133    /// Given a slice of parsed [`typedstream`](crate::util::typedstream) data, populate the attachment's metadata fields.
134    ///
135    /// # Example
136    /// ```
137    /// use imessage_database::util::typedstream::models::{Archivable, Class, OutputData};
138    /// use imessage_database::tables::messages::models::AttachmentMeta;
139    ///
140    /// // Sample components
141    /// let components = vec![
142    ///    Archivable::Object(
143    ///        Class {
144    ///            name: "NSString".to_string(),
145    ///            version: 1,
146    ///        },
147    ///        vec![OutputData::String(
148    ///            "__kIMFileTransferGUIDAttributeName".to_string(),
149    ///        )],
150    ///    ),
151    ///    Archivable::Object(
152    ///        Class {
153    ///            name: "NSString".to_string(),
154    ///            version: 1,
155    ///        },
156    ///        vec![OutputData::String(
157    ///            "4C339597-EBBB-4978-9B87-521C0471A848".to_string(),
158    ///        )],
159    ///    ),
160    /// ];
161    /// let meta = AttachmentMeta::from_components(&components);
162    /// ```
163    #[must_use]
164    pub fn from_components(components: &'a [Archivable]) -> Option<Self> {
165        let mut guid = None;
166        let mut transcription = None;
167        let mut height = None;
168        let mut width = None;
169        let mut name = None;
170
171        for (idx, key) in components.iter().enumerate() {
172            if let Some(key_name) = key.as_nsstring() {
173                match key_name {
174                    "__kIMFileTransferGUIDAttributeName" => {
175                        guid = components.get(idx + 1)?.as_nsstring();
176                    }
177                    "IMAudioTranscription" => {
178                        transcription = components.get(idx + 1)?.as_nsstring();
179                    }
180                    "__kIMInlineMediaHeightAttributeName" => {
181                        height = components.get(idx + 1)?.as_nsnumber_float();
182                    }
183                    "__kIMInlineMediaWidthAttributeName" => {
184                        width = components.get(idx + 1)?.as_nsnumber_float();
185                    }
186                    "__kIMFilenameAttributeName" => name = components.get(idx + 1)?.as_nsstring(),
187                    _ => {}
188                }
189            }
190        }
191
192        Some(Self {
193            guid,
194            transcription,
195            height,
196            width,
197            name,
198        })
199    }
200}
201
202/// Represents different types of group message actions that can occur in a chat system
203#[derive(Debug)]
204pub enum GroupAction<'a> {
205    /// A new participant has been added to the group
206    ParticipantAdded(i32),
207    /// A participant has been removed from the group
208    ParticipantRemoved(i32),
209    /// The group name has been changed
210    NameChange(&'a str),
211    /// A participant has voluntarily left the group
212    ParticipantLeft,
213    /// The group icon/avatar has been updated with a new image
214    GroupIconChanged,
215    /// The group icon/avatar has been removed, reverting to default
216    GroupIconRemoved,
217}
218
219impl<'a> GroupAction<'a> {
220    /// Creates a new `EventType` based on the provided `item_type` and `group_action_type`
221    #[must_use]
222    pub fn from_message(message: &'a Message) -> Option<Self> {
223        match (
224            message.item_type,
225            message.group_action_type,
226            message.other_handle,
227            &message.group_title,
228        ) {
229            (1, 0, Some(who), _) => Some(Self::ParticipantAdded(who)),
230            (1, 1, Some(who), _) => Some(Self::ParticipantRemoved(who)),
231            (2, _, _, Some(name)) => Some(Self::NameChange(name)),
232            (3, 0, _, _) => Some(Self::ParticipantLeft),
233            (3, 1, _, _) => Some(Self::GroupIconChanged),
234            (3, 2, _, _) => Some(Self::GroupIconRemoved),
235            _ => None,
236        }
237    }
238}