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,