From 33c4dcf75831f9ca63e6c6878953bd1c4dd6b63f Mon Sep 17 00:00:00 2001 From: Jens Reidel Date: Fri, 15 Sep 2023 09:37:42 +0200 Subject: [PATCH 1/6] build(examples): Update ed25519-dalek (#2277) Signed-off-by: Jens Reidel --- examples/Cargo.toml | 2 +- examples/model-webhook-slash.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/Cargo.toml b/examples/Cargo.toml index a0a7502cf02..b72ab7f6a71 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,7 +7,7 @@ version = "0.0.0" [dev-dependencies] anyhow = { default-features = false, features = ["std"], version = "1" } -ed25519-dalek = "1" +ed25519-dalek = "2" futures-util = { default-features = false, version = "0.3" } hex = "0.4" hyper = { features = ["client", "server", "http2", "runtime"], version = "0.14" } diff --git a/examples/model-webhook-slash.rs b/examples/model-webhook-slash.rs index 08f0d1fbf89..a4783d0e2ed 100644 --- a/examples/model-webhook-slash.rs +++ b/examples/model-webhook-slash.rs @@ -1,4 +1,4 @@ -use ed25519_dalek::{PublicKey, Verifier, PUBLIC_KEY_LENGTH}; +use ed25519_dalek::{Verifier, VerifyingKey, PUBLIC_KEY_LENGTH}; use hex::FromHex; use hyper::{ header::CONTENT_TYPE, @@ -16,8 +16,8 @@ use twilight_model::{ }; /// Public key given from Discord. -static PUB_KEY: Lazy = Lazy::new(|| { - PublicKey::from_bytes(&<[u8; PUBLIC_KEY_LENGTH] as FromHex>::from_hex("PUBLIC_KEY").unwrap()) +static PUB_KEY: Lazy = Lazy::new(|| { + VerifyingKey::from_bytes(&<[u8; PUBLIC_KEY_LENGTH] as FromHex>::from_hex("PUBLIC_KEY").unwrap()) .unwrap() }); From 5f774bafa8fb7bed0213d8c535c36f5fe1f19898 Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Thu, 21 Sep 2023 19:08:50 -0400 Subject: [PATCH 2/6] fix(http): Use `default_message_seconds` as a json field for `create_ban` (#2280) --- .../src/request/guild/ban/create_ban.rs | 9 ++++-- twilight-http/src/routing.rs | 31 +++---------------- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/twilight-http/src/request/guild/ban/create_ban.rs b/twilight-http/src/request/guild/ban/create_ban.rs index bfa6ccb2641..b0d65c8df8d 100644 --- a/twilight-http/src/request/guild/ban/create_ban.rs +++ b/twilight-http/src/request/guild/ban/create_ban.rs @@ -5,6 +5,7 @@ use crate::{ response::{marker::EmptyBody, Response, ResponseFuture}, routing::Route, }; +use serde::Serialize; use std::future::IntoFuture; use twilight_model::id::{ marker::{GuildMarker, UserMarker}, @@ -16,7 +17,9 @@ use twilight_validate::request::{ ValidationError, }; +#[derive(Serialize)] struct CreateBanFields { + /// Number of seconds to delete messages for, between `0` and `604800`. delete_message_seconds: Option, } @@ -127,11 +130,12 @@ impl IntoFuture for CreateBan<'_> { impl TryIntoRequest for CreateBan<'_> { fn try_into_request(self) -> Result { let mut request = Request::builder(&Route::CreateBan { - delete_message_seconds: self.fields.delete_message_seconds, guild_id: self.guild_id.get(), user_id: self.user_id.get(), }); + request = request.json(&self.fields)?; + if let Some(reason) = self.reason.as_ref() { let header = request::audit_header(reason)?; @@ -165,10 +169,11 @@ mod tests { let client = Client::new(String::new()); let request = client .create_ban(GUILD_ID, USER_ID) + .delete_message_seconds(100)? .reason(REASON)? .try_into_request()?; - assert!(request.body().is_none()); + assert!(request.body().is_some()); assert!(request.form().is_none()); assert_eq!(Method::Put, request.method()); diff --git a/twilight-http/src/routing.rs b/twilight-http/src/routing.rs index ce7fa8df8af..82466e57b45 100644 --- a/twilight-http/src/routing.rs +++ b/twilight-http/src/routing.rs @@ -33,9 +33,6 @@ pub enum Route<'a> { }, /// Route information to create a ban on a user in a guild. CreateBan { - /// The number of seconds' worth of the user's messages to delete in the - /// guild's channels. - delete_message_seconds: Option, /// The ID of the guild. guild_id: u64, /// The ID of the user. @@ -1695,24 +1692,6 @@ impl Display for Route<'_> { f.write_str("/auto-moderation/rules") } - Route::CreateBan { - guild_id, - delete_message_seconds, - user_id, - } => { - f.write_str("guilds/")?; - Display::fmt(guild_id, f)?; - f.write_str("/bans/")?; - Display::fmt(user_id, f)?; - f.write_str("?")?; - - if let Some(delete_message_seconds) = delete_message_seconds { - f.write_str("delete_message_seconds=")?; - Display::fmt(delete_message_seconds, f)?; - } - - Ok(()) - } Route::CreateChannel { guild_id } | Route::GetChannels { guild_id } | Route::UpdateGuildChannels { guild_id } => { @@ -1955,7 +1934,9 @@ impl Display for Route<'_> { f.write_str("/crosspost") } - Route::DeleteBan { guild_id, user_id } | Route::GetBan { guild_id, user_id } => { + Route::DeleteBan { guild_id, user_id } + | Route::GetBan { guild_id, user_id } + | Route::CreateBan { guild_id, user_id } => { f.write_str("guilds/")?; Display::fmt(guild_id, f)?; f.write_str("/bans/")?; @@ -4346,22 +4327,20 @@ mod tests { fn create_ban() { let mut route = Route::CreateBan { guild_id: GUILD_ID, - delete_message_seconds: None, user_id: USER_ID, }; assert_eq!( route.to_string(), - format!("guilds/{GUILD_ID}/bans/{USER_ID}?") + format!("guilds/{GUILD_ID}/bans/{USER_ID}") ); route = Route::CreateBan { guild_id: GUILD_ID, - delete_message_seconds: Some(259_200), user_id: USER_ID, }; assert_eq!( route.to_string(), - format!("guilds/{GUILD_ID}/bans/{USER_ID}?delete_message_seconds=259200") + format!("guilds/{GUILD_ID}/bans/{USER_ID}") ); } From bebc7f16b048d19194416fe2faad4e6d6b8a6738 Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Thu, 21 Sep 2023 19:09:41 -0400 Subject: [PATCH 3/6] feat(model): Add support for guild media channels (#2222) --- twilight-model/src/channel/channel_type.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/twilight-model/src/channel/channel_type.rs b/twilight-model/src/channel/channel_type.rs index a77aab2810c..ec57aa1f55b 100644 --- a/twilight-model/src/channel/channel_type.rs +++ b/twilight-model/src/channel/channel_type.rs @@ -20,6 +20,12 @@ pub enum ChannelType { GuildDirectory, /// Channel that can only contain threads. GuildForum, + /// Channel the can only contain threads with media content. + /// + /// See the [help center article] for more information. + /// + /// [help center article]: https://creator-support.discord.com/hc/en-us/articles/14346342766743 + GuildMedia, Unknown(u8), } @@ -38,6 +44,7 @@ impl From for ChannelType { 13 => ChannelType::GuildStageVoice, 14 => ChannelType::GuildDirectory, 15 => ChannelType::GuildForum, + 16 => ChannelType::GuildMedia, unknown => ChannelType::Unknown(unknown), } } @@ -58,6 +65,7 @@ impl From for u8 { ChannelType::GuildStageVoice => 13, ChannelType::GuildDirectory => 14, ChannelType::GuildForum => 15, + ChannelType::GuildMedia => 16, ChannelType::Unknown(unknown) => unknown, } } @@ -77,6 +85,7 @@ impl ChannelType { /// - [`GuildVoice`][`Self::GuildVoice`] /// - [`PublicThread`][`Self::PublicThread`] /// - [`PrivateThread`][`Self::PrivateThread`] + /// - [`GuildMedia`][`Self::GuildMedia`] pub const fn is_guild(self) -> bool { matches!( self, @@ -89,6 +98,7 @@ impl ChannelType { | Self::GuildStageVoice | Self::GuildText | Self::GuildVoice + | Self::GuildMedia ) } @@ -121,6 +131,7 @@ impl ChannelType { Self::Private => "Private", Self::PrivateThread => "PrivateThread", Self::PublicThread => "PublicThread", + Self::GuildMedia => "GuildMedia", Self::Unknown(_) => "Unknown", } } @@ -141,6 +152,7 @@ mod tests { const_assert!(ChannelType::GuildStageVoice.is_guild()); const_assert!(ChannelType::GuildText.is_guild()); const_assert!(ChannelType::GuildVoice.is_guild()); + const_assert!(ChannelType::GuildMedia.is_guild()); const_assert!(ChannelType::AnnouncementThread.is_thread()); const_assert!(ChannelType::PublicThread.is_thread()); @@ -159,6 +171,8 @@ mod tests { serde_test::assert_tokens(&ChannelType::PrivateThread, &[Token::U8(12)]); serde_test::assert_tokens(&ChannelType::GuildStageVoice, &[Token::U8(13)]); serde_test::assert_tokens(&ChannelType::GuildDirectory, &[Token::U8(14)]); + serde_test::assert_tokens(&ChannelType::GuildForum, &[Token::U8(15)]); + serde_test::assert_tokens(&ChannelType::GuildMedia, &[Token::U8(16)]); serde_test::assert_tokens(&ChannelType::Unknown(99), &[Token::U8(99)]); } @@ -175,6 +189,7 @@ mod tests { assert_eq!("Private", ChannelType::Private.name()); assert_eq!("PrivateThread", ChannelType::PrivateThread.name()); assert_eq!("PublicThread", ChannelType::PublicThread.name()); + assert_eq!("GuildMedia", ChannelType::GuildMedia.name()); assert_eq!("Unknown", ChannelType::Unknown(99).name()); } } From 00600faf3d3e441393288736378a6bd736a73d26 Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Sat, 7 Oct 2023 10:27:33 -0400 Subject: [PATCH 4/6] fix: new rust CI errors (#2285) - Because the CI seems to be using the latest rust version, a redundant `deref()` was removed as the compiler gave a warning. - The CI for spell checks seems to have an updated dictionary. The new spelling errors found have been corrected in this PR. - Clippy was throwing an error for inconsistent `PartialOrd` implementations. This was also fixed. --- examples/gateway-reshard.rs | 2 +- twilight-cache-inmemory/src/lib.rs | 2 +- twilight-gateway/src/config.rs | 2 +- twilight-http/CHANGELOG.md | 4 ++-- twilight-mention/src/timestamp.rs | 2 +- twilight-model/src/id/mod.rs | 2 +- twilight-standby/src/lib.rs | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/gateway-reshard.rs b/examples/gateway-reshard.rs index 0c571e523e2..c23061c5000 100644 --- a/examples/gateway-reshard.rs +++ b/examples/gateway-reshard.rs @@ -28,7 +28,7 @@ async fn main() -> anyhow::Result<()> { // Run `gateway_runner` and `reshard` concurrently until the first one // finishes. tokio::select! { - // Gateway_runner only finises on errors, so break the loop and exit + // Gateway_runner only finishes on errors, so break the loop and exit // the program. _ = gateway_runner(Arc::clone(&client), shards) => break, // Resharding complete! Time to run `gateway_runner` with the new diff --git a/twilight-cache-inmemory/src/lib.rs b/twilight-cache-inmemory/src/lib.rs index 981a13eab87..6f163360a83 100644 --- a/twilight-cache-inmemory/src/lib.rs +++ b/twilight-cache-inmemory/src/lib.rs @@ -861,7 +861,7 @@ impl UpdateCache for Event { Event::GuildStickersUpdate(v) => c.update(v), Event::GuildUpdate(v) => c.update(v.deref()), Event::IntegrationCreate(v) => c.update(v.deref()), - Event::IntegrationDelete(v) => c.update(v.deref()), + Event::IntegrationDelete(v) => c.update(v), Event::IntegrationUpdate(v) => c.update(v.deref()), Event::InteractionCreate(v) => c.update(v.deref()), Event::MemberAdd(v) => c.update(v.deref()), diff --git a/twilight-gateway/src/config.rs b/twilight-gateway/src/config.rs index 7db1acff5a8..538b9f5e1a9 100644 --- a/twilight-gateway/src/config.rs +++ b/twilight-gateway/src/config.rs @@ -64,7 +64,7 @@ pub struct Config { /// Session information to resume a shard on initialization. session: Option, /// TLS connector for Websocket connections. - // We need this to be public so [`stream`] can re-use TLS on multiple shards + // We need this to be public so [`stream`] can reuse TLS on multiple shards // if unconfigured. tls: TlsContainer, /// Token used to authenticate when identifying with the gateway. diff --git a/twilight-http/CHANGELOG.md b/twilight-http/CHANGELOG.md index 52790cfc697..9879da5426d 100644 --- a/twilight-http/CHANGELOG.md +++ b/twilight-http/CHANGELOG.md @@ -1581,7 +1581,7 @@ Replace references to `Path::WebhooksIdTokenMessageId` with `CreateInvite::{max_age, max_uses}` now return validation errors, so the results returned from them need to be handled. -Don't re-use `hyper` clients via the builder. If you need to configure the +Don't reuse `hyper` clients via the builder. If you need to configure the underlying `hyper` client please create an issue with the reason why. Errors are no longer enums and don't expose their concrete underlying error @@ -1632,7 +1632,7 @@ Return validation errors for `CreateInvite::max_age` and Remove ability to get current user's DM channels ([#782] - [@vivian]). Remove `ClientBuilder::hyper_client` and `From for Client` which -were available to re-use `hyper` clients ([#768] - [@vivian]). +were available to reuse `hyper` clients ([#768] - [@vivian]). Return updated copy of member when updating a member ([#758] - [@vivian]). diff --git a/twilight-mention/src/timestamp.rs b/twilight-mention/src/timestamp.rs index 33ff54396fe..be93cc5b648 100644 --- a/twilight-mention/src/timestamp.rs +++ b/twilight-mention/src/timestamp.rs @@ -163,7 +163,7 @@ impl Ord for Timestamp { impl PartialOrd for Timestamp { fn partial_cmp(&self, other: &Timestamp) -> Option { - self.unix.partial_cmp(&other.unix) + Some(self.cmp(other)) } } diff --git a/twilight-model/src/id/mod.rs b/twilight-model/src/id/mod.rs index cc6e078dc50..afab72b5a42 100644 --- a/twilight-model/src/id/mod.rs +++ b/twilight-model/src/id/mod.rs @@ -382,7 +382,7 @@ impl PartialEq> for u64 { impl PartialOrd for Id { fn partial_cmp(&self, other: &Self) -> Option { - self.value.partial_cmp(&other.value) + Some(self.cmp(other)) } } diff --git a/twilight-standby/src/lib.rs b/twilight-standby/src/lib.rs index d52a6bae3e2..9eeae67d6cd 100644 --- a/twilight-standby/src/lib.rs +++ b/twilight-standby/src/lib.rs @@ -849,7 +849,7 @@ impl Standby { // // A form of enumeration can't be used because sometimes the index // doesn't advance; iterators would continue to provide incrementing - // enumeration indexes while we sometimes want to re-use an index. + // enumeration indexes while we sometimes want to reuse an index. let mut index = 0; let mut results = ProcessResults::new(); From c269fe569249073ec5ebfd7d5251b52235415d3a Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Sat, 7 Oct 2023 10:31:30 -0400 Subject: [PATCH 5/6] feat(model, cache): add support for super reactions (#2275) Ref: - https://github.com/discord/discord-api-docs/pull/6056 This PR also adds a new `HexColor` struct for efficiently storing hex strings. --- twilight-cache-inmemory/src/event/reaction.rs | 8 +- twilight-model/src/channel/message/mod.rs | 26 +++- .../src/channel/message/reaction.rs | 48 ++++++- twilight-model/src/util/hex_color.rs | 130 ++++++++++++++++++ twilight-model/src/util/mod.rs | 3 +- 5 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 twilight-model/src/util/hex_color.rs diff --git a/twilight-cache-inmemory/src/event/reaction.rs b/twilight-cache-inmemory/src/event/reaction.rs index 7ef5894257b..d18983e9bb6 100644 --- a/twilight-cache-inmemory/src/event/reaction.rs +++ b/twilight-cache-inmemory/src/event/reaction.rs @@ -1,6 +1,6 @@ use crate::{config::ResourceType, InMemoryCache, UpdateCache}; use twilight_model::{ - channel::message::{Reaction, ReactionType}, + channel::message::{Reaction, ReactionCountDetails, ReactionType}, gateway::payload::incoming::{ ReactionAdd, ReactionRemove, ReactionRemoveAll, ReactionRemoveEmoji, }, @@ -39,9 +39,15 @@ impl UpdateCache for ReactionAdd { .unwrap_or_default(); message.reactions.push(Reaction { + burst_colors: Vec::new(), count: 1, + count_details: ReactionCountDetails { + burst: 0, + normal: 1, + }, emoji: self.0.emoji.clone(), me, + me_burst: false, }); } } diff --git a/twilight-model/src/channel/message/mod.rs b/twilight-model/src/channel/message/mod.rs index 6dba6c160d8..cf262e16be6 100644 --- a/twilight-model/src/channel/message/mod.rs +++ b/twilight-model/src/channel/message/mod.rs @@ -26,7 +26,7 @@ pub use self::{ interaction::MessageInteraction, kind::MessageType, mention::Mention, - reaction::{Reaction, ReactionType}, + reaction::{Reaction, ReactionCountDetails, ReactionType}, reference::MessageReference, role_subscription_data::RoleSubscriptionData, sticker::Sticker, @@ -196,6 +196,7 @@ pub struct Message { #[cfg(test)] mod tests { use super::{ + reaction::ReactionCountDetails, sticker::{MessageSticker, StickerFormatType}, Message, MessageActivity, MessageActivityType, MessageApplication, MessageFlags, MessageReference, MessageType, Reaction, ReactionType, @@ -478,11 +479,17 @@ mod tests { mentions: Vec::new(), pinned: false, reactions: vec![Reaction { + burst_colors: Vec::new(), count: 7, + count_details: ReactionCountDetails { + burst: 0, + normal: 7, + }, emoji: ReactionType::Unicode { name: "a".to_owned(), }, me: true, + me_burst: false, }], reference: Some(MessageReference { channel_id: Some(Id::new(1)), @@ -653,10 +660,23 @@ mod tests { Token::Seq { len: Some(1) }, Token::Struct { name: "Reaction", - len: 3, + len: 6, }, + Token::Str("burst_colors"), + Token::Seq { len: Some(0) }, + Token::SeqEnd, Token::Str("count"), Token::U64(7), + Token::Str("count_details"), + Token::Struct { + name: "ReactionCountDetails", + len: 2, + }, + Token::Str("burst"), + Token::U64(0), + Token::Str("normal"), + Token::U64(7), + Token::StructEnd, Token::Str("emoji"), Token::Struct { name: "ReactionType", @@ -667,6 +687,8 @@ mod tests { Token::StructEnd, Token::Str("me"), Token::Bool(true), + Token::Str("me_burst"), + Token::Bool(false), Token::StructEnd, Token::SeqEnd, Token::Str("message_reference"), diff --git a/twilight-model/src/channel/message/reaction.rs b/twilight-model/src/channel/message/reaction.rs index e1ddee51af8..6538529554a 100644 --- a/twilight-model/src/channel/message/reaction.rs +++ b/twilight-model/src/channel/message/reaction.rs @@ -1,15 +1,24 @@ -use crate::id::{marker::EmojiMarker, Id}; +use crate::{ + id::{marker::EmojiMarker, Id}, + util::HexColor, +}; use serde::{Deserialize, Serialize}; /// Reaction below a message. #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct Reaction { + /// HEX colors used for super reaction. + pub burst_colors: Vec, /// Amount of reactions this emoji has. pub count: u64, + /// Reaction count details for each type of reaction. + pub count_details: ReactionCountDetails, /// Emoji of this reaction. pub emoji: ReactionType, /// 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`]. @@ -46,20 +55,35 @@ pub enum ReactionType { }, } +/// Breakdown of normal and super reaction counts for the associated emoji. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] +pub struct ReactionCountDetails { + /// Count of super reactions. + pub burst: u64, + /// Count of normal reactions. + pub normal: u64, +} + #[cfg(test)] mod tests { - use super::{Reaction, ReactionType}; - use crate::id::Id; + use super::{Reaction, ReactionCountDetails, ReactionType}; + use crate::{id::Id, util::HexColor}; use serde_test::Token; #[test] fn message_reaction_unicode() { let value = Reaction { + burst_colors: Vec::from([HexColor(255, 255, 255)]), count: 7, + count_details: ReactionCountDetails { + burst: 0, + normal: 7, + }, emoji: ReactionType::Unicode { name: "a".to_owned(), }, me: true, + me_burst: false, }; serde_test::assert_tokens( @@ -67,10 +91,24 @@ mod tests { &[ Token::Struct { name: "Reaction", - len: 3, + len: 6, }, + Token::Str("burst_colors"), + Token::Seq { len: Some(1) }, + Token::Str("#FFFFFF"), + Token::SeqEnd, Token::Str("count"), Token::U64(7), + Token::Str("count_details"), + Token::Struct { + name: "ReactionCountDetails", + len: 2, + }, + Token::Str("burst"), + Token::U64(0), + Token::Str("normal"), + Token::U64(7), + Token::StructEnd, Token::Str("emoji"), Token::Struct { name: "ReactionType", @@ -81,6 +119,8 @@ mod tests { Token::StructEnd, Token::Str("me"), Token::Bool(true), + Token::Str("me_burst"), + Token::Bool(false), Token::StructEnd, ], ); diff --git a/twilight-model/src/util/hex_color.rs b/twilight-model/src/util/hex_color.rs new file mode 100644 index 00000000000..fb66b0ed6b9 --- /dev/null +++ b/twilight-model/src/util/hex_color.rs @@ -0,0 +1,130 @@ +use std::fmt::Formatter; +use std::fmt::{Display, Result as FmtResult}; +use std::num::ParseIntError; +use std::str::FromStr; + +use serde::de::Visitor; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// Represents a color in the RGB format using hexadecimal notation. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct HexColor( + /// Red component of the color. + pub u8, + /// Green component of the color. + pub u8, + /// Blue component of the color. + pub u8, +); + +impl Display for HexColor { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + f.write_fmt(format_args!("#{:02X}{:02X}{:02X}", self.0, self.1, self.2)) + } +} + +impl Serialize for HexColor { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_string()) + } +} + +pub enum HexColorParseError { + InvalidLength, + InvalidFormat, + InvalidCharacter(ParseIntError), +} + +impl From for HexColorParseError { + fn from(err: ParseIntError) -> Self { + Self::InvalidCharacter(err) + } +} + +impl FromStr for HexColor { + type Err = HexColorParseError; + + fn from_str(s: &str) -> Result { + if !s.starts_with('#') { + return Err(HexColorParseError::InvalidFormat); + } + + let s = s.trim_start_matches('#'); + + let (r, g, b) = match s.len() { + 3 => ( + u8::from_str_radix(&s[0..1], 16)?, + u8::from_str_radix(&s[1..2], 16)?, + u8::from_str_radix(&s[2..3], 16)?, + ), + 6 => ( + u8::from_str_radix(&s[0..2], 16)?, + u8::from_str_radix(&s[2..4], 16)?, + u8::from_str_radix(&s[4..6], 16)?, + ), + _ => return Err(HexColorParseError::InvalidLength), + }; + + Ok(Self(r, g, b)) + } +} + +struct HexColorVisitor; + +impl<'de> Visitor<'de> for HexColorVisitor { + type Value = HexColor; + + fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + formatter.write_str("a hex color string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + HexColor::from_str(v).map_err(|_| E::custom("invalid hex color")) + } +} + +impl<'de> Deserialize<'de> for HexColor { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_str(HexColorVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::HexColor; + + #[test] + fn hex_color_display() { + let hex_color = HexColor(255, 255, 255); + assert_eq!(hex_color.to_string(), "#FFFFFF"); + } + + #[test] + fn serialize() { + let hex_color = HexColor(252, 177, 3); + let serialized = serde_json::to_string(&hex_color).unwrap(); + assert_eq!(serialized, "\"#FCB103\""); + } + + #[test] + fn serialize_2() { + let hex_color = HexColor(255, 255, 255); + let serialized = serde_json::to_string(&hex_color).unwrap(); + assert_eq!(serialized, "\"#FFFFFF\""); + } + + #[test] + fn deserialize() { + let deserialized: HexColor = serde_json::from_str("\"#FFFFFF\"").unwrap(); + assert_eq!(deserialized, HexColor(255, 255, 255)); + } + + #[test] + fn deserialize_invalid() { + let deserialized: Result = serde_json::from_str("\"#GGGGGG\""); + assert!(deserialized.is_err()); + } +} diff --git a/twilight-model/src/util/mod.rs b/twilight-model/src/util/mod.rs index b1d0a6e9f5f..c30df90e120 100644 --- a/twilight-model/src/util/mod.rs +++ b/twilight-model/src/util/mod.rs @@ -1,9 +1,10 @@ //! Utilities for efficiently parsing and representing data from Discord's API. pub mod datetime; +pub mod hex_color; pub mod image_hash; -pub use self::{datetime::Timestamp, image_hash::ImageHash}; +pub use self::{datetime::Timestamp, hex_color::HexColor, image_hash::ImageHash}; #[allow(clippy::trivially_copy_pass_by_ref)] pub(crate) fn is_false(value: &bool) -> bool { From b936a541f538a71f15b5a230240a5bee88f36f28 Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Sat, 7 Oct 2023 22:14:16 -0400 Subject: [PATCH 6/6] feat(http): support default thread timeout for channel creation (#2274) --- .../src/request/guild/create_guild_channel.rs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/twilight-http/src/request/guild/create_guild_channel.rs b/twilight-http/src/request/guild/create_guild_channel.rs index f371a5b4ed6..1e7c79cfc2b 100644 --- a/twilight-http/src/request/guild/create_guild_channel.rs +++ b/twilight-http/src/request/guild/create_guild_channel.rs @@ -42,6 +42,12 @@ struct CreateGuildChannelFields<'a> { default_reaction_emoji: Option<&'a DefaultReaction>, #[serde(skip_serializing_if = "Option::is_none")] default_sort_order: Option, + /// Initial `rate_limit_per_user` to set on newly created threads in a channel. + /// This field is copied to the thread at creation time and does not live update. + /// + /// This field is only applicable for text, announcement, media, and forum channels. + #[serde(skip_serializing_if = "Option::is_none")] + default_thread_rate_limit_per_user: Option, #[serde(rename = "type", skip_serializing_if = "Option::is_none")] kind: Option, name: &'a str, @@ -93,6 +99,7 @@ impl<'a> CreateGuildChannel<'a> { default_forum_layout: None, default_reaction_emoji: None, default_sort_order: None, + default_thread_rate_limit_per_user: None, kind: None, name, nsfw: None, @@ -176,6 +183,33 @@ impl<'a> CreateGuildChannel<'a> { self } + /// Set the default number of seconds that a user must wait before before they are + /// able to send another message in any newly-created thread in the channel. + /// + /// This field is only applicable for text, announcement, media, and forum channels. + /// The minimum is 0 and the maximum is 21600. This is also known as "Slow Mode". See + /// [Discord Docs/Channel Object]. + /// + /// # Errors + /// + /// Returns an error of type [`RateLimitPerUserInvalid`] if the time is invalid. + /// + /// [`RateLimitPerUserInvalid`]: twilight_validate::channel::ChannelValidationErrorType::RateLimitPerUserInvalid + /// [Discord Docs/Channel Object]: https://discordapp.com/developers/docs/resources/channel#channel-object-channel-structure + pub const fn default_thread_rate_limit_per_user( + mut self, + default_thread_rate_limit_per_user: u16, + ) -> Result { + #[allow(clippy::question_mark)] + if let Err(source) = validate_rate_limit_per_user(default_thread_rate_limit_per_user) { + return Err(source); + } + + self.fields.default_thread_rate_limit_per_user = Some(default_thread_rate_limit_per_user); + + Ok(self) + } + /// Set the kind of channel. pub const fn kind(mut self, kind: ChannelType) -> Self { self.fields.kind = Some(kind);