1use std::collections::HashMap;
7
8use plist::Value;
9
10use crate::{
11 error::plist::PlistParseError,
12 message_types::variants::BalloonProvider,
13 util::plist::{get_string_from_dict, get_string_from_nested_dict},
14};
15
16#[derive(Debug, PartialEq, Eq)]
18pub struct AppMessage<'a> {
19 pub image: Option<&'a str>,
21 pub url: Option<&'a str>,
23 pub title: Option<&'a str>,
25 pub subtitle: Option<&'a str>,
27 pub caption: Option<&'a str>,
29 pub subcaption: Option<&'a str>,
31 pub trailing_caption: Option<&'a str>,
33 pub trailing_subcaption: Option<&'a str>,
35 pub app_name: Option<&'a str>,
37 pub ldtext: Option<&'a str>,
40}
41
42impl<'a> BalloonProvider<'a> for AppMessage<'a> {
43 fn from_map(payload: &'a Value) -> Result<Self, PlistParseError> {
44 let user_info = payload
45 .as_dictionary()
46 .ok_or_else(|| {
47 PlistParseError::InvalidType("root".to_string(), "dictionary".to_string())
48 })?
49 .get("userInfo")
50 .ok_or_else(|| PlistParseError::MissingKey("userInfo".to_string()))?;
51 Ok(AppMessage {
52 image: get_string_from_dict(payload, "image"),
53 url: get_string_from_nested_dict(payload, "URL"),
54 title: get_string_from_dict(user_info, "image-title"),
55 subtitle: get_string_from_dict(user_info, "image-subtitle"),
56 caption: get_string_from_dict(user_info, "caption"),
57 subcaption: get_string_from_dict(user_info, "subcaption"),
58 trailing_caption: get_string_from_dict(user_info, "secondary-subcaption"),
59 trailing_subcaption: get_string_from_dict(user_info, "tertiary-subcaption"),
60 app_name: get_string_from_dict(payload, "an"),
61 ldtext: get_string_from_dict(payload, "ldtext"),
62 })
63 }
64}
65
66impl AppMessage<'_> {
67 pub fn parse_query_string(&self) -> HashMap<&str, &str> {
69 let mut map = HashMap::new();
70
71 if let Some(url) = self.url {
72 if url.starts_with('?') {
73 let parts = url.strip_prefix('?').unwrap_or(url).split('&');
74 for part in parts {
75 let key_val_split: Vec<&str> = part.split('=').collect();
76 if key_val_split.len() == 2 {
77 map.insert(key_val_split[0], key_val_split[1]);
78 }
79 }
80 }
81 }
82
83 map
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use crate::{
90 message_types::{app::AppMessage, variants::BalloonProvider},
91 util::plist::parse_ns_keyed_archiver,
92 };
93 use plist::Value;
94 use std::fs::File;
95 use std::{collections::HashMap, env::current_dir};
96
97 #[test]
98 fn test_parse_apple_pay_sent_265() {
99 let plist_path = current_dir()
100 .unwrap()
101 .as_path()
102 .join("test_data/app_message/Sent265.plist");
103 let plist_data = File::open(plist_path).unwrap();
104 let plist = Value::from_reader(plist_data).unwrap();
105 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
106
107 let balloon = AppMessage::from_map(&parsed).unwrap();
108 let expected = AppMessage {
109 image: None,
110 url: Some("data:application/vnd.apple.pkppm;base64,FAKE_BASE64_DATA="),
111 title: None,
112 subtitle: None,
113 caption: Some("Apple\u{a0}Cash"),
114 subcaption: Some("$265\u{a0}Payment"),
115 trailing_caption: None,
116 trailing_subcaption: None,
117 app_name: Some("Apple\u{a0}Pay"),
118 ldtext: Some("Sent $265 with Apple\u{a0}Pay."),
119 };
120
121 assert_eq!(balloon, expected);
122 }
123
124 #[test]
125 fn test_parse_apple_pay_recurring_1() {
126 let plist_path = current_dir()
127 .unwrap()
128 .as_path()
129 .join("test_data/app_message/ApplePayRecurring.plist");
130 let plist_data = File::open(plist_path).unwrap();
131 let plist = Value::from_reader(plist_data).unwrap();
132 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
133
134 let balloon = AppMessage::from_map(&parsed).unwrap();
135 let expected = AppMessage {
136 image: None,
137 url: Some("data:application/vnd.apple.pkppm;base64,FAKEDATA"),
138 title: None,
139 subtitle: None,
140 caption: None,
141 subcaption: None,
142 trailing_caption: None,
143 trailing_subcaption: None,
144 app_name: Some("Apple\u{a0}Cash"),
145 ldtext: Some("Sending you $1 weekly starting Nov 18, 2023"),
146 };
147
148 assert_eq!(balloon, expected);
149 }
150
151 #[test]
152 fn test_parse_opentable_invite() {
153 let plist_path = current_dir()
154 .unwrap()
155 .as_path()
156 .join("test_data/app_message/OpenTableInvited.plist");
157 let plist_data = File::open(plist_path).unwrap();
158 let plist = Value::from_reader(plist_data).unwrap();
159 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
160
161 let balloon = AppMessage::from_map(&parsed).unwrap();
162 let expected = AppMessage {
163 image: None,
164 url: Some("https://www.opentable.com/book/view?rid=0000000&confnumber=00000&invitationId=1234567890-abcd-def-ghij-4u5t1sv3ryc00l"),
165 title: Some("Rusty Grill - Boise"),
166 subtitle: Some("Reservation Confirmed"),
167 caption: Some("Table for 4 people\nSunday, October 17 at 7:45 PM"),
168 subcaption: Some("You're invited! Tap to accept."),
169 trailing_caption: None,
170 trailing_subcaption: None,
171 app_name: Some("OpenTable"),
172 ldtext: None,
173 };
174
175 assert_eq!(balloon, expected);
176 }
177
178 #[test]
179 fn test_parse_slideshow() {
180 let plist_path = current_dir()
181 .unwrap()
182 .as_path()
183 .join("test_data/app_message/Slideshow.plist");
184 let plist_data = File::open(plist_path).unwrap();
185 let plist = Value::from_reader(plist_data).unwrap();
186 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
187
188 let balloon = AppMessage::from_map(&parsed).unwrap();
189 let expected = AppMessage {
190 image: None,
191 url: Some("https://share.icloud.com/photos/1337h4x0r_jk#Home"),
192 title: None,
193 subtitle: None,
194 caption: Some("Home"),
195 subcaption: Some("37 Photos"),
196 trailing_caption: None,
197 trailing_subcaption: None,
198 app_name: Some("Photos"),
199 ldtext: Some("Home - 37 Photos"),
200 };
201
202 assert_eq!(balloon, expected);
203 }
204
205 #[test]
206 fn test_parse_game() {
207 let plist_path = current_dir()
208 .unwrap()
209 .as_path()
210 .join("test_data/app_message/Game.plist");
211 let plist_data = File::open(plist_path).unwrap();
212 let plist = Value::from_reader(plist_data).unwrap();
213 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
214
215 let balloon = AppMessage::from_map(&parsed).unwrap();
216 let expected = AppMessage {
217 image: None,
218 url: Some("data:?ver=48&data=pr3t3ndth3r3154b10b0fd4t4h3re=3"),
219 title: None,
220 subtitle: None,
221 caption: Some("Your move."),
222 subcaption: None,
223 trailing_caption: None,
224 trailing_subcaption: None,
225 app_name: Some("GamePigeon"),
226 ldtext: Some("Dots & Boxes"),
227 };
228
229 assert_eq!(balloon, expected);
230 }
231
232 #[test]
233 fn test_parse_business() {
234 let plist_path = current_dir()
235 .unwrap()
236 .as_path()
237 .join("test_data/app_message/Business.plist");
238 let plist_data = File::open(plist_path).unwrap();
239 let plist = Value::from_reader(plist_data).unwrap();
240 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
241
242 let balloon = AppMessage::from_map(&parsed).unwrap();
243 let expected = AppMessage {
244 image: None,
245 url: Some("?receivedMessage=33c309ab520bc2c76e99c493157ed578&replyMessage=6a991da615f2e75d4aa0de334e529024"),
246 title: None,
247 subtitle: None,
248 caption: Some("Yes, connect me with Goldman Sachs."),
249 subcaption: None,
250 trailing_caption: None,
251 trailing_subcaption: None,
252 app_name: Some("Business"),
253 ldtext: Some("Yes, connect me with Goldman Sachs."),
254 };
255
256 assert_eq!(balloon, expected);
257 }
258
259 #[test]
260 fn test_parse_business_query_string() {
261 let plist_path = current_dir()
262 .unwrap()
263 .as_path()
264 .join("test_data/app_message/Business.plist");
265 let plist_data = File::open(plist_path).unwrap();
266 let plist = Value::from_reader(plist_data).unwrap();
267 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
268
269 let balloon = AppMessage::from_map(&parsed).unwrap();
270 let mut expected = HashMap::new();
271 expected.insert("receivedMessage", "33c309ab520bc2c76e99c493157ed578");
272 expected.insert("replyMessage", "6a991da615f2e75d4aa0de334e529024");
273
274 assert_eq!(balloon.parse_query_string(), expected);
275 }
276
277 #[test]
278 fn test_parse_check_in_timer() {
279 let plist_path = current_dir()
280 .unwrap()
281 .as_path()
282 .join("test_data/app_message/CheckinTimer.plist");
283 let plist_data = File::open(plist_path).unwrap();
284 let plist = Value::from_reader(plist_data).unwrap();
285 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
286
287 let balloon = AppMessage::from_map(&parsed).unwrap();
288
289 let expected = AppMessage {
290 image: None,
291 url: Some("?messageType=1&interfaceVersion=1&sendDate=1697316869.688709"),
292 title: None,
293 subtitle: None,
294 caption: Some("Check In: Timer Started"),
295 subcaption: None,
296 trailing_caption: None,
297 trailing_subcaption: None,
298 app_name: Some("Check In"),
299 ldtext: Some("Check In: Timer Started"),
300 };
301
302 assert_eq!(balloon, expected);
303 }
304
305 #[test]
306 fn test_parse_check_in_timer_late() {
307 let plist_path = current_dir()
308 .unwrap()
309 .as_path()
310 .join("test_data/app_message/CheckinLate.plist");
311 let plist_data = File::open(plist_path).unwrap();
312 let plist = Value::from_reader(plist_data).unwrap();
313 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
314
315 let balloon = AppMessage::from_map(&parsed).unwrap();
316
317 let expected = AppMessage {
318 image: None,
319 url: Some("?messageType=1&interfaceVersion=1&sendDate=1697316869.688709"),
320 title: None,
321 subtitle: None,
322 caption: Some("Check In: Has not checked in when expected, location shared"),
323 subcaption: None,
324 trailing_caption: None,
325 trailing_subcaption: None,
326 app_name: Some("Check In"),
327 ldtext: Some("Check In: Has not checked in when expected, location shared"),
328 };
329
330 assert_eq!(balloon, expected);
331 }
332
333 #[test]
334 fn test_parse_check_in_location() {
335 let plist_path = current_dir()
336 .unwrap()
337 .as_path()
338 .join("test_data/app_message/CheckinLocation.plist");
339 let plist_data = File::open(plist_path).unwrap();
340 let plist = Value::from_reader(plist_data).unwrap();
341 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
342
343 let balloon = AppMessage::from_map(&parsed).unwrap();
344
345 let expected = AppMessage {
346 image: None,
347 url: Some("?messageType=1&interfaceVersion=1&sendDate=1697316869.688709"),
348 title: None,
349 subtitle: None,
350 caption: Some("Check In: Fake Location"),
351 subcaption: None,
352 trailing_caption: None,
353 trailing_subcaption: None,
354 app_name: Some("Check In"),
355 ldtext: Some("Check In: Fake Location"),
356 };
357
358 assert_eq!(balloon, expected);
359 }
360
361 #[test]
362 fn test_parse_check_in_query_string() {
363 let plist_path = current_dir()
364 .unwrap()
365 .as_path()
366 .join("test_data/app_message/CheckinTimer.plist");
367 let plist_data = File::open(plist_path).unwrap();
368 let plist = Value::from_reader(plist_data).unwrap();
369 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
370
371 let balloon = AppMessage::from_map(&parsed).unwrap();
372 let mut expected = HashMap::new();
373 expected.insert("messageType", "1");
374 expected.insert("interfaceVersion", "1");
375 expected.insert("sendDate", "1697316869.688709");
376
377 assert_eq!(balloon.parse_query_string(), expected);
378 }
379
380 #[test]
381 fn test_parse_find_my() {
382 let plist_path = current_dir()
383 .unwrap()
384 .as_path()
385 .join("test_data/app_message/FindMy.plist");
386 let plist_data = File::open(plist_path).unwrap();
387 let plist = Value::from_reader(plist_data).unwrap();
388 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
389
390 let balloon = AppMessage::from_map(&parsed).unwrap();
391 let expected = AppMessage {
392 image: None,
393 url: Some(
394 "?FindMyMessagePayloadVersionKey=v0&FindMyMessagePayloadZippedDataKey=FAKEDATA",
395 ),
396 title: None,
397 subtitle: None,
398 caption: None,
399 subcaption: None,
400 trailing_caption: None,
401 trailing_subcaption: None,
402 app_name: Some("Find My"),
403 ldtext: Some("Started Sharing Location"),
404 };
405
406 assert_eq!(balloon, expected);
407 }
408}