imessage_database/tables/
chat.rs1use std::collections::HashMap;
6
7use plist::Value;
8use rusqlite::{CachedStatement, Connection, 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::{
17 extract_dictionary, extract_string_key, get_bool_from_dict, get_owned_string_from_dict,
18 plist_as_dictionary,
19 },
20};
21
22#[derive(Debug, PartialEq, Eq)]
26pub struct Properties {
27 pub read_receipts_enabled: bool,
29 pub last_message_guid: Option<String>,
31 pub forced_sms: bool,
33 pub group_photo_guid: Option<String>,
35 pub has_chat_background: bool,
37}
38
39impl Properties {
40 pub(self) fn from_plist(plist: &Value) -> Result<Self, PlistParseError> {
42 Ok(Self {
43 read_receipts_enabled: get_bool_from_dict(plist, "EnableReadReceiptForChat")
44 .unwrap_or(false),
45 last_message_guid: get_owned_string_from_dict(plist, "lastSeenMessageGuid"),
46 forced_sms: get_bool_from_dict(plist, "shouldForceToSMS").unwrap_or(false),
47 group_photo_guid: get_owned_string_from_dict(plist, "groupPhotoGuid"),
48 has_chat_background: plist_as_dictionary(plist)
49 .and_then(|dict| extract_dictionary(dict, "backgroundProperties"))
50 .and_then(|dict| extract_string_key(dict, "trabar"))
51 .is_ok(),
52 })
53 }
54}
55
56#[derive(Debug)]
59pub struct Chat {
60 pub rowid: i32,
62 pub chat_identifier: String,
64 pub service_name: Option<String>,
66 pub display_name: Option<String>,
68}
69
70impl Table for Chat {
72 fn from_row(row: &Row) -> Result<Chat> {
73 Ok(Chat {
74 rowid: row.get("rowid")?,
75 chat_identifier: row.get("chat_identifier")?,
76 service_name: row.get("service_name")?,
77 display_name: row.get("display_name").unwrap_or(None),
78 })
79 }
80
81 fn get(db: &'_ Connection) -> Result<CachedStatement<'_>, TableError> {
82 Ok(db.prepare_cached(&format!("SELECT * from {CHAT}"))?)
83 }
84}
85
86impl Cacheable for Chat {
88 type K = i32;
89 type V = Chat;
90 fn cache(db: &Connection) -> Result<HashMap<Self::K, Self::V>, TableError> {
107 let mut map = HashMap::new();
108
109 let mut statement = Chat::get(db)?;
110
111 let chats = statement.query_map([], |row| Ok(Chat::from_row(row)))?;
112
113 for chat in chats {
114 let result = Chat::extract(chat)?;
115 map.insert(result.rowid, result);
116 }
117 Ok(map)
118 }
119}
120
121impl Chat {
122 #[must_use]
124 pub fn name(&self) -> &str {
125 match self.display_name() {
126 Some(name) => name,
127 None => &self.chat_identifier,
128 }
129 }
130
131 #[must_use]
133 pub fn display_name(&self) -> Option<&str> {
134 match &self.display_name {
135 Some(name) => {
136 if !name.is_empty() {
137 return Some(name.as_str());
138 }
139 None
140 }
141 None => None,
142 }
143 }
144
145 #[must_use]
147 pub fn service(&'_ self) -> Service<'_> {
148 Service::from_name(self.service_name.as_deref())
149 }
150
151 #[must_use]
156 pub fn properties(&self, db: &Connection) -> Option<Properties> {
157 match Value::from_reader(self.get_blob(db, CHAT, PROPERTIES, self.rowid.into())?) {
158 Ok(plist) => Properties::from_plist(&plist).ok(),
159 Err(_) => None,
160 }
161 }
162}
163
164#[cfg(test)]
166mod test_properties {
167 use plist::Value;
168 use std::env::current_dir;
169 use std::fs::File;
170
171 use crate::tables::chat::Properties;
172
173 #[test]
174 fn test_can_parse_properties_simple() {
175 let plist_path = current_dir()
176 .unwrap()
177 .as_path()
178 .join("test_data/chat_properties/ChatProp1.plist");
179 let plist_data = File::open(plist_path).unwrap();
180 let plist = Value::from_reader(plist_data).unwrap();
181 println!("Parsed plist: {plist:#?}");
182
183 let actual = Properties::from_plist(&plist).unwrap();
184 let expected = Properties {
185 read_receipts_enabled: false,
186 last_message_guid: Some(String::from("FF0615B9-C4AF-4BD8-B9A8-1B5F9351033F")),
187 forced_sms: false,
188 group_photo_guid: None,
189 has_chat_background: false,
190 };
191 print!("Parsed properties: {expected:?}");
192 assert_eq!(actual, expected);
193 }
194
195 #[test]
196 fn test_can_parse_properties_enable_read_receipts() {
197 let plist_path = current_dir()
198 .unwrap()
199 .as_path()
200 .join("test_data/chat_properties/ChatProp2.plist");
201 let plist_data = File::open(plist_path).unwrap();
202 let plist = Value::from_reader(plist_data).unwrap();
203 println!("Parsed plist: {plist:#?}");
204
205 let actual = Properties::from_plist(&plist).unwrap();
206 let expected = Properties {
207 read_receipts_enabled: true,
208 last_message_guid: Some(String::from("678BA15C-C309-FAAC-3678-78ACE995EB54")),
209 forced_sms: false,
210 group_photo_guid: None,
211 has_chat_background: false,
212 };
213 print!("Parsed properties: {expected:?}");
214 assert_eq!(actual, expected);
215 }
216
217 #[test]
218 fn test_can_parse_properties_third_with_summary() {
219 let plist_path = current_dir()
220 .unwrap()
221 .as_path()
222 .join("test_data/chat_properties/ChatProp3.plist");
223 let plist_data = File::open(plist_path).unwrap();
224 let plist = Value::from_reader(plist_data).unwrap();
225 println!("Parsed plist: {plist:#?}");
226
227 let actual = Properties::from_plist(&plist).unwrap();
228 let expected = Properties {
229 read_receipts_enabled: false,
230 last_message_guid: Some(String::from("CEE419B6-17C7-42F7-8C2A-09A38CCA5730")),
231 forced_sms: false,
232 group_photo_guid: None,
233 has_chat_background: false,
234 };
235 print!("Parsed properties: {expected:?}");
236 assert_eq!(actual, expected);
237 }
238
239 #[test]
240 fn test_can_parse_properties_forced_sms() {
241 let plist_path = current_dir()
242 .unwrap()
243 .as_path()
244 .join("test_data/chat_properties/ChatProp4.plist");
245 let plist_data = File::open(plist_path).unwrap();
246 let plist = Value::from_reader(plist_data).unwrap();
247 println!("Parsed plist: {plist:#?}");
248
249 let actual = Properties::from_plist(&plist).unwrap();
250 let expected = Properties {
251 read_receipts_enabled: false,
252 last_message_guid: Some(String::from("87D5257D-6536-4067-A8A0-E7EF10ECBA9D")),
253 forced_sms: true,
254 group_photo_guid: None,
255 has_chat_background: false,
256 };
257 print!("Parsed properties: {expected:?}");
258 assert_eq!(actual, expected);
259 }
260
261 #[test]
262 fn test_can_parse_properties_no_background() {
263 let plist_path = current_dir()
264 .unwrap()
265 .as_path()
266 .join("test_data/chat_properties/before_background.plist");
267 let plist_data = File::open(plist_path).unwrap();
268 let plist = Value::from_reader(plist_data).unwrap();
269 println!("Parsed plist: {plist:#?}");
270
271 let actual = Properties::from_plist(&plist).unwrap();
272 let expected = Properties {
273 read_receipts_enabled: true,
274 last_message_guid: Some(String::from("49DA49E8-0000-0000-B59E-290294670E7D")),
275 forced_sms: false,
276 group_photo_guid: None,
277 has_chat_background: false,
278 };
279 print!("Parsed properties: {expected:?}");
280 assert_eq!(actual, expected);
281 }
282
283 #[test]
284 fn test_can_parse_properties_added_background() {
285 let plist_path = current_dir()
286 .unwrap()
287 .as_path()
288 .join("test_data/chat_properties/after_background_preset.plist");
289 let plist_data = File::open(plist_path).unwrap();
290 let plist = Value::from_reader(plist_data).unwrap();
291 println!("Parsed plist: {plist:#?}");
292
293 let actual = Properties::from_plist(&plist).unwrap();
294 let expected = Properties {
295 read_receipts_enabled: true,
296 last_message_guid: Some(String::from("49DA49E8-0000-0000-B59E-290294670E7D")),
297 forced_sms: false,
298 group_photo_guid: None,
299 has_chat_background: true,
300 };
301 print!("Parsed properties: {expected:?}");
302 assert_eq!(actual, expected);
303 }
304
305 #[test]
306 fn test_can_parse_properties_removed_background() {
307 let plist_path = current_dir()
308 .unwrap()
309 .as_path()
310 .join("test_data/chat_properties/after_background_removed.plist");
311 let plist_data = File::open(plist_path).unwrap();
312 let plist = Value::from_reader(plist_data).unwrap();
313 println!("Parsed plist: {plist:#?}");
314
315 let actual = Properties::from_plist(&plist).unwrap();
316 let expected = Properties {
317 read_receipts_enabled: true,
318 last_message_guid: Some(String::from("49DA49E8-0000-0000-B59E-290294670E7D")),
319 forced_sms: false,
320 group_photo_guid: None,
321 has_chat_background: false,
322 };
323 print!("Parsed properties: {expected:?}");
324 assert_eq!(actual, expected);
325 }
326}