diff --git a/Cargo.lock b/Cargo.lock index 71d1bd4b..ff7098ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,6 +235,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd4c6dcc3b0aea2f5c0b4b82c2b15fe39ddbc76041a310848f4706edf76bb31" + [[package]] name = "byteorder" version = "1.5.0" @@ -1668,6 +1674,15 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "rgb" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f86ae463694029097b846d8f99fd5536740602ae00022c0c50c5600720b2f71" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.8" @@ -2302,6 +2317,7 @@ dependencies = [ "pretty_env_logger", "rc-box", "reqwest", + "rgb", "ron", "serde", "serde_json", @@ -2791,7 +2807,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/crates/teloxide-core/CHANGELOG.md b/crates/teloxide-core/CHANGELOG.md index c3565652..7d3eb13d 100644 --- a/crates/teloxide-core/CHANGELOG.md +++ b/crates/teloxide-core/CHANGELOG.md @@ -49,6 +49,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - MSRV (Minimal Supported Rust Version) was bumped from `1.70.0` to `1.80.0` - Some dependencies was bumped: `reqwest` to `0.12.7` and `ron` to `0.8.1` - `tokio` version was explicitly specified as `1.39` and feature `io-util` was enabled for it +- `[u8; 3]` sometimes used for RGB values was replaced with dedicated `Rgb` struct: ([#1151][pr1151]) + - `serde_rgb` module from `types.rs` file was removed + - `CreateForumTopic`, `ForumTopicCreated` and `ForumTopic` structs now use `Rgb` instead of `[u8; 3]` for `icon_color` field + - Added `rgb` crate dependency to Cargo.toml + - Added `Rgb` struct with `From` implementation for `RGB8` type from popular `rgb` crate - Support for TBA 7.2 ([#1146](pr1146)) - Remove `flags` field from `StickerSet` struct @@ -60,6 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [pr1134]: https://github.com/teloxide/teloxide/pull/1134 [pr1146]: https://github.com/teloxide/teloxide/pull/1146 [pr1147]: https://github.com/teloxide/teloxide/pull/1147 +[pr1151]: https://github.com/teloxide/teloxide/pull/1151 ### Removed diff --git a/crates/teloxide-core/Cargo.toml b/crates/teloxide-core/Cargo.toml index e3d1fdd3..6c30efef 100644 --- a/crates/teloxide-core/Cargo.toml +++ b/crates/teloxide-core/Cargo.toml @@ -76,6 +76,7 @@ rc-box = "1.1.1" chrono = { version = "0.4.32", default-features = false } either = "1.6.1" bitflags = { version = "1.2" } +rgb = "0.8.48" vecrem = { version = "0.1", optional = true } diff --git a/crates/teloxide-core/schema.ron b/crates/teloxide-core/schema.ron index 540cc047..c1216ca7 100644 --- a/crates/teloxide-core/schema.ron +++ b/crates/teloxide-core/schema.ron @@ -2754,9 +2754,11 @@ Schema( ), Param( name: "icon_color", - // FIXME: use an Rgb or something - ty: u32, - descr: Doc(md: "Color of the topic icon in RGB format. Currently, must be one of 7322096 (0x6FB9F0), 16766590 (0xFFD67E), 13338331 (0xCB86DB), 9367192 (0x8EEE98), 16749490 (0xFF93B2), or 16478047 (0xFB6F5F)") + ty: RawTy("Rgb"), + descr: Doc( + md: "Color of the topic icon in RGB format. Currently, must be one of 7322096 (`0x6FB9F0`), 16766590 (`0xFFD67E`), 13338331 (`0xCB86DB`), 9367192 (`0x8EEE98`), 16749490 (`0xFF93B2`), or 16478047 (`0xFB6F5F`). To construct color from these values use [`Rgb::from_u32`]", + md_links: {"`Rgb::from_u32`": "crate::types::Rgb::from_u32"} + ) ), Param( name: "icon_custom_emoji_id", diff --git a/crates/teloxide-core/src/adaptors/erased.rs b/crates/teloxide-core/src/adaptors/erased.rs index e813300a..1e0728a4 100644 --- a/crates/teloxide-core/src/adaptors/erased.rs +++ b/crates/teloxide-core/src/adaptors/erased.rs @@ -671,7 +671,7 @@ trait ErasableRequester<'a> { &self, chat_id: Recipient, name: String, - icon_color: u32, + icon_color: Rgb, icon_custom_emoji_id: String, ) -> ErasedRequest<'a, CreateForumTopic, Self::Err>; @@ -1506,7 +1506,7 @@ where &self, chat_id: Recipient, name: String, - icon_color: u32, + icon_color: Rgb, icon_custom_emoji_id: String, ) -> ErasedRequest<'a, CreateForumTopic, Self::Err> { Requester::create_forum_topic(self, chat_id, name, icon_color, icon_custom_emoji_id).erase() diff --git a/crates/teloxide-core/src/bot/api.rs b/crates/teloxide-core/src/bot/api.rs index 4305505c..7fed7fc5 100644 --- a/crates/teloxide-core/src/bot/api.rs +++ b/crates/teloxide-core/src/bot/api.rs @@ -6,7 +6,7 @@ use crate::{ requests::{JsonRequest, MultipartRequest}, types::{ BotCommand, BusinessConnectionId, ChatId, ChatPermissions, InlineQueryResult, InputFile, - InputMedia, InputSticker, LabeledPrice, MessageId, Recipient, StickerFormat, ThreadId, + InputMedia, InputSticker, LabeledPrice, MessageId, Recipient, Rgb, StickerFormat, ThreadId, UserId, }, Bot, @@ -686,7 +686,7 @@ impl Requester for Bot { &self, chat_id: C, name: N, - icon_color: u32, + icon_color: Rgb, icon_custom_emoji_id: I, ) -> Self::CreateForumTopic where diff --git a/crates/teloxide-core/src/local_macros.rs b/crates/teloxide-core/src/local_macros.rs index db71eea2..a6d2abbf 100644 --- a/crates/teloxide-core/src/local_macros.rs +++ b/crates/teloxide-core/src/local_macros.rs @@ -949,11 +949,11 @@ macro_rules! requester_forward { (@method create_forum_topic $body:ident $ty:ident) => { type CreateForumTopic = $ty![CreateForumTopic]; - fn create_forum_topic(&self, chat_id: C, name: N, icon_color: u32, icon_custom_emoji_id: I) -> Self::CreateForumTopic where C: Into, + fn create_forum_topic(&self, chat_id: C, name: N, icon_color: Rgb, icon_custom_emoji_id: I) -> Self::CreateForumTopic where C: Into, N: Into, I: Into { let this = self; - $body!(create_forum_topic this (chat_id: C, name: N, icon_color: u32, icon_custom_emoji_id: I)) + $body!(create_forum_topic this (chat_id: C, name: N, icon_color: Rgb, icon_custom_emoji_id: I)) } }; (@method edit_forum_topic $body:ident $ty:ident) => { diff --git a/crates/teloxide-core/src/payloads/create_forum_topic.rs b/crates/teloxide-core/src/payloads/create_forum_topic.rs index a1412fb3..ef9824f2 100644 --- a/crates/teloxide-core/src/payloads/create_forum_topic.rs +++ b/crates/teloxide-core/src/payloads/create_forum_topic.rs @@ -2,7 +2,7 @@ use serde::Serialize; -use crate::types::{ForumTopic, Recipient}; +use crate::types::{ForumTopic, Recipient, Rgb}; impl_payload! { /// Use this method to create a topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the _can\_manage\_topics_ administrator rights. Returns information about the created topic as a `ForumTopic` object. @@ -13,8 +13,10 @@ impl_payload! { pub chat_id: Recipient [into], /// Topic name, 1-128 characters pub name: String [into], - /// Color of the topic icon in RGB format. Currently, must be one of 7322096 (0x6FB9F0), 16766590 (0xFFD67E), 13338331 (0xCB86DB), 9367192 (0x8EEE98), 16749490 (0xFF93B2), or 16478047 (0xFB6F5F) - pub icon_color: u32, + /// Color of the topic icon in RGB format. Currently, must be one of 7322096 (`0x6FB9F0`), 16766590 (`0xFFD67E`), 13338331 (`0xCB86DB`), 9367192 (`0x8EEE98`), 16749490 (`0xFF93B2`), or 16478047 (`0xFB6F5F`). To construct color from these values use [`Rgb::from_u32`] + /// + /// [`Rgb::from_u32`]: crate::types::Rgb::from_u32 + pub icon_color: Rgb, /// Unique identifier of the custom emoji shown as the topic icon. Use `getForumTopicIconStickers` to get all allowed custom emoji identifiers. pub icon_custom_emoji_id: String [into], } diff --git a/crates/teloxide-core/src/requests/requester.rs b/crates/teloxide-core/src/requests/requester.rs index 7e34161a..a758f167 100644 --- a/crates/teloxide-core/src/requests/requester.rs +++ b/crates/teloxide-core/src/requests/requester.rs @@ -702,7 +702,7 @@ pub trait Requester { &self, chat_id: C, name: N, - icon_color: u32, + icon_color: Rgb, icon_custom_emoji_id: I, ) -> Self::CreateForumTopic where diff --git a/crates/teloxide-core/src/types.rs b/crates/teloxide-core/src/types.rs index 36b73ac9..3d10253b 100644 --- a/crates/teloxide-core/src/types.rs +++ b/crates/teloxide-core/src/types.rs @@ -128,6 +128,7 @@ pub use reply_markup::*; pub use reply_parameters::*; pub use request_id::*; pub use response_parameters::*; +pub use rgb::*; pub use sent_web_app_message::*; pub use shared_user::*; pub use shipping_address::*; @@ -262,6 +263,7 @@ mod reply_markup; mod reply_parameters; mod request_id; mod response_parameters; +mod rgb; mod sent_web_app_message; mod shared_user; mod shipping_address; @@ -544,67 +546,3 @@ pub(crate) mod option_msg_id_as_int { } } } - -pub(crate) mod serde_rgb { - use serde::{de::Visitor, Deserializer, Serializer}; - - pub fn serialize(&this: &[u8; 3], s: S) -> Result { - s.serialize_u32(to_u32(this)) - } - - pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 3], D::Error> { - struct V; - - impl Visitor<'_> for V { - type Value = [u8; 3]; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("an integer represeting an RGB color") - } - - fn visit_u32(self, v: u32) -> Result - where - E: serde::de::Error, - { - Ok(from_u32(v)) - } - - fn visit_u64(self, v: u64) -> Result - where - E: serde::de::Error, - { - self.visit_u32(v.try_into().map_err(|_| E::custom("rgb value doesn't fit u32"))?) - } - } - d.deserialize_u32(V) - } - - fn to_u32([r, g, b]: [u8; 3]) -> u32 { - u32::from_be_bytes([0, r, g, b]) - } - - fn from_u32(rgb: u32) -> [u8; 3] { - let [_, r, g, b] = rgb.to_be_bytes(); - [r, g, b] - } - - #[test] - fn bytes() { - assert_eq!(to_u32([0xAA, 0xBB, 0xCC]), 0x00AABBCC); - assert_eq!(from_u32(0x00AABBCC), [0xAA, 0xBB, 0xCC]); - } - - #[test] - fn json() { - #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] - struct Struct { - #[serde(with = "self")] - color: [u8; 3], - } - - let json = format!(r#"{{"color":{}}}"#, 0x00AABBCC); - let Struct { color } = serde_json::from_str(&json).unwrap(); - - assert_eq!(color, [0xAA, 0xBB, 0xCC]) - } -} diff --git a/crates/teloxide-core/src/types/forum_topic.rs b/crates/teloxide-core/src/types/forum_topic.rs index 1eae5e8e..50228823 100644 --- a/crates/teloxide-core/src/types/forum_topic.rs +++ b/crates/teloxide-core/src/types/forum_topic.rs @@ -1,7 +1,7 @@ -use crate::types::ThreadId; - use serde::{Deserialize, Serialize}; +use crate::types::{Rgb, ThreadId}; + /// This object represents a forum topic. /// /// [The official docs](https://core.telegram.org/bots/api#forumtopiccreated). @@ -16,9 +16,7 @@ pub struct ForumTopic { pub name: String, /// Color of the topic icon in RGB format. - // FIXME: use/add a specialized rgb color type? - #[serde(with = "crate::types::serde_rgb")] - pub icon_color: [u8; 3], + pub icon_color: Rgb, /// Unique identifier of the custom emoji shown as the topic icon. // FIXME: CustomEmojiId diff --git a/crates/teloxide-core/src/types/forum_topic_created.rs b/crates/teloxide-core/src/types/forum_topic_created.rs index b7f04344..02662649 100644 --- a/crates/teloxide-core/src/types/forum_topic_created.rs +++ b/crates/teloxide-core/src/types/forum_topic_created.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use crate::types::Rgb; + /// This object represents a service message about a new forum topic created in /// the chat. /// @@ -11,9 +13,7 @@ pub struct ForumTopicCreated { pub name: String, /// Color of the topic icon in RGB format. - // FIXME: use/add a specialized rgb color type? - #[serde(with = "crate::types::serde_rgb")] - pub icon_color: [u8; 3], + pub icon_color: Rgb, /// Unique identifier of the custom emoji shown as the topic icon. // FIXME: CustomEmojiId @@ -22,7 +22,7 @@ pub struct ForumTopicCreated { #[cfg(test)] mod tests { - use crate::types::ForumTopicCreated; + use super::*; #[test] fn deserialization() { @@ -32,7 +32,7 @@ mod tests { let event = serde_json::from_str::(json).unwrap(); assert_eq!(event.name, "???"); - assert_eq!(event.icon_color, [0x8E, 0xEE, 0x98]); + assert_eq!(event.icon_color, Rgb { r: 0x8E, g: 0xEE, b: 0x98 }); assert_eq!(event.icon_custom_emoji_id.as_deref(), Some("5312536423851630001")); } } diff --git a/crates/teloxide-core/src/types/rgb.rs b/crates/teloxide-core/src/types/rgb.rs new file mode 100644 index 00000000..22b4b568 --- /dev/null +++ b/crates/teloxide-core/src/types/rgb.rs @@ -0,0 +1,113 @@ +use rgb::RGB8; +use serde::{de::Visitor, Deserialize, Serialize}; + +/// RGB color format +#[repr(C)] +#[derive(Clone, Copy, Debug)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Rgb { + pub r: u8, + pub g: u8, + pub b: u8, +} + +impl Rgb { + /// Convert a [`Rgb`] struct into a big endian `u32` representing the RGB + /// color. + /// + /// # Example + /// + /// ``` + /// use teloxide_core::types::Rgb; + /// assert_eq!(Rgb { r: 0xAA, g: 0xBB, b: 0xCC }.to_u32(), 0xAABBCC); + /// ``` + /// + /// [`Rgb`]: Rgb + pub fn to_u32(self) -> u32 { + u32::from_be_bytes([0, self.r, self.g, self.b]) + } + + /// Convert a big endian `u32` representing the RGB color into a [`Rgb`] + /// struct. + /// + /// # Example + /// + /// ``` + /// use teloxide_core::types::Rgb; + /// assert_eq!(Rgb::from_u32(0xAABBCC), Rgb { r: 0xAA, g: 0xBB, b: 0xCC }); + /// ``` + /// + /// [`Rgb`]: Rgb + pub fn from_u32(rgb: u32) -> Self { + let [_, r, g, b] = rgb.to_be_bytes(); + Rgb { r, g, b } + } +} + +impl Serialize for Rgb { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_u32(self.to_u32()) + } +} + +impl<'de> Deserialize<'de> for Rgb { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct V; + + impl Visitor<'_> for V { + type Value = Rgb; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("an integer represeting an RGB color") + } + + fn visit_u32(self, v: u32) -> Result + where + E: serde::de::Error, + { + Ok(Self::Value::from_u32(v)) + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + self.visit_u32(v.try_into().map_err(|_| E::custom("rgb value doesn't fit u32"))?) + } + } + + deserializer.deserialize_u32(V) + } +} + +impl From for Rgb { + fn from(color: RGB8) -> Self { + Rgb { r: color.r, g: color.g, b: color.b } + } +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + + use super::*; + + #[test] + fn json() { + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct Struct { + color: Rgb, + } + + let json = format!(r#"{{"color":{}}}"#, 0x00AABBCC); + let Struct { color } = serde_json::from_str(&json).unwrap(); + + assert_eq!(color, Rgb { r: 0xAA, g: 0xBB, b: 0xCC }) + } +}