diff --git a/Cargo.lock b/Cargo.lock index 4d7d2d31..713778fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,6 +441,12 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -1395,6 +1401,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "pretty_env_logger" version = "0.5.0" @@ -2216,6 +2232,7 @@ dependencies = [ "mime", "once_cell", "pin-project", + "pretty_assertions", "pretty_env_logger", "rc-box", "reqwest", @@ -2875,6 +2892,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d422e8e38ec76e2f06ee439ccc765e9c6a9638b9e7c9f2e8255e4d41e8bd852" +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/crates/teloxide-core/CHANGELOG.md b/crates/teloxide-core/CHANGELOG.md index 525b3455..c2b49259 100644 --- a/crates/teloxide-core/CHANGELOG.md +++ b/crates/teloxide-core/CHANGELOG.md @@ -21,6 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [pr1131]: https://github.com/teloxide/teloxide/pull/1131 +### Changed + +- `MaybeAnonymousUser` type introduced, which replaced `PollAnswer::voter: Voter` and `MessageReactionUpdated::{user, actor_chat}` in `MessageReactionUpdated`([#1134][pr1134]) + +[pr1134]: https://github.com/teloxide/teloxide/pull/1134 + ## 0.10.1 - 2024-08-17 ### Fixed diff --git a/crates/teloxide-core/Cargo.toml b/crates/teloxide-core/Cargo.toml index 48476554..ef695fe9 100644 --- a/crates/teloxide-core/Cargo.toml +++ b/crates/teloxide-core/Cargo.toml @@ -95,6 +95,7 @@ ron = "0.7" indexmap = { version = "1.9", features = ["serde-1"] } aho-corasick = "0.7" itertools = "0.10" +pretty_assertions = "1.4.0" [package.metadata.docs.rs] diff --git a/crates/teloxide-core/src/types.rs b/crates/teloxide-core/src/types.rs index bbdd499c..0177bd91 100644 --- a/crates/teloxide-core/src/types.rs +++ b/crates/teloxide-core/src/types.rs @@ -91,6 +91,7 @@ pub use link_preview_options::*; pub use location::*; pub use login_url::*; pub use mask_position::*; +pub use maybe_anonymous_user::*; pub use maybe_inaccessible_message::*; pub use me::*; pub use menu_button::*; @@ -218,6 +219,7 @@ mod link_preview_options; mod location; mod login_url; mod mask_position; +mod maybe_anonymous_user; mod maybe_inaccessible_message; mod me; mod menu_button; diff --git a/crates/teloxide-core/src/types/maybe_anonymous_user.rs b/crates/teloxide-core/src/types/maybe_anonymous_user.rs new file mode 100644 index 00000000..04b75a96 --- /dev/null +++ b/crates/teloxide-core/src/types/maybe_anonymous_user.rs @@ -0,0 +1,69 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::{Chat, User}; + +/// Represents either [`User`] or anonymous user ([`Chat`]) that acts on behalf +/// of the chat +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MaybeAnonymousUser { + User(User), + Chat(Chat), +} + +impl MaybeAnonymousUser { + pub fn is_user(&self) -> bool { + self.user().is_some() + } + + pub fn is_chat(&self) -> bool { + self.chat().is_some() + } + + #[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, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn user_de() { + let json = r#"{ + "id": 42, + "is_bot": false, + "first_name": "blah" + }"#; + + let user: MaybeAnonymousUser = serde_json::from_str(json).unwrap(); + + assert!(user.user().is_some()); + } + + #[test] + fn chat_de() { + let json = r#"{ + "id": -1001160242915, + "title": "a", + "type": "group" + }"#; + + let chat: MaybeAnonymousUser = serde_json::from_str(json).unwrap(); + + assert!(chat.chat().is_some()); + } +} diff --git a/crates/teloxide-core/src/types/message_reaction_updated.rs b/crates/teloxide-core/src/types/message_reaction_updated.rs index 8aeae9e5..ff15c850 100644 --- a/crates/teloxide-core/src/types/message_reaction_updated.rs +++ b/crates/teloxide-core/src/types/message_reaction_updated.rs @@ -1,7 +1,7 @@ use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; -use crate::types::{Chat, MessageId, ReactionType, User}; +use crate::types::{Chat, MaybeAnonymousUser, MessageId, ReactionType, User}; /// This object represents a change of a reaction on a message performed by a /// user. @@ -15,12 +15,11 @@ pub struct MessageReactionUpdated { #[serde(flatten)] pub message_id: MessageId, - /// The user that changed the reaction, if the user isn't anonymous - pub user: Option, - - /// The chat on behalf of which the reaction was changed, if the user is - /// anonymous - pub actor_chat: Option, + /// The [`MaybeAnonymousUser::User`] that changed the reaction, if the user + /// isn't anonymous or the [`MaybeAnonymousUser::Chat`] on behalf of + /// which the reaction was changed, if the user is anonymous + #[serde(deserialize_with = "deserialize_actor", flatten)] + pub actor: MaybeAnonymousUser, /// Date of the change in Unix time #[serde(with = "crate::types::serde_date_from_unix_timestamp")] @@ -35,22 +34,37 @@ pub struct MessageReactionUpdated { impl MessageReactionUpdated { #[must_use] - pub fn actor_chat(&self) -> Option<&Chat> { - self.actor_chat.as_ref() + pub fn chat(&self) -> Option<&Chat> { + self.actor.chat() } #[must_use] pub fn user(&self) -> Option<&User> { - self.user.as_ref() + self.actor.user() } } +#[derive(Deserialize)] +struct ActorDe { + /// The user that changed the reaction, if the user isn't anonymous + pub user: Option, + /// The chat on behalf of which the reaction was changed, if the user is + /// anonymous + pub actor_chat: Option, +} + +fn deserialize_actor<'d, D: Deserializer<'d>>(d: D) -> Result { + let ActorDe { user, actor_chat } = ActorDe::deserialize(d)?; + + Ok(actor_chat.map(MaybeAnonymousUser::Chat).or(user.map(MaybeAnonymousUser::User)).unwrap()) +} + #[cfg(test)] mod tests { use super::*; #[test] - fn deserialize() { + fn deserialize_user() { let data = r#" { "chat": { @@ -77,6 +91,37 @@ mod tests { ] } "#; - serde_json::from_str::(data).unwrap(); + let message_reaction_update = serde_json::from_str::(data).unwrap(); + + assert!(message_reaction_update.actor.is_user()); + } + + #[test] + fn deserialize_chat() { + let data = r#"{ + "chat": { + "id": -1002199793788, + "title": "тест", + "type": "supergroup" + }, + "message_id": 2, + "actor_chat": { + "id": -1002199793788, + "title": "тест", + "type": "supergroup" + }, + "date": 1723798597, + "old_reaction": [ + { + "type": "emoji", + "emoji": "❤" + } + ], + "new_reaction": [] + }"#; + + let message_reaction_update = serde_json::from_str::(data).unwrap(); + + assert!(message_reaction_update.actor.is_chat()) } } diff --git a/crates/teloxide-core/src/types/poll_answer.rs b/crates/teloxide-core/src/types/poll_answer.rs index 5fb670b0..706c4062 100644 --- a/crates/teloxide-core/src/types/poll_answer.rs +++ b/crates/teloxide-core/src/types/poll_answer.rs @@ -1,10 +1,11 @@ use serde::{Deserialize, Deserializer, Serialize}; -use crate::types::{Chat, User}; +use crate::types::{Chat, MaybeAnonymousUser, User}; #[serde_with::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct PollAnswer { + // FIXME: PollId /// Unique poll identifier. pub poll_id: String, @@ -14,7 +15,7 @@ pub struct PollAnswer { /// 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, + pub voter: MaybeAnonymousUser, /// 0-based identifiers of answer options, chosen by the user. /// @@ -22,31 +23,6 @@ pub struct PollAnswer { 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` @@ -61,9 +37,9 @@ struct VoterDe { pub user: Option, } -fn deserialize_voter<'d, D: Deserializer<'d>>(d: D) -> Result { +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()) + Ok(voter_chat.map(MaybeAnonymousUser::Chat).or(user.map(MaybeAnonymousUser::User)).unwrap()) } #[cfg(test)] @@ -71,21 +47,22 @@ mod tests { use super::*; #[test] - fn test_poll_answer_with_user_de() { + fn poll_answer_with_user_de() { let json = r#"{ - "poll_id":"POLL_ID", - "user": {"id":42,"is_bot":false,"first_name":"blah"}, + "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(_))); + + assert!(poll_answer.voter.is_user()); } #[test] - fn test_poll_answer_with_voter_chat_de() { + fn poll_answer_with_voter_chat_de() { let json = r#"{ - "poll_id":"POLL_ID", + "poll_id": "POLL_ID", "voter_chat": { "id": -1001160242915, "title": "a", @@ -95,11 +72,11 @@ mod tests { }"#; let poll_answer: PollAnswer = serde_json::from_str(json).unwrap(); - assert!(matches!(poll_answer.voter, Voter::Chat(_))); + assert!(poll_answer.voter.is_chat()); } #[test] - fn test_poll_answer_with_both_user_and_voter_chat_de() { + fn poll_answer_with_both_user_and_voter_chat_de() { let json = r#"{ "poll_id":"POLL_ID", "voter_chat": { @@ -107,11 +84,11 @@ mod tests { "title": "a", "type": "group" }, - "user": {"id":136817688,"is_bot":true,"first_name":"Channel_Bot"}, + "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(_))); + assert!(poll_answer.voter.is_chat()); } } diff --git a/crates/teloxide-core/src/types/update.rs b/crates/teloxide-core/src/types/update.rs index 92bebc8f..62b1b0f6 100644 --- a/crates/teloxide-core/src/types/update.rs +++ b/crates/teloxide-core/src/types/update.rs @@ -469,13 +469,14 @@ mod test { use crate::types::{ 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, + LinkPreviewOptions, MaybeAnonymousUser, MediaKind, MediaText, Message, MessageCommon, + MessageId, MessageKind, MessageReactionCountUpdated, MessageReactionUpdated, + PublicChatChannel, PublicChatKind, PublicChatSupergroup, ReactionCount, ReactionType, + Update, UpdateId, UpdateKind, User, UserId, }; use chrono::DateTime; + use pretty_assertions::assert_eq; // TODO: more tests for deserialization #[test] @@ -892,7 +893,7 @@ mod test { chat_full_info: ChatFullInfo::default(), }, message_id: MessageId(35), - user: Some(User { + actor: MaybeAnonymousUser::User(User { id: UserId(1459074222), is_bot: false, first_name: "shadowchain".to_owned(), @@ -902,7 +903,6 @@ mod test { 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() }], @@ -911,6 +911,76 @@ mod test { let actual = serde_json::from_str::(json).unwrap(); assert_eq!(expected, actual); + + let json = r#" + { + "update_id": 767844136, + "message_reaction": { + "chat": { + "id": -1002199793788, + "title": "тест", + "type": "supergroup" + }, + "message_id": 2, + "actor_chat": { + "id": -1002199793788, + "title": "тест", + "type": "supergroup" + }, + "date": 1723798597, + "old_reaction": [ + { + "type": "emoji", + "emoji": "❤" + } + ], + "new_reaction": [] + } + } + "#; + let chat = Chat { + id: ChatId(-1002199793788), + kind: ChatKind::Public(ChatPublic { + title: Some("тест".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(), + }; + let expected = Update { + id: UpdateId(767844136), + kind: UpdateKind::MessageReaction(MessageReactionUpdated { + chat: chat.clone(), + message_id: MessageId(2), + actor: MaybeAnonymousUser::Chat(chat), + date: DateTime::from_timestamp(1723798597, 0).unwrap(), + old_reaction: vec![ReactionType::Emoji { emoji: "❤".to_owned() }], + new_reaction: vec![], + }), + }; + + let actual = serde_json::from_str::(json).unwrap(); + assert_eq!(expected, actual); } #[test]