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