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