imessage_database/message_types/
polls.rs1use std::collections::{HashMap, HashSet};
6
7use base64::{Engine as _, engine::general_purpose};
8use jzon::{self, JsonValue};
9use plist::Value;
10
11use crate::{
12 error::plist::PlistParseError,
13 util::plist::{get_string_from_nested_dict, parse_ns_keyed_archiver},
14};
15
16pub type PollOptionID = String;
18
19#[derive(Debug, PartialEq, Eq)]
21pub struct PollOption {
22 pub text: String,
24 pub creator: String,
26 pub votes: Vec<PollVote>,
28}
29
30impl PollOption {
31 fn from_json(json_data: &JsonValue) -> (PollOptionID, Self) {
32 let poll_id = json_data["optionIdentifier"]
33 .as_str()
34 .unwrap_or_default()
35 .to_string();
36 let creator = json_data["creatorHandle"]
37 .as_str()
38 .unwrap_or_default()
39 .to_string();
40 let text = json_data["text"].as_str().unwrap_or_default().to_string();
41
42 (
43 poll_id,
44 PollOption {
45 text,
46 creator,
47 votes: Vec::new(),
48 },
49 )
50 }
51}
52
53#[derive(Debug, PartialEq, Eq)]
55pub struct PollVote {
56 pub voter: String,
58 pub option_id: PollOptionID,
60}
61
62impl PollVote {
63 fn from_json(payload: &JsonValue) -> Result<Self, PlistParseError> {
64 let voter = payload["participantHandle"]
65 .as_str()
66 .ok_or(PlistParseError::MissingKey("participantHandle".to_string()))?
67 .to_string();
68
69 let option_id = payload["voteOptionIdentifier"]
70 .as_str()
71 .ok_or(PlistParseError::MissingKey(
72 "voteOptionIdentifier".to_string(),
73 ))?
74 .to_string();
75
76 Ok(PollVote { voter, option_id })
77 }
78}
79
80#[derive(Debug, PartialEq, Eq)]
82pub struct Poll {
83 pub options: HashMap<PollOptionID, PollOption>,
85 pub order: Vec<PollOptionID>,
87}
88
89impl Poll {
90 pub fn from_payload(payload: &Value) -> Result<Self, PlistParseError> {
92 let parsed = parse_ns_keyed_archiver(payload)?;
93
94 let url = get_string_from_nested_dict(&parsed, "URL")
95 .ok_or(PlistParseError::MissingKey("URL".to_string()))?;
96
97 let parsed_json = base64_url_to_json(url)?;
99
100 let mut options = HashMap::new();
101 let mut order = Vec::new();
102 let ordered_options = parsed_json["item"]["orderedPollOptions"].as_array().ok_or(
103 PlistParseError::MissingKey("orderedPollOptions".to_string()),
104 )?;
105
106 for option in ordered_options {
107 let (id, poll_option) = PollOption::from_json(option);
108 order.push(id.clone());
109 options.insert(id, poll_option);
110 }
111
112 Ok(Poll { options, order })
113 }
114
115 pub fn count_votes(&mut self, payload: &Value) -> Result<(), PlistParseError> {
117 let parsed = parse_ns_keyed_archiver(payload)?;
118
119 let url = get_string_from_nested_dict(&parsed, "URL")
120 .ok_or(PlistParseError::MissingKey("URL".to_string()))?;
121
122 let parsed_json = base64_url_to_json(url)?;
124
125 let votes = parsed_json["item"]["votes"]
126 .as_array()
127 .ok_or(PlistParseError::MissingKey("votes".to_string()))?
128 .iter()
129 .map(PollVote::from_json)
130 .collect::<Result<Vec<PollVote>, PlistParseError>>()?;
131
132 self.apply_votes(votes);
133
134 Ok(())
135 }
136
137 pub fn apply_votes(&mut self, votes: Vec<PollVote>) {
139 let voters: HashSet<&str> = votes.iter().map(|vote| vote.voter.as_str()).collect();
140 for option in self.options.values_mut() {
141 option
142 .votes
143 .retain(|vote| !voters.contains(vote.voter.as_str()));
144 }
145
146 let mut distinct: Vec<PollVote> = Vec::with_capacity(votes.len());
148 for vote in votes {
149 if !distinct.contains(&vote) {
150 distinct.push(vote);
151 }
152 }
153
154 for vote in distinct {
155 if let Some(option) = self.options.get_mut(&vote.option_id) {
156 option.votes.push(vote);
157 }
158 }
159 }
160}
161
162fn base64_url_to_json(data: &str) -> Result<JsonValue, PlistParseError> {
163 let after_prefix = data
165 .strip_prefix("data:,")
166 .ok_or(PlistParseError::WrongMessageType)?;
167
168 let base64_part = after_prefix
170 .split_once('?')
171 .map_or(after_prefix, |(before, _)| before);
172
173 let bytes = String::from_utf8(
175 general_purpose::URL_SAFE
176 .decode(base64_part)
177 .map_err(|_| PlistParseError::WrongMessageType)?,
178 )
179 .map_err(|_| PlistParseError::WrongMessageType)?;
180
181 jzon::parse(&bytes).map_err(|_| PlistParseError::WrongMessageType)
183}
184
185#[cfg(test)]
186mod tests {
187 use std::{env::current_dir, fs::File};
188
189 use plist::Value;
190
191 use crate::message_types::polls::{Poll, PollVote};
192
193 const OPTION_1: &str = "630CD0F8-9502-40D4-B2E1-C53E02FD435E";
195 const OPTION_2: &str = "58B59D74-EF73-4BA7-AA8E-5A30FBAF3F44";
196 const OPTION_3: &str = "D493DDC8-6EE2-408F-888C-3986838A1FBB";
197 const OPTION_4: &str = "541DCA6D-686A-4FEB-9092-00BA4A45E2A4";
198
199 fn archive(filename: &str) -> Value {
200 let path = current_dir()
201 .unwrap()
202 .join("test_data/app_message")
203 .join(filename);
204 Value::from_reader(File::open(path).unwrap()).unwrap()
205 }
206
207 fn vote(voter: &str, option_id: &str) -> PollVote {
208 PollVote {
209 voter: voter.to_string(),
210 option_id: option_id.to_string(),
211 }
212 }
213
214 #[test]
215 fn test_parse_poll_creation() {
216 let poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
217
218 assert_eq!(poll.order, vec![OPTION_1, OPTION_2, OPTION_3, OPTION_4]);
219 let options: Vec<(&str, &str)> = poll
220 .order
221 .iter()
222 .map(|id| {
223 (
224 poll.options[id].text.as_str(),
225 poll.options[id].creator.as_str(),
226 )
227 })
228 .collect();
229 assert_eq!(
230 options,
231 vec![
232 ("Option 1", "steven@applenet.com"),
233 ("Option 2", "steven@applenet.com"),
234 ("Option 3", "steven@applenet.com"),
235 ("Option 4", "+12223334444"),
236 ]
237 );
238 assert!(poll.options.values().all(|option| option.votes.is_empty()));
239 }
240
241 #[test]
242 fn test_parse_poll_votes() {
243 let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
244 poll.count_votes(&archive("PollVote.plist")).unwrap();
245 poll.count_votes(&archive("PollVote2.plist")).unwrap();
246
247 assert_eq!(
249 poll.options[OPTION_1].votes,
250 vec![vote("steven@applenet.com", OPTION_1)]
251 );
252 assert_eq!(
253 poll.options[OPTION_2].votes,
254 vec![
255 vote("+12223334444", OPTION_2),
256 vote("steven@applenet.com", OPTION_2),
257 ]
258 );
259 assert!(poll.options[OPTION_3].votes.is_empty());
260 assert!(poll.options[OPTION_4].votes.is_empty());
261 }
262
263 #[test]
264 fn test_parse_poll_vote_unknown_option_dropped() {
265 let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
266 poll.count_votes(&archive("PollRemovedVote.plist")).unwrap();
267
268 assert!(poll.options.values().all(|option| option.votes.is_empty()));
274 }
275
276 #[test]
277 fn test_parse_poll_votes_reapplied_snapshot_does_not_double_count() {
278 let mut once = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
279 once.count_votes(&archive("PollVote2.plist")).unwrap();
280
281 let mut twice = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
282 twice.count_votes(&archive("PollVote2.plist")).unwrap();
283 twice.count_votes(&archive("PollVote2.plist")).unwrap();
284
285 assert_eq!(twice, once);
286 assert_eq!(
287 once.options[OPTION_1].votes,
288 vec![vote("steven@applenet.com", OPTION_1)]
289 );
290 assert_eq!(
291 once.options[OPTION_2].votes,
292 vec![vote("steven@applenet.com", OPTION_2)]
293 );
294 }
295
296 #[test]
297 fn test_apply_votes_snapshot_replaces_across_options() {
298 let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
299 poll.apply_votes(vec![vote("a", OPTION_1)]);
300 poll.apply_votes(vec![vote("a", OPTION_2)]);
301
302 assert!(poll.options[OPTION_1].votes.is_empty());
303 assert_eq!(poll.options[OPTION_2].votes, vec![vote("a", OPTION_2)]);
304 }
305
306 #[test]
307 fn test_apply_votes_same_snapshot_twice_counts_once() {
308 let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
309 poll.apply_votes(vec![vote("a", OPTION_1), vote("a", OPTION_2)]);
310 poll.apply_votes(vec![vote("a", OPTION_1), vote("a", OPTION_2)]);
311
312 assert_eq!(poll.options[OPTION_1].votes, vec![vote("a", OPTION_1)]);
313 assert_eq!(poll.options[OPTION_2].votes, vec![vote("a", OPTION_2)]);
314 }
315
316 #[test]
317 fn test_apply_votes_leaves_other_participants_in_order() {
318 let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
319 poll.apply_votes(vec![vote("a", OPTION_1)]);
320 poll.apply_votes(vec![vote("b", OPTION_1)]);
321 poll.apply_votes(vec![vote("c", OPTION_1)]);
322 poll.apply_votes(vec![vote("b", OPTION_1), vote("b", OPTION_2)]);
324
325 assert_eq!(
326 poll.options[OPTION_1].votes,
327 vec![
328 vote("a", OPTION_1),
329 vote("c", OPTION_1),
330 vote("b", OPTION_1)
331 ]
332 );
333 assert_eq!(poll.options[OPTION_2].votes, vec![vote("b", OPTION_2)]);
334 }
335
336 #[test]
337 fn test_apply_votes_partial_retraction_removes_absent_option() {
338 let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
339 poll.apply_votes(vec![vote("a", OPTION_1), vote("a", OPTION_3)]);
340 poll.apply_votes(vec![vote("a", OPTION_3)]);
341
342 assert!(poll.options[OPTION_1].votes.is_empty());
343 assert!(poll.options[OPTION_2].votes.is_empty());
344 assert_eq!(poll.options[OPTION_3].votes, vec![vote("a", OPTION_3)]);
345 assert!(poll.options[OPTION_4].votes.is_empty());
346 }
347
348 #[test]
349 fn test_apply_votes_duplicate_pair_collapses() {
350 let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
351 poll.apply_votes(vec![
352 vote("a", OPTION_1),
353 vote("a", OPTION_1),
354 vote("a", OPTION_2),
355 ]);
356
357 assert_eq!(poll.options[OPTION_1].votes, vec![vote("a", OPTION_1)]);
358 assert_eq!(poll.options[OPTION_2].votes, vec![vote("a", OPTION_2)]);
359 }
360
361 #[test]
362 fn test_apply_votes_empty_snapshot_is_no_op() {
363 let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
364 poll.apply_votes(vec![vote("a", OPTION_1)]);
365 poll.apply_votes(Vec::new());
366
367 assert_eq!(poll.options[OPTION_1].votes, vec![vote("a", OPTION_1)]);
369 }
370}