imessage_database/tables/messages/
models.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/*!
 This module contains Data structures and models that represent message data.
*/

use crate::{message_types::text_effects::TextEffect, util::typedstream::models::Archivable};

/// Defines the parts of a message bubble, i.e. the content that can exist in a single message.
///
/// # Component Types
///
/// A single iMessage contains data that may be represented across multiple bubbles.
///
/// iMessage bubbles can only contain data of one variant of this enum at a time.
#[derive(Debug, PartialEq)]
pub enum BubbleComponent<'a> {
    /// A text message with associated formatting, generally representing ranges present in a `NSAttributedString`
    Text(Vec<TextAttributes<'a>>),
    /// An attachment
    Attachment(AttachmentMeta<'a>),
    /// An [app integration](crate::message_types::app)
    App,
    /// A component that was retracted, found by parsing the [`EditedMessage`](crate::message_types::edited::EditedMessage)
    Retracted,
}

/// Defines different types of services we can receive messages from.
#[derive(Debug)]
pub enum Service<'a> {
    /// An iMessage
    #[allow(non_camel_case_types)]
    iMessage,
    /// A message sent as SMS
    SMS,
    /// A message sent as RCS
    RCS,
    /// Any other type of message
    Other(&'a str),
    /// Used when service field is not set
    Unknown,
}

/// Defines ranges of text and associated attributes parsed from [`typedstream`](crate::util::typedstream) `attributedBody` data.
///
/// Ranges specify locations attributes applied to specific portions of a [`Message`](crate::tables::messages::Message)'s [`text`](crate::tables::messages::Message::text). For example, given message text with a [`Mention`](TextEffect::Mention) like:
///
/// ```
/// let message_text = "What's up, Christopher?";
/// ```
///
/// There will be 3 ranges:
///
/// ```
/// use imessage_database::message_types::text_effects::TextEffect;
/// use imessage_database::tables::messages::models::{TextAttributes, BubbleComponent};
///  
/// let result = vec![BubbleComponent::Text(vec![
///     TextAttributes::new(0, 11, TextEffect::Default),  // `What's up, `
///     TextAttributes::new(11, 22, TextEffect::Mention("+5558675309")), // `Christopher`
///     TextAttributes::new(22, 23, TextEffect::Default)  // `?`
/// ])];
/// ```
#[derive(Debug, PartialEq, Eq)]
pub struct TextAttributes<'a> {
    /// The start index of the affected range of message text
    pub start: usize,
    /// The end index of the affected range of message text
    pub end: usize,
    /// The effects applied to the specified range
    pub effect: TextEffect<'a>,
}

impl<'a> TextAttributes<'a> {
    pub fn new(start: usize, end: usize, effect: TextEffect<'a>) -> Self {
        Self { start, end, effect }
    }
}

/// Representation of attachment metadata used for rendering message body in a conversation feed.
#[derive(Debug, PartialEq, Default)]
pub struct AttachmentMeta<'a> {
    /// GUID of the attachment in the `attachment` table
    pub guid: Option<&'a str>,
    /// 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).
    pub transcription: Option<&'a str>,
    /// The height of the attachment in points
    pub height: Option<&'a f64>,
    /// The width of the attachment in points
    pub width: Option<&'a f64>,
    /// The attachment's original filename
    pub name: Option<&'a str>,
}

impl<'a> AttachmentMeta<'a> {
    /// Given a slice of parsed [`typedstream`](crate::util::typedstream) data, populate the attachment's metadata fields.
    ///
    /// # Example
    /// ```
    /// use imessage_database::util::typedstream::models::{Archivable, Class, OutputData};
    /// use imessage_database::tables::messages::models::AttachmentMeta;
    ///
    /// // Sample components
    /// let components = vec![
    ///    Archivable::Object(
    ///        Class {
    ///            name: "NSString".to_string(),
    ///            version: 1,
    ///        },
    ///        vec![OutputData::String(
    ///            "__kIMFileTransferGUIDAttributeName".to_string(),
    ///        )],
    ///    ),
    ///    Archivable::Object(
    ///        Class {
    ///            name: "NSString".to_string(),
    ///            version: 1,
    ///        },
    ///        vec![OutputData::String(
    ///            "4C339597-EBBB-4978-9B87-521C0471A848".to_string(),
    ///        )],
    ///    ),
    /// ];
    /// let meta = AttachmentMeta::from_components(&components);
    /// ```
    pub fn from_components(components: &'a [Archivable]) -> Option<Self> {
        let mut guid = None;
        let mut transcription = None;
        let mut height = None;
        let mut width = None;
        let mut name = None;

        for (idx, key) in components.iter().enumerate() {
            if let Some(key_name) = key.as_nsstring() {
                match key_name {
                    "__kIMFileTransferGUIDAttributeName" => {
                        guid = components.get(idx + 1)?.as_nsstring()
                    }
                    "IMAudioTranscription" => {
                        transcription = components.get(idx + 1)?.as_nsstring()
                    }
                    "__kIMInlineMediaHeightAttributeName" => {
                        height = components.get(idx + 1)?.as_nsnumber_float()
                    }
                    "__kIMInlineMediaWidthAttributeName" => {
                        width = components.get(idx + 1)?.as_nsnumber_float()
                    }
                    "__kIMFilenameAttributeName" => name = components.get(idx + 1)?.as_nsstring(),
                    _ => {}
                }
            }
        }

        Some(Self {
            guid,
            transcription,
            height,
            width,
            name,
        })
    }
}