Merge pull request #1134 from syrtcevvi/fix/more-rusty-api

Add `MaybeAnonymousUser` type
This commit is contained in:
Tima Kinsart 2024-08-23 12:38:14 +00:00 committed by GitHub
commit ae83ff15a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 253 additions and 58 deletions

23
Cargo.lock generated
View file

@ -441,6 +441,12 @@ dependencies = [
"syn 2.0.52", "syn 2.0.52",
] ]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -1395,6 +1401,16 @@ dependencies = [
"zerocopy", "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]] [[package]]
name = "pretty_env_logger" name = "pretty_env_logger"
version = "0.5.0" version = "0.5.0"
@ -2216,6 +2232,7 @@ dependencies = [
"mime", "mime",
"once_cell", "once_cell",
"pin-project", "pin-project",
"pretty_assertions",
"pretty_env_logger", "pretty_env_logger",
"rc-box", "rc-box",
"reqwest", "reqwest",
@ -2875,6 +2892,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d422e8e38ec76e2f06ee439ccc765e9c6a9638b9e7c9f2e8255e4d41e8bd852" checksum = "9d422e8e38ec76e2f06ee439ccc765e9c6a9638b9e7c9f2e8255e4d41e8bd852"
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" version = "0.7.35"

View file

@ -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 [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 ## 0.10.1 - 2024-08-17
### Fixed ### Fixed

View file

@ -95,6 +95,7 @@ ron = "0.7"
indexmap = { version = "1.9", features = ["serde-1"] } indexmap = { version = "1.9", features = ["serde-1"] }
aho-corasick = "0.7" aho-corasick = "0.7"
itertools = "0.10" itertools = "0.10"
pretty_assertions = "1.4.0"
[package.metadata.docs.rs] [package.metadata.docs.rs]

View file

@ -91,6 +91,7 @@ pub use link_preview_options::*;
pub use location::*; pub use location::*;
pub use login_url::*; pub use login_url::*;
pub use mask_position::*; pub use mask_position::*;
pub use maybe_anonymous_user::*;
pub use maybe_inaccessible_message::*; pub use maybe_inaccessible_message::*;
pub use me::*; pub use me::*;
pub use menu_button::*; pub use menu_button::*;
@ -218,6 +219,7 @@ mod link_preview_options;
mod location; mod location;
mod login_url; mod login_url;
mod mask_position; mod mask_position;
mod maybe_anonymous_user;
mod maybe_inaccessible_message; mod maybe_inaccessible_message;
mod me; mod me;
mod menu_button; mod menu_button;

View file

@ -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());
}
}

View file

@ -1,7 +1,7 @@
use chrono::{DateTime, Utc}; 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 /// This object represents a change of a reaction on a message performed by a
/// user. /// user.
@ -15,12 +15,11 @@ pub struct MessageReactionUpdated {
#[serde(flatten)] #[serde(flatten)]
pub message_id: MessageId, pub message_id: MessageId,
/// The user that changed the reaction, if the user isn't anonymous /// The [`MaybeAnonymousUser::User`] that changed the reaction, if the user
pub user: Option<User>, /// isn't anonymous or the [`MaybeAnonymousUser::Chat`] on behalf of
/// which the reaction was changed, if the user is anonymous
/// The chat on behalf of which the reaction was changed, if the user is #[serde(deserialize_with = "deserialize_actor", flatten)]
/// anonymous pub actor: MaybeAnonymousUser,
pub actor_chat: Option<Chat>,
/// Date of the change in Unix time /// Date of the change in Unix time
#[serde(with = "crate::types::serde_date_from_unix_timestamp")] #[serde(with = "crate::types::serde_date_from_unix_timestamp")]
@ -35,22 +34,37 @@ pub struct MessageReactionUpdated {
impl MessageReactionUpdated { impl MessageReactionUpdated {
#[must_use] #[must_use]
pub fn actor_chat(&self) -> Option<&Chat> { pub fn chat(&self) -> Option<&Chat> {
self.actor_chat.as_ref() self.actor.chat()
} }
#[must_use] #[must_use]
pub fn user(&self) -> Option<&User> { 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
user: Option<User>,
/// The chat on behalf of which the reaction was changed, if the user is
/// anonymous
actor_chat: Option<Chat>,
}
fn deserialize_actor<'d, D: Deserializer<'d>>(d: D) -> Result<MaybeAnonymousUser, D::Error> {
let ActorDe { user, actor_chat } = ActorDe::deserialize(d)?;
Ok(actor_chat.map(MaybeAnonymousUser::Chat).or(user.map(MaybeAnonymousUser::User)).unwrap())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn deserialize() { fn deserialize_user() {
let data = r#" let data = r#"
{ {
"chat": { "chat": {
@ -77,6 +91,37 @@ mod tests {
] ]
} }
"#; "#;
serde_json::from_str::<MessageReactionUpdated>(data).unwrap(); let message_reaction_update = serde_json::from_str::<MessageReactionUpdated>(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::<MessageReactionUpdated>(data).unwrap();
assert!(message_reaction_update.actor.is_chat())
} }
} }

View file

@ -1,10 +1,11 @@
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use crate::types::{Chat, User}; use crate::types::{Chat, MaybeAnonymousUser, User};
#[serde_with::skip_serializing_none] #[serde_with::skip_serializing_none]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PollAnswer { pub struct PollAnswer {
// FIXME: PollId
/// Unique poll identifier. /// Unique poll identifier.
pub poll_id: String, pub poll_id: String,
@ -14,7 +15,7 @@ pub struct PollAnswer {
/// If the voter isn't anonymous, stores the user that changed /// If the voter isn't anonymous, stores the user that changed
/// the answer to the poll /// the answer to the poll
#[serde(deserialize_with = "deserialize_voter", flatten)] #[serde(deserialize_with = "deserialize_voter", flatten)]
pub voter: Voter, pub voter: MaybeAnonymousUser,
/// 0-based identifiers of answer options, chosen by the user. /// 0-based identifiers of answer options, chosen by the user.
/// ///
@ -22,31 +23,6 @@ pub struct PollAnswer {
pub option_ids: Vec<u8>, pub option_ids: Vec<u8>,
} }
#[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 /// 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 /// 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` /// `None`, but rather actual value for backward compatibility, the field `user`
@ -61,9 +37,9 @@ struct VoterDe {
pub user: Option<User>, pub user: Option<User>,
} }
fn deserialize_voter<'d, D: Deserializer<'d>>(d: D) -> Result<Voter, D::Error> { fn deserialize_voter<'d, D: Deserializer<'d>>(d: D) -> Result<MaybeAnonymousUser, D::Error> {
let VoterDe { voter_chat, user } = VoterDe::deserialize(d)?; 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)] #[cfg(test)]
@ -71,21 +47,22 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_poll_answer_with_user_de() { fn poll_answer_with_user_de() {
let json = r#"{ let json = r#"{
"poll_id":"POLL_ID", "poll_id": "POLL_ID",
"user": {"id":42,"is_bot":false,"first_name":"blah"}, "user": {"id": 42,"is_bot": false,"first_name": "blah"},
"option_ids": [] "option_ids": []
}"#; }"#;
let poll_answer: PollAnswer = serde_json::from_str(json).unwrap(); let poll_answer: PollAnswer = serde_json::from_str(json).unwrap();
assert!(matches!(poll_answer.voter, Voter::User(_)));
assert!(poll_answer.voter.is_user());
} }
#[test] #[test]
fn test_poll_answer_with_voter_chat_de() { fn poll_answer_with_voter_chat_de() {
let json = r#"{ let json = r#"{
"poll_id":"POLL_ID", "poll_id": "POLL_ID",
"voter_chat": { "voter_chat": {
"id": -1001160242915, "id": -1001160242915,
"title": "a", "title": "a",
@ -95,11 +72,11 @@ mod tests {
}"#; }"#;
let poll_answer: PollAnswer = serde_json::from_str(json).unwrap(); let poll_answer: PollAnswer = serde_json::from_str(json).unwrap();
assert!(matches!(poll_answer.voter, Voter::Chat(_))); assert!(poll_answer.voter.is_chat());
} }
#[test] #[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#"{ let json = r#"{
"poll_id":"POLL_ID", "poll_id":"POLL_ID",
"voter_chat": { "voter_chat": {
@ -107,11 +84,11 @@ mod tests {
"title": "a", "title": "a",
"type": "group" "type": "group"
}, },
"user": {"id":136817688,"is_bot":true,"first_name":"Channel_Bot"}, "user": {"id": 136817688,"is_bot": true,"first_name": "Channel_Bot"},
"option_ids": [] "option_ids": []
}"#; }"#;
let poll_answer: PollAnswer = serde_json::from_str(json).unwrap(); let poll_answer: PollAnswer = serde_json::from_str(json).unwrap();
assert!(matches!(poll_answer.voter, Voter::Chat(_))); assert!(poll_answer.voter.is_chat());
} }
} }

View file

@ -469,13 +469,14 @@ mod test {
use crate::types::{ use crate::types::{
Chat, ChatBoost, ChatBoostRemoved, ChatBoostSource, ChatBoostSourcePremium, Chat, ChatBoost, ChatBoostRemoved, ChatBoostSource, ChatBoostSourcePremium,
ChatBoostUpdated, ChatFullInfo, ChatId, ChatKind, ChatPrivate, ChatPublic, ChatBoostUpdated, ChatFullInfo, ChatId, ChatKind, ChatPrivate, ChatPublic,
LinkPreviewOptions, MediaKind, MediaText, Message, MessageCommon, MessageId, MessageKind, LinkPreviewOptions, MaybeAnonymousUser, MediaKind, MediaText, Message, MessageCommon,
MessageReactionCountUpdated, MessageReactionUpdated, PublicChatChannel, PublicChatKind, MessageId, MessageKind, MessageReactionCountUpdated, MessageReactionUpdated,
PublicChatSupergroup, ReactionCount, ReactionType, Update, UpdateId, UpdateKind, User, PublicChatChannel, PublicChatKind, PublicChatSupergroup, ReactionCount, ReactionType,
UserId, Update, UpdateId, UpdateKind, User, UserId,
}; };
use chrono::DateTime; use chrono::DateTime;
use pretty_assertions::assert_eq;
// TODO: more tests for deserialization // TODO: more tests for deserialization
#[test] #[test]
@ -892,7 +893,7 @@ mod test {
chat_full_info: ChatFullInfo::default(), chat_full_info: ChatFullInfo::default(),
}, },
message_id: MessageId(35), message_id: MessageId(35),
user: Some(User { actor: MaybeAnonymousUser::User(User {
id: UserId(1459074222), id: UserId(1459074222),
is_bot: false, is_bot: false,
first_name: "shadowchain".to_owned(), first_name: "shadowchain".to_owned(),
@ -902,7 +903,6 @@ mod test {
is_premium: true, is_premium: true,
added_to_attachment_menu: false, added_to_attachment_menu: false,
}), }),
actor_chat: None,
date: DateTime::from_timestamp(1721306082, 0).unwrap(), date: DateTime::from_timestamp(1721306082, 0).unwrap(),
old_reaction: vec![], old_reaction: vec![],
new_reaction: vec![ReactionType::Emoji { emoji: "🌭".to_owned() }], new_reaction: vec![ReactionType::Emoji { emoji: "🌭".to_owned() }],
@ -911,6 +911,78 @@ mod test {
let actual = serde_json::from_str::<Update>(json).unwrap(); let actual = serde_json::from_str::<Update>(json).unwrap();
assert_eq!(expected, actual); 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,
custom_emoji_sticker_set_name: None,
unrestrict_boost_count: 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::<Update>(json).unwrap();
assert_eq!(expected, actual);
} }
#[test] #[test]