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}