1use crabstep::TypedStreamDeserializer;
7use plist::Value;
8
9use crate::{
10 error::plist::PlistParseError,
11 message_types::variants::BalloonProvider,
12 tables::messages::{body::parse_body_typedstream, models::BubbleComponent},
13 util::{
14 dates::TIMESTAMP_FACTOR,
15 plist::{
16 extract_array_key, extract_bytes_key, extract_dictionary, extract_int_key,
17 plist_as_dictionary,
18 },
19 },
20};
21
22#[derive(Debug, PartialEq, Eq)]
24pub enum EditStatus {
25 Edited,
27 Unsent,
29 Original,
31}
32
33#[derive(Debug, PartialEq)]
35pub struct EditedEvent {
36 pub date: i64,
38 pub text: String,
41 pub components: Vec<BubbleComponent>,
43 pub guid: Option<String>,
45}
46
47impl EditedEvent {
48 pub(crate) fn new(
49 date: i64,
50 text: String,
51 components: Vec<BubbleComponent>,
52 guid: Option<String>,
53 ) -> Self {
54 Self {
55 date,
56 text,
57 components,
58 guid,
59 }
60 }
61}
62
63#[derive(Debug, PartialEq)]
65pub struct EditedMessagePart {
66 pub status: EditStatus,
68 pub edit_history: Vec<EditedEvent>,
70}
71
72impl Default for EditedMessagePart {
73 fn default() -> Self {
74 Self {
75 status: EditStatus::Original,
76 edit_history: vec![],
77 }
78 }
79}
80
81#[derive(Debug, PartialEq)]
111pub struct EditedMessage {
112 pub parts: Vec<EditedMessagePart>,
114}
115
116impl<'a> BalloonProvider<'a> for EditedMessage {
117 fn from_map(payload: &'a Value) -> Result<Self, PlistParseError> {
118 let plist_root = plist_as_dictionary(payload)?;
120
121 let message_parts = extract_dictionary(plist_root, "otr")?;
123
124 let mut edited = Self::with_capacity(message_parts.len());
126 message_parts
127 .values()
128 .for_each(|_| edited.parts.push(EditedMessagePart::default()));
129
130 if let Ok(edited_message_events) = extract_dictionary(plist_root, "ec") {
131 for (idx, (key, events)) in edited_message_events.iter().enumerate() {
132 let events = events
133 .as_array()
134 .ok_or_else(|| PlistParseError::InvalidTypeIndex(idx, "array".to_string()))?;
135 let parsed_key = key.parse::<usize>().map_err(|_| {
136 PlistParseError::InvalidType(
137 "ec dictionary key".to_string(),
138 "numeric string".to_string(),
139 )
140 })?;
141
142 for event in events {
143 let message_data = event.as_dictionary().ok_or_else(|| {
144 PlistParseError::InvalidTypeIndex(idx, "dictionary".to_string())
145 })?;
146
147 let timestamp = extract_int_key(message_data, "d")? * TIMESTAMP_FACTOR;
148
149 let data = extract_bytes_key(message_data, "t")?;
150
151 let mut typedstream = TypedStreamDeserializer::new(data);
152 let result = parse_body_typedstream(Some(typedstream.iter_root()?), None)
153 .ok_or_else(|| {
154 PlistParseError::InvalidEditedMessage(
155 "Failed to parse typedstream data".to_string(),
156 )
157 })?;
158
159 let text = result.text.ok_or_else(|| {
160 PlistParseError::InvalidEditedMessage(
161 "Edit-history entry missing text!".to_string(),
162 )
163 })?;
164
165 let guid = message_data
166 .get("bcg")
167 .and_then(|item| item.as_string())
168 .map(Into::into);
169
170 if let Some(item) = edited.parts.get_mut(parsed_key) {
171 item.status = EditStatus::Edited;
172 item.edit_history.push(EditedEvent::new(
173 timestamp,
174 text,
175 result.components,
176 guid,
177 ));
178 }
179 }
180 }
181 }
182
183 if let Ok(unsent_message_indexes) = extract_array_key(plist_root, "rp") {
184 for (idx, unsent_message_idx) in unsent_message_indexes.iter().enumerate() {
185 let parsed_idx = unsent_message_idx
186 .as_signed_integer()
187 .ok_or_else(|| PlistParseError::InvalidTypeIndex(idx, "int".to_string()))?
188 as usize;
189 if let Some(item) = edited.parts.get_mut(parsed_idx) {
190 item.status = EditStatus::Unsent;
191 }
192 }
193 }
194
195 Ok(edited)
196 }
197}
198
199impl EditedMessage {
200 fn with_capacity(capacity: usize) -> Self {
202 EditedMessage {
203 parts: Vec::with_capacity(capacity),
204 }
205 }
206
207 #[must_use]
209 pub fn part(&self, index: usize) -> Option<&EditedMessagePart> {
210 self.parts.get(index)
211 }
212
213 #[must_use]
215 pub fn is_unedited_at(&self, index: usize) -> bool {
216 match self.parts.get(index) {
217 Some(part) => matches!(part.status, EditStatus::Original),
218 None => false,
219 }
220 }
221
222 #[must_use]
224 pub fn items(&self) -> usize {
225 self.parts.len()
226 }
227}
228
229#[cfg(test)]
230mod test_parser {
231 use crate::message_types::edited::{EditStatus, EditedEvent, EditedMessagePart};
232 use crate::message_types::text_effects::{Style, TextEffect};
233 use crate::message_types::{edited::EditedMessage, variants::BalloonProvider};
234 use crate::tables::messages::models::{BubbleComponent, TextAttributes};
235
236 use plist::Value;
237 use std::env::current_dir;
238 use std::fs::File;
239
240 #[test]
241 fn test_parse_edited() {
242 let plist_path = current_dir()
243 .unwrap()
244 .as_path()
245 .join("test_data/edited_message/Edited.plist");
246 let plist_data = File::open(plist_path).unwrap();
247 let plist = Value::from_reader(plist_data).unwrap();
248 let parsed = EditedMessage::from_map(&plist).unwrap();
249
250 let expected = EditedMessage {
251 parts: vec![EditedMessagePart {
252 status: EditStatus::Edited,
253 edit_history: vec![
254 EditedEvent::new(
255 690513474000000000,
256 "First message ".to_string(),
257 vec![BubbleComponent::Text(vec![TextAttributes {
258 start: 0,
259 end: 15,
260 effects: vec![TextEffect::Default],
261 }])],
262 None,
263 ),
264 EditedEvent::new(
265 690513480000000000,
266 "Edit 1".to_string(),
267 vec![BubbleComponent::Text(vec![TextAttributes {
268 start: 0,
269 end: 6,
270 effects: vec![TextEffect::Default],
271 }])],
272 None,
273 ),
274 EditedEvent::new(
275 690513485000000000,
276 "Edit 2".to_string(),
277 vec![BubbleComponent::Text(vec![TextAttributes {
278 start: 0,
279 end: 6,
280 effects: vec![TextEffect::Default],
281 }])],
282 None,
283 ),
284 EditedEvent::new(
285 690513494000000000,
286 "Edited message".to_string(),
287 vec![BubbleComponent::Text(vec![TextAttributes {
288 start: 0,
289 end: 14,
290 effects: vec![TextEffect::Default],
291 }])],
292 None,
293 ),
294 ],
295 }],
296 };
297
298 assert_eq!(parsed, expected);
299
300 let expected_item = Some(expected.parts.first().unwrap());
301 assert_eq!(parsed.part(0), expected_item);
302 }
303
304 #[test]
305 fn test_parse_edited_to_link() {
306 let plist_path = current_dir()
307 .unwrap()
308 .as_path()
309 .join("test_data/edited_message/EditedToLink.plist");
310 let plist_data = File::open(plist_path).unwrap();
311 let plist = Value::from_reader(plist_data).unwrap();
312 let parsed = EditedMessage::from_map(&plist).unwrap();
313
314 let expected = EditedMessage {
315 parts: vec![
316 EditedMessagePart {
317 status: EditStatus::Original,
318 edit_history: vec![], },
320 EditedMessagePart {
321 status: EditStatus::Edited,
322 edit_history: vec![
323 EditedEvent::new(
324 690514004000000000,
325 "here we go!".to_string(),
326 vec![BubbleComponent::Text(vec![TextAttributes {
327 start: 0,
328 end: 11,
329 effects: vec![TextEffect::Default],
330 }])],
331 None,
332 ),
333 EditedEvent::new(
334 690514772000000000,
335 "https://github.com/ReagentX/imessage-exporter/issues/10".to_string(),
336 vec![BubbleComponent::Text(vec![TextAttributes {
337 start: 0,
338 end: 55,
339 effects: vec![TextEffect::Default],
340 }])],
341 Some("292BF9C6-C9B8-4827-BE65-6EA1C9B5B384".to_string()),
342 ),
343 ],
344 },
345 ],
346 };
347
348 assert_eq!(parsed, expected);
349 }
350
351 #[test]
352 fn test_parse_edited_to_link_and_back() {
353 let plist_path = current_dir()
354 .unwrap()
355 .as_path()
356 .join("test_data/edited_message/EditedToLinkAndBack.plist");
357 let plist_data = File::open(plist_path).unwrap();
358 let plist = Value::from_reader(plist_data).unwrap();
359 let parsed = EditedMessage::from_map(&plist).unwrap();
360
361 let expected = EditedMessage {
362 parts: vec![EditedMessagePart {
363 status: EditStatus::Edited,
364 edit_history: vec![
365 EditedEvent::new(
366 690514809000000000,
367 "This is a normal message".to_string(),
368 vec![BubbleComponent::Text(vec![TextAttributes {
369 start: 0,
370 end: 24,
371 effects: vec![TextEffect::Default],
372 }])],
373 None,
374 ),
375 EditedEvent::new(
376 690514819000000000,
377 "Edit to a url https://github.com/ReagentX/imessage-exporter/issues/10"
378 .to_string(),
379 vec![BubbleComponent::Text(vec![TextAttributes {
380 start: 0,
381 end: 69,
382 effects: vec![TextEffect::Default],
383 }])],
384 Some("0B9103FE-280C-4BD0-A66F-4EDEE3443247".to_string()),
385 ),
386 EditedEvent::new(
387 690514834000000000,
388 "And edit it back to a normal message...".to_string(),
389 vec![BubbleComponent::Text(vec![TextAttributes {
390 start: 0,
391 end: 39,
392 effects: vec![TextEffect::Default],
393 }])],
394 Some("0D93DF88-05BA-4418-9B20-79918ADD9923".to_string()),
395 ),
396 ],
397 }],
398 };
399
400 assert_eq!(parsed, expected);
401 }
402
403 #[test]
404 fn test_parse_deleted() {
405 let plist_path = current_dir()
406 .unwrap()
407 .as_path()
408 .join("test_data/edited_message/Deleted.plist");
409 let plist_data = File::open(plist_path).unwrap();
410 let plist = Value::from_reader(plist_data).unwrap();
411 let parsed = EditedMessage::from_map(&plist).unwrap();
412
413 let expected = EditedMessage {
414 parts: vec![EditedMessagePart {
415 status: EditStatus::Unsent,
416 edit_history: vec![],
417 }],
418 };
419
420 assert_eq!(parsed, expected);
421 }
422
423 #[test]
424 fn test_parse_multipart_deleted() {
425 let plist_path = current_dir()
426 .unwrap()
427 .as_path()
428 .join("test_data/edited_message/MultiPartOneDeleted.plist");
429 let plist_data = File::open(plist_path).unwrap();
430 let plist = Value::from_reader(plist_data).unwrap();
431 let parsed = EditedMessage::from_map(&plist).unwrap();
432
433 let expected = EditedMessage {
434 parts: vec![
435 EditedMessagePart {
436 status: EditStatus::Original,
437 edit_history: vec![],
438 },
439 EditedMessagePart {
440 status: EditStatus::Original,
441 edit_history: vec![],
442 },
443 EditedMessagePart {
444 status: EditStatus::Original,
445 edit_history: vec![],
446 },
447 EditedMessagePart {
448 status: EditStatus::Unsent,
449 edit_history: vec![],
450 },
451 ],
452 };
453
454 assert_eq!(parsed, expected);
455 }
456
457 #[test]
458 fn test_parse_multipart_edited_and_deleted() {
459 let plist_path = current_dir()
460 .unwrap()
461 .as_path()
462 .join("test_data/edited_message/EditedAndDeleted.plist");
463 let plist_data = File::open(plist_path).unwrap();
464 let plist = Value::from_reader(plist_data).unwrap();
465 let parsed = EditedMessage::from_map(&plist).unwrap();
466
467 let expected = EditedMessage {
468 parts: vec![
469 EditedMessagePart {
470 status: EditStatus::Original,
471 edit_history: vec![],
472 },
473 EditedMessagePart {
474 status: EditStatus::Original,
475 edit_history: vec![],
476 },
477 EditedMessagePart {
478 status: EditStatus::Edited,
479 edit_history: vec![
480 EditedEvent::new(
481 743907180000000000,
482 "Second message".to_string(),
483 vec![BubbleComponent::Text(vec![TextAttributes {
484 start: 0,
485 end: 14,
486 effects: vec![TextEffect::Default],
487 }])],
488 None,
489 ),
490 EditedEvent::new(
491 743907190000000000,
492 "Second message got edited!".to_string(),
493 vec![BubbleComponent::Text(vec![TextAttributes {
494 start: 0,
495 end: 26,
496 effects: vec![TextEffect::Default],
497 }])],
498 None,
499 ),
500 ],
501 },
502 ],
503 };
504
505 assert_eq!(parsed, expected);
506 }
507
508 #[test]
509 fn test_parse_multipart_edited_and_unsent() {
510 let plist_path = current_dir()
511 .unwrap()
512 .as_path()
513 .join("test_data/edited_message/EditedAndUnsent.plist");
514 let plist_data = File::open(plist_path).unwrap();
515 let plist = Value::from_reader(plist_data).unwrap();
516 let parsed = EditedMessage::from_map(&plist).unwrap();
517
518 let expected = EditedMessage {
519 parts: vec![
520 EditedMessagePart {
521 status: EditStatus::Original,
522 edit_history: vec![],
523 },
524 EditedMessagePart {
525 status: EditStatus::Original,
526 edit_history: vec![],
527 },
528 EditedMessagePart {
529 status: EditStatus::Edited,
530 edit_history: vec![
531 EditedEvent::new(
532 743907435000000000,
533 "Second test".to_string(),
534 vec![BubbleComponent::Text(vec![TextAttributes {
535 start: 0,
536 end: 11,
537 effects: vec![TextEffect::Default],
538 }])],
539 None,
540 ),
541 EditedEvent::new(
542 743907448000000000,
543 "Second test was edited!".to_string(),
544 vec![BubbleComponent::Text(vec![TextAttributes {
545 start: 0,
546 end: 23,
547 effects: vec![TextEffect::Default],
548 }])],
549 None,
550 ),
551 ],
552 },
553 EditedMessagePart {
554 status: EditStatus::Unsent,
555 edit_history: vec![],
556 },
557 ],
558 };
559
560 assert_eq!(parsed, expected);
561 }
562
563 #[test]
564 fn test_parse_edited_with_formatting() {
565 let plist_path = current_dir()
566 .unwrap()
567 .as_path()
568 .join("test_data/edited_message/EditedWithFormatting.plist");
569 let plist_data = File::open(plist_path).unwrap();
570 let plist = Value::from_reader(plist_data).unwrap();
571 let parsed = EditedMessage::from_map(&plist).unwrap();
572
573 let expected = EditedMessage {
574 parts: vec![EditedMessagePart {
575 status: EditStatus::Edited,
576 edit_history: vec![
577 EditedEvent::new(
578 758573156000000000,
579 "Test".to_string(),
580 vec![BubbleComponent::Text(vec![TextAttributes {
581 start: 0,
582 end: 4,
583 effects: vec![TextEffect::Default],
584 }])],
585 None,
586 ),
587 EditedEvent::new(
588 758573166000000000,
589 "Test".to_string(),
590 vec![BubbleComponent::Text(vec![TextAttributes {
591 start: 0,
592 end: 4,
593 effects: vec![TextEffect::Styles(vec![Style::Strikethrough])],
594 }])],
595 Some("76A466B8-D21E-4A20-AF62-FF2D3A20D31C".to_string()),
596 ),
597 ],
598 }],
599 };
600
601 assert_eq!(parsed, expected);
602
603 let expected_item = Some(expected.parts.first().unwrap());
604 assert_eq!(parsed.part(0), expected_item);
605 }
606}
607
608#[cfg(test)]
609mod test_gen {
610 use plist::Value;
611 use std::env::current_dir;
612 use std::fs::File;
613
614 use crate::message_types::text_effects::{Style, TextEffect};
615 use crate::message_types::{edited::EditedMessage, variants::BalloonProvider};
616 use crate::tables::messages::models::{BubbleComponent, TextAttributes};
617
618 #[test]
619 fn test_parse_edited() {
620 let plist_path = current_dir()
621 .unwrap()
622 .as_path()
623 .join("test_data/edited_message/Edited.plist");
624 let plist_data = File::open(plist_path).unwrap();
625 let plist = Value::from_reader(plist_data).unwrap();
626 let parsed = EditedMessage::from_map(&plist).unwrap();
627
628 let expected_attrs = [
629 vec![BubbleComponent::Text(vec![TextAttributes::new(
630 0,
631 15,
632 vec![TextEffect::Default],
633 )])],
634 vec![BubbleComponent::Text(vec![TextAttributes::new(
635 0,
636 6,
637 vec![TextEffect::Default],
638 )])],
639 vec![BubbleComponent::Text(vec![TextAttributes::new(
640 0,
641 6,
642 vec![TextEffect::Default],
643 )])],
644 vec![BubbleComponent::Text(vec![TextAttributes::new(
645 0,
646 14,
647 vec![TextEffect::Default],
648 )])],
649 ];
650
651 for event in parsed.parts {
652 for (idx, part) in event.edit_history.iter().enumerate() {
653 assert_eq!(part.components, expected_attrs[idx]);
654 }
655 }
656 }
657
658 #[test]
659 fn test_parse_edited_to_link() {
660 let plist_path = current_dir()
661 .unwrap()
662 .as_path()
663 .join("test_data/edited_message/EditedToLink.plist");
664 let plist_data = File::open(plist_path).unwrap();
665 let plist = Value::from_reader(plist_data).unwrap();
666 let parsed = EditedMessage::from_map(&plist).unwrap();
667
668 let expected_attrs = [
669 vec![BubbleComponent::Text(vec![TextAttributes::new(
670 0,
671 11,
672 vec![TextEffect::Default],
673 )])],
674 vec![BubbleComponent::Text(vec![TextAttributes::new(
675 0,
676 55,
677 vec![TextEffect::Default],
678 )])],
679 ];
680
681 for event in parsed.parts {
682 for (idx, part) in event.edit_history.iter().enumerate() {
683 assert_eq!(part.components, expected_attrs[idx]);
684 }
685 }
686 }
687
688 #[test]
689 fn test_parse_edited_to_link_and_back() {
690 let plist_path = current_dir()
691 .unwrap()
692 .as_path()
693 .join("test_data/edited_message/EditedToLinkAndBack.plist");
694 let plist_data = File::open(plist_path).unwrap();
695 let plist = Value::from_reader(plist_data).unwrap();
696 let parsed = EditedMessage::from_map(&plist).unwrap();
697
698 let expected_attrs = [
699 vec![BubbleComponent::Text(vec![TextAttributes::new(
700 0,
701 24,
702 vec![TextEffect::Default],
703 )])],
704 vec![BubbleComponent::Text(vec![TextAttributes::new(
705 0,
706 69,
707 vec![TextEffect::Default],
708 )])],
709 vec![BubbleComponent::Text(vec![TextAttributes::new(
710 0,
711 39,
712 vec![TextEffect::Default],
713 )])],
714 ];
715
716 for event in parsed.parts {
717 for (idx, part) in event.edit_history.iter().enumerate() {
718 assert_eq!(part.components, expected_attrs[idx]);
719 }
720 }
721 }
722
723 #[test]
724 fn test_parse_deleted() {
725 let plist_path = current_dir()
726 .unwrap()
727 .as_path()
728 .join("test_data/edited_message/Deleted.plist");
729 let plist_data = File::open(plist_path).unwrap();
730 let plist = Value::from_reader(plist_data).unwrap();
731 let parsed = EditedMessage::from_map(&plist).unwrap();
732
733 let expected_attrs: [Vec<BubbleComponent>; 0] = [];
734
735 for event in parsed.parts {
736 for (idx, part) in event.edit_history.iter().enumerate() {
737 assert_eq!(part.components, expected_attrs[idx]);
738 }
739 }
740 }
741
742 #[test]
743 fn test_parse_multipart_deleted() {
744 let plist_path = current_dir()
745 .unwrap()
746 .as_path()
747 .join("test_data/edited_message/MultiPartOneDeleted.plist");
748 let plist_data = File::open(plist_path).unwrap();
749 let plist = Value::from_reader(plist_data).unwrap();
750 let parsed = EditedMessage::from_map(&plist).unwrap();
751
752 let expected_attrs: [Vec<BubbleComponent>; 0] = [];
753
754 for event in parsed.parts {
755 for (idx, part) in event.edit_history.iter().enumerate() {
756 assert_eq!(part.components, expected_attrs[idx]);
757 }
758 }
759 }
760
761 #[test]
762 fn test_parse_multipart_edited_and_deleted() {
763 let plist_path = current_dir()
764 .unwrap()
765 .as_path()
766 .join("test_data/edited_message/EditedAndDeleted.plist");
767 let plist_data = File::open(plist_path).unwrap();
768 let plist = Value::from_reader(plist_data).unwrap();
769 let parsed = EditedMessage::from_map(&plist).unwrap();
770
771 let expected_attrs = [
772 vec![BubbleComponent::Text(vec![TextAttributes::new(
773 0,
774 14,
775 vec![TextEffect::Default],
776 )])],
777 vec![BubbleComponent::Text(vec![TextAttributes::new(
778 0,
779 26,
780 vec![TextEffect::Default],
781 )])],
782 ];
783
784 for event in parsed.parts {
785 for (idx, part) in event.edit_history.iter().enumerate() {
786 assert_eq!(part.components, expected_attrs[idx]);
787 }
788 }
789 }
790
791 #[test]
792 fn test_parse_multipart_edited_and_unsent() {
793 let plist_path = current_dir()
794 .unwrap()
795 .as_path()
796 .join("test_data/edited_message/EditedAndUnsent.plist");
797 let plist_data = File::open(plist_path).unwrap();
798 let plist = Value::from_reader(plist_data).unwrap();
799 let parsed = EditedMessage::from_map(&plist).unwrap();
800
801 for parts in &parsed.parts {
802 for part in &parts.edit_history {
803 println!("{:#?}", part.components);
804 }
805 }
806
807 let expected_attrs: [Vec<BubbleComponent>; 2] = [
808 vec![BubbleComponent::Text(vec![TextAttributes::new(
809 0,
810 11,
811 vec![TextEffect::Default],
812 )])],
813 vec![BubbleComponent::Text(vec![TextAttributes::new(
814 0,
815 23,
816 vec![TextEffect::Default],
817 )])],
818 ];
819
820 for event in parsed.parts {
821 for (idx, part) in event.edit_history.iter().enumerate() {
822 assert_eq!(part.components, expected_attrs[idx]);
823 }
824 }
825 }
826
827 #[test]
828 fn test_parse_edited_with_formatting() {
829 let plist_path = current_dir()
830 .unwrap()
831 .as_path()
832 .join("test_data/edited_message/EditedWithFormatting.plist");
833 let plist_data = File::open(plist_path).unwrap();
834 let plist = Value::from_reader(plist_data).unwrap();
835 let parsed = EditedMessage::from_map(&plist).unwrap();
836
837 let expected_attrs: [Vec<BubbleComponent>; 2] = [
838 vec![BubbleComponent::Text(vec![TextAttributes::new(
839 0,
840 4,
841 vec![TextEffect::Default],
842 )])],
843 vec![BubbleComponent::Text(vec![TextAttributes::new(
844 0,
845 4,
846 vec![TextEffect::Styles(vec![Style::Strikethrough])],
847 )])],
848 ];
849
850 for event in parsed.parts {
851 for (idx, part) in event.edit_history.iter().enumerate() {
852 assert_eq!(part.components, expected_attrs[idx]);
853 }
854 }
855 }
856}