events: Add unstable poll events
Using the unstable format defined in MSC3381
This commit is contained in:
parent
e780633cce
commit
d53362c689
@ -70,9 +70,18 @@ event_enum! {
|
|||||||
#[cfg(feature = "unstable-msc3381")]
|
#[cfg(feature = "unstable-msc3381")]
|
||||||
"m.poll.start" => super::poll::start,
|
"m.poll.start" => super::poll::start,
|
||||||
#[cfg(feature = "unstable-msc3381")]
|
#[cfg(feature = "unstable-msc3381")]
|
||||||
|
#[ruma_enum(ident = UnstablePollStart)]
|
||||||
|
"org.matrix.msc3381.poll.start" => super::poll::unstable_start,
|
||||||
|
#[cfg(feature = "unstable-msc3381")]
|
||||||
"m.poll.response" => super::poll::response,
|
"m.poll.response" => super::poll::response,
|
||||||
#[cfg(feature = "unstable-msc3381")]
|
#[cfg(feature = "unstable-msc3381")]
|
||||||
|
#[ruma_enum(ident = UnstablePollResponse)]
|
||||||
|
"org.matrix.msc3381.poll.response" => super::poll::unstable_response,
|
||||||
|
#[cfg(feature = "unstable-msc3381")]
|
||||||
"m.poll.end" => super::poll::end,
|
"m.poll.end" => super::poll::end,
|
||||||
|
#[cfg(feature = "unstable-msc3381")]
|
||||||
|
#[ruma_enum(ident = UnstablePollEnd)]
|
||||||
|
"org.matrix.msc3381.poll.end" => super::poll::unstable_end,
|
||||||
"m.reaction" => super::reaction,
|
"m.reaction" => super::reaction,
|
||||||
"m.room.encrypted" => super::room::encrypted,
|
"m.room.encrypted" => super::room::encrypted,
|
||||||
"m.room.message" => super::room::message,
|
"m.room.message" => super::room::message,
|
||||||
@ -293,7 +302,11 @@ impl AnyMessageLikeEventContent {
|
|||||||
start::KeyVerificationStartEventContent,
|
start::KeyVerificationStartEventContent,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "unstable-msc3381")]
|
#[cfg(feature = "unstable-msc3381")]
|
||||||
use super::poll::{end::PollEndEventContent, response::PollResponseEventContent};
|
use super::poll::{
|
||||||
|
end::PollEndEventContent, response::PollResponseEventContent,
|
||||||
|
unstable_end::UnstablePollEndEventContent,
|
||||||
|
unstable_response::UnstablePollResponseEventContent,
|
||||||
|
};
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
@ -329,11 +342,13 @@ impl AnyMessageLikeEventContent {
|
|||||||
Self::Video(ev) => ev.relates_to.clone().map(Into::into),
|
Self::Video(ev) => ev.relates_to.clone().map(Into::into),
|
||||||
#[cfg(feature = "unstable-msc3381")]
|
#[cfg(feature = "unstable-msc3381")]
|
||||||
Self::PollResponse(PollResponseEventContent { relates_to, .. })
|
Self::PollResponse(PollResponseEventContent { relates_to, .. })
|
||||||
| Self::PollEnd(PollEndEventContent { relates_to, .. }) => {
|
| Self::UnstablePollResponse(UnstablePollResponseEventContent { relates_to, .. })
|
||||||
|
| Self::PollEnd(PollEndEventContent { relates_to, .. })
|
||||||
|
| Self::UnstablePollEnd(UnstablePollEndEventContent { relates_to, .. }) => {
|
||||||
Some(encrypted::Relation::Reference(relates_to.clone()))
|
Some(encrypted::Relation::Reference(relates_to.clone()))
|
||||||
}
|
}
|
||||||
#[cfg(feature = "unstable-msc3381")]
|
#[cfg(feature = "unstable-msc3381")]
|
||||||
Self::PollStart(_) => None,
|
Self::PollStart(_) | Self::UnstablePollStart(_) => None,
|
||||||
Self::CallNegotiate(_)
|
Self::CallNegotiate(_)
|
||||||
| Self::CallReject(_)
|
| Self::CallReject(_)
|
||||||
| Self::CallSelectAnswer(_)
|
| Self::CallSelectAnswer(_)
|
||||||
|
@ -11,11 +11,18 @@ use js_int::uint;
|
|||||||
|
|
||||||
use crate::{MilliSecondsSinceUnixEpoch, UserId};
|
use crate::{MilliSecondsSinceUnixEpoch, UserId};
|
||||||
|
|
||||||
use self::{response::OriginalSyncPollResponseEvent, start::PollContentBlock};
|
use self::{
|
||||||
|
response::OriginalSyncPollResponseEvent, start::PollContentBlock,
|
||||||
|
unstable_response::OriginalSyncUnstablePollResponseEvent,
|
||||||
|
unstable_start::UnstablePollStartContentBlock,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod end;
|
pub mod end;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
pub mod start;
|
pub mod start;
|
||||||
|
pub mod unstable_end;
|
||||||
|
pub mod unstable_response;
|
||||||
|
pub mod unstable_start;
|
||||||
|
|
||||||
/// Generate the current results with the given poll and responses.
|
/// Generate the current results with the given poll and responses.
|
||||||
///
|
///
|
||||||
@ -53,9 +60,57 @@ pub fn compile_poll_results<'a>(
|
|||||||
acc
|
acc
|
||||||
});
|
});
|
||||||
|
|
||||||
// Aggregate the selections by answer.
|
aggregate_results(poll.answers.iter().map(|a| a.id.as_str()), users_selections)
|
||||||
let mut results =
|
}
|
||||||
IndexMap::from_iter(poll.answers.iter().map(|a| (a.id.as_str(), BTreeSet::new())));
|
|
||||||
|
/// Generate the current results with the given unstable poll and responses.
|
||||||
|
///
|
||||||
|
/// If the `end_timestamp` is provided, any response with an `origin_server_ts` after that timestamp
|
||||||
|
/// is ignored. If it is not provided, `MilliSecondsSinceUnixEpoch::now()` will be used instead.
|
||||||
|
///
|
||||||
|
/// This method will handle invalid responses, or several response from the same user so all
|
||||||
|
/// responses to the poll should be provided.
|
||||||
|
///
|
||||||
|
/// Returns a map of answer ID to a set of user IDs that voted for them. When using `.iter()` or
|
||||||
|
/// `.into_iter()` on the map, the results are sorted from the highest number of votes to the
|
||||||
|
/// lowest.
|
||||||
|
pub fn compile_unstable_poll_results<'a>(
|
||||||
|
poll: &'a UnstablePollStartContentBlock,
|
||||||
|
responses: impl IntoIterator<Item = &'a OriginalSyncUnstablePollResponseEvent>,
|
||||||
|
end_timestamp: Option<MilliSecondsSinceUnixEpoch>,
|
||||||
|
) -> IndexMap<&'a str, BTreeSet<&'a UserId>> {
|
||||||
|
let end_ts = end_timestamp.unwrap_or_else(MilliSecondsSinceUnixEpoch::now);
|
||||||
|
|
||||||
|
let users_selections = responses
|
||||||
|
.into_iter()
|
||||||
|
.filter(|ev| {
|
||||||
|
// Filter out responses after the end_timestamp.
|
||||||
|
ev.origin_server_ts <= end_ts
|
||||||
|
})
|
||||||
|
.fold(BTreeMap::new(), |mut acc, ev| {
|
||||||
|
let response =
|
||||||
|
acc.entry(&*ev.sender).or_insert((MilliSecondsSinceUnixEpoch(uint!(0)), None));
|
||||||
|
|
||||||
|
// Only keep the latest selections for each user.
|
||||||
|
if response.0 < ev.origin_server_ts {
|
||||||
|
*response = (ev.origin_server_ts, ev.content.poll_response.validate(poll));
|
||||||
|
}
|
||||||
|
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
|
||||||
|
aggregate_results(poll.answers.iter().map(|a| a.id.as_str()), users_selections)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate the given selections by answer.
|
||||||
|
fn aggregate_results<'a>(
|
||||||
|
answers: impl Iterator<Item = &'a str>,
|
||||||
|
users_selections: BTreeMap<
|
||||||
|
&'a UserId,
|
||||||
|
(MilliSecondsSinceUnixEpoch, Option<impl Iterator<Item = &'a str>>),
|
||||||
|
>,
|
||||||
|
) -> IndexMap<&'a str, BTreeSet<&'a UserId>> {
|
||||||
|
let mut results = IndexMap::from_iter(answers.into_iter().map(|a| (a, BTreeSet::new())));
|
||||||
|
|
||||||
for (user, (_, selections)) in users_selections {
|
for (user, (_, selections)) in users_selections {
|
||||||
if let Some(selections) = selections {
|
if let Some(selections) = selections {
|
||||||
@ -72,3 +127,51 @@ pub fn compile_poll_results<'a>(
|
|||||||
|
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate the fallback text representation of a poll end event.
|
||||||
|
///
|
||||||
|
/// This is a sentence that lists the top answers for the given results, in english. It is used to
|
||||||
|
/// generate a valid poll end event when using
|
||||||
|
/// `OriginalSync(Unstable)PollStartEvent::compile_results()`.
|
||||||
|
///
|
||||||
|
/// `answers` is an iterator of `(answer ID, answer plain text representation)` and `results` is an
|
||||||
|
/// iterator of `(answer ID, count)` ordered in descending order.
|
||||||
|
fn generate_poll_end_fallback_text<'a>(
|
||||||
|
answers: &[(&'a str, &'a str)],
|
||||||
|
results: impl Iterator<Item = (&'a str, usize)>,
|
||||||
|
) -> String {
|
||||||
|
let mut top_answers = Vec::new();
|
||||||
|
let mut top_count = 0;
|
||||||
|
|
||||||
|
for (id, count) in results {
|
||||||
|
if count >= top_count {
|
||||||
|
top_answers.push(id);
|
||||||
|
top_count = count;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let top_answers_text = top_answers
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| {
|
||||||
|
answers
|
||||||
|
.iter()
|
||||||
|
.find(|(a_id, _)| *a_id == id)
|
||||||
|
.expect("top answer ID should be a valid answer ID")
|
||||||
|
.1
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Construct the plain text representation.
|
||||||
|
match top_answers_text.len() {
|
||||||
|
l if l > 1 => {
|
||||||
|
let answers = top_answers_text.join(", ");
|
||||||
|
format!("The poll has closed. Top answers: {answers}")
|
||||||
|
}
|
||||||
|
l if l == 1 => {
|
||||||
|
format!("The poll has closed. Top answer: {}", top_answers_text[0])
|
||||||
|
}
|
||||||
|
_ => "The poll has closed with no top answer".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,7 +19,14 @@ use crate::{
|
|||||||
/// This type can be generated from the poll start and poll response events with
|
/// This type can be generated from the poll start and poll response events with
|
||||||
/// [`OriginalSyncPollStartEvent::compile_results()`].
|
/// [`OriginalSyncPollStartEvent::compile_results()`].
|
||||||
///
|
///
|
||||||
|
/// 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 end event for a room version that does not support extensible events, use
|
||||||
|
/// [`UnstablePollEndEventContent`].
|
||||||
|
///
|
||||||
/// [`OriginalSyncPollStartEvent::compile_results()`]: super::start::OriginalSyncPollStartEvent::compile_results
|
/// [`OriginalSyncPollStartEvent::compile_results()`]: super::start::OriginalSyncPollStartEvent::compile_results
|
||||||
|
/// [`UnstablePollEndEventContent`]: super::unstable_end::UnstablePollEndEventContent
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
#[ruma_event(type = "m.poll.end", kind = MessageLike)]
|
#[ruma_event(type = "m.poll.end", kind = MessageLike)]
|
||||||
|
@ -10,6 +10,14 @@ use crate::{events::relation::Reference, OwnedEventId};
|
|||||||
use super::start::PollContentBlock;
|
use super::start::PollContentBlock;
|
||||||
|
|
||||||
/// The payload for a poll response event.
|
/// The payload for a poll response 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 response event for a room version that does not support extensible events, use
|
||||||
|
/// [`UnstablePollResponseEventContent`].
|
||||||
|
///
|
||||||
|
/// [`UnstablePollResponseEventContent`]: super::unstable_response::UnstablePollResponseEventContent
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
#[ruma_event(type = "m.poll.response", kind = MessageLike)]
|
#[ruma_event(type = "m.poll.response", kind = MessageLike)]
|
||||||
|
@ -15,10 +15,19 @@ use crate::{events::message::TextContentBlock, serde::StringEnum, PrivOwnedStr};
|
|||||||
use super::{
|
use super::{
|
||||||
compile_poll_results,
|
compile_poll_results,
|
||||||
end::{PollEndEventContent, PollResultsContentBlock},
|
end::{PollEndEventContent, PollResultsContentBlock},
|
||||||
|
generate_poll_end_fallback_text,
|
||||||
response::OriginalSyncPollResponseEvent,
|
response::OriginalSyncPollResponseEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The payload for a poll start event.
|
/// 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)]
|
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
||||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
#[ruma_event(type = "m.poll.start", kind = MessageLike)]
|
#[ruma_event(type = "m.poll.start", kind = MessageLike)]
|
||||||
@ -75,59 +84,32 @@ impl OriginalSyncPollStartEvent {
|
|||||||
&'a self,
|
&'a self,
|
||||||
responses: impl IntoIterator<Item = &'a OriginalSyncPollResponseEvent>,
|
responses: impl IntoIterator<Item = &'a OriginalSyncPollResponseEvent>,
|
||||||
) -> PollEndEventContent {
|
) -> PollEndEventContent {
|
||||||
let computed = compile_poll_results(&self.content.poll, responses, None);
|
let full_results = compile_poll_results(&self.content.poll, responses, None);
|
||||||
|
let results =
|
||||||
|
full_results.into_iter().map(|(id, users)| (id, users.len())).collect::<Vec<_>>();
|
||||||
|
|
||||||
// Construct the results and get the top answer(s).
|
// Construct the results and get the top answer(s).
|
||||||
let mut top_answers = Vec::new();
|
let poll_results = PollResultsContentBlock::from_iter(
|
||||||
let mut top_count = uint!(0);
|
results
|
||||||
|
.iter()
|
||||||
let results =
|
.map(|(id, count)| ((*id).to_owned(), (*count).try_into().unwrap_or(UInt::MAX))),
|
||||||
PollResultsContentBlock::from_iter(computed.into_iter().map(|(id, users)| {
|
);
|
||||||
let count = users.len().try_into().unwrap_or(UInt::MAX);
|
|
||||||
|
|
||||||
if count >= top_count {
|
|
||||||
top_answers.push(id);
|
|
||||||
top_count = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
(id.to_owned(), count)
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Get the text representation of the best answers.
|
// Get the text representation of the best answers.
|
||||||
let top_answers_text = top_answers
|
let answers = self
|
||||||
.into_iter()
|
.content
|
||||||
.map(|id| {
|
.poll
|
||||||
let text = &self
|
.answers
|
||||||
.content
|
.iter()
|
||||||
.poll
|
.map(|a| {
|
||||||
.answers
|
let text = a.text.find_plain().unwrap_or(&a.id);
|
||||||
.iter()
|
(a.id.as_str(), text)
|
||||||
.find(|a| a.id == id)
|
|
||||||
.expect("top answer ID should be a valid answer ID")
|
|
||||||
.text;
|
|
||||||
// Use the plain text representation, fallback to the first text representation or
|
|
||||||
// to the answer ID.
|
|
||||||
text.find_plain()
|
|
||||||
.or_else(|| text.get(0).map(|t| t.body.as_ref()))
|
|
||||||
.unwrap_or(id)
|
|
||||||
.to_owned()
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let plain_text = generate_poll_end_fallback_text(&answers, results.into_iter());
|
||||||
// Construct the plain text representation.
|
|
||||||
let plain_text = match top_answers_text.len() {
|
|
||||||
l if l > 1 => {
|
|
||||||
let answers = top_answers_text.join(", ");
|
|
||||||
format!("The poll has closed. Top answers: {answers}")
|
|
||||||
}
|
|
||||||
l if l == 1 => {
|
|
||||||
format!("The poll has closed. Top answer: {}", top_answers_text[0])
|
|
||||||
}
|
|
||||||
_ => "The poll has closed with no top answer".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut end = PollEndEventContent::with_plain_text(plain_text, self.event_id.clone());
|
let mut end = PollEndEventContent::with_plain_text(plain_text, self.event_id.clone());
|
||||||
end.poll_results = Some(results);
|
end.poll_results = Some(poll_results);
|
||||||
|
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
@ -170,7 +152,7 @@ impl PollContentBlock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_max_selections() -> UInt {
|
pub(super) fn default_max_selections() -> UInt {
|
||||||
uint!(1)
|
uint!(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
56
crates/ruma-common/src/events/poll/unstable_end.rs
Normal file
56
crates/ruma-common/src/events/poll/unstable_end.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
//! Types for the `org.matrix.msc3381.poll.end` event, the unstable version of `m.poll.end`.
|
||||||
|
|
||||||
|
use ruma_macros::EventContent;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{events::relation::Reference, OwnedEventId};
|
||||||
|
|
||||||
|
/// The payload for an unstable poll end event.
|
||||||
|
///
|
||||||
|
/// This type can be generated from the unstable poll start and poll response events with
|
||||||
|
/// [`OriginalSyncUnstablePollStartEvent::compile_results()`].
|
||||||
|
///
|
||||||
|
/// This is the event content that should be sent for room versions that don't support extensible
|
||||||
|
/// events. As of Matrix 1.7, none of the stable room versions (1 through 10) support extensible
|
||||||
|
/// events.
|
||||||
|
///
|
||||||
|
/// To send a poll end event for a room version that supports extensible events, use
|
||||||
|
/// [`PollEndEventContent`].
|
||||||
|
///
|
||||||
|
/// [`OriginalSyncUnstablePollStartEvent::compile_results()`]: super::unstable_start::OriginalSyncUnstablePollStartEvent::compile_results
|
||||||
|
/// [`PollEndEventContent`]: super::end::PollEndEventContent
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[ruma_event(type = "org.matrix.msc3381.poll.end", kind = MessageLike)]
|
||||||
|
pub struct UnstablePollEndEventContent {
|
||||||
|
/// The text representation of the results.
|
||||||
|
#[serde(rename = "org.matrix.msc1767.text")]
|
||||||
|
pub text: String,
|
||||||
|
|
||||||
|
/// The poll end content.
|
||||||
|
#[serde(default, rename = "org.matrix.msc3381.poll.end")]
|
||||||
|
pub poll_end: UnstablePollEndContentBlock,
|
||||||
|
|
||||||
|
/// Information about the poll start event this responds to.
|
||||||
|
#[serde(rename = "m.relates_to")]
|
||||||
|
pub relates_to: Reference,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnstablePollEndEventContent {
|
||||||
|
/// Creates a new `PollEndEventContent` with the given fallback representation and
|
||||||
|
/// that responds to the given poll start event ID.
|
||||||
|
pub fn new(text: impl Into<String>, poll_start_id: OwnedEventId) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.into(),
|
||||||
|
poll_end: UnstablePollEndContentBlock {},
|
||||||
|
relates_to: Reference::new(poll_start_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A block for the results of a poll.
|
||||||
|
///
|
||||||
|
/// This is currently an empty struct.
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct UnstablePollEndContentBlock {}
|
86
crates/ruma-common/src/events/poll/unstable_response.rs
Normal file
86
crates/ruma-common/src/events/poll/unstable_response.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
//! Types for the `org.matrix.msc3381.poll.response` event, the unstable version of
|
||||||
|
//! `m.poll.response`.
|
||||||
|
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use ruma_macros::EventContent;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{events::relation::Reference, OwnedEventId};
|
||||||
|
|
||||||
|
use super::unstable_start::UnstablePollStartContentBlock;
|
||||||
|
|
||||||
|
/// The payload for an unstable poll response event.
|
||||||
|
///
|
||||||
|
/// This is the event content that should be sent for room versions that don't support extensible
|
||||||
|
/// events. As of Matrix 1.7, none of the stable room versions (1 through 10) support extensible
|
||||||
|
/// events.
|
||||||
|
///
|
||||||
|
/// To send a poll response event for a room version that supports extensible events, use
|
||||||
|
/// [`PollResponseEventContent`].
|
||||||
|
///
|
||||||
|
/// [`PollResponseEventContent`]: super::response::PollResponseEventContent
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[ruma_event(type = "org.matrix.msc3381.poll.response", kind = MessageLike)]
|
||||||
|
pub struct UnstablePollResponseEventContent {
|
||||||
|
/// The response's content.
|
||||||
|
#[serde(rename = "org.matrix.msc3381.poll.response")]
|
||||||
|
pub poll_response: UnstablePollResponseContentBlock,
|
||||||
|
|
||||||
|
/// Information about the poll start event this responds to.
|
||||||
|
#[serde(rename = "m.relates_to")]
|
||||||
|
pub relates_to: Reference,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnstablePollResponseEventContent {
|
||||||
|
/// Creates a new `UnstablePollResponseEventContent` that responds to the given poll start event
|
||||||
|
/// ID, with the given answers.
|
||||||
|
pub fn new(answers: Vec<String>, poll_start_id: OwnedEventId) -> Self {
|
||||||
|
Self {
|
||||||
|
poll_response: UnstablePollResponseContentBlock::new(answers),
|
||||||
|
relates_to: Reference::new(poll_start_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An unstable block for poll response content.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct UnstablePollResponseContentBlock {
|
||||||
|
/// The selected answers for the response.
|
||||||
|
pub answers: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnstablePollResponseContentBlock {
|
||||||
|
/// Creates a new `UnstablePollResponseContentBlock` with the given answers.
|
||||||
|
pub fn new(answers: Vec<String>) -> Self {
|
||||||
|
Self { answers }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate these selections against the given `UnstablePollStartContentBlock`.
|
||||||
|
///
|
||||||
|
/// Returns the list of valid selections in this `UnstablePollResponseContentBlock`, or `None`
|
||||||
|
/// if there is no valid selection.
|
||||||
|
pub fn validate(
|
||||||
|
&self,
|
||||||
|
poll: &UnstablePollStartContentBlock,
|
||||||
|
) -> Option<impl Iterator<Item = &str>> {
|
||||||
|
// Vote is spoiled if any answer is unknown.
|
||||||
|
if self.answers.iter().any(|s| !poll.answers.iter().any(|a| a.id == *s)) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to the maximum value for usize because we can't have more selections than that
|
||||||
|
// in memory.
|
||||||
|
let max_selections: usize = poll.max_selections.try_into().unwrap_or(usize::MAX);
|
||||||
|
|
||||||
|
Some(self.answers.iter().take(max_selections).map(Deref::deref))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<String>> for UnstablePollResponseContentBlock {
|
||||||
|
fn from(value: Vec<String>) -> Self {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
194
crates/ruma-common/src/events/poll/unstable_start.rs
Normal file
194
crates/ruma-common/src/events/poll/unstable_start.rs
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
//! Types for the `org.matrix.msc3381.poll.start` event, the unstable version of `m.poll.start`.
|
||||||
|
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use js_int::UInt;
|
||||||
|
use ruma_macros::EventContent;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
mod unstable_poll_answers_serde;
|
||||||
|
mod unstable_poll_kind_serde;
|
||||||
|
|
||||||
|
use self::unstable_poll_answers_serde::UnstablePollAnswersDeHelper;
|
||||||
|
use super::{
|
||||||
|
compile_unstable_poll_results, generate_poll_end_fallback_text,
|
||||||
|
start::{PollAnswers, PollAnswersError, PollContentBlock, PollKind},
|
||||||
|
unstable_end::UnstablePollEndEventContent,
|
||||||
|
unstable_response::OriginalSyncUnstablePollResponseEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The payload for an unstable poll start event.
|
||||||
|
///
|
||||||
|
/// This is the event content that should be sent for room versions that don't 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 supports extensible events, use
|
||||||
|
/// [`PollStartEventContent`].
|
||||||
|
///
|
||||||
|
/// [`PollStartEventContent`]: super::start::PollStartEventContent
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
#[ruma_event(type = "org.matrix.msc3381.poll.start", kind = MessageLike)]
|
||||||
|
pub struct UnstablePollStartEventContent {
|
||||||
|
/// The poll content of the message.
|
||||||
|
#[serde(rename = "org.matrix.msc3381.poll.start")]
|
||||||
|
pub poll_start: UnstablePollStartContentBlock,
|
||||||
|
|
||||||
|
/// Text representation of the message, for clients that don't support polls.
|
||||||
|
#[serde(rename = "org.matrix.msc1767.text")]
|
||||||
|
pub text: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnstablePollStartEventContent {
|
||||||
|
/// Creates a new `PollStartEventContent` with the given poll content.
|
||||||
|
pub fn new(poll_start: UnstablePollStartContentBlock) -> Self {
|
||||||
|
Self { poll_start, text: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `PollStartEventContent` with the given plain text fallback
|
||||||
|
/// representation and poll content.
|
||||||
|
pub fn plain_text(text: impl Into<String>, poll_start: UnstablePollStartContentBlock) -> Self {
|
||||||
|
Self { poll_start, text: Some(text.into()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OriginalSyncUnstablePollStartEvent {
|
||||||
|
/// Compile the results for this poll with the given response into an
|
||||||
|
/// `UnstablePollEndEventContent`.
|
||||||
|
///
|
||||||
|
/// It generates a default text representation of the results in English.
|
||||||
|
///
|
||||||
|
/// This uses [`compile_unstable_poll_results()`] internally.
|
||||||
|
pub fn compile_results<'a>(
|
||||||
|
&'a self,
|
||||||
|
responses: impl IntoIterator<Item = &'a OriginalSyncUnstablePollResponseEvent>,
|
||||||
|
) -> UnstablePollEndEventContent {
|
||||||
|
let full_results = compile_unstable_poll_results(&self.content.poll_start, responses, None);
|
||||||
|
let results =
|
||||||
|
full_results.into_iter().map(|(id, users)| (id, users.len())).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Get the text representation of the best answers.
|
||||||
|
let answers = self
|
||||||
|
.content
|
||||||
|
.poll_start
|
||||||
|
.answers
|
||||||
|
.iter()
|
||||||
|
.map(|a| (a.id.as_str(), a.text.as_str()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let plain_text = generate_poll_end_fallback_text(&answers, results.into_iter());
|
||||||
|
|
||||||
|
UnstablePollEndEventContent::new(plain_text, self.event_id.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An unstable block for poll start content.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct UnstablePollStartContentBlock {
|
||||||
|
/// The question of the poll.
|
||||||
|
pub question: UnstablePollQuestion,
|
||||||
|
|
||||||
|
/// The kind of the poll.
|
||||||
|
#[serde(default, with = "unstable_poll_kind_serde")]
|
||||||
|
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")]
|
||||||
|
pub max_selections: UInt,
|
||||||
|
|
||||||
|
/// The possible answers to the poll.
|
||||||
|
pub answers: UnstablePollAnswers,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnstablePollStartContentBlock {
|
||||||
|
/// Creates a new `PollStartContent` with the given question and answers.
|
||||||
|
pub fn new(question: impl Into<String>, answers: UnstablePollAnswers) -> Self {
|
||||||
|
Self {
|
||||||
|
question: UnstablePollQuestion::new(question),
|
||||||
|
kind: Default::default(),
|
||||||
|
max_selections: PollContentBlock::default_max_selections(),
|
||||||
|
answers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An unstable poll question.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct UnstablePollQuestion {
|
||||||
|
/// The text representation of the question.
|
||||||
|
#[serde(rename = "org.matrix.msc1767.text")]
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnstablePollQuestion {
|
||||||
|
/// Creates a new `UnstablePollQuestion` with the given plain text.
|
||||||
|
pub fn new(text: impl Into<String>) -> Self {
|
||||||
|
Self { text: text.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The unstable answers to a poll.
|
||||||
|
///
|
||||||
|
/// Must include between 1 and 20 `UnstablePollAnswer`s.
|
||||||
|
///
|
||||||
|
/// To build this, use one of the `TryFrom` implementations.
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(try_from = "UnstablePollAnswersDeHelper")]
|
||||||
|
pub struct UnstablePollAnswers(Vec<UnstablePollAnswer>);
|
||||||
|
|
||||||
|
impl TryFrom<Vec<UnstablePollAnswer>> for UnstablePollAnswers {
|
||||||
|
type Error = PollAnswersError;
|
||||||
|
|
||||||
|
fn try_from(value: Vec<UnstablePollAnswer>) -> Result<Self, Self::Error> {
|
||||||
|
if value.len() < PollAnswers::MIN_LENGTH {
|
||||||
|
Err(PollAnswersError::NotEnoughValues)
|
||||||
|
} else if value.len() > PollAnswers::MAX_LENGTH {
|
||||||
|
Err(PollAnswersError::TooManyValues)
|
||||||
|
} else {
|
||||||
|
Ok(Self(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&[UnstablePollAnswer]> for UnstablePollAnswers {
|
||||||
|
type Error = PollAnswersError;
|
||||||
|
|
||||||
|
fn try_from(value: &[UnstablePollAnswer]) -> Result<Self, Self::Error> {
|
||||||
|
Self::try_from(value.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for UnstablePollAnswers {
|
||||||
|
type Target = [UnstablePollAnswer];
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unstable poll answer.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||||
|
pub struct UnstablePollAnswer {
|
||||||
|
/// The ID of the answer.
|
||||||
|
///
|
||||||
|
/// This must be unique among the answers of a poll.
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
/// The text representation of the answer.
|
||||||
|
#[serde(rename = "org.matrix.msc1767.text")]
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnstablePollAnswer {
|
||||||
|
/// Creates a new `PollAnswer` with the given id and text representation.
|
||||||
|
pub fn new(id: impl Into<String>, text: impl Into<String>) -> Self {
|
||||||
|
Self { id: id.into(), text: text.into() }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
//! `Deserialize` helpers for unstable poll answers (MSC3381).
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::events::poll::start::{PollAnswers, PollAnswersError};
|
||||||
|
|
||||||
|
use super::{UnstablePollAnswer, UnstablePollAnswers};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
pub(crate) struct UnstablePollAnswersDeHelper(Vec<UnstablePollAnswer>);
|
||||||
|
|
||||||
|
impl TryFrom<UnstablePollAnswersDeHelper> for UnstablePollAnswers {
|
||||||
|
type Error = PollAnswersError;
|
||||||
|
|
||||||
|
fn try_from(helper: UnstablePollAnswersDeHelper) -> Result<Self, Self::Error> {
|
||||||
|
let mut answers = helper.0;
|
||||||
|
answers.truncate(PollAnswers::MAX_LENGTH);
|
||||||
|
UnstablePollAnswers::try_from(answers)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
//! `Serialize` and `Deserialize` helpers for unstable poll kind (MSC3381).
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
use crate::{events::poll::start::PollKind, PrivOwnedStr};
|
||||||
|
|
||||||
|
/// Serializes a PollKind using the unstable prefixes.
|
||||||
|
pub(super) fn serialize<S>(kind: &PollKind, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let s = match kind {
|
||||||
|
PollKind::Undisclosed => "org.matrix.msc3381.poll.undisclosed",
|
||||||
|
PollKind::Disclosed => "org.matrix.msc3381.poll.disclosed",
|
||||||
|
PollKind::_Custom(s) => &s.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
s.serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes a PollKind using the unstable prefixes.
|
||||||
|
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<PollKind, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = Cow::<'_, str>::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
let kind = match &*s {
|
||||||
|
"org.matrix.msc3381.poll.undisclosed" => PollKind::Undisclosed,
|
||||||
|
"org.matrix.msc3381.poll.disclosed" => PollKind::Disclosed,
|
||||||
|
_ => PollKind::_Custom(PrivOwnedStr(s.into())),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(kind)
|
||||||
|
}
|
@ -8,13 +8,21 @@ use ruma_common::{
|
|||||||
events::{
|
events::{
|
||||||
message::TextContentBlock,
|
message::TextContentBlock,
|
||||||
poll::{
|
poll::{
|
||||||
compile_poll_results,
|
compile_poll_results, compile_unstable_poll_results,
|
||||||
end::PollEndEventContent,
|
end::PollEndEventContent,
|
||||||
response::{OriginalSyncPollResponseEvent, PollResponseEventContent},
|
response::{OriginalSyncPollResponseEvent, PollResponseEventContent},
|
||||||
start::{
|
start::{
|
||||||
OriginalSyncPollStartEvent, PollAnswer, PollAnswers, PollAnswersError,
|
OriginalSyncPollStartEvent, PollAnswer, PollAnswers, PollAnswersError,
|
||||||
PollContentBlock, PollKind, PollStartEventContent,
|
PollContentBlock, PollKind, PollStartEventContent,
|
||||||
},
|
},
|
||||||
|
unstable_end::UnstablePollEndEventContent,
|
||||||
|
unstable_response::{
|
||||||
|
OriginalSyncUnstablePollResponseEvent, UnstablePollResponseEventContent,
|
||||||
|
},
|
||||||
|
unstable_start::{
|
||||||
|
OriginalSyncUnstablePollStartEvent, UnstablePollAnswer,
|
||||||
|
UnstablePollStartContentBlock, UnstablePollStartEventContent,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
relation::Reference,
|
relation::Reference,
|
||||||
AnyMessageLikeEvent, MessageLikeEvent,
|
AnyMessageLikeEvent, MessageLikeEvent,
|
||||||
@ -348,6 +356,194 @@ fn end_event_deserialization() {
|
|||||||
assert_eq!(event_id, "$related_event:notareal.hs");
|
assert_eq!(event_id, "$related_event:notareal.hs");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unstable_start_content_serialization() {
|
||||||
|
let event_content = UnstablePollStartEventContent::plain_text(
|
||||||
|
"How's the weather?\n1. Not bad…\n2. Fine.\n3. Amazing!",
|
||||||
|
UnstablePollStartContentBlock::new(
|
||||||
|
"How's the weather?",
|
||||||
|
vec![
|
||||||
|
UnstablePollAnswer::new("not-bad", "Not bad…"),
|
||||||
|
UnstablePollAnswer::new("fine", "Fine."),
|
||||||
|
UnstablePollAnswer::new("amazing", "Amazing!"),
|
||||||
|
]
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
to_json_value(&event_content).unwrap(),
|
||||||
|
json!({
|
||||||
|
"org.matrix.msc1767.text": "How's the weather?\n1. Not bad…\n2. Fine.\n3. Amazing!",
|
||||||
|
"org.matrix.msc3381.poll.start": {
|
||||||
|
"kind": "org.matrix.msc3381.poll.undisclosed",
|
||||||
|
"max_selections": 1,
|
||||||
|
"question": { "org.matrix.msc1767.text": "How's the weather?" },
|
||||||
|
"answers": [
|
||||||
|
{ "id": "not-bad", "org.matrix.msc1767.text": "Not bad…" },
|
||||||
|
{ "id": "fine", "org.matrix.msc1767.text": "Fine." },
|
||||||
|
{ "id": "amazing", "org.matrix.msc1767.text": "Amazing!" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unstable_start_event_deserialization() {
|
||||||
|
let json_data = json!({
|
||||||
|
"content": {
|
||||||
|
"org.matrix.msc1767.text": "How's the weather?\n1. Not bad…\n2. Fine.\n3. Amazing!",
|
||||||
|
"org.matrix.msc3381.poll.start": {
|
||||||
|
"question": { "org.matrix.msc1767.text": "How's the weather?" },
|
||||||
|
"max_selections": 2,
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"id": "not-bad",
|
||||||
|
"org.matrix.msc1767.text": "Not bad…",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fine",
|
||||||
|
"org.matrix.msc1767.text": "Fine.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "amazing",
|
||||||
|
"org.matrix.msc1767.text": "Amazing!",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"event_id": "$event:notareal.hs",
|
||||||
|
"origin_server_ts": 134_829_848,
|
||||||
|
"room_id": "!roomid:notareal.hs",
|
||||||
|
"sender": "@user:notareal.hs",
|
||||||
|
"type": "org.matrix.msc3381.poll.start",
|
||||||
|
});
|
||||||
|
|
||||||
|
let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap();
|
||||||
|
assert_matches!(
|
||||||
|
event,
|
||||||
|
AnyMessageLikeEvent::UnstablePollStart(MessageLikeEvent::Original(message_event))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
message_event.content.text.unwrap(),
|
||||||
|
"How's the weather?\n1. Not bad…\n2. Fine.\n3. Amazing!"
|
||||||
|
);
|
||||||
|
let poll = message_event.content.poll_start;
|
||||||
|
assert_eq!(poll.question.text, "How's the weather?");
|
||||||
|
assert_eq!(poll.kind, PollKind::Undisclosed);
|
||||||
|
assert_eq!(poll.max_selections, uint!(2));
|
||||||
|
let answers = poll.answers;
|
||||||
|
assert_eq!(answers.len(), 3);
|
||||||
|
assert_eq!(answers[0].id, "not-bad");
|
||||||
|
assert_eq!(answers[0].text, "Not bad…");
|
||||||
|
assert_eq!(answers[1].id, "fine");
|
||||||
|
assert_eq!(answers[1].text, "Fine.");
|
||||||
|
assert_eq!(answers[2].id, "amazing");
|
||||||
|
assert_eq!(answers[2].text, "Amazing!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unstable_response_content_serialization() {
|
||||||
|
let event_content = UnstablePollResponseEventContent::new(
|
||||||
|
vec!["my-answer".to_owned()],
|
||||||
|
owned_event_id!("$related_event:notareal.hs"),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
to_json_value(&event_content).unwrap(),
|
||||||
|
json!({
|
||||||
|
"org.matrix.msc3381.poll.response": {
|
||||||
|
"answers": ["my-answer"],
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.reference",
|
||||||
|
"event_id": "$related_event:notareal.hs",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unstable_response_event_deserialization() {
|
||||||
|
let json_data = json!({
|
||||||
|
"content": {
|
||||||
|
"org.matrix.msc3381.poll.response": {
|
||||||
|
"answers": ["my-answer"],
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.reference",
|
||||||
|
"event_id": "$related_event:notareal.hs",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"event_id": "$event:notareal.hs",
|
||||||
|
"origin_server_ts": 134_829_848,
|
||||||
|
"room_id": "!roomid:notareal.hs",
|
||||||
|
"sender": "@user:notareal.hs",
|
||||||
|
"type": "org.matrix.msc3381.poll.response",
|
||||||
|
});
|
||||||
|
|
||||||
|
let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap();
|
||||||
|
assert_matches!(
|
||||||
|
event,
|
||||||
|
AnyMessageLikeEvent::UnstablePollResponse(MessageLikeEvent::Original(message_event))
|
||||||
|
);
|
||||||
|
let selections = message_event.content.poll_response.answers;
|
||||||
|
assert_eq!(selections.len(), 1);
|
||||||
|
assert_eq!(selections[0], "my-answer");
|
||||||
|
assert_matches!(message_event.content.relates_to, Reference { event_id, .. });
|
||||||
|
assert_eq!(event_id, "$related_event:notareal.hs");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unstable_end_content_serialization() {
|
||||||
|
let event_content = UnstablePollEndEventContent::new(
|
||||||
|
"The poll has closed. Top answer: Amazing!",
|
||||||
|
owned_event_id!("$related_event:notareal.hs"),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
to_json_value(&event_content).unwrap(),
|
||||||
|
json!({
|
||||||
|
"org.matrix.msc1767.text": "The poll has closed. Top answer: Amazing!",
|
||||||
|
"org.matrix.msc3381.poll.end": {},
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.reference",
|
||||||
|
"event_id": "$related_event:notareal.hs",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unstable_end_event_deserialization() {
|
||||||
|
let json_data = json!({
|
||||||
|
"content": {
|
||||||
|
"org.matrix.msc1767.text": "The poll has closed. Top answer: Amazing!",
|
||||||
|
"org.matrix.msc3381.poll.end": {},
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.reference",
|
||||||
|
"event_id": "$related_event:notareal.hs",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"event_id": "$event:notareal.hs",
|
||||||
|
"origin_server_ts": 134_829_848,
|
||||||
|
"room_id": "!roomid:notareal.hs",
|
||||||
|
"sender": "@user:notareal.hs",
|
||||||
|
"type": "org.matrix.msc3381.poll.end",
|
||||||
|
});
|
||||||
|
|
||||||
|
let event = from_json_value::<AnyMessageLikeEvent>(json_data).unwrap();
|
||||||
|
assert_matches!(
|
||||||
|
event,
|
||||||
|
AnyMessageLikeEvent::UnstablePollEnd(MessageLikeEvent::Original(message_event))
|
||||||
|
);
|
||||||
|
assert_eq!(message_event.content.text, "The poll has closed. Top answer: Amazing!");
|
||||||
|
assert_matches!(message_event.content.relates_to, Reference { event_id, .. });
|
||||||
|
assert_eq!(event_id, "$related_event:notareal.hs");
|
||||||
|
}
|
||||||
|
|
||||||
fn new_poll_response(
|
fn new_poll_response(
|
||||||
event_id: &str,
|
event_id: &str,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
@ -644,3 +840,91 @@ fn compute_results() {
|
|||||||
assert_eq!(*results.get("italian").unwrap(), uint!(6));
|
assert_eq!(*results.get("italian").unwrap(), uint!(6));
|
||||||
assert_eq!(*results.get("wings").unwrap(), uint!(7));
|
assert_eq!(*results.get("wings").unwrap(), uint!(7));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn new_unstable_poll_response(
|
||||||
|
event_id: &str,
|
||||||
|
user_id: &str,
|
||||||
|
ts: UInt,
|
||||||
|
selections: &[&str],
|
||||||
|
) -> OriginalSyncUnstablePollResponseEvent {
|
||||||
|
from_json_value(json!({
|
||||||
|
"type": "org.matrix.msc3381.poll.response",
|
||||||
|
"sender": user_id,
|
||||||
|
"origin_server_ts": ts,
|
||||||
|
"event_id": event_id,
|
||||||
|
"content": {
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.reference",
|
||||||
|
"event_id": "$poll_start_event_id"
|
||||||
|
},
|
||||||
|
"org.matrix.msc3381.poll.response": {
|
||||||
|
"answers": selections,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_unstable_poll_responses(
|
||||||
|
range: Range<usize>,
|
||||||
|
selections: &[&str],
|
||||||
|
) -> Vec<OriginalSyncUnstablePollResponseEvent> {
|
||||||
|
let mut responses = Vec::with_capacity(range.len());
|
||||||
|
|
||||||
|
for i in range {
|
||||||
|
let event_id = format!("$valid_event_{i}");
|
||||||
|
let user_id = format!("@valid_user_{i}:localhost");
|
||||||
|
let ts = 1000 + i as u16;
|
||||||
|
|
||||||
|
responses.push(new_unstable_poll_response(&event_id, &user_id, ts.into(), selections));
|
||||||
|
}
|
||||||
|
|
||||||
|
responses
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compute_unstable_results() {
|
||||||
|
let poll: OriginalSyncUnstablePollStartEvent = from_json_value(json!({
|
||||||
|
"type": "org.matrix.msc3381.poll.start",
|
||||||
|
"sender": "@alice:localhost",
|
||||||
|
"event_id": "$poll_start_event_id",
|
||||||
|
"origin_server_ts": 1,
|
||||||
|
"content": {
|
||||||
|
"org.matrix.msc1767.text": "What should we order for the party?\n1. Pizza 🍕\n2. Poutine 🍟\n3. Italian 🍝\n4. Wings 🔥",
|
||||||
|
"org.matrix.msc3381.poll.start": {
|
||||||
|
"kind": "org.matrix.msc3381.poll.disclosed",
|
||||||
|
"max_selections": 2,
|
||||||
|
"question": {
|
||||||
|
"org.matrix.msc1767.text": "What should we order for the party?",
|
||||||
|
},
|
||||||
|
"answers": [
|
||||||
|
{ "id": "pizza", "org.matrix.msc1767.text": "Pizza 🍕" },
|
||||||
|
{ "id": "poutine", "org.matrix.msc1767.text": "Poutine 🍟" },
|
||||||
|
{ "id": "italian", "org.matrix.msc1767.text": "Italian 🍝" },
|
||||||
|
{ "id": "wings", "org.matrix.msc1767.text": "Wings 🔥" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})).unwrap();
|
||||||
|
|
||||||
|
// Populate responses.
|
||||||
|
let mut responses = generate_unstable_poll_responses(0..5, &["pizza"]);
|
||||||
|
responses.extend(generate_unstable_poll_responses(5..6, &["poutine"]));
|
||||||
|
responses.extend(generate_unstable_poll_responses(6..8, &["italian"]));
|
||||||
|
responses.extend(generate_unstable_poll_responses(8..11, &["wings"]));
|
||||||
|
|
||||||
|
let counted = compile_unstable_poll_results(&poll.content.poll_start, &responses, None);
|
||||||
|
assert_eq!(counted.get("pizza").unwrap().len(), 5);
|
||||||
|
assert_eq!(counted.get("poutine").unwrap().len(), 1);
|
||||||
|
assert_eq!(counted.get("italian").unwrap().len(), 2);
|
||||||
|
assert_eq!(counted.get("wings").unwrap().len(), 3);
|
||||||
|
let mut iter = counted.keys();
|
||||||
|
assert_eq!(iter.next(), Some(&"pizza"));
|
||||||
|
assert_eq!(iter.next(), Some(&"wings"));
|
||||||
|
assert_eq!(iter.next(), Some(&"italian"));
|
||||||
|
assert_eq!(iter.next(), Some(&"poutine"));
|
||||||
|
assert_eq!(iter.next(), None);
|
||||||
|
|
||||||
|
let poll_end = poll.compile_results(&responses);
|
||||||
|
assert_eq!(poll_end.text, "The poll has closed. Top answer: Pizza 🍕");
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user