imessage_database/tables/
chat.rs1use std::collections::HashMap;
6
7use plist::Value;
8use rusqlite::{Connection, Error, Result, Row, Statement, blob::Blob};
9
10use crate::{
11 error::{plist::PlistParseError, table::TableError},
12 tables::{
13 messages::models::Service,
14 table::{CHAT, Cacheable, GetBlob, 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<Statement, TableError> {
70 Ok(db.prepare(&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 GetBlob for Chat {
116 fn get_blob<'a>(&self, db: &'a Connection, column: &str) -> Option<Blob<'a>> {
118 db.blob_open(rusqlite::MAIN_DB, CHAT, column, i64::from(self.rowid), true)
119 .ok()
120 }
121}
122
123impl Chat {
124 #[must_use]
126 pub fn name(&self) -> &str {
127 match self.display_name() {
128 Some(name) => name,
129 None => &self.chat_identifier,
130 }
131 }
132
133 #[must_use]
135 pub fn display_name(&self) -> Option<&str> {
136 match &self.display_name {
137 Some(name) => {
138 if !name.is_empty() {
139 return Some(name.as_str());
140 }
141 None
142 }
143 None => None,
144 }
145 }
146
147 #[must_use]
149 pub fn service(&self) -> Service {
150 Service::from(self.service_name.as_deref())
151 }
152
153 #[must_use]
158 pub fn properties(&self, db: &Connection) -> Option<Properties> {
159 match Value::from_reader(self.get_blob(db, PROPERTIES)?) {
160 Ok(plist) => Properties::from_plist(&plist).ok(),
161 Err(_) => None,
162 }
163 }
164}
165
166#[cfg(test)]
167mod test_properties {
168 use plist::Value;
169 use std::env::current_dir;
170 use std::fs::File;
171
172 use crate::tables::chat::Properties;
173
174 #[test]
175 fn test_can_parse_properties_simple() {
176 let plist_path = current_dir()
177 .unwrap()
178 .as_path()
179 .join("test_data/chat_properties/ChatProp1.plist");
180 let plist_data = File::open(plist_path).unwrap();
181 let plist = Value::from_reader(plist_data).unwrap();
182 println!("Parsed plist: {plist:#?}");
183
184 let actual = Properties::from_plist(&plist).unwrap();
185 let expected = Properties {
186 read_receipts_enabled: false,
187 last_message_guid: Some(String::from("FF0615B9-C4AF-4BD8-B9A8-1B5F9351033F")),
188 forced_sms: false,
189 group_photo_guid: None,
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 };
212 print!("Parsed properties: {expected:?}");
213 assert_eq!(actual, expected);
214 }
215
216 #[test]
217 fn test_can_parse_properties_third_with_summary() {
218 let plist_path = current_dir()
219 .unwrap()
220 .as_path()
221 .join("test_data/chat_properties/ChatProp3.plist");
222 let plist_data = File::open(plist_path).unwrap();
223 let plist = Value::from_reader(plist_data).unwrap();
224 println!("Parsed plist: {plist:#?}");
225
226 let actual = Properties::from_plist(&plist).unwrap();
227 let expected = Properties {
228 read_receipts_enabled: false,
229 last_message_guid: Some(String::from("CEE419B6-17C7-42F7-8C2A-09A38CCA5730")),
230 forced_sms: false,
231 group_photo_guid: None,
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 };
254 print!("Parsed properties: {expected:?}");
255 assert_eq!(actual, expected);
256 }
257}