use alloc::borrow::Cow;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use core::str::FromStr;
use crate::{Event, EventBuilder, EventId, Kind, RelayUrl, Tag, TagKind, TagStandard, Timestamp};
pub(crate) const ENDS_AT_TAG_KIND_STR: &str = "endsAt";
pub(crate) const ENDS_AT_TAG_KIND: TagKind = TagKind::Custom(Cow::Borrowed(ENDS_AT_TAG_KIND_STR));
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
UnknownPollType,
UnexpectedTag,
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownPollType => f.write_str("unknown poll type"),
Self::UnexpectedTag => f.write_str("unexpected tag"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PollType {
SingleChoice,
MultipleChoice,
}
impl fmt::Display for PollType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl PollType {
pub fn as_str(&self) -> &str {
match self {
Self::SingleChoice => "singlechoice",
Self::MultipleChoice => "multiplechoice",
}
}
}
impl FromStr for PollType {
type Err = Error;
fn from_str(poll_type: &str) -> Result<Self, Self::Err> {
match poll_type {
"singlechoice" => Ok(Self::SingleChoice),
"multiplechoice" => Ok(Self::MultipleChoice),
_ => Err(Error::UnknownPollType),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PollOption {
pub id: String,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Poll {
pub title: String,
pub r#type: PollType,
pub options: Vec<PollOption>,
pub relays: Vec<RelayUrl>,
pub ends_at: Option<Timestamp>,
}
impl Poll {
pub fn from_event(event: &Event) -> Result<Self, Error> {
let poll_type: PollType = match event.tags.find_standardized(TagKind::PollType) {
Some(TagStandard::PollType(poll_type)) => *poll_type,
Some(..) => return Err(Error::UnexpectedTag),
None => PollType::SingleChoice,
};
let options: Vec<PollOption> = event
.tags
.filter_standardized(TagKind::Option)
.filter_map(|tag| match tag {
TagStandard::PollOption(option) => Some(option.clone()),
_ => None,
})
.collect();
let relays: Vec<RelayUrl> = event
.tags
.filter_standardized(TagKind::Relay)
.filter_map(|tag| match tag {
TagStandard::Relay(url) => Some(url.clone()),
_ => None,
})
.collect();
let ends_at: Option<Timestamp> = match event.tags.find_standardized(ENDS_AT_TAG_KIND) {
Some(TagStandard::PollEndsAt(timestamp)) => Some(*timestamp),
Some(..) => return Err(Error::UnexpectedTag),
None => None,
};
Ok(Self {
title: event.content.clone(),
r#type: poll_type,
options,
relays,
ends_at,
})
}
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_event_builder(self) -> EventBuilder {
let mut tags: Vec<Tag> = Vec::with_capacity(1 + self.options.len() + self.relays.len());
tags.push(Tag::from_standardized_without_cell(TagStandard::PollType(
self.r#type,
)));
for option in self.options.into_iter() {
tags.push(Tag::from_standardized_without_cell(
TagStandard::PollOption(option),
));
}
for url in self.relays.into_iter() {
tags.push(Tag::relay(url));
}
if let Some(timestamp) = self.ends_at {
tags.push(Tag::custom(ENDS_AT_TAG_KIND, [timestamp.to_string()]));
}
EventBuilder::new(Kind::Poll, self.title).tags(tags)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PollResponse {
SingleChoice {
poll_id: EventId,
response: String,
},
MultipleChoice {
poll_id: EventId,
responses: Vec<String>,
},
}
impl PollResponse {
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_event_builder(self) -> EventBuilder {
let tags: Vec<Tag> = match self {
Self::SingleChoice { poll_id, response } => {
vec![
Tag::event(poll_id),
Tag::from_standardized_without_cell(TagStandard::PollResponse(response)),
]
}
Self::MultipleChoice { poll_id, responses } => {
let mut tags: Vec<Tag> = Vec::with_capacity(1 + responses.len());
tags.push(Tag::event(poll_id));
for response in responses.into_iter() {
tags.push(Tag::from_standardized_without_cell(
TagStandard::PollResponse(response),
));
}
tags
}
};
EventBuilder::new(Kind::PollResponse, "").tags(tags)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::JsonUtil;
#[test]
fn test_poll_type() {
assert_eq!(PollType::SingleChoice.as_str(), "singlechoice");
assert_eq!(PollType::MultipleChoice.as_str(), "multiplechoice");
assert_eq!(
PollType::from_str("singlechoice").unwrap(),
PollType::SingleChoice
);
assert_eq!(
PollType::from_str("multiplechoice").unwrap(),
PollType::MultipleChoice
);
assert!(PollType::from_str("unknown").is_err());
}
#[test]
fn test_poll_from_event() {
let event = Event::from_json(r#"{
"content": "Pineapple on pizza",
"created_at": 1719888496,
"id": "9d1b6b9562e66f2ecf35eb0a3c2decc736c47fddb13d6fb8f87185a153ea3634",
"kind": 1068,
"pubkey": "dee45a23c4f1d93f3a2043650c5081e4ac14a778e0acbef03de3768e4f81ac7b",
"sig": "7fa93bf3c430eaef784b0dacc217d3cd5eff1c520e7ef5d961381bc0f014dde6286618048d924808e54d1be03f2f2c2f0f8b5c9c2082a4480caf45a565ca9797",
"tags": [
["option", "qj518h583", "Yay"],
["option", "gga6cdnqj", "Nay"],
["relay", "wss://relay.damus.io"],
["relay", "wss://relay.example.com"],
["polltype", "multiplechoice"],
["endsAt", "1788888888"]
]
}"#).unwrap();
let poll = Poll::from_event(&event).unwrap();
assert_eq!(poll.title, "Pineapple on pizza");
assert_eq!(poll.r#type, PollType::MultipleChoice);
assert_eq!(
poll.options,
vec![
PollOption {
id: "qj518h583".to_string(),
text: "Yay".to_string(),
},
PollOption {
id: "gga6cdnqj".to_string(),
text: "Nay".to_string(),
}
]
);
assert_eq!(
poll.relays,
vec![
RelayUrl::from_str("wss://relay.damus.io").unwrap(),
RelayUrl::from_str("wss://relay.example.com").unwrap(),
]
);
assert_eq!(poll.ends_at, Some(Timestamp::from_secs(1788888888)));
}
#[test]
fn test_poll_from_event_without_poll_type() {
let event = Event::from_json(r#"{
"content": "Pineapple on pizza",
"created_at": 1719888496,
"id": "9d1b6b9562e66f2ecf35eb0a3c2decc736c47fddb13d6fb8f87185a153ea3634",
"kind": 1068,
"pubkey": "dee45a23c4f1d93f3a2043650c5081e4ac14a778e0acbef03de3768e4f81ac7b",
"sig": "7fa93bf3c430eaef784b0dacc217d3cd5eff1c520e7ef5d961381bc0f014dde6286618048d924808e54d1be03f2f2c2f0f8b5c9c2082a4480caf45a565ca9797",
"tags": [
["option", "qj518h583", "Yay"]
]
}"#).unwrap();
let poll = Poll::from_event(&event).unwrap();
assert_eq!(poll.title, "Pineapple on pizza");
assert_eq!(poll.r#type, PollType::SingleChoice);
assert_eq!(
poll.options,
vec![PollOption {
id: "qj518h583".to_string(),
text: "Yay".to_string(),
},]
);
assert!(poll.relays.is_empty());
assert!(poll.ends_at.is_none());
}
#[test]
fn test_poll_from_event_with_empty_poll_type() {
let event = Event::from_json(r#"{
"content": "Pineapple on pizza",
"created_at": 1719888496,
"id": "9d1b6b9562e66f2ecf35eb0a3c2decc736c47fddb13d6fb8f87185a153ea3634",
"kind": 1068,
"pubkey": "dee45a23c4f1d93f3a2043650c5081e4ac14a778e0acbef03de3768e4f81ac7b",
"sig": "7fa93bf3c430eaef784b0dacc217d3cd5eff1c520e7ef5d961381bc0f014dde6286618048d924808e54d1be03f2f2c2f0f8b5c9c2082a4480caf45a565ca9797",
"tags": [
["option", "qj518h583", "Yay"],
["polltype", ""]
]
}"#).unwrap();
let poll = Poll::from_event(&event).unwrap();
assert_eq!(poll.title, "Pineapple on pizza");
assert_eq!(poll.r#type, PollType::SingleChoice);
assert_eq!(
poll.options,
vec![PollOption {
id: "qj518h583".to_string(),
text: "Yay".to_string(),
},]
);
assert!(poll.relays.is_empty());
assert!(poll.ends_at.is_none());
}
#[test]
fn test_poll_from_event_with_malformed_polltype_tag() {
let event = Event::from_json(r#"{
"content": "Pineapple on pizza",
"created_at": 1719888496,
"id": "9d1b6b9562e66f2ecf35eb0a3c2decc736c47fddb13d6fb8f87185a153ea3634",
"kind": 1068,
"pubkey": "dee45a23c4f1d93f3a2043650c5081e4ac14a778e0acbef03de3768e4f81ac7b",
"sig": "7fa93bf3c430eaef784b0dacc217d3cd5eff1c520e7ef5d961381bc0f014dde6286618048d924808e54d1be03f2f2c2f0f8b5c9c2082a4480caf45a565ca9797",
"tags": [
["option", "qj518h583", "Yay"],
["polltype"]
]
}"#).unwrap();
let poll = Poll::from_event(&event).unwrap();
assert_eq!(poll.title, "Pineapple on pizza");
assert_eq!(poll.r#type, PollType::SingleChoice);
assert_eq!(
poll.options,
vec![PollOption {
id: "qj518h583".to_string(),
text: "Yay".to_string(),
},]
);
assert!(poll.relays.is_empty());
assert!(poll.ends_at.is_none());
}
}