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