(data).unwrap();
+ }
+}
diff --git a/crates/teloxide-core/src/types/parse_mode.rs b/crates/teloxide-core/src/types/parse_mode.rs
index 0edc69eb..0ac230f8 100644
--- a/crates/teloxide-core/src/types/parse_mode.rs
+++ b/crates/teloxide-core/src/types/parse_mode.rs
@@ -55,6 +55,11 @@ use serde::{Deserialize, Serialize};
/// ```rust
#[doc = "pre-formatted fixed-width code block written in the Rust programming language"]
/// ```
+/// >Block quotation started
+/// >Block quotation continued
+/// >Block quotation continued
+/// >Block quotation continued
+/// >The last line of the block quotation
/// ````
///
/// Please note:
@@ -98,6 +103,8 @@ use serde::{Deserialize, Serialize};
/// pre-formatted fixed-width code block
#[doc = "pre-formatted fixed-width code block written in the \
Rust programming language
"]
+/// Block quotation started\nBlock quotation continued\nThe last
+/// line of the block quotation
/// ````
///
/// Please note:
diff --git a/crates/teloxide-core/src/types/reaction_type.rs b/crates/teloxide-core/src/types/reaction_type.rs
new file mode 100644
index 00000000..9ba328fb
--- /dev/null
+++ b/crates/teloxide-core/src/types/reaction_type.rs
@@ -0,0 +1,43 @@
+use serde::{Deserialize, Serialize};
+
+/// The reaction type is based on an emoji or custom emoji.
+#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
+#[serde(tag = "type")]
+#[serde(rename_all = "snake_case")]
+pub enum ReactionType {
+ /// Emoji reaction.
+ Emoji {
+ /// Reaction emoji. Currently, it can be one of "๐", "๐", "โค", "๐ฅ",
+ /// "๐ฅฐ", "๐", "๐", "๐ค", "๐คฏ", "๐ฑ", "๐คฌ", "๐ข", "๐", "๐คฉ",
+ /// "๐คฎ", "๐ฉ", "๐", "๐", "๐", "๐คก", "๐ฅฑ", "๐ฅด", "๐", "๐ณ",
+ /// "โคโ๐ฅ", "๐", "๐ญ", "๐ฏ", "๐คฃ", "โก", "๐", "๐", "๐", "๐คจ",
+ /// "๐", "๐", "๐พ", "๐", "๐", "๐", "๐ด", "๐ญ", "๐ค", "๐ป",
+ /// "๐จโ๐ป", "๐", "๐", "๐", "๐", "๐จ", "๐ค", "โ", "๐ค", "๐ซก",
+ /// "๐
", "๐", "โ", "๐
", "๐คช", "๐ฟ", "๐", "๐", "๐", "๐ฆ", "๐",
+ /// "๐", "๐", "๐", "๐พ", "๐คทโโ", "๐คท", "๐คทโโ", "๐ก"
+ emoji: String,
+ },
+ /// Custom emoji reaction.
+ CustomEmoji {
+ /// Custom emoji identifier.
+ custom_emoji_id: String,
+ },
+}
+
+impl ReactionType {
+ #[must_use]
+ pub fn emoji(&self) -> Option<&String> {
+ match &self {
+ Self::Emoji { emoji } => Some(emoji),
+ _ => None,
+ }
+ }
+
+ #[must_use]
+ pub fn custom_emoji_id(&self) -> Option<&String> {
+ match &self {
+ Self::CustomEmoji { custom_emoji_id } => Some(custom_emoji_id),
+ _ => None,
+ }
+ }
+}
diff --git a/crates/teloxide-core/src/types/reply_parameters.rs b/crates/teloxide-core/src/types/reply_parameters.rs
new file mode 100644
index 00000000..09c1ede8
--- /dev/null
+++ b/crates/teloxide-core/src/types/reply_parameters.rs
@@ -0,0 +1,61 @@
+use serde::{Deserialize, Serialize};
+
+use crate::types::{MessageEntity, MessageId, Recipient};
+
+/// Describes reply parameters for the message that is being sent.
+#[serde_with::skip_serializing_none]
+#[derive(Default, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
+pub struct ReplyParameters {
+ /// Identifier of the message that will be replied to in the current chat,
+ /// or in the chat _chat\_id_ if it is specified
+ #[serde(flatten)]
+ pub message_id: MessageId,
+ /// If the message to be replied to is from a different chat, unique
+ /// identifier for the chat or username of the channel (in the format
+ /// `@channelusername`)
+ pub chat_id: Option,
+ /// Pass _true_ if the message should be sent even if the specified message
+ /// to be replied to is not found; can be used only for replies in the
+ /// same chat and forum topic.
+ pub allow_sending_without_reply: Option,
+ /// Quoted part of the message to be replied to; 0-1024 characters after
+ /// entities parsing. The quote must be an exact substring of the message to
+ /// be replied to, including _bold_, _italic_, _underline_, _strikethrough_,
+ /// _spoiler_, and _custom_emoji_ entities. The message will fail to send if
+ /// the quote isn't found in the original message.
+ pub quote: Option,
+ /// Mode for parsing entities in the quote. See [formatting options] for
+ /// more details.
+ ///
+ /// [formatting options]: https://core.telegram.org/bots/api#formatting-options
+ pub quote_parse_mode: Option,
+ /// A JSON-serialized list of special entities that appear in the quote. It
+ /// can be specified instead of quote_parse_mode.
+ pub quote_entities: Option>,
+ /// Position of the quote in the original message in UTF-16 code units
+ pub quote_position: Option,
+}
+
+impl ReplyParameters {
+ pub fn new(message_id: MessageId) -> Self {
+ Self { message_id, ..Self::default() }
+ }
+
+ /// Setter for the `chat_id` field
+ pub fn chat_id(mut self, chat_id: Recipient) -> Self {
+ self.chat_id = Some(chat_id);
+ self
+ }
+
+ /// Sets the `allow_sending_without_reply_field` to _true_
+ pub fn allow_sending_without_reply(mut self) -> Self {
+ self.allow_sending_without_reply = Some(true);
+ self
+ }
+
+ /// Setter for the `quote` field
+ pub fn quote(mut self, quote: String) -> Self {
+ self.quote = Some(quote);
+ self
+ }
+}
diff --git a/crates/teloxide-core/src/types/request_id.rs b/crates/teloxide-core/src/types/request_id.rs
new file mode 100644
index 00000000..98cd87f3
--- /dev/null
+++ b/crates/teloxide-core/src/types/request_id.rs
@@ -0,0 +1,24 @@
+use derive_more::Display;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Copy, Display, PartialEq, Eq, Hash, Deserialize, Serialize)]
+#[serde(transparent)]
+pub struct RequestId(pub i32);
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_request_id_de() {
+ #[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
+ struct Dummy {
+ request_id: RequestId,
+ }
+ let json = r#"{"request_id":42}"#;
+ let dummy = Dummy { request_id: RequestId(42) };
+
+ assert_eq!(serde_json::to_string(&dummy).unwrap(), json);
+ assert_eq!(dummy, serde_json::from_str(json).unwrap());
+ }
+}
diff --git a/crates/teloxide-core/src/types/text_quote.rs b/crates/teloxide-core/src/types/text_quote.rs
new file mode 100644
index 00000000..eda8811e
--- /dev/null
+++ b/crates/teloxide-core/src/types/text_quote.rs
@@ -0,0 +1,24 @@
+use serde::{Deserialize, Serialize};
+
+use crate::types::MessageEntity;
+
+/// This object contains information about the quoted part of a message that is
+/// replied to by the given message.
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct TextQuote {
+ /// Text of the quoted part of a message that is replied to by the given
+ /// message
+ pub text: String,
+ /// Special entities that appear in the quote. Currently, only _bold_,
+ /// _italic_, _underline_, _strikethrough_, _spoiler_, and
+ /// _custom_emoji_ entities are kept in quotes.
+ #[serde(default)]
+ pub entities: Vec,
+ /// Approximate quote position in the original message in UTF-16 code units
+ /// as specified by the sender
+ pub position: u32,
+ /// True, if the quote was chosen manually by the message sender. Otherwise,
+ /// the quote was added automatically by the server.
+ #[serde(default)]
+ pub is_manual: bool,
+}
diff --git a/crates/teloxide-core/src/types/update.rs b/crates/teloxide-core/src/types/update.rs
index cd08a326..33d3f4a0 100644
--- a/crates/teloxide-core/src/types/update.rs
+++ b/crates/teloxide-core/src/types/update.rs
@@ -3,8 +3,9 @@ use serde::{de::MapAccess, Deserialize, Serialize, Serializer};
use serde_json::Value;
use crate::types::{
- CallbackQuery, Chat, ChatJoinRequest, ChatMemberUpdated, ChosenInlineResult, InlineQuery,
- Message, Poll, PollAnswer, PreCheckoutQuery, ShippingQuery, User,
+ CallbackQuery, Chat, ChatBoostRemoved, ChatBoostUpdated, ChatJoinRequest, ChatMemberUpdated,
+ ChosenInlineResult, InlineQuery, Message, MessageReactionCountUpdated, MessageReactionUpdated,
+ Poll, PollAnswer, PreCheckoutQuery, ShippingQuery, User,
};
/// This [object] represents an incoming update.
@@ -59,6 +60,24 @@ pub enum UpdateKind {
/// New version of a channel post that is known to the bot and was edited.
EditedChannelPost(Message),
+ /// A reaction to a message was changed by a user. The bot must be an
+ /// administrator in the chat and must explicitly specify
+ /// [`AllowedUpdate::MessageReaction`] in the list of `allowed_updates`
+ /// to receive these updates. The update isn't received for reactions
+ /// set by bots.
+ ///
+ /// [`AllowedUpdate::MessageReaction`]: crate::types::AllowedUpdate::MessageReaction
+ MessageReaction(MessageReactionUpdated),
+
+ /// Reactions to a message with anonymous reactions were changed. The bot
+ /// must be an administrator in the chat and must explicitly specify
+ /// [`AllowedUpdate::MessageReactionCount`] in the list of `allowed_updates`
+ /// to receive these updates. The updates are grouped and can be sent
+ /// with delay up to a few minutes.
+ ///
+ /// [`AllowedUpdate::MessageReactionCount`]: crate::types::AllowedUpdate::MessageReactionCount
+ MessageReactionCount(MessageReactionCountUpdated),
+
/// New incoming [inline] query.
///
/// [inline]: https://core.telegram.org/bots/api#inline-mode
@@ -108,6 +127,14 @@ pub enum UpdateKind {
/// updates.
ChatJoinRequest(ChatJoinRequest),
+ /// A chat boost was added or changed. The bot must be an administrator in
+ /// the chat to receive these updates.
+ ChatBoost(ChatBoostUpdated),
+
+ /// A boost was removed from a chat. The bot must be an administrator in the
+ /// chat to receive these updates.
+ RemovedChatBoost(ChatBoostRemoved),
+
/// An error that happened during deserialization.
///
/// This allows `teloxide` to continue working even if telegram adds a new
@@ -129,10 +156,13 @@ impl Update {
use UpdateKind::*;
let from = match &self.kind {
- Message(m) | EditedMessage(m) | ChannelPost(m) | EditedChannelPost(m) => m.from()?,
+ Message(m) | EditedMessage(m) | ChannelPost(m) | EditedChannelPost(m) => {
+ m.from.as_ref()?
+ }
CallbackQuery(query) => &query.from,
ChosenInlineResult(chosen) => &chosen.from,
+ MessageReaction(reaction) => return reaction.user(),
InlineQuery(query) => &query.from,
ShippingQuery(query) => &query.from,
PreCheckoutQuery(query) => &query.from,
@@ -140,8 +170,10 @@ impl Update {
MyChatMember(m) | ChatMember(m) => &m.from,
ChatJoinRequest(r) => &r.from,
+ ChatBoost(b) => return b.boost.source.user(),
+ RemovedChatBoost(b) => return b.source.user(),
- Poll(_) | Error(_) => return None,
+ MessageReactionCount(_) | Poll(_) | Error(_) => return None,
};
Some(from)
@@ -191,6 +223,13 @@ impl Update {
| UpdateKind::ChannelPost(message)
| UpdateKind::EditedChannelPost(message) => i0(message.mentioned_users()),
+ UpdateKind::MessageReaction(answer) => {
+ if let Some(user) = answer.user() {
+ return i1(once(user));
+ }
+ i6(empty())
+ }
+
UpdateKind::InlineQuery(query) => i1(once(&query.from)),
UpdateKind::ChosenInlineResult(query) => i1(once(&query.from)),
UpdateKind::CallbackQuery(query) => i2(query.mentioned_users()),
@@ -209,7 +248,21 @@ impl Update {
i4(member.mentioned_users())
}
UpdateKind::ChatJoinRequest(request) => i5(request.mentioned_users()),
- UpdateKind::Error(_) => i6(empty()),
+
+ UpdateKind::ChatBoost(b) => {
+ if let Some(user) = b.boost.source.user() {
+ return i1(once(user));
+ }
+ i6(empty())
+ }
+ UpdateKind::RemovedChatBoost(b) => {
+ if let Some(user) = b.source.user() {
+ return i1(once(user));
+ }
+ i6(empty())
+ }
+
+ UpdateKind::MessageReactionCount(_) | UpdateKind::Error(_) => i6(empty()),
}
}
@@ -220,10 +273,14 @@ impl Update {
let chat = match &self.kind {
Message(m) | EditedMessage(m) | ChannelPost(m) | EditedChannelPost(m) => &m.chat,
- CallbackQuery(q) => &q.message.as_ref()?.chat,
+ CallbackQuery(q) => q.message.as_ref()?.chat(),
ChatMember(m) => &m.chat,
MyChatMember(m) => &m.chat,
ChatJoinRequest(c) => &c.chat,
+ MessageReaction(r) => &r.chat,
+ MessageReactionCount(r) => &r.chat,
+ ChatBoost(b) => &b.chat,
+ RemovedChatBoost(b) => &b.chat,
InlineQuery(_)
| ChosenInlineResult(_)
@@ -293,6 +350,14 @@ impl<'de> Deserialize<'de> for UpdateKind {
"edited_channel_post" => {
map.next_value::().ok().map(UpdateKind::EditedChannelPost)
}
+ "message_reaction" => map
+ .next_value::()
+ .ok()
+ .map(UpdateKind::MessageReaction),
+ "message_reaction_count" => map
+ .next_value::()
+ .ok()
+ .map(UpdateKind::MessageReactionCount),
"inline_query" => {
map.next_value::().ok().map(UpdateKind::InlineQuery)
}
@@ -324,6 +389,13 @@ impl<'de> Deserialize<'de> for UpdateKind {
.next_value::()
.ok()
.map(UpdateKind::ChatJoinRequest),
+ "chat_boost" => {
+ map.next_value::().ok().map(UpdateKind::ChatBoost)
+ }
+ "removed_chat_boost" => map
+ .next_value::()
+ .ok()
+ .map(UpdateKind::RemovedChatBoost),
_ => Some(empty_error()),
})
.unwrap_or_else(empty_error);
@@ -351,27 +423,37 @@ impl Serialize for UpdateKind {
UpdateKind::EditedChannelPost(v) => {
s.serialize_newtype_variant(name, 3, "edited_channel_post", v)
}
- UpdateKind::InlineQuery(v) => s.serialize_newtype_variant(name, 4, "inline_query", v),
+ UpdateKind::MessageReaction(v) => {
+ s.serialize_newtype_variant(name, 4, "message_reaction", v)
+ }
+ UpdateKind::MessageReactionCount(v) => {
+ s.serialize_newtype_variant(name, 5, "message_reaction_count", v)
+ }
+ UpdateKind::InlineQuery(v) => s.serialize_newtype_variant(name, 6, "inline_query", v),
UpdateKind::ChosenInlineResult(v) => {
- s.serialize_newtype_variant(name, 5, "chosen_inline_result", v)
+ s.serialize_newtype_variant(name, 7, "chosen_inline_result", v)
}
UpdateKind::CallbackQuery(v) => {
- s.serialize_newtype_variant(name, 6, "callback_query", v)
+ s.serialize_newtype_variant(name, 8, "callback_query", v)
}
UpdateKind::ShippingQuery(v) => {
- s.serialize_newtype_variant(name, 7, "shipping_query", v)
+ s.serialize_newtype_variant(name, 9, "shipping_query", v)
}
UpdateKind::PreCheckoutQuery(v) => {
- s.serialize_newtype_variant(name, 8, "pre_checkout_query", v)
+ s.serialize_newtype_variant(name, 10, "pre_checkout_query", v)
}
- UpdateKind::Poll(v) => s.serialize_newtype_variant(name, 9, "poll", v),
- UpdateKind::PollAnswer(v) => s.serialize_newtype_variant(name, 10, "poll_answer", v),
+ UpdateKind::Poll(v) => s.serialize_newtype_variant(name, 11, "poll", v),
+ UpdateKind::PollAnswer(v) => s.serialize_newtype_variant(name, 12, "poll_answer", v),
UpdateKind::MyChatMember(v) => {
- s.serialize_newtype_variant(name, 11, "my_chat_member", v)
+ s.serialize_newtype_variant(name, 13, "my_chat_member", v)
}
- UpdateKind::ChatMember(v) => s.serialize_newtype_variant(name, 12, "chat_member", v),
+ UpdateKind::ChatMember(v) => s.serialize_newtype_variant(name, 14, "chat_member", v),
UpdateKind::ChatJoinRequest(v) => {
- s.serialize_newtype_variant(name, 13, "chat_join_request", v)
+ s.serialize_newtype_variant(name, 15, "chat_join_request", v)
+ }
+ UpdateKind::ChatBoost(v) => s.serialize_newtype_variant(name, 16, "chat_boost", v),
+ UpdateKind::RemovedChatBoost(v) => {
+ s.serialize_newtype_variant(name, 17, "removed_chat_boost", v)
}
UpdateKind::Error(v) => v.serialize(s),
}
@@ -385,8 +467,12 @@ fn empty_error() -> UpdateKind {
#[cfg(test)]
mod test {
use crate::types::{
- Chat, ChatFullInfo, ChatId, ChatKind, ChatPrivate, MediaKind, MediaText, Message,
- MessageCommon, MessageId, MessageKind, Update, UpdateId, UpdateKind, User, UserId,
+ Chat, ChatBoost, ChatBoostRemoved, ChatBoostSource, ChatBoostSourcePremium,
+ ChatBoostUpdated, ChatFullInfo, ChatId, ChatKind, ChatPrivate, ChatPublic,
+ LinkPreviewOptions, MediaKind, MediaText, Message, MessageCommon, MessageId, MessageKind,
+ MessageReactionCountUpdated, MessageReactionUpdated, PublicChatChannel, PublicChatKind,
+ PublicChatSupergroup, ReactionCount, ReactionType, Update, UpdateId, UpdateKind, User,
+ UserId,
};
use chrono::DateTime;
@@ -415,7 +501,8 @@ mod test {
"type":"private"
},
"date":1569518342,
- "text":"hello there"
+ "text":"hello there",
+ "link_preview_options":{"is_disabled":true}
}
}"#;
@@ -425,6 +512,18 @@ mod test {
via_bot: None,
id: MessageId(6557),
thread_id: None,
+ from: Some(User {
+ id: UserId(218_485_655),
+ is_bot: false,
+ first_name: String::from("Waffle"),
+ last_name: None,
+ username: Some(String::from("WaffleLapkin")),
+ language_code: Some(String::from("en")),
+ is_premium: false,
+ added_to_attachment_menu: false,
+ }),
+ sender_chat: None,
+ is_topic_message: false,
date,
chat: Chat {
id: ChatId(218_485_655),
@@ -435,37 +534,34 @@ mod test {
bio: None,
has_private_forwards: None,
has_restricted_voice_and_video_messages: None,
- emoji_status_custom_emoji_id: None,
}),
photo: None,
+ available_reactions: None,
pinned_message: None,
message_auto_delete_time: None,
has_hidden_members: false,
has_aggressive_anti_spam_enabled: false,
- chat_full_info: ChatFullInfo { emoji_status_expiration_date: None },
+ chat_full_info: ChatFullInfo::default(),
},
kind: MessageKind::Common(MessageCommon {
- from: Some(User {
- id: UserId(218_485_655),
- is_bot: false,
- first_name: String::from("Waffle"),
- last_name: None,
- username: Some(String::from("WaffleLapkin")),
- language_code: Some(String::from("en")),
- is_premium: false,
- added_to_attachment_menu: false,
- }),
reply_to_message: None,
- forward: None,
+ forward_origin: None,
+ external_reply: None,
+ quote: None,
edit_date: None,
media_kind: MediaKind::Text(MediaText {
text: String::from("hello there"),
entities: vec![],
+ link_preview_options: Some(LinkPreviewOptions {
+ is_disabled: true,
+ url: None,
+ prefer_small_media: false,
+ prefer_large_media: false,
+ show_above_text: false,
+ }),
}),
reply_markup: None,
- sender_chat: None,
author_signature: None,
- is_topic_message: false,
is_automatic_forward: false,
has_protected_content: false,
}),
@@ -726,4 +822,315 @@ mod test {
_ => panic!("Expected `MyChatMember`"),
}
}
+
+ #[test]
+ fn message_reaction_updated() {
+ let json = r#"
+ {
+ "update_id": 71651249,
+ "message_reaction": {
+ "chat": {
+ "id": -1002184233434,
+ "title": "Test",
+ "type": "supergroup"
+ },
+ "message_id": 35,
+ "user": {
+ "id": 1459074222,
+ "is_bot": false,
+ "first_name": "shadowchain",
+ "username": "shdwchn10",
+ "language_code": "en",
+ "is_premium": true
+ },
+ "date": 1721306082,
+ "old_reaction": [],
+ "new_reaction": [
+ {
+ "type": "emoji",
+ "emoji": "๐ญ"
+ }
+ ]
+ }
+ }
+ "#;
+
+ let expected = Update {
+ id: UpdateId(71651249),
+ kind: UpdateKind::MessageReaction(MessageReactionUpdated {
+ chat: Chat {
+ id: ChatId(-1002184233434),
+ kind: ChatKind::Public(ChatPublic {
+ title: Some("Test".to_owned()),
+ kind: PublicChatKind::Supergroup(PublicChatSupergroup {
+ username: None,
+ active_usernames: None,
+ is_forum: false,
+ sticker_set_name: None,
+ can_set_sticker_set: None,
+ permissions: None,
+ slow_mode_delay: None,
+ linked_chat_id: None,
+ location: None,
+ join_to_send_messages: None,
+ join_by_request: None,
+ }),
+ description: None,
+ invite_link: None,
+ has_protected_content: None,
+ }),
+ photo: None,
+ available_reactions: None,
+ pinned_message: None,
+ message_auto_delete_time: None,
+ has_hidden_members: false,
+ has_aggressive_anti_spam_enabled: false,
+ chat_full_info: ChatFullInfo::default(),
+ },
+ message_id: MessageId(35),
+ user: Some(User {
+ id: UserId(1459074222),
+ is_bot: false,
+ first_name: "shadowchain".to_owned(),
+ last_name: None,
+ username: Some("shdwchn10".to_owned()),
+ language_code: Some("en".to_owned()),
+ is_premium: true,
+ added_to_attachment_menu: false,
+ }),
+ actor_chat: None,
+ date: DateTime::from_timestamp(1721306082, 0).unwrap(),
+ old_reaction: vec![],
+ new_reaction: vec![ReactionType::Emoji { emoji: "๐ญ".to_owned() }],
+ }),
+ };
+
+ let actual = serde_json::from_str::(json).unwrap();
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn message_reaction_count_updated() {
+ let json = r#"
+ {
+ "update_id": 71651251,
+ "message_reaction_count": {
+ "chat": {
+ "id": -1002236736395,
+ "title": "Test",
+ "type": "channel"
+ },
+ "message_id": 36,
+ "date": 1721306391,
+ "reactions": [
+ {
+ "type": {
+ "type": "emoji",
+ "emoji": "๐ฟ"
+ },
+ "total_count": 2
+ },
+ {
+ "type": {
+ "type": "emoji",
+ "emoji": "๐ญ"
+ },
+ "total_count": 1
+ }
+ ]
+ }
+ }
+ "#;
+
+ let expected = Update {
+ id: UpdateId(71651251),
+ kind: UpdateKind::MessageReactionCount(MessageReactionCountUpdated {
+ chat: Chat {
+ id: ChatId(-1002236736395),
+ kind: ChatKind::Public(ChatPublic {
+ title: Some("Test".to_owned()),
+ kind: PublicChatKind::Channel(PublicChatChannel {
+ username: None,
+ linked_chat_id: None,
+ }),
+ description: None,
+ invite_link: None,
+ has_protected_content: None,
+ }),
+ photo: None,
+ available_reactions: None,
+ pinned_message: None,
+ message_auto_delete_time: None,
+ has_hidden_members: false,
+ has_aggressive_anti_spam_enabled: false,
+ chat_full_info: ChatFullInfo::default(),
+ },
+ message_id: MessageId(36),
+ date: DateTime::from_timestamp(1721306391, 0).unwrap(),
+ reactions: vec![
+ ReactionCount {
+ r#type: ReactionType::Emoji { emoji: "๐ฟ".to_owned() },
+ total_count: 2,
+ },
+ ReactionCount {
+ r#type: ReactionType::Emoji { emoji: "๐ญ".to_owned() },
+ total_count: 1,
+ },
+ ],
+ }),
+ };
+
+ let actual = serde_json::from_str::(json).unwrap();
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn chat_boost_updated() {
+ let json = r#"
+ {
+ "update_id": 71651297,
+ "chat_boost": {
+ "chat": {
+ "id": -1002236736395,
+ "title": "Test",
+ "type": "channel"
+ },
+ "boost": {
+ "boost_id": "4506e1b7e866e33fcbde78fe1746ec3a",
+ "add_date": 1721399621,
+ "expiration_date": 1745088963,
+ "source": {
+ "source": "premium",
+ "user": {
+ "id": 1459074222,
+ "is_bot": false,
+ "first_name": "shadowchain",
+ "username": "shdwchn10",
+ "language_code": "en",
+ "is_premium": true
+ }
+ }
+ }
+ }
+ }
+ "#;
+
+ let expected = Update {
+ id: UpdateId(71651297),
+ kind: UpdateKind::ChatBoost(ChatBoostUpdated {
+ chat: Chat {
+ id: ChatId(-1002236736395),
+ kind: ChatKind::Public(ChatPublic {
+ title: Some("Test".to_owned()),
+ kind: PublicChatKind::Channel(PublicChatChannel {
+ username: None,
+ linked_chat_id: None,
+ }),
+ description: None,
+ invite_link: None,
+ has_protected_content: None,
+ }),
+ photo: None,
+ available_reactions: None,
+ pinned_message: None,
+ message_auto_delete_time: None,
+ has_hidden_members: false,
+ has_aggressive_anti_spam_enabled: false,
+ chat_full_info: ChatFullInfo::default(),
+ },
+ boost: ChatBoost {
+ boost_id: "4506e1b7e866e33fcbde78fe1746ec3a".to_owned(),
+ add_date: DateTime::from_timestamp(1721399621, 0).unwrap(),
+ expiration_date: DateTime::from_timestamp(1745088963, 0).unwrap(),
+ source: ChatBoostSource::Premium(ChatBoostSourcePremium {
+ user: User {
+ id: UserId(1459074222),
+ is_bot: false,
+ first_name: "shadowchain".to_owned(),
+ last_name: None,
+ username: Some("shdwchn10".to_owned()),
+ language_code: Some("en".to_owned()),
+ is_premium: true,
+ added_to_attachment_menu: false,
+ },
+ }),
+ },
+ }),
+ };
+
+ let actual = serde_json::from_str::(json).unwrap();
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn chat_boost_removed() {
+ let json = r#"
+ {
+ "update_id": 71651297,
+ "removed_chat_boost": {
+ "chat": {
+ "id": -1002236736395,
+ "title": "Test",
+ "type": "channel"
+ },
+ "boost_id": "4506e1b7e866e33fcbde78fe1746ec3a",
+ "remove_date": 1721999621,
+ "source": {
+ "source": "premium",
+ "user": {
+ "id": 1459074222,
+ "is_bot": false,
+ "first_name": "shadowchain",
+ "username": "shdwchn10",
+ "language_code": "en",
+ "is_premium": true
+ }
+ }
+ }
+ }
+ "#;
+
+ let expected = Update {
+ id: UpdateId(71651297),
+ kind: UpdateKind::RemovedChatBoost(ChatBoostRemoved {
+ chat: Chat {
+ id: ChatId(-1002236736395),
+ kind: ChatKind::Public(ChatPublic {
+ title: Some("Test".to_owned()),
+ kind: PublicChatKind::Channel(PublicChatChannel {
+ username: None,
+ linked_chat_id: None,
+ }),
+ description: None,
+ invite_link: None,
+ has_protected_content: None,
+ }),
+ photo: None,
+ available_reactions: None,
+ pinned_message: None,
+ message_auto_delete_time: None,
+ has_hidden_members: false,
+ has_aggressive_anti_spam_enabled: false,
+ chat_full_info: ChatFullInfo::default(),
+ },
+ boost_id: "4506e1b7e866e33fcbde78fe1746ec3a".to_owned(),
+ remove_date: DateTime::from_timestamp(1721999621, 0).unwrap(),
+ source: ChatBoostSource::Premium(ChatBoostSourcePremium {
+ user: User {
+ id: UserId(1459074222),
+ is_bot: false,
+ first_name: "shadowchain".to_owned(),
+ last_name: None,
+ username: Some("shdwchn10".to_owned()),
+ language_code: Some("en".to_owned()),
+ is_premium: true,
+ added_to_attachment_menu: false,
+ },
+ }),
+ }),
+ };
+
+ let actual = serde_json::from_str::(json).unwrap();
+ assert_eq!(expected, actual);
+ }
}
diff --git a/crates/teloxide-core/src/types/user_chat_boosts.rs b/crates/teloxide-core/src/types/user_chat_boosts.rs
new file mode 100644
index 00000000..4b11bd4b
--- /dev/null
+++ b/crates/teloxide-core/src/types/user_chat_boosts.rs
@@ -0,0 +1,43 @@
+use serde::{Deserialize, Serialize};
+
+use crate::types::ChatBoost;
+
+/// This object represents a list of boosts added to a chat by a user.
+#[serde_with::skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct UserChatBoosts {
+ /// The list of boosts added to the chat by the user.
+ pub boosts: Vec,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn deserialize() {
+ let data = r#"
+ {
+ "boosts": [
+ {
+ "boost_id": "4506e1b7e866e33fcbde78fe1746ec3a",
+ "add_date": 1721399621,
+ "expiration_date": 1745088963,
+ "source": {
+ "source": "premium",
+ "user": {
+ "id": 1459074222,
+ "is_bot": false,
+ "first_name": "shadowchain",
+ "username": "shdwchn10",
+ "language_code": "en",
+ "is_premium": true
+ }
+ }
+ }
+ ]
+ }
+ "#;
+ serde_json::from_str::(data).unwrap();
+ }
+}
diff --git a/crates/teloxide-core/src/types/user_shared.rs b/crates/teloxide-core/src/types/user_shared.rs
deleted file mode 100644
index 1fd0cefe..00000000
--- a/crates/teloxide-core/src/types/user_shared.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use serde::{Deserialize, Serialize};
-
-use crate::types::UserId;
-
-/// Information about the chat whose identifier was shared with the bot using a
-/// [`KeyboardButtonRequestUser`] button.
-///
-/// [`KeyboardButtonRequestUser`]: crate::types::KeyboardButtonRequestUser
-#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
-pub struct UserShared {
- /// Identifier of the request.
- pub request_id: i32,
- /// Identifier of the shared user.
- pub user_id: UserId,
-}
diff --git a/crates/teloxide-core/src/types/users_shared.rs b/crates/teloxide-core/src/types/users_shared.rs
new file mode 100644
index 00000000..d522fa54
--- /dev/null
+++ b/crates/teloxide-core/src/types/users_shared.rs
@@ -0,0 +1,15 @@
+use serde::{Deserialize, Serialize};
+
+use crate::types::{RequestId, UserId};
+
+/// This object contains information about the users whose identifiers were
+/// shared with the bot using a [KeyboardButtonRequestUsers] button.
+///
+/// [KeyboardButtonRequestUsers]: crate::types::KeyboardButtonRequestUsers
+#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
+pub struct UsersShared {
+ /// Identifier of the request
+ pub request_id: RequestId,
+ /// Identifiers of the shared users
+ pub user_ids: Vec,
+}
diff --git a/crates/teloxide-core/src/util.rs b/crates/teloxide-core/src/util.rs
index a917428b..7fd8d533 100644
--- a/crates/teloxide-core/src/util.rs
+++ b/crates/teloxide-core/src/util.rs
@@ -44,6 +44,7 @@ pub(crate) fn mentioned_users_from_entities(
| Email
| PhoneNumber
| Bold
+ | Blockquote
| Italic
| Underline
| Strikethrough
diff --git a/crates/teloxide/examples/admin.rs b/crates/teloxide/examples/admin.rs
index c54b7b67..a240a3bc 100644
--- a/crates/teloxide/examples/admin.rs
+++ b/crates/teloxide/examples/admin.rs
@@ -79,7 +79,7 @@ async fn kick_user(bot: Bot, msg: Message) -> ResponseResult<()> {
match msg.reply_to_message() {
Some(replied) => {
// bot.unban_chat_member can also kicks a user from a group chat.
- bot.unban_chat_member(msg.chat.id, replied.from().unwrap().id).await?;
+ bot.unban_chat_member(msg.chat.id, replied.from.as_ref().unwrap().id).await?;
}
None => {
bot.send_message(msg.chat.id, "Use this command in reply to another message").await?;
@@ -94,7 +94,7 @@ async fn ban_user(bot: Bot, msg: Message, time: Duration) -> ResponseResult<()>
Some(replied) => {
bot.kick_chat_member(
msg.chat.id,
- replied.from().expect("Must be MessageKind::Common").id,
+ replied.from.as_ref().expect("Must be MessageKind::Common").id,
)
.until_date(msg.date + time)
.await?;
@@ -113,7 +113,7 @@ async fn mute_user(bot: Bot, msg: Message, time: Duration) -> ResponseResult<()>
Some(replied) => {
bot.restrict_chat_member(
msg.chat.id,
- replied.from().expect("Must be MessageKind::Common").id,
+ replied.from.as_ref().expect("Must be MessageKind::Common").id,
ChatPermissions::empty(),
)
.until_date(msg.date + time)
diff --git a/crates/teloxide/examples/buttons.rs b/crates/teloxide/examples/buttons.rs
index c640d708..daa538c2 100644
--- a/crates/teloxide/examples/buttons.rs
+++ b/crates/teloxide/examples/buttons.rs
@@ -116,8 +116,8 @@ async fn callback_handler(bot: Bot, q: CallbackQuery) -> Result<(), Box()
.endpoint(|msg: Message, bot: Bot, cmd: MaintainerCommands| async move {
@@ -60,7 +64,7 @@ async fn main() {
// filter only messages with dices.
Message::filter_dice().endpoint(|bot: Bot, msg: Message, dice: Dice| async move {
bot.send_message(msg.chat.id, format!("Dice value: {}", dice.value))
- .reply_to_message_id(msg.id)
+ .reply_parameters(ReplyParameters::new(msg.id))
.await?;
Ok(())
}),
@@ -121,7 +125,7 @@ async fn simple_commands_handler(
) -> Result<(), teloxide::RequestError> {
let text = match cmd {
SimpleCommand::Help => {
- if msg.from().unwrap().id == cfg.bot_maintainer {
+ if msg.from.unwrap().id == cfg.bot_maintainer {
format!(
"{}\n\n{}",
SimpleCommand::descriptions(),
@@ -134,7 +138,7 @@ async fn simple_commands_handler(
}
}
SimpleCommand::Maintainer => {
- if msg.from().unwrap().id == cfg.bot_maintainer {
+ if msg.from.as_ref().unwrap().id == cfg.bot_maintainer {
"Maintainer is you!".into()
} else if let Some(username) = cfg.maintainer_username {
format!("Maintainer is @{username}")
@@ -143,7 +147,7 @@ async fn simple_commands_handler(
}
}
SimpleCommand::MyId => {
- format!("{}", msg.from().unwrap().id)
+ format!("{}", msg.from.unwrap().id)
}
};
diff --git a/crates/teloxide/src/dispatching/dialogue/get_chat_id.rs b/crates/teloxide/src/dispatching/dialogue/get_chat_id.rs
index 632f4bbe..11fac664 100644
--- a/crates/teloxide/src/dispatching/dialogue/get_chat_id.rs
+++ b/crates/teloxide/src/dispatching/dialogue/get_chat_id.rs
@@ -16,7 +16,7 @@ impl GetChatId for Message {
impl GetChatId for CallbackQuery {
fn chat_id(&self) -> Option {
- self.message.as_ref().map(|mes| mes.chat.id)
+ self.message.as_ref().map(|mes| mes.chat().id)
}
}
diff --git a/crates/teloxide/src/dispatching/filter_ext.rs b/crates/teloxide/src/dispatching/filter_ext.rs
index 532f614f..1f7394e3 100644
--- a/crates/teloxide/src/dispatching/filter_ext.rs
+++ b/crates/teloxide/src/dispatching/filter_ext.rs
@@ -1,4 +1,6 @@
#![allow(clippy::redundant_closure_call)]
+// Required for the `filter_from` currently
+#![allow(deprecated)]
use dptree::{di::DependencyMap, Handler};
@@ -68,6 +70,7 @@ macro_rules! define_message_ext {
}
}
+// FIXME: change macro so that we can filter things without getters
// May be expanded in the future.
define_message_ext! {
// MessageCommon
@@ -92,7 +95,7 @@ define_message_ext! {
(filter_migration_from, Message::migrate_from_chat_id),
(filter_migration_to, Message::migrate_to_chat_id),
(filter_reply_to_message, Message::reply_to_message),
- (filter_forward_from, Message::forward_from),
+ (filter_forward_origin, Message::forward_origin),
// Rest variants of a MessageKind
(filter_new_chat_members, Message::new_chat_members),
(filter_left_chat_member, Message::left_chat_member),
@@ -117,6 +120,10 @@ define_message_ext! {
(filter_forum_topic_reopened, Message::forum_topic_reopened),
(filter_general_forum_topic_hidden, Message::general_forum_topic_hidden),
(filter_general_forum_topic_unhidden, Message::general_forum_topic_unhidden),
+ (filter_giveaway, Message::giveaway),
+ (filter_giveaway_completed, Message::giveaway_completed),
+ (filter_giveaway_created, Message::giveaway_created),
+ (filter_giveaway_winners, Message::giveaway_winners),
(filter_video_chat_scheduled, Message::video_chat_scheduled),
(filter_video_chat_started, Message::video_chat_started),
(filter_video_chat_ended, Message::video_chat_ended),
@@ -146,6 +153,8 @@ define_update_ext! {
(filter_edited_message, UpdateKind::EditedMessage, EditedMessage),
(filter_channel_post, UpdateKind::ChannelPost, ChannelPost),
(filter_edited_channel_post, UpdateKind::EditedChannelPost, EditedChannelPost),
+ (filter_message_reaction_updated, UpdateKind::MessageReaction, MessageReaction),
+ (filter_message_reaction_count_updated, UpdateKind::MessageReactionCount, MessageReactionCount),
(filter_inline_query, UpdateKind::InlineQuery, InlineQuery),
(filter_chosen_inline_result, UpdateKind::ChosenInlineResult, ChosenInlineResult),
(filter_callback_query, UpdateKind::CallbackQuery, CallbackQuery),
@@ -156,4 +165,6 @@ define_update_ext! {
(filter_my_chat_member, UpdateKind::MyChatMember, MyChatMember),
(filter_chat_member, UpdateKind::ChatMember, ChatMember),
(filter_chat_join_request, UpdateKind::ChatJoinRequest, ChatJoinRequest),
+ (filter_chat_boost, UpdateKind::ChatBoost, ChatBoost),
+ (filter_removed_chat_boost, UpdateKind::RemovedChatBoost, RemovedChatBoost),
}
diff --git a/crates/teloxide/src/dispatching/handler_description.rs b/crates/teloxide/src/dispatching/handler_description.rs
index eb49dda0..0398f2df 100644
--- a/crates/teloxide/src/dispatching/handler_description.rs
+++ b/crates/teloxide/src/dispatching/handler_description.rs
@@ -65,6 +65,8 @@ impl EventKind for Kind {
EditedMessage,
ChannelPost,
EditedChannelPost,
+ MessageReaction,
+ MessageReactionCount,
InlineQuery,
ChosenInlineResult,
CallbackQuery,
@@ -75,6 +77,8 @@ impl EventKind for Kind {
MyChatMember,
ChatMember,
ChatJoinRequest,
+ ChatBoost,
+ RemovedChatBoost,
]
.into_iter()
.map(Kind)
@@ -91,10 +95,12 @@ mod tests {
#[cfg(feature = "macros")]
use crate::{
self as teloxide, // fixup for the `BotCommands` macro
- dispatching::{HandlerExt, UpdateFilterExt},
+ dispatching::{handler_description::Kind, HandlerExt, UpdateFilterExt},
types::{AllowedUpdate::*, Update},
utils::command::BotCommands,
};
+ #[cfg(feature = "macros")]
+ use dptree::description::EventKind;
#[cfg(feature = "macros")]
#[derive(BotCommands, Clone)]
@@ -128,4 +134,43 @@ mod tests {
fn discussion_648() {
panic!("this test requires `macros` feature")
}
+
+ // Test that all possible updates are specified in `Kind::full_set()`
+ #[test]
+ #[cfg(feature = "macros")]
+ fn allowed_updates_full_set() {
+ let full_set = Kind::full_set();
+ let allowed_updates_reference = vec![
+ Message,
+ EditedMessage,
+ ChannelPost,
+ EditedChannelPost,
+ MessageReaction,
+ MessageReactionCount,
+ InlineQuery,
+ ChosenInlineResult,
+ CallbackQuery,
+ ShippingQuery,
+ PreCheckoutQuery,
+ Poll,
+ PollAnswer,
+ MyChatMember,
+ ChatMember,
+ ChatJoinRequest,
+ ChatBoost,
+ RemovedChatBoost,
+ ];
+
+ for update in allowed_updates_reference {
+ match update {
+ // CAUTION: Don't forget to add new `UpdateKind` to `allowed_updates_reference`!
+ Message | EditedMessage | ChannelPost | EditedChannelPost | MessageReaction
+ | MessageReactionCount | InlineQuery | ChosenInlineResult | CallbackQuery
+ | ShippingQuery | PreCheckoutQuery | Poll | PollAnswer | MyChatMember
+ | ChatMember | ChatJoinRequest | ChatBoost | RemovedChatBoost => {
+ assert!(full_set.contains(&Kind(update)))
+ }
+ }
+ }
+ }
}
diff --git a/crates/teloxide/src/lib.rs b/crates/teloxide/src/lib.rs
index 7be22d58..097643af 100644
--- a/crates/teloxide/src/lib.rs
+++ b/crates/teloxide/src/lib.rs
@@ -1,6 +1,6 @@
//! A full-featured framework that empowers you to easily build [Telegram bots]
//! using [Rust]. It handles all the difficult stuff so you can focus only on
-//! your business logic. Currently, version `6.9` of [Telegram Bot API] is
+//! your business logic. Currently, version `7.0` of [Telegram Bot API] is
//! supported.
//!
//! For a high-level overview, see [our GitHub repository](https://github.com/teloxide/teloxide).
diff --git a/crates/teloxide/src/utils/html.rs b/crates/teloxide/src/utils/html.rs
index 5fa2ee91..823ecda1 100644
--- a/crates/teloxide/src/utils/html.rs
+++ b/crates/teloxide/src/utils/html.rs
@@ -14,6 +14,16 @@ pub fn bold(s: &str) -> String {
format!("{s}")
}
+/// Applies the block quotation style to the string.
+///
+/// Passed string will not be automatically escaped because it can contain
+/// nested markup.
+#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
+ without using its output does nothing useful"]
+pub fn blockquote(s: &str) -> String {
+ format!("{s}
")
+}
+
/// Applies the italic font style to the string.
///
/// Passed string will not be automatically escaped because it can contain
diff --git a/crates/teloxide/src/utils/markdown.rs b/crates/teloxide/src/utils/markdown.rs
index ad6eb26c..ec007c29 100644
--- a/crates/teloxide/src/utils/markdown.rs
+++ b/crates/teloxide/src/utils/markdown.rs
@@ -14,6 +14,16 @@ pub fn bold(s: &str) -> String {
format!("*{s}*")
}
+/// Applies the block quotation style to the string.
+///
+/// Passed string will not be automatically escaped because it can contain
+/// nested markup.
+#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
+ without using its output does nothing useful"]
+pub fn blockquote(s: &str) -> String {
+ format!(">{s}")
+}
+
/// Applies the italic font style to the string.
///
/// Can be safely used with `utils::markdown::underline()`.