ruma_events/poll/
unstable_start.rs

1//! Types for the `org.matrix.msc3381.poll.start` event, the unstable version of `m.poll.start`.
2
3use std::ops::Deref;
4
5use js_int::UInt;
6use ruma_macros::EventContent;
7use serde::{Deserialize, Serialize};
8
9mod content_serde;
10mod unstable_poll_answers_serde;
11mod unstable_poll_kind_serde;
12
13use ruma_common::{MilliSecondsSinceUnixEpoch, OwnedEventId, room_version_rules::RedactionRules};
14
15use self::unstable_poll_answers_serde::UnstablePollAnswersDeHelper;
16use super::{
17    PollResponseData, compile_unstable_poll_results, generate_poll_end_fallback_text,
18    start::{PollAnswers, PollAnswersError, PollContentBlock, PollKind},
19    unstable_end::UnstablePollEndEventContent,
20};
21use crate::{
22    MessageLikeEventContent, MessageLikeEventType, RedactContent, RedactedMessageLikeEventContent,
23    StaticEventContent, relation::Replacement, room::message::RelationWithoutReplacement,
24};
25
26/// The payload for an unstable poll start event.
27///
28/// This is the event content that should be sent for room versions that don't support extensible
29/// events. As of Matrix 1.7, none of the stable room versions (1 through 10) support extensible
30/// events.
31///
32/// To send a poll start event for a room version that supports extensible events, use
33/// [`PollStartEventContent`].
34///
35/// [`PollStartEventContent`]: super::start::PollStartEventContent
36#[derive(Clone, Debug, Serialize, EventContent)]
37#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
38#[ruma_event(type = "org.matrix.msc3381.poll.start", kind = MessageLike, custom_redacted)]
39#[serde(untagged)]
40#[allow(clippy::large_enum_variant)]
41pub enum UnstablePollStartEventContent {
42    /// A new poll start event.
43    New(NewUnstablePollStartEventContent),
44
45    /// A replacement poll start event.
46    Replacement(ReplacementUnstablePollStartEventContent),
47}
48
49impl UnstablePollStartEventContent {
50    /// Get the poll start content of this event content.
51    pub fn poll_start(&self) -> &UnstablePollStartContentBlock {
52        match self {
53            Self::New(c) => &c.poll_start,
54            Self::Replacement(c) => &c.relates_to.new_content.poll_start,
55        }
56    }
57}
58
59impl RedactContent for UnstablePollStartEventContent {
60    type Redacted = RedactedUnstablePollStartEventContent;
61
62    fn redact(self, _rules: &RedactionRules) -> Self::Redacted {
63        RedactedUnstablePollStartEventContent::default()
64    }
65}
66
67impl From<NewUnstablePollStartEventContent> for UnstablePollStartEventContent {
68    fn from(value: NewUnstablePollStartEventContent) -> Self {
69        Self::New(value)
70    }
71}
72
73impl From<ReplacementUnstablePollStartEventContent> for UnstablePollStartEventContent {
74    fn from(value: ReplacementUnstablePollStartEventContent) -> Self {
75        Self::Replacement(value)
76    }
77}
78
79impl OriginalSyncUnstablePollStartEvent {
80    /// Compile the results for this poll with the given response into an
81    /// `UnstablePollEndEventContent`.
82    ///
83    /// It generates a default text representation of the results in English.
84    ///
85    /// This uses [`compile_unstable_poll_results()`] internally.
86    pub fn compile_results<'a>(
87        &'a self,
88        responses: impl IntoIterator<Item = PollResponseData<'a>>,
89    ) -> UnstablePollEndEventContent {
90        let poll_start = self.content.poll_start();
91
92        let full_results = compile_unstable_poll_results(
93            poll_start,
94            responses,
95            Some(MilliSecondsSinceUnixEpoch::now()),
96        );
97        let results =
98            full_results.into_iter().map(|(id, users)| (id, users.len())).collect::<Vec<_>>();
99
100        // Get the text representation of the best answers.
101        let answers =
102            poll_start.answers.iter().map(|a| (a.id.as_str(), a.text.as_str())).collect::<Vec<_>>();
103        let plain_text = generate_poll_end_fallback_text(&answers, results.into_iter());
104
105        UnstablePollEndEventContent::new(plain_text, self.event_id.clone())
106    }
107}
108
109/// A new unstable poll start event.
110#[derive(Clone, Debug, Serialize)]
111#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
112pub struct NewUnstablePollStartEventContent {
113    /// The poll content of the message.
114    #[serde(rename = "org.matrix.msc3381.poll.start")]
115    pub poll_start: UnstablePollStartContentBlock,
116
117    /// Text representation of the message, for clients that don't support polls.
118    #[serde(rename = "org.matrix.msc1767.text")]
119    pub text: Option<String>,
120
121    /// Information about related messages.
122    #[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")]
123    pub relates_to: Option<RelationWithoutReplacement>,
124}
125
126impl NewUnstablePollStartEventContent {
127    /// Creates a `NewUnstablePollStartEventContent` with the given poll content.
128    pub fn new(poll_start: UnstablePollStartContentBlock) -> Self {
129        Self { poll_start, text: None, relates_to: None }
130    }
131
132    /// Creates a `NewUnstablePollStartEventContent` with the given plain text fallback
133    /// representation and poll content.
134    pub fn plain_text(text: impl Into<String>, poll_start: UnstablePollStartContentBlock) -> Self {
135        Self { poll_start, text: Some(text.into()), relates_to: None }
136    }
137}
138
139impl StaticEventContent for NewUnstablePollStartEventContent {
140    const TYPE: &'static str = UnstablePollStartEventContent::TYPE;
141    type IsPrefix = <UnstablePollStartEventContent as StaticEventContent>::IsPrefix;
142}
143
144impl MessageLikeEventContent for NewUnstablePollStartEventContent {
145    fn event_type(&self) -> MessageLikeEventType {
146        MessageLikeEventType::UnstablePollStart
147    }
148}
149
150/// Form of [`NewUnstablePollStartEventContent`] without relation.
151///
152/// To construct this type, construct a [`NewUnstablePollStartEventContent`] and then use one of its
153/// `::from()` / `.into()` methods.
154#[derive(Clone, Debug, Serialize, Deserialize)]
155#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
156pub struct NewUnstablePollStartEventContentWithoutRelation {
157    /// The poll content of the message.
158    #[serde(rename = "org.matrix.msc3381.poll.start")]
159    pub poll_start: UnstablePollStartContentBlock,
160
161    /// Text representation of the message, for clients that don't support polls.
162    #[serde(rename = "org.matrix.msc1767.text")]
163    pub text: Option<String>,
164}
165
166impl From<NewUnstablePollStartEventContent> for NewUnstablePollStartEventContentWithoutRelation {
167    fn from(value: NewUnstablePollStartEventContent) -> Self {
168        let NewUnstablePollStartEventContent { poll_start, text, .. } = value;
169        Self { poll_start, text }
170    }
171}
172
173/// A replacement unstable poll start event.
174#[derive(Clone, Debug)]
175#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
176pub struct ReplacementUnstablePollStartEventContent {
177    /// The poll content of the message.
178    pub poll_start: Option<UnstablePollStartContentBlock>,
179
180    /// Text representation of the message, for clients that don't support polls.
181    pub text: Option<String>,
182
183    /// Information about related messages.
184    pub relates_to: Replacement<NewUnstablePollStartEventContentWithoutRelation>,
185}
186
187impl ReplacementUnstablePollStartEventContent {
188    /// Creates a `ReplacementUnstablePollStartEventContent` with the given poll content that
189    /// replaces the event with the given ID.
190    ///
191    /// The constructed content does not have a fallback by default.
192    pub fn new(poll_start: UnstablePollStartContentBlock, replaces: OwnedEventId) -> Self {
193        Self {
194            poll_start: None,
195            text: None,
196            relates_to: Replacement {
197                event_id: replaces,
198                new_content: NewUnstablePollStartEventContent::new(poll_start).into(),
199            },
200        }
201    }
202
203    /// Creates a `ReplacementUnstablePollStartEventContent` with the given plain text fallback
204    /// representation and poll content that replaces the event with the given ID.
205    ///
206    /// The constructed content does not have a fallback by default.
207    pub fn plain_text(
208        text: impl Into<String>,
209        poll_start: UnstablePollStartContentBlock,
210        replaces: OwnedEventId,
211    ) -> Self {
212        Self {
213            poll_start: None,
214            text: None,
215            relates_to: Replacement {
216                event_id: replaces,
217                new_content: NewUnstablePollStartEventContent::plain_text(text, poll_start).into(),
218            },
219        }
220    }
221}
222
223impl StaticEventContent for ReplacementUnstablePollStartEventContent {
224    const TYPE: &'static str = UnstablePollStartEventContent::TYPE;
225    type IsPrefix = <UnstablePollStartEventContent as StaticEventContent>::IsPrefix;
226}
227
228impl MessageLikeEventContent for ReplacementUnstablePollStartEventContent {
229    fn event_type(&self) -> MessageLikeEventType {
230        MessageLikeEventType::UnstablePollStart
231    }
232}
233
234/// Redacted form of UnstablePollStartEventContent
235#[derive(Clone, Debug, Default, Serialize, Deserialize)]
236#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
237pub struct RedactedUnstablePollStartEventContent {}
238
239impl RedactedUnstablePollStartEventContent {
240    /// Creates an empty RedactedUnstablePollStartEventContent.
241    pub fn new() -> RedactedUnstablePollStartEventContent {
242        Self::default()
243    }
244}
245
246impl StaticEventContent for RedactedUnstablePollStartEventContent {
247    const TYPE: &'static str = UnstablePollStartEventContent::TYPE;
248    type IsPrefix = <UnstablePollStartEventContent as StaticEventContent>::IsPrefix;
249}
250
251impl RedactedMessageLikeEventContent for RedactedUnstablePollStartEventContent {
252    fn event_type(&self) -> MessageLikeEventType {
253        MessageLikeEventType::UnstablePollStart
254    }
255}
256
257/// An unstable block for poll start content.
258#[derive(Debug, Clone, Serialize, Deserialize)]
259#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
260pub struct UnstablePollStartContentBlock {
261    /// The question of the poll.
262    pub question: UnstablePollQuestion,
263
264    /// The kind of the poll.
265    #[serde(default, with = "unstable_poll_kind_serde")]
266    pub kind: PollKind,
267
268    /// The maximum number of responses a user is able to select.
269    ///
270    /// Must be greater or equal to `1`.
271    ///
272    /// Defaults to `1`.
273    #[serde(default = "PollContentBlock::default_max_selections")]
274    pub max_selections: UInt,
275
276    /// The possible answers to the poll.
277    pub answers: UnstablePollAnswers,
278}
279
280impl UnstablePollStartContentBlock {
281    /// Creates a new `PollStartContent` with the given question and answers.
282    pub fn new(question: impl Into<String>, answers: UnstablePollAnswers) -> Self {
283        Self {
284            question: UnstablePollQuestion::new(question),
285            kind: Default::default(),
286            max_selections: PollContentBlock::default_max_selections(),
287            answers,
288        }
289    }
290}
291
292/// An unstable poll question.
293#[derive(Debug, Clone, Serialize, Deserialize)]
294#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
295pub struct UnstablePollQuestion {
296    /// The text representation of the question.
297    #[serde(rename = "org.matrix.msc1767.text")]
298    pub text: String,
299}
300
301impl UnstablePollQuestion {
302    /// Creates a new `UnstablePollQuestion` with the given plain text.
303    pub fn new(text: impl Into<String>) -> Self {
304        Self { text: text.into() }
305    }
306}
307
308/// The unstable answers to a poll.
309///
310/// Must include between 1 and 20 `UnstablePollAnswer`s.
311///
312/// To build this, use one of the `TryFrom` implementations.
313#[derive(Clone, Debug, Deserialize, Serialize)]
314#[serde(try_from = "UnstablePollAnswersDeHelper")]
315pub struct UnstablePollAnswers(Vec<UnstablePollAnswer>);
316
317impl TryFrom<Vec<UnstablePollAnswer>> for UnstablePollAnswers {
318    type Error = PollAnswersError;
319
320    fn try_from(value: Vec<UnstablePollAnswer>) -> Result<Self, Self::Error> {
321        if value.len() < PollAnswers::MIN_LENGTH {
322            Err(PollAnswersError::NotEnoughValues)
323        } else if value.len() > PollAnswers::MAX_LENGTH {
324            Err(PollAnswersError::TooManyValues)
325        } else {
326            Ok(Self(value))
327        }
328    }
329}
330
331impl TryFrom<&[UnstablePollAnswer]> for UnstablePollAnswers {
332    type Error = PollAnswersError;
333
334    fn try_from(value: &[UnstablePollAnswer]) -> Result<Self, Self::Error> {
335        Self::try_from(value.to_owned())
336    }
337}
338
339impl Deref for UnstablePollAnswers {
340    type Target = [UnstablePollAnswer];
341
342    fn deref(&self) -> &Self::Target {
343        &self.0
344    }
345}
346
347/// Unstable poll answer.
348#[derive(Clone, Debug, Serialize, Deserialize)]
349#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
350pub struct UnstablePollAnswer {
351    /// The ID of the answer.
352    ///
353    /// This must be unique among the answers of a poll.
354    pub id: String,
355
356    /// The text representation of the answer.
357    #[serde(rename = "org.matrix.msc1767.text")]
358    pub text: String,
359}
360
361impl UnstablePollAnswer {
362    /// Creates a new `PollAnswer` with the given id and text representation.
363    pub fn new(id: impl Into<String>, text: impl Into<String>) -> Self {
364        Self { id: id.into(), text: text.into() }
365    }
366}