Merge pull request #151 from teloxide/fix_some_message_deserialization_bugs

fix some message deserialization bugs
This commit is contained in:
Waffle Maybe 2021-12-28 16:07:38 +03:00 committed by GitHub
commit 5ad920f342
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 199 additions and 106 deletions

View file

@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `Limits::messages_per_min_channel` ([#121][pr121])
- `media_group_id` field to `MediaDocument` and `MediaAudio` ([#139][pr139])
- `caption_entities` method to `InputMediaPhoto` ([#140][pr140])
- `User::is_anonymous` and `User::is_channel` functions ([#151][pr151])
[pr109]: https://github.com/teloxide/teloxide-core/pull/109
[pr116]: https://github.com/teloxide/teloxide-core/pull/116
@ -25,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[pr139]: https://github.com/teloxide/teloxide-core/pull/139
[pr140]: https://github.com/teloxide/teloxide-core/pull/140
[pr143]: https://github.com/teloxide/teloxide-core/pull/143
[pr151]: https://github.com/teloxide/teloxide-core/pull/151
### Changed
@ -37,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use `url::Url` for urls, use `chrono::DateTime<Utc>` for dates in types ([#115][pr115])
- Mark `ApiError` as `non_exhaustive` ([#125][pr125])
- `InputFile` and related structures now do **not** implement `PartialEq`, `Eq` and `Hash` ([#133][pr133])
- How forwarded messages are represented ([#151][pr151])
- `RequestError::InvalidJson` now has a `raw` field with raw json for easier debugability ([#150][pr150])
[pr115]: https://github.com/teloxide/teloxide-core/pull/115

View file

@ -75,8 +75,14 @@ pub struct MessageCommon {
/// title of an anonymous group administrator.
pub author_signature: Option<String>,
/// For forwarded messages, information about the forward
#[serde(flatten)]
pub forward_kind: ForwardKind,
pub forward: Option<Forward>,
/// For replies, the original message. Note that the Message object in this
/// field will not contain further `reply_to_message` fields even if it
/// itself is a reply.
pub reply_to_message: Option<Box<Message>>,
/// Date the message was last edited in Unix time.
#[serde(with = "crate::types::serde_opt_date_from_unix_timestamp")]
@ -236,51 +242,44 @@ pub struct MessagePassportData {
pub passport_data: PassportData,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub enum ForwardedFrom {
#[serde(rename = "forward_from")]
User(User),
#[serde(rename = "forward_sender_name")]
SenderName(String),
}
/// Information about forwarded message.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ForwardKind {
Channel(ForwardChannel),
NonChannel(ForwardNonChannel),
Origin(ForwardOrigin),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ForwardChannel {
#[serde(rename = "forward_date")]
#[serde(with = "crate::types::serde_date_from_unix_timestamp")]
pub date: DateTime<Utc>,
#[serde(rename = "forward_from_chat")]
pub chat: Chat,
#[serde(rename = "forward_from_message_id")]
pub message_id: i32,
#[serde(rename = "forward_signature")]
pub signature: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ForwardNonChannel {
pub struct Forward {
/// Date the original message was sent in Unix time.
#[serde(rename = "forward_date")]
#[serde(with = "crate::types::serde_date_from_unix_timestamp")]
pub date: DateTime<Utc>,
/// The entity that sent the original message.
#[serde(flatten)]
pub from: ForwardedFrom,
/// For messages forwarded from channels, signature of the post author if
/// present. For messages forwarded from anonymous admins, authors title, if
/// present.
#[serde(rename = "forward_signature")]
pub signature: Option<String>,
/// For messages forwarded from channels, identifier of the original message
/// in the channel
#[serde(rename = "forward_from_message_id")]
pub message_id: Option<i32>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ForwardOrigin {
pub reply_to_message: Option<Box<Message>>,
/// The entity that sent the original message that later was forwarded.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ForwardedFrom {
/// The message was sent by a user.
#[serde(rename = "forward_from")]
User(User),
/// The message was sent by an anonymous user on behalf of a group or
/// channel.
#[serde(rename = "forward_from_chat")]
Chat(Chat),
/// The message was sent by a user who disallow adding a link to their
/// account in forwarded messages.
#[serde(rename = "forward_sender_name")]
SenderName(String),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -521,17 +520,14 @@ mod getters {
use std::ops::Deref;
use crate::types::{
self,
message::{ForwardKind::NonChannel, MessageKind::*},
Chat, ChatMigration, ForwardChannel, ForwardKind, ForwardNonChannel, ForwardOrigin,
ForwardedFrom, MediaAnimation, MediaAudio, MediaContact, MediaDocument, MediaGame,
MediaKind, MediaLocation, MediaPhoto, MediaPoll, MediaSticker, MediaText, MediaVenue,
MediaVideo, MediaVideoNote, MediaVoice, Message, MessageChannelChatCreated, MessageCommon,
MessageConnectedWebsite, MessageDeleteChatPhoto, MessageDice, MessageEntity,
MessageGroupChatCreated, MessageInvoice, MessageLeftChatMember, MessageNewChatMembers,
MessageNewChatPhoto, MessageNewChatTitle, MessagePassportData, MessagePinned,
MessageProximityAlertTriggered, MessageSuccessfulPayment, MessageSupergroupChatCreated,
PhotoSize, True, User,
self, message::MessageKind::*, Chat, ChatMigration, Forward, ForwardedFrom, MediaAnimation,
MediaAudio, MediaContact, MediaDocument, MediaGame, MediaKind, MediaLocation, MediaPhoto,
MediaPoll, MediaSticker, MediaText, MediaVenue, MediaVideo, MediaVideoNote, MediaVoice,
Message, MessageChannelChatCreated, MessageCommon, MessageConnectedWebsite,
MessageDeleteChatPhoto, MessageDice, MessageEntity, MessageGroupChatCreated,
MessageInvoice, MessageLeftChatMember, MessageNewChatMembers, MessageNewChatPhoto,
MessageNewChatTitle, MessagePassportData, MessagePinned, MessageProximityAlertTriggered,
MessageSuccessfulPayment, MessageSupergroupChatCreated, PhotoSize, True, User,
};
/// Getters for [Message] fields from [telegram docs].
@ -566,73 +562,49 @@ mod getters {
self.chat.id
}
/// NOTE: this is getter for both `forward_from` and
/// `forward_sender_name`
pub fn forward(&self) -> Option<&Forward> {
self.common().and_then(|m| m.forward.as_ref())
}
pub fn forward_date(&self) -> Option<DateTime<Utc>> {
self.forward().map(|f| f.date)
}
pub fn forward_from(&self) -> Option<&ForwardedFrom> {
match &self.kind {
Common(MessageCommon {
forward_kind: NonChannel(ForwardNonChannel { from, .. }),
..
}) => Some(from),
self.forward().map(|f| &f.from)
}
pub fn forward_from_user(&self) -> Option<&User> {
self.forward_from().and_then(|from| match from {
ForwardedFrom::User(user) => Some(user),
_ => None,
}
})
}
pub fn forward_from_chat(&self) -> Option<&Chat> {
match &self.kind {
Common(MessageCommon {
forward_kind: ForwardKind::Channel(ForwardChannel { chat, .. }),
..
}) => Some(chat),
self.forward_from().and_then(|from| match from {
ForwardedFrom::Chat(chat) => Some(chat),
_ => None,
}
})
}
pub fn forward_from_message_id(&self) -> Option<&i32> {
match &self.kind {
Common(MessageCommon {
forward_kind: ForwardKind::Channel(ForwardChannel { message_id, .. }),
..
}) => Some(message_id),
pub fn forward_from_sender_name(&self) -> Option<&str> {
self.forward_from().and_then(|from| match from {
ForwardedFrom::SenderName(sender_name) => Some(&**sender_name),
_ => None,
}
})
}
pub fn forward_from_message_id(&self) -> Option<i32> {
self.forward().and_then(|f| f.message_id)
}
pub fn forward_signature(&self) -> Option<&str> {
match &self.kind {
Common(MessageCommon {
forward_kind: ForwardKind::Channel(ForwardChannel { signature, .. }),
..
}) => signature.as_ref().map(Deref::deref),
_ => None,
}
}
pub fn forward_date(&self) -> Option<&DateTime<Utc>> {
match &self.kind {
Common(MessageCommon {
forward_kind: ForwardKind::Channel(ForwardChannel { date, .. }),
..
})
| Common(MessageCommon {
forward_kind: ForwardKind::NonChannel(ForwardNonChannel { date, .. }),
..
}) => Some(date),
_ => None,
}
self.forward().and_then(|f| f.signature.as_deref())
}
pub fn reply_to_message(&self) -> Option<&Message> {
match &self.kind {
Common(MessageCommon {
forward_kind:
ForwardKind::Origin(ForwardOrigin {
reply_to_message, ..
}),
..
}) => reply_to_message.as_ref().map(Deref::deref),
_ => None,
}
self.common().and_then(|m| m.reply_to_message.as_deref())
}
pub fn edit_date(&self) -> Option<&DateTime<Utc>> {
@ -1060,6 +1032,14 @@ mod getters {
_ => false,
}
}
/// Common message (text, image, etc)
fn common(&self) -> Option<&MessageCommon> {
match &self.kind {
Common(message) => Some(message),
_ => None,
}
}
}
}
@ -1307,6 +1287,75 @@ mod tests {
assert!(message.is_ok());
}
/// Regression test for <https://github.com/teloxide/teloxide/issues/419>
#[test]
fn issue_419() {
let json = r#"{
"message_id": 1,
"from": {
"id": 1087968824,
"is_bot": true,
"first_name": "Group",
"username": "GroupAnonymousBot"
},
"author_signature": "TITLE2",
"sender_chat": {
"id": -1001160242915,
"title": "a",
"type": "supergroup"
},
"chat": {
"id": -1001160242915,
"title": "a",
"type": "supergroup"
},
"date": 1640359576,
"forward_from_chat": {
"id": -1001160242915,
"title": "a",
"type": "supergroup"
},
"forward_signature": "TITLE",
"forward_date": 1640359544,
"text": "text"
}"#;
// Anonymous admin with title "TITLE2" forwards a message from anonymous
// admin with title "TITLE" with text "a", everything is happening in
// the same group.
let message: Message = serde_json::from_str(json).unwrap();
let group = Chat {
id: -1001160242915,
kind: ChatKind::Public(ChatPublic {
title: Some("a".to_owned()),
kind: PublicChatKind::Supergroup(PublicChatSupergroup {
username: None,
sticker_set_name: None,
can_set_sticker_set: None,
permissions: None,
slow_mode_delay: None,
linked_chat_id: None,
location: None,
}),
description: None,
invite_link: None,
}),
photo: None,
pinned_message: None,
message_auto_delete_time: None,
};
assert!(message.from().unwrap().is_anonymous());
assert_eq!(message.author_signature().unwrap(), "TITLE2");
assert_eq!(message.sender_chat().unwrap(), &group);
assert_eq!(&message.chat, &group);
assert_eq!(message.forward_from_chat().unwrap(), &group);
assert_eq!(message.forward_signature().unwrap(), "TITLE");
assert!(message.forward_date().is_some());
assert_eq!(message.text().unwrap(), "text");
}
/// Regression test for <https://github.com/teloxide/teloxide/issues/427>
#[test]
fn issue_427() {

View file

@ -145,8 +145,8 @@ impl Update {
#[cfg(test)]
mod test {
use crate::types::{
Chat, ChatKind, ChatPrivate, ForwardKind, ForwardOrigin, MediaKind, MediaText, Message,
MessageCommon, MessageKind, Update, UpdateKind, User,
Chat, ChatKind, ChatPrivate, MediaKind, MediaText, Message, MessageCommon, MessageKind,
Update, UpdateKind, User,
};
use chrono::{DateTime, NaiveDateTime, Utc};
@ -208,9 +208,8 @@ mod test {
username: Some(String::from("WaffleLapkin")),
language_code: Some(String::from("en")),
}),
forward_kind: ForwardKind::Origin(ForwardOrigin {
reply_to_message: None,
}),
reply_to_message: None,
forward: None,
edit_date: None,
media_kind: MediaKind::Text(MediaText {
text: String::from("hello there"),

View file

@ -28,6 +28,8 @@ pub struct User {
}
impl User {
/// Returns full name of this user, ie first and last names joined with a
/// space.
pub fn full_name(&self) -> String {
match &self.last_name {
Some(last_name) => (format!("{0} {1}", self.first_name, last_name)),
@ -35,12 +37,52 @@ impl User {
}
}
/// Returns a username mention of this user. Returns `None` if
/// `self.username.is_none()`.
pub fn mention(&self) -> Option<String> {
Some(format!("@{}", self.username.as_ref()?))
}
/// Returns an URL that links to this user in the form of
/// `tg://user/?id=<...>`
pub fn url(&self) -> reqwest::Url {
reqwest::Url::parse(format!("tg://user/?id={}", self.id).as_str()).unwrap()
reqwest::Url::parse(&format!("tg://user/?id={}", self.id)).unwrap()
}
/// Returns `true` if this is special user used by telegram bot API to
/// denote an annonymous user that sends messages on behalf of a group.
pub fn is_anonymous(&self) -> bool {
// https://github.com/tdlib/td/blob/4791fb6a2af0257f6cad8396e10424a79ee5f768/td/telegram/ContactsManager.cpp#L4941-L4943
const ANON_ID: i64 = 1087968824;
// Sanity check
debug_assert!(
(self.id != ANON_ID)
|| (self.is_bot
&& self.first_name == "Group"
&& self.last_name.is_none()
&& self.username.as_deref() == Some("GroupAnonymousBot"))
);
self.id == ANON_ID
}
/// Returns `true` if this is special user used by telegram bot API to
/// denote an annonymous user that sends messages on behalf of a channel.
pub fn is_channel(&self) -> bool {
// https://github.com/tdlib/td/blob/4791fb6a2af0257f6cad8396e10424a79ee5f768/td/telegram/ContactsManager.cpp#L4945-L4947
const ANON_CHANNEL_ID: i64 = 136817688;
// Sanity check
debug_assert!(
(self.id != ANON_CHANNEL_ID)
|| (self.is_bot
&& self.first_name == "Group"
&& self.last_name.is_none()
&& self.username.as_deref() == Some("GroupAnonymousBot"))
);
self.id == ANON_CHANNEL_ID
}
}