imessage_database/tables/
chat.rs1use std::collections::HashMap;
6
7use plist::Value;
8use rusqlite::{CachedStatement, Connection, Error, Result, Row};
9
10use crate::{
11 error::{plist::PlistParseError, table::TableError},
12 tables::{
13 messages::models::Service,
14 table::{CHAT, Cacheable, PROPERTIES, Table},
15 },
16 util::plist::{get_bool_from_dict, get_owned_string_from_dict},
17};
18
19#[derive(Debug, PartialEq, Eq)]
22pub struct Properties {
23 read_receipts_enabled: bool,
25 last_message_guid: Option<String>,
27 forced_sms: bool,
29 group_photo_guid: Option<String>,
31}
32
33impl Properties {
34 pub(self) fn from_plist(plist: &Value) -> Result<Self, PlistParseError> {
36 Ok(Self {
37 read_receipts_enabled: get_bool_from_dict(plist, "EnableReadReceiptForChat")
38 .unwrap_or(false),
39 last_message_guid: get_owned_string_from_dict(plist, "lastSeenMessageGuid"),
40 forced_sms: get_bool_from_dict(plist, "shouldForceToSMS").unwrap_or(false),
41 group_photo_guid: get_owned_string_from_dict(plist, "groupPhotoGuid"),
42 })
43 }
44}
45
46#[derive(Debug)]
48pub struct Chat {
49 pub rowid: i32,
51 pub chat_identifier: String,
53 pub service_name: Option<String>,
55 pub display_name: Option<String>,
57}
58
59impl Table for Chat {
60 fn from_row(row: &Row) -> Result<Chat> {
61 Ok(Chat {
62 rowid: row.get("rowid")?,
63 chat_identifier: row.get("chat_identifier")?,
64 service_name: row.get("service_name")?,
65 display_name: row.get("display_name").unwrap_or(None),
66 })
67 }
68
69 fn get(db: &Connection) -> Result<CachedStatement, TableError> {
70 Ok(db.prepare_cached(&format!("SELECT * from {CHAT}"))?)
71 }
72
73 fn extract(chat: Result<Result<Self, Error>, Error>) -> Result<Self, TableError> {
74 match chat {
75 Ok(Ok(chat)) => Ok(chat),
76 Err(why) | Ok(Err(why)) => Err(TableError::QueryError(why)),
77 }
78 }
79}
80
81impl Cacheable for Chat {
82 type K = i32;
83 type V = Chat;
84 fn cache(db: &Connection) -> Result<HashMap<Self::K, Self::V>, TableError> {
101 let mut map = HashMap::new();
102
103 let mut statement = Chat::get(db)?;
104
105 let chats = statement.query_map([], |row| Ok(Chat::from_row(row)))?;
106
107 for chat in chats {
108 let result = Chat::extract(chat)?;
109 map.insert(result.rowid, result);
110 }
111 Ok(map)
112 }
113}
114
115impl Chat {
116 #[must_use]
118 pub fn name(&self) -> &str {
119 match self.display_name() {
120 Some(name) => name,
121 None => &self.chat_identifier,
122 }
123 }
124
125 #[must_use]
127 pub fn display_name(&self) -> Option<&str> {
128 match &self.display_name {
129 Some(name) => {
130 if !name.is_empty() {
131 return Some(name.as_str());
132 }
133 None
134 }
135 None => None,
136 }
137 }
138
139 #[must_use]
141 pub fn service(&self) -> Service {
142 Service::from(self.service_name.as_deref())
143 }
144
145 #[must_use]
150 pub fn properties(&self, db: &Connection) -> Option<Properties> {
151 match Value::from_reader(self.get_blob(db, CHAT, PROPERTIES, self.rowid.into())?) {
152 Ok(plist) => Properties::from_plist(&plist).ok(),
153 Err(_) => None,
154 }
155 }
156}
157
158#[cfg(test)]
159mod test_properties {
160 use plist::Value;
161 use std::env::current_dir;
162 use std::fs::File;
163
164 use crate::tables::chat::Properties;
165
166 #[test]
167 fn test_can_parse_properties_simple() {
168 let plist_path = current_dir()
169 .unwrap()
170 .as_path()
171 .join("test_data/chat_properties/ChatProp1.plist");
172 let plist_data = File::open(plist_path).unwrap();
173 let plist = Value::from_reader(plist_data).unwrap();
174 println!("Parsed plist: {plist:#?}");
175
176 let actual = Properties::from_plist(&plist).unwrap();
177 let expected = Properties {
178 read_receipts_enabled: false,
179 last_message_guid: Some(String::from("FF0615B9-C4AF-4BD8-B9A8-1B5F9351033F")),
180 forced_sms: false,
181 group_photo_guid: None,
182 };
183 print!("Parsed properties: {expected:?}");
184 assert_eq!(actual, expected);
185 }
186
187 #[test]
188 fn test_can_parse_properties_enable_read_receipts() {
189 let plist_path = current_dir()
190 .unwrap()
191 .as_path()
192 .join("test_data/chat_properties/ChatProp2.plist");
193 let plist_data = File::open(plist_path).unwrap();
194 let plist = Value::from_reader(plist_data).unwrap();
195 println!("Parsed plist: {plist:#?}");
196
197 let actual = Properties::from_plist(&plist).unwrap();
198 let expected = Properties {
199 read_receipts_enabled: true,
200 last_message_guid: Some(String::from("678BA15C-C309-FAAC-3678-78ACE995EB54")),
201 forced_sms: false,
202 group_photo_guid: None,
203 };
204 print!("Parsed properties: {expected:?}");
205 assert_eq!(actual, expected);
206 }
207
208 #[test]
209 fn test_can_parse_properties_third_with_summary() {
210 let plist_path = current_dir()
211 .unwrap()
212 .as_path()
213 .join("test_data/chat_properties/ChatProp3.plist");
214 let plist_data = File::open(plist_path).unwrap();
215 let plist = Value::from_reader(plist_data).unwrap();
216 println!("Parsed plist: {plist:#?}");
217
218 let actual = Properties::from_plist(&plist).unwrap();
219 let expected = Properties {
220 read_receipts_enabled: false,
221 last_message_guid: Some(String::from("CEE419B6-17C7-42F7-8C2A-09A38CCA5730")),
222 forced_sms: false,
223 group_photo_guid: None,
224 };
225 print!("Parsed properties: {expected:?}");
226 assert_eq!(actual, expected);
227 }
228
229 #[test]
230 fn test_can_parse_properties_forced_sms() {
231 let plist_path = current_dir()
232 .unwrap()
233 .as_path()
234 .join("test_data/chat_properties/ChatProp4.plist");
235 let plist_data = File::open(plist_path).unwrap();
236 let plist = Value::from_reader(plist_data).unwrap();
237 println!("Parsed plist: {plist:#?}");
238
239 let actual = Properties::from_plist(&plist).unwrap();
240 let expected = Properties {
241 read_receipts_enabled: false,
242 last_message_guid: Some(String::from("87D5257D-6536-4067-A8A0-E7EF10ECBA9D")),
243 forced_sms: true,
244 group_photo_guid: None,
245 };
246 print!("Parsed properties: {expected:?}");
247 assert_eq!(actual, expected);
248 }
249}