diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 2f696d4a58f..1e9defb48ff 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -3,7 +3,7 @@ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, +identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. diff --git a/twilight-cache-inmemory/src/event/interaction.rs b/twilight-cache-inmemory/src/event/interaction.rs index c4917d8e3e7..2e7dfbd3844 100644 --- a/twilight-cache-inmemory/src/event/interaction.rs +++ b/twilight-cache-inmemory/src/event/interaction.rs @@ -169,6 +169,7 @@ mod tests { accent_color: None, avatar: Some(avatar3), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -184,6 +185,7 @@ mod tests { system: None, verified: None, }, + call: None, channel_id: Id::new(2), components: Vec::new(), content: "ping".to_owned(), @@ -213,6 +215,7 @@ mod tests { mentions: Vec::new(), message_snapshots: Vec::new(), pinned: false, + poll: None, reactions: Vec::new(), reference: None, role_subscription_data: None, @@ -251,6 +254,7 @@ mod tests { accent_color: None, avatar: Some(avatar2), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 5678, @@ -270,6 +274,7 @@ mod tests { }), target_id: None, }))), + entitlements: Vec::new(), guild_id: Some(Id::new(3)), guild_locale: None, id: Id::new(4), @@ -290,6 +295,7 @@ mod tests { accent_color: None, avatar: Some(avatar3), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1234, diff --git a/twilight-cache-inmemory/src/event/message.rs b/twilight-cache-inmemory/src/event/message.rs index 74f7f125ab1..b136cd9ba12 100644 --- a/twilight-cache-inmemory/src/event/message.rs +++ b/twilight-cache-inmemory/src/event/message.rs @@ -121,6 +121,7 @@ mod tests { accent_color: None, avatar: Some(avatar), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -136,6 +137,7 @@ mod tests { system: None, verified: None, }, + call: None, channel_id: Id::new(2), components: Vec::new(), content: "ping".to_owned(), @@ -165,6 +167,7 @@ mod tests { mentions: Vec::new(), message_snapshots: Vec::new(), pinned: false, + poll: None, reactions: Vec::new(), reference: None, role_subscription_data: None, diff --git a/twilight-cache-inmemory/src/event/reaction.rs b/twilight-cache-inmemory/src/event/reaction.rs index 1393c666917..1d456b13c75 100644 --- a/twilight-cache-inmemory/src/event/reaction.rs +++ b/twilight-cache-inmemory/src/event/reaction.rs @@ -4,7 +4,7 @@ use crate::{ CacheableModels, InMemoryCache, UpdateCache, }; use twilight_model::{ - channel::message::{Reaction, ReactionCountDetails, ReactionType}, + channel::message::{EmojiReactionType, Reaction, ReactionCountDetails}, gateway::payload::incoming::{ ReactionAdd, ReactionRemove, ReactionRemoveAll, ReactionRemoveEmoji, }, @@ -124,14 +124,16 @@ impl UpdateCache for ReactionRemoveEm } } -fn reactions_eq(a: &ReactionType, b: &ReactionType) -> bool { +fn reactions_eq(a: &EmojiReactionType, b: &EmojiReactionType) -> bool { match (a, b) { - (ReactionType::Custom { id: id_a, .. }, ReactionType::Custom { id: id_b, .. }) => { - id_a == id_b - } - (ReactionType::Unicode { name: name_a }, ReactionType::Unicode { name: name_b }) => { - name_a == name_b - } + ( + EmojiReactionType::Custom { id: id_a, .. }, + EmojiReactionType::Custom { id: id_b, .. }, + ) => id_a == id_b, + ( + EmojiReactionType::Unicode { name: name_a }, + EmojiReactionType::Unicode { name: name_b }, + ) => name_a == name_b, _ => false, } } @@ -141,7 +143,7 @@ mod tests { use super::reactions_eq; use crate::{model::CachedMessage, test}; use twilight_model::{ - channel::message::{Reaction, ReactionType}, + channel::message::{EmojiReactionType, Reaction}, gateway::{ payload::incoming::{ReactionRemove, ReactionRemoveAll, ReactionRemoveEmoji}, GatewayReaction, @@ -153,7 +155,7 @@ mod tests { msg.reactions.iter().find(|&r| { reactions_eq( &r.emoji, - &ReactionType::Custom { + &EmojiReactionType::Custom { animated: false, id: Id::new(6), name: None, @@ -172,11 +174,11 @@ mod tests { let world_react = msg .reactions .iter() - .find(|&r| matches!(&r.emoji, ReactionType::Unicode {name} if name == "πŸ—ΊοΈ")); + .find(|&r| matches!(&r.emoji, EmojiReactionType::Unicode {name} if name == "πŸ—ΊοΈ")); let smiley_react = msg .reactions .iter() - .find(|&r| matches!(&r.emoji, ReactionType::Unicode {name} if name == "πŸ˜€")); + .find(|&r| matches!(&r.emoji, EmojiReactionType::Unicode {name} if name == "πŸ˜€")); let custom_react = find_custom_react(&msg); assert!(world_react.is_some()); @@ -191,8 +193,10 @@ mod tests { fn reaction_remove() { let cache = test::cache_with_message_and_reactions(); cache.update(&ReactionRemove(GatewayReaction { + burst: false, + burst_colors: Vec::new(), channel_id: Id::new(2), - emoji: ReactionType::Unicode { + emoji: EmojiReactionType::Unicode { name: "πŸ˜€".to_owned(), }, guild_id: Some(Id::new(1)), @@ -202,8 +206,10 @@ mod tests { user_id: Id::new(5), })); cache.update(&ReactionRemove(GatewayReaction { + burst: false, + burst_colors: Vec::new(), channel_id: Id::new(2), - emoji: ReactionType::Custom { + emoji: EmojiReactionType::Custom { animated: false, id: Id::new(6), name: None, @@ -222,11 +228,11 @@ mod tests { let world_react = msg .reactions .iter() - .find(|&r| matches!(&r.emoji, ReactionType::Unicode {name} if name == "πŸ—ΊοΈ")); + .find(|&r| matches!(&r.emoji, EmojiReactionType::Unicode {name} if name == "πŸ—ΊοΈ")); let smiley_react = msg .reactions .iter() - .find(|&r| matches!(&r.emoji, ReactionType::Unicode {name} if name == "πŸ˜€")); + .find(|&r| matches!(&r.emoji, EmojiReactionType::Unicode {name} if name == "πŸ˜€")); let custom_react = find_custom_react(&msg); assert!(world_react.is_some()); @@ -255,7 +261,7 @@ mod tests { let cache = test::cache_with_message_and_reactions(); cache.update(&ReactionRemoveEmoji { channel_id: Id::new(2), - emoji: ReactionType::Unicode { + emoji: EmojiReactionType::Unicode { name: "πŸ˜€".to_owned(), }, guild_id: Id::new(1), @@ -263,7 +269,7 @@ mod tests { }); cache.update(&ReactionRemoveEmoji { channel_id: Id::new(2), - emoji: ReactionType::Custom { + emoji: EmojiReactionType::Custom { animated: false, id: Id::new(6), name: None, @@ -279,11 +285,11 @@ mod tests { let world_react = msg .reactions .iter() - .find(|&r| matches!(&r.emoji, ReactionType::Unicode {name} if name == "πŸ—ΊοΈ")); + .find(|&r| matches!(&r.emoji, EmojiReactionType::Unicode {name} if name == "πŸ—ΊοΈ")); let smiley_react = msg .reactions .iter() - .find(|&r| matches!(&r.emoji, ReactionType::Unicode {name} if name == "πŸ˜€")); + .find(|&r| matches!(&r.emoji, EmojiReactionType::Unicode {name} if name == "πŸ˜€")); let custom_react = find_custom_react(&msg); assert!(world_react.is_some()); diff --git a/twilight-cache-inmemory/src/event/voice_state.rs b/twilight-cache-inmemory/src/event/voice_state.rs index 1c068139d68..31e19a7f7b4 100644 --- a/twilight-cache-inmemory/src/event/voice_state.rs +++ b/twilight-cache-inmemory/src/event/voice_state.rs @@ -307,6 +307,7 @@ mod tests { accent_color: None, avatar: Some(avatar), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, diff --git a/twilight-cache-inmemory/src/lib.rs b/twilight-cache-inmemory/src/lib.rs index 81f3baaf71c..c0b939bd7d5 100644 --- a/twilight-cache-inmemory/src/lib.rs +++ b/twilight-cache-inmemory/src/lib.rs @@ -976,6 +976,9 @@ impl UpdateCache for Event { | Event::BanAdd(_) | Event::BanRemove(_) | Event::CommandPermissionsUpdate(_) + | Event::EntitlementCreate(_) + | Event::EntitlementDelete(_) + | Event::EntitlementUpdate(_) | Event::GatewayClose(_) | Event::GatewayHeartbeat(_) | Event::GatewayHeartbeatAck @@ -991,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/member.rs b/twilight-cache-inmemory/src/model/member.rs index 3a88609373a..7cbbfd9207e 100644 --- a/twilight-cache-inmemory/src/model/member.rs +++ b/twilight-cache-inmemory/src/model/member.rs @@ -339,6 +339,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, diff --git a/twilight-cache-inmemory/src/model/message.rs b/twilight-cache-inmemory/src/model/message.rs index 7e64364dc2e..faf44efd6b6 100644 --- a/twilight-cache-inmemory/src/model/message.rs +++ b/twilight-cache-inmemory/src/model/message.rs @@ -6,7 +6,7 @@ use twilight_model::{ channel::{ message::{ sticker::MessageSticker, Component, Embed, Message, MessageActivity, - MessageApplication, MessageFlags, MessageInteraction, MessageReference, + MessageApplication, MessageCall, MessageFlags, MessageInteraction, MessageReference, MessageSnapshot, MessageType, Reaction, RoleSubscriptionData, }, Attachment, ChannelMention, @@ -20,6 +20,7 @@ use twilight_model::{ }, Id, }, + poll::Poll, util::Timestamp, }; @@ -98,6 +99,7 @@ pub struct CachedMessage { application_id: Option>, pub(crate) attachments: Vec, author: Id, + pub(crate) call: Option, channel_id: Id, components: Vec, pub(crate) content: String, @@ -115,6 +117,7 @@ pub struct CachedMessage { pub(crate) mentions: Vec>, pub(crate) message_snapshots: Vec, pub(crate) pinned: bool, + pub(crate) poll: Option, pub(crate) reactions: Vec, reference: Option, role_subscription_data: Option, @@ -305,6 +308,7 @@ impl From for CachedMessage { application_id, attachments, author, + call, channel_id, components, content, @@ -322,6 +326,7 @@ impl From for CachedMessage { mentions, message_snapshots, pinned, + poll, reactions, reference, referenced_message: _, @@ -340,6 +345,7 @@ impl From for CachedMessage { application_id, attachments, author: author.id, + call, channel_id, components, content, @@ -356,6 +362,7 @@ impl From for CachedMessage { mentions: mentions.into_iter().map(|mention| mention.id).collect(), message_snapshots, pinned, + poll, reactions, reference, role_subscription_data, @@ -376,6 +383,7 @@ impl PartialEq for CachedMessage { && self.application_id == other.application_id && self.attachments == other.attachments && self.author == other.author.id + && self.call == other.call && self.channel_id == other.channel_id && self.components == other.components && self.content == other.content diff --git a/twilight-cache-inmemory/src/model/sticker.rs b/twilight-cache-inmemory/src/model/sticker.rs index 2e4cbe53de2..1e7b880932b 100644 --- a/twilight-cache-inmemory/src/model/sticker.rs +++ b/twilight-cache-inmemory/src/model/sticker.rs @@ -211,6 +211,7 @@ mod tests { accent_color: None, avatar: Some(avatar), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, diff --git a/twilight-cache-inmemory/src/test.rs b/twilight-cache-inmemory/src/test.rs index ed244f3736e..68a1d3b7ece 100644 --- a/twilight-cache-inmemory/src/test.rs +++ b/twilight-cache-inmemory/src/test.rs @@ -3,7 +3,7 @@ use twilight_model::{ channel::{ message::{ sticker::{Sticker, StickerFormatType, StickerType}, - Message, MessageFlags, MessageType, ReactionType, + EmojiReactionType, Message, MessageFlags, MessageType, }, Channel, ChannelType, }, @@ -45,6 +45,7 @@ pub fn cache_with_message_and_reactions() -> DefaultInMemoryCache { accent_color: None, avatar: Some(avatar), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -60,6 +61,7 @@ pub fn cache_with_message_and_reactions() -> DefaultInMemoryCache { system: None, verified: None, }, + call: None, channel_id: Id::new(2), components: Vec::new(), content: "ping".to_owned(), @@ -89,6 +91,7 @@ pub fn cache_with_message_and_reactions() -> DefaultInMemoryCache { mentions: Vec::new(), message_snapshots: Vec::new(), pinned: false, + poll: None, reactions: Vec::new(), reference: None, role_subscription_data: None, @@ -103,8 +106,10 @@ pub fn cache_with_message_and_reactions() -> DefaultInMemoryCache { cache.update(&MessageCreate(msg)); let mut reaction = ReactionAdd(GatewayReaction { + burst: false, + burst_colors: Vec::new(), channel_id: Id::new(2), - emoji: ReactionType::Unicode { + emoji: EmojiReactionType::Unicode { name: "πŸ˜€".to_owned(), }, guild_id: Some(Id::new(1)), @@ -123,6 +128,7 @@ pub fn cache_with_message_and_reactions() -> DefaultInMemoryCache { accent_color: None, avatar: Some(avatar), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -164,6 +170,7 @@ pub fn cache_with_message_and_reactions() -> DefaultInMemoryCache { accent_color: None, avatar: Some(user_5_avatar), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 2, @@ -184,13 +191,13 @@ pub fn cache_with_message_and_reactions() -> DefaultInMemoryCache { cache.update(&reaction); - reaction.emoji = ReactionType::Unicode { + reaction.emoji = EmojiReactionType::Unicode { name: "πŸ—ΊοΈ".to_owned(), }; cache.update(&reaction); - reaction.emoji = ReactionType::Custom { + reaction.emoji = EmojiReactionType::Custom { animated: true, id: Id::new(6), name: Some("custom".to_owned()), @@ -359,6 +366,7 @@ pub fn user(id: Id) -> User { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: Some(banner), bot: false, discriminator: 1, diff --git a/twilight-gateway/src/event.rs b/twilight-gateway/src/event.rs index 31925ba1831..86e1ce1eb72 100644 --- a/twilight-gateway/src/event.rs +++ b/twilight-gateway/src/event.rs @@ -52,6 +52,12 @@ bitflags! { /// /// [`AutoModerationRule`]: crate::guild::auto_moderation::AutoModerationRule const AUTO_MODERATION_RULE_UPDATE = 1 << 74; + /// An entitlement has been created. + const ENTITLEMENT_CREATE = 1 << 76; + /// An entitlement has been deleted. + const ENTITLEMENT_DELETE = 1 << 77; + /// An entitlement has been updated. + const ENTITLEMENT_UPDATE = 1 << 78; /// User has been banned from a guild. const BAN_ADD = 1; /// User has been unbanned from a guild. @@ -132,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. @@ -276,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 @@ -330,6 +346,9 @@ impl From for EventTypeFlags { EventType::ChannelPinsUpdate => Self::CHANNEL_PINS_UPDATE, EventType::ChannelUpdate => Self::CHANNEL_UPDATE, EventType::CommandPermissionsUpdate => Self::COMMAND_PERMISSIONS_UPDATE, + EventType::EntitlementCreate => Self::ENTITLEMENT_CREATE, + EventType::EntitlementDelete => Self::ENTITLEMENT_DELETE, + EventType::EntitlementUpdate => Self::ENTITLEMENT_UPDATE, EventType::GatewayClose => Self::empty(), EventType::GatewayHeartbeat => Self::GATEWAY_HEARTBEAT, EventType::GatewayHeartbeatAck => Self::GATEWAY_HEARTBEAT_ACK, @@ -361,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/Cargo.toml b/twilight-http-ratelimiting/Cargo.toml index 3485b2b3bde..60ac8f6982c 100644 --- a/twilight-http-ratelimiting/Cargo.toml +++ b/twilight-http-ratelimiting/Cargo.toml @@ -18,7 +18,7 @@ tokio = { version = "1", default-features = false, features = ["rt", "sync", "ti tracing = { default-features = false, features = ["std", "attributes"], version = "0.1.23" } [dev-dependencies] -criterion = { default-features = false, version = "0.4" } +criterion = { default-features = false, version = "0.5" } http = { version = "1", default-features = false } static_assertions = { default-features = false, version = "1.1.0" } tokio = { default-features = false, features = ["macros", "rt-multi-thread"], version = "1.0" } diff --git a/twilight-http-ratelimiting/src/in_memory/bucket.rs b/twilight-http-ratelimiting/src/in_memory/bucket.rs index 3843e6c7c78..08fcf2e6093 100644 --- a/twilight-http-ratelimiting/src/in_memory/bucket.rs +++ b/twilight-http-ratelimiting/src/in_memory/bucket.rs @@ -57,11 +57,11 @@ impl Bucket { /// Create a new bucket for the specified [`Path`]. pub fn new(path: Path) -> Self { Self { - limit: AtomicU64::new(u64::max_value()), + limit: AtomicU64::new(u64::MAX), path, queue: BucketQueue::default(), - remaining: AtomicU64::new(u64::max_value()), - reset_after: AtomicU64::new(u64::max_value()), + remaining: AtomicU64::new(u64::MAX), + reset_after: AtomicU64::new(u64::MAX), started_at: Mutex::new(None), } } @@ -134,7 +134,7 @@ impl Bucket { } if let Some((limit, remaining, reset_after)) = ratelimits { - if bucket_limit != limit && bucket_limit == u64::max_value() { + if bucket_limit != limit && bucket_limit == u64::MAX { self.reset_after.store(reset_after, Ordering::SeqCst); self.limit.store(limit, Ordering::SeqCst); } diff --git a/twilight-http-ratelimiting/src/request.rs b/twilight-http-ratelimiting/src/request.rs index d9d1ad7d02a..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. @@ -166,6 +168,10 @@ pub enum Path { ChannelsIdTyping(u64), /// Operating on a channel's webhooks. ChannelsIdWebhooks(u64), + /// Operating on an application's entitlements. + ApplicationIdEntitlements(u64), + /// Operating on an application's SKUs. + ApplicationIdSKUs(u64), /// Operating with the gateway information. Gateway, /// Operating with the gateway information tailored to the current user. @@ -334,9 +340,10 @@ impl FromStr for Path { let parts = s.split('/').skip(skip).collect::>(); Ok(match parts[..] { - ["applications", "me"] => ApplicationsMe, + ["applications", "@me"] => ApplicationsMe, ["applications", id, "commands"] => ApplicationCommand(parse_id(id)?), ["applications", id, "commands", _] => ApplicationCommandId(parse_id(id)?), + ["applications", id, "entitlements"] => ApplicationIdEntitlements(parse_id(id)?), ["applications", id, "guilds", _, "commands"] | ["applications", id, "guilds", _, "commands", "permissions"] => { ApplicationGuildCommand(parse_id(id)?) @@ -345,6 +352,7 @@ impl FromStr for Path { | ["applications", id, "guilds", _, "commands", _, "permissions"] => { ApplicationGuildCommandId(parse_id(id)?) } + ["applications", id, "skus"] => ApplicationIdSKUs(parse_id(id)?), ["channels", id] => ChannelsId(parse_id(id)?), ["channels", id, "followers"] => ChannelsIdFollowers(parse_id(id)?), ["channels", id, "invites"] => ChannelsIdInvites(parse_id(id)?), @@ -393,6 +401,12 @@ impl FromStr for Path { ["guilds", "templates", code] => GuildsTemplatesCode(code.to_string()), ["guilds", id] => GuildsId(parse_id(id)?), ["guilds", id, "audit-logs"] => GuildsIdAuditLogs(parse_id(id)?), + ["guilds", id, "auto-moderation", "rules"] => { + GuildsIdAutoModerationRules(parse_id(id)?) + } + ["guilds", id, "auto-moderation", "rules", _] => { + GuildsIdAutoModerationRulesId(parse_id(id)?) + } ["guilds", id, "bans"] => GuildsIdBans(parse_id(id)?), ["guilds", id, "bans", _] => GuildsIdBansUserId(parse_id(id)?), ["guilds", id, "channels"] => GuildsIdChannels(parse_id(id)?), diff --git a/twilight-http/src/client/mod.rs b/twilight-http/src/client/mod.rs index e3877bea19f..45aca5aa153 100644 --- a/twilight-http/src/client/mod.rs +++ b/twilight-http/src/client/mod.rs @@ -4,6 +4,11 @@ mod interaction; pub use self::{builder::ClientBuilder, interaction::InteractionClient}; +use crate::request::application::monetization::{ + CreateTestEntitlement, CreateTestEntitlementOwner, DeleteTestEntitlement, GetEntitlements, + GetSKUs, +}; +#[allow(deprecated)] use crate::{ client::connector::Connector, error::{Error, ErrorType}, @@ -61,6 +66,7 @@ use crate::{ UpdateCurrentMember, UpdateGuild, UpdateGuildChannelPositions, UpdateGuildMfa, UpdateGuildWelcomeScreen, UpdateGuildWidgetSettings, }, + poll::{EndPoll, GetAnswerVoters}, scheduled_event::{ CreateGuildScheduledEvent, DeleteGuildScheduledEvent, GetGuildScheduledEvent, GetGuildScheduledEventUsers, GetGuildScheduledEvents, UpdateGuildScheduledEvent, @@ -100,13 +106,16 @@ use tokio::time; use twilight_http_ratelimiting::Ratelimiter; use twilight_model::{ channel::{message::AllowedMentions, ChannelType}, - guild::{auto_moderation::AutoModerationEventType, scheduled_event::PrivacyLevel, MfaLevel}, + guild::{ + auto_moderation::AutoModerationEventType, scheduled_event::PrivacyLevel, MfaLevel, + RolePosition, + }, http::{channel_position::Position, permission_overwrite::PermissionOverwrite}, id::{ marker::{ - ApplicationMarker, AutoModerationRuleMarker, ChannelMarker, EmojiMarker, GuildMarker, - IntegrationMarker, MessageMarker, RoleMarker, ScheduledEventMarker, StickerMarker, - UserMarker, WebhookMarker, + ApplicationMarker, AutoModerationRuleMarker, ChannelMarker, EmojiMarker, + EntitlementMarker, GuildMarker, IntegrationMarker, MessageMarker, RoleMarker, + ScheduledEventMarker, SkuMarker, StickerMarker, UserMarker, WebhookMarker, }, Id, }, @@ -775,6 +784,29 @@ impl Client { GetEmojis::new(self, guild_id) } + /// Get the entitlements for an application. + /// + /// # Examples + /// + /// Get emojis for the application `100`: + /// + /// ```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 application_id = Id::new(100); + /// + /// client.entitlements(application_id).await?; + /// # Ok(()) } + /// ``` + pub const fn entitlements(&self, application_id: Id) -> GetEntitlements<'_> { + GetEntitlements::new(self, application_id) + } + /// Get an emoji for a guild by the the guild's ID and emoji's ID. /// /// # Examples @@ -1680,7 +1712,7 @@ impl Client { pub const fn update_role_positions<'a>( &'a self, guild_id: Id, - roles: &'a [(Id, u64)], + roles: &'a [RolePosition], ) -> UpdateRolePositions<'a> { UpdateRolePositions::new(self, guild_id, roles) } @@ -2557,6 +2589,145 @@ impl Client { DeleteGuildSticker::new(self, guild_id, sticker_id) } + /// Creates a test entitlement to a given SKU for a given guild or user. Discord + /// will act as though that user or guild has entitlement to your premium offering. + /// + /// # Examples + /// + /// ```no_run + /// use twilight_http::{Client, request::application::monetization::CreateTestEntitlementOwner}; + /// use twilight_model::id::Id; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let client = Client::new("my token".to_owned()); + /// + /// let application_id = Id::new(1); + /// let sku_id = Id::new(2); + /// let owner = CreateTestEntitlementOwner::Guild(Id::new(3)); + /// + /// client.create_test_entitlement( + /// application_id, + /// sku_id, + /// owner, + /// ).await?; + /// + /// # Ok(()) } + pub const fn create_test_entitlement( + &self, + application_id: Id, + sku_id: Id, + owner: CreateTestEntitlementOwner, + ) -> CreateTestEntitlement<'_> { + 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. + /// + /// # 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 application_id = Id::new(1); + /// let entitlement_id = Id::new(2); + /// + /// client.delete_test_entitlement( + /// application_id, + /// entitlement_id, + /// ).await?; + /// + /// # Ok(()) } + pub const fn delete_test_entitlement( + &self, + application_id: Id, + entitlement_id: Id, + ) -> DeleteTestEntitlement<'_> { + 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 + /// + /// ```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 application_id = Id::new(1); + /// + /// let skus = client.get_skus(application_id).await?; + /// + /// # Ok(()) } + pub const fn get_skus(&self, application_id: Id) -> GetSKUs<'_> { + GetSKUs::new(self, application_id) + } + /// Execute a request, returning a future resolving to a [`Response`]. /// /// # Errors diff --git a/twilight-http/src/lib.rs b/twilight-http/src/lib.rs index 7562599b991..7fb0cf867d2 100644 --- a/twilight-http/src/lib.rs +++ b/twilight-http/src/lib.rs @@ -14,6 +14,7 @@ pub mod response; pub mod routing; mod json; +mod query_formatter; /// Discord API version used by this crate. pub const API_VERSION: u8 = 10; diff --git a/twilight-http/src/query_formatter.rs b/twilight-http/src/query_formatter.rs new file mode 100644 index 00000000000..4b42d9a51de --- /dev/null +++ b/twilight-http/src/query_formatter.rs @@ -0,0 +1,126 @@ +use std::fmt::{Display, Formatter, Write}; + +/// A helper struct to write query paramseters to a formatter. +pub struct QueryStringFormatter<'w1, 'w2> { + formatter: &'w1 mut Formatter<'w2>, + is_first: bool, +} + +impl<'w1, 'w2> QueryStringFormatter<'w1, 'w2> { + pub fn new(formatter: &'w1 mut Formatter<'w2>) -> Self { + Self { + formatter, + is_first: true, + } + } + + /// Writes a query parameter to the formatter. + /// + /// # Errors + /// + /// This returns a [`std::fmt::Error`] if the formatter returns an error. + pub fn write_param(&mut self, key: &str, value: &impl Display) -> std::fmt::Result { + if self.is_first { + self.formatter.write_char('?')?; + self.is_first = false; + } else { + self.formatter.write_char('&')?; + } + + self.formatter.write_str(key)?; + self.formatter.write_char('=')?; + Display::fmt(value, self.formatter) + } + + /// Writes a query parameter to the formatter. + /// + /// # Errors + /// + /// This returns a [`std::fmt::Error`] if the formatter returns an error. + pub fn write_opt_param(&mut self, key: &str, value: Option<&impl Display>) -> std::fmt::Result { + if let Some(value) = value { + self.write_param(key, value) + } else { + Ok(()) + } + } +} + +/// Provides a display implementation for serializing iterable objects into +/// query params. +#[derive(Debug)] +pub struct QueryArray(pub T); + +impl Display for QueryArray +where + T: IntoIterator + Clone, + U: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut iter = self.0.clone().into_iter().peekable(); + + while let Some(item) = iter.next() { + Display::fmt(&item, f)?; + if iter.peek().is_some() { + f.write_str(",")?; + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct Test { + a: Option, + b: Option, + } + + impl Display for Test { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut writer = QueryStringFormatter::new(f); + writer.write_opt_param("a", self.a.as_ref())?; + writer.write_opt_param("b", self.b.as_ref()) + } + } + + #[test] + fn test_query_string_formatter_filled() { + let test = Test { + a: Some(1), + b: Some("hello".to_string()), + }; + + assert_eq!(test.to_string(), "?a=1&b=hello"); + } + + #[test] + fn test_query_string_formatter_empty() { + let test = Test { a: None, b: None }; + + assert_eq!(test.to_string(), ""); + } + + #[test] + fn test_query_string_formatter_single() { + let test = Test { + a: Some(1), + b: None, + }; + + assert_eq!(test.to_string(), "?a=1"); + } + + #[test] + fn test_query_array() { + let query_array = QueryArray([1, 2, 3]); + assert_eq!(query_array.to_string(), "1,2,3"); + + let params = vec!["a", "b", "c"]; + let query_array = QueryArray(¶ms); + assert_eq!(query_array.to_string(), "a,b,c"); + } +} diff --git a/twilight-http/src/request/application/mod.rs b/twilight-http/src/request/application/mod.rs index c87d55ae72c..c327b956239 100644 --- a/twilight-http/src/request/application/mod.rs +++ b/twilight-http/src/request/application/mod.rs @@ -1,2 +1,3 @@ pub mod command; pub mod interaction; +pub mod monetization; diff --git a/twilight-http/src/request/application/monetization/create_test_entitlement.rs b/twilight-http/src/request/application/monetization/create_test_entitlement.rs new file mode 100644 index 00000000000..e053a62fd13 --- /dev/null +++ b/twilight-http/src/request/application/monetization/create_test_entitlement.rs @@ -0,0 +1,136 @@ +use std::future::IntoFuture; + +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use twilight_model::{ + application::monetization::Entitlement, + id::{ + marker::{ApplicationMarker, GuildMarker, SkuMarker, UserMarker}, + Id, + }, +}; + +use crate::{ + request::{Request, TryIntoRequest}, + response::ResponseFuture, + routing::Route, + Client, Error, Response, +}; + +/// Owner of a test entitlement. +pub enum CreateTestEntitlementOwner { + Guild(Id), + User(Id), +} + +impl CreateTestEntitlementOwner { + pub const fn id(&self) -> u64 { + match self { + CreateTestEntitlementOwner::Guild(id) => id.get(), + CreateTestEntitlementOwner::User(id) => id.get(), + } + } + + pub const fn kind(&self) -> u8 { + match self { + CreateTestEntitlementOwner::Guild(_) => 1, + CreateTestEntitlementOwner::User(_) => 2, + } + } +} + +struct CreateTestEntitlementFields { + sku_id: Id, + owner: CreateTestEntitlementOwner, +} + +impl Serialize for CreateTestEntitlementFields { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("CreateTestEntitlementFields", 2)?; + state.serialize_field("sku_id", &self.sku_id.get())?; + state.serialize_field("owner_id", &self.owner.id())?; + state.serialize_field("owner_type", &self.owner.kind())?; + state.end() + } +} + +pub struct CreateTestEntitlement<'a> { + application_id: Id, + fields: CreateTestEntitlementFields, + http: &'a Client, +} + +impl<'a> CreateTestEntitlement<'a> { + pub(crate) const fn new( + http: &'a Client, + application_id: Id, + sku_id: Id, + owner: CreateTestEntitlementOwner, + ) -> Self { + Self { + application_id, + fields: CreateTestEntitlementFields { sku_id, owner }, + http, + } + } +} + +impl IntoFuture for CreateTestEntitlement<'_> { + 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 CreateTestEntitlement<'_> { + fn try_into_request(self) -> Result { + Request::builder(&Route::CreateTestEntitlement { + application_id: self.application_id.get(), + }) + .json(&self.fields) + .build() + } +} + +#[cfg(test)] +mod tests { + use serde_test::Token; + use twilight_model::id::Id; + + use super::{CreateTestEntitlementFields, CreateTestEntitlementOwner}; + + #[test] + fn fields_serialization() { + let value = CreateTestEntitlementFields { + sku_id: Id::new(1), + owner: CreateTestEntitlementOwner::Guild(Id::new(2)), + }; + + serde_test::assert_ser_tokens( + &value, + &[ + Token::Struct { + name: "CreateTestEntitlementFields", + len: 2, + }, + Token::Str("sku_id"), + Token::U64(1), + Token::Str("owner_id"), + Token::U64(2), + Token::Str("owner_type"), + Token::U8(1), + Token::StructEnd, + ], + ); + } +} diff --git a/twilight-http/src/request/application/monetization/delete_test_entitlement.rs b/twilight-http/src/request/application/monetization/delete_test_entitlement.rs new file mode 100644 index 00000000000..b72ad8ee8be --- /dev/null +++ b/twilight-http/src/request/application/monetization/delete_test_entitlement.rs @@ -0,0 +1,58 @@ +use std::future::IntoFuture; + +use twilight_model::id::{ + marker::{ApplicationMarker, EntitlementMarker}, + Id, +}; + +use crate::{ + request::{Request, TryIntoRequest}, + response::{marker::EmptyBody, ResponseFuture}, + routing::Route, + Client, Error, Response, +}; + +pub struct DeleteTestEntitlement<'a> { + application_id: Id, + entitlement_id: Id, + http: &'a Client, +} + +impl<'a> DeleteTestEntitlement<'a> { + pub(crate) const fn new( + http: &'a Client, + application_id: Id, + entitlement_id: Id, + ) -> Self { + Self { + application_id, + entitlement_id, + http, + } + } +} + +impl IntoFuture for DeleteTestEntitlement<'_> { + 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 DeleteTestEntitlement<'_> { + fn try_into_request(self) -> Result { + Request::builder(&Route::DeleteTestEntitlement { + application_id: self.application_id.get(), + entitlement_id: self.entitlement_id.get(), + }) + .build() + } +} diff --git a/twilight-http/src/request/application/monetization/get_entitlements.rs b/twilight-http/src/request/application/monetization/get_entitlements.rs new file mode 100644 index 00000000000..517467e490a --- /dev/null +++ b/twilight-http/src/request/application/monetization/get_entitlements.rs @@ -0,0 +1,146 @@ +use std::future::IntoFuture; + +use twilight_model::{ + application::monetization::Entitlement, + id::{ + marker::{ApplicationMarker, EntitlementMarker, GuildMarker, SkuMarker, UserMarker}, + Id, + }, +}; + +use crate::{ + request::{Request, TryIntoRequest}, + response::{marker::ListBody, ResponseFuture}, + routing::Route, + Client, Error, Response, +}; + +use twilight_validate::request::{ + get_entitlements_limit as validate_get_entitlements_limit, ValidationError, +}; + +struct GetEntitlementsFields<'a> { + after: Option>, + before: Option>, + exclude_ended: Option, + guild_id: Option>, + limit: Option, + sku_ids: &'a [Id], + user_id: Option>, +} + +/// Get all entitlements for a given app, active and expired. +#[must_use = "requests must be configured and executed"] +pub struct GetEntitlements<'a> { + application_id: Id, + fields: GetEntitlementsFields<'a>, + http: &'a Client, +} + +impl<'a> GetEntitlements<'a> { + pub(crate) const fn new(http: &'a Client, application_id: Id) -> Self { + Self { + application_id, + fields: GetEntitlementsFields { + after: None, + before: None, + exclude_ended: None, + guild_id: None, + limit: None, + sku_ids: &[], + user_id: None, + }, + http, + } + } + + /// Retrieve entitlements after this time. + pub const fn after(mut self, after: Id) -> Self { + self.fields.after = Some(after); + + self + } + + /// Retrieve entitlements before this time. + pub const fn before(mut self, before: Id) -> Self { + self.fields.before = Some(before); + + self + } + + /// Whether to exclude ended entitlements. + pub const fn exclude_ended(mut self, exclude_ended: bool) -> Self { + self.fields.exclude_ended = Some(exclude_ended); + + self + } + + /// Guild ID to look up entitlements for. + pub const fn guild_id(mut self, guild_id: Id) -> Self { + self.fields.guild_id = Some(guild_id); + + self + } + + /// Number of entitlements to return. Set to 100 if unspecified. + /// + /// The minimum is 1 and the maximum is 100. + /// + /// # Errors + /// + /// Returns a [`GetEntitlementsError`] error type if the amount + /// is less than 1 or greater than 100. + /// + /// [`GetEntitlementsError`]: twilight_validate::request::ValidationErrorType::GetEntitlements + pub fn limit(mut self, limit: u8) -> Result { + validate_get_entitlements_limit(limit)?; + + self.fields.limit = Some(limit); + + Ok(self) + } + + /// List of SKU IDs to check entitlements for. + pub const fn sku_ids(mut self, sku_ids: &'a [Id]) -> Self { + self.fields.sku_ids = sku_ids; + + self + } + + /// User ID to look up entitlements for. + pub const fn user_id(mut self, user_id: Id) -> Self { + self.fields.user_id = Some(user_id); + + self + } +} + +impl IntoFuture for GetEntitlements<'_> { + 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 GetEntitlements<'_> { + fn try_into_request(self) -> Result { + Ok(Request::from_route(&Route::GetEntitlements { + after: self.fields.after.map(Id::get), + application_id: self.application_id.get(), + before: self.fields.before.map(Id::get), + exclude_ended: self.fields.exclude_ended, + guild_id: self.fields.guild_id.map(Id::get), + limit: self.fields.limit, + sku_ids: self.fields.sku_ids, + user_id: self.fields.user_id.map(Id::get), + })) + } +} diff --git a/twilight-http/src/request/application/monetization/get_skus.rs b/twilight-http/src/request/application/monetization/get_skus.rs new file mode 100644 index 00000000000..46555b01335 --- /dev/null +++ b/twilight-http/src/request/application/monetization/get_skus.rs @@ -0,0 +1,49 @@ +use std::future::IntoFuture; + +use twilight_model::{ + application::monetization::Sku, + id::{marker::ApplicationMarker, Id}, +}; + +use crate::{ + request::{Request, TryIntoRequest}, + response::{marker::ListBody, ResponseFuture}, + routing::Route, + Client, Error, Response, +}; + +pub struct GetSKUs<'a> { + application_id: Id, + http: &'a Client, +} + +impl<'a> GetSKUs<'a> { + pub(crate) const fn new(http: &'a Client, application_id: Id) -> Self { + Self { + application_id, + http, + } + } +} + +impl IntoFuture for GetSKUs<'_> { + 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 GetSKUs<'_> { + fn try_into_request(self) -> Result { + Ok(Request::from_route(&Route::GetSKUs { + application_id: self.application_id.get(), + })) + } +} diff --git a/twilight-http/src/request/application/monetization/mod.rs b/twilight-http/src/request/application/monetization/mod.rs new file mode 100644 index 00000000000..d68ddf51228 --- /dev/null +++ b/twilight-http/src/request/application/monetization/mod.rs @@ -0,0 +1,9 @@ +pub mod create_test_entitlement; +pub mod delete_test_entitlement; +pub mod get_entitlements; +pub mod get_skus; + +pub use self::create_test_entitlement::{CreateTestEntitlement, CreateTestEntitlementOwner}; +pub use self::delete_test_entitlement::DeleteTestEntitlement; +pub use self::get_entitlements::GetEntitlements; +pub use self::get_skus::GetSKUs; diff --git a/twilight-http/src/request/audit_reason.rs b/twilight-http/src/request/audit_reason.rs index 763f2ffadd5..eafe55d0edf 100644 --- a/twilight-http/src/request/audit_reason.rs +++ b/twilight-http/src/request/audit_reason.rs @@ -34,7 +34,7 @@ mod private { emoji::{CreateEmoji, DeleteEmoji, UpdateEmoji}, integration::DeleteGuildIntegration, member::{AddRoleToMember, RemoveMember, RemoveRoleFromMember, UpdateGuildMember}, - role::{CreateRole, DeleteRole, UpdateRole}, + role::{CreateRole, DeleteRole, UpdateRole, UpdateRolePositions}, sticker::{CreateGuildSticker, UpdateGuildSticker}, update_guild_onboarding::UpdateGuildOnboarding, CreateGuildChannel, CreateGuildPrune, UpdateCurrentMember, UpdateGuild, UpdateGuildMfa, @@ -95,6 +95,7 @@ mod private { impl Sealed for UpdateGuildSticker<'_> {} impl Sealed for UpdateGuildWidgetSettings<'_> {} impl Sealed for UpdateRole<'_> {} + impl Sealed for UpdateRolePositions<'_> {} impl Sealed for UpdateThread<'_> {} impl Sealed for UpdateWebhook<'_> {} } @@ -115,7 +116,7 @@ mod tests { emoji::{CreateEmoji, DeleteEmoji, UpdateEmoji}, integration::DeleteGuildIntegration, member::{AddRoleToMember, RemoveMember, RemoveRoleFromMember, UpdateGuildMember}, - role::{CreateRole, DeleteRole, UpdateRole}, + role::{CreateRole, DeleteRole, UpdateRole, UpdateRolePositions}, sticker::{CreateGuildSticker, UpdateGuildSticker}, CreateGuildChannel, CreateGuildPrune, UpdateCurrentMember, UpdateGuild, }, @@ -157,5 +158,6 @@ mod tests { assert_impl_all!(UpdateGuildMember<'_>: AuditLogReason<'static>); assert_impl_all!(UpdateGuildSticker<'_>: AuditLogReason<'static>); assert_impl_all!(UpdateRole<'_>: AuditLogReason<'static>); + assert_impl_all!(UpdateRolePositions<'_>: AuditLogReason<'static>); assert_impl_all!(UpdateWebhook<'_>: AuditLogReason<'static>); } diff --git a/twilight-http/src/request/channel/message/create_message.rs b/twilight-http/src/request/channel/message/create_message.rs index b4941ce6a3f..c215c77b1eb 100644 --- a/twilight-http/src/request/channel/message/create_message.rs +++ b/twilight-http/src/request/channel/message/create_message.rs @@ -20,6 +20,7 @@ use twilight_model::{ marker::{ChannelMarker, MessageMarker, StickerMarker}, Id, }, + poll::Poll, }; use twilight_validate::message::{ attachment as validate_attachment, components as validate_components, @@ -48,6 +49,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, @@ -103,6 +106,7 @@ impl<'a> CreateMessage<'a> { message_reference: None, nonce: None, payload_json: None, + poll: None, allowed_mentions: None, sticker_ids: None, tts: None, @@ -223,6 +227,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/channel/reaction/get_reactions.rs b/twilight-http/src/request/channel/reaction/get_reactions.rs index 76497f13f36..b8bd6fa6391 100644 --- a/twilight-http/src/request/channel/reaction/get_reactions.rs +++ b/twilight-http/src/request/channel/reaction/get_reactions.rs @@ -8,6 +8,7 @@ use crate::{ }; use std::future::IntoFuture; use twilight_model::{ + channel::message::ReactionType, id::{ marker::{ChannelMarker, MessageMarker, UserMarker}, Id, @@ -21,6 +22,7 @@ use twilight_validate::request::{ struct GetReactionsFields { after: Option>, limit: Option, + kind: Option, } /// Get a list of users that reacted to a message with an `emoji`. @@ -49,6 +51,7 @@ impl<'a> GetReactions<'a> { fields: Ok(GetReactionsFields { after: None, limit: None, + kind: None, }), http, message_id, @@ -85,6 +88,17 @@ impl<'a> GetReactions<'a> { self } + + /// Set the kind of reaction to retrieve. + /// + /// This can be either a super reaction or a normal reaction. + pub fn kind(mut self, kind: ReactionType) -> Self { + if let Ok(fields) = self.fields.as_mut() { + fields.kind = Some(kind); + } + + self + } } impl IntoFuture for GetReactions<'_> { @@ -112,6 +126,7 @@ impl TryIntoRequest for GetReactions<'_> { emoji: self.emoji, limit: fields.limit, message_id: self.message_id.get(), + kind: fields.kind.map(Into::into), })) } } diff --git a/twilight-http/src/request/guild/get_guild_prune_count.rs b/twilight-http/src/request/guild/get_guild_prune_count.rs index eeb0f3d73ad..894180af96a 100644 --- a/twilight-http/src/request/guild/get_guild_prune_count.rs +++ b/twilight-http/src/request/guild/get_guild_prune_count.rs @@ -118,6 +118,6 @@ mod tests { assert!(!days_valid(0)); assert!(days_valid(1)); - assert!(!days_valid(u16::max_value())); + assert!(!days_valid(u16::MAX)); } } diff --git a/twilight-http/src/request/guild/role/update_role_positions.rs b/twilight-http/src/request/guild/role/update_role_positions.rs index 51f94d0ec0d..5f4a35d7193 100644 --- a/twilight-http/src/request/guild/role/update_role_positions.rs +++ b/twilight-http/src/request/guild/role/update_role_positions.rs @@ -1,18 +1,16 @@ use crate::{ client::Client, error::Error, - request::{Request, TryIntoRequest}, + request::{self, AuditLogReason, Request, TryIntoRequest}, response::{marker::ListBody, Response, ResponseFuture}, routing::Route, }; use std::future::IntoFuture; use twilight_model::{ - guild::Role, - id::{ - marker::{GuildMarker, RoleMarker}, - Id, - }, + guild::{Role, RolePosition}, + id::{marker::GuildMarker, Id}, }; +use twilight_validate::request::{audit_reason as validate_audit_reason, ValidationError}; /// Modify the position of the roles. /// @@ -21,23 +19,33 @@ use twilight_model::{ pub struct UpdateRolePositions<'a> { guild_id: Id, http: &'a Client, - roles: &'a [(Id, u64)], + roles: &'a [RolePosition], + reason: Result, ValidationError>, } impl<'a> UpdateRolePositions<'a> { pub(crate) const fn new( http: &'a Client, guild_id: Id, - roles: &'a [(Id, u64)], + roles: &'a [RolePosition], ) -> Self { Self { guild_id, http, roles, + reason: Ok(None), } } } +impl<'a> AuditLogReason<'a> for UpdateRolePositions<'a> { + fn reason(mut self, reason: &'a str) -> Self { + self.reason = validate_audit_reason(reason).and(Ok(Some(reason))); + + self + } +} + impl IntoFuture for UpdateRolePositions<'_> { type Output = Result>, Error>; @@ -55,10 +63,15 @@ impl IntoFuture for UpdateRolePositions<'_> { impl TryIntoRequest for UpdateRolePositions<'_> { fn try_into_request(self) -> Result { - Request::builder(&Route::UpdateRolePositions { + let mut request = Request::builder(&Route::UpdateRolePositions { guild_id: self.guild_id.get(), }) - .json(&self.roles) - .build() + .json(&self.roles); + + if let Some(reason) = self.reason.map_err(Error::validation)? { + request = request.headers(request::audit_header(reason)?); + } + + request.build() } } 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 db118053a32..91f5832f578 100644 --- a/twilight-http/src/request/try_into_request.rs +++ b/twilight-http/src/request/try_into_request.rs @@ -18,6 +18,10 @@ mod private { CreateFollowup, CreateResponse, DeleteFollowup, DeleteResponse, GetFollowup, GetResponse, UpdateFollowup, UpdateResponse, }, + monetization::{ + create_test_entitlement::CreateTestEntitlement, get_entitlements::GetEntitlements, + DeleteTestEntitlement, GetSKUs, + }, }, channel::{ invite::{CreateInvite, DeleteInvite, GetChannelInvites, GetInvite}, @@ -72,6 +76,7 @@ mod private { UpdateCurrentMember, UpdateGuild, UpdateGuildChannelPositions, UpdateGuildMfa, UpdateGuildWelcomeScreen, UpdateGuildWidgetSettings, }, + poll::{EndPoll, GetAnswerVoters}, scheduled_event::{ CreateGuildExternalScheduledEvent, CreateGuildStageInstanceScheduledEvent, CreateGuildVoiceScheduledEvent, DeleteGuildScheduledEvent, GetGuildScheduledEvent, @@ -127,6 +132,7 @@ mod private { impl Sealed for CreateRole<'_> {} impl Sealed for CreateStageInstance<'_> {} impl Sealed for CreateTemplate<'_> {} + impl Sealed for CreateTestEntitlement<'_> {} impl Sealed for CreateThread<'_> {} impl Sealed for CreateThreadFromMessage<'_> {} impl Sealed for CreateTypingTrigger<'_> {} @@ -158,10 +164,13 @@ mod private { impl Sealed for DeleteTemplate<'_> {} 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<'_> {} @@ -179,6 +188,7 @@ mod private { impl Sealed for GetCurrentUserGuilds<'_> {} impl Sealed for GetEmoji<'_> {} impl Sealed for GetEmojis<'_> {} + impl Sealed for GetEntitlements<'_> {} impl Sealed for GetFollowup<'_> {} impl Sealed for GetGateway<'_> {} impl Sealed for GetGatewayAuthed<'_> {} @@ -218,6 +228,7 @@ mod private { impl Sealed for GetPublicArchivedThreads<'_> {} impl Sealed for GetReactions<'_> {} impl Sealed for GetResponse<'_> {} + impl Sealed for GetSKUs<'_> {} impl Sealed for GetStageInstance<'_> {} impl Sealed for GetSticker<'_> {} impl Sealed for GetTemplate<'_> {} diff --git a/twilight-http/src/request/user/update_current_user.rs b/twilight-http/src/request/user/update_current_user.rs index e166a439744..761458bc4d3 100644 --- a/twilight-http/src/request/user/update_current_user.rs +++ b/twilight-http/src/request/user/update_current_user.rs @@ -17,6 +17,8 @@ struct UpdateCurrentUserFields<'a> { #[serde(skip_serializing_if = "Option::is_none")] avatar: Option>, #[serde(skip_serializing_if = "Option::is_none")] + banner: Option>, + #[serde(skip_serializing_if = "Option::is_none")] username: Option<&'a str>, } @@ -36,6 +38,7 @@ impl<'a> UpdateCurrentUser<'a> { Self { fields: Ok(UpdateCurrentUserFields { avatar: None, + banner: None, username: None, }), http, @@ -58,6 +61,21 @@ impl<'a> UpdateCurrentUser<'a> { self } + /// Set the user's banner. + /// + /// This must be a Data URI, in the form of + /// `data:image/{type};base64,{data}` where `{type}` is the image MIME type + /// and `{data}` is the base64-encoded image. See [Discord Docs/Image Data]. + /// + /// [Discord Docs/Image Data]: https://discord.com/developers/docs/reference#image-data + pub fn banner(mut self, banner: Option<&'a str>) -> Self { + if let Ok(fields) = self.fields.as_mut() { + fields.banner = Some(Nullable(banner)); + } + + self + } + /// Set the username. /// /// The minimum length is 1 UTF-16 character and the maximum is 32 UTF-16 characters. diff --git a/twilight-http/src/routing.rs b/twilight-http/src/routing.rs index 90fdaf2bf90..42fdd9ac85d 100644 --- a/twilight-http/src/routing.rs +++ b/twilight-http/src/routing.rs @@ -1,9 +1,15 @@ use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; pub use twilight_http_ratelimiting::request::{Path, PathParseError, PathParseErrorType}; -use crate::request::{channel::reaction::RequestReactionType, Method}; +use crate::{ + query_formatter::{QueryArray, QueryStringFormatter}, + request::{channel::reaction::RequestReactionType, Method}, +}; use std::fmt::{Display, Formatter, Result as FmtResult}; -use twilight_model::id::{marker::RoleMarker, Id}; +use twilight_model::id::{ + marker::{RoleMarker, SkuMarker}, + Id, +}; #[derive(Clone, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] @@ -138,6 +144,10 @@ pub enum Route<'a> { /// The ID of the guild. guild_id: u64, }, + CreateTestEntitlement { + /// The ID of the application. + application_id: u64, + }, /// Route information to create a thread in a channel. CreateThread { /// ID of the channel. @@ -338,6 +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. @@ -359,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. @@ -456,6 +491,24 @@ pub enum Route<'a> { /// The ID of the guild. guild_id: u64, }, + GetEntitlements { + /// Retrieve entitlements after this time. + after: Option, + /// The ID of the application. + application_id: u64, + /// Retrieve entitlements before this time. + before: Option, + /// Whether to exclude ended entitlements. + exclude_ended: Option, + /// Guild ID to look up entitlements for. + guild_id: Option, + /// Number of entitlements to return. Set to 100 if unspecified. + limit: Option, + /// List of SKU IDs to check entitlements for. + sku_ids: &'a [Id], + /// User ID to look up entitlements for. + user_id: Option, + }, /// Route to get a followup message for an interaction. GetFollowupMessage { /// ID of the application. @@ -751,6 +804,12 @@ pub enum Route<'a> { limit: Option, /// The ID of the message. message_id: u64, + /// The type of reactions to fetch. + kind: Option, + }, + GetSKUs { + /// The ID of the application. + application_id: u64, }, /// Route information to get a stage instance. GetStageInstance { @@ -1137,6 +1196,7 @@ impl<'a> Route<'a> { | Self::DeleteGuildIntegration { .. } | Self::DeleteGuildScheduledEvent { .. } | Self::DeleteGuildSticker { .. } + | Self::DeleteTestEntitlement { .. } | Self::DeleteInteractionOriginal { .. } | Self::DeleteInvite { .. } | Self::DeleteMessageReactions { .. } @@ -1157,6 +1217,7 @@ impl<'a> Route<'a> { | Self::RemoveThreadMember { .. } | Self::UnpinMessage { .. } => Method::Delete, Self::GetActiveThreads { .. } + | Self::GetAnswerVoters { .. } | Self::GetAuditLogs { .. } | Self::GetAutoModerationRule { .. } | Self::GetBan { .. } @@ -1174,6 +1235,7 @@ impl<'a> Route<'a> { | Self::GetCurrentUserGuildMember { .. } | Self::GetEmoji { .. } | Self::GetEmojis { .. } + | Self::GetEntitlements { .. } | Self::GetGateway | Self::GetFollowupMessage { .. } | Self::GetGlobalCommand { .. } @@ -1214,6 +1276,7 @@ impl<'a> Route<'a> { | Self::GetPrivateArchivedThreads { .. } | Self::GetPublicArchivedThreads { .. } | Self::GetReactionUsers { .. } + | Self::GetSKUs { .. } | Self::GetStageInstance { .. } | Self::GetSticker { .. } | Self::GetTemplate { .. } @@ -1275,10 +1338,12 @@ impl<'a> Route<'a> { | Self::CreateRole { .. } | Self::CreateStageInstance { .. } | Self::CreateTemplate { .. } + | Self::CreateTestEntitlement { .. } | Self::CreateTypingTrigger { .. } | Self::CreateWebhook { .. } | Self::CrosspostMessage { .. } | Self::DeleteMessages { .. } + | Self::EndPoll { .. } | Self::ExecuteWebhook { .. } | Self::FollowNewsChannel { .. } | Self::InteractionCallback { .. } @@ -1416,6 +1481,11 @@ impl<'a> Route<'a> { Self::CreateThreadFromMessage { channel_id, .. } => { Path::ChannelsIdMessagesIdThreads(channel_id) } + Self::CreateTestEntitlement { application_id } + | Self::GetEntitlements { application_id, .. } + | Self::DeleteTestEntitlement { application_id, .. } => { + Path::ApplicationIdEntitlements(application_id) + } Self::CreateTypingTrigger { channel_id } => Path::ChannelsIdTyping(channel_id), Self::CreateWebhook { channel_id } | Self::GetChannelWebhooks { channel_id } => { Path::ChannelsIdWebhooks(channel_id) @@ -1591,6 +1661,7 @@ impl<'a> Route<'a> { Self::GetPins { channel_id } | Self::PinMessage { channel_id, .. } => { Path::ChannelsIdPins(channel_id) } + Self::GetSKUs { application_id } => Path::ApplicationIdSKUs(application_id), Self::GetSticker { .. } => Path::Stickers, Self::GetUserConnections => Path::UsersIdConnections, Self::GetVoiceRegions => Path::VoiceRegions, @@ -1610,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) + } } } } @@ -1748,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, @@ -1756,12 +1849,9 @@ impl Display for Route<'_> { Display::fmt(application_id, f)?; f.write_str("/commands")?; - if let Some(with_localizations) = with_localizations { - f.write_str("?with_localizations=")?; - Display::fmt(with_localizations, f)?; - } + let mut writer = QueryStringFormatter::new(f); - Ok(()) + writer.write_opt_param("with_localizations", with_localizations.as_ref()) } Route::CreateGuild => f.write_str("guilds"), Route::CreateGuildCommand { @@ -1790,12 +1880,8 @@ impl Display for Route<'_> { Display::fmt(guild_id, f)?; f.write_str("/commands")?; - if let Some(with_localizations) = with_localizations { - f.write_str("?with_localizations=")?; - Display::fmt(with_localizations, f)?; - } - - Ok(()) + let mut writer = QueryStringFormatter::new(f); + writer.write_opt_param("with_localizations", with_localizations.as_ref()) } Route::CreateGuildFromTemplate { template_code } | Route::GetTemplate { template_code } => { @@ -1803,6 +1889,12 @@ impl Display for Route<'_> { f.write_str(template_code) } + Route::CreateTestEntitlement { application_id } => { + f.write_str("applications/")?; + Display::fmt(application_id, f)?; + + f.write_str("/entitlements") + } Route::CreateGuildIntegration { guild_id } | Route::GetGuildIntegrations { guild_id } => { f.write_str("guilds/")?; @@ -1818,30 +1910,15 @@ impl Display for Route<'_> { } => { f.write_str("guilds/")?; Display::fmt(guild_id, f)?; - f.write_str("/prune?")?; + f.write_str("/prune")?; - if let Some(compute_prune_count) = compute_prune_count { - f.write_str("compute_prune_count=")?; - Display::fmt(compute_prune_count, f)?; - } + let mut writer = QueryStringFormatter::new(f); - if let Some(days) = days { - f.write_str("&days=")?; - Display::fmt(days, f)?; - } + writer.write_opt_param("compute_prune_count", compute_prune_count.as_ref())?; + writer.write_opt_param("days", days.as_ref())?; if !include_roles.is_empty() { - let role_count = include_roles.len() - 1; - - f.write_str("&include_roles=")?; - - for (idx, role_id) in include_roles.iter().enumerate() { - Display::fmt(role_id, f)?; - - if idx < role_count { - f.write_str(",")?; - } - } + writer.write_param("include_roles", &QueryArray(*include_roles))?; } Ok(()) @@ -1973,6 +2050,68 @@ impl Display for Route<'_> { Display::fmt(emoji_id, f) } + Route::GetEntitlements { + after, + application_id, + before, + exclude_ended, + guild_id, + limit, + sku_ids, + user_id, + } => { + f.write_str("applications/")?; + Display::fmt(application_id, f)?; + f.write_str("/entitlements")?; + + f.write_str("?")?; + + if let Some(after) = after { + f.write_str("after=")?; + Display::fmt(after, f)?; + } + + if let Some(before) = before { + f.write_str("&before=")?; + Display::fmt(before, f)?; + } + + if let Some(exclude_ended) = exclude_ended { + f.write_str("&exclude_ended=")?; + Display::fmt(exclude_ended, f)?; + } + + if let Some(guild_id) = guild_id { + f.write_str("&guild_id=")?; + Display::fmt(guild_id, f)?; + } + + if let Some(limit) = limit { + f.write_str("&limit=")?; + Display::fmt(limit, f)?; + } + + if !sku_ids.is_empty() { + let sku_id_count = sku_ids.len() - 1; + + f.write_str("&sku_ids=")?; + + for (idx, sku_id) in sku_ids.iter().enumerate() { + Display::fmt(sku_id, f)?; + + if idx < sku_id_count { + f.write_str(",")?; + } + } + } + + if let Some(user_id) = user_id { + f.write_str("&user_id=")?; + Display::fmt(user_id, f)?; + } + + Ok(()) + } Route::DeleteGlobalCommand { application_id, command_id, @@ -2198,12 +2337,9 @@ impl Display for Route<'_> { f.write_str("/messages/")?; Display::fmt(message_id, f)?; - if let Some(thread_id) = thread_id { - f.write_str("?thread_id=")?; - Display::fmt(thread_id, f)?; - } + let mut query_formatter = QueryStringFormatter::new(f); - Ok(()) + query_formatter.write_opt_param("thread_id", thread_id.as_ref()) } Route::DeleteWebhook { token, webhook_id } | Route::GetWebhook { token, webhook_id } @@ -2218,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, @@ -2228,20 +2375,21 @@ impl Display for Route<'_> { Display::fmt(webhook_id, f)?; f.write_str("/")?; f.write_str(token)?; - f.write_str("?")?; - if let Some(thread_id) = thread_id { - f.write_str("thread_id=")?; - Display::fmt(thread_id, f)?; - f.write_str("&")?; - } + let mut query_formatter = QueryStringFormatter::new(f); - if let Some(wait) = wait { - f.write_str("wait=")?; - f.write_str(if *wait { "true" } else { "false" })?; - } + query_formatter.write_opt_param("thread_id", thread_id.as_ref())?; + query_formatter.write_opt_param("wait", wait.as_ref()) + } + Route::DeleteTestEntitlement { + application_id, + entitlement_id, + } => { + f.write_str("applications/")?; + Display::fmt(application_id, f)?; + f.write_str("/entitlements/")?; - Ok(()) + Display::fmt(entitlement_id, f) } Route::FollowNewsChannel { channel_id } => { f.write_str("channels/")?; @@ -2265,34 +2413,15 @@ impl Display for Route<'_> { } => { f.write_str("guilds/")?; Display::fmt(guild_id, f)?; - f.write_str("/audit-logs?")?; - - if let Some(action_type) = action_type { - f.write_str("action_type=")?; - Display::fmt(action_type, f)?; - } - - if let Some(after) = after { - f.write_str("&after=")?; - Display::fmt(after, f)?; - } - - if let Some(before) = before { - f.write_str("&before=")?; - Display::fmt(before, f)?; - } - - if let Some(limit) = limit { - f.write_str("&limit=")?; - Display::fmt(limit, f)?; - } + f.write_str("/audit-logs")?; - if let Some(user_id) = user_id { - f.write_str("&user_id=")?; - Display::fmt(user_id, f)?; - } + let mut query_formatter = QueryStringFormatter::new(f); - Ok(()) + query_formatter.write_opt_param("action_type", action_type.as_ref())?; + query_formatter.write_opt_param("after", after.as_ref())?; + query_formatter.write_opt_param("before", before.as_ref())?; + query_formatter.write_opt_param("limit", limit.as_ref())?; + query_formatter.write_opt_param("user_id", user_id.as_ref()) } Route::GetBans { guild_id } => { f.write_str("guilds/")?; @@ -2308,24 +2437,13 @@ impl Display for Route<'_> { } => { f.write_str("guilds/")?; Display::fmt(guild_id, f)?; - f.write_str("/bans?")?; - - if let Some(after) = after { - f.write_str("after=")?; - Display::fmt(after, f)?; - } + f.write_str("/bans")?; - if let Some(before) = before { - f.write_str("&before=")?; - Display::fmt(before, f)?; - } - - if let Some(limit) = limit { - f.write_str("&limit=")?; - Display::fmt(limit, f)?; - } + let mut query_formatter = QueryStringFormatter::new(f); - Ok(()) + query_formatter.write_opt_param("after", after.as_ref())?; + query_formatter.write_opt_param("before", before.as_ref())?; + query_formatter.write_opt_param("limit", limit.as_ref()) } Route::GetGatewayBot => f.write_str("gateway/bot"), Route::GetCommandPermissions { @@ -2366,8 +2484,10 @@ impl Display for Route<'_> { f.write_str("guilds/")?; Display::fmt(guild_id, f)?; + let mut query_formatter = QueryStringFormatter::new(f); + if *with_counts { - f.write_str("?with_counts=true")?; + query_formatter.write_param("with_counts", &true)?; } Ok(()) @@ -2396,19 +2516,12 @@ impl Display for Route<'_> { } => { f.write_str("guilds/")?; Display::fmt(guild_id, f)?; - f.write_str("/members?")?; + f.write_str("/members")?; - if let Some(after) = after { - f.write_str("after=")?; - Display::fmt(after, f)?; - } + let mut query_formatter = QueryStringFormatter::new(f); - if let Some(limit) = limit { - f.write_str("&limit=")?; - Display::fmt(limit, f)?; - } - - Ok(()) + query_formatter.write_opt_param("after", after.as_ref())?; + query_formatter.write_opt_param("limit", limit.as_ref()) } Route::GetGuildOnboarding { guild_id } | Route::UpdateGuildOnboarding { guild_id } => { f.write_str("guilds/")?; @@ -2429,25 +2542,14 @@ impl Display for Route<'_> { } => { f.write_str("guilds/")?; Display::fmt(guild_id, f)?; - f.write_str("/prune?")?; + f.write_str("/prune")?; - if let Some(days) = days { - f.write_str("days=")?; - Display::fmt(days, f)?; - } - - if !include_roles.is_empty() { - f.write_str("&include_roles=")?; + let mut query_formatter = QueryStringFormatter::new(f); - let role_count = include_roles.len() - 1; + query_formatter.write_opt_param("days", days.as_ref())?; - for (idx, role_id) in include_roles.iter().enumerate() { - Display::fmt(role_id, f)?; - - if idx < role_count { - f.write_str(",")?; - } - } + if !include_roles.is_empty() { + query_formatter.write_param("include_roles", &QueryArray(*include_roles))?; } Ok(()) @@ -2462,8 +2564,10 @@ impl Display for Route<'_> { f.write_str("/scheduled-events/")?; Display::fmt(scheduled_event_id, f)?; + let mut query_formatter = QueryStringFormatter::new(f); + if *with_user_count { - f.write_str("?with_user_count=true")?; + query_formatter.write_param("with_user_count", &true)?; } Ok(()) @@ -2480,25 +2584,16 @@ impl Display for Route<'_> { Display::fmt(guild_id, f)?; f.write_str("/scheduled-events/")?; Display::fmt(scheduled_event_id, f)?; - f.write_str("/users?")?; + f.write_str("/users")?; - if let Some(after) = after { - f.write_str("after=")?; - Display::fmt(after, f)?; - } + let mut query_formatter = QueryStringFormatter::new(f); - if let Some(before) = before { - f.write_str("&before=")?; - Display::fmt(before, f)?; - } - - if let Some(limit) = limit { - f.write_str("&limit=")?; - Display::fmt(limit, f)?; - } + query_formatter.write_opt_param("after", after.as_ref())?; + query_formatter.write_opt_param("before", before.as_ref())?; + query_formatter.write_opt_param("limit", limit.as_ref())?; if *with_member { - f.write_str("&with_member=true")?; + query_formatter.write_param("with_member", &true)?; } Ok(()) @@ -2509,10 +2604,12 @@ impl Display for Route<'_> { } => { f.write_str("guilds/")?; Display::fmt(guild_id, f)?; - f.write_str("/scheduled-events?")?; + f.write_str("/scheduled-events")?; + + let mut query_formatter = QueryStringFormatter::new(f); if *with_user_count { - f.write_str("with_user_count=true")?; + query_formatter.write_param("with_user_count", &true)?; } Ok(()) @@ -2581,31 +2678,22 @@ impl Display for Route<'_> { before, limit, } => { - f.write_str("users/@me/guilds?")?; - - if let Some(after) = after { - f.write_str("after=")?; - Display::fmt(after, f)?; - } + f.write_str("users/@me/guilds")?; - if let Some(before) = before { - f.write_str("&before=")?; - Display::fmt(before, f)?; - } + let mut query_formatter = QueryStringFormatter::new(f); - if let Some(limit) = limit { - f.write_str("&limit=")?; - Display::fmt(limit, f)?; - } - - Ok(()) + query_formatter.write_opt_param("after", after.as_ref())?; + query_formatter.write_opt_param("before", before.as_ref())?; + query_formatter.write_opt_param("limit", limit.as_ref()) } Route::GetInvite { code, with_counts } => { f.write_str("invites/")?; f.write_str(code)?; + let mut query_formatter = QueryStringFormatter::new(f); + if *with_counts { - f.write_str("?with_counts=true")?; + query_formatter.write_param("with_counts", &true)?; } Ok(()) @@ -2617,14 +2705,15 @@ impl Display for Route<'_> { } => { f.write_str("invites/")?; f.write_str(code)?; - f.write_str("?")?; + + let mut query_formatter = QueryStringFormatter::new(f); if *with_counts { - f.write_str("with_counts=true")?; + query_formatter.write_param("with_counts", &true)?; } if *with_expiration { - f.write_str("&with_expiration=true")?; + query_formatter.write_param("with_expiration", &true)?; } Ok(()) @@ -2638,29 +2727,14 @@ impl Display for Route<'_> { } => { f.write_str("channels/")?; Display::fmt(channel_id, f)?; - f.write_str("/messages?")?; - - if let Some(after) = after { - f.write_str("after=")?; - Display::fmt(after, f)?; - } - - if let Some(around) = around { - f.write_str("&around=")?; - Display::fmt(around, f)?; - } + f.write_str("/messages")?; - if let Some(before) = before { - f.write_str("&before=")?; - Display::fmt(before, f)?; - } + let mut query_formatter = QueryStringFormatter::new(f); - if let Some(limit) = limit { - f.write_str("&limit=")?; - Display::fmt(limit, f)?; - } - - Ok(()) + query_formatter.write_opt_param("after", after.as_ref())?; + query_formatter.write_opt_param("around", around.as_ref())?; + query_formatter.write_opt_param("before", before.as_ref())?; + query_formatter.write_opt_param("limit", limit.as_ref()) } Route::GetNitroStickerPacks { .. } => f.write_str("sticker-packs"), Route::GetPins { channel_id } => { @@ -2676,19 +2750,12 @@ impl Display for Route<'_> { } => { f.write_str("channels/")?; Display::fmt(channel_id, f)?; - f.write_str("/users/@me/threads/archived/private?")?; + f.write_str("/users/@me/threads/archived/private")?; - if let Some(before) = before { - f.write_str("before=")?; - Display::fmt(before, f)?; - } + let mut query_formatter = QueryStringFormatter::new(f); - if let Some(limit) = limit { - f.write_str("&limit=")?; - Display::fmt(limit, f)?; - } - - Ok(()) + query_formatter.write_opt_param("before", before.as_ref())?; + query_formatter.write_opt_param("limit", limit.as_ref()) } Route::GetPrivateArchivedThreads { before, @@ -2697,19 +2764,12 @@ impl Display for Route<'_> { } => { f.write_str("channels/")?; Display::fmt(channel_id, f)?; - f.write_str("/threads/archived/private?")?; + f.write_str("/threads/archived/private")?; - if let Some(before) = before { - f.write_str("before=")?; - Display::fmt(before, f)?; - } + let mut query_formatter = QueryStringFormatter::new(f); - if let Some(limit) = limit { - f.write_str("&limit=")?; - Display::fmt(limit, f)?; - } - - Ok(()) + query_formatter.write_opt_param("before", before.as_ref())?; + query_formatter.write_opt_param("limit", limit.as_ref()) } Route::GetPublicArchivedThreads { before, @@ -2718,19 +2778,12 @@ impl Display for Route<'_> { } => { f.write_str("channels/")?; Display::fmt(channel_id, f)?; - f.write_str("/threads/archived/public?")?; + f.write_str("/threads/archived/public")?; - if let Some(before) = before { - f.write_str("before=")?; - Display::fmt(before, f)?; - } - - if let Some(limit) = limit { - f.write_str("&limit=")?; - Display::fmt(limit, f)?; - } + let mut query_formatter = QueryStringFormatter::new(f); - Ok(()) + query_formatter.write_opt_param("before", before.as_ref())?; + query_formatter.write_opt_param("limit", limit.as_ref()) } Route::GetReactionUsers { after, @@ -2738,6 +2791,7 @@ impl Display for Route<'_> { emoji, limit, message_id, + kind, } => { f.write_str("channels/")?; Display::fmt(channel_id, f)?; @@ -2745,19 +2799,12 @@ impl Display for Route<'_> { Display::fmt(message_id, f)?; f.write_str("/reactions/")?; Display::fmt(&emoji, f)?; - f.write_str("?")?; - if let Some(after) = after { - f.write_str("after=")?; - Display::fmt(after, f)?; - } + let mut query_formatter = QueryStringFormatter::new(f); - if let Some(limit) = limit { - f.write_str("&limit=")?; - Display::fmt(limit, f)?; - } - - Ok(()) + query_formatter.write_opt_param("after", after.as_ref())?; + query_formatter.write_opt_param("limit", limit.as_ref())?; + query_formatter.write_opt_param("type", kind.as_ref()) } Route::GetSticker { sticker_id } => { f.write_str("stickers/")?; @@ -2773,24 +2820,12 @@ impl Display for Route<'_> { f.write_str("channels/")?; Display::fmt(channel_id, f)?; f.write_str("/thread-members")?; - f.write_str("?")?; - if let Some(after) = after { - f.write_str("after=")?; - Display::fmt(after, f)?; - } + let mut query_formatter = QueryStringFormatter::new(f); - if let Some(limit) = limit { - f.write_str("&limit=")?; - Display::fmt(limit, f)?; - } - - if let Some(with_member) = with_member { - f.write_str("&with_member=")?; - Display::fmt(with_member, f)?; - } - - Ok(()) + query_formatter.write_opt_param("after", after.as_ref())?; + query_formatter.write_opt_param("limit", limit.as_ref())?; + query_formatter.write_opt_param("with_member", with_member.as_ref()) } Route::GetUserConnections => f.write_str("users/@me/connections"), Route::GetUser { user_id } => { @@ -2842,15 +2877,13 @@ impl Display for Route<'_> { } => { f.write_str("guilds/")?; Display::fmt(guild_id, f)?; - f.write_str("/members/search?query=")?; - Display::fmt(&utf8_percent_encode(query, NON_ALPHANUMERIC), f)?; + f.write_str("/members/search")?; - if let Some(limit) = limit { - f.write_str("&limit=")?; - Display::fmt(limit, f)?; - } + let mut query_formatter = QueryStringFormatter::new(f); - Ok(()) + query_formatter + .write_param("query", &utf8_percent_encode(query, NON_ALPHANUMERIC))?; + query_formatter.write_opt_param("limit", limit.as_ref()) } Route::SyncGuildIntegration { guild_id, @@ -2908,6 +2941,12 @@ impl Display for Route<'_> { f.write_str("/mfa") } + Route::GetSKUs { application_id } => { + f.write_str("applications/")?; + Display::fmt(application_id, f)?; + + f.write_str("/skus") + } } } } @@ -3918,7 +3957,7 @@ mod tests { guild_id: GUILD_ID, limit: None, }; - assert_eq!(route.to_string(), format!("guilds/{GUILD_ID}/bans?")); + assert_eq!(route.to_string(), format!("guilds/{GUILD_ID}/bans")); let route = Route::GetBansWithParameters { after: Some(USER_ID), @@ -3939,7 +3978,7 @@ mod tests { }; assert_eq!( route.to_string(), - format!("guilds/{GUILD_ID}/bans?&before={USER_ID}") + format!("guilds/{GUILD_ID}/bans?before={USER_ID}") ); let route = Route::GetBansWithParameters { @@ -3950,7 +3989,7 @@ mod tests { }; assert_eq!( route.to_string(), - format!("guilds/{GUILD_ID}/bans?&limit={limit}", limit = 100) + format!("guilds/{GUILD_ID}/bans?limit={limit}", limit = 100) ); let route = Route::GetBansWithParameters { @@ -3975,6 +4014,32 @@ mod tests { assert_eq!(route.to_string(), "gateway/bot"); } + #[test] + fn get_entitlements() { + let route = Route::GetEntitlements { + after: Some(32), + application_id: 1, + before: Some(2), + exclude_ended: Some(true), + guild_id: Some(42), + limit: Some(99), + sku_ids: &[Id::new(7)], + user_id: Some(11), + }; + + assert_eq!( + route.to_string(), + "applications/1/entitlements?after=32&before=2&exclude_ended=true&guild_id=42&limit=99&sku_ids=7&user_id=11" + ); + } + + #[test] + fn create_test_entitlement() { + let route = Route::CreateTestEntitlement { application_id: 1 }; + + assert_eq!(route.to_string(), "applications/1/entitlements"); + } + #[test] fn get_command_permissions() { let route = Route::GetCommandPermissions { @@ -4197,7 +4262,7 @@ mod tests { }; assert_eq!( route.to_string(), - format!("channels/{CHANNEL_ID}/thread-members?") + format!("channels/{CHANNEL_ID}/thread-members") ); let route = Route::GetThreadMembers { @@ -4374,7 +4439,7 @@ mod tests { guild_id: GUILD_ID, include_roles: &[], }; - assert_eq!(route.to_string(), format!("guilds/{GUILD_ID}/prune?")); + assert_eq!(route.to_string(), format!("guilds/{GUILD_ID}/prune")); } #[test] @@ -4413,10 +4478,7 @@ mod tests { guild_id: GUILD_ID, include_roles: &[], }; - assert_eq!( - route.to_string(), - format!("guilds/{GUILD_ID}/prune?&days=4") - ); + assert_eq!(route.to_string(), format!("guilds/{GUILD_ID}/prune?days=4")); } #[test] @@ -4431,7 +4493,7 @@ mod tests { }; assert_eq!( route.to_string(), - format!("guilds/{GUILD_ID}/prune?&include_roles=1") + format!("guilds/{GUILD_ID}/prune?include_roles=1") ); } @@ -4447,7 +4509,7 @@ mod tests { }; assert_eq!( route.to_string(), - format!("guilds/{GUILD_ID}/prune?&include_roles=1,2") + format!("guilds/{GUILD_ID}/prune?include_roles=1,2") ); } @@ -4476,7 +4538,7 @@ mod tests { assert_eq!( route.to_string(), - format!("guilds/{GUILD_ID}/scheduled-events?") + format!("guilds/{GUILD_ID}/scheduled-events") ); let route = Route::GetGuildScheduledEvents { @@ -4565,7 +4627,7 @@ mod tests { assert_eq!( route.to_string(), format!( - "guilds/{GUILD_ID}/scheduled-events/{SCHEDULED_EVENT_ID}/users?&before={USER_ID}&with_member=true" + "guilds/{GUILD_ID}/scheduled-events/{SCHEDULED_EVENT_ID}/users?before={USER_ID}&with_member=true" ) ); @@ -4692,4 +4754,10 @@ mod tests { let route = Route::GetGuildOnboarding { guild_id: GUILD_ID }; assert_eq!(route.to_string(), format!("guilds/{GUILD_ID}/onboarding")); } + + #[test] + fn get_skus() { + let route = Route::GetSKUs { application_id: 1 }; + assert_eq!(route.to_string(), format!("applications/1/skus")); + } } diff --git a/twilight-mention/Cargo.toml b/twilight-mention/Cargo.toml index bc279e492ad..050dd54fc32 100644 --- a/twilight-mention/Cargo.toml +++ b/twilight-mention/Cargo.toml @@ -16,7 +16,7 @@ version = "0.16.0-rc.1" twilight-model = { default-features = false, path = "../twilight-model", version = "0.16.0-rc.1" } [dev-dependencies] -criterion = { default-features = false, version = "0.4" } +criterion = { default-features = false, version = "0.5" } static_assertions = { default-features = false, version = "1" } [[bench]] diff --git a/twilight-model/Cargo.toml b/twilight-model/Cargo.toml index 0cbb071034e..20673c62586 100644 --- a/twilight-model/Cargo.toml +++ b/twilight-model/Cargo.toml @@ -20,7 +20,7 @@ serde_repr = { default-features = false, version = "0.1.5" } time = { default-features = false, features = ["parsing", "std"], version = "0.3" } [dev-dependencies] -criterion = { default-features = false, version = "0.4" } +criterion = { default-features = false, version = "0.5" } serde_json = { default-features = false, features = ["std"], version = "1" } serde_test = { default-features = false, version = "1" } static_assertions = { default-features = false, version = "1.0" } diff --git a/twilight-model/src/application/interaction/mod.rs b/twilight-model/src/application/interaction/mod.rs index cf2d561d7f1..69d0385c003 100644 --- a/twilight-model/src/application/interaction/mod.rs +++ b/twilight-model/src/application/interaction/mod.rs @@ -36,6 +36,8 @@ use serde::{ use serde_value::{DeserializerError, Value}; use std::fmt::{Formatter, Result as FmtResult}; +use super::monetization::Entitlement; + /// Payload received when a user executes an interaction. /// /// See [Discord Docs/Interaction Object]. @@ -79,6 +81,8 @@ pub struct Interaction { /// [`ModalSubmit`]: InteractionType::ModalSubmit #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, + /// For monetized apps, any entitlements for the invoking user, representing access to premium SKUs + pub entitlements: Vec, /// ID of the guild the interaction was invoked in. #[serde(skip_serializing_if = "Option::is_none")] pub guild_id: Option>, @@ -177,6 +181,7 @@ enum InteractionField { Channel, ChannelId, Data, + Entitlements, GuildId, GuildLocale, Id, @@ -205,6 +210,7 @@ impl<'de> Visitor<'de> for InteractionVisitor { let mut channel: Option = None; let mut channel_id: Option> = None; let mut data: Option = None; + let mut entitlements: Option> = None; let mut guild_id: Option> = None; let mut guild_locale: Option = None; let mut id: Option> = None; @@ -262,6 +268,13 @@ impl<'de> Visitor<'de> for InteractionVisitor { data = map.next_value()?; } + InteractionField::Entitlements => { + if entitlements.is_some() { + return Err(DeError::duplicate_field("entitlements")); + } + + entitlements = map.next_value()?; + } InteractionField::GuildId => { if guild_id.is_some() { return Err(DeError::duplicate_field("guild_id")); @@ -374,12 +387,15 @@ impl<'de> Visitor<'de> for InteractionVisitor { } }; + let entitlements = entitlements.unwrap_or_default(); + Ok(Self::Value { app_permissions, application_id, channel, channel_id, data, + entitlements, guild_id, guild_locale, id, @@ -421,7 +437,10 @@ mod tests { Interaction, InteractionData, InteractionDataResolved, InteractionMember, InteractionType, }; use crate::{ - application::command::{CommandOptionType, CommandType}, + application::{ + command::{CommandOptionType, CommandType}, + monetization::{entitlement::Entitlement, EntitlementType}, + }, channel::Channel, guild::{MemberFlags, PartialMember, Permissions}, id::Id, @@ -514,6 +533,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1111, @@ -534,6 +554,18 @@ mod tests { }), target_id: None, }))), + entitlements: vec![Entitlement { + application_id: Id::new(100), + consumed: false, + deleted: false, + ends_at: None, + guild_id: None, + id: Id::new(200), + kind: EntitlementType::ApplicationSubscription, + sku_id: Id::new(300), + starts_at: None, + user_id: None, + }], guild_id: Some(Id::new(400)), guild_locale: Some("de".to_owned()), id: Id::new(500), @@ -554,6 +586,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1111, @@ -581,7 +614,7 @@ mod tests { &[ Token::Struct { name: "Interaction", - len: 12, + len: 13, }, Token::Str("app_permissions"), Token::Some, @@ -672,7 +705,7 @@ mod tests { Token::Str("600"), Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -681,6 +714,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), @@ -699,6 +734,37 @@ mod tests { Token::MapEnd, Token::StructEnd, Token::StructEnd, + Token::Str("entitlements"), + Token::Seq { len: Some(1) }, + Token::Struct { + name: "Entitlement", + len: 10, + }, + Token::Str("application_id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("100"), + Token::Str("consumed"), + Token::Bool(false), + Token::Str("deleted"), + Token::Bool(false), + Token::Str("ends_at"), + Token::None, + Token::Str("guild_id"), + Token::None, + Token::Str("id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("200"), + Token::Str("type"), + Token::U8(8), + Token::Str("sku_id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("300"), + Token::Str("starts_at"), + Token::None, + Token::Str("user_id"), + Token::None, + Token::StructEnd, + Token::SeqEnd, Token::Str("guild_id"), Token::Some, Token::NewtypeStruct { name: "Id" }, @@ -744,7 +810,7 @@ mod tests { Token::Some, Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -753,6 +819,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-model/src/application/interaction/resolved.rs b/twilight-model/src/application/interaction/resolved.rs index cc856a3383d..5511d3bcd86 100644 --- a/twilight-model/src/application/interaction/resolved.rs +++ b/twilight-model/src/application/interaction/resolved.rs @@ -136,6 +136,7 @@ mod tests { id: Id::new(400), proxy_url: "https://proxy.example.com/rainbow_dash.png".to_owned(), size: 13370, + title: None, url: "https://example.com/rainbow_dash.png".to_owned(), waveform: None, width: Some(1337), @@ -180,6 +181,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -195,6 +197,7 @@ mod tests { system: None, verified: None, }, + call: None, channel_id: Id::new(2), components: Vec::new(), content: "ping".to_owned(), @@ -224,6 +227,7 @@ mod tests { mentions: Vec::new(), message_snapshots: Vec::new(), pinned: false, + poll: None, reactions: Vec::new(), reference: None, role_subscription_data: None, @@ -264,6 +268,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -375,7 +380,7 @@ mod tests { Token::Str("4"), Token::Struct { name: "Message", - len: 18, + len: 19, }, Token::Str("attachments"), Token::Seq { len: Some(0) }, @@ -383,7 +388,7 @@ mod tests { Token::Str("author"), Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -392,6 +397,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), @@ -407,6 +414,8 @@ mod tests { Token::Str("username"), Token::Str("test"), Token::StructEnd, + Token::Str("call"), + Token::None, Token::Str("channel_id"), Token::NewtypeStruct { name: "Id" }, Token::Str("2"), @@ -521,7 +530,7 @@ mod tests { Token::Str("300"), Token::Struct { name: "User", - len: 16, + len: 17, }, Token::Str("accent_color"), Token::None, @@ -530,6 +539,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-model/src/application/mod.rs b/twilight-model/src/application/mod.rs index c87d55ae72c..c327b956239 100644 --- a/twilight-model/src/application/mod.rs +++ b/twilight-model/src/application/mod.rs @@ -1,2 +1,3 @@ pub mod command; pub mod interaction; +pub mod monetization; diff --git a/twilight-model/src/application/monetization/entitlement.rs b/twilight-model/src/application/monetization/entitlement.rs new file mode 100644 index 00000000000..6c2be6b706f --- /dev/null +++ b/twilight-model/src/application/monetization/entitlement.rs @@ -0,0 +1,112 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + id::{ + marker::{ApplicationMarker, EntitlementMarker, GuildMarker, SkuMarker, UserMarker}, + Id, + }, + util::Timestamp, +}; + +use super::entitlement_type::EntitlementType; + +/// Entitlements in Discord represent that a user or guild has access to a premium offering in your application. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct Entitlement { + /// ID of the parent application. + pub application_id: Id, + /// Not applicable for App Subscriptions. Subscriptions are not consumed and will be `false` + pub consumed: bool, + /// Entitlement was deleted. + pub deleted: bool, + /// Date at which the entitlement is no longer valid. Not present when using test entitlements. + pub ends_at: Option, + /// ID of the guild that is granted access to the entitlement's sku. + pub guild_id: Option>, + /// ID of the entitlement. + pub id: Id, + /// Type of entitlement. + #[serde(rename = "type")] + pub kind: EntitlementType, + /// ID of the SKU. + pub sku_id: Id, + /// Start date at which the entitlement is valid. Not present when using test entitlements. + pub starts_at: Option, + /// ID of the user that is granted access to the entitlement's sku. + pub user_id: Option>, +} + +#[cfg(test)] +mod tests { + use std::error::Error; + + use serde_test::Token; + + use super::Entitlement; + use crate::application::monetization::entitlement_type::EntitlementType; + use crate::id::Id; + use crate::util::Timestamp; + + #[test] + fn entitlement() -> Result<(), Box> { + let starts_at_str = "2022-09-14T17:00:18.704163+00:00"; + let ends_at_str = "2022-10-14T17:00:21.704163+00:00"; + let starts_at = Timestamp::parse(starts_at_str)?; + let ends_at = Timestamp::parse(ends_at_str)?; + + let value = Entitlement { + application_id: Id::new(1), + consumed: false, + deleted: false, + ends_at: ends_at.into(), + guild_id: Some(Id::new(10)), + id: Id::new(2), + kind: EntitlementType::ApplicationSubscription, + sku_id: Id::new(3), + starts_at: starts_at.into(), + user_id: Some(Id::new(42)), + }; + + serde_test::assert_tokens( + &value, + &[ + Token::Struct { + name: "Entitlement", + len: 10, + }, + Token::Str("application_id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("1"), + Token::Str("consumed"), + Token::Bool(false), + Token::Str("deleted"), + Token::Bool(false), + Token::Str("ends_at"), + Token::Some, + Token::Str(ends_at_str), + Token::Str("guild_id"), + Token::Some, + Token::NewtypeStruct { name: "Id" }, + Token::Str("10"), + Token::Str("id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("2"), + Token::Str("type"), + Token::U8(8), + Token::Str("sku_id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("3"), + Token::Str("starts_at"), + Token::Some, + Token::Str(starts_at_str), + Token::Str("user_id"), + Token::Some, + Token::NewtypeStruct { name: "Id" }, + Token::Str("42"), + Token::StructEnd, + ], + ); + + Ok(()) + } +} diff --git a/twilight-model/src/application/monetization/entitlement_type.rs b/twilight-model/src/application/monetization/entitlement_type.rs new file mode 100644 index 00000000000..c6f77337600 --- /dev/null +++ b/twilight-model/src/application/monetization/entitlement_type.rs @@ -0,0 +1,58 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(from = "u8", into = "u8")] +pub enum EntitlementType { + /// Entitlement was purchased as an app subscription. + ApplicationSubscription, + Unknown(u8), +} + +impl From for EntitlementType { + fn from(value: u8) -> Self { + match value { + 8 => Self::ApplicationSubscription, + other => Self::Unknown(other), + } + } +} + +impl From for u8 { + fn from(value: EntitlementType) -> Self { + match value { + EntitlementType::ApplicationSubscription => 8, + EntitlementType::Unknown(other) => other, + } + } +} + +impl EntitlementType { + pub const fn name(self) -> &'static str { + match self { + Self::ApplicationSubscription => "ApplicationSubscription", + Self::Unknown(_) => "Unknown", + } + } +} + +#[cfg(test)] +mod tests { + use super::EntitlementType; + use serde_test::Token; + + #[test] + fn variants() { + serde_test::assert_tokens(&EntitlementType::ApplicationSubscription, &[Token::U8(8)]); + serde_test::assert_tokens(&EntitlementType::Unknown(99), &[Token::U8(99)]); + } + + #[test] + fn names() { + assert_eq!( + EntitlementType::ApplicationSubscription.name(), + "ApplicationSubscription" + ); + assert_eq!(EntitlementType::Unknown(99).name(), "Unknown"); + } +} diff --git a/twilight-model/src/application/monetization/mod.rs b/twilight-model/src/application/monetization/mod.rs new file mode 100644 index 00000000000..b9582652369 --- /dev/null +++ b/twilight-model/src/application/monetization/mod.rs @@ -0,0 +1,10 @@ +pub mod entitlement; +pub mod entitlement_type; +pub mod sku; +pub mod sku_flags; +pub mod sku_type; + +pub use self::{ + entitlement::Entitlement, entitlement_type::EntitlementType, sku::Sku, sku_flags::SkuFlags, + sku_type::SkuType, +}; diff --git a/twilight-model/src/application/monetization/sku.rs b/twilight-model/src/application/monetization/sku.rs new file mode 100644 index 00000000000..517d7ccd04a --- /dev/null +++ b/twilight-model/src/application/monetization/sku.rs @@ -0,0 +1,75 @@ +use serde::{Deserialize, Serialize}; + +use crate::id::{ + marker::{ApplicationMarker, SkuMarker}, + Id, +}; + +use super::{SkuFlags, SkuType}; + +/// SKUs (stock-keeping units) in Discord represent premium offerings that can be made available to your application's users or guilds. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct Sku { + /// ID of the parent application. + application_id: Id, + /// Flags for the SKU. + flags: SkuFlags, + /// ID of SKU. + id: Id, + /// Type of SKU. + #[serde(rename = "type")] + kind: SkuType, + /// Customer-facing name of your premium offering. + name: String, + /// System-generated URL slug based on the SKU's name. + slug: String, +} + +#[cfg(test)] +mod tests { + use serde_test::Token; + + use crate::{ + application::monetization::{SkuFlags, SkuType}, + id::Id, + }; + + use super::Sku; + + #[test] + fn sku() { + let value = Sku { + application_id: Id::new(1), + flags: SkuFlags::GUILD_SUBSCRIPTION, + id: Id::new(2), + kind: SkuType::Subscription, + name: "a name".to_owned(), + slug: "a-slug".to_owned(), + }; + + serde_test::assert_tokens( + &value, + &[ + Token::Struct { + name: "Sku", + len: 6, + }, + Token::Str("application_id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("1"), + Token::Str("flags"), + Token::U64(1 << 7), + Token::Str("id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("2"), + Token::Str("type"), + Token::U8(5), + Token::Str("name"), + Token::Str("a name"), + Token::Str("slug"), + Token::Str("a-slug"), + Token::StructEnd, + ], + ); + } +} diff --git a/twilight-model/src/application/monetization/sku_flags.rs b/twilight-model/src/application/monetization/sku_flags.rs new file mode 100644 index 00000000000..f07830744fb --- /dev/null +++ b/twilight-model/src/application/monetization/sku_flags.rs @@ -0,0 +1,101 @@ +use bitflags::bitflags; +use serde::{ + de::{Deserialize, Deserializer}, + ser::{Serialize, Serializer}, +}; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct SkuFlags: u64 { + /// SKU is available for purchase. + const AVAILABLE = 1 << 2; + /// A subscription purchased by a user and applied to a single server. + /// Everyone in that server gets your premium benefits. + const GUILD_SUBSCRIPTION = 1 << 7; + /// A subscription purchased by a user for themselves. They get access + /// to your premium benefits in every server. + const USER_SUBSCRIPTION = 1 << 8; + } +} + +impl<'de> Deserialize<'de> for SkuFlags { + fn deserialize>(deserializer: D) -> Result { + Ok(Self::from_bits_truncate(u64::deserialize(deserializer)?)) + } +} + +impl Serialize for SkuFlags { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u64(self.bits()) + } +} + +#[cfg(test)] +mod tests { + use super::SkuFlags; + use serde::{Deserialize, Serialize}; + use serde_test::Token; + use static_assertions::{assert_impl_all, const_assert_eq}; + use std::{ + fmt::{Binary, Debug, LowerHex, Octal, UpperHex}, + hash::Hash, + ops::{ + BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not, Sub, SubAssign, + }, + }; + + assert_impl_all!( + SkuFlags: Binary, + BitAnd, + BitAndAssign, + BitOr, + BitOrAssign, + BitXor, + BitXorAssign, + Clone, + Copy, + Debug, + Deserialize<'static>, + Eq, + Extend, + FromIterator, + Hash, + LowerHex, + Not, + Octal, + PartialEq, + Send, + Serialize, + Sub, + SubAssign, + Sync, + UpperHex + ); + + const_assert_eq!(SkuFlags::AVAILABLE.bits(), 1 << 2); + const_assert_eq!(SkuFlags::GUILD_SUBSCRIPTION.bits(), 1 << 7); + const_assert_eq!(SkuFlags::USER_SUBSCRIPTION.bits(), 1 << 8); + + #[test] + fn serde() { + serde_test::assert_tokens( + &SkuFlags::AVAILABLE, + &[Token::U64(SkuFlags::AVAILABLE.bits())], + ); + + serde_test::assert_tokens( + &SkuFlags::GUILD_SUBSCRIPTION, + &[Token::U64(SkuFlags::GUILD_SUBSCRIPTION.bits())], + ); + + serde_test::assert_tokens( + &SkuFlags::USER_SUBSCRIPTION, + &[Token::U64(SkuFlags::USER_SUBSCRIPTION.bits())], + ); + + serde_test::assert_de_tokens(&SkuFlags::empty(), &[Token::U64(1 << 63)]); + } +} diff --git a/twilight-model/src/application/monetization/sku_type.rs b/twilight-model/src/application/monetization/sku_type.rs new file mode 100644 index 00000000000..5564e2bac74 --- /dev/null +++ b/twilight-model/src/application/monetization/sku_type.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[serde(from = "u8", into = "u8")] +pub enum SkuType { + Subscription, + SubscriptionGroup, + Unknown(u8), +} + +impl From for SkuType { + fn from(value: u8) -> Self { + match value { + 5 => SkuType::Subscription, + 6 => SkuType::SubscriptionGroup, + other => SkuType::Unknown(other), + } + } +} + +impl From for u8 { + fn from(value: SkuType) -> Self { + match value { + SkuType::Subscription => 5, + SkuType::SubscriptionGroup => 6, + SkuType::Unknown(other) => other, + } + } +} + +#[cfg(test)] +mod tests { + use super::SkuType; + use serde_test::Token; + #[test] + fn sku_type() { + serde_test::assert_tokens(&SkuType::Subscription, &[Token::U8(5)]); + serde_test::assert_tokens(&SkuType::SubscriptionGroup, &[Token::U8(6)]); + serde_test::assert_tokens(&SkuType::Unknown(3), &[Token::U8(3)]); + } +} diff --git a/twilight-model/src/channel/attachment.rs b/twilight-model/src/channel/attachment.rs index d548061cc57..83c79e1172c 100644 --- a/twilight-model/src/channel/attachment.rs +++ b/twilight-model/src/channel/attachment.rs @@ -32,6 +32,9 @@ pub struct Attachment { pub id: Id, pub proxy_url: String, pub size: u64, + /// The title of the file. + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, pub url: String, /// Base64 encoded bytearray representing a sampled waveform (currently for voice messages). #[serde(skip_serializing_if = "Option::is_none")] @@ -82,6 +85,7 @@ mod tests { id: Id::new(700_000_000_000_000_000), proxy_url: "https://cdn.example.com/1.png".to_owned(), size: 13_593, + title: Some("a title".to_owned()), url: "https://example.com/1.png".to_owned(), waveform: Some(String::from("waveform")), width: Some(184), @@ -92,7 +96,7 @@ mod tests { &[ Token::Struct { name: "Attachment", - len: 12, + len: 13, }, Token::Str("content_type"), Token::Some, @@ -118,6 +122,9 @@ mod tests { Token::Str("https://cdn.example.com/1.png"), Token::Str("size"), Token::U64(13_593), + Token::Str("title"), + Token::Some, + Token::Str("a title"), Token::Str("url"), Token::Str("https://example.com/1.png"), Token::Str("waveform"), diff --git a/twilight-model/src/channel/message/call.rs b/twilight-model/src/channel/message/call.rs new file mode 100644 index 00000000000..b4ae74c15b1 --- /dev/null +++ b/twilight-model/src/channel/message/call.rs @@ -0,0 +1,16 @@ +use crate::{ + id::{marker::UserMarker, Id}, + util::Timestamp, +}; + +use serde::{Deserialize, Serialize}; + +/// Information about the call in a private channel. +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct MessageCall { + /// The timestamp of when the call ended. + pub ended_timestamp: Option, + /// The IDs of the users that participated in the call. + #[serde(default)] + pub participants: Vec>, +} diff --git a/twilight-model/src/channel/message/component/button.rs b/twilight-model/src/channel/message/component/button.rs index dcdb5ffbc35..932b15be837 100644 --- a/twilight-model/src/channel/message/component/button.rs +++ b/twilight-model/src/channel/message/component/button.rs @@ -1,4 +1,4 @@ -use crate::channel::message::ReactionType; +use crate::channel::message::EmojiReactionType; use serde::{Deserialize, Serialize}; /// Clickable [`Component`] below messages. @@ -20,7 +20,7 @@ pub struct Button { /// Defaults to `false`. pub disabled: bool, /// Visual emoji for clients to display with the button. - pub emoji: Option, + pub emoji: Option, /// Text appearing on the button. pub label: Option, /// Style variant of the button. diff --git a/twilight-model/src/channel/message/component/mod.rs b/twilight-model/src/channel/message/component/mod.rs index dd006dea37b..8c1b22fbca8 100644 --- a/twilight-model/src/channel/message/component/mod.rs +++ b/twilight-model/src/channel/message/component/mod.rs @@ -19,7 +19,7 @@ pub use self::{ text_input::{TextInput, TextInputStyle}, }; -use super::ReactionType; +use super::EmojiReactionType; use crate::channel::ChannelType; use serde::{ de::{Deserializer, Error as DeError, IgnoredAny, MapAccess, Visitor}, @@ -58,7 +58,7 @@ use std::fmt::{Formatter, Result as FmtResult}; /// use twilight_model::{ /// channel::message::{ /// component::{ActionRow, Component, SelectMenu, SelectMenuOption, SelectMenuType}, -/// ReactionType, +/// EmojiReactionType, /// }, /// id::Id, /// }; @@ -75,7 +75,7 @@ use std::fmt::{Formatter, Result as FmtResult}; /// options: Some(Vec::from([ /// SelectMenuOption { /// default: false, -/// emoji: Some(ReactionType::Custom { +/// emoji: Some(EmojiReactionType::Custom { /// animated: false, /// id: Id::new(625891304148303894), /// name: Some("rogue".to_owned()), @@ -86,7 +86,7 @@ use std::fmt::{Formatter, Result as FmtResult}; /// }, /// SelectMenuOption { /// default: false, -/// emoji: Some(ReactionType::Custom { +/// emoji: Some(EmojiReactionType::Custom { /// animated: false, /// id: Id::new(625891304081063986), /// name: Some("mage".to_owned()), @@ -97,7 +97,7 @@ use std::fmt::{Formatter, Result as FmtResult}; /// }, /// SelectMenuOption { /// default: false, -/// emoji: Some(ReactionType::Custom { +/// emoji: Some(EmojiReactionType::Custom { /// animated: false, /// id: Id::new(625891303795982337), /// name: Some("priest".to_owned()), @@ -239,7 +239,7 @@ impl<'de> Visitor<'de> for ComponentVisitor { let mut channel_types: Option> = None; let mut default_values: Option> = None; let mut disabled: Option = None; - let mut emoji: Option> = None; + let mut emoji: Option> = None; let mut max_length: Option> = None; let mut max_values: Option> = None; let mut min_length: Option> = None; @@ -899,7 +899,7 @@ mod tests { let value = Component::Button(Button { custom_id: Some("test".to_owned()), disabled: false, - emoji: Some(ReactionType::Unicode { + emoji: Some(EmojiReactionType::Unicode { name: FLAG.to_owned(), }), label: Some("Test".to_owned()), @@ -922,7 +922,7 @@ mod tests { Token::String("emoji"), Token::Some, Token::Struct { - name: "ReactionType", + name: "EmojiReactionType", len: 1, }, Token::String("name"), diff --git a/twilight-model/src/channel/message/component/select_menu.rs b/twilight-model/src/channel/message/component/select_menu.rs index 330bc45f555..52e5eb1bcaa 100644 --- a/twilight-model/src/channel/message/component/select_menu.rs +++ b/twilight-model/src/channel/message/component/select_menu.rs @@ -1,4 +1,4 @@ -use crate::channel::{message::ReactionType, ChannelType}; +use crate::channel::{message::EmojiReactionType, ChannelType}; use crate::id::marker::{ChannelMarker, RoleMarker, UserMarker}; use crate::id::Id; use serde::{Deserialize, Serialize}; @@ -65,7 +65,7 @@ pub struct SelectMenuOption { /// Emoji associated with the option. Appears left of the label and /// description. #[serde(skip_serializing_if = "Option::is_none")] - pub emoji: Option, + pub emoji: Option, /// User-facing name. pub label: String, /// Developer defined value. diff --git a/twilight-model/src/channel/message/interaction.rs b/twilight-model/src/channel/message/interaction.rs index 4e282256d11..d32abc3b997 100644 --- a/twilight-model/src/channel/message/interaction.rs +++ b/twilight-model/src/channel/message/interaction.rs @@ -64,6 +64,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: Some(image_hash::BANNER), bot: false, discriminator: 1, @@ -128,7 +129,7 @@ mod tests { Token::Str("user"), Token::Struct { name: "User", - len: 16, + len: 17, }, Token::Str("accent_color"), Token::None, @@ -137,6 +138,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::Some, Token::Str(image_hash::BANNER_INPUT), diff --git a/twilight-model/src/channel/message/kind.rs b/twilight-model/src/channel/message/kind.rs index e9be02913d8..f0b460bc0f7 100644 --- a/twilight-model/src/channel/message/kind.rs +++ b/twilight-model/src/channel/message/kind.rs @@ -70,6 +70,14 @@ pub enum MessageType { StageTopic, /// System message denoting a guild application premium subscription. GuildApplicationPremiumSubscription, + /// System message denoting a guild auto moderation incident alerts are enabled. + GuildIncidentAlertModeEnabled, + /// System message denoting a guild auto moderation incident alerts are disabled. + GuildIncidentAlertModeDisabled, + /// System message denoting a guild raid incident report. + GuildIncidentReportRaid, + /// System message denoting a false positive guild raid incident report. + GuildIncidentReportRaidFalseAlarm, /// Variant value is unknown to the library. Unknown(u8), } @@ -111,6 +119,11 @@ impl MessageType { | Self::StageEnd | Self::StageSpeaker | Self::StageTopic + | Self::GuildApplicationPremiumSubscription + | Self::GuildIncidentAlertModeEnabled + | Self::GuildIncidentAlertModeDisabled + | Self::GuildIncidentReportRaid + | Self::GuildIncidentReportRaidFalseAlarm ) } @@ -173,6 +186,10 @@ impl From for MessageType { 29 => Self::StageSpeaker, 31 => Self::StageTopic, 32 => Self::GuildApplicationPremiumSubscription, + 36 => Self::GuildIncidentAlertModeEnabled, + 37 => Self::GuildIncidentAlertModeDisabled, + 38 => Self::GuildIncidentReportRaid, + 39 => Self::GuildIncidentReportRaidFalseAlarm, unknown => Self::Unknown(unknown), } } @@ -212,6 +229,10 @@ impl From for u8 { MessageType::StageSpeaker => 29, MessageType::StageTopic => 31, MessageType::GuildApplicationPremiumSubscription => 32, + MessageType::GuildIncidentAlertModeEnabled => 36, + MessageType::GuildIncidentAlertModeDisabled => 37, + MessageType::GuildIncidentReportRaid => 38, + MessageType::GuildIncidentReportRaidFalseAlarm => 39, MessageType::Unknown(unknown) => unknown, } } @@ -280,7 +301,11 @@ mod tests { (MessageType::StageEnd, 28, true), (MessageType::StageSpeaker, 29, true), (MessageType::StageTopic, 31, true), - (MessageType::GuildApplicationPremiumSubscription, 32, false), + (MessageType::GuildApplicationPremiumSubscription, 32, true), + (MessageType::GuildIncidentAlertModeEnabled, 36, true), + (MessageType::GuildIncidentAlertModeDisabled, 37, true), + (MessageType::GuildIncidentReportRaid, 38, true), + (MessageType::GuildIncidentReportRaidFalseAlarm, 39, true), ]; for (message_type, number, deletable) in MAP { diff --git a/twilight-model/src/channel/message/mod.rs b/twilight-model/src/channel/message/mod.rs index f318816e72c..af85fc179ce 100644 --- a/twilight-model/src/channel/message/mod.rs +++ b/twilight-model/src/channel/message/mod.rs @@ -8,11 +8,13 @@ pub mod sticker; mod activity; mod allowed_mentions; mod application; +mod call; mod flags; mod interaction; mod kind; mod mention; mod reaction; +mod reaction_type; mod reference; mod reference_type; mod role_subscription_data; @@ -22,13 +24,15 @@ pub use self::{ activity::{MessageActivity, MessageActivityType}, allowed_mentions::{AllowedMentions, MentionType}, application::MessageApplication, + call::MessageCall, component::Component, embed::Embed, flags::MessageFlags, interaction::MessageInteraction, kind::MessageType, mention::Mention, - reaction::{Reaction, ReactionCountDetails, ReactionType}, + reaction::{EmojiReactionType, Reaction, ReactionCountDetails}, + reaction_type::ReactionType, reference::MessageReference, reference_type::MessageReferenceType, role_subscription_data::RoleSubscriptionData, @@ -45,6 +49,7 @@ use crate::{ }, Id, }, + poll::Poll, user::User, util::Timestamp, }; @@ -81,6 +86,8 @@ pub struct Message { pub attachments: Vec, /// Author of the message. pub author: User, + /// The call associated with the message. + pub call: Option, /// ID of the [`Channel`] the message was sent in. pub channel_id: Id, /// List of provided components, such as buttons. @@ -166,6 +173,9 @@ pub struct Message { pub message_snapshots: 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, @@ -206,8 +216,8 @@ mod tests { reaction::ReactionCountDetails, reference_type::MessageReferenceType, sticker::{MessageSticker, StickerFormatType}, - Message, MessageActivity, MessageActivityType, MessageApplication, MessageFlags, - MessageReference, MessageType, Reaction, ReactionType, + EmojiReactionType, Message, MessageActivity, MessageActivityType, MessageApplication, + MessageCall, MessageFlags, MessageReference, MessageType, Reaction, }; use crate::{ channel::{ChannelMention, ChannelType}, @@ -236,6 +246,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -251,6 +262,10 @@ mod tests { system: None, verified: None, }, + call: Some(MessageCall { + ended_timestamp: None, + participants: Vec::new(), + }), channel_id: Id::new(2), components: Vec::new(), content: "ping".to_owned(), @@ -280,6 +295,7 @@ mod tests { mentions: Vec::new(), message_snapshots: Vec::new(), pinned: false, + poll: None, reactions: Vec::new(), reference: None, role_subscription_data: None, @@ -300,7 +316,7 @@ mod tests { &[ Token::Struct { name: "Message", - len: 18, + len: 19, }, Token::Str("attachments"), Token::Seq { len: Some(0) }, @@ -308,7 +324,7 @@ mod tests { Token::Str("author"), Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -317,6 +333,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), @@ -332,6 +350,18 @@ mod tests { Token::Str("username"), Token::Str("test"), Token::StructEnd, + Token::Str("call"), + Token::Some, + Token::Struct { + name: "MessageCall", + len: 2, + }, + Token::Str("ended_timestamp"), + Token::None, + Token::Str("participants"), + Token::Seq { len: Some(0) }, + Token::SeqEnd, + Token::StructEnd, Token::Str("channel_id"), Token::NewtypeStruct { name: "Id" }, Token::Str("2"), @@ -440,6 +470,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -455,6 +486,7 @@ mod tests { system: None, verified: None, }, + call: None, channel_id: Id::new(2), components: Vec::new(), content: "ping".to_owned(), @@ -489,6 +521,7 @@ mod tests { mentions: Vec::new(), message_snapshots: Vec::new(), pinned: false, + poll: None, reactions: vec![Reaction { burst_colors: Vec::new(), count: 7, @@ -496,7 +529,7 @@ mod tests { burst: 0, normal: 7, }, - emoji: ReactionType::Unicode { + emoji: EmojiReactionType::Unicode { name: "a".to_owned(), }, me: true, @@ -527,7 +560,7 @@ mod tests { &[ Token::Struct { name: "Message", - len: 25, + len: 26, }, Token::Str("activity"), Token::Some, @@ -568,7 +601,7 @@ mod tests { Token::Str("author"), Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -577,6 +610,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), @@ -592,6 +627,8 @@ mod tests { Token::Str("username"), Token::Str("test"), Token::StructEnd, + Token::Str("call"), + Token::None, Token::Str("channel_id"), Token::NewtypeStruct { name: "Id" }, Token::Str("2"), @@ -692,7 +729,7 @@ mod tests { Token::StructEnd, Token::Str("emoji"), Token::Struct { - name: "ReactionType", + name: "EmojiReactionType", len: 1, }, Token::Str("name"), diff --git a/twilight-model/src/channel/message/reaction.rs b/twilight-model/src/channel/message/reaction.rs index b66809e93c0..477d181b81f 100644 --- a/twilight-model/src/channel/message/reaction.rs +++ b/twilight-model/src/channel/message/reaction.rs @@ -14,17 +14,17 @@ pub struct Reaction { /// Reaction count details for each type of reaction. pub count_details: ReactionCountDetails, /// Emoji of this reaction. - pub emoji: ReactionType, + pub emoji: EmojiReactionType, /// Whether the current user has reacted with this emoji. pub me: bool, /// Whether the current user super-reacted using this emoji pub me_burst: bool, } -/// Type of [`Reaction`]. +/// Type of emoji in a [`Reaction`]. #[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] #[serde(untagged)] -pub enum ReactionType { +pub enum EmojiReactionType { /// Custom [`Emoji`]. /// /// [`Emoji`]: crate::guild::Emoji @@ -66,7 +66,7 @@ pub struct ReactionCountDetails { #[cfg(test)] mod tests { - use super::{Reaction, ReactionCountDetails, ReactionType}; + use super::{EmojiReactionType, Reaction, ReactionCountDetails}; use crate::{id::Id, util::HexColor}; use serde_test::Token; @@ -79,7 +79,7 @@ mod tests { burst: 0, normal: 7, }, - emoji: ReactionType::Unicode { + emoji: EmojiReactionType::Unicode { name: "a".to_owned(), }, me: true, @@ -111,7 +111,7 @@ mod tests { Token::StructEnd, Token::Str("emoji"), Token::Struct { - name: "ReactionType", + name: "EmojiReactionType", len: 1, }, Token::Str("name"), @@ -128,7 +128,7 @@ mod tests { #[test] fn custom() { - let value = ReactionType::Custom { + let value = EmojiReactionType::Custom { animated: false, id: Id::new(1337), name: Some("foo".to_owned()), @@ -138,7 +138,7 @@ mod tests { &value, &[ Token::Struct { - name: "ReactionType", + name: "EmojiReactionType", len: 3, }, Token::Str("animated"), @@ -175,7 +175,7 @@ mod tests { #[test] fn unicode() { - let value = ReactionType::Unicode { + let value = EmojiReactionType::Unicode { name: "\u{1f643}".to_owned(), }; @@ -183,7 +183,7 @@ mod tests { &value, &[ Token::Struct { - name: "ReactionType", + name: "EmojiReactionType", len: 1, }, Token::Str("name"), diff --git a/twilight-model/src/channel/message/reaction_type.rs b/twilight-model/src/channel/message/reaction_type.rs new file mode 100644 index 00000000000..0d5353e8467 --- /dev/null +++ b/twilight-model/src/channel/message/reaction_type.rs @@ -0,0 +1,65 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(from = "u8", into = "u8")] +/// The kind of reaction. +pub enum ReactionType { + /// A non-burst/super reaction. + Normal, + /// A super reaction. + Burst, + /// An unknown reaction type. + Unknown(u8), +} + +impl From for ReactionType { + fn from(value: u8) -> Self { + match value { + 0 => Self::Normal, + 1 => Self::Burst, + unknown => Self::Unknown(unknown), + } + } +} + +impl From for u8 { + fn from(value: ReactionType) -> Self { + match value { + ReactionType::Normal => 0, + ReactionType::Burst => 1, + ReactionType::Unknown(unknown) => unknown, + } + } +} + +impl ReactionType { + /// The name of the reaction type. + pub const fn name(self) -> &'static str { + match self { + Self::Normal => "Normal", + Self::Burst => "Burst", + Self::Unknown(_) => "Unknown", + } + } +} + +#[cfg(test)] +mod tests { + use super::ReactionType; + use serde_test::Token; + + #[test] + fn variants() { + serde_test::assert_tokens(&ReactionType::Normal, &[Token::U8(0)]); + serde_test::assert_tokens(&ReactionType::Burst, &[Token::U8(1)]); + serde_test::assert_tokens(&ReactionType::Unknown(255), &[Token::U8(255)]); + } + + #[test] + fn names() { + assert_eq!(ReactionType::Normal.name(), "Normal"); + assert_eq!(ReactionType::Burst.name(), "Burst"); + assert_eq!(ReactionType::Unknown(255).name(), "Unknown"); + } +} diff --git a/twilight-model/src/channel/message/sticker/mod.rs b/twilight-model/src/channel/message/sticker/mod.rs index 256c20ab587..86ec53f1290 100644 --- a/twilight-model/src/channel/message/sticker/mod.rs +++ b/twilight-model/src/channel/message/sticker/mod.rs @@ -155,6 +155,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -212,7 +213,7 @@ mod tests { Token::Some, Token::Struct { name: "User", - len: 17, + len: 18, }, Token::Str("accent_color"), Token::None, @@ -221,6 +222,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-model/src/channel/permission_overwrite.rs b/twilight-model/src/channel/permission_overwrite.rs index dc1f77ac0bd..f05b8d7c4c5 100644 --- a/twilight-model/src/channel/permission_overwrite.rs +++ b/twilight-model/src/channel/permission_overwrite.rs @@ -5,7 +5,7 @@ use crate::{ use serde::{Deserialize, Serialize}; /// Permission overwrite data for a role or member. -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct PermissionOverwrite { pub allow: Permissions, pub deny: Permissions, diff --git a/twilight-model/src/channel/webhook/mod.rs b/twilight-model/src/channel/webhook/mod.rs index 66cff9e06e4..5a6ed12e573 100644 --- a/twilight-model/src/channel/webhook/mod.rs +++ b/twilight-model/src/channel/webhook/mod.rs @@ -158,6 +158,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -241,7 +242,7 @@ mod tests { Token::Some, Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -249,6 +250,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-model/src/gateway/event/dispatch.rs b/twilight-model/src/gateway/event/dispatch.rs index 1b7ab66604e..cc6b0971525 100644 --- a/twilight-model/src/gateway/event/dispatch.rs +++ b/twilight-model/src/gateway/event/dispatch.rs @@ -15,9 +15,9 @@ use serde::{ #[serde(untagged)] pub enum DispatchEvent { AutoModerationActionExecution(AutoModerationActionExecution), - AutoModerationRuleCreate(Box), - AutoModerationRuleDelete(Box), - AutoModerationRuleUpdate(Box), + AutoModerationRuleCreate(AutoModerationRuleCreate), + AutoModerationRuleDelete(AutoModerationRuleDelete), + AutoModerationRuleUpdate(AutoModerationRuleUpdate), BanAdd(BanAdd), BanRemove(BanRemove), ChannelCreate(Box), @@ -25,6 +25,9 @@ pub enum DispatchEvent { ChannelPinsUpdate(ChannelPinsUpdate), ChannelUpdate(Box), CommandPermissionsUpdate(CommandPermissionsUpdate), + EntitlementCreate(EntitlementCreate), + EntitlementDelete(EntitlementDelete), + EntitlementUpdate(EntitlementUpdate), GuildAuditLogEntryCreate(Box), GuildCreate(Box), GuildDelete(GuildDelete), @@ -50,6 +53,8 @@ pub enum DispatchEvent { MessageCreate(Box), MessageDelete(MessageDelete), MessageDeleteBulk(MessageDeleteBulk), + MessagePollVoteAdd(MessagePollVoteAdd), + MessagePollVoteRemove(MessagePollVoteRemove), MessageUpdate(Box), PresenceUpdate(Box), ReactionAdd(Box), @@ -93,6 +98,9 @@ impl DispatchEvent { Self::ChannelPinsUpdate(_) => EventType::ChannelPinsUpdate, Self::ChannelUpdate(_) => EventType::ChannelUpdate, Self::CommandPermissionsUpdate(_) => EventType::CommandPermissionsUpdate, + Self::EntitlementCreate(_) => EventType::EntitlementCreate, + Self::EntitlementDelete(_) => EventType::EntitlementDelete, + Self::EntitlementUpdate(_) => EventType::EntitlementUpdate, Self::GuildAuditLogEntryCreate(_) => EventType::GuildAuditLogEntryCreate, Self::GuildCreate(_) => EventType::GuildCreate, Self::GuildDelete(_) => EventType::GuildDelete, @@ -118,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, @@ -188,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), @@ -242,15 +254,15 @@ impl<'de, 'a> DeserializeSeed<'de> for DispatchEventWithTypeDeserializer<'a> { "AUTO_MODERATION_ACTION_EXECUTION" => DispatchEvent::AutoModerationActionExecution( AutoModerationActionExecution::deserialize(deserializer)?, ), - "AUTO_MODERATION_RULE_CREATE" => DispatchEvent::AutoModerationRuleCreate(Box::new( + "AUTO_MODERATION_RULE_CREATE" => DispatchEvent::AutoModerationRuleCreate( AutoModerationRuleCreate::deserialize(deserializer)?, - )), - "AUTO_MODERATION_RULE_DELETE" => DispatchEvent::AutoModerationRuleDelete(Box::new( + ), + "AUTO_MODERATION_RULE_DELETE" => DispatchEvent::AutoModerationRuleDelete( AutoModerationRuleDelete::deserialize(deserializer)?, - )), - "AUTO_MODERATION_RULE_UPDATE" => DispatchEvent::AutoModerationRuleUpdate(Box::new( + ), + "AUTO_MODERATION_RULE_UPDATE" => DispatchEvent::AutoModerationRuleUpdate( AutoModerationRuleUpdate::deserialize(deserializer)?, - )), + ), "CHANNEL_CREATE" => { DispatchEvent::ChannelCreate(Box::new(ChannelCreate::deserialize(deserializer)?)) } @@ -266,6 +278,9 @@ impl<'de, 'a> DeserializeSeed<'de> for DispatchEventWithTypeDeserializer<'a> { "APPLICATION_COMMAND_PERMISSIONS_UPDATE" => DispatchEvent::CommandPermissionsUpdate( CommandPermissionsUpdate::deserialize(deserializer)?, ), + "ENTITLEMENT_CREATE" => { + DispatchEvent::EntitlementCreate(EntitlementCreate::deserialize(deserializer)?) + } "GUILD_AUDIT_LOG_ENTRY_CREATE" => DispatchEvent::GuildAuditLogEntryCreate(Box::new( GuildAuditLogEntryCreate::deserialize(deserializer)?, )), @@ -362,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 3173f29177a..5dfeeb77f10 100644 --- a/twilight-model/src/gateway/event/kind.rs +++ b/twilight-model/src/gateway/event/kind.rs @@ -18,6 +18,9 @@ pub enum EventType { ChannelUpdate, #[serde(rename = "APPLICATION_COMMAND_PERMISSIONS_UPDATE")] CommandPermissionsUpdate, + EntitlementCreate, + EntitlementDelete, + EntitlementUpdate, GatewayClose, GatewayHeartbeat, GatewayHeartbeatAck, @@ -53,6 +56,8 @@ pub enum EventType { MessageCreate, MessageDelete, MessageDeleteBulk, + MessagePollVoteAdd, + MessagePollVoteRemove, MessageUpdate, PresenceUpdate, #[serde(rename = "MESSAGE_REACTION_ADD")] @@ -102,6 +107,9 @@ impl EventType { Self::ChannelPinsUpdate => Some("CHANNEL_PINS_UPDATE"), Self::ChannelUpdate => Some("CHANNEL_UPDATE"), Self::CommandPermissionsUpdate => Some("APPLICATION_COMMAND_PERMISSIONS_UPDATE"), + Self::EntitlementCreate => Some("ENTITLEMENT_CREATE"), + Self::EntitlementDelete => Some("ENTITLEMENT_DELETE"), + Self::EntitlementUpdate => Some("ENTITLEMENT_UPDATE"), Self::GuildAuditLogEntryCreate => Some("GUILD_AUDIT_LOG_ENTRY_CREATE"), Self::GuildCreate => Some("GUILD_CREATE"), Self::GuildDelete => Some("GUILD_DELETE"), @@ -128,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"), @@ -180,6 +190,7 @@ impl<'a> TryFrom<&'a str> for EventType { "CHANNEL_PINS_UPDATE" => Ok(Self::ChannelPinsUpdate), "CHANNEL_UPDATE" => Ok(Self::ChannelUpdate), "APPLICATION_COMMAND_PERMISSIONS_UPDATE" => Ok(Self::CommandPermissionsUpdate), + "ENTITLEMENT_CREATE" => Ok(Self::EntitlementCreate), "GUILD_CREATE" => Ok(Self::GuildCreate), "GUILD_DELETE" => Ok(Self::GuildDelete), "GUILD_EMOJIS_UPDATE" => Ok(Self::GuildEmojisUpdate), @@ -204,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), @@ -333,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"); @@ -361,5 +376,6 @@ mod tests { assert_variant(EventType::VoiceServerUpdate, "VOICE_SERVER_UPDATE"); assert_variant(EventType::VoiceStateUpdate, "VOICE_STATE_UPDATE"); assert_variant(EventType::WebhooksUpdate, "WEBHOOKS_UPDATE"); + assert_variant(EventType::EntitlementCreate, "ENTITLEMENT_CREATE"); } } diff --git a/twilight-model/src/gateway/event/mod.rs b/twilight-model/src/gateway/event/mod.rs index 26ef405f755..b57944df45f 100644 --- a/twilight-model/src/gateway/event/mod.rs +++ b/twilight-model/src/gateway/event/mod.rs @@ -27,11 +27,11 @@ pub enum Event { /// Message was blocked by AutoMod according to a rule. AutoModerationActionExecution(AutoModerationActionExecution), /// Sent when an auto moderation rule is created. - AutoModerationRuleCreate(Box), + AutoModerationRuleCreate(AutoModerationRuleCreate), /// Sent when an auto moderation rule is deleted. - AutoModerationRuleDelete(Box), + AutoModerationRuleDelete(AutoModerationRuleDelete), /// Sent when an auto moderation rule is updated. - AutoModerationRuleUpdate(Box), + AutoModerationRuleUpdate(AutoModerationRuleUpdate), /// A user was banned from a guild. BanAdd(BanAdd), /// A user's ban from a guild was removed. @@ -46,6 +46,16 @@ pub enum Event { ChannelUpdate(Box), /// A command's permissions were updated. CommandPermissionsUpdate(CommandPermissionsUpdate), + /// A user subscribes to a SKU. + EntitlementCreate(EntitlementCreate), + /// A user's entitlement is removed. + EntitlementDelete(EntitlementDelete), + /// A user's subscription renews for the + /// next billing period. + /// + /// The `ends_at` field will have an updated value with + /// the new expiration date. + EntitlementUpdate(EntitlementUpdate), /// Close message with an optional frame including information about the /// reason for the close. GatewayClose(Option>), @@ -111,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. @@ -208,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, @@ -231,6 +247,9 @@ impl Event { Event::VoiceStateUpdate(e) => e.0.guild_id, Event::WebhooksUpdate(e) => Some(e.guild_id), Event::GatewayClose(_) + | Event::EntitlementCreate(_) + | Event::EntitlementDelete(_) + | Event::EntitlementUpdate(_) | Event::GatewayHeartbeat(_) | Event::GatewayHeartbeatAck | Event::GatewayHello(_) @@ -255,6 +274,9 @@ impl Event { Self::ChannelPinsUpdate(_) => EventType::ChannelPinsUpdate, Self::ChannelUpdate(_) => EventType::ChannelUpdate, Self::CommandPermissionsUpdate(_) => EventType::CommandPermissionsUpdate, + Self::EntitlementCreate(_) => EventType::EntitlementCreate, + Self::EntitlementDelete(_) => EventType::EntitlementDelete, + Self::EntitlementUpdate(_) => EventType::EntitlementUpdate, Self::GatewayClose(_) => EventType::GatewayClose, Self::GatewayHeartbeat(_) => EventType::GatewayHeartbeat, Self::GatewayHeartbeatAck => EventType::GatewayHeartbeatAck, @@ -286,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, @@ -332,6 +356,9 @@ impl From for Event { DispatchEvent::ChannelPinsUpdate(v) => Self::ChannelPinsUpdate(v), DispatchEvent::ChannelUpdate(v) => Self::ChannelUpdate(v), DispatchEvent::CommandPermissionsUpdate(v) => Self::CommandPermissionsUpdate(v), + DispatchEvent::EntitlementCreate(v) => Self::EntitlementCreate(v), + DispatchEvent::EntitlementDelete(v) => Self::EntitlementDelete(v), + DispatchEvent::EntitlementUpdate(v) => Self::EntitlementUpdate(v), DispatchEvent::GuildAuditLogEntryCreate(v) => Self::GuildAuditLogEntryCreate(v), DispatchEvent::GuildCreate(v) => Self::GuildCreate(v), DispatchEvent::GuildDelete(v) => Self::GuildDelete(v), @@ -362,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), @@ -457,14 +486,11 @@ mod tests { // requires a variable to be used in a function, so this is a false // positive. #[allow(dead_code)] - const EVENT_THRESHOLD: usize = 224; + const EVENT_THRESHOLD: usize = 256; const_assert!(mem::size_of::() == EVENT_THRESHOLD); // Boxed events. - 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); const_assert!(mem::size_of::() > EVENT_THRESHOLD); @@ -491,6 +517,9 @@ mod tests { const_assert!(mem::size_of::() > EVENT_THRESHOLD); // Unboxed. + 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); const_assert!(mem::size_of::() <= EVENT_THRESHOLD); @@ -521,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/entitlement_create.rs b/twilight-model/src/gateway/payload/incoming/entitlement_create.rs new file mode 100644 index 00000000000..ebc45593d53 --- /dev/null +++ b/twilight-model/src/gateway/payload/incoming/entitlement_create.rs @@ -0,0 +1,20 @@ +use crate::application::monetization::Entitlement; +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct EntitlementCreate(pub Entitlement); + +impl Deref for EntitlementCreate { + type Target = Entitlement; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for EntitlementCreate { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/twilight-model/src/gateway/payload/incoming/entitlement_delete.rs b/twilight-model/src/gateway/payload/incoming/entitlement_delete.rs new file mode 100644 index 00000000000..dd4d5ed2c36 --- /dev/null +++ b/twilight-model/src/gateway/payload/incoming/entitlement_delete.rs @@ -0,0 +1,20 @@ +use crate::application::monetization::Entitlement; +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct EntitlementDelete(pub Entitlement); + +impl Deref for EntitlementDelete { + type Target = Entitlement; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for EntitlementDelete { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/twilight-model/src/gateway/payload/incoming/entitlement_update.rs b/twilight-model/src/gateway/payload/incoming/entitlement_update.rs new file mode 100644 index 00000000000..3e180edd901 --- /dev/null +++ b/twilight-model/src/gateway/payload/incoming/entitlement_update.rs @@ -0,0 +1,20 @@ +use crate::application::monetization::Entitlement; +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct EntitlementUpdate(pub Entitlement); + +impl Deref for EntitlementUpdate { + type Target = Entitlement; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for EntitlementUpdate { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/twilight-model/src/gateway/payload/incoming/member_add.rs b/twilight-model/src/gateway/payload/incoming/member_add.rs index 63fc759f81d..a2e4567b18f 100644 --- a/twilight-model/src/gateway/payload/incoming/member_add.rs +++ b/twilight-model/src/gateway/payload/incoming/member_add.rs @@ -60,6 +60,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 987, @@ -105,7 +106,7 @@ mod tests { Token::Str("user"), Token::Struct { name: "User", - len: 8, + len: 9, }, Token::Str("accent_color"), Token::None, @@ -113,6 +114,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-model/src/gateway/payload/incoming/member_chunk.rs b/twilight-model/src/gateway/payload/incoming/member_chunk.rs index 2a3d8118797..e46c2458f4c 100644 --- a/twilight-model/src/gateway/payload/incoming/member_chunk.rs +++ b/twilight-model/src/gateway/payload/incoming/member_chunk.rs @@ -310,6 +310,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: true, discriminator: 1, @@ -341,6 +342,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: true, discriminator: 1, @@ -372,6 +374,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -403,6 +406,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, diff --git a/twilight-model/src/gateway/payload/incoming/member_update.rs b/twilight-model/src/gateway/payload/incoming/member_update.rs index e933ea5c5b9..bdc4f9bdd94 100644 --- a/twilight-model/src/gateway/payload/incoming/member_update.rs +++ b/twilight-model/src/gateway/payload/incoming/member_update.rs @@ -41,6 +41,7 @@ mod tests { use serde_test::Token; #[test] + #[allow(clippy::too_many_lines)] fn member_update() { let joined_at = Some(Timestamp::from_micros(1_488_234_110_121_000).expect("non zero")); let communication_disabled_until = @@ -60,6 +61,7 @@ mod tests { user: User { accent_color: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, name: "Twilight Sparkle".to_string(), public_flags: None, @@ -115,7 +117,7 @@ mod tests { Token::Str("user"), Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -124,6 +126,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), 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 a89f3683421..d2dfac3274a 100644 --- a/twilight-model/src/gateway/payload/incoming/mod.rs +++ b/twilight-model/src/gateway/payload/incoming/mod.rs @@ -25,6 +25,9 @@ mod channel_delete; mod channel_pins_update; mod channel_update; mod command_permissions_update; +mod entitlement_create; +mod entitlement_delete; +mod entitlement_update; mod guild_audit_log_entry_create; mod guild_create; mod guild_delete; @@ -50,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; @@ -82,7 +87,8 @@ pub use self::{ auto_moderation_rule_update::AutoModerationRuleUpdate, ban_add::BanAdd, ban_remove::BanRemove, channel_create::ChannelCreate, channel_delete::ChannelDelete, channel_pins_update::ChannelPinsUpdate, channel_update::ChannelUpdate, - command_permissions_update::CommandPermissionsUpdate, + command_permissions_update::CommandPermissionsUpdate, entitlement_create::EntitlementCreate, + entitlement_delete::EntitlementDelete, entitlement_update::EntitlementUpdate, guild_audit_log_entry_create::GuildAuditLogEntryCreate, guild_create::GuildCreate, guild_delete::GuildDelete, guild_emojis_update::GuildEmojisUpdate, guild_integrations_update::GuildIntegrationsUpdate, @@ -97,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/gateway/payload/incoming/reaction_remove_emoji.rs b/twilight-model/src/gateway/payload/incoming/reaction_remove_emoji.rs index 1d81c28cc90..4703a3022c8 100644 --- a/twilight-model/src/gateway/payload/incoming/reaction_remove_emoji.rs +++ b/twilight-model/src/gateway/payload/incoming/reaction_remove_emoji.rs @@ -1,5 +1,5 @@ use crate::{ - channel::message::ReactionType, + channel::message::EmojiReactionType, id::{ marker::{ChannelMarker, GuildMarker, MessageMarker}, Id, @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct ReactionRemoveEmoji { pub channel_id: Id, - pub emoji: ReactionType, + pub emoji: EmojiReactionType, pub guild_id: Id, pub message_id: Id, } diff --git a/twilight-model/src/gateway/payload/incoming/thread_members_update.rs b/twilight-model/src/gateway/payload/incoming/thread_members_update.rs index 8c040356d33..4e54e59424d 100644 --- a/twilight-model/src/gateway/payload/incoming/thread_members_update.rs +++ b/twilight-model/src/gateway/payload/incoming/thread_members_update.rs @@ -126,6 +126,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -260,6 +261,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-model/src/gateway/payload/incoming/typing_start.rs b/twilight-model/src/gateway/payload/incoming/typing_start.rs index 032ba6433c5..a1d8ff90594 100644 --- a/twilight-model/src/gateway/payload/incoming/typing_start.rs +++ b/twilight-model/src/gateway/payload/incoming/typing_start.rs @@ -56,6 +56,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -119,7 +120,7 @@ mod tests { Token::Str("user"), Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -128,6 +129,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-model/src/gateway/payload/incoming/voice_state_update.rs b/twilight-model/src/gateway/payload/incoming/voice_state_update.rs index 9d74d98b7a1..afff69c7d53 100644 --- a/twilight-model/src/gateway/payload/incoming/voice_state_update.rs +++ b/twilight-model/src/gateway/payload/incoming/voice_state_update.rs @@ -58,6 +58,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 909, @@ -131,7 +132,7 @@ mod tests { Token::Str("user"), Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -139,6 +140,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), @@ -206,6 +209,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 4242, diff --git a/twilight-model/src/gateway/reaction.rs b/twilight-model/src/gateway/reaction.rs index 645a8894dd3..0ef83767a60 100644 --- a/twilight-model/src/gateway/reaction.rs +++ b/twilight-model/src/gateway/reaction.rs @@ -1,17 +1,25 @@ use crate::{ - channel::message::ReactionType, + channel::message::EmojiReactionType, guild::Member, id::{ marker::{ChannelMarker, GuildMarker, MessageMarker, UserMarker}, Id, }, + util::HexColor, }; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct GatewayReaction { + /// True if this is a super-reaction. + pub burst: bool, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + /// Colors used for super-reaction animation in hex format. + /// + /// This is only present when super-reactions are added. + pub burst_colors: Vec, pub channel_id: Id, - pub emoji: ReactionType, + pub emoji: EmojiReactionType, pub guild_id: Option>, pub member: Option, /// ID of the user who authored the message which was reacted to. @@ -25,12 +33,12 @@ pub struct GatewayReaction { mod tests { use super::GatewayReaction; use crate::{ - channel::message::ReactionType, + channel::message::EmojiReactionType, guild::{Member, MemberFlags}, id::Id, test::image_hash, user::User, - util::Timestamp, + util::{HexColor, Timestamp}, }; use serde_test::Token; use std::str::FromStr; @@ -40,10 +48,16 @@ mod tests { fn reaction_with_member() { let joined_at = Some(Timestamp::from_str("2020-01-01T00:00:00.000000+00:00").unwrap()); let flags = MemberFlags::BYPASSES_VERIFICATION | MemberFlags::DID_REJOIN; + let burst_colors = ["#F200FF"]; let value = GatewayReaction { + burst: true, + burst_colors: burst_colors + .iter() + .map(|c| HexColor::from_str(c).unwrap()) + .collect(), channel_id: Id::new(2), - emoji: ReactionType::Unicode { + emoji: EmojiReactionType::Unicode { name: "a".to_owned(), }, guild_id: Some(Id::new(1)), @@ -62,6 +76,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -88,14 +103,20 @@ mod tests { &[ Token::Struct { name: "GatewayReaction", - len: 7, + len: 9, }, + Token::Str("burst"), + Token::Bool(true), + Token::Str("burst_colors"), + Token::Seq { len: Some(1) }, + Token::Str(burst_colors[0]), + Token::SeqEnd, Token::Str("channel_id"), Token::NewtypeStruct { name: "Id" }, Token::Str("2"), Token::Str("emoji"), Token::Struct { - name: "ReactionType", + name: "EmojiReactionType", len: 1, }, Token::Str("name"), @@ -135,7 +156,7 @@ mod tests { Token::Str("user"), Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -144,6 +165,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), @@ -178,8 +201,10 @@ mod tests { #[test] fn reaction_without_member() { let value = GatewayReaction { + burst: false, + burst_colors: Vec::new(), channel_id: Id::new(2), - emoji: ReactionType::Unicode { + emoji: EmojiReactionType::Unicode { name: "a".to_owned(), }, guild_id: None, @@ -194,14 +219,16 @@ mod tests { &[ Token::Struct { name: "GatewayReaction", - len: 7, + len: 8, }, + Token::Str("burst"), + Token::Bool(false), Token::Str("channel_id"), Token::NewtypeStruct { name: "Id" }, Token::Str("2"), Token::Str("emoji"), Token::Struct { - name: "ReactionType", + name: "EmojiReactionType", len: 1, }, Token::Str("name"), diff --git a/twilight-model/src/guild/auto_moderation/action.rs b/twilight-model/src/guild/auto_moderation/action.rs index 5c9fa34b39e..28a8ec0cd03 100644 --- a/twilight-model/src/guild/auto_moderation/action.rs +++ b/twilight-model/src/guild/auto_moderation/action.rs @@ -49,6 +49,8 @@ pub enum AutoModerationActionType { /// [`Keyword`]: super::AutoModerationTriggerType::Keyword /// [`Permissions::MODERATE_MEMBERS`]: crate::guild::Permissions::MODERATE_MEMBERS Timeout, + /// Prevents a member from using text, voice, or other interactions. + BlockMemberInteraction, /// Variant value is unknown to the library. Unknown(u8), } @@ -59,6 +61,7 @@ impl From for AutoModerationActionType { 1 => Self::BlockMessage, 2 => Self::SendAlertMessage, 3 => Self::Timeout, + 4 => Self::BlockMemberInteraction, _ => Self::Unknown(value), } } @@ -70,6 +73,7 @@ impl From for u8 { AutoModerationActionType::BlockMessage => 1, AutoModerationActionType::SendAlertMessage => 2, AutoModerationActionType::Timeout => 3, + AutoModerationActionType::BlockMemberInteraction => 4, AutoModerationActionType::Unknown(unknown) => unknown, } } @@ -128,6 +132,10 @@ mod tests { assert_eq!(1, u8::from(AutoModerationActionType::BlockMessage)); assert_eq!(2, u8::from(AutoModerationActionType::SendAlertMessage)); assert_eq!(3, u8::from(AutoModerationActionType::Timeout)); + assert_eq!( + 4, + u8::from(AutoModerationActionType::BlockMemberInteraction) + ); assert_eq!(250, u8::from(AutoModerationActionType::Unknown(250))); } } diff --git a/twilight-model/src/guild/auto_moderation/event_type.rs b/twilight-model/src/guild/auto_moderation/event_type.rs index 294f3f7bef5..9f6dcdc2e46 100644 --- a/twilight-model/src/guild/auto_moderation/event_type.rs +++ b/twilight-model/src/guild/auto_moderation/event_type.rs @@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize}; pub enum AutoModerationEventType { /// When a member sends or edits a message in a guild. MessageSend, + /// When a member edits their profile. + MemberUpdate, /// Variant value is unknown to the library. Unknown(u8), } @@ -14,6 +16,7 @@ impl From for AutoModerationEventType { fn from(value: u8) -> Self { match value { 1 => Self::MessageSend, + 2 => Self::MemberUpdate, _ => Self::Unknown(value), } } @@ -23,6 +26,7 @@ impl From for u8 { fn from(value: AutoModerationEventType) -> Self { match value { AutoModerationEventType::MessageSend => 1, + AutoModerationEventType::MemberUpdate => 2, AutoModerationEventType::Unknown(unknown) => unknown, } } @@ -51,6 +55,7 @@ mod tests { #[test] fn values() { assert_eq!(1, u8::from(AutoModerationEventType::MessageSend)); + assert_eq!(2, u8::from(AutoModerationEventType::MemberUpdate)); assert_eq!(250, u8::from(AutoModerationEventType::Unknown(250))); } } diff --git a/twilight-model/src/guild/auto_moderation/trigger_type.rs b/twilight-model/src/guild/auto_moderation/trigger_type.rs index 1ba72b9daaa..d20ec196b5f 100644 --- a/twilight-model/src/guild/auto_moderation/trigger_type.rs +++ b/twilight-model/src/guild/auto_moderation/trigger_type.rs @@ -18,6 +18,8 @@ pub enum AutoModerationTriggerType { KeywordPreset, /// Check if content contains more unique mentions than allowed. MentionSpam, + /// Check if member profile contains words from a user defined list of keywords. + MemberProfile, /// Variant value is unknown to the library. Unknown(u8), } @@ -29,6 +31,7 @@ impl From for AutoModerationTriggerType { 3 => Self::Spam, 4 => Self::KeywordPreset, 5 => Self::MentionSpam, + 6 => Self::MemberProfile, _ => Self::Unknown(value), } } @@ -41,6 +44,7 @@ impl From for u8 { AutoModerationTriggerType::Spam => 3, AutoModerationTriggerType::KeywordPreset => 4, AutoModerationTriggerType::MentionSpam => 5, + AutoModerationTriggerType::MemberProfile => 6, AutoModerationTriggerType::Unknown(unknown) => unknown, } } @@ -72,6 +76,7 @@ mod tests { assert_eq!(3, u8::from(AutoModerationTriggerType::Spam)); assert_eq!(4, u8::from(AutoModerationTriggerType::KeywordPreset)); assert_eq!(5, u8::from(AutoModerationTriggerType::MentionSpam)); + assert_eq!(6, u8::from(AutoModerationTriggerType::MemberProfile)); assert_eq!(250, u8::from(AutoModerationTriggerType::Unknown(250))); } } diff --git a/twilight-model/src/guild/ban.rs b/twilight-model/src/guild/ban.rs index 352c483deae..6ef221260bf 100644 --- a/twilight-model/src/guild/ban.rs +++ b/twilight-model/src/guild/ban.rs @@ -21,6 +21,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, diff --git a/twilight-model/src/guild/emoji.rs b/twilight-model/src/guild/emoji.rs index 1322c707d7c..7921e8ef9e0 100644 --- a/twilight-model/src/guild/emoji.rs +++ b/twilight-model/src/guild/emoji.rs @@ -49,6 +49,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -90,7 +91,7 @@ mod tests { Token::Some, Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -98,6 +99,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), @@ -132,6 +135,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -178,7 +182,7 @@ mod tests { Token::Some, Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -186,6 +190,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-model/src/guild/integration.rs b/twilight-model/src/guild/integration.rs index 45a3a4e8c52..81f6f411123 100644 --- a/twilight-model/src/guild/integration.rs +++ b/twilight-model/src/guild/integration.rs @@ -99,6 +99,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: true, discriminator: 1000, @@ -178,7 +179,7 @@ mod tests { Token::Some, Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -187,6 +188,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), @@ -247,6 +250,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: true, discriminator: 1000, @@ -341,7 +345,7 @@ mod tests { Token::Some, Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -350,6 +354,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-model/src/guild/integration_application.rs b/twilight-model/src/guild/integration_application.rs index f37cd4c8085..840dccb0bdb 100644 --- a/twilight-model/src/guild/integration_application.rs +++ b/twilight-model/src/guild/integration_application.rs @@ -60,6 +60,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -92,7 +93,7 @@ mod tests { Token::Some, Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -100,6 +101,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-model/src/guild/invite/mod.rs b/twilight-model/src/guild/invite/mod.rs index c62b099084d..b0d71acf778 100644 --- a/twilight-model/src/guild/invite/mod.rs +++ b/twilight-model/src/guild/invite/mod.rs @@ -197,6 +197,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -219,6 +220,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -355,7 +357,7 @@ mod tests { Token::Some, Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -363,6 +365,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), @@ -391,7 +395,7 @@ mod tests { Token::Some, Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -399,6 +403,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-model/src/guild/member.rs b/twilight-model/src/guild/member.rs index e8a76fecf22..eae0717cf96 100644 --- a/twilight-model/src/guild/member.rs +++ b/twilight-model/src/guild/member.rs @@ -70,6 +70,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -122,7 +123,7 @@ mod tests { Token::Str("user"), Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -130,6 +131,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), @@ -153,6 +156,7 @@ mod tests { } #[test] + #[allow(clippy::too_many_lines)] fn guild_member_communication_disabled_until() -> Result<(), TimestampParseError> { let communication_disabled_until = Timestamp::from_str("2021-12-23T14:29:19.046000+00:00")?; let joined_at = Some(Timestamp::from_str("2015-04-26T06:26:56.936000+00:00")?); @@ -174,6 +178,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -227,7 +232,7 @@ mod tests { Token::Str("user"), Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -235,6 +240,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-model/src/guild/mod.rs b/twilight-model/src/guild/mod.rs index 3a26bc830cf..e1ddf9a5b4a 100644 --- a/twilight-model/src/guild/mod.rs +++ b/twilight-model/src/guild/mod.rs @@ -37,6 +37,7 @@ mod preview; mod prune; mod role; mod role_flags; +mod role_position; mod role_tags; mod system_channel_flags; mod unavailable_guild; @@ -54,9 +55,10 @@ pub use self::{ integration_expire_behavior::IntegrationExpireBehavior, integration_type::GuildIntegrationType, member::Member, member_flags::MemberFlags, mfa_level::MfaLevel, partial_guild::PartialGuild, partial_member::PartialMember, premium_tier::PremiumTier, preview::GuildPreview, - prune::GuildPrune, role::Role, role_flags::RoleFlags, role_tags::RoleTags, - system_channel_flags::SystemChannelFlags, unavailable_guild::UnavailableGuild, - vanity_url::VanityUrl, verification_level::VerificationLevel, widget::GuildWidget, + prune::GuildPrune, role::Role, role_flags::RoleFlags, role_position::RolePosition, + role_tags::RoleTags, system_channel_flags::SystemChannelFlags, + unavailable_guild::UnavailableGuild, vanity_url::VanityUrl, + verification_level::VerificationLevel, widget::GuildWidget, }; use super::gateway::presence::PresenceListDeserializer; diff --git a/twilight-model/src/guild/permissions.rs b/twilight-model/src/guild/permissions.rs index 6be411800fe..d449ee62c45 100644 --- a/twilight-model/src/guild/permissions.rs +++ b/twilight-model/src/guild/permissions.rs @@ -77,6 +77,12 @@ 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. + const USE_EXTERNAL_APPS = 1 << 50; } } @@ -203,6 +209,9 @@ 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] fn serde() { diff --git a/twilight-model/src/guild/role_position.rs b/twilight-model/src/guild/role_position.rs new file mode 100644 index 00000000000..957cf4d8193 --- /dev/null +++ b/twilight-model/src/guild/role_position.rs @@ -0,0 +1,41 @@ +use crate::id::{marker::RoleMarker, Id}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +/// Data used to update the positions of roles. +pub struct RolePosition { + /// Role identifier. + pub id: Id, + /// Sorting position of the role. + pub position: u64, +} + +#[cfg(test)] +mod tests { + use super::{Id, RolePosition}; + use serde_test::Token; + + #[test] + fn role_position() { + let role_position = RolePosition { + id: Id::new(123), + position: 12, + }; + + serde_test::assert_tokens( + &role_position, + &[ + Token::Struct { + name: "RolePosition", + len: 2, + }, + Token::Str("id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("123"), + Token::Str("position"), + Token::U64(12), + Token::StructEnd, + ], + ); + } +} diff --git a/twilight-model/src/guild/template/mod.rs b/twilight-model/src/guild/template/mod.rs index 6c082fddca6..0fe042a4f6c 100644 --- a/twilight-model/src/guild/template/mod.rs +++ b/twilight-model/src/guild/template/mod.rs @@ -183,6 +183,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: None, + avatar_decoration_data: None, banner: Some(image_hash::BANNER), bot: false, email: None, @@ -447,7 +448,7 @@ mod tests { Token::Str("creator"), Token::Struct { name: "User", - len: 10, + len: 11, }, Token::Str("accent_color"), Token::None, @@ -456,6 +457,8 @@ mod tests { Token::Str(image_hash::AVATAR_INPUT), Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::Some, Token::Str(image_hash::BANNER_INPUT), diff --git a/twilight-model/src/http/interaction.rs b/twilight-model/src/http/interaction.rs index ce05c35bbc9..f2357d087c8 100644 --- a/twilight-model/src/http/interaction.rs +++ b/twilight-model/src/http/interaction.rs @@ -100,6 +100,9 @@ pub enum InteractionResponseType { ApplicationCommandAutocompleteResult = 8, /// Respond to an interaction with a popup modal. Modal = 9, + /// Respond to an interaction with an upgrade button, only available + /// for apps with monetization enabled + PremiumRequired = 10, } #[cfg(test)] diff --git a/twilight-model/src/id/marker.rs b/twilight-model/src/id/marker.rs index 9b333908394..172d8e52531 100644 --- a/twilight-model/src/id/marker.rs +++ b/twilight-model/src/id/marker.rs @@ -85,6 +85,24 @@ pub struct CommandVersionMarker; #[non_exhaustive] pub struct EmojiMarker; +/// Marker for entitlement IDs. +/// +/// Types such as [`Entitlement`] use this ID marker. +/// +/// [`Entitlement`]: crate::application::monetization::entitlement::Entitlement +#[derive(Debug)] +#[non_exhaustive] +pub struct EntitlementMarker; + +/// Marker for entitlement SKU IDs. +/// +/// Types such as [`SKU`] use this ID marker. +/// +/// [`SKU`]: crate::application::monetization::sku::SKU +#[derive(Debug)] +#[non_exhaustive] +pub struct SkuMarker; + /// Marker for generic IDs. /// /// Types such as [`AuditLogChange::Id`] or [`CommandOptionValue`] use this @@ -285,3 +303,12 @@ pub struct UserMarker; #[derive(Debug)] #[non_exhaustive] pub struct WebhookMarker; + +/// SKU ID marker for avatar decoration data. +/// +/// Types such as [`AvatarDecorationData`] use this ID marker. +/// +/// [`AvatarDecorationData`]: crate::user::AvatarDecorationData +#[derive(Debug)] +#[non_exhaustive] +pub struct AvatarDecorationDataSkuMarker; diff --git a/twilight-model/src/id/mod.rs b/twilight-model/src/id/mod.rs index fa6bc214612..2d0ecb438c7 100644 --- a/twilight-model/src/id/mod.rs +++ b/twilight-model/src/id/mod.rs @@ -418,9 +418,9 @@ mod tests { use super::{ marker::{ ApplicationMarker, AttachmentMarker, AuditLogEntryMarker, ChannelMarker, CommandMarker, - CommandVersionMarker, EmojiMarker, GenericMarker, GuildMarker, IntegrationMarker, - InteractionMarker, MessageMarker, RoleMarker, RoleSubscriptionSkuMarker, StageMarker, - UserMarker, WebhookMarker, + CommandVersionMarker, EmojiMarker, EntitlementMarker, GenericMarker, GuildMarker, + IntegrationMarker, InteractionMarker, MessageMarker, RoleMarker, + RoleSubscriptionSkuMarker, SkuMarker, StageMarker, UserMarker, WebhookMarker, }, Id, }; @@ -443,6 +443,8 @@ mod tests { assert_impl_all!(CommandMarker: Debug, Send, Sync); assert_impl_all!(CommandVersionMarker: Debug, Send, Sync); assert_impl_all!(EmojiMarker: Debug, Send, Sync); + assert_impl_all!(EntitlementMarker: Debug, Send, Sync); + assert_impl_all!(SkuMarker: Debug, Send, Sync); assert_impl_all!(GenericMarker: Debug, Send, Sync); assert_impl_all!(GuildMarker: Debug, Send, Sync); assert_impl_all!(IntegrationMarker: Debug, Send, Sync); @@ -661,6 +663,34 @@ mod tests { Token::U64(114_941_315_417_899_012), ], ); + serde_test::assert_tokens( + &Id::::new(114_941_315_417_899_012), + &[ + Token::NewtypeStruct { name: "Id" }, + Token::Str("114941315417899012"), + ], + ); + serde_test::assert_de_tokens( + &Id::::new(114_941_315_417_899_012), + &[ + Token::NewtypeStruct { name: "Id" }, + Token::Str("114941315417899012"), + ], + ); + serde_test::assert_tokens( + &Id::::new(114_941_315_417_899_012), + &[ + Token::NewtypeStruct { name: "Id" }, + Token::Str("114941315417899012"), + ], + ); + serde_test::assert_de_tokens( + &Id::::new(114_941_315_417_899_012), + &[ + Token::NewtypeStruct { name: "Id" }, + Token::Str("114941315417899012"), + ], + ); serde_test::assert_tokens( &Id::::new(114_941_315_417_899_012), &[ 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/oauth/application.rs b/twilight-model/src/oauth/application.rs index 1d1067af35c..441ffc70011 100644 --- a/twilight-model/src/oauth/application.rs +++ b/twilight-model/src/oauth/application.rs @@ -134,6 +134,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -213,7 +214,7 @@ mod tests { Token::Some, Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -221,6 +222,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-model/src/oauth/team/member.rs b/twilight-model/src/oauth/team/member.rs index bc8f3c1c95d..28f0bdeec15 100644 --- a/twilight-model/src/oauth/team/member.rs +++ b/twilight-model/src/oauth/team/member.rs @@ -29,6 +29,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -65,7 +66,7 @@ mod tests { Token::Str("user"), Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -73,6 +74,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), 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-model/src/user/avatar_decoration_data.rs b/twilight-model/src/user/avatar_decoration_data.rs new file mode 100644 index 00000000000..c502534ef69 --- /dev/null +++ b/twilight-model/src/user/avatar_decoration_data.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + id::{marker::AvatarDecorationDataSkuMarker, Id}, + util::ImageHash, +}; + +/// The data for the user's avatar decoration. +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct AvatarDecorationData { + /// The avatar decoration hash. + pub asset: ImageHash, + /// ID of the avatar decoration's SKU. + pub sku_id: Id, +} + +#[cfg(test)] +mod tests { + use super::{AvatarDecorationData, Id, ImageHash}; + use serde_test::Token; + + #[test] + fn test_avatar_decoration_data() { + let hash = "b2a6536641da91a0b59bd66557c56c36"; + let value = AvatarDecorationData { + asset: ImageHash::parse(hash.as_bytes()).unwrap(), + sku_id: Id::new(1), + }; + + serde_test::assert_tokens( + &value, + &[ + Token::Struct { + name: "AvatarDecorationData", + len: 2, + }, + Token::Str("asset"), + Token::Str(hash), + Token::Str("sku_id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("1"), + Token::StructEnd, + ], + ); + } +} diff --git a/twilight-model/src/user/mod.rs b/twilight-model/src/user/mod.rs index c5e22e789be..45a4bd25758 100644 --- a/twilight-model/src/user/mod.rs +++ b/twilight-model/src/user/mod.rs @@ -1,3 +1,4 @@ +mod avatar_decoration_data; mod connection; mod connection_visibility; mod current_user; @@ -6,7 +7,8 @@ mod flags; mod premium_type; pub use self::{ - connection::Connection, connection_visibility::ConnectionVisibility, current_user::CurrentUser, + avatar_decoration_data::AvatarDecorationData, connection::Connection, + connection_visibility::ConnectionVisibility, current_user::CurrentUser, current_user_guild::CurrentUserGuild, flags::UserFlags, premium_type::PremiumType, }; @@ -125,6 +127,8 @@ pub struct User { pub avatar: Option, /// Hash of the user's avatar decoration. pub avatar_decoration: Option, + /// Data for the user's avatar decoration. + pub avatar_decoration_data: Option, /// Hash of the user's banner image. pub banner: Option, #[serde(default)] @@ -207,7 +211,7 @@ mod tests { vec![ Token::Struct { name: "User", - len: 16, + len: 17, }, Token::Str("accent_color"), Token::None, @@ -217,6 +221,8 @@ mod tests { Token::Str("avatar_decoration"), Token::Some, Token::Str(image_hash::AVATAR_DECORATION_INPUT), + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::Some, Token::Str(image_hash::BANNER_INPUT), @@ -261,7 +267,7 @@ mod tests { vec![ Token::Struct { name: "User", - len: 17, + len: 18, }, Token::Str("accent_color"), Token::None, @@ -271,6 +277,8 @@ mod tests { Token::Str("avatar_decoration"), Token::Some, Token::Str(image_hash::AVATAR_DECORATION_INPUT), + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::Some, Token::Str(image_hash::BANNER_INPUT), @@ -330,6 +338,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: Some(image_hash::AVATAR_DECORATION), + avatar_decoration_data: None, banner: Some(image_hash::BANNER), bot: false, discriminator: 1, @@ -362,6 +371,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: Some(image_hash::AVATAR_DECORATION), + avatar_decoration_data: None, banner: Some(image_hash::BANNER), bot: false, discriminator: 0, @@ -391,6 +401,7 @@ mod tests { accent_color: None, avatar: Some(image_hash::AVATAR), avatar_decoration: Some(image_hash::AVATAR_DECORATION), + avatar_decoration_data: None, banner: Some(image_hash::BANNER), bot: false, discriminator: 1, diff --git a/twilight-model/src/util/hex_color.rs b/twilight-model/src/util/hex_color.rs index fb66b0ed6b9..57cd86a1983 100644 --- a/twilight-model/src/util/hex_color.rs +++ b/twilight-model/src/util/hex_color.rs @@ -29,6 +29,7 @@ impl Serialize for HexColor { } } +#[derive(Debug)] pub enum HexColorParseError { InvalidLength, InvalidFormat, diff --git a/twilight-model/src/voice/voice_state.rs b/twilight-model/src/voice/voice_state.rs index ec22d3da16c..a15aa28e003 100644 --- a/twilight-model/src/voice/voice_state.rs +++ b/twilight-model/src/voice/voice_state.rs @@ -152,6 +152,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -227,7 +228,7 @@ mod tests { Token::Str("user"), Token::Struct { name: "User", - len: 9, + len: 10, }, Token::Str("accent_color"), Token::None, @@ -235,6 +236,8 @@ mod tests { Token::None, Token::Str("avatar_decoration"), Token::None, + Token::Str("avatar_decoration_data"), + Token::None, Token::Str("banner"), Token::None, Token::Str("bot"), diff --git a/twilight-standby/src/lib.rs b/twilight-standby/src/lib.rs index f1323853cf3..4fef66fa384 100644 --- a/twilight-standby/src/lib.rs +++ b/twilight-standby/src/lib.rs @@ -582,7 +582,7 @@ impl Standby { /// # async fn main() -> Result<(), Box> { /// use tokio_stream::StreamExt; /// use twilight_model::{ - /// channel::message::ReactionType, + /// channel::message::EmojiReactionType, /// gateway::payload::incoming::ReactionAdd, /// id::Id, /// }; @@ -593,7 +593,7 @@ impl Standby { /// let message_id = Id::new(123); /// /// let mut reactions = standby.wait_for_reaction_stream(message_id, |event: &ReactionAdd| { - /// matches!(&event.emoji, ReactionType::Unicode { name } if name == "🀠") + /// matches!(&event.emoji, EmojiReactionType::Unicode { name } if name == "🀠") /// }); /// /// while let Some(reaction) = reactions.next().await { @@ -1066,7 +1066,7 @@ mod tests { InteractionType, }, channel::{ - message::{component::ComponentType, Message, MessageType, ReactionType}, + message::{component::ComponentType, EmojiReactionType, Message, MessageType}, Channel, ChannelType, }, gateway::{ @@ -1092,6 +1092,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, @@ -1107,6 +1108,7 @@ mod tests { system: None, verified: None, }, + call: None, channel_id: Id::new(1), components: Vec::new(), content: "test".to_owned(), @@ -1124,6 +1126,7 @@ mod tests { mentions: Vec::new(), message_snapshots: Vec::new(), pinned: false, + poll: None, reactions: Vec::new(), reference: None, referenced_message: None, @@ -1138,8 +1141,10 @@ mod tests { fn reaction() -> GatewayReaction { GatewayReaction { + burst: false, + burst_colors: Vec::new(), channel_id: Id::new(2), - emoji: ReactionType::Unicode { + emoji: EmojiReactionType::Unicode { name: "🍎".to_owned(), }, guild_id: Some(Id::new(1)), @@ -1201,6 +1206,7 @@ mod tests { values: Vec::new(), }, ))), + entitlements: Vec::new(), guild_id: Some(Id::new(3)), guild_locale: None, id: Id::new(4), @@ -1213,6 +1219,7 @@ mod tests { accent_color: None, avatar: None, avatar_decoration: None, + avatar_decoration_data: None, banner: None, bot: false, discriminator: 1, diff --git a/twilight-validate/src/component.rs b/twilight-validate/src/component.rs index 03e1acc7096..3be666cf902 100644 --- a/twilight-validate/src/component.rs +++ b/twilight-validate/src/component.rs @@ -1190,7 +1190,7 @@ fn component_text_input_value(value: impl AsRef) -> Result<(), ComponentVal mod tests { use super::*; use static_assertions::{assert_fields, assert_impl_all}; - use twilight_model::channel::message::ReactionType; + use twilight_model::channel::message::EmojiReactionType; assert_fields!(ComponentValidationErrorType::ActionRowComponentCount: count); assert_fields!(ComponentValidationErrorType::ComponentCount: count); @@ -1221,7 +1221,7 @@ mod tests { let button = Button { custom_id: None, disabled: false, - emoji: Some(ReactionType::Unicode { + emoji: Some(EmojiReactionType::Unicode { name: "πŸ“š".into() }), label: Some("Read".into()), diff --git a/twilight-validate/src/request.rs b/twilight-validate/src/request.rs index f6baf1bbcda..e6c54dec620 100644 --- a/twilight-validate/src/request.rs +++ b/twilight-validate/src/request.rs @@ -71,6 +71,12 @@ pub const GET_CURRENT_USER_GUILDS_LIMIT_MAX: u16 = 200; /// Minimum amount of guilds to get. pub const GET_CURRENT_USER_GUILDS_LIMIT_MIN: u16 = 1; +/// Maximum amount of entitlements to get. +pub const GET_ENTITLEMENTS_LIMIT_MAX: u8 = 100; + +/// Minimum amount of entitlements to get. +pub const GET_ENTITLEMENTS_LIMIT_MIN: u8 = 1; + /// Maximum amount of audit log entries to list. pub const GET_GUILD_AUDIT_LOG_LIMIT_MAX: u16 = 100; @@ -344,6 +350,15 @@ impl Display for ValidationError { Display::fmt(&GET_CURRENT_USER_GUILDS_LIMIT_MAX, f) } + ValidationErrorType::GetEntitlements { limit } => { + f.write_str("provided get entitlements limit is ")?; + Display::fmt(limit, f)?; + f.write_str(", but it must be at least ")?; + Display::fmt(&GET_ENTITLEMENTS_LIMIT_MIN, f)?; + f.write_str(" and at most ")?; + + Display::fmt(&GET_ENTITLEMENTS_LIMIT_MAX, f) + } ValidationErrorType::GetGuildAuditLog { limit } => { f.write_str("provided get guild audit log limit is ")?; Display::fmt(limit, f)?; @@ -611,6 +626,11 @@ pub enum ValidationErrorType { /// Invalid limit. limit: u16, }, + /// Provided get entitlements limit was invalid. + GetEntitlements { + /// Invalid limit. + limit: u8, + }, /// Provided get guild audit log limit was invalid. GetGuildAuditLog { /// Invalid limit. @@ -1198,6 +1218,28 @@ pub const fn get_current_user_guilds_limit(limit: u16) -> Result<(), ValidationE } } +/// Ensure that the limit for the Get Entitlements endpoint is correct. +/// +/// The limit must be at least [`GET_ENTITLEMENTS_LIMIT_MIN`] and at most +/// [`GET_ENTITLEMENTS_LIMIT_MAX`]. This is based on +/// [this documentation entry]. +/// +/// # Errors +/// +/// Returns an error of type [`GetEntitlements`] if the limit is invalid. +/// +/// [`GetEntitlements`]: ValidationErrorType::GetEntitlements +/// [this documentation entry]: https://discord.com/developers/docs/monetization/entitlements#list-entitlements +pub const fn get_entitlements_limit(limit: u8) -> Result<(), ValidationError> { + if limit >= GET_ENTITLEMENTS_LIMIT_MIN && limit <= GET_ENTITLEMENTS_LIMIT_MAX { + Ok(()) + } else { + Err(ValidationError { + kind: ValidationErrorType::GetEntitlements { limit }, + }) + } +} + /// Ensure that the limit for the Get Guild Audit Log endpoint is correct. /// /// The limit must be at least [`GET_GUILD_AUDIT_LOG_LIMIT_MIN`] and at most