diff --git a/twilight-cache-inmemory/src/event/interaction.rs b/twilight-cache-inmemory/src/event/interaction.rs index 851bdf6342e..a31f7b310ee 100644 --- a/twilight-cache-inmemory/src/event/interaction.rs +++ b/twilight-cache-inmemory/src/event/interaction.rs @@ -213,6 +213,7 @@ mod tests { mention_roles: Vec::new(), mentions: Vec::new(), pinned: false, + poll: None, reactions: Vec::new(), reference: None, role_subscription_data: None, diff --git a/twilight-cache-inmemory/src/event/message.rs b/twilight-cache-inmemory/src/event/message.rs index 181b5333a1e..93ed48976ef 100644 --- a/twilight-cache-inmemory/src/event/message.rs +++ b/twilight-cache-inmemory/src/event/message.rs @@ -165,6 +165,7 @@ mod tests { mention_roles: Vec::new(), mentions: Vec::new(), pinned: false, + poll: None, reactions: Vec::new(), reference: None, role_subscription_data: None, diff --git a/twilight-cache-inmemory/src/lib.rs b/twilight-cache-inmemory/src/lib.rs index 04469ad6386..c0b939bd7d5 100644 --- a/twilight-cache-inmemory/src/lib.rs +++ b/twilight-cache-inmemory/src/lib.rs @@ -994,6 +994,8 @@ impl UpdateCache for Event { | Event::GuildScheduledEventUserRemove(_) | Event::InviteCreate(_) | Event::InviteDelete(_) + | Event::MessagePollVoteAdd(_) + | Event::MessagePollVoteRemove(_) | Event::Resumed | Event::ThreadMembersUpdate(_) | Event::ThreadMemberUpdate(_) diff --git a/twilight-cache-inmemory/src/model/message.rs b/twilight-cache-inmemory/src/model/message.rs index a5ba685f60c..f0bec88e3b3 100644 --- a/twilight-cache-inmemory/src/model/message.rs +++ b/twilight-cache-inmemory/src/model/message.rs @@ -20,6 +20,7 @@ use twilight_model::{ }, Id, }, + poll::Poll, util::Timestamp, }; @@ -114,6 +115,7 @@ pub struct CachedMessage { pub(crate) mention_roles: Vec>, pub(crate) mentions: Vec>, pub(crate) pinned: bool, + pub(crate) poll: Option, pub(crate) reactions: Vec, reference: Option, role_subscription_data: Option, @@ -320,6 +322,7 @@ impl From for CachedMessage { mention_roles, mentions, pinned, + poll, reactions, reference, referenced_message: _, @@ -353,6 +356,7 @@ impl From for CachedMessage { mention_roles, mentions: mentions.into_iter().map(|mention| mention.id).collect(), pinned, + poll, reactions, reference, role_subscription_data, diff --git a/twilight-cache-inmemory/src/test.rs b/twilight-cache-inmemory/src/test.rs index 14701ae8f18..413ff02bdea 100644 --- a/twilight-cache-inmemory/src/test.rs +++ b/twilight-cache-inmemory/src/test.rs @@ -89,6 +89,7 @@ pub fn cache_with_message_and_reactions() -> DefaultInMemoryCache { mention_roles: Vec::new(), mentions: Vec::new(), pinned: false, + poll: None, reactions: Vec::new(), reference: None, role_subscription_data: None, diff --git a/twilight-gateway/src/event.rs b/twilight-gateway/src/event.rs index 35cd147fe6d..86e1ce1eb72 100644 --- a/twilight-gateway/src/event.rs +++ b/twilight-gateway/src/event.rs @@ -138,6 +138,10 @@ bitflags! { const MESSAGE_DELETE = 1 << 20; /// Multiple messages have been deleted in a channel. const MESSAGE_DELETE_BULK = 1 << 21; + /// Message poll vote has been added. + const MESSAGE_POLL_VOTE_ADD = 1 << 28; + /// Message poll vote has been removed. + const MESSAGE_POLL_VOTE_REMOVE = 1 << 29; /// Message in a channel has been updated. const MESSAGE_UPDATE = 1 << 22; /// User's presence details are updated. @@ -282,6 +286,12 @@ bitflags! { | Self::MESSAGE_DELETE_BULK.bits() | Self::MESSAGE_UPDATE.bits(); + /// All [`EventTypeFlags`] in [`Intents::DIRECT_MESSAGE_POLLS`] and [`Intents::GUILD_MESSAGE_POLLS`]. + /// + /// [`Intents::DIRECT_MESSAGE_POLLS`]: crate::Intents::DIRECT_MESSAGE_POLLS + /// [`Intents::GUILD_MESSAGE_POLLS`]: crate::Intents::GUILD_MESSAGE_POLLS + const MESSAGE_POLLS = Self::MESSAGE_POLL_VOTE_ADD.bits() | Self::MESSAGE_POLL_VOTE_REMOVE.bits(); + /// All [`EventTypeFlags`] in [`Intents::GUILD_MESSAGE_REACTIONS`]. /// /// [`Intents::GUILD_MESSAGE_REACTIONS`]: crate::Intents::GUILD_MESSAGE_REACTIONS @@ -370,6 +380,8 @@ impl From for EventTypeFlags { EventType::MessageCreate => Self::MESSAGE_CREATE, EventType::MessageDelete => Self::MESSAGE_DELETE, EventType::MessageDeleteBulk => Self::MESSAGE_DELETE_BULK, + EventType::MessagePollVoteAdd => Self::MESSAGE_POLL_VOTE_ADD, + EventType::MessagePollVoteRemove => Self::MESSAGE_POLL_VOTE_REMOVE, EventType::MessageUpdate => Self::MESSAGE_UPDATE, EventType::PresenceUpdate => Self::PRESENCE_UPDATE, EventType::ReactionAdd => Self::REACTION_ADD, diff --git a/twilight-http-ratelimiting/src/request.rs b/twilight-http-ratelimiting/src/request.rs index 44e91d3b651..0d34befbae3 100644 --- a/twilight-http-ratelimiting/src/request.rs +++ b/twilight-http-ratelimiting/src/request.rs @@ -154,6 +154,8 @@ pub enum Path { ChannelsIdPins(u64), /// Operating on a channel's individual pinned message. ChannelsIdPinsMessageId(u64), + /// Operating on a channel's polls. + ChannelsIdPolls(u64), /// Operating on a group DM's recipients. ChannelsIdRecipients(u64), /// Operating on a thread's members. diff --git a/twilight-http/src/client/mod.rs b/twilight-http/src/client/mod.rs index 7ea356f300f..45aca5aa153 100644 --- a/twilight-http/src/client/mod.rs +++ b/twilight-http/src/client/mod.rs @@ -66,6 +66,7 @@ use crate::{ UpdateCurrentMember, UpdateGuild, UpdateGuildChannelPositions, UpdateGuildMfa, UpdateGuildWelcomeScreen, UpdateGuildWidgetSettings, }, + poll::{EndPoll, GetAnswerVoters}, scheduled_event::{ CreateGuildScheduledEvent, DeleteGuildScheduledEvent, GetGuildScheduledEvent, GetGuildScheduledEventUsers, GetGuildScheduledEvents, UpdateGuildScheduledEvent, @@ -2621,6 +2622,32 @@ impl Client { CreateTestEntitlement::new(self, application_id, sku_id, owner) } + /// Ends a poll in a channel. + /// + /// # Examples + /// + /// ```no_run + /// use twilight_http::Client; + /// use twilight_model::id::Id; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let client = Client::new("my token".to_owned()); + /// + /// let channel_id = Id::new(1); + /// let message_id = Id::new(2); + /// + /// client.end_poll(channel_id, message_id).await?; + /// # Ok(()) } + /// ``` + pub const fn end_poll( + &self, + channel_id: Id, + message_id: Id, + ) -> EndPoll<'_> { + EndPoll::new(self, channel_id, message_id) + } + /// Deletes a currently-active test entitlement. Discord will act as though that user or /// guild no longer has entitlement to your premium offering. /// @@ -2651,6 +2678,35 @@ impl Client { DeleteTestEntitlement::new(self, application_id, entitlement_id) } + /// /// Get the voters for an answer in a poll. + /// + /// # Examples + /// + /// ```no_run + /// use twilight_http::Client; + /// use twilight_model::id::Id; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let client = Client::new("my token".to_owned()); + /// + /// let channel_id = Id::new(1); + /// let message_id = Id::new(2); + /// let answer_id = 1; + /// + /// let voters = client.get_answer_voters(channel_id, message_id, answer_id).await?; + /// + /// println!("{:?}", voters); + /// # Ok(()) } + pub const fn get_answer_voters( + &self, + channel_id: Id, + message_id: Id, + answer_id: u8, + ) -> GetAnswerVoters<'_> { + GetAnswerVoters::new(self, channel_id, message_id, answer_id) + } + /// Returns all SKUs for a given application. /// /// # Examples diff --git a/twilight-http/src/request/channel/message/create_message.rs b/twilight-http/src/request/channel/message/create_message.rs index 9084aba32d3..e70e87fb434 100644 --- a/twilight-http/src/request/channel/message/create_message.rs +++ b/twilight-http/src/request/channel/message/create_message.rs @@ -19,6 +19,7 @@ use twilight_model::{ marker::{ChannelMarker, MessageMarker, StickerMarker}, Id, }, + poll::Poll, }; use twilight_validate::message::{ attachment as validate_attachment, components as validate_components, @@ -47,6 +48,8 @@ pub(crate) struct CreateMessageFields<'a> { #[serde(skip_serializing_if = "Option::is_none")] payload_json: Option<&'a [u8]>, #[serde(skip_serializing_if = "Option::is_none")] + poll: Option<&'a Poll>, + #[serde(skip_serializing_if = "Option::is_none")] sticker_ids: Option<&'a [Id]>, #[serde(skip_serializing_if = "Option::is_none")] tts: Option, @@ -102,6 +105,7 @@ impl<'a> CreateMessage<'a> { message_reference: None, nonce: None, payload_json: None, + poll: None, allowed_mentions: None, sticker_ids: None, tts: None, @@ -222,6 +226,15 @@ impl<'a> CreateMessage<'a> { self } + /// Specify if this message is a poll. + pub fn poll(mut self, poll: &'a Poll) -> Self { + if let Ok(fields) = self.fields.as_mut() { + fields.poll = Some(poll); + } + + self + } + /// Whether to fail sending if the reply no longer exists. /// /// Defaults to [`true`]. diff --git a/twilight-http/src/request/mod.rs b/twilight-http/src/request/mod.rs index 873abe5fe99..77e07abf129 100644 --- a/twilight-http/src/request/mod.rs +++ b/twilight-http/src/request/mod.rs @@ -44,6 +44,7 @@ pub mod application; pub mod attachment; pub mod channel; pub mod guild; +pub mod poll; pub mod scheduled_event; pub mod sticker; pub mod template; diff --git a/twilight-http/src/request/poll/end_poll.rs b/twilight-http/src/request/poll/end_poll.rs new file mode 100644 index 00000000000..0b01b4a528a --- /dev/null +++ b/twilight-http/src/request/poll/end_poll.rs @@ -0,0 +1,68 @@ +use crate::{ + client::Client, + error::Error, + request::{Request, TryIntoRequest}, + response::{Response, ResponseFuture}, + routing::Route, +}; +use serde::Serialize; +use std::future::IntoFuture; +use twilight_model::{ + channel::Message, + id::{ + marker::{ChannelMarker, MessageMarker}, + Id, + }, +}; + +#[derive(Serialize)] +struct EndPollFields { + channel_id: Id, + message_id: Id, +} + +// Ends a poll in a channel. +#[must_use = "requests must be configured and executed"] +pub struct EndPoll<'a> { + fields: EndPollFields, + http: &'a Client, +} + +impl<'a> EndPoll<'a> { + pub(crate) const fn new( + http: &'a Client, + channel_id: Id, + message_id: Id, + ) -> Self { + Self { + fields: EndPollFields { + channel_id, + message_id, + }, + http, + } + } +} + +impl IntoFuture for EndPoll<'_> { + type Output = Result, Error>; + type IntoFuture = ResponseFuture; + + fn into_future(self) -> Self::IntoFuture { + let http = self.http; + + match self.try_into_request() { + Ok(request) => http.request(request), + Err(source) => ResponseFuture::error(source), + } + } +} + +impl TryIntoRequest for EndPoll<'_> { + fn try_into_request(self) -> Result { + Ok(Request::from_route(&Route::EndPoll { + channel_id: self.fields.channel_id.get(), + message_id: self.fields.message_id.get(), + })) + } +} diff --git a/twilight-http/src/request/poll/get_answer_voters.rs b/twilight-http/src/request/poll/get_answer_voters.rs new file mode 100644 index 00000000000..92096dfa644 --- /dev/null +++ b/twilight-http/src/request/poll/get_answer_voters.rs @@ -0,0 +1,99 @@ +use crate::{ + client::Client, + error::Error, + request::{Request, TryIntoRequest}, + response::{Response, ResponseFuture}, + routing::Route, +}; +use serde::{Deserialize, Serialize}; +use std::future::IntoFuture; +use twilight_model::{ + id::{ + marker::{ChannelMarker, MessageMarker, UserMarker}, + Id, + }, + user::User, +}; + +#[derive(Serialize)] +struct GetAnswerVotersFields { + after: Option>, + answer_id: u8, + channel_id: Id, + limit: Option, + message_id: Id, +} + +/// Gets the data for a poll answer. +#[must_use = "requests must be configured and executed"] +pub struct GetAnswerVoters<'a> { + fields: GetAnswerVotersFields, + http: &'a Client, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct GetAnswerVotersResponse { + pub users: Vec, +} + +impl<'a> GetAnswerVoters<'a> { + pub(crate) const fn new( + http: &'a Client, + channel_id: Id, + message_id: Id, + answer_id: u8, + ) -> Self { + Self { + fields: GetAnswerVotersFields { + after: None, + answer_id, + channel_id, + limit: None, + message_id, + }, + http, + } + } + + /// Set the user ID to get voters after. + pub fn after(mut self, after: Id) -> Self { + self.fields.after.replace(after); + + self + } + + /// Set the limit of voters to get. + /// + /// The minimum is 1 and the maximum is 100. + pub fn limit(mut self, limit: u8) -> Self { + self.fields.limit.replace(limit); + + self + } +} + +impl IntoFuture for GetAnswerVoters<'_> { + type Output = Result, Error>; + type IntoFuture = ResponseFuture; + + fn into_future(self) -> Self::IntoFuture { + let http = self.http; + + match self.try_into_request() { + Ok(request) => http.request(request), + Err(source) => ResponseFuture::error(source), + } + } +} + +impl TryIntoRequest for GetAnswerVoters<'_> { + fn try_into_request(self) -> Result { + Ok(Request::from_route(&Route::GetAnswerVoters { + after: self.fields.after.map(Id::get), + answer_id: self.fields.answer_id, + channel_id: self.fields.channel_id.get(), + limit: self.fields.limit, + message_id: self.fields.message_id.get(), + })) + } +} diff --git a/twilight-http/src/request/poll/mod.rs b/twilight-http/src/request/poll/mod.rs new file mode 100644 index 00000000000..2f018b5e868 --- /dev/null +++ b/twilight-http/src/request/poll/mod.rs @@ -0,0 +1,4 @@ +mod end_poll; +mod get_answer_voters; + +pub use self::{end_poll::EndPoll, get_answer_voters::GetAnswerVoters}; diff --git a/twilight-http/src/request/try_into_request.rs b/twilight-http/src/request/try_into_request.rs index 46babfc8747..91f5832f578 100644 --- a/twilight-http/src/request/try_into_request.rs +++ b/twilight-http/src/request/try_into_request.rs @@ -76,6 +76,7 @@ mod private { UpdateCurrentMember, UpdateGuild, UpdateGuildChannelPositions, UpdateGuildMfa, UpdateGuildWelcomeScreen, UpdateGuildWidgetSettings, }, + poll::{EndPoll, GetAnswerVoters}, scheduled_event::{ CreateGuildExternalScheduledEvent, CreateGuildStageInstanceScheduledEvent, CreateGuildVoiceScheduledEvent, DeleteGuildScheduledEvent, GetGuildScheduledEvent, @@ -164,10 +165,12 @@ mod private { impl Sealed for DeleteWebhook<'_> {} impl Sealed for DeleteWebhookMessage<'_> {} impl Sealed for DeleteTestEntitlement<'_> {} + impl Sealed for EndPoll<'_> {} impl Sealed for ExecuteWebhook<'_> {} impl Sealed for ExecuteWebhookAndWait<'_> {} impl Sealed for FollowNewsChannel<'_> {} impl Sealed for GetActiveThreads<'_> {} + impl Sealed for GetAnswerVoters<'_> {} impl Sealed for GetAuditLog<'_> {} impl Sealed for GetAutoModerationRule<'_> {} impl Sealed for GetBan<'_> {} diff --git a/twilight-http/src/routing.rs b/twilight-http/src/routing.rs index 369111449a9..42fdd9ac85d 100644 --- a/twilight-http/src/routing.rs +++ b/twilight-http/src/routing.rs @@ -348,12 +348,18 @@ pub enum Route<'a> { token: &'a str, webhook_id: u64, }, + /// Route information to delete a test entitlement. DeleteTestEntitlement { /// The ID of the application. application_id: u64, /// The ID of the entitlement. entitlement_id: u64, }, + /// Route information to end a poll. + EndPoll { + channel_id: u64, + message_id: u64, + }, /// Route information to execute a webhook by ID and token. ExecuteWebhook { /// ID of the thread channel, if there is one. @@ -375,6 +381,19 @@ pub enum Route<'a> { /// ID of the guild. guild_id: u64, }, + /// Route information for fetching poll vote information. + GetAnswerVoters { + /// Get users after this user ID. + after: Option, + /// The id of the poll answer. + answer_id: u8, + /// The ID of the channel the poll is in. + channel_id: u64, + /// The maximum number of users to return (1-100). + limit: Option, + /// The message ID of the poll. + message_id: u64, + }, /// Route information to get a paginated list of audit logs in a guild. GetAuditLogs { /// The type of action to get audit logs for. @@ -1198,6 +1217,7 @@ impl<'a> Route<'a> { | Self::RemoveThreadMember { .. } | Self::UnpinMessage { .. } => Method::Delete, Self::GetActiveThreads { .. } + | Self::GetAnswerVoters { .. } | Self::GetAuditLogs { .. } | Self::GetAutoModerationRule { .. } | Self::GetBan { .. } @@ -1323,6 +1343,7 @@ impl<'a> Route<'a> { | Self::CreateWebhook { .. } | Self::CrosspostMessage { .. } | Self::DeleteMessages { .. } + | Self::EndPoll { .. } | Self::ExecuteWebhook { .. } | Self::FollowNewsChannel { .. } | Self::InteractionCallback { .. } @@ -1660,6 +1681,9 @@ impl<'a> Route<'a> { } Self::UpdateNickname { guild_id } => Path::GuildsIdMembersMeNick(guild_id), Self::UpdateGuildMfa { guild_id } => Path::GuildsIdMfa(guild_id), + Self::EndPoll { channel_id, .. } | Self::GetAnswerVoters { channel_id, .. } => { + Path::ChannelsIdPolls(channel_id) + } } } } @@ -1798,6 +1822,25 @@ impl Display for Route<'_> { Display::fmt(auto_moderation_rule_id, f) } + Route::GetAnswerVoters { + after, + answer_id, + channel_id, + limit, + message_id, + } => { + f.write_str("channels/")?; + Display::fmt(channel_id, f)?; + f.write_str("/polls/")?; + Display::fmt(message_id, f)?; + f.write_str("/answers/")?; + Display::fmt(answer_id, f)?; + f.write_str("?")?; + + let mut writer = QueryStringFormatter::new(f); + writer.write_opt_param("after", after.as_ref())?; + writer.write_opt_param("limit", limit.as_ref()) + } Route::GetGlobalCommands { application_id, with_localizations, @@ -2311,6 +2354,17 @@ impl Display for Route<'_> { Ok(()) } + Route::EndPoll { + channel_id, + message_id, + } => { + f.write_str("channels/")?; + Display::fmt(channel_id, f)?; + f.write_str("/polls/")?; + Display::fmt(message_id, f)?; + + f.write_str("/expire") + } Route::ExecuteWebhook { thread_id, token, diff --git a/twilight-model/src/application/interaction/resolved.rs b/twilight-model/src/application/interaction/resolved.rs index 291e01ea2de..698be60ca0c 100644 --- a/twilight-model/src/application/interaction/resolved.rs +++ b/twilight-model/src/application/interaction/resolved.rs @@ -224,6 +224,7 @@ mod tests { mention_roles: Vec::new(), mentions: Vec::new(), pinned: false, + poll: None, reactions: Vec::new(), reference: None, role_subscription_data: None, diff --git a/twilight-model/src/channel/message/mod.rs b/twilight-model/src/channel/message/mod.rs index c5c377b465b..bb8d22150c9 100644 --- a/twilight-model/src/channel/message/mod.rs +++ b/twilight-model/src/channel/message/mod.rs @@ -44,6 +44,7 @@ use crate::{ }, Id, }, + poll::Poll, user::User, util::Timestamp, }; @@ -161,6 +162,9 @@ pub struct Message { pub mentions: Vec, /// Whether the message is pinned. pub pinned: bool, + /// The poll associated with the message. + #[serde(skip_serializing_if = "Option::is_none")] + pub poll: Option, /// List of reactions to the message. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub reactions: Vec, @@ -274,6 +278,7 @@ mod tests { mention_roles: Vec::new(), mentions: Vec::new(), pinned: false, + poll: None, reactions: Vec::new(), reference: None, role_subscription_data: None, @@ -485,6 +490,7 @@ mod tests { mention_roles: Vec::new(), mentions: Vec::new(), pinned: false, + poll: None, reactions: vec![Reaction { burst_colors: Vec::new(), count: 7, diff --git a/twilight-model/src/gateway/event/dispatch.rs b/twilight-model/src/gateway/event/dispatch.rs index 8faf9de8d42..cc6b0971525 100644 --- a/twilight-model/src/gateway/event/dispatch.rs +++ b/twilight-model/src/gateway/event/dispatch.rs @@ -53,6 +53,8 @@ pub enum DispatchEvent { MessageCreate(Box), MessageDelete(MessageDelete), MessageDeleteBulk(MessageDeleteBulk), + MessagePollVoteAdd(MessagePollVoteAdd), + MessagePollVoteRemove(MessagePollVoteRemove), MessageUpdate(Box), PresenceUpdate(Box), ReactionAdd(Box), @@ -124,6 +126,8 @@ impl DispatchEvent { Self::MessageCreate(_) => EventType::MessageCreate, Self::MessageDelete(_) => EventType::MessageDelete, Self::MessageDeleteBulk(_) => EventType::MessageDeleteBulk, + Self::MessagePollVoteAdd(_) => EventType::MessagePollVoteAdd, + Self::MessagePollVoteRemove(_) => EventType::MessagePollVoteRemove, Self::MessageUpdate(_) => EventType::MessageUpdate, Self::PresenceUpdate(_) => EventType::PresenceUpdate, Self::ReactionAdd(_) => EventType::ReactionAdd, @@ -194,6 +198,8 @@ impl TryFrom for DispatchEvent { Event::MessageCreate(v) => Self::MessageCreate(v), Event::MessageDelete(v) => Self::MessageDelete(v), Event::MessageDeleteBulk(v) => Self::MessageDeleteBulk(v), + Event::MessagePollVoteAdd(v) => Self::MessagePollVoteAdd(v), + Event::MessagePollVoteRemove(v) => Self::MessagePollVoteRemove(v), Event::MessageUpdate(v) => Self::MessageUpdate(v), Event::PresenceUpdate(v) => Self::PresenceUpdate(v), Event::ReactionAdd(v) => Self::ReactionAdd(v), @@ -371,6 +377,12 @@ impl<'de, 'a> DeserializeSeed<'de> for DispatchEventWithTypeDeserializer<'a> { "MESSAGE_REACTION_REMOVE_ALL" => { DispatchEvent::ReactionRemoveAll(ReactionRemoveAll::deserialize(deserializer)?) } + "MESSAGE_POLL_VOTE_ADD" => { + DispatchEvent::MessagePollVoteAdd(MessagePollVoteAdd::deserialize(deserializer)?) + } + "MESSAGE_POLL_VOTE_REMOVE" => DispatchEvent::MessagePollVoteRemove( + MessagePollVoteRemove::deserialize(deserializer)?, + ), "MESSAGE_UPDATE" => { DispatchEvent::MessageUpdate(Box::new(MessageUpdate::deserialize(deserializer)?)) } diff --git a/twilight-model/src/gateway/event/kind.rs b/twilight-model/src/gateway/event/kind.rs index d43dd68db97..5dfeeb77f10 100644 --- a/twilight-model/src/gateway/event/kind.rs +++ b/twilight-model/src/gateway/event/kind.rs @@ -56,6 +56,8 @@ pub enum EventType { MessageCreate, MessageDelete, MessageDeleteBulk, + MessagePollVoteAdd, + MessagePollVoteRemove, MessageUpdate, PresenceUpdate, #[serde(rename = "MESSAGE_REACTION_ADD")] @@ -134,6 +136,8 @@ impl EventType { Self::MessageDelete => Some("MESSAGE_DELETE"), Self::MessageDeleteBulk => Some("MESSAGE_DELETE_BULK"), Self::MessageUpdate => Some("MESSAGE_UPDATE"), + Self::MessagePollVoteAdd => Some("MESSAGE_POLL_VOTE_ADD"), + Self::MessagePollVoteRemove => Some("MESSAGE_POLL_VOTE_REMOVE"), Self::PresenceUpdate => Some("PRESENCE_UPDATE"), Self::ReactionAdd => Some("MESSAGE_REACTION_ADD"), Self::ReactionRemove => Some("MESSAGE_REACTION_REMOVE"), @@ -211,6 +215,8 @@ impl<'a> TryFrom<&'a str> for EventType { "MESSAGE_DELETE" => Ok(Self::MessageDelete), "MESSAGE_DELETE_BULK" => Ok(Self::MessageDeleteBulk), "MESSAGE_UPDATE" => Ok(Self::MessageUpdate), + "MESSAGE_POLL_VOTE_ADD" => Ok(Self::MessagePollVoteAdd), + "MESSAGE_POLL_VOTE_REMOVE" => Ok(Self::MessagePollVoteRemove), "PRESENCE_UPDATE" => Ok(Self::PresenceUpdate), "MESSAGE_REACTION_ADD" => Ok(Self::ReactionAdd), "MESSAGE_REACTION_REMOVE" => Ok(Self::ReactionRemove), @@ -340,6 +346,8 @@ mod tests { assert_variant(EventType::MessageDelete, "MESSAGE_DELETE"); assert_variant(EventType::MessageDeleteBulk, "MESSAGE_DELETE_BULK"); assert_variant(EventType::MessageUpdate, "MESSAGE_UPDATE"); + assert_variant(EventType::MessagePollVoteAdd, "MESSAGE_POLL_VOTE_ADD"); + assert_variant(EventType::MessagePollVoteRemove, "MESSAGE_POLL_VOTE_REMOVE"); assert_variant(EventType::PresenceUpdate, "PRESENCE_UPDATE"); assert_variant(EventType::ReactionAdd, "MESSAGE_REACTION_ADD"); assert_variant(EventType::ReactionRemove, "MESSAGE_REACTION_REMOVE"); diff --git a/twilight-model/src/gateway/event/mod.rs b/twilight-model/src/gateway/event/mod.rs index 6edc296d500..b57944df45f 100644 --- a/twilight-model/src/gateway/event/mod.rs +++ b/twilight-model/src/gateway/event/mod.rs @@ -121,6 +121,10 @@ pub enum Event { MessageDelete(MessageDelete), /// Multiple messages were deleted in a channel. MessageDeleteBulk(MessageDeleteBulk), + /// A vote was added to a poll. + MessagePollVoteAdd(MessagePollVoteAdd), + /// A vote was removed from a poll. + MessagePollVoteRemove(MessagePollVoteRemove), /// A message was updated in a channel. MessageUpdate(Box), /// A user's active presence (such as game or online status) was updated. @@ -218,6 +222,8 @@ impl Event { Event::MessageDelete(e) => e.guild_id, Event::MessageDeleteBulk(e) => e.guild_id, Event::MessageUpdate(e) => e.guild_id, + Event::MessagePollVoteAdd(e) => e.guild_id, + Event::MessagePollVoteRemove(e) => e.guild_id, Event::PresenceUpdate(e) => Some(e.0.guild_id), Event::ReactionAdd(e) => e.0.guild_id, Event::ReactionRemove(e) => e.0.guild_id, @@ -302,6 +308,8 @@ impl Event { Self::MessageCreate(_) => EventType::MessageCreate, Self::MessageDelete(_) => EventType::MessageDelete, Self::MessageDeleteBulk(_) => EventType::MessageDeleteBulk, + Self::MessagePollVoteAdd(_) => EventType::MessagePollVoteAdd, + Self::MessagePollVoteRemove(_) => EventType::MessagePollVoteRemove, Self::MessageUpdate(_) => EventType::MessageUpdate, Self::PresenceUpdate(_) => EventType::PresenceUpdate, Self::ReactionAdd(_) => EventType::ReactionAdd, @@ -381,6 +389,8 @@ impl From for Event { DispatchEvent::MessageCreate(v) => Self::MessageCreate(v), DispatchEvent::MessageDelete(v) => Self::MessageDelete(v), DispatchEvent::MessageDeleteBulk(v) => Self::MessageDeleteBulk(v), + DispatchEvent::MessagePollVoteAdd(v) => Self::MessagePollVoteAdd(v), + DispatchEvent::MessagePollVoteRemove(v) => Self::MessagePollVoteRemove(v), DispatchEvent::MessageUpdate(v) => Self::MessageUpdate(v), DispatchEvent::PresenceUpdate(v) => Self::PresenceUpdate(v), DispatchEvent::ReactionAdd(v) => Self::ReactionAdd(v), @@ -540,4 +550,6 @@ mod tests { const_assert!(mem::size_of::() <= EVENT_THRESHOLD); const_assert!(mem::size_of::() <= EVENT_THRESHOLD); const_assert!(mem::size_of::() <= EVENT_THRESHOLD); + const_assert!(mem::size_of::() <= EVENT_THRESHOLD); + const_assert!(mem::size_of::() <= EVENT_THRESHOLD); } diff --git a/twilight-model/src/gateway/intents.rs b/twilight-model/src/gateway/intents.rs index 5f8cf18ff9b..640d3e19d1d 100644 --- a/twilight-model/src/gateway/intents.rs +++ b/twilight-model/src/gateway/intents.rs @@ -262,6 +262,24 @@ bitflags! { /// /// [`AUTO_MODERATION_ACTION_EXECUTION`]: super::event::Event::AutoModerationActionExecution const AUTO_MODERATION_EXECUTION = 1 << 21; + /// Guild polls intent. + /// + /// Event(s) received: + /// - [`MESSAGE_POLL_VOTE_ADD`] + /// - [`MESSAGE_POLL_VOTE_REMOVE`] + /// + /// [`MESSAGE_POLL_VOTE_ADD`]: super::event::Event::MessagePollVoteAdd + /// [`MESSAGE_POLL_VOTE_REMOVE`]: super::event::Event::MessagePollVoteRemove + const GUILD_MESSAGE_POLLS = 1 << 24; + /// Direct message polls intent. + /// + /// Event(s) received: + /// - [`MESSAGE_POLL_VOTE_ADD`] + /// - [`MESSAGE_POLL_VOTE_REMOVE`] + /// + /// [`MESSAGE_POLL_VOTE_ADD`]: super::event::Event::MessagePollVoteAdd + /// [`MESSAGE_POLL_VOTE_REMOVE`]: super::event::Event::MessagePollVoteRemove + const DIRECT_MESSAGE_POLLS = 1 << 25; } } @@ -342,6 +360,8 @@ mod tests { const_assert_eq!(Intents::GUILD_SCHEDULED_EVENTS.bits(), 1 << 16); const_assert_eq!(Intents::AUTO_MODERATION_CONFIGURATION.bits(), 1 << 20); const_assert_eq!(Intents::AUTO_MODERATION_EXECUTION.bits(), 1 << 21); + const_assert_eq!(Intents::GUILD_MESSAGE_POLLS.bits(), 1 << 24); + const_assert_eq!(Intents::DIRECT_MESSAGE_POLLS.bits(), 1 << 25); #[test] fn serde() { diff --git a/twilight-model/src/gateway/payload/incoming/message_poll_vote_add.rs b/twilight-model/src/gateway/payload/incoming/message_poll_vote_add.rs new file mode 100644 index 00000000000..d33da60f269 --- /dev/null +++ b/twilight-model/src/gateway/payload/incoming/message_poll_vote_add.rs @@ -0,0 +1,65 @@ +use crate::id::{ + marker::{ChannelMarker, GuildMarker, MessageMarker, UserMarker}, + Id, +}; +use serde::{Deserialize, Serialize}; + +/// Sent when a user votes on a poll. If the poll allows multiple selection, +/// one event will be sent per answer. +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct MessagePollVoteAdd { + /// ID of the answer. + pub answer_id: u8, + /// ID of the channel. + pub channel_id: Id, + /// ID of the guild. + #[serde(skip_serializing_if = "Option::is_none")] + pub guild_id: Option>, + /// ID of the message. + pub message_id: Id, + /// ID of the user. + pub user_id: Id, +} + +#[cfg(test)] +mod tests { + use super::{Id, MessagePollVoteAdd}; + use serde_test::Token; + + #[test] + fn test_message_poll_vote_add() { + let value = MessagePollVoteAdd { + answer_id: 1, + channel_id: Id::new(2), + guild_id: Some(Id::new(3)), + message_id: Id::new(4), + user_id: Id::new(5), + }; + + serde_test::assert_tokens( + &value, + &[ + Token::Struct { + name: "MessagePollVoteAdd", + len: 5, + }, + Token::Str("answer_id"), + Token::U8(1), + Token::Str("channel_id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("2"), + Token::Str("guild_id"), + Token::Some, + Token::NewtypeStruct { name: "Id" }, + Token::Str("3"), + Token::Str("message_id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("4"), + Token::Str("user_id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("5"), + Token::StructEnd, + ], + ); + } +} diff --git a/twilight-model/src/gateway/payload/incoming/message_poll_vote_remove.rs b/twilight-model/src/gateway/payload/incoming/message_poll_vote_remove.rs new file mode 100644 index 00000000000..d92ff3f3de9 --- /dev/null +++ b/twilight-model/src/gateway/payload/incoming/message_poll_vote_remove.rs @@ -0,0 +1,65 @@ +use crate::id::{ + marker::{ChannelMarker, GuildMarker, MessageMarker, UserMarker}, + Id, +}; +use serde::{Deserialize, Serialize}; + +/// Sent when a user removes a vote on a poll. If the poll allows multiple selection, +/// one event will be sent per answer. +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct MessagePollVoteRemove { + /// ID of the answer. + pub answer_id: u8, + /// ID of the channel. + pub channel_id: Id, + /// ID of the guild. + #[serde(skip_serializing_if = "Option::is_none")] + pub guild_id: Option>, + /// ID of the message. + pub message_id: Id, + /// ID of the user. + pub user_id: Id, +} + +#[cfg(test)] +mod tests { + use super::{Id, MessagePollVoteRemove}; + use serde_test::Token; + + #[test] + fn test_message_poll_vote_remove() { + let value = MessagePollVoteRemove { + answer_id: 1, + channel_id: Id::new(2), + guild_id: Some(Id::new(3)), + message_id: Id::new(4), + user_id: Id::new(5), + }; + + serde_test::assert_tokens( + &value, + &[ + Token::Struct { + name: "MessagePollVoteRemove", + len: 5, + }, + Token::Str("answer_id"), + Token::U8(1), + Token::Str("channel_id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("2"), + Token::Str("guild_id"), + Token::Some, + Token::NewtypeStruct { name: "Id" }, + Token::Str("3"), + Token::Str("message_id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("4"), + Token::Str("user_id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("5"), + Token::StructEnd, + ], + ); + } +} diff --git a/twilight-model/src/gateway/payload/incoming/mod.rs b/twilight-model/src/gateway/payload/incoming/mod.rs index b99b4cf1d00..d2dfac3274a 100644 --- a/twilight-model/src/gateway/payload/incoming/mod.rs +++ b/twilight-model/src/gateway/payload/incoming/mod.rs @@ -53,6 +53,8 @@ mod member_update; mod message_create; mod message_delete; mod message_delete_bulk; +mod message_poll_vote_add; +mod message_poll_vote_remove; mod message_update; mod presence_update; mod reaction_add; @@ -101,7 +103,8 @@ pub use self::{ invite_create::InviteCreate, invite_delete::InviteDelete, member_add::MemberAdd, member_chunk::MemberChunk, member_remove::MemberRemove, member_update::MemberUpdate, message_create::MessageCreate, message_delete::MessageDelete, - message_delete_bulk::MessageDeleteBulk, message_update::MessageUpdate, + message_delete_bulk::MessageDeleteBulk, message_poll_vote_add::MessagePollVoteAdd, + message_poll_vote_remove::MessagePollVoteRemove, message_update::MessageUpdate, presence_update::PresenceUpdate, reaction_add::ReactionAdd, reaction_remove::ReactionRemove, reaction_remove_all::ReactionRemoveAll, reaction_remove_emoji::ReactionRemoveEmoji, ready::Ready, role_create::RoleCreate, role_delete::RoleDelete, role_update::RoleUpdate, diff --git a/twilight-model/src/guild/permissions.rs b/twilight-model/src/guild/permissions.rs index 0f73dc6fa25..d449ee62c45 100644 --- a/twilight-model/src/guild/permissions.rs +++ b/twilight-model/src/guild/permissions.rs @@ -77,6 +77,8 @@ bitflags! { const USE_EXTERNAL_SOUNDS = 1 << 45; /// Allows sending voice messages const SEND_VOICE_MESSAGES = 1 << 46; + /// Allows sending polls. + const SEND_POLLS = 1 << 49; /// Allows user-installed apps to send public responses. When disabled, users will still /// be allowed to use their apps but the responses will be ephemeral. This only applies to /// apps not also installed to the server. @@ -207,6 +209,8 @@ mod tests { ); const_assert_eq!(Permissions::USE_SOUNDBOARD.bits(), 1 << 42); const_assert_eq!(Permissions::USE_EXTERNAL_SOUNDS.bits(), 1 << 45); + const_assert_eq!(Permissions::SEND_VOICE_MESSAGES.bits(), 1 << 46); + const_assert_eq!(Permissions::SEND_POLLS.bits(), 1 << 49); const_assert_eq!(Permissions::USE_EXTERNAL_APPS.bits(), 1 << 50); #[test] diff --git a/twilight-model/src/lib.rs b/twilight-model/src/lib.rs index 179a4ac8c57..be628aa556b 100644 --- a/twilight-model/src/lib.rs +++ b/twilight-model/src/lib.rs @@ -13,6 +13,7 @@ pub mod guild; pub mod http; pub mod id; pub mod oauth; +pub mod poll; pub mod user; pub mod util; pub mod voice; diff --git a/twilight-model/src/poll/answer.rs b/twilight-model/src/poll/answer.rs new file mode 100644 index 00000000000..a88d22e7340 --- /dev/null +++ b/twilight-model/src/poll/answer.rs @@ -0,0 +1,75 @@ +use super::media::PollMedia; +use serde::{Deserialize, Serialize}; + +/// A poll answer. +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct PollAnswer { + /// The ID of the answer. + /// + /// This is unique within the poll. And increases + /// sequentially with each answer. Currently, only + /// 1-10 answers are allowed. + pub answer_id: u8, + /// The data of the answer. + pub poll_media: PollMedia, +} + +#[cfg(test)] +mod tests { + use super::{PollAnswer, PollMedia}; + use crate::{id::Id, poll::media::PartialPollMediaEmoji}; + use serde_test::Token; + + #[test] + fn poll_answer() { + let value = PollAnswer { + answer_id: 1, + poll_media: PollMedia { + emoji: Some(PartialPollMediaEmoji { + animated: true, + id: Some(Id::new(1)), + name: Some("a".to_owned()), + }), + text: Some("b".to_owned()), + }, + }; + + serde_test::assert_tokens( + &value, + &[ + Token::Struct { + name: "PollAnswer", + len: 2, + }, + Token::Str("answer_id"), + Token::U8(1), + Token::Str("poll_media"), + Token::Struct { + name: "PollMedia", + len: 2, + }, + Token::Str("emoji"), + Token::Some, + Token::Struct { + name: "PartialPollMediaEmoji", + len: 3, + }, + Token::Str("animated"), + Token::Bool(true), + Token::Str("id"), + Token::Some, + Token::NewtypeStruct { name: "Id" }, + Token::Str("1"), + Token::Str("name"), + Token::Some, + Token::Str("a"), + Token::StructEnd, + Token::Str("text"), + Token::Some, + Token::Str("b"), + Token::StructEnd, + Token::StructEnd, + ], + ); + } +} diff --git a/twilight-model/src/poll/answer_count.rs b/twilight-model/src/poll/answer_count.rs new file mode 100644 index 00000000000..1c00cfdfca5 --- /dev/null +++ b/twilight-model/src/poll/answer_count.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct AnswerCount { + /// The answer ID. + pub id: u8, + /// The number of votes for this answer. + pub count: u64, + /// Whether the current user voted for this answer. + pub me_voted: bool, +} + +#[cfg(test)] +mod tests { + use super::AnswerCount; + use serde_test::Token; + + #[test] + fn answer_count() { + let value = AnswerCount { + id: 1, + count: 2, + me_voted: true, + }; + + serde_test::assert_tokens( + &value, + &[ + Token::Struct { + name: "AnswerCount", + len: 3, + }, + Token::Str("id"), + Token::U8(1), + Token::Str("count"), + Token::U64(2), + Token::Str("me_voted"), + Token::Bool(true), + Token::StructEnd, + ], + ); + } +} diff --git a/twilight-model/src/poll/layout_type.rs b/twilight-model/src/poll/layout_type.rs new file mode 100644 index 00000000000..b1f0d77575d --- /dev/null +++ b/twilight-model/src/poll/layout_type.rs @@ -0,0 +1,57 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(from = "u8", into = "u8")] +/// Layout of a poll. +pub enum PollLayoutType { + /// Default layout. + Default, + /// Unknown layout. + Unknown(u8), +} + +impl From for PollLayoutType { + fn from(value: u8) -> Self { + match value { + 1 => PollLayoutType::Default, + unknown => PollLayoutType::Unknown(unknown), + } + } +} + +impl From for u8 { + fn from(value: PollLayoutType) -> Self { + match value { + PollLayoutType::Default => 1, + PollLayoutType::Unknown(unknown) => unknown, + } + } +} + +impl PollLayoutType { + pub const fn name(&self) -> &str { + match self { + PollLayoutType::Default => "Default", + PollLayoutType::Unknown(_) => "Unknown", + } + } +} + +#[cfg(test)] +mod tests { + use super::PollLayoutType; + use serde_test::Token; + + #[test] + fn variants() { + serde_test::assert_tokens(&PollLayoutType::Default, &[Token::U8(1)]); + serde_test::assert_tokens(&PollLayoutType::Unknown(2), &[Token::U8(2)]); + } + + #[test] + fn names() { + assert_eq!(PollLayoutType::Default.name(), "Default"); + assert_eq!(PollLayoutType::Unknown(2).name(), "Unknown"); + } +} diff --git a/twilight-model/src/poll/media.rs b/twilight-model/src/poll/media.rs new file mode 100644 index 00000000000..baa0675ab7f --- /dev/null +++ b/twilight-model/src/poll/media.rs @@ -0,0 +1,73 @@ +use crate::id::{marker::EmojiMarker, Id}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct PollMedia { + /// The emoji of the field. + /// + /// When creating a poll answer with an emoji, one only + /// needs to send either the id (custom emoji) or name + /// (default emoji) as the only field. + #[serde(skip_serializing_if = "Option::is_none")] + pub emoji: Option, + /// The text of the field. + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct PartialPollMediaEmoji { + #[serde(default)] + pub animated: bool, + pub id: Option>, + pub name: Option, +} + +#[cfg(test)] +mod tests { + use super::{PartialPollMediaEmoji, PollMedia}; + use crate::id::Id; + use serde_test::Token; + + #[test] + fn poll_media() { + let value = PollMedia { + emoji: Some(PartialPollMediaEmoji { + animated: true, + id: Some(Id::new(1)), + name: Some("a".to_owned()), + }), + text: Some("b".to_owned()), + }; + + serde_test::assert_tokens( + &value, + &[ + Token::Struct { + name: "PollMedia", + len: 2, + }, + Token::Str("emoji"), + Token::Some, + Token::Struct { + name: "PartialPollMediaEmoji", + len: 3, + }, + Token::Str("animated"), + Token::Bool(true), + Token::Str("id"), + Token::Some, + Token::NewtypeStruct { name: "Id" }, + Token::Str("1"), + Token::Str("name"), + Token::Some, + Token::Str("a"), + Token::StructEnd, + Token::Str("text"), + Token::Some, + Token::Str("b"), + Token::StructEnd, + ], + ); + } +} diff --git a/twilight-model/src/poll/mod.rs b/twilight-model/src/poll/mod.rs new file mode 100644 index 00000000000..402f9c9f976 --- /dev/null +++ b/twilight-model/src/poll/mod.rs @@ -0,0 +1,220 @@ +mod answer; +mod answer_count; +mod layout_type; +mod media; +mod results; + +use crate::util::Timestamp; +use serde::{Deserialize, Serialize}; + +pub use self::{ + answer::PollAnswer, + answer_count::AnswerCount, + layout_type::PollLayoutType, + media::{PartialPollMediaEmoji, PollMedia}, + results::PollResults, +}; + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct Poll { + /// Each of the answers available in the poll. + pub answers: Vec, + /// Whether a user can select multiple answers. + pub allow_multiselect: bool, + /// The time when the poll ends. + pub expiry: Option, + /// The layout type of the poll. + pub layout_type: PollLayoutType, + /// The question of the poll. Only text is supported. + pub question: PollMedia, + /// The results of the poll. + #[serde(skip_serializing_if = "Option::is_none")] + pub results: Option, +} + +#[cfg(test)] +mod tests { + use super::{AnswerCount, Poll, PollAnswer, PollLayoutType, PollMedia, PollResults}; + use crate::{id::Id, poll::media::PartialPollMediaEmoji}; + use serde_test::Token; + + #[test] + #[allow(clippy::too_many_lines)] + fn poll() { + let value = Poll { + answers: vec![ + PollAnswer { + answer_id: 1, + poll_media: PollMedia { + emoji: Some(PartialPollMediaEmoji { + animated: true, + id: Some(Id::new(1)), + name: Some("a".to_owned()), + }), + text: Some("b".to_owned()), + }, + }, + PollAnswer { + answer_id: 2, + poll_media: PollMedia { + emoji: Some(PartialPollMediaEmoji { + animated: false, + id: Some(Id::new(3)), + name: Some("c".to_owned()), + }), + text: Some("d".to_owned()), + }, + }, + ], + allow_multiselect: true, + expiry: None, + layout_type: PollLayoutType::Default, + question: PollMedia { + emoji: None, + text: Some("e".to_owned()), + }, + results: Some(PollResults { + answer_counts: vec![ + AnswerCount { + id: 1, + count: 2, + me_voted: true, + }, + AnswerCount { + id: 3, + count: 4, + me_voted: false, + }, + ], + is_finalized: true, + }), + }; + + serde_test::assert_tokens( + &value, + &[ + Token::Struct { + name: "Poll", + len: 6, + }, + Token::Str("answers"), + Token::Seq { len: Some(2) }, + Token::Struct { + name: "PollAnswer", + len: 2, + }, + Token::Str("answer_id"), + Token::U8(1), + Token::Str("poll_media"), + Token::Struct { + name: "PollMedia", + len: 2, + }, + Token::Str("emoji"), + Token::Some, + Token::Struct { + name: "PartialPollMediaEmoji", + len: 3, + }, + Token::Str("animated"), + Token::Bool(true), + Token::Str("id"), + Token::Some, + Token::NewtypeStruct { name: "Id" }, + Token::Str("1"), + Token::Str("name"), + Token::Some, + Token::Str("a"), + Token::StructEnd, + Token::Str("text"), + Token::Some, + Token::Str("b"), + Token::StructEnd, + Token::StructEnd, + Token::Struct { + name: "PollAnswer", + len: 2, + }, + Token::Str("answer_id"), + Token::U8(2), + Token::Str("poll_media"), + Token::Struct { + name: "PollMedia", + len: 2, + }, + Token::Str("emoji"), + Token::Some, + Token::Struct { + name: "PartialPollMediaEmoji", + len: 3, + }, + Token::Str("animated"), + Token::Bool(false), + Token::Str("id"), + Token::Some, + Token::NewtypeStruct { name: "Id" }, + Token::Str("3"), + Token::Str("name"), + Token::Some, + Token::Str("c"), + Token::StructEnd, + Token::Str("text"), + Token::Some, + Token::Str("d"), + Token::StructEnd, + Token::StructEnd, + Token::SeqEnd, + Token::Str("allow_multiselect"), + Token::Bool(true), + Token::Str("expiry"), + Token::None, + Token::Str("layout_type"), + Token::U8(1), + Token::Str("question"), + Token::Struct { + name: "PollMedia", + len: 1, + }, + Token::Str("text"), + Token::Some, + Token::Str("e"), + Token::StructEnd, + Token::Str("results"), + Token::Some, + Token::Struct { + name: "PollResults", + len: 2, + }, + Token::Str("answer_counts"), + Token::Seq { len: Some(2) }, + Token::Struct { + name: "AnswerCount", + len: 3, + }, + Token::Str("id"), + Token::U8(1), + Token::Str("count"), + Token::U64(2), + Token::Str("me_voted"), + Token::Bool(true), + Token::StructEnd, + Token::Struct { + name: "AnswerCount", + len: 3, + }, + Token::Str("id"), + Token::U8(3), + Token::Str("count"), + Token::U64(4), + Token::Str("me_voted"), + Token::Bool(false), + Token::StructEnd, + Token::SeqEnd, + Token::Str("is_finalized"), + Token::Bool(true), + Token::StructEnd, + Token::StructEnd, + ], + ); + } +} diff --git a/twilight-model/src/poll/results.rs b/twilight-model/src/poll/results.rs new file mode 100644 index 00000000000..192c3051432 --- /dev/null +++ b/twilight-model/src/poll/results.rs @@ -0,0 +1,74 @@ +use super::answer_count::AnswerCount; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +/// This contains the number of votes for each answer. +pub struct PollResults { + /// The counts for each answer. + pub answer_counts: Vec, + /// Whether the votes have been precisely counted. + pub is_finalized: bool, +} + +#[cfg(test)] +mod tests { + use super::{AnswerCount, PollResults}; + use serde_test::Token; + + #[test] + fn poll_results() { + let value = PollResults { + answer_counts: vec![ + AnswerCount { + id: 1, + count: 2, + me_voted: true, + }, + AnswerCount { + id: 3, + count: 4, + me_voted: false, + }, + ], + is_finalized: true, + }; + + serde_test::assert_tokens( + &value, + &[ + Token::Struct { + name: "PollResults", + len: 2, + }, + Token::Str("answer_counts"), + Token::Seq { len: Some(2) }, + Token::Struct { + name: "AnswerCount", + len: 3, + }, + Token::Str("id"), + Token::U8(1), + Token::Str("count"), + Token::U64(2), + Token::Str("me_voted"), + Token::Bool(true), + Token::StructEnd, + Token::Struct { + name: "AnswerCount", + len: 3, + }, + Token::Str("id"), + Token::U8(3), + Token::Str("count"), + Token::U64(4), + Token::Str("me_voted"), + Token::Bool(false), + Token::StructEnd, + Token::SeqEnd, + Token::Str("is_finalized"), + Token::Bool(true), + Token::StructEnd, + ], + ); + } +} diff --git a/twilight-standby/src/lib.rs b/twilight-standby/src/lib.rs index 73c77935412..6643632f6c4 100644 --- a/twilight-standby/src/lib.rs +++ b/twilight-standby/src/lib.rs @@ -1124,6 +1124,7 @@ mod tests { mention_roles: Vec::new(), mentions: Vec::new(), pinned: false, + poll: None, reactions: Vec::new(), reference: None, referenced_message: None,