,
}
diff --git a/crates/teloxide-core/src/types/message.rs b/crates/teloxide-core/src/types/message.rs
index e2de607f..b80c4bbb 100644
--- a/crates/teloxide-core/src/types/message.rs
+++ b/crates/teloxide-core/src/types/message.rs
@@ -1009,17 +1009,16 @@ mod getters {
#[must_use]
pub fn caption(&self) -> Option<&str> {
match &self.kind {
- Common(MessageCommon { media_kind, .. }) => match media_kind {
- MediaKind::Animation(MediaAnimation { caption, .. })
- | MediaKind::Audio(MediaAudio { caption, .. })
- | MediaKind::Document(MediaDocument { caption, .. })
- | MediaKind::Photo(MediaPhoto { caption, .. })
- | MediaKind::Video(MediaVideo { caption, .. })
- | MediaKind::Voice(MediaVoice { caption, .. }) => {
- caption.as_ref().map(Deref::deref)
- }
- _ => None,
- },
+ Common(MessageCommon {
+ media_kind:
+ MediaKind::Animation(MediaAnimation { caption, .. })
+ | MediaKind::Audio(MediaAudio { caption, .. })
+ | MediaKind::Document(MediaDocument { caption, .. })
+ | MediaKind::Photo(MediaPhoto { caption, .. })
+ | MediaKind::Video(MediaVideo { caption, .. })
+ | MediaKind::Voice(MediaVoice { caption, .. }),
+ ..
+ }) => caption.as_ref().map(Deref::deref),
_ => None,
}
}
@@ -1708,7 +1707,7 @@ mod tests {
"width": 512,
"height": 640,
"mime_type": "video/mp4",
- "thumb": {
+ "thumbnail": {
"file_id": "AAQCAAOmBAACBf2oS53pByA-I4CWWCObDwAEAQAHbQADMWcAAhYE",
"file_unique_id":"",
"file_size": 10339,
@@ -1800,7 +1799,7 @@ mod tests {
"width": 512,
"height": 640,
"mime_type": "video/mp4",
- "thumb": {
+ "thumbnail": {
"file_id": "AAQCAAOmBAACBf2oS53pByA-I4CWWCObDwAEAQAHbQADMWcAAhYE",
"file_unique_id":"",
"file_size": 10339,
@@ -1870,7 +1869,7 @@ mod tests {
"is_animated": true,
"is_video": false,
"type": "regular",
- "thumb": {
+ "thumbnail": {
"file_id": "AAMCAgADGQEAARIt0GMwiZ6n4nRbxdpM3pL8vPX6PVAhAAIjAAOw0PgMaabKAcaXKCABAAdtAAMpBA",
"file_unique_id": "AQADIwADsND4DHI",
"file_size": 4118,
diff --git a/crates/teloxide-core/src/types/parse_mode.rs b/crates/teloxide-core/src/types/parse_mode.rs
index ba79c760..0edc69eb 100644
--- a/crates/teloxide-core/src/types/parse_mode.rs
+++ b/crates/teloxide-core/src/types/parse_mode.rs
@@ -47,6 +47,7 @@ use serde::{Deserialize, Serialize};
/// *bold _italic bold ~italic bold strikethrough ||italic bold strikethrough spoiler||~ __underline italic bold___ bold*
/// [inline URL](http://www.example.com/)
/// [inline mention of a user](tg://user?id=123456789)
+/// ![👍](tg://emoji?id=5368324170671202286)
/// `inline fixed-width code`
/// ```
/// pre-formatted fixed-width code block
@@ -72,6 +73,12 @@ use serde::{Deserialize, Serialize};
/// `underline` entity, so instead of `___italic underline___` use `___italic
/// underline_\r__`, where `\r` is a character with code `13`, which will be
/// ignored.
+/// - A valid emoji must be provided as an alternative value for the custom
+/// emoji. The emoji will be shown instead of the custom emoji in places where
+/// a custom emoji cannot be displayed (e.g., system notifications) or if the
+/// message is forwarded by a non-premium user. It is recommended to use the
+/// emoji from the emoji field of the custom emoji [sticker](https://core.telegram.org/bots/api#sticker).
+/// - Custom emoji entities can only be used by bots that purchased additional usernames on [Fragment](https://fragment.com/).
///
/// ## HTML style
///
@@ -86,6 +93,7 @@ use serde::{Deserialize, Serialize};
/// bold italic bold italic bold strikethrough italic bold strikethrough spoiler underline italic bold bold
/// inline URL
/// inline mention of a user
+/// 👍
/// inline fixed-width code
/// pre-formatted fixed-width code block
#[doc = "pre-formatted fixed-width code block written in the \
@@ -104,6 +112,8 @@ use serde::{Deserialize, Serialize};
/// - Use nested `pre` and `code` tags, to define programming language for `pre`
/// entity.
/// - Programming language can't be specified for standalone `code` tags.
+/// - A valid emoji must be used as the content of the `tg-emoji` tag. The emoji will be shown instead of the custom emoji in places where a custom emoji cannot be displayed (e.g., system notifications) or if the message is forwarded by a non-premium user. It is recommended to use the emoji from the emoji field of the custom emoji [sticker](https://core.telegram.org/bots/api#sticker).
+/// - Custom emoji entities can only be used by bots that purchased additional usernames on [Fragment](https://fragment.com/).
///
/// ## Markdown style
///
diff --git a/crates/teloxide-core/src/types/sticker.rs b/crates/teloxide-core/src/types/sticker.rs
index c687908b..0153912d 100644
--- a/crates/teloxide-core/src/types/sticker.rs
+++ b/crates/teloxide-core/src/types/sticker.rs
@@ -36,21 +36,30 @@ pub struct Sticker {
#[serde(flatten)]
pub kind: StickerKind,
- /// Format of this sticker - raster/`.webp`, animated/`.tgs` or
- /// video/`.webm`.
+ /// Format flags of this sticker:
+ ///
+ /// `(is_animated, is_video)` == `(false, false)` - raster/`.webp` or
+ /// `is_animated == true` - animated/`.tgs` or
+ /// `is_video == true` - video/`.webm`.
///
/// In other words this represents how the sticker is encoded.
#[serde(flatten)]
- pub format: StickerFormat,
+ pub flags: StickerFormatFlags,
/// Sticker thumbnail in the `.webp` or `.jpg` format.
- pub thumb: Option,
+ pub thumbnail: Option,
/// Emoji associated with the sticker.
pub emoji: Option,
/// Name of the sticker set to which the sticker belongs.
pub set_name: Option,
+
+ /// True, if the sticker must be repainted to a text color in messages, the
+ /// color of the Telegram Premium badge in emoji status, white color on
+ /// chat photos, or another appropriate color in other places
+ #[serde(default)]
+ pub needs_repainting: bool,
}
/// Kind of a [`Sticker`] - regular, mask or custom emoji.
@@ -93,12 +102,22 @@ pub enum StickerType {
CustomEmoji,
}
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct StickerFormatFlags {
+ /// True, if the sticker is animated
+ #[serde(default)]
+ pub is_animated: bool,
+ /// True, if the sticker is a video sticker
+ #[serde(default)]
+ pub is_video: bool,
+}
+
/// Format of a [`Sticker`] - regular/webp, animated/tgs or video/webm.
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
-#[serde(try_from = "StickerFormatRaw", into = "StickerFormatRaw")]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
pub enum StickerFormat {
- /// "Normal", raster, `.webp` sticker.
- Raster,
+ /// Image in `.png` or `.webp` format.
+ Static,
/// [Animated], `.tgs` sticker.
///
/// [Animated]: https://telegram.org/blog/animated-stickers
@@ -131,36 +150,45 @@ impl Deref for Sticker {
}
impl Sticker {
+ /// Returns the format of the [`Sticker`] based on the [`self.flags`] values
+ ///
+ /// [`Sticker`]: Sticker
+ /// [`self.flags`]: Sticker::flags
+ #[must_use]
+ pub fn format(&self) -> StickerFormat {
+ self.flags.format()
+ }
+
/// Returns `true` is this is a "normal" raster sticker.
///
- /// Alias to [`self.format.is_raster()`].
+ /// Alias to [`self.format().is_raster()`].
///
- /// [`self.format.is_raster()`]: StickerFormat::is_raster
+ /// [`self.format().is_static()`]: StickerFormat::is_static
#[must_use]
- pub fn is_raster(&self) -> bool {
- self.format.is_raster()
+ pub fn is_static(&self) -> bool {
+ self.format().is_static()
}
/// Returns `true` is this is an [animated] sticker.
///
- /// Alias to [`self.format.is_animated()`].
+ /// Alias to [`self.format().is_animated()`].
///
- /// [`self.format.is_animated()`]: StickerFormat::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()
+ self.format().is_animated()
}
/// Returns `true` is this is a [video] sticker.
///
- /// Alias to [`self.format.is_video()`].
+ /// Alias to [`self.format().is_video()`].
///
- /// [`self.format.is_video()`]: StickerFormat::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()
+ self.format().is_video()
}
}
@@ -256,13 +284,24 @@ impl StickerType {
}
}
+impl StickerFormatFlags {
+ pub fn format(&self) -> StickerFormat {
+ match (self.is_animated, self.is_video) {
+ (false, false) => StickerFormat::Static,
+ (true, false) => StickerFormat::Animated,
+ (false, true) => StickerFormat::Video,
+ (true, true) => panic!("`is_animated` and `is_video` flags present at the same time"),
+ }
+ }
+}
+
impl StickerFormat {
- /// Returns `true` if the sticker format is [`Raster`].
+ /// Returns `true` if the sticker format is [`Static`].
///
- /// [`Raster`]: StickerFormat::Raster
+ /// [`Static`]: StickerFormat::Static
#[must_use]
- pub fn is_raster(&self) -> bool {
- matches!(self, Self::Raster)
+ pub fn is_static(&self) -> bool {
+ matches!(self, Self::Static)
}
/// Returns `true` if the sticker format is [`Animated`].
@@ -282,42 +321,31 @@ impl StickerFormat {
}
}
-#[derive(Serialize, Deserialize)]
-struct StickerFormatRaw {
- is_animated: bool,
- is_video: bool,
-}
-
-impl TryFrom for StickerFormat {
- type Error = &'static str;
-
- fn try_from(
- StickerFormatRaw { is_animated, is_video }: StickerFormatRaw,
- ) -> Result {
- let ret = match (is_animated, is_video) {
- (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"),
- };
-
- Ok(ret)
- }
-}
-
-impl From for StickerFormatRaw {
- fn from(kind: StickerFormat) -> Self {
- match kind {
- StickerFormat::Raster => Self { is_animated: false, is_video: false },
- StickerFormat::Animated => Self { is_animated: true, is_video: false },
- StickerFormat::Video => Self { is_animated: false, is_video: true },
- }
- }
-}
-
#[cfg(test)]
mod tests {
- use crate::types::{MaskPoint, Sticker, StickerFormat, StickerType};
+ use crate::types::{MaskPoint, Sticker, StickerFormat, StickerFormatFlags, StickerType};
+
+ #[test]
+ fn sticker_format_serde() {
+ // Ser
+ assert_eq!(serde_json::to_string(&StickerFormat::Static).unwrap(), r#""static""#);
+ assert_eq!(serde_json::to_string(&StickerFormat::Animated).unwrap(), r#""animated""#);
+ assert_eq!(serde_json::to_string(&StickerFormat::Video).unwrap(), r#""video""#);
+
+ // De
+ assert_eq!(
+ serde_json::from_str::(r#""static""#).unwrap(),
+ StickerFormat::Static
+ );
+ assert_eq!(
+ serde_json::from_str::(r#""animated""#).unwrap(),
+ StickerFormat::Animated
+ );
+ assert_eq!(
+ serde_json::from_str::(r#""video""#).unwrap(),
+ StickerFormat::Video
+ );
+ }
#[test]
fn mask_serde() {
@@ -336,7 +364,7 @@ mod tests {
"y_shift": 0.5525,
"scale": 1.94
},
- "thumb": {
+ "thumbnail": {
"file_id": "AAMCAQADFQABYzA0qlYHijpjMzMwBFKnEVE5XdkAAjIKAAK_jJAE1TRw7D936M8BAAdtAAMpBA",
"file_unique_id": "AQADMgoAAr-MkARy",
"file_size": 11028,
@@ -355,7 +383,7 @@ mod tests {
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.thumbnail.clone().unwrap().file.size, 11028);
assert_eq!(sticker.file.size, 18290);
assert_eq!(sticker.width, 512);
assert_eq!(sticker.height, 512);
@@ -376,7 +404,7 @@ mod tests {
"is_animated": false,
"is_video": false,
"type": "regular",
- "thumb": {
+ "thumbnail": {
"file_id": "AAMCAgADFQABYzBxOJ1GWrttqL7FSRwdAtrq-AkAAtkHAALBGJ4LUUUh5CUew90BAAdtAAMpBA",
"file_unique_id": "AQAD2QcAAsEYngty",
"file_size": 4558,
@@ -395,7 +423,7 @@ mod tests {
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.thumbnail.clone().unwrap().file.size, 4558);
assert_eq!(sticker.file.size, 25734);
assert_eq!(sticker.width, 463);
assert_eq!(sticker.height, 512);
@@ -407,35 +435,73 @@ mod tests {
}
#[test]
- fn sticker_format_serde() {
+ fn regular_serde_with_options() {
+ let json = r#"{
+ "width": 463,
+ "height": 512,
+ "is_animated": false,
+ "is_video": false,
+ "type": "regular",
+ "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.thumbnail, None);
+ assert_eq!(sticker.emoji, None);
+ assert_eq!(sticker.file.size, 25734);
+ assert_eq!(sticker.width, 463);
+ assert_eq!(sticker.height, 512);
+ assert_eq!(sticker.set_name, None);
+ assert_eq!(sticker.needs_repainting, false);
+
+ 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_flags_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 fmt_flags: StickerFormatFlags = serde_json::from_str(json).unwrap();
+ assert_eq!(fmt_flags.format(), StickerFormat::Static);
- let json2 = serde_json::to_string(&fmt).unwrap();
+ let json2 = serde_json::to_string(&fmt_flags).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 fmt_flags: StickerFormatFlags = serde_json::from_str(json).unwrap();
+ assert_eq!(fmt_flags.format(), StickerFormat::Animated);
- let json2 = serde_json::to_string(&fmt).unwrap();
+ let json2 = serde_json::to_string(&fmt_flags).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 fmt_flags: StickerFormatFlags = serde_json::from_str(json).unwrap();
+ assert_eq!(fmt_flags.format(), StickerFormat::Video);
- let json2 = serde_json::to_string(&fmt).unwrap();
+ let json2 = serde_json::to_string(&fmt_flags).unwrap();
assert_eq!(json, json2);
}
+ }
+
+ #[test]
+ #[should_panic]
+ fn wrong_sticker_format_flags_serde() {
{
let json = r#"{"is_animated":true,"is_video":true}"#;
- let fmt: Result = serde_json::from_str(json);
- assert!(fmt.is_err());
+ let fmt_flags: StickerFormatFlags = serde_json::from_str(json).unwrap();
+ fmt_flags.format();
}
}
}
diff --git a/crates/teloxide-core/src/types/sticker_set.rs b/crates/teloxide-core/src/types/sticker_set.rs
index 0f45eacb..17a41e0a 100644
--- a/crates/teloxide-core/src/types/sticker_set.rs
+++ b/crates/teloxide-core/src/types/sticker_set.rs
@@ -2,7 +2,7 @@ use std::ops::Deref;
use serde::{Deserialize, Serialize};
-use crate::types::{PhotoSize, Sticker, StickerFormat, StickerType};
+use crate::types::{PhotoSize, Sticker, StickerFormat, StickerFormatFlags, StickerType};
/// This object represents a sticker set.
///
@@ -20,15 +20,16 @@ pub struct StickerSet {
#[serde(flatten)]
pub kind: StickerType,
- /// Sticker format shared by all stickers in this set.
+ // FIXME: remove it in 7.2 https://core.telegram.org/bots/api#march-31-2024
+ /// Sticker format flags shared by all stickers in this set.
#[serde(flatten)]
- pub format: StickerFormat,
+ pub flags: StickerFormatFlags,
/// List of all set stickers.
pub stickers: Vec,
/// Sticker set thumbnail in the `.webp`, `.tgs` or `.webm` format.
- pub thumb: Option,
+ pub thumbnail: Option