imessage_database/message_types/
url.rs1use plist::Value;
8
9use crate::{
10 error::plist::PlistParseError,
11 message_types::{
12 app_store::AppStoreMessage,
13 collaboration::CollaborationMessage,
14 music::MusicMessage,
15 placemark::PlacemarkMessage,
16 variants::{BalloonProvider, URLOverride},
17 },
18 util::plist::{get_bool_from_dict, get_string_from_dict, get_string_from_nested_dict},
19};
20
21#[derive(Debug, PartialEq, Eq)]
24pub struct URLMessage<'a> {
25 pub title: Option<&'a str>,
27 pub summary: Option<&'a str>,
29 pub url: Option<&'a str>,
31 pub original_url: Option<&'a str>,
33 pub item_type: Option<&'a str>,
35 pub images: Vec<&'a str>,
37 pub icons: Vec<&'a str>,
39 pub site_name: Option<&'a str>,
41 pub placeholder: bool,
42}
43
44impl<'a> BalloonProvider<'a> for URLMessage<'a> {
45 fn from_map(payload: &'a Value) -> Result<Self, PlistParseError> {
46 let url_metadata = URLMessage::get_body(payload)?;
47 Ok(URLMessage {
48 title: get_string_from_dict(url_metadata, "title"),
49 summary: get_string_from_dict(url_metadata, "summary"),
50 url: get_string_from_nested_dict(url_metadata, "URL"),
51 original_url: get_string_from_nested_dict(url_metadata, "originalURL"),
52 item_type: get_string_from_dict(url_metadata, "itemType"),
53 images: URLMessage::get_array_from_nested_dict(url_metadata, "images")
54 .unwrap_or_default(),
55 icons: URLMessage::get_array_from_nested_dict(url_metadata, "icons")
56 .unwrap_or_default(),
57 site_name: get_string_from_dict(url_metadata, "siteName"),
58 placeholder: get_bool_from_dict(url_metadata, "richLinkIsPlaceholder").unwrap_or(false),
59 })
60 }
61}
62
63impl<'a> URLMessage<'a> {
64 pub fn get_url_message_override(
66 payload: &'a Value,
67 ) -> Result<URLOverride<'a>, PlistParseError> {
68 if let Ok(balloon) = CollaborationMessage::from_map(payload) {
69 return Ok(URLOverride::Collaboration(balloon));
70 }
71 if let Ok(balloon) = MusicMessage::from_map(payload) {
72 return Ok(URLOverride::AppleMusic(balloon));
73 }
74 if let Ok(balloon) = AppStoreMessage::from_map(payload) {
75 return Ok(URLOverride::AppStore(balloon));
76 }
77 if let Ok(balloon) = PlacemarkMessage::from_map(payload) {
78 return Ok(URLOverride::SharedPlacemark(balloon));
79 }
80 if let Ok(balloon) = URLMessage::from_map(payload) {
81 return Ok(URLOverride::Normal(balloon));
82 }
83 Err(PlistParseError::NoPayload)
84 }
85
86 fn get_body(payload: &'a Value) -> Result<&'a Value, PlistParseError> {
91 let root_dict = payload.as_dictionary().ok_or_else(|| {
92 PlistParseError::InvalidType("root".to_string(), "dictionary".to_string())
93 })?;
94
95 if let Some(meta) = root_dict.get("richLinkMetadata") {
96 return Ok(meta);
97 }
98 if let Some(meta) = root_dict.get("metadata") {
99 return Ok(meta);
100 }
101 Err(PlistParseError::NoPayload)
102 }
103
104 fn get_array_from_nested_dict(payload: &'a Value, key: &str) -> Option<Vec<&'a str>> {
120 payload
121 .as_dictionary()?
122 .get(key)?
123 .as_dictionary()?
124 .get(key)?
125 .as_array()?
126 .iter()
127 .map(|item| get_string_from_nested_dict(item, "URL"))
128 .collect()
129 }
130
131 #[must_use]
133 pub fn get_url(&self) -> Option<&str> {
134 self.url.or(self.original_url)
135 }
136}
137
138#[cfg(test)]
139mod url_tests {
140 use crate::{
141 message_types::{url::URLMessage, variants::BalloonProvider},
142 util::plist::parse_ns_keyed_archiver,
143 };
144 use plist::Value;
145 use std::env::current_dir;
146 use std::fs::File;
147
148 #[test]
149 fn test_parse_url_me() {
150 let plist_path = current_dir()
151 .unwrap()
152 .as_path()
153 .join("test_data/url_message/URL.plist");
154 let plist_data = File::open(plist_path).unwrap();
155 let plist = Value::from_reader(plist_data).unwrap();
156 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
157
158 let balloon = URLMessage::from_map(&parsed).unwrap();
159 let expected = URLMessage {
160 title: Some("Christopher Sardegna"),
161 summary: None,
162 url: Some("https://chrissardegna.com/"),
163 original_url: Some("https://chrissardegna.com"),
164 item_type: None,
165 images: vec![],
166 icons: vec!["https://chrissardegna.com/favicon.ico"],
167 site_name: None,
168 placeholder: false,
169 };
170
171 assert_eq!(balloon, expected);
172 }
173
174 #[test]
175 fn test_parse_url_me_metadata() {
176 let plist_path = current_dir()
177 .unwrap()
178 .as_path()
179 .join("test_data/url_message/MetadataURL.plist");
180 let plist_data = File::open(plist_path).unwrap();
181 let plist = Value::from_reader(plist_data).unwrap();
182 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
183
184 let balloon = URLMessage::from_map(&parsed).unwrap();
185 let expected = URLMessage {
186 title: Some("Christopher Sardegna"),
187 summary: Some("Sample page description"),
188 url: Some("https://chrissardegna.com"),
189 original_url: Some("https://chrissardegna.com"),
190 item_type: Some("article"),
191 images: vec!["https://chrissardegna.com/ddc-facebook-icon.png"],
192 icons: vec![
193 "https://chrissardegna.com/apple-touch-icon-180x180.png",
194 "https://chrissardegna.com/ddc-icon-32x32.png",
195 "https://chrissardegna.com/ddc-icon-16x16.png",
196 ],
197 site_name: Some("Christopher Sardegna"),
198 placeholder: false,
199 };
200
201 assert_eq!(balloon, expected);
202 }
203
204 #[test]
205 fn test_parse_url_twitter() {
206 let plist_path = current_dir()
207 .unwrap()
208 .as_path()
209 .join("test_data/url_message/Twitter.plist");
210 let plist_data = File::open(plist_path).unwrap();
211 let plist = Value::from_reader(plist_data).unwrap();
212 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
213
214 let balloon = URLMessage::from_map(&parsed).unwrap();
215 let expected = URLMessage {
216 title: Some("Christopher Sardegna on Twitter"),
217 summary: Some("“Hello Twitter, meet Bella”"),
218 url: Some("https://twitter.com/rxcs/status/1175874352946077696"),
219 original_url: Some("https://twitter.com/rxcs/status/1175874352946077696"),
220 item_type: Some("article"),
221 images: vec![
222 "https://pbs.twimg.com/media/EFGLfR2X4AE8ItK.jpg:large",
223 "https://pbs.twimg.com/media/EFGLfRmX4AMnwqW.jpg:large",
224 "https://pbs.twimg.com/media/EFGLfRlXYAYn9Ce.jpg:large",
225 ],
226 icons: vec![
227 "https://abs.twimg.com/icons/apple-touch-icon-192x192.png",
228 "https://abs.twimg.com/favicons/favicon.ico",
229 ],
230 site_name: Some("Twitter"),
231 placeholder: false,
232 };
233
234 assert_eq!(balloon, expected);
235 }
236
237 #[test]
238 fn test_parse_url_reminder() {
239 let plist_path = current_dir()
240 .unwrap()
241 .as_path()
242 .join("test_data/url_message/Reminder.plist");
243 let plist_data = File::open(plist_path).unwrap();
244 let plist = Value::from_reader(plist_data).unwrap();
245 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
246
247 let balloon = URLMessage::from_map(&parsed).unwrap();
248 let expected = URLMessage {
249 title: None,
250 summary: None,
251 url: None,
252 original_url: Some(
253 "https://www.icloud.com/reminders/ZmFrZXVybF9mb3JfcmVtaW5kZXI#TestList",
254 ),
255 item_type: None,
256 images: vec![],
257 icons: vec![],
258 site_name: None,
259 placeholder: false,
260 };
261
262 assert_eq!(balloon, expected);
263 }
264
265 #[test]
266 fn test_get_url() {
267 let expected = URLMessage {
268 title: Some("Christopher Sardegna"),
269 summary: None,
270 url: Some("https://chrissardegna.com/"),
271 original_url: Some("https://chrissardegna.com"),
272 item_type: None,
273 images: vec![],
274 icons: vec!["https://chrissardegna.com/favicon.ico"],
275 site_name: None,
276 placeholder: false,
277 };
278 assert_eq!(expected.get_url(), Some("https://chrissardegna.com/"));
279 }
280
281 #[test]
282 fn test_get_original_url() {
283 let expected = URLMessage {
284 title: Some("Christopher Sardegna"),
285 summary: None,
286 url: None,
287 original_url: Some("https://chrissardegna.com"),
288 item_type: None,
289 images: vec![],
290 icons: vec!["https://chrissardegna.com/favicon.ico"],
291 site_name: None,
292 placeholder: false,
293 };
294 assert_eq!(expected.get_url(), Some("https://chrissardegna.com"));
295 }
296
297 #[test]
298 fn test_get_no_url() {
299 let expected = URLMessage {
300 title: Some("Christopher Sardegna"),
301 summary: None,
302 url: None,
303 original_url: None,
304 item_type: None,
305 images: vec![],
306 icons: vec!["https://chrissardegna.com/favicon.ico"],
307 site_name: None,
308 placeholder: false,
309 };
310 assert_eq!(expected.get_url(), None);
311 }
312}
313
314#[cfg(test)]
315mod url_override_tests {
316 use crate::{
317 message_types::{url::URLMessage, variants::URLOverride},
318 util::plist::parse_ns_keyed_archiver,
319 };
320 use plist::Value;
321 use std::env::current_dir;
322 use std::fs::File;
323
324 #[test]
325 fn can_parse_normal() {
326 let plist_path = current_dir()
327 .unwrap()
328 .as_path()
329 .join("test_data/url_message/URL.plist");
330 let plist_data = File::open(plist_path).unwrap();
331 let plist = Value::from_reader(plist_data).unwrap();
332 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
333
334 let balloon = URLMessage::get_url_message_override(&parsed).unwrap();
335 assert!(matches!(balloon, URLOverride::Normal(_)));
336 }
337
338 #[test]
339 fn can_parse_music() {
340 let plist_path = current_dir()
341 .unwrap()
342 .as_path()
343 .join("test_data/music_message/AppleMusic.plist");
344 let plist_data = File::open(plist_path).unwrap();
345 let plist = Value::from_reader(plist_data).unwrap();
346 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
347
348 let balloon = URLMessage::get_url_message_override(&parsed).unwrap();
349 assert!(matches!(balloon, URLOverride::AppleMusic(_)));
350 }
351
352 #[test]
353 fn can_parse_app_store() {
354 let plist_path = current_dir()
355 .unwrap()
356 .as_path()
357 .join("test_data/app_store/AppStoreLink.plist");
358 let plist_data = File::open(plist_path).unwrap();
359 let plist = Value::from_reader(plist_data).unwrap();
360 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
361
362 let balloon = URLMessage::get_url_message_override(&parsed).unwrap();
363 assert!(matches!(balloon, URLOverride::AppStore(_)));
364 }
365
366 #[test]
367 fn can_parse_collaboration() {
368 let plist_path = current_dir()
369 .unwrap()
370 .as_path()
371 .join("test_data/collaboration_message/Freeform.plist");
372 let plist_data = File::open(plist_path).unwrap();
373 let plist = Value::from_reader(plist_data).unwrap();
374 let parsed = parse_ns_keyed_archiver(&plist).unwrap();
375
376 let balloon = URLMessage::get_url_message_override(&parsed).unwrap();
377 assert!(matches!(balloon, URLOverride::Collaboration(_)));
378 }
379
380 #[test]
381 fn can_parse_placemark() {
382 let plist_path = current_dir()
383 .unwrap()
384 .as_path()
385 .join("test_data/shared_placemark/SharedPlacemark.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 = URLMessage::get_url_message_override(&parsed).unwrap();
391 println!("{balloon:?}");
392 assert!(matches!(balloon, URLOverride::SharedPlacemark(_)));
393 }
394}