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