//! Types for the `m.poll.start` event. use std::ops::Deref; use js_int::{uint, UInt}; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; mod poll_answers_serde; use poll_answers_serde::PollAnswersDeHelper; use super::{ compile_poll_results, end::{PollEndEventContent, PollResultsContentBlock}, generate_poll_end_fallback_text, PollResponseData, }; use crate::{ events::{message::TextContentBlock, room::message::Relation}, serde::StringEnum, PrivOwnedStr, }; /// The payload for a poll start event. /// /// This is the event content that should be sent for room versions that support extensible events. /// As of Matrix 1.7, none of the stable room versions (1 through 10) support extensible events. /// /// To send a poll start event for a room version that does not support extensible events, use /// [`UnstablePollStartEventContent`]. /// /// [`UnstablePollStartEventContent`]: super::unstable_start::UnstablePollStartEventContent #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.poll.start", kind = MessageLike, without_relation)] pub struct PollStartEventContent { /// The poll content of the message. #[serde(rename = "m.poll")] pub poll: PollContentBlock, /// Text representation of the message, for clients that don't support polls. #[serde(rename = "m.text")] pub text: TextContentBlock, /// Information about related messages. #[serde( flatten, skip_serializing_if = "Option::is_none", deserialize_with = "crate::events::room::message::relation_serde::deserialize_relation" )] pub relates_to: Option>, /// Whether this message is automated. #[cfg(feature = "unstable-msc3955")] #[serde( default, skip_serializing_if = "crate::serde::is_default", rename = "org.matrix.msc1767.automated" )] pub automated: bool, } impl PollStartEventContent { /// Creates a new `PollStartEventContent` with the given fallback representation and poll /// content. pub fn new(text: TextContentBlock, poll: PollContentBlock) -> Self { Self { poll, text, relates_to: None, #[cfg(feature = "unstable-msc3955")] automated: false, } } /// Creates a new `PollStartEventContent` with the given plain text fallback /// representation and poll content. pub fn with_plain_text(plain_text: impl Into, poll: PollContentBlock) -> Self { Self::new(TextContentBlock::plain(plain_text), poll) } } impl OriginalSyncPollStartEvent { /// Compile the results for this poll with the given response into a `PollEndEventContent`. /// /// It generates a default text representation of the results in English. /// /// This uses [`compile_poll_results()`] internally. pub fn compile_results<'a>( &'a self, responses: impl IntoIterator>, ) -> PollEndEventContent { let full_results = compile_poll_results(&self.content.poll, responses, None); let results = full_results.into_iter().map(|(id, users)| (id, users.len())).collect::>(); // Construct the results and get the top answer(s). let poll_results = PollResultsContentBlock::from_iter( results .iter() .map(|(id, count)| ((*id).to_owned(), (*count).try_into().unwrap_or(UInt::MAX))), ); // Get the text representation of the best answers. let answers = self .content .poll .answers .iter() .map(|a| { let text = a.text.find_plain().unwrap_or(&a.id); (a.id.as_str(), text) }) .collect::>(); let plain_text = generate_poll_end_fallback_text(&answers, results.into_iter()); let mut end = PollEndEventContent::with_plain_text(plain_text, self.event_id.clone()); end.poll_results = Some(poll_results); end } } /// A block for poll content. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PollContentBlock { /// The question of the poll. pub question: PollQuestion, /// The kind of the poll. #[serde(default, skip_serializing_if = "crate::serde::is_default")] pub kind: PollKind, /// The maximum number of responses a user is able to select. /// /// Must be greater or equal to `1`. /// /// Defaults to `1`. #[serde( default = "PollContentBlock::default_max_selections", skip_serializing_if = "PollContentBlock::max_selections_is_default" )] pub max_selections: UInt, /// The possible answers to the poll. pub answers: PollAnswers, } impl PollContentBlock { /// Creates a new `PollStartContent` with the given question and answers. pub fn new(question: TextContentBlock, answers: PollAnswers) -> Self { Self { question: question.into(), kind: Default::default(), max_selections: Self::default_max_selections(), answers, } } pub(super) fn default_max_selections() -> UInt { uint!(1) } fn max_selections_is_default(max_selections: &UInt) -> bool { max_selections == &Self::default_max_selections() } } /// The question of a poll. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PollQuestion { /// The text representation of the question. #[serde(rename = "m.text")] pub text: TextContentBlock, } impl From for PollQuestion { fn from(text: TextContentBlock) -> Self { Self { text } } } /// The kind of poll. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Default, PartialEq, Eq, StringEnum)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum PollKind { /// The results are revealed once the poll is closed. #[default] #[ruma_enum(rename = "m.undisclosed")] Undisclosed, /// The votes are visible up until and including when the poll is closed. #[ruma_enum(rename = "m.disclosed")] Disclosed, #[doc(hidden)] _Custom(PrivOwnedStr), } /// The answers to a poll. /// /// Must include between 1 and 20 `PollAnswer`s. /// /// To build this, use the `TryFrom` implementations. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(try_from = "PollAnswersDeHelper")] pub struct PollAnswers(Vec); impl PollAnswers { /// The smallest number of values contained in a `PollAnswers`. pub const MIN_LENGTH: usize = 1; /// The largest number of values contained in a `PollAnswers`. pub const MAX_LENGTH: usize = 20; } /// An error encountered when trying to convert to a `PollAnswers`. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)] #[non_exhaustive] pub enum PollAnswersError { /// There are more than [`PollAnswers::MAX_LENGTH`] values. #[error("too many values")] TooManyValues, /// There are less that [`PollAnswers::MIN_LENGTH`] values. #[error("not enough values")] NotEnoughValues, } impl TryFrom> for PollAnswers { type Error = PollAnswersError; fn try_from(value: Vec) -> Result { if value.len() < Self::MIN_LENGTH { Err(PollAnswersError::NotEnoughValues) } else if value.len() > Self::MAX_LENGTH { Err(PollAnswersError::TooManyValues) } else { Ok(Self(value)) } } } impl TryFrom<&[PollAnswer]> for PollAnswers { type Error = PollAnswersError; fn try_from(value: &[PollAnswer]) -> Result { Self::try_from(value.to_owned()) } } impl Deref for PollAnswers { type Target = [PollAnswer]; fn deref(&self) -> &Self::Target { &self.0 } } /// Poll answer. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct PollAnswer { /// The ID of the answer. /// /// This must be unique among the answers of a poll. #[serde(rename = "m.id")] pub id: String, /// The text representation of the answer. #[serde(rename = "m.text")] pub text: TextContentBlock, } impl PollAnswer { /// Creates a new `PollAnswer` with the given id and text representation. pub fn new(id: String, text: TextContentBlock) -> Self { Self { id, text } } }