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 for chat in Chat::rows(&mut statement, [])? {
112 let result = chat?;
113 map.insert(result.rowid, result);
114 }
115 Ok(map)
116 }
117}
118
119impl Chat {
120 #[must_use]
122 pub fn name(&self) -> &str {
123 match self.display_name() {
124 Some(name) => name,
125 None => &self.chat_identifier,
126 }
127 }
128
129 #[must_use]
131 pub fn display_name(&self) -> Option<&str> {
132 match &self.display_name {
133 Some(name) => {
134 if !name.is_empty() {
135 return Some(name.as_str());
136 }
137 None
138 }
139 None => None,
140 }
141 }
142
143 #[must_use]
145 pub fn service(&'_ self) -> Service<'_> {
146 Service::from_name(self.service_name.as_deref())
147 }
148
149 #[must_use]
154 pub fn properties(&self, db: &Connection) -> Option<Properties> {
155 match Value::from_reader(self.get_blob(db, CHAT, PROPERTIES, self.rowid.into())?) {
156 Ok(plist) => Properties::from_plist(&plist).ok(),
157 Err(_) => None,
158 }
159 }
160}
161
162#[cfg(test)]
164mod test_properties {
165 use plist::Value;
166 use std::env::current_dir;
167 use std::fs::File;
168
169 use crate::tables::chat::Properties;
170
171 #[test]
172 fn test_can_parse_properties_simple() {
173 let plist_path = current_dir()
174 .unwrap()
175 .as_path()
176 .join("test_data/chat_properties/ChatProp1.plist");
177 let plist_data = File::open(plist_path).unwrap();
178 let plist = Value::from_reader(plist_data).unwrap();
179 println!("Parsed plist: {plist:#?}");
180
181 let actual = Properties::from_plist(&plist).unwrap();
182 let expected = Properties {
183 read_receipts_enabled: false,
184 last_message_guid: Some(String::from("FF0615B9-C4AF-4BD8-B9A8-1B5F9351033F")),
185 forced_sms: false,
186 group_photo_guid: None,
187 has_chat_background: false,
188 };
189 print!("Parsed properties: {expected:?}");
190 assert_eq!(actual, expected);
191 }
192
193 #[test]
194 fn test_can_parse_properties_enable_read_receipts() {
195 let plist_path = current_dir()
196 .unwrap()
197 .as_path()
198 .join("test_data/chat_properties/ChatProp2.plist");
199 let plist_data = File::open(plist_path).unwrap();
200 let plist = Value::from_reader(plist_data).unwrap();
201 println!("Parsed plist: {plist:#?}");
202
203 let actual = Properties::from_plist(&plist).unwrap();
204 let expected = Properties {
205 read_receipts_enabled: true,
206 last_message_guid: Some(String::from("678BA15C-C309-FAAC-3678-78ACE995EB54")),
207 forced_sms: false,
208 group_photo_guid: None,
209 has_chat_background: false,
210 };
211 print!("Parsed properties: {expected:?}");
212 assert_eq!(actual, expected);
213 }
214
215 #[test]
216 fn test_can_parse_properties_third_with_summary() {
217 let plist_path = current_dir()
218 .unwrap()
219 .as_path()
220 .join("test_data/chat_properties/ChatProp3.plist");
221 let plist_data = File::open(plist_path).unwrap();
222 let plist = Value::from_reader(plist_data).unwrap();
223 println!("Parsed plist: {plist:#?}");
224
225 let actual = Properties::from_plist(&plist).unwrap();
226 let expected = Properties {
227 read_receipts_enabled: false,
228 last_message_guid: Some(String::from("CEE419B6-17C7-42F7-8C2A-09A38CCA5730")),
229 forced_sms: false,
230 group_photo_guid: None,
231 has_chat_background: false,
232 };
233 print!("Parsed properties: {expected:?}");
234 assert_eq!(actual, expected);
235 }
236
237 #[test]
238 fn test_can_parse_properties_forced_sms() {
239 let plist_path = current_dir()
240 .unwrap()
241 .as_path()
242 .join("test_data/chat_properties/ChatProp4.plist");
243 let plist_data = File::open(plist_path).unwrap();
244 let plist = Value::from_reader(plist_data).unwrap();
245 println!("Parsed plist: {plist:#?}");
246
247 let actual = Properties::from_plist(&plist).unwrap();
248 let expected = Properties {
249 read_receipts_enabled: false,
250 last_message_guid: Some(String::from("87D5257D-6536-4067-A8A0-E7EF10ECBA9D")),
251 forced_sms: true,
252 group_photo_guid: None,
253 has_chat_background: false,
254 };
255 print!("Parsed properties: {expected:?}");
256 assert_eq!(actual, expected);
257 }
258
259 #[test]
260 fn test_can_parse_properties_no_background() {
261 let plist_path = current_dir()
262 .unwrap()
263 .as_path()
264 .join("test_data/chat_properties/before_background.plist");
265 let plist_data = File::open(plist_path).unwrap();
266 let plist = Value::from_reader(plist_data).unwrap();
267 println!("Parsed plist: {plist:#?}");
268
269 let actual = Properties::from_plist(&plist).unwrap();
270 let expected = Properties {
271 read_receipts_enabled: true,
272 last_message_guid: Some(String::from("49DA49E8-0000-0000-B59E-290294670E7D")),
273 forced_sms: false,
274 group_photo_guid: None,
275 has_chat_background: false,
276 };
277 print!("Parsed properties: {expected:?}");
278 assert_eq!(actual, expected);
279 }
280
281 #[test]
282 fn test_can_parse_properties_added_background() {
283 let plist_path = current_dir()
284 .unwrap()
285 .as_path()
286 .join("test_data/chat_properties/after_background_preset.plist");
287 let plist_data = File::open(plist_path).unwrap();
288 let plist = Value::from_reader(plist_data).unwrap();
289 println!("Parsed plist: {plist:#?}");
290
291 let actual = Properties::from_plist(&plist).unwrap();
292 let expected = Properties {
293 read_receipts_enabled: true,
294 last_message_guid: Some(String::from("49DA49E8-0000-0000-B59E-290294670E7D")),
295 forced_sms: false,
296 group_photo_guid: None,
297 has_chat_background: true,
298 };
299 print!("Parsed properties: {expected:?}");
300 assert_eq!(actual, expected);
301 }
302
303 #[test]
304 fn test_can_parse_properties_removed_background() {
305 let plist_path = current_dir()
306 .unwrap()
307 .as_path()
308 .join("test_data/chat_properties/after_background_removed.plist");
309 let plist_data = File::open(plist_path).unwrap();
310 let plist = Value::from_reader(plist_data).unwrap();
311 println!("Parsed plist: {plist:#?}");
312
313 let actual = Properties::from_plist(&plist).unwrap();
314 let expected = Properties {
315 read_receipts_enabled: true,
316 last_message_guid: Some(String::from("49DA49E8-0000-0000-B59E-290294670E7D")),
317 forced_sms: false,
318 group_photo_guid: None,
319 has_chat_background: false,
320 };
321 print!("Parsed properties: {expected:?}");
322 assert_eq!(actual, expected);
323 }
324}