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