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