diff --git a/CHANGELOG.md b/CHANGELOG.md index 57697d33..c1861a0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased +### Added + +- Support for Telegram Bot API [version 6.2](https://core.telegram.org/bots/api#august-12-2022) ([#251][pr251]) + +[pr251]: https://github.com/teloxide/teloxide-core/pull/251 + ### Changed - `Animation`, `Audio`, `Document`, `PassportFile`, `PhotoSize`, `Video`, `VideoNote` and `Voice` now contain `FileMeta` instead of its fields ([#253][pr253]) @@ -15,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Request` now requires `Self: IntoFuture` - There is no need for `AutoSend` anymore - MSRV (Minimal Supported Rust Version) was bumped from `1.58.0` to `1.64.0` +- Refactored `Sticker` and related types ([#251][pr251]) [pr253]: https://github.com/teloxide/teloxide-core/pull/253 diff --git a/README.md b/README.md index 25e08273..1fc5fc94 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ - + diff --git a/schema.ron b/schema.ron index bd79cb18..b5d6d0c2 100644 --- a/schema.ron +++ b/schema.ron @@ -3164,6 +3164,20 @@ Schema( ), ], ), + Method( + names: ("getCustomEmojiStickers", "GetCustomEmojiStickers", "get_custom_emoji_stickers"), + return_ty: ArrayOf(RawTy("Sticker")), + doc: Doc(md: "Use this method to get information about custom emoji stickers by their identifiers. Returns an Array of Sticker objects."), + tg_doc: "https://core.telegram.org/bots/api#getcustomemojistickers", + tg_category: "Stickers", + params: [ + Param( + name: "custom_emoji_ids", + ty: ArrayOf(String), + descr: Doc(md: "List of custom emoji identifiers. At most 200 custom emoji identifiers can be specified."), + ), + ], + ), Method( names: ("uploadStickerFile", "UploadStickerFile", "upload_sticker_file"), return_ty: RawTy("FileMeta"), @@ -3212,7 +3226,7 @@ Schema( name: "sticker", ty: RawTy("InputSticker"), descr: Doc( - md: "**PNG** or **TGS** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a _file\\_id_ as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »]", + md: "**PNG** image, **TGS** animation or **WEBM** video with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a _file\\_id_ as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »]", md_links: {"More info on Sending Files »": "https://core.telegram.org/bots/api#sending-files"}, ), ), @@ -3222,9 +3236,9 @@ Schema( descr: Doc(md: "One or more emoji corresponding to the sticker"), ), Param( - name: "contains_masks", - ty: Option(bool), - descr: Doc(md: "Pass _True_, if a set of mask stickers should be created"), + name: "sticker_type", + ty: Option(RawTy("StickerType")), + descr: Doc(md: "Type of stickers in the set, pass “regular” or “mask”. Custom emoji sticker sets can't be created via the Bot API at the moment. By default, a regular sticker set is created."), ), Param( name: "mask_position", diff --git a/src/adaptors/auto_send.rs b/src/adaptors/auto_send.rs index dfd4a4c8..607d69c0 100644 --- a/src/adaptors/auto_send.rs +++ b/src/adaptors/auto_send.rs @@ -142,6 +142,7 @@ where delete_message, send_sticker, get_sticker_set, + get_custom_emoji_stickers, upload_sticker_file, create_new_sticker_set, add_sticker_to_set, diff --git a/src/adaptors/cache_me.rs b/src/adaptors/cache_me.rs index 5ae8669a..30365693 100644 --- a/src/adaptors/cache_me.rs +++ b/src/adaptors/cache_me.rs @@ -169,6 +169,7 @@ where delete_message, send_sticker, get_sticker_set, + get_custom_emoji_stickers, upload_sticker_file, create_new_sticker_set, add_sticker_to_set, diff --git a/src/adaptors/erased.rs b/src/adaptors/erased.rs index fc12d8ac..aee6c21c 100644 --- a/src/adaptors/erased.rs +++ b/src/adaptors/erased.rs @@ -166,6 +166,9 @@ macro_rules! fwd_erased { (@convert $m:ident, $arg:ident, errors : $T:ty) => { $arg.into_iter().collect() }; + (@convert $m:ident, $arg:ident, custom_emoji_ids : $T:ty) => { + $arg.into_iter().collect() + }; (@convert $m:ident, $arg:ident, $arg_:ident : $T:ty) => { $arg.into() }; @@ -258,6 +261,7 @@ where delete_message, send_sticker, get_sticker_set, + get_custom_emoji_stickers, upload_sticker_file, create_new_sticker_set, add_sticker_to_set, @@ -713,6 +717,11 @@ trait ErasableRequester<'a> { fn get_sticker_set(&self, name: String) -> ErasedRequest<'a, GetStickerSet, Self::Err>; + fn get_custom_emoji_stickers( + &self, + custom_emoji_ids: Vec, + ) -> ErasedRequest<'a, GetCustomEmojiStickers, Self::Err>; + fn upload_sticker_file( &self, user_id: UserId, @@ -1426,6 +1435,13 @@ where Requester::get_sticker_set(self, name).erase() } + fn get_custom_emoji_stickers( + &self, + custom_emoji_ids: Vec, + ) -> ErasedRequest<'a, GetCustomEmojiStickers, Self::Err> { + Requester::get_custom_emoji_stickers(self, custom_emoji_ids).erase() + } + fn upload_sticker_file( &self, user_id: UserId, diff --git a/src/adaptors/parse_mode.rs b/src/adaptors/parse_mode.rs index ca53a67e..04516105 100644 --- a/src/adaptors/parse_mode.rs +++ b/src/adaptors/parse_mode.rs @@ -164,6 +164,7 @@ impl Requester for DefaultParseMode { delete_message, send_sticker, get_sticker_set, + get_custom_emoji_stickers, upload_sticker_file, create_new_sticker_set, add_sticker_to_set, diff --git a/src/adaptors/throttle/requester_impl.rs b/src/adaptors/throttle/requester_impl.rs index 50cbccf6..e7817f50 100644 --- a/src/adaptors/throttle/requester_impl.rs +++ b/src/adaptors/throttle/requester_impl.rs @@ -148,6 +148,7 @@ where stop_poll, delete_message, get_sticker_set, + get_custom_emoji_stickers, upload_sticker_file, create_new_sticker_set, add_sticker_to_set, diff --git a/src/adaptors/trace.rs b/src/adaptors/trace.rs index 7e0e9a78..fef86e76 100644 --- a/src/adaptors/trace.rs +++ b/src/adaptors/trace.rs @@ -195,6 +195,7 @@ where delete_message, send_sticker, get_sticker_set, + get_custom_emoji_stickers, upload_sticker_file, create_new_sticker_set, add_sticker_to_set, diff --git a/src/bot/api.rs b/src/bot/api.rs index 96c6239a..d78cad73 100644 --- a/src/bot/api.rs +++ b/src/bot/api.rs @@ -911,6 +911,18 @@ impl Requester for Bot { Self::GetStickerSet::new(self.clone(), payloads::GetStickerSet::new(name)) } + type GetCustomEmojiStickers = JsonRequest; + + fn get_custom_emoji_stickers(&self, custom_emoji_ids: C) -> Self::GetCustomEmojiStickers + where + C: IntoIterator, + { + Self::GetCustomEmojiStickers::new( + self.clone(), + payloads::GetCustomEmojiStickers::new(custom_emoji_ids), + ) + } + type UploadStickerFile = MultipartRequest; fn upload_sticker_file( diff --git a/src/codegen.rs b/src/codegen.rs index c26575f1..fa165066 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -15,6 +15,7 @@ pub(crate) mod schema; use std::{ fs, + io::{Read, Write}, path::{Path, PathBuf}, }; @@ -79,25 +80,32 @@ pub fn ensure_files_contents<'a>( ) { let mut err_count = 0; - for (file, contents) in files_and_contents { - if let Ok(old_contents) = fs::read_to_string(file) { - if normalize_newlines(&old_contents) == normalize_newlines(contents) { - // File is already up to date. - continue; - } + for (path, contents) in files_and_contents { + let mut file = fs::File::options() + .read(true) + .write(true) + .create(true) + .open(path) + .unwrap(); + let mut old_contents = String::with_capacity(contents.len()); + file.read_to_string(&mut old_contents).unwrap(); - err_count += 1; - - let display_path = file.strip_prefix(&project_root()).unwrap_or(file); - eprintln!( - "\n\x1b[31;1merror\x1b[0m: {} was not up-to-date, updating\n", - display_path.display() - ); - if let Some(parent) = file.parent() { - let _ = fs::create_dir_all(parent); - } - fs::write(file, contents).unwrap(); + if normalize_newlines(&old_contents) == normalize_newlines(contents) { + // File is already up to date. + continue; } + + err_count += 1; + + let display_path = path.strip_prefix(&project_root()).unwrap_or(path); + eprintln!( + "\n\x1b[31;1merror\x1b[0m: {} was not up-to-date, updating\n", + display_path.display() + ); + if let Some(parent) = path.parent() { + let _ = fs::create_dir_all(parent); + } + file.write_all(contents.as_bytes()).unwrap(); } let (s, were) = match err_count { diff --git a/src/lib.rs b/src/lib.rs index 5e92048c..e7c945f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ //! Core part of the [`teloxide`] library. //! //! This library provides tools for making requests to the [Telegram Bot API] -//! (Currently, version `6.1` is supported) with ease. The library is fully +//! (Currently, version `6.2` is supported) with ease. The library is fully //! asynchronous and built using [`tokio`]. //! //!```toml @@ -86,6 +86,7 @@ //#![deny(missing_docs)] #![warn(clippy::print_stdout, clippy::dbg_macro)] #![allow(clippy::let_and_return)] +#![allow(clippy::bool_assert_comparison)] // Unless this becomes machine applicable, I'm not adding 334 #[must_use]s (waffle) #![allow(clippy::return_self_not_must_use)] // Workaround for CI diff --git a/src/local_macros.rs b/src/local_macros.rs index a1d668cb..56659c5e 100644 --- a/src/local_macros.rs +++ b/src/local_macros.rs @@ -1087,6 +1087,14 @@ macro_rules! requester_forward { $body!(get_sticker_set this (name: N)) } }; + (@method get_custom_emoji_stickers $body:ident $ty:ident) => { + type GetCustomEmojiStickers = $ty![GetCustomEmojiStickers]; + + fn get_custom_emoji_stickers(&self, custom_emoji_ids: C) -> Self::GetCustomEmojiStickers where C: IntoIterator { + let this = self; + $body!(get_custom_emoji_stickers this (custom_emoji_ids: C)) + } + }; (@method upload_sticker_file $body:ident $ty:ident) => { type UploadStickerFile = $ty![UploadStickerFile]; diff --git a/src/payloads.rs b/src/payloads.rs index 5989336f..168c257a 100644 --- a/src/payloads.rs +++ b/src/payloads.rs @@ -55,6 +55,7 @@ mod get_chat_member; mod get_chat_member_count; mod get_chat_members_count; mod get_chat_menu_button; +mod get_custom_emoji_stickers; mod get_file; mod get_game_high_scores; mod get_me; @@ -157,6 +158,7 @@ pub use get_chat_member::{GetChatMember, GetChatMemberSetters}; pub use get_chat_member_count::{GetChatMemberCount, GetChatMemberCountSetters}; pub use get_chat_members_count::{GetChatMembersCount, GetChatMembersCountSetters}; pub use get_chat_menu_button::{GetChatMenuButton, GetChatMenuButtonSetters}; +pub use get_custom_emoji_stickers::{GetCustomEmojiStickers, GetCustomEmojiStickersSetters}; pub use get_file::{GetFile, GetFileSetters}; pub use get_game_high_scores::{GetGameHighScores, GetGameHighScoresSetters}; pub use get_me::{GetMe, GetMeSetters}; diff --git a/src/payloads/codegen.rs b/src/payloads/codegen.rs index c4e266d1..6cc60bad 100644 --- a/src/payloads/codegen.rs +++ b/src/payloads/codegen.rs @@ -222,7 +222,9 @@ fn params(params: impl Iterator>) -> String { let field = ¶m.name; let ty = ¶m.ty; let flatten = match ty { - Type::RawTy(s) if s == "InputSticker" || s == "TargetMessage" => { + Type::RawTy(s) + if s == "InputSticker" || s == "TargetMessage" || s == "StickerType" => + { "\n #[serde(flatten)]" } _ => "", diff --git a/src/payloads/create_new_sticker_set.rs b/src/payloads/create_new_sticker_set.rs index cf4c4453..2fa6b2b7 100644 --- a/src/payloads/create_new_sticker_set.rs +++ b/src/payloads/create_new_sticker_set.rs @@ -2,7 +2,7 @@ use serde::Serialize; -use crate::types::{InputSticker, MaskPosition, True, UserId}; +use crate::types::{InputSticker, MaskPosition, StickerType, True, UserId}; impl_payload! { @[multipart = sticker] @@ -16,7 +16,7 @@ impl_payload! { pub name: String [into], /// Sticker set title, 1-64 characters pub title: String [into], - /// **PNG** or **TGS** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a _file\_id_ as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »] + /// **PNG** image, **TGS** animation or **WEBM** video with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a _file\_id_ as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. [More info on Sending Files »] /// /// [More info on Sending Files »]: crate::types::InputFile #[serde(flatten)] @@ -25,8 +25,9 @@ impl_payload! { pub emojis: String [into], } optional { - /// Pass _True_, if a set of mask stickers should be created - pub contains_masks: bool, + /// Type of stickers in the set, pass “regular” or “mask”. Custom emoji sticker sets can't be created via the Bot API at the moment. By default, a regular sticker set is created. + #[serde(flatten)] + pub sticker_type: StickerType, /// A JSON-serialized object for position where the mask should be placed on faces pub mask_position: MaskPosition, } diff --git a/src/payloads/get_custom_emoji_stickers.rs b/src/payloads/get_custom_emoji_stickers.rs new file mode 100644 index 00000000..68cedfff --- /dev/null +++ b/src/payloads/get_custom_emoji_stickers.rs @@ -0,0 +1,16 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::Sticker; + +impl_payload! { + /// Use this method to get information about custom emoji stickers by their identifiers. Returns an Array of Sticker objects. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub GetCustomEmojiStickers (GetCustomEmojiStickersSetters) => Vec { + required { + /// List of custom emoji identifiers. At most 200 custom emoji identifiers can be specified. + pub custom_emoji_ids: Vec [collect], + } + } +} diff --git a/src/payloads/setters.rs b/src/payloads/setters.rs index a25e3026..d9e68c24 100644 --- a/src/payloads/setters.rs +++ b/src/payloads/setters.rs @@ -18,8 +18,8 @@ pub use crate::payloads::{ EditMessageTextSetters as _, ExportChatInviteLinkSetters as _, ForwardMessageSetters as _, GetChatAdministratorsSetters as _, GetChatMemberCountSetters as _, GetChatMemberSetters as _, GetChatMembersCountSetters as _, GetChatMenuButtonSetters as _, GetChatSetters as _, - GetFileSetters as _, GetGameHighScoresSetters as _, GetMeSetters as _, - GetMyCommandsSetters as _, GetMyDefaultAdministratorRightsSetters as _, + GetCustomEmojiStickersSetters as _, GetFileSetters as _, GetGameHighScoresSetters as _, + GetMeSetters as _, GetMyCommandsSetters as _, GetMyDefaultAdministratorRightsSetters as _, GetStickerSetSetters as _, GetUpdatesSetters as _, GetUserProfilePhotosSetters as _, GetWebhookInfoSetters as _, KickChatMemberSetters as _, LeaveChatSetters as _, LogOutSetters as _, PinChatMessageSetters as _, PromoteChatMemberSetters as _, diff --git a/src/requests/requester.rs b/src/requests/requester.rs index 874517bf..b18a825c 100644 --- a/src/requests/requester.rs +++ b/src/requests/requester.rs @@ -750,6 +750,13 @@ pub trait Requester { where N: Into; + type GetCustomEmojiStickers: Request; + + /// For Telegram documentation see [`GetCustomEmojiStickers`]. + fn get_custom_emoji_stickers(&self, custom_emoji_ids: C) -> Self::GetCustomEmojiStickers + where + C: IntoIterator; + type UploadStickerFile: Request; /// For Telegram documentation see [`UploadStickerFile`]. @@ -1019,6 +1026,7 @@ macro_rules! forward_all { delete_message, send_sticker, get_sticker_set, + get_custom_emoji_stickers, upload_sticker_file, create_new_sticker_set, add_sticker_to_set, diff --git a/src/types/chat.rs b/src/types/chat.rs index 6131b87d..6500dfcb 100644 --- a/src/types/chat.rs +++ b/src/types/chat.rs @@ -99,6 +99,13 @@ pub struct ChatPrivate { /// /// [`GetChat`]: crate::payloads::GetChat pub has_private_forwards: Option, + + /// `True`, if the privacy settings of the other party restrict sending + /// voice and video note messages in the private chat. Returned only in + /// [`GetChat`]. + /// + /// [`GetChat`]: crate::payloads::GetChat + pub has_restricted_voice_and_video_messages: Option, } #[serde_with_macros::skip_serializing_none] @@ -492,6 +499,7 @@ mod serde_helper { last_name: Option, bio: Option, has_private_forwards: Option, + has_restricted_voice_and_video_messages: Option, } impl From for super::ChatPrivate { @@ -503,6 +511,7 @@ mod serde_helper { last_name, bio, has_private_forwards, + has_restricted_voice_and_video_messages, }: ChatPrivate, ) -> Self { Self { @@ -511,6 +520,7 @@ mod serde_helper { last_name, bio, has_private_forwards, + has_restricted_voice_and_video_messages, } } } @@ -523,6 +533,7 @@ mod serde_helper { last_name, bio, has_private_forwards, + has_restricted_voice_and_video_messages, }: super::ChatPrivate, ) -> Self { Self { @@ -532,6 +543,7 @@ mod serde_helper { last_name, bio, has_private_forwards, + has_restricted_voice_and_video_messages, } } } @@ -576,6 +588,7 @@ mod tests { last_name: None, bio: None, has_private_forwards: None, + has_restricted_voice_and_video_messages: None, }), photo: None, pinned_message: None, @@ -596,6 +609,7 @@ mod tests { last_name: None, bio: None, has_private_forwards: None, + has_restricted_voice_and_video_messages: None, }), photo: None, pinned_message: None, diff --git a/src/types/mask_position.rs b/src/types/mask_position.rs index f6fd809a..4f42c675 100644 --- a/src/types/mask_position.rs +++ b/src/types/mask_position.rs @@ -4,11 +4,11 @@ use serde::{Deserialize, Serialize}; /// default. /// /// [The official docs](https://core.telegram.org/bots/api#maskposition). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct MaskPosition { /// The part of the face relative to which the mask should be placed. One /// of `forehead`, `eyes`, `mouth`, or `chin`. - pub point: String, + pub point: MaskPoint, /// Shift by X-axis measured in widths of the mask scaled to the face size, /// from left to right. For example, choosing `-1.0` will place mask just @@ -24,41 +24,45 @@ pub struct MaskPosition { pub scale: f64, } +/// The part of the face relative to which the mask should be placed. +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum MaskPoint { + Forehead, + Eyes, + Mouth, + Chin, +} + impl MaskPosition { - pub fn new(point: S, x_shift: f64, y_shift: f64, scale: f64) -> Self - where - S: Into, - { + pub const fn new(point: MaskPoint, x_shift: f64, y_shift: f64, scale: f64) -> Self { Self { - point: point.into(), + point, x_shift, y_shift, scale, } } - pub fn point(mut self, val: S) -> Self - where - S: Into, - { - self.point = val.into(); + pub const fn point(mut self, val: MaskPoint) -> Self { + self.point = val; self } #[must_use] - pub fn x_shift(mut self, val: f64) -> Self { + pub const fn x_shift(mut self, val: f64) -> Self { self.x_shift = val; self } #[must_use] - pub fn y_shift(mut self, val: f64) -> Self { + pub const fn y_shift(mut self, val: f64) -> Self { self.y_shift = val; self } #[must_use] - pub fn scale(mut self, val: f64) -> Self { + pub const fn scale(mut self, val: f64) -> Self { self.scale = val; self } diff --git a/src/types/message.rs b/src/types/message.rs index 54cc09af..d42835ae 100644 --- a/src/types/message.rs +++ b/src/types/message.rs @@ -1435,42 +1435,43 @@ mod tests { #[test] fn de_sticker() { let json = r#"{ - "message_id": 199787, - "from": { - "id": 250918540, - "is_bot": false, - "first_name": "Андрей", - "last_name": "Власов", - "username": "aka_dude", - "language_code": "en" - }, - "chat": { - "id": 250918540, - "first_name": "Андрей", - "last_name": "Власов", - "username": "aka_dude", - "type": "private" - }, - "date": 1568290188, - "sticker": { - "width": 512, - "height": 512, - "emoji": "😡", - "set_name": "AdvenTimeAnim", - "is_animated": true, - "is_video": false, - "thumb": { - "file_id": "AAQCAAMjAAOw0PgMaabKAcaXKCBLubkPAAQBAAdtAAPGKwACFgQ", - "file_unique_id":"", - "file_size": 4118, - "width": 128, - "height": 128 - }, - "file_id": "CAADAgADIwADsND4DGmmygHGlyggFgQ", - "file_unique_id":"", - "file_size": 16639 - } - }"#; + "message_id": 199787, + "from": { + "id": 250918540, + "is_bot": false, + "first_name": "Андрей", + "last_name": "Власов", + "username": "aka_dude", + "language_code": "en" + }, + "chat": { + "id": 250918540, + "first_name": "Андрей", + "last_name": "Власов", + "username": "aka_dude", + "type": "private" + }, + "date": 1568290188, + "sticker": { + "width": 512, + "height": 512, + "emoji": "😡", + "set_name": "AdvenTimeAnim", + "is_animated": true, + "is_video": false, + "type": "regular", + "thumb": { + "file_id": "AAMCAgADGQEAARIt0GMwiZ6n4nRbxdpM3pL8vPX6PVAhAAIjAAOw0PgMaabKAcaXKCABAAdtAAMpBA", + "file_unique_id": "AQADIwADsND4DHI", + "file_size": 4118, + "width": 128, + "height": 128 + }, + "file_id": "CAACAgIAAxkBAAESLdBjMImep-J0W8XaTN6S_Lz1-j1QIQACIwADsND4DGmmygHGlyggKQQ", + "file_unique_id": "AgADIwADsND4DA", + "file_size": 16639 + } + }"#; from_str::(json).unwrap(); } diff --git a/src/types/message_entity.rs b/src/types/message_entity.rs index 4ac72484..f417d442 100644 --- a/src/types/message_entity.rs +++ b/src/types/message_entity.rs @@ -139,7 +139,7 @@ impl MessageEntity { /// If you don't have a complete [`User`] value, please use /// [`MessageEntity::text_mention_id`] instead. #[must_use] - pub fn text_mention(user: User, offset: usize, length: usize) -> Self { + pub const fn text_mention(user: User, offset: usize, length: usize) -> Self { Self { kind: MessageEntityKind::TextMention { user }, offset, @@ -158,6 +158,16 @@ impl MessageEntity { } } + /// Create a message entity representing a custom emoji. + #[must_use] + pub const fn custom_emoji(custom_emoji_id: String, offset: usize, length: usize) -> Self { + Self { + kind: MessageEntityKind::CustomEmoji { custom_emoji_id }, + offset, + length, + } + } + #[must_use] pub fn kind(mut self, val: MessageEntityKind) -> Self { self.kind = val; @@ -300,13 +310,14 @@ pub enum MessageEntityKind { PhoneNumber, Bold, Italic, + Underline, + Strikethrough, + Spoiler, Code, Pre { language: Option }, TextLink { url: reqwest::Url }, TextMention { user: User }, - Underline, - Strikethrough, - Spoiler, + CustomEmoji { custom_emoji_id: String }, // FIXME(waffle): newtype this } #[cfg(test)] diff --git a/src/types/sticker.rs b/src/types/sticker.rs index 3d96af32..4ee875fd 100644 --- a/src/types/sticker.rs +++ b/src/types/sticker.rs @@ -10,25 +10,40 @@ use crate::types::{FileMeta, MaskPosition, PhotoSize}; #[serde_with_macros::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Sticker { - /// Identifier for this file. - pub file_id: String, + /// Metadata of the sticker file. + #[serde(flatten)] + pub file: FileMeta, - /// Unique identifier for this file, which is supposed to be the same over - /// time and for different bots. Can't be used to download or reuse the - /// file. - pub file_unique_id: String, - - /// Sticker width. + /// Sticker width, in pixels. + /// + /// You can assume that `max(width, height) = 512`, `min(width, height) <= + /// 512`. In other words one dimension is exactly 512 pixels and the other + /// is at most 512 pixels. pub width: u16, - /// Sticker height. + /// Sticker height, in pixels. + /// + /// You can assume that `max(width, height) = 512`, `min(width, height) <= + /// 512`. In other words one dimension is exactly 512 pixels and the other + /// is at most 512 pixels. pub height: u16, - /// Kind of this sticker - webp, animated or video. + /// Kind of this sticker - regular, mask or custom emoji. + /// + /// In other words this represent how the sticker is presented, as a big + /// picture/video, as a mask while editing pictures or as a custom emoji in + /// messages. #[serde(flatten)] pub kind: StickerKind, - /// Sticker thumbnail in the .webp or .jpg format. + /// Format of this sticker - raster/`.webp`, animated/`.tgs` or + /// video/`.webm`. + /// + /// In other words this represents how the sticker is encoded. + #[serde(flatten)] + pub format: StickerFormat, + + /// Sticker thumbnail in the `.webp` or `.jpg` format. pub thumb: Option, /// Emoji associated with the sticker. @@ -36,29 +51,59 @@ pub struct Sticker { /// Name of the sticker set to which the sticker belongs. pub set_name: Option, - - /// Premium animation for the sticker, if the sticker is premium. - pub premium_animation: Option, - - /// For mask stickers, the position where the mask should be placed. - pub mask_position: Option, - - /// File size in bytes. - #[serde(default = "crate::types::file::file_size_fallback")] - pub file_size: u32, } -/// Kind of a sticker - webp, animated or video. +/// Kind of a [`Sticker`] - regular, mask or custom emoji. +/// +/// Dataful version of [`StickerType`]. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(try_from = "StickerKindRaw", into = "StickerKindRaw")] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] pub enum StickerKind { - /// "Normal", raster sticker. - Webp, - /// [Animated] sticker. + /// "Normal", raster, animated or video sticker. + Regular { + /// Premium animation for the sticker, if the sticker is premium. + premium_animation: Option, + }, + /// Mask sticker. + Mask { + /// For mask stickers, the position where the mask should be placed. + mask_position: MaskPosition, + }, + /// Custom emoji sticker. + CustomEmoji { + /// A unique identifier of the custom emoji. + // FIXME(waffle): newtype + custom_emoji_id: String, + }, +} + +/// Type of a [`Sticker`] - regular, mask or custom emoji. +/// +/// Dataless version of [`StickerType`]. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "sticker_type")] +#[serde(rename_all = "snake_case")] +pub enum StickerType { + /// "Normal", raster, animated or video sticker. + Regular, + /// Mask sticker. + Mask, + /// Custom emoji sticker. + CustomEmoji, +} + +/// Format of a [`Sticker`] - regular/webp, animated/tgs or video/webm. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "StickerFormatRaw", into = "StickerFormatRaw")] +pub enum StickerFormat { + /// "Normal", raster, `.webp` sticker. + Raster, + /// [Animated], `.tgs` sticker. /// /// [Animated]: https://telegram.org/blog/animated-stickers Animated, - /// [Video] sticker. + /// [Video], `.webm` sticker. /// /// [Video]: https://telegram.org/blog/video-stickers-better-reactions Video, @@ -71,8 +116,11 @@ pub enum StickerKind { /// /// let sticker: Sticker = todo!(); /// -/// let _ = sticker.is_video(); -/// let _ = sticker.kind.is_video(); +/// let _ = sticker.is_regular(); +/// let _ = sticker.kind.is_regular(); +/// +/// let _ = sticker.mask_position(); +/// let _ = sticker.kind.mask_position(); /// ``` impl Deref for Sticker { type Target = StickerKind; @@ -82,24 +130,149 @@ impl Deref for Sticker { } } -impl StickerKind { +impl Sticker { /// Returns `true` is this is a "normal" raster sticker. + /// + /// Alias to [`self.format.is_raster()`]. + /// + /// [`self.format.is_raster()`]: StickerFormat::is_raster #[must_use] - pub fn is_webp(&self) -> bool { - matches!(self, Self::Webp) + pub fn is_raster(&self) -> bool { + self.format.is_raster() } /// Returns `true` is this is an [animated] sticker. /// + /// Alias to [`self.format.is_animated()`]. + /// + /// [`self.format.is_animated()`]: StickerFormat::is_animated /// [animated]: https://telegram.org/blog/animated-stickers #[must_use] + pub fn is_animated(&self) -> bool { + self.format.is_animated() + } + + /// Returns `true` is this is a [video] sticker. + /// + /// Alias to [`self.format.is_video()`]. + /// + /// [`self.format.is_video()`]: StickerFormat::is_video + /// [video]: https://telegram.org/blog/video-stickers-better-reactions + #[must_use] + pub fn is_video(&self) -> bool { + self.format.is_video() + } +} + +impl StickerKind { + /// Converts [`StickerKind`] to [`StickerType`] + #[must_use] + pub fn type_(&self) -> StickerType { + match self { + StickerKind::Regular { .. } => StickerType::Regular, + StickerKind::Mask { .. } => StickerType::Mask, + StickerKind::CustomEmoji { .. } => StickerType::CustomEmoji, + } + } + + /// Returns `true` if the sticker kind is [`Regular`]. + /// + /// [`Regular`]: StickerKind::Regular + #[must_use] + pub fn is_regular(&self) -> bool { + self.type_().is_regular() + } + + /// Returns `true` if the sticker kind is [`Mask`]. + /// + /// [`Mask`]: StickerKind::Mask + #[must_use] + pub fn is_mask(&self) -> bool { + self.type_().is_mask() + } + + /// Returns `true` if the sticker kind is [`CustomEmoji`]. + /// + /// [`CustomEmoji`]: StickerKind::CustomEmoji + #[must_use] + pub fn is_custom_emoji(&self) -> bool { + self.type_().is_custom_emoji() + } + + /// Getter for [`StickerKind::Regular::premium_animation`]. + pub fn premium_animation(&self) -> Option<&FileMeta> { + if let Self::Regular { premium_animation } = self { + premium_animation.as_ref() + } else { + None + } + } + + /// Getter for [`StickerKind::Mask::mask_position`]. + pub fn mask_position(&self) -> Option { + if let Self::Mask { mask_position } = self { + Some(*mask_position) + } else { + None + } + } + + /// Getter for [`StickerKind::CustomEmoji::custom_emoji_id`]. + pub fn custom_emoji_id(&self) -> Option<&str> { + if let Self::CustomEmoji { custom_emoji_id } = self { + Some(custom_emoji_id) + } else { + None + } + } +} + +impl StickerType { + /// Returns `true` if the sticker type is [`Regular`]. + /// + /// [`Regular`]: StickerType::Regular + #[must_use] + pub fn is_regular(&self) -> bool { + matches!(self, Self::Regular) + } + + /// Returns `true` if the sticker type is [`Mask`]. + /// + /// [`Mask`]: StickerType::Mask + #[must_use] + pub fn is_mask(&self) -> bool { + matches!(self, Self::Mask) + } + + /// Returns `true` if the sticker type is [`CustomEmoji`]. + /// + /// [`CustomEmoji`]: StickerType::CustomEmoji + #[must_use] + pub fn is_custom_emoji(&self) -> bool { + matches!(self, Self::CustomEmoji) + } +} + +impl StickerFormat { + /// Returns `true` if the sticker format is [`Raster`]. + /// + /// [`Raster`]: StickerFormat::Raster + #[must_use] + pub fn is_raster(&self) -> bool { + matches!(self, Self::Raster) + } + + /// Returns `true` if the sticker format is [`Animated`]. + /// + /// [`Animated`]: StickerFormat::Animated + #[must_use] pub fn is_animated(&self) -> bool { matches!(self, Self::Animated) } - /// Returns `true` is this is a [video] sticker. + /// Returns `true` if the sticker format is [`Video`]. /// - /// [video]: https://telegram.org/blog/video-stickers-better-reactions + /// [`Video`]: StickerFormat::Video #[must_use] pub fn is_video(&self) -> bool { matches!(self, Self::Video) @@ -107,22 +280,22 @@ impl StickerKind { } #[derive(Serialize, Deserialize)] -struct StickerKindRaw { +struct StickerFormatRaw { is_animated: bool, is_video: bool, } -impl TryFrom for StickerKind { +impl TryFrom for StickerFormat { type Error = &'static str; fn try_from( - StickerKindRaw { + StickerFormatRaw { is_animated, is_video, - }: StickerKindRaw, + }: StickerFormatRaw, ) -> Result { let ret = match (is_animated, is_video) { - (false, false) => Self::Webp, + (false, false) => Self::Raster, (true, false) => Self::Animated, (false, true) => Self::Video, (true, true) => return Err("`is_animated` and `is_video` present at the same time"), @@ -132,21 +305,146 @@ impl TryFrom for StickerKind { } } -impl From for StickerKindRaw { - fn from(kind: StickerKind) -> Self { +impl From for StickerFormatRaw { + fn from(kind: StickerFormat) -> Self { match kind { - StickerKind::Webp => Self { + StickerFormat::Raster => Self { is_animated: false, is_video: false, }, - StickerKind::Animated => Self { + StickerFormat::Animated => Self { is_animated: true, is_video: false, }, - StickerKind::Video => Self { + StickerFormat::Video => Self { is_animated: false, is_video: true, }, } } } + +#[cfg(test)] +mod tests { + use crate::types::{MaskPoint, Sticker, StickerFormat, StickerType}; + + #[test] + fn mask_serde() { + // Taken from a real (mask) sticker set + let json = r#"{ + "width": 512, + "height": 512, + "emoji": "🎭", + "set_name": "Coronamask", + "is_animated": false, + "is_video": false, + "type": "mask", + "mask_position": { + "point": "forehead", + "x_shift": -0.0125, + "y_shift": 0.5525, + "scale": 1.94 + }, + "thumb": { + "file_id": "AAMCAQADFQABYzA0qlYHijpjMzMwBFKnEVE5XdkAAjIKAAK_jJAE1TRw7D936M8BAAdtAAMpBA", + "file_unique_id": "AQADMgoAAr-MkARy", + "file_size": 11028, + "width": 320, + "height": 320 + }, + "file_id": "CAACAgEAAxUAAWMwNKpWB4o6YzMzMARSpxFROV3ZAAIyCgACv4yQBNU0cOw_d-jPKQQ", + "file_unique_id": "AgADMgoAAr-MkAQ", + "file_size": 18290 + }"#; + + let sticker: Sticker = serde_json::from_str(json).unwrap(); + + // Assert some basic properties are correctly deserialized + assert_eq!(sticker.type_(), StickerType::Mask); + assert_eq!(sticker.mask_position().unwrap().point, MaskPoint::Forehead); + assert_eq!(sticker.is_animated(), false); + assert_eq!(sticker.is_video(), false); + assert_eq!(sticker.thumb.clone().unwrap().file_size, 11028); + assert_eq!(sticker.file.file_size, 18290); + assert_eq!(sticker.width, 512); + assert_eq!(sticker.height, 512); + + let json2 = serde_json::to_string(&sticker).unwrap(); + let sticker2: Sticker = serde_json::from_str(&json2).unwrap(); + assert_eq!(sticker, sticker2); + } + + #[test] + fn regular_serde() { + // Taken from a real sticker set + let json = r#"{ + "width": 463, + "height": 512, + "emoji": "🍿", + "set_name": "menhera2", + "is_animated": false, + "is_video": false, + "type": "regular", + "thumb": { + "file_id": "AAMCAgADFQABYzBxOJ1GWrttqL7FSRwdAtrq-AkAAtkHAALBGJ4LUUUh5CUew90BAAdtAAMpBA", + "file_unique_id": "AQAD2QcAAsEYngty", + "file_size": 4558, + "width": 116, + "height": 128 + }, + "file_id": "CAACAgIAAxUAAWMwcTidRlq7bai-xUkcHQLa6vgJAALZBwACwRieC1FFIeQlHsPdKQQ", + "file_unique_id": "AgAD2QcAAsEYngs", + "file_size": 25734 + }"#; + + let sticker: Sticker = serde_json::from_str(json).unwrap(); + + // Assert some basic properties are correctly deserialized + assert_eq!(sticker.type_(), StickerType::Regular); + assert_eq!(sticker.premium_animation(), None); + assert_eq!(sticker.is_animated(), false); + assert_eq!(sticker.is_video(), false); + assert_eq!(sticker.thumb.clone().unwrap().file_size, 4558); + assert_eq!(sticker.file.file_size, 25734); + assert_eq!(sticker.width, 463); + assert_eq!(sticker.height, 512); + assert_eq!(sticker.set_name.as_deref(), Some("menhera2")); + + let json2 = serde_json::to_string(&sticker).unwrap(); + let sticker2: Sticker = serde_json::from_str(&json2).unwrap(); + assert_eq!(sticker, sticker2); + } + + #[test] + fn sticker_format_serde() { + { + let json = r#"{"is_animated":false,"is_video":false}"#; + let fmt: StickerFormat = serde_json::from_str(json).unwrap(); + assert_eq!(fmt, StickerFormat::Raster); + + let json2 = serde_json::to_string(&fmt).unwrap(); + assert_eq!(json, json2); + } + { + let json = r#"{"is_animated":true,"is_video":false}"#; + let fmt: StickerFormat = serde_json::from_str(json).unwrap(); + assert_eq!(fmt, StickerFormat::Animated); + + let json2 = serde_json::to_string(&fmt).unwrap(); + assert_eq!(json, json2); + } + { + let json = r#"{"is_animated":false,"is_video":true}"#; + let fmt: StickerFormat = serde_json::from_str(json).unwrap(); + assert_eq!(fmt, StickerFormat::Video); + + let json2 = serde_json::to_string(&fmt).unwrap(); + assert_eq!(json, json2); + } + { + let json = r#"{"is_animated":true,"is_video":true}"#; + let fmt: Result = serde_json::from_str(json); + assert!(fmt.is_err()); + } + } +} diff --git a/src/types/sticker_set.rs b/src/types/sticker_set.rs index e666c1c8..d7adaf29 100644 --- a/src/types/sticker_set.rs +++ b/src/types/sticker_set.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use serde::{Deserialize, Serialize}; -use crate::types::{PhotoSize, Sticker, StickerKind}; +use crate::types::{PhotoSize, Sticker, StickerFormat, StickerType}; /// This object represents a sticker set. /// @@ -15,33 +15,134 @@ pub struct StickerSet { /// Sticker set title. pub title: String, - /// Sticker kind shared by all stickers in this set. - pub kind: StickerKind, + /// Sticker type shared by all stickers in this set. + #[serde(flatten)] + pub kind: StickerType, - /// `true`, if the sticker set contains masks. - pub contains_masks: bool, + /// Sticker format shared by all stickers in this set. + #[serde(flatten)] + pub format: StickerFormat, /// List of all set stickers. pub stickers: Vec, - /// Sticker set thumbnail in the .WEBP or .TGS format. + /// Sticker set thumbnail in the `.webp`, `.tgs` or `.webm` format. pub thumb: Option, } -/// This allows calling [`StickerKind`]'s methods directly on [`StickerSet`]. +/// This allows calling [`StickerType`]'s methods directly on [`StickerSet`]. /// /// ```no_run /// use teloxide_core::types::StickerSet; /// /// let sticker: StickerSet = todo!(); /// -/// let _ = sticker.is_video(); -/// let _ = sticker.kind.is_video(); +/// let _ = sticker.is_mask(); +/// let _ = sticker.kind.is_mask(); /// ``` impl Deref for StickerSet { - type Target = StickerKind; + type Target = StickerType; fn deref(&self) -> &Self::Target { &self.kind } } + +impl StickerSet { + /// Returns `true` is this is a "normal" raster sticker. + /// + /// Alias to [`self.format.is_raster()`]. + /// + /// [`self.format.is_raster()`]: StickerFormat::is_raster + #[must_use] + pub fn is_raster(&self) -> bool { + self.format.is_raster() + } + + /// Returns `true` is this is an [animated] sticker. + /// + /// Alias to [`self.format.is_animated()`]. + /// + /// [`self.format.is_animated()`]: StickerFormat::is_animated + /// [animated]: https://telegram.org/blog/animated-stickers + #[must_use] + pub fn is_animated(&self) -> bool { + self.format.is_animated() + } + + /// Returns `true` is this is a [video] sticker. + /// + /// Alias to [`self.format.is_video()`]. + /// + /// [`self.format.is_video()`]: StickerFormat::is_video + /// [video]: https://telegram.org/blog/video-stickers-better-reactions + #[must_use] + pub fn is_video(&self) -> bool { + self.format.is_video() + } +} + +#[cfg(test)] +mod tests { + use crate::types::StickerSet; + + #[test] + fn smoke_serde() { + // https://t.me/addstickers/teloxide_test + let json = r#"{ + "name": "teloxide_test", + "title": "teloxide-test", + "is_animated": false, + "is_video": false, + "sticker_type": "regular", + "contains_masks": false, + "stickers": [ + { + "width": 512, + "height": 512, + "emoji": "⚙️", + "set_name": "teloxide_test", + "is_animated": false, + "is_video": false, + "type": "regular", + "thumb": { + "file_id": "AAMCAQADFQABYzB4ATH0sqXx351gZ5GpY1Z3Tl8AAlgCAAJ1t4hFbxNCoAg1-akBAAdtAAMpBA", + "file_unique_id": "AQADWAIAAnW3iEVy", + "file_size": 7698, + "width": 320, + "height": 320 + }, + "file_id": "CAACAgEAAxUAAWMweAEx9LKl8d-dYGeRqWNWd05fAAJYAgACdbeIRW8TQqAINfmpKQQ", + "file_unique_id": "AgADWAIAAnW3iEU", + "file_size": 12266 + }, + { + "width": 512, + "height": 512, + "emoji": "⚙️", + "set_name": "teloxide_test", + "is_animated": false, + "is_video": false, + "type": "regular", + "thumb": { + "file_id": "AAMCAQADFQABYzB4AcABR8-MuvGagis9Pk6liSAAAs8DAAL2YYBFNbvduoN1p7oBAAdtAAMpBA", + "file_unique_id": "AQADzwMAAvZhgEVy", + "file_size": 7780, + "width": 320, + "height": 320 + }, + "file_id": "CAACAgEAAxUAAWMweAHAAUfPjLrxmoIrPT5OpYkgAALPAwAC9mGARTW73bqDdae6KQQ", + "file_unique_id": "AgADzwMAAvZhgEU", + "file_size": 12158 + } + ] + }"#; + + let set: StickerSet = serde_json::from_str(json).unwrap(); + + assert!(set.is_raster()); + assert!(set.is_regular()); + assert!(set.thumb.is_none()); + assert_eq!(set.stickers.len(), 2); + } +} diff --git a/src/types/update.rs b/src/types/update.rs index 05b55133..0ca68c3e 100644 --- a/src/types/update.rs +++ b/src/types/update.rs @@ -344,6 +344,7 @@ mod test { last_name: None, bio: None, has_private_forwards: None, + has_restricted_voice_and_video_messages: None, }), photo: None, pinned_message: None,