use std::collections::{HashMap, HashSet};
use base64::{Engine as _, engine::general_purpose};
use jzon::{self, JsonValue};
use plist::Value;
use crate::{
error::plist::PlistParseError,
util::plist::{get_string_from_nested_dict, parse_ns_keyed_archiver},
};
pub type PollOptionID = String;
#[derive(Debug, PartialEq, Eq)]
pub struct PollOption {
pub text: String,
pub creator: String,
pub votes: Vec<PollVote>,
}
impl PollOption {
fn from_json(json_data: &JsonValue) -> (PollOptionID, Self) {
let poll_id = json_data["optionIdentifier"]
.as_str()
.unwrap_or_default()
.to_string();
let creator = json_data["creatorHandle"]
.as_str()
.unwrap_or_default()
.to_string();
let text = json_data["text"].as_str().unwrap_or_default().to_string();
(
poll_id,
PollOption {
text,
creator,
votes: Vec::new(),
},
)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct PollVote {
pub voter: String,
pub option_id: PollOptionID,
}
impl PollVote {
fn from_json(payload: &JsonValue) -> Result<Self, PlistParseError> {
let voter = payload["participantHandle"]
.as_str()
.ok_or(PlistParseError::MissingKey("participantHandle".to_string()))?
.to_string();
let option_id = payload["voteOptionIdentifier"]
.as_str()
.ok_or(PlistParseError::MissingKey(
"voteOptionIdentifier".to_string(),
))?
.to_string();
Ok(PollVote { voter, option_id })
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Poll {
pub options: HashMap<PollOptionID, PollOption>,
pub order: Vec<PollOptionID>,
}
impl Poll {
pub fn from_payload(payload: &Value) -> Result<Self, PlistParseError> {
let parsed = parse_ns_keyed_archiver(payload)?;
let url = get_string_from_nested_dict(&parsed, "URL")
.ok_or(PlistParseError::MissingKey("URL".to_string()))?;
let parsed_json = base64_url_to_json(url)?;
let mut options = HashMap::new();
let mut order = Vec::new();
let ordered_options = parsed_json["item"]["orderedPollOptions"].as_array().ok_or(
PlistParseError::MissingKey("orderedPollOptions".to_string()),
)?;
for option in ordered_options {
let (id, poll_option) = PollOption::from_json(option);
order.push(id.clone());
options.insert(id, poll_option);
}
Ok(Poll { options, order })
}
pub fn count_votes(&mut self, payload: &Value) -> Result<(), PlistParseError> {
let parsed = parse_ns_keyed_archiver(payload)?;
let url = get_string_from_nested_dict(&parsed, "URL")
.ok_or(PlistParseError::MissingKey("URL".to_string()))?;
let parsed_json = base64_url_to_json(url)?;
let votes = parsed_json["item"]["votes"]
.as_array()
.ok_or(PlistParseError::MissingKey("votes".to_string()))?
.iter()
.map(PollVote::from_json)
.collect::<Result<Vec<PollVote>, PlistParseError>>()?;
self.apply_votes(votes);
Ok(())
}
pub fn apply_votes(&mut self, votes: Vec<PollVote>) {
let voters: HashSet<&str> = votes.iter().map(|vote| vote.voter.as_str()).collect();
for option in self.options.values_mut() {
option
.votes
.retain(|vote| !voters.contains(vote.voter.as_str()));
}
let mut distinct: Vec<PollVote> = Vec::with_capacity(votes.len());
for vote in votes {
if !distinct.contains(&vote) {
distinct.push(vote);
}
}
for vote in distinct {
if let Some(option) = self.options.get_mut(&vote.option_id) {
option.votes.push(vote);
}
}
}
}
fn base64_url_to_json(data: &str) -> Result<JsonValue, PlistParseError> {
let after_prefix = data
.strip_prefix("data:,")
.ok_or(PlistParseError::WrongMessageType)?;
let base64_part = after_prefix
.split_once('?')
.map_or(after_prefix, |(before, _)| before);
let bytes = String::from_utf8(
general_purpose::URL_SAFE
.decode(base64_part)
.map_err(|_| PlistParseError::WrongMessageType)?,
)
.map_err(|_| PlistParseError::WrongMessageType)?;
jzon::parse(&bytes).map_err(|_| PlistParseError::WrongMessageType)
}
#[cfg(test)]
mod tests {
use std::{env::current_dir, fs::File};
use plist::Value;
use crate::message_types::polls::{Poll, PollVote};
const OPTION_1: &str = "630CD0F8-9502-40D4-B2E1-C53E02FD435E";
const OPTION_2: &str = "58B59D74-EF73-4BA7-AA8E-5A30FBAF3F44";
const OPTION_3: &str = "D493DDC8-6EE2-408F-888C-3986838A1FBB";
const OPTION_4: &str = "541DCA6D-686A-4FEB-9092-00BA4A45E2A4";
fn archive(filename: &str) -> Value {
let path = current_dir()
.unwrap()
.join("test_data/app_message")
.join(filename);
Value::from_reader(File::open(path).unwrap()).unwrap()
}
fn vote(voter: &str, option_id: &str) -> PollVote {
PollVote {
voter: voter.to_string(),
option_id: option_id.to_string(),
}
}
#[test]
fn test_parse_poll_creation() {
let poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
assert_eq!(poll.order, vec![OPTION_1, OPTION_2, OPTION_3, OPTION_4]);
let options: Vec<(&str, &str)> = poll
.order
.iter()
.map(|id| {
(
poll.options[id].text.as_str(),
poll.options[id].creator.as_str(),
)
})
.collect();
assert_eq!(
options,
vec![
("Option 1", "steven@applenet.com"),
("Option 2", "steven@applenet.com"),
("Option 3", "steven@applenet.com"),
("Option 4", "+12223334444"),
]
);
assert!(poll.options.values().all(|option| option.votes.is_empty()));
}
#[test]
fn test_parse_poll_votes() {
let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
poll.count_votes(&archive("PollVote.plist")).unwrap();
poll.count_votes(&archive("PollVote2.plist")).unwrap();
assert_eq!(
poll.options[OPTION_1].votes,
vec![vote("steven@applenet.com", OPTION_1)]
);
assert_eq!(
poll.options[OPTION_2].votes,
vec![
vote("+12223334444", OPTION_2),
vote("steven@applenet.com", OPTION_2),
]
);
assert!(poll.options[OPTION_3].votes.is_empty());
assert!(poll.options[OPTION_4].votes.is_empty());
}
#[test]
fn test_parse_poll_vote_unknown_option_dropped() {
let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
poll.count_votes(&archive("PollRemovedVote.plist")).unwrap();
assert!(poll.options.values().all(|option| option.votes.is_empty()));
}
#[test]
fn test_parse_poll_votes_reapplied_snapshot_does_not_double_count() {
let mut once = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
once.count_votes(&archive("PollVote2.plist")).unwrap();
let mut twice = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
twice.count_votes(&archive("PollVote2.plist")).unwrap();
twice.count_votes(&archive("PollVote2.plist")).unwrap();
assert_eq!(twice, once);
assert_eq!(
once.options[OPTION_1].votes,
vec![vote("steven@applenet.com", OPTION_1)]
);
assert_eq!(
once.options[OPTION_2].votes,
vec![vote("steven@applenet.com", OPTION_2)]
);
}
#[test]
fn test_apply_votes_snapshot_replaces_across_options() {
let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
poll.apply_votes(vec![vote("a", OPTION_1)]);
poll.apply_votes(vec![vote("a", OPTION_2)]);
assert!(poll.options[OPTION_1].votes.is_empty());
assert_eq!(poll.options[OPTION_2].votes, vec![vote("a", OPTION_2)]);
}
#[test]
fn test_apply_votes_same_snapshot_twice_counts_once() {
let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
poll.apply_votes(vec![vote("a", OPTION_1), vote("a", OPTION_2)]);
poll.apply_votes(vec![vote("a", OPTION_1), vote("a", OPTION_2)]);
assert_eq!(poll.options[OPTION_1].votes, vec![vote("a", OPTION_1)]);
assert_eq!(poll.options[OPTION_2].votes, vec![vote("a", OPTION_2)]);
}
#[test]
fn test_apply_votes_leaves_other_participants_in_order() {
let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
poll.apply_votes(vec![vote("a", OPTION_1)]);
poll.apply_votes(vec![vote("b", OPTION_1)]);
poll.apply_votes(vec![vote("c", OPTION_1)]);
poll.apply_votes(vec![vote("b", OPTION_1), vote("b", OPTION_2)]);
assert_eq!(
poll.options[OPTION_1].votes,
vec![
vote("a", OPTION_1),
vote("c", OPTION_1),
vote("b", OPTION_1)
]
);
assert_eq!(poll.options[OPTION_2].votes, vec![vote("b", OPTION_2)]);
}
#[test]
fn test_apply_votes_partial_retraction_removes_absent_option() {
let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
poll.apply_votes(vec![vote("a", OPTION_1), vote("a", OPTION_3)]);
poll.apply_votes(vec![vote("a", OPTION_3)]);
assert!(poll.options[OPTION_1].votes.is_empty());
assert!(poll.options[OPTION_2].votes.is_empty());
assert_eq!(poll.options[OPTION_3].votes, vec![vote("a", OPTION_3)]);
assert!(poll.options[OPTION_4].votes.is_empty());
}
#[test]
fn test_apply_votes_duplicate_pair_collapses() {
let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
poll.apply_votes(vec![
vote("a", OPTION_1),
vote("a", OPTION_1),
vote("a", OPTION_2),
]);
assert_eq!(poll.options[OPTION_1].votes, vec![vote("a", OPTION_1)]);
assert_eq!(poll.options[OPTION_2].votes, vec![vote("a", OPTION_2)]);
}
#[test]
fn test_apply_votes_empty_snapshot_is_no_op() {
let mut poll = Poll::from_payload(&archive("PollCreate.plist")).unwrap();
poll.apply_votes(vec![vote("a", OPTION_1)]);
poll.apply_votes(Vec::new());
assert_eq!(poll.options[OPTION_1].votes, vec![vote("a", OPTION_1)]);
}
}