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 #[must_use]
51 pub fn from(service: Option<&'a str>) -> Self {
52 if let Some(service_name) = service {
53 return match service_name.trim() {
54 "iMessage" => Service::iMessage,
55 "iMessageLite" => Service::Satellite,
56 "SMS" => Service::SMS,
57 "rcs" | "RCS" => Service::RCS,
58 service_name => Service::Other(service_name),
59 };
60 }
61 Service::Unknown
62 }
63}
64
65impl Display for Service<'_> {
66 fn fmt(&self, fmt: &mut Formatter<'_>) -> Result {
67 match self {
68 Service::iMessage => write!(fmt, "iMessage"),
69 Service::SMS => write!(fmt, "SMS"),
70 Service::RCS => write!(fmt, "RCS"),
71 Service::Satellite => write!(fmt, "Satellite"),
72 Service::Other(other) => write!(fmt, "{other}"),
73 Service::Unknown => write!(fmt, "Unknown"),
74 }
75 }
76}
77
78/// Defines ranges of text and associated attributes parsed from [`typedstream`](crate::util::typedstream) `attributedBody` data.
79///
80/// 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:
81///
82/// ```
83/// let message_text = "What's up, Christopher?";
84/// ```
85///
86/// There will be 3 ranges:
87///
88/// ```
89/// use imessage_database::message_types::text_effects::TextEffect;
90/// use imessage_database::tables::messages::models::{TextAttributes, BubbleComponent};
91///
92/// let result = vec![BubbleComponent::Text(vec![
93/// TextAttributes::new(0, 11, TextEffect::Default), // `What's up, `
94/// TextAttributes::new(11, 22, TextEffect::Mention("+5558675309")), // `Christopher`
95/// TextAttributes::new(22, 23, TextEffect::Default) // `?`
96/// ])];
97/// ```
98#[derive(Debug, PartialEq, Eq)]
99pub struct TextAttributes<'a> {
100 /// The start index of the affected range of message text
101 pub start: usize,
102 /// The end index of the affected range of message text
103 pub end: usize,
104 /// The effects applied to the specified range
105 pub effect: TextEffect<'a>,
106}
107
108impl<'a> TextAttributes<'a> {
109 #[must_use]
110 pub fn new(start: usize, end: usize, effect: TextEffect<'a>) -> Self {
111 Self { start, end, effect }
112 }
113}
114
115/// Representation of attachment metadata used for rendering message body in a conversation feed.
116#[derive(Debug, PartialEq, Default)]
117pub struct AttachmentMeta<'a> {
118 /// GUID of the attachment in the `attachment` table
119 pub guid: Option<&'a str>,
120 /// 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).
121 pub transcription: Option<&'a str>,
122 /// The height of the attachment in points
123 pub height: Option<&'a f64>,
124 /// The width of the attachment in points
125 pub width: Option<&'a f64>,
126 /// The attachment's original filename
127 pub name: Option<&'a str>,
128}
129
130impl<'a> AttachmentMeta<'a> {
131 /// Given a slice of parsed [`typedstream`](crate::util::typedstream) data, populate the attachment's metadata fields.
132 ///
133 /// # Example
134 /// ```
135 /// use imessage_database::util::typedstream::models::{Archivable, Class, OutputData};
136 /// use imessage_database::tables::messages::models::AttachmentMeta;
137 ///
138 /// // Sample components
139 /// let components = vec![
140 /// Archivable::Object(
141 /// Class {
142 /// name: "NSString".to_string(),
143 /// version: 1,
144 /// },
145 /// vec![OutputData::String(
146 /// "__kIMFileTransferGUIDAttributeName".to_string(),
147 /// )],
148 /// ),
149 /// Archivable::Object(
150 /// Class {
151 /// name: "NSString".to_string(),
152 /// version: 1,
153 /// },
154 /// vec![OutputData::String(
155 /// "4C339597-EBBB-4978-9B87-521C0471A848".to_string(),
156 /// )],
157 /// ),
158 /// ];
159 /// let meta = AttachmentMeta::from_components(&components);
160 /// ```
161 #[must_use]
162 pub fn from_components(components: &'a [Archivable]) -> Option<Self> {
163 let mut guid = None;
164 let mut transcription = None;
165 let mut height = None;
166 let mut width = None;
167 let mut name = None;
168
169 for (idx, key) in components.iter().enumerate() {
170 if let Some(key_name) = key.as_nsstring() {
171 match key_name {
172 "__kIMFileTransferGUIDAttributeName" => {
173 guid = components.get(idx + 1)?.as_nsstring();
174 }
175 "IMAudioTranscription" => {
176 transcription = components.get(idx + 1)?.as_nsstring();
177 }
178 "__kIMInlineMediaHeightAttributeName" => {
179 height = components.get(idx + 1)?.as_nsnumber_float();
180 }
181 "__kIMInlineMediaWidthAttributeName" => {
182 width = components.get(idx + 1)?.as_nsnumber_float();
183 }
184 "__kIMFilenameAttributeName" => name = components.get(idx + 1)?.as_nsstring(),
185 _ => {}
186 }
187 }
188 }
189
190 Some(Self {
191 guid,
192 transcription,
193 height,
194 width,
195 name,
196 })
197 }
198}
199
200/// Represents different types of group message actions that can occur in a chat system
201#[derive(Debug)]
202pub enum GroupAction<'a> {
203 /// A new participant has been added to the group
204 ParticipantAdded(i32),
205 /// A participant has been removed from the group
206 ParticipantRemoved(i32),
207 /// The group name has been changed
208 NameChange(&'a str),
209 /// A participant has voluntarily left the group
210 ParticipantLeft,
211 /// The group icon/avatar has been updated with a new image
212 GroupIconChanged,
213 /// The group icon/avatar has been removed, reverting to default
214 GroupIconRemoved,
215}
216
217impl<'a> GroupAction<'a> {
218 /// Creates a new `EventType` based on the provided `item_type` and `group_action_type`
219 #[must_use]
220 pub fn from_message(message: &'a Message) -> Option<Self> {
221 match (
222 message.item_type,
223 message.group_action_type,
224 message.other_handle,
225 &message.group_title,
226 ) {
227 (1, 0, Some(who), _) => Some(Self::ParticipantAdded(who)),
228 (1, 1, Some(who), _) => Some(Self::ParticipantRemoved(who)),
229 (2, _, _, Some(name)) => Some(Self::NameChange(name)),
230 (3, 0, _, _) => Some(Self::ParticipantLeft),
231 (3, 1, _, _) => Some(Self::GroupIconChanged),
232 (3, 2, _, _) => Some(Self::GroupIconRemoved),
233 _ => None,
234 }
235 }
236}