diff --git a/CHANGELOG.md b/CHANGELOG.md index be577e57..f4a82ef4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `filter_web_app_data` - Implement `PostgresStorage`, a persistent dialogue storage based on [PostgreSQL](https://www.postgresql.org/)([PR 996](https://github.com/teloxide/teloxide/pull/996)). - Implement `GetChatId` for `teloxide_core::types::{Chat, ChatJoinRequest, ChatMemberUpdated}`. +- Add `MessageExt::filter_story` method for the corresponding `MediaKind::Story` variant ([PR 1087](https://github.com/teloxide/teloxide/pull/1087)) ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 0ba28d75..41efcc3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -531,12 +531,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "flume" version = "0.11.0" diff --git a/README.md b/README.md index 9d335b9e..6eda6f18 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - + diff --git a/crates/teloxide-core/CHANGELOG.md b/crates/teloxide-core/CHANGELOG.md index 36317098..951ca6b0 100644 --- a/crates/teloxide-core/CHANGELOG.md +++ b/crates/teloxide-core/CHANGELOG.md @@ -79,7 +79,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `set_my_name` - `get_my_name` - Add the ability to specify custom emoji entities using `HTML` and `MarkdownV2` formatting options for bots that purchased additional usernames on [Fragment](https://fragment.com/) - +- Support for TBA 6.8 ([#1087](pr1087)) + - Add the `MediaKind::Story` + - Add new fields + - `PollAnswer::voter` to support anonymous poll answers in chats + - `emoji_status_expiration_date` to `Chat` as part of the future `ChatFullInfo` type TBA type + - Add the `unpin_all_general_forum_topic_messages` method [pr851]: https://github.com/teloxide/teloxide/pull/851 [pr887]: https://github.com/teloxide/teloxide/pull/887 @@ -87,6 +92,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [pr982]: https://github.com/teloxide/teloxide/pull/982 [pr1040]: https://github.com/teloxide/teloxide/pull/1040 [pr1086]: https://github.com/teloxide/teloxide/pull/1086 +[pr1087]: https://github.com/teloxide/teloxide/pull/1087 ### Fixed diff --git a/crates/teloxide-core/README.md b/crates/teloxide-core/README.md index 3ae43842..6a382bb3 100644 --- a/crates/teloxide-core/README.md +++ b/crates/teloxide-core/README.md @@ -12,7 +12,7 @@ - + diff --git a/crates/teloxide-core/schema.ron b/crates/teloxide-core/schema.ron index 2e876287..cc649e5d 100644 --- a/crates/teloxide-core/schema.ron +++ b/crates/teloxide-core/schema.ron @@ -39,7 +39,7 @@ //! [github]: https://github.com/WaffleLapkin/tg-methods-schema Schema( - api_version: ApiVersion(ver: "6.7", date: "April 21, 2023"), + api_version: ApiVersion(ver: "6.8", date: "August 18, 2023"), methods: [ Method( names: ("getUpdates", "GetUpdates", "get_updates"), @@ -2801,6 +2801,20 @@ Schema( ), ], ), + Method( + names: ("unpinAllGeneralForumTopicMessages", "UnpinAllGeneralForumTopicMessages", "unpin_all_general_forum_topic_messages"), + return_ty: True, + doc: Doc(md: "Use this method to clear the list of pinned messages in a General forum topic. The bot must be an administrator in the chat for this to work and must have the _can\\_pin\\_messages_ administrator right in the supergroup. Returns True on success."), + tg_doc: "https://core.telegram.org/bots/api#unpinallgeneralforumtopicmessages", + tg_category: "Available methods", + params: [ + Param( + name: "chat_id", + ty: RawTy("Recipient"), + descr: Doc(md: "Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername)") + ), + ], + ), Method( names: ("answerCallbackQuery", "AnswerCallbackQuery", "answer_callback_query"), return_ty: True, diff --git a/crates/teloxide-core/src/adaptors/cache_me.rs b/crates/teloxide-core/src/adaptors/cache_me.rs index 84b4887e..eecac8bc 100644 --- a/crates/teloxide-core/src/adaptors/cache_me.rs +++ b/crates/teloxide-core/src/adaptors/cache_me.rs @@ -156,6 +156,7 @@ where reopen_general_forum_topic, hide_general_forum_topic, unhide_general_forum_topic, + unpin_all_general_forum_topic_messages, answer_callback_query, set_my_commands, get_my_commands, diff --git a/crates/teloxide-core/src/adaptors/erased.rs b/crates/teloxide-core/src/adaptors/erased.rs index 7e4f390c..509cbdaa 100644 --- a/crates/teloxide-core/src/adaptors/erased.rs +++ b/crates/teloxide-core/src/adaptors/erased.rs @@ -251,6 +251,7 @@ where reopen_general_forum_topic, hide_general_forum_topic, unhide_general_forum_topic, + unpin_all_general_forum_topic_messages, answer_callback_query, set_my_commands, get_my_commands, @@ -699,6 +700,11 @@ trait ErasableRequester<'a> { chat_id: Recipient, ) -> ErasedRequest<'a, UnhideGeneralForumTopic, Self::Err>; + fn unpin_all_general_forum_topic_messages( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, UnpinAllGeneralForumTopicMessages, Self::Err>; + fn answer_callback_query( &self, callback_query_id: String, @@ -1500,6 +1506,13 @@ where Requester::unhide_general_forum_topic(self, chat_id).erase() } + fn unpin_all_general_forum_topic_messages( + &self, + chat_id: Recipient, + ) -> ErasedRequest<'a, UnpinAllGeneralForumTopicMessages, Self::Err> { + Requester::unpin_all_general_forum_topic_messages(self, chat_id).erase() + } + fn answer_callback_query( &self, callback_query_id: String, diff --git a/crates/teloxide-core/src/adaptors/parse_mode.rs b/crates/teloxide-core/src/adaptors/parse_mode.rs index d7540115..216893a4 100644 --- a/crates/teloxide-core/src/adaptors/parse_mode.rs +++ b/crates/teloxide-core/src/adaptors/parse_mode.rs @@ -231,12 +231,13 @@ where close_forum_topic, reopen_forum_topic, delete_forum_topic, + unpin_all_forum_topic_messages, edit_general_forum_topic, close_general_forum_topic, reopen_general_forum_topic, hide_general_forum_topic, unhide_general_forum_topic, - unpin_all_forum_topic_messages, + unpin_all_general_forum_topic_messages, answer_callback_query, set_my_commands, get_my_commands, diff --git a/crates/teloxide-core/src/adaptors/throttle/requester_impl.rs b/crates/teloxide-core/src/adaptors/throttle/requester_impl.rs index 436f6488..28d82051 100644 --- a/crates/teloxide-core/src/adaptors/throttle/requester_impl.rs +++ b/crates/teloxide-core/src/adaptors/throttle/requester_impl.rs @@ -139,6 +139,7 @@ where reopen_general_forum_topic, hide_general_forum_topic, unhide_general_forum_topic, + unpin_all_general_forum_topic_messages, answer_callback_query, set_my_commands, get_my_commands, diff --git a/crates/teloxide-core/src/adaptors/trace.rs b/crates/teloxide-core/src/adaptors/trace.rs index 466674d1..bf4141bc 100644 --- a/crates/teloxide-core/src/adaptors/trace.rs +++ b/crates/teloxide-core/src/adaptors/trace.rs @@ -185,6 +185,7 @@ where reopen_general_forum_topic, hide_general_forum_topic, unhide_general_forum_topic, + unpin_all_general_forum_topic_messages, answer_callback_query, set_my_commands, get_my_commands, diff --git a/crates/teloxide-core/src/bot/api.rs b/crates/teloxide-core/src/bot/api.rs index 35c6231c..6ff8b81d 100644 --- a/crates/teloxide-core/src/bot/api.rs +++ b/crates/teloxide-core/src/bot/api.rs @@ -802,6 +802,22 @@ impl Requester for Bot { ) } + type UnpinAllGeneralForumTopicMessages = + JsonRequest; + + fn unpin_all_general_forum_topic_messages( + &self, + chat_id: C, + ) -> Self::UnpinAllGeneralForumTopicMessages + where + C: Into, + { + Self::UnpinAllGeneralForumTopicMessages::new( + self.clone(), + payloads::UnpinAllGeneralForumTopicMessages::new(chat_id), + ) + } + type AnswerCallbackQuery = JsonRequest; fn answer_callback_query(&self, callback_query_id: C) -> Self::AnswerCallbackQuery diff --git a/crates/teloxide-core/src/local_macros.rs b/crates/teloxide-core/src/local_macros.rs index 9ce59f58..8207bd01 100644 --- a/crates/teloxide-core/src/local_macros.rs +++ b/crates/teloxide-core/src/local_macros.rs @@ -1009,6 +1009,14 @@ macro_rules! requester_forward { $body!(unhide_general_forum_topic this (chat_id: C)) } }; + (@method unpin_all_general_forum_topic_messages $body:ident $ty:ident) => { + type UnpinAllGeneralForumTopicMessages = $ty![UnpinAllGeneralForumTopicMessages]; + + fn unpin_all_general_forum_topic_messages(&self, chat_id: C) -> Self::UnpinAllGeneralForumTopicMessages where C: Into { + let this = self; + $body!(unpin_all_general_forum_topic_messages this (chat_id: C)) + } + }; (@method answer_callback_query $body:ident $ty:ident) => { type AnswerCallbackQuery = $ty![AnswerCallbackQuery]; diff --git a/crates/teloxide-core/src/payloads.rs b/crates/teloxide-core/src/payloads.rs index 97d2e61d..7f3cf308 100644 --- a/crates/teloxide-core/src/payloads.rs +++ b/crates/teloxide-core/src/payloads.rs @@ -135,6 +135,7 @@ mod unban_chat_sender_chat; mod unhide_general_forum_topic; mod unpin_all_chat_messages; mod unpin_all_forum_topic_messages; +mod unpin_all_general_forum_topic_messages; mod unpin_chat_message; mod upload_sticker_file; @@ -276,6 +277,9 @@ pub use unpin_all_chat_messages::{UnpinAllChatMessages, UnpinAllChatMessagesSett pub use unpin_all_forum_topic_messages::{ UnpinAllForumTopicMessages, UnpinAllForumTopicMessagesSetters, }; +pub use unpin_all_general_forum_topic_messages::{ + UnpinAllGeneralForumTopicMessages, UnpinAllGeneralForumTopicMessagesSetters, +}; pub use unpin_chat_message::{UnpinChatMessage, UnpinChatMessageSetters}; pub use upload_sticker_file::{UploadStickerFile, UploadStickerFileSetters}; // END BLOCK payload_modules diff --git a/crates/teloxide-core/src/payloads/setters.rs b/crates/teloxide-core/src/payloads/setters.rs index 6faa7d16..9771e38b 100644 --- a/crates/teloxide-core/src/payloads/setters.rs +++ b/crates/teloxide-core/src/payloads/setters.rs @@ -47,6 +47,6 @@ pub use crate::payloads::{ StopMessageLiveLocationInlineSetters as _, StopMessageLiveLocationSetters as _, StopPollSetters as _, UnbanChatMemberSetters as _, UnbanChatSenderChatSetters as _, UnhideGeneralForumTopicSetters as _, UnpinAllChatMessagesSetters as _, - UnpinAllForumTopicMessagesSetters as _, UnpinChatMessageSetters as _, - UploadStickerFileSetters as _, + UnpinAllForumTopicMessagesSetters as _, UnpinAllGeneralForumTopicMessagesSetters as _, + UnpinChatMessageSetters as _, UploadStickerFileSetters as _, }; diff --git a/crates/teloxide-core/src/payloads/unpin_all_general_forum_topic_messages.rs b/crates/teloxide-core/src/payloads/unpin_all_general_forum_topic_messages.rs new file mode 100644 index 00000000..776b066a --- /dev/null +++ b/crates/teloxide-core/src/payloads/unpin_all_general_forum_topic_messages.rs @@ -0,0 +1,16 @@ +//! Generated by `codegen_payloads`, do not edit by hand. + +use serde::Serialize; + +use crate::types::{Recipient, True}; + +impl_payload! { + /// Use this method to clear the list of pinned messages in a General forum topic. The bot must be an administrator in the chat for this to work and must have the _can\_pin\_messages_ administrator right in the supergroup. Returns True on success. + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] + pub UnpinAllGeneralForumTopicMessages (UnpinAllGeneralForumTopicMessagesSetters) => True { + required { + /// Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) + pub chat_id: Recipient [into], + } + } +} diff --git a/crates/teloxide-core/src/requests/requester.rs b/crates/teloxide-core/src/requests/requester.rs index 58adb2b6..f178ab8b 100644 --- a/crates/teloxide-core/src/requests/requester.rs +++ b/crates/teloxide-core/src/requests/requester.rs @@ -758,6 +758,19 @@ pub trait Requester { where C: Into; + type UnpinAllGeneralForumTopicMessages: Request< + Payload = UnpinAllGeneralForumTopicMessages, + Err = Self::Err, + >; + + /// For Telegram documentation see [`UnpinAllGeneralForumTopicMessages`]. + fn unpin_all_general_forum_topic_messages( + &self, + chat_id: C, + ) -> Self::UnpinAllGeneralForumTopicMessages + where + C: Into; + type AnswerCallbackQuery: Request; /// For Telegram documentation see [`AnswerCallbackQuery`]. @@ -1297,6 +1310,7 @@ macro_rules! forward_all { reopen_general_forum_topic, hide_general_forum_topic, unhide_general_forum_topic, + unpin_all_general_forum_topic_messages, answer_callback_query, set_my_commands, get_my_commands, diff --git a/crates/teloxide-core/src/types.rs b/crates/teloxide-core/src/types.rs index 26128929..f142825d 100644 --- a/crates/teloxide-core/src/types.rs +++ b/crates/teloxide-core/src/types.rs @@ -13,6 +13,7 @@ pub use callback_query::*; pub use chat::*; pub use chat_action::*; pub use chat_administrator_rights::*; +pub use chat_full_info::*; pub use chat_invite_link::*; pub use chat_join_request::*; pub use chat_location::*; @@ -105,6 +106,7 @@ pub use shipping_option::*; pub use shipping_query::*; pub use sticker::*; pub use sticker_set::*; +pub use story::*; pub use successful_payment::*; pub use switch_inline_query_chosen_chat::*; pub use target_message::*; @@ -141,6 +143,7 @@ mod callback_query; mod chat; mod chat_action; mod chat_administrator_rights; +mod chat_full_info; mod chat_invite_link; mod chat_join_request; mod chat_location; @@ -206,6 +209,7 @@ mod shipping_option; mod shipping_query; mod sticker; mod sticker_set; +mod story; mod successful_payment; mod switch_inline_query_chosen_chat; mod target_message; diff --git a/crates/teloxide-core/src/types/chat.rs b/crates/teloxide-core/src/types/chat.rs index 2bd38dd9..6f12745d 100644 --- a/crates/teloxide-core/src/types/chat.rs +++ b/crates/teloxide-core/src/types/chat.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::types::{ - ChatId, ChatLocation, ChatPermissions, ChatPhoto, Message, Seconds, True, User, + ChatFullInfo, ChatId, ChatLocation, ChatPermissions, ChatPhoto, Message, Seconds, True, User, }; /// This object represents a chat. @@ -47,6 +47,9 @@ pub struct Chat { /// [`GetChat`]: crate::payloads::GetChat #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub has_aggressive_anti_spam_enabled: bool, + + #[serde(flatten)] + pub chat_full_info: ChatFullInfo, } #[serde_with_macros::skip_serializing_none] @@ -615,6 +618,7 @@ mod tests { message_auto_delete_time: None, has_hidden_members: false, has_aggressive_anti_spam_enabled: false, + chat_full_info: ChatFullInfo { emoji_status_expiration_date: None }, }; let actual = from_str(r#"{"id":-1,"type":"channel","username":"channel_name"}"#).unwrap(); assert_eq!(expected, actual); @@ -639,6 +643,7 @@ mod tests { message_auto_delete_time: None, has_hidden_members: false, has_aggressive_anti_spam_enabled: false, + chat_full_info: ChatFullInfo { emoji_status_expiration_date: None } }, from_str(r#"{"id":0,"type":"private","username":"username","first_name":"Anon"}"#) .unwrap() @@ -663,6 +668,7 @@ mod tests { message_auto_delete_time: None, has_hidden_members: false, has_aggressive_anti_spam_enabled: false, + chat_full_info: ChatFullInfo { emoji_status_expiration_date: None }, }; let json = to_string(&chat).unwrap(); diff --git a/crates/teloxide-core/src/types/chat_full_info.rs b/crates/teloxide-core/src/types/chat_full_info.rs new file mode 100644 index 00000000..9857ce7d --- /dev/null +++ b/crates/teloxide-core/src/types/chat_full_info.rs @@ -0,0 +1,35 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +// TODO: in the TBA7.3 the Chat will be splitted into Chat and ChatInfo +// Currently it's just a container for the some fields of the Chat struct +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ChatFullInfo { + /// Expiration date of the emoji status of the chat or the other party in a + /// private chat, in Unix time, if any + #[serde(default, with = "crate::types::serde_opt_date_from_unix_timestamp")] + pub emoji_status_expiration_date: Option>, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_chat_full_info_de() { + assert_eq!( + serde_json::from_str::("{}").unwrap(), + ChatFullInfo { emoji_status_expiration_date: None } + ); + assert_eq!( + serde_json::from_str::( + r#"{ + "emoji_status_expiration_date": 1720708004 + }"# + ) + .unwrap(), + ChatFullInfo { emoji_status_expiration_date: DateTime::from_timestamp(1720708004, 0) } + ); + } +} diff --git a/crates/teloxide-core/src/types/message.rs b/crates/teloxide-core/src/types/message.rs index b80c4bbb..0eb7f353 100644 --- a/crates/teloxide-core/src/types/message.rs +++ b/crates/teloxide-core/src/types/message.rs @@ -9,9 +9,9 @@ use crate::types::{ ForumTopicClosed, ForumTopicCreated, ForumTopicEdited, ForumTopicReopened, Game, GeneralForumTopicHidden, GeneralForumTopicUnhidden, InlineKeyboardMarkup, Invoice, Location, MessageAutoDeleteTimerChanged, MessageEntity, MessageEntityRef, MessageId, PassportData, - PhotoSize, Poll, ProximityAlertTriggered, Sticker, SuccessfulPayment, ThreadId, True, User, - UserShared, Venue, Video, VideoChatEnded, VideoChatParticipantsInvited, VideoChatScheduled, - VideoChatStarted, VideoNote, Voice, WebAppData, WriteAccessAllowed, + PhotoSize, Poll, ProximityAlertTriggered, Sticker, Story, SuccessfulPayment, ThreadId, True, + User, UserShared, Venue, Video, VideoChatEnded, VideoChatParticipantsInvited, + VideoChatScheduled, VideoChatStarted, VideoNote, Voice, WebAppData, WriteAccessAllowed, }; /// This object represents a message. @@ -366,6 +366,7 @@ pub enum MediaKind { Photo(MediaPhoto), Poll(MediaPoll), Sticker(MediaSticker), + Story(MediaStory), Text(MediaText), Video(MediaVideo), VideoNote(MediaVideoNote), @@ -494,6 +495,13 @@ pub struct MediaSticker { pub sticker: Sticker, } +#[serde_with_macros::skip_serializing_none] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MediaStory { + /// Message is a forwarded story + pub story: Story, +} + #[serde_with_macros::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct MediaText { @@ -668,14 +676,14 @@ mod getters { use crate::types::{ self, message::MessageKind::*, Chat, ChatId, ChatMigration, Forward, ForwardedFrom, MediaAnimation, MediaAudio, MediaContact, MediaDocument, MediaGame, MediaKind, - MediaLocation, MediaPhoto, MediaPoll, MediaSticker, MediaText, MediaVenue, MediaVideo, - MediaVideoNote, MediaVoice, Message, MessageChannelChatCreated, MessageChatShared, - MessageCommon, MessageConnectedWebsite, MessageDeleteChatPhoto, MessageDice, MessageEntity, - MessageGroupChatCreated, MessageId, MessageInvoice, MessageLeftChatMember, - MessageNewChatMembers, MessageNewChatPhoto, MessageNewChatTitle, MessagePassportData, - MessagePinned, MessageProximityAlertTriggered, MessageSuccessfulPayment, - MessageSupergroupChatCreated, MessageUserShared, MessageVideoChatParticipantsInvited, - PhotoSize, User, + MediaLocation, MediaPhoto, MediaPoll, MediaSticker, MediaStory, MediaText, MediaVenue, + MediaVideo, MediaVideoNote, MediaVoice, Message, MessageChannelChatCreated, + MessageChatShared, MessageCommon, MessageConnectedWebsite, MessageDeleteChatPhoto, + MessageDice, MessageEntity, MessageGroupChatCreated, MessageId, MessageInvoice, + MessageLeftChatMember, MessageNewChatMembers, MessageNewChatPhoto, MessageNewChatTitle, + MessagePassportData, MessagePinned, MessageProximityAlertTriggered, + MessageSuccessfulPayment, MessageSupergroupChatCreated, MessageUserShared, + MessageVideoChatParticipantsInvited, PhotoSize, User, }; use super::{ @@ -899,6 +907,7 @@ mod getters { | MediaKind::Location(_) | MediaKind::Poll(_) | MediaKind::Sticker(_) + | MediaKind::Story(_) | MediaKind::Text(_) | MediaKind::VideoNote(_) | MediaKind::Voice(_) @@ -973,6 +982,17 @@ mod getters { } } + #[must_use] + pub fn story(&self) -> Option<&types::Story> { + match &self.kind { + Common(MessageCommon { + media_kind: MediaKind::Story(MediaStory { story, .. }), + .. + }) => Some(story), + _ => None, + } + } + #[must_use] pub fn video(&self) -> Option<&types::Video> { match &self.kind { @@ -1763,7 +1783,8 @@ mod tests { has_aggressive_anti_spam_enabled: false, pinned_message: None, message_auto_delete_time: None, - has_hidden_members: false + has_hidden_members: false, + chat_full_info: ChatFullInfo { emoji_status_expiration_date: None } }, kind: MessageKind::ChatShared(MessageChatShared { chat_shared: ChatShared { request_id: 348349, chat_id: ChatId(384939) } @@ -1996,6 +2017,7 @@ mod tests { pinned_message: None, has_hidden_members: false, has_aggressive_anti_spam_enabled: false, + chat_full_info: ChatFullInfo { emoji_status_expiration_date: None }, }; assert!(message.from().unwrap().is_anonymous()); diff --git a/crates/teloxide-core/src/types/poll_answer.rs b/crates/teloxide-core/src/types/poll_answer.rs index e87e0878..f40b55fb 100644 --- a/crates/teloxide-core/src/types/poll_answer.rs +++ b/crates/teloxide-core/src/types/poll_answer.rs @@ -1,17 +1,117 @@ -use crate::types::User; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; + +use crate::types::{Chat, User}; #[serde_with_macros::skip_serializing_none] -#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct PollAnswer { /// Unique poll identifier. pub poll_id: String, - /// The user, who changed the answer to the poll. - pub user: User, + /// If the voter is anonymous, stores the chat that changed the answer to + /// the poll. + /// + /// If the voter isn't anonymous, stores the user that changed + /// the answer to the poll + #[serde(deserialize_with = "deserialize_voter", flatten)] + pub voter: Voter, /// 0-based identifiers of answer options, chosen by the user. /// /// May be empty if the user retracted their vote. pub option_ids: Vec, } + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Voter { + Chat(Chat), + User(User), +} + +impl Voter { + #[must_use] + pub fn chat(&self) -> Option<&Chat> { + match self { + Self::Chat(chat) => Some(chat), + _ => None, + } + } + + #[must_use] + pub fn user(&self) -> Option<&User> { + match self { + Self::User(user) => Some(user), + _ => None, + } + } +} + +/// These fields `chat` and `user` from the original [`PollAnswer`] should be +/// exclusive, but in cases when the `voter_chat` is presented the `user` isn't +/// `None`, but rather actual value for backward compatibility, the field `user` +/// in such objects will contain the user 136817688 (@Channel_Bot). +#[derive(Deserialize)] +struct VoterDe { + /// The chat that changed the answer to the poll, if the voter is anonymous + pub voter_chat: Option, + + /// The user that changed the answer to the poll, if the voter isn't + /// anonymous + pub user: Option, +} + +fn deserialize_voter<'d, D: Deserializer<'d>>(d: D) -> Result { + let VoterDe { voter_chat, user } = VoterDe::deserialize(d)?; + Ok(voter_chat.map(Voter::Chat).or(user.map(Voter::User)).unwrap()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_poll_answer_with_user_de() { + let json = r#"{ + "poll_id":"POLL_ID", + "user": {"id":42,"is_bot":false,"first_name":"blah"}, + "option_ids": [] + }"#; + + let poll_answer: PollAnswer = serde_json::from_str(json).unwrap(); + assert!(matches!(poll_answer.voter, Voter::User(_))); + } + + #[test] + fn test_poll_answer_with_voter_chat_de() { + let json = r#"{ + "poll_id":"POLL_ID", + "voter_chat": { + "id": -1001160242915, + "title": "a", + "type": "group" + }, + "option_ids": [] + }"#; + + let poll_answer: PollAnswer = serde_json::from_str(json).unwrap(); + assert!(matches!(poll_answer.voter, Voter::Chat(_))); + } + + #[test] + fn test_poll_answer_with_both_user_and_voter_chat_de() { + let json = r#"{ + "poll_id":"POLL_ID", + "voter_chat": { + "id": -1001160242915, + "title": "a", + "type": "group" + }, + "user": {"id":136817688,"is_bot":true,"first_name":"Channel_Bot"}, + "option_ids": [] + }"#; + + let poll_answer: PollAnswer = serde_json::from_str(json).unwrap(); + assert!(matches!(poll_answer.voter, Voter::Chat(_))); + } +} diff --git a/crates/teloxide-core/src/types/story.rs b/crates/teloxide-core/src/types/story.rs new file mode 100644 index 00000000..a59e1568 --- /dev/null +++ b/crates/teloxide-core/src/types/story.rs @@ -0,0 +1,5 @@ +use serde::{Deserialize, Serialize}; + +/// TBA 6.8: currently it holds no information +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Story {} diff --git a/crates/teloxide-core/src/types/update.rs b/crates/teloxide-core/src/types/update.rs index caff4c50..adc8056a 100644 --- a/crates/teloxide-core/src/types/update.rs +++ b/crates/teloxide-core/src/types/update.rs @@ -136,7 +136,7 @@ impl Update { InlineQuery(query) => &query.from, ShippingQuery(query) => &query.from, PreCheckoutQuery(query) => &query.from, - PollAnswer(answer) => &answer.user, + PollAnswer(answer) => return answer.voter.user(), MyChatMember(m) | ChatMember(m) => &m.from, ChatJoinRequest(r) => &r.from, @@ -198,7 +198,12 @@ impl Update { UpdateKind::PreCheckoutQuery(query) => i1(once(&query.from)), UpdateKind::Poll(poll) => i3(poll.mentioned_users()), - UpdateKind::PollAnswer(answer) => i1(once(&answer.user)), + UpdateKind::PollAnswer(answer) => { + if let Some(user) = answer.voter.user() { + return i1(once(user)); + } + i6(empty()) + } UpdateKind::MyChatMember(member) | UpdateKind::ChatMember(member) => { i4(member.mentioned_users()) @@ -380,8 +385,8 @@ fn empty_error() -> UpdateKind { #[cfg(test)] mod test { use crate::types::{ - Chat, ChatId, ChatKind, ChatPrivate, MediaKind, MediaText, Message, MessageCommon, - MessageId, MessageKind, Update, UpdateId, UpdateKind, User, UserId, + Chat, ChatFullInfo, ChatId, ChatKind, ChatPrivate, MediaKind, MediaText, Message, + MessageCommon, MessageId, MessageKind, Update, UpdateId, UpdateKind, User, UserId, }; use chrono::DateTime; @@ -437,6 +442,7 @@ mod test { message_auto_delete_time: None, has_hidden_members: false, has_aggressive_anti_spam_enabled: false, + chat_full_info: ChatFullInfo { emoji_status_expiration_date: None }, }, kind: MessageKind::Common(MessageCommon { from: Some(User { diff --git a/crates/teloxide/src/dispatching/filter_ext.rs b/crates/teloxide/src/dispatching/filter_ext.rs index c0ab8e04..532f614f 100644 --- a/crates/teloxide/src/dispatching/filter_ext.rs +++ b/crates/teloxide/src/dispatching/filter_ext.rs @@ -83,6 +83,7 @@ define_message_ext! { (filter_photo, Message::photo), (filter_poll, Message::poll), (filter_sticker, Message::sticker), + (filter_story, Message::story), (filter_text, Message::text), (filter_video, Message::video), (filter_video_note, Message::video_note),