diff --git a/crates/teloxide-core/src/types.rs b/crates/teloxide-core/src/types.rs index 3359badc..591e0ca3 100644 --- a/crates/teloxide-core/src/types.rs +++ b/crates/teloxide-core/src/types.rs @@ -41,6 +41,7 @@ pub use game::*; pub use game_high_score::*; pub use general_forum_topic_hidden::*; pub use general_forum_topic_unhidden::*; +pub use inaccessible_message::*; pub use inline_keyboard_button::*; pub use inline_keyboard_markup::*; pub use inline_query::*; @@ -79,6 +80,7 @@ pub use label_price::*; pub use location::*; pub use login_url::*; pub use mask_position::*; +pub use maybe_inaccessible_message::*; pub use me::*; pub use menu_button::*; pub use message::*; @@ -170,6 +172,7 @@ mod game; mod game_high_score; mod general_forum_topic_hidden; mod general_forum_topic_unhidden; +mod inaccessible_message; mod inline_keyboard_button; mod inline_keyboard_markup; mod inline_query_results_button; @@ -186,6 +189,7 @@ mod label_price; mod location; mod login_url; mod mask_position; +mod maybe_inaccessible_message; mod me; mod menu_button; mod message; diff --git a/crates/teloxide-core/src/types/callback_query.rs b/crates/teloxide-core/src/types/callback_query.rs index bb765aa0..72fbe80f 100644 --- a/crates/teloxide-core/src/types/callback_query.rs +++ b/crates/teloxide-core/src/types/callback_query.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::types::{Message, User}; +use crate::types::{MaybeInaccessibleMessage, Message, User}; /// This object represents an incoming callback query from a callback button in /// an [inline keyboard]. @@ -24,10 +24,9 @@ pub struct CallbackQuery { /// A sender. pub from: User, - /// A message with the callback button that originated the query. Note that - /// message content and message date will not be available if the message - /// is too old. - pub message: Option, + /// Message sent by the bot with the callback button that originated the + /// query + pub message: Option, /// An identifier of the message sent via the bot in inline mode, that /// originated the query. @@ -58,7 +57,13 @@ impl CallbackQuery { use crate::util::flatten; use std::iter::once; - once(&self.from).chain(flatten(self.message.as_ref().map(Message::mentioned_users))) + once(&self.from).chain(flatten( + self.message + .as_ref() + // If we can access the message + .and_then(|maybe| maybe.message()) + .map(Message::mentioned_users), + )) } } diff --git a/crates/teloxide-core/src/types/inaccessible_message.rs b/crates/teloxide-core/src/types/inaccessible_message.rs new file mode 100644 index 00000000..8e1cd6d2 --- /dev/null +++ b/crates/teloxide-core/src/types/inaccessible_message.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::{Chat, MessageId}; + +/// This object describes a message that was deleted or is otherwise +/// inaccessible to the bot. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct InaccessibleMessage { + /// Chat the message belonged to + pub chat: Chat, + /// Unique message identifier inside the chat + pub message_id: MessageId, +} diff --git a/crates/teloxide-core/src/types/maybe_inaccessible_message.rs b/crates/teloxide-core/src/types/maybe_inaccessible_message.rs new file mode 100644 index 00000000..ff0d09ed --- /dev/null +++ b/crates/teloxide-core/src/types/maybe_inaccessible_message.rs @@ -0,0 +1,98 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::{Chat, InaccessibleMessage, Message, MessageId}; + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(untagged)] +pub enum MaybeInaccessibleMessage { + Inaccessible(InaccessibleMessage), + Regular(Message), +} + +impl MaybeInaccessibleMessage { + pub fn id(&self) -> MessageId { + match self { + Self::Inaccessible(i_message) => i_message.message_id, + Self::Regular(message) => message.id, + } + } + + pub fn message(&self) -> Option<&Message> { + match self { + Self::Regular(message) => Some(message), + Self::Inaccessible(_) => None, + } + } + + pub fn chat_and_id(&self) -> (&Chat, MessageId) { + (self.chat(), self.id()) + } + + pub fn chat(&self) -> &Chat { + match self { + Self::Regular(message) => &message.chat, + Self::Inaccessible(i_message) => &i_message.chat, + } + } +} + +impl<'de> Deserialize<'de> for MaybeInaccessibleMessage { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let message: Message = Message::deserialize(deserializer)?; + + // Thank you, TBA 7.0 authors! + if message.date.timestamp() == 0 { + return Ok(MaybeInaccessibleMessage::Inaccessible(InaccessibleMessage { + chat: message.chat, + message_id: message.id, + })); + } + Ok(MaybeInaccessibleMessage::Regular(message)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_inaccessible_message() { + let json = r#"{ + "chat": { + "id": 42, + "first_name": "Вадим Игоревич", + "last_name": "Сырцев", + "username": "syrtcevvi", + "type": "private" + }, + "message_id": 4, + "date": 0 + }"#; + + let inaccessible_message = serde_json::from_str::(json); + assert!(inaccessible_message.is_ok()); + assert!(matches!(inaccessible_message.unwrap(), MaybeInaccessibleMessage::Inaccessible(_))); + } + + #[test] + fn test_regular_message() { + let json = r#"{ + "chat": { + "id": 42, + "first_name": "Вадим Игоревич", + "last_name": "Сырцев", + "username": "syrtcevvi", + "type": "private" + }, + "message_id": 4, + "date": 1 + }"#; + + let regular_message = serde_json::from_str::(json); + assert!(regular_message.is_ok()); + assert!(matches!(regular_message.unwrap(), MaybeInaccessibleMessage::Regular(_))); + } +} diff --git a/crates/teloxide-core/src/types/message.rs b/crates/teloxide-core/src/types/message.rs index 575d2a82..48cde3c6 100644 --- a/crates/teloxide-core/src/types/message.rs +++ b/crates/teloxide-core/src/types/message.rs @@ -8,10 +8,11 @@ use crate::types::{ Animation, Audio, BareChatId, Chat, ChatId, ChatShared, Contact, Dice, Document, ForumTopicClosed, ForumTopicCreated, ForumTopicEdited, ForumTopicReopened, Game, GeneralForumTopicHidden, GeneralForumTopicUnhidden, InlineKeyboardMarkup, Invoice, Location, - MessageAutoDeleteTimerChanged, MessageEntity, MessageEntityRef, MessageId, MessageOrigin, - PassportData, PhotoSize, Poll, ProximityAlertTriggered, Sticker, Story, SuccessfulPayment, - ThreadId, True, User, UsersShared, Venue, Video, VideoChatEnded, VideoChatParticipantsInvited, - VideoChatScheduled, VideoChatStarted, VideoNote, Voice, WebAppData, WriteAccessAllowed, + MaybeInaccessibleMessage, MessageAutoDeleteTimerChanged, MessageEntity, MessageEntityRef, + MessageId, MessageOrigin, PassportData, PhotoSize, Poll, ProximityAlertTriggered, Sticker, + Story, SuccessfulPayment, ThreadId, True, User, UsersShared, Venue, Video, VideoChatEnded, + VideoChatParticipantsInvited, VideoChatScheduled, VideoChatStarted, VideoNote, Voice, + WebAppData, WriteAccessAllowed, }; /// This object represents a message. @@ -245,7 +246,7 @@ pub struct MessagePinned { /// field will not contain further `reply_to_message` fields even if it /// is itself a reply. #[serde(rename = "pinned_message")] - pub pinned: Box, + pub pinned: Box, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -627,14 +628,14 @@ mod getters { use std::ops::Deref; use crate::types::{ - self, message::MessageKind::*, Chat, ChatId, ChatMigration, MediaAnimation, MediaAudio, - MediaContact, MediaDocument, MediaGame, MediaKind, 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, MessageOrigin, - MessagePassportData, MessagePinned, MessageProximityAlertTriggered, + self, message::MessageKind::*, Chat, ChatId, ChatMigration, MaybeInaccessibleMessage, + MediaAnimation, MediaAudio, MediaContact, MediaDocument, MediaGame, MediaKind, + 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, + MessageOrigin, MessagePassportData, MessagePinned, MessageProximityAlertTriggered, MessageSuccessfulPayment, MessageSupergroupChatCreated, MessageUsersShared, MessageVideoChatParticipantsInvited, PhotoSize, User, }; @@ -1208,7 +1209,7 @@ mod getters { } #[must_use] - pub fn pinned_message(&self) -> Option<&Message> { + pub fn pinned_message(&self) -> Option<&MaybeInaccessibleMessage> { match &self.kind { Pinned(MessagePinned { pinned }) => Some(pinned), _ => None, diff --git a/crates/teloxide-core/src/types/update.rs b/crates/teloxide-core/src/types/update.rs index ed1b2cac..66906ead 100644 --- a/crates/teloxide-core/src/types/update.rs +++ b/crates/teloxide-core/src/types/update.rs @@ -220,7 +220,7 @@ 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, diff --git a/crates/teloxide/examples/buttons.rs b/crates/teloxide/examples/buttons.rs index c640d708..8a2269f7 100644 --- a/crates/teloxide/examples/buttons.rs +++ b/crates/teloxide/examples/buttons.rs @@ -116,7 +116,8 @@ async fn callback_handler(bot: Bot, q: CallbackQuery) -> Result<(), Box Option { - self.message.as_ref().map(|mes| mes.chat.id) + self.message.as_ref().map(|mes| mes.chat().id) } }