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