Merge remote-tracking branch 'master' into api_errors_custom_deserialize

This commit is contained in:
puh 2023-03-30 19:59:56 +03:00
commit 081f75546c
No known key found for this signature in database
GPG key ID: 171E3E1356CEE151
31 changed files with 735 additions and 129 deletions

View file

@ -3,6 +3,7 @@ on:
branches: [ master ]
pull_request:
branches: [ master ]
merge_group:
name: Continuous integration
@ -88,7 +89,7 @@ jobs:
features: "--features full"
- rust: nightly
toolchain: nightly-2022-12-23
features: "--all-features"
features: "--features full nightly"
- rust: msrv
toolchain: 1.64.0
features: "--features full"
@ -187,7 +188,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --all-features
args: --all-targets --features "full nightly"
doc:
name: check docs

View file

@ -6,13 +6,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## unreleased
## 0.12.2 - 2023-02-15
### Fixed
- `docs.rs` documentation build
## 0.12.1 - 2023-02-15
### Fixed
- Allow `ChatJoinRequest` updates
- Some example links in documentation
### Added
- `Update::filter_chat_join_request`
- `sqlite-storage-rustls` feature, that allows using sqlite storage without `native-tls`
### Changed
- Updated `teloxide-core` to v0.9.1; see its [changelog](https://github.com/teloxide/teloxide/blob/master/crates/teloxide-core/CHANGELOG.md#091---2023-02-15) for more
## 0.12.0 - 2023-01-17

View file

@ -7,11 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## unreleased
### Added
- `ChatPermission::can_*` helper functions ([#851][pr851])
- `mentioned_users` functions for `CallbackQuery`, `Chat`, `ChatJoinRequest`, `ChatMemberUpdated`, `Game`, `Message`, `Poll`, `Update` which return all contained `User` instances ([#850][pr850])
- `Message::video_chat_participants_invited` ([#850][pr850])
- `Update::from`, a replacement for `Update::user` ([#850][pr850])
[pr851]: https://github.com/teloxide/teloxide/pull/851
### Deprecated
- `Update::user`, use `Update::from` instead ([#850][pr850])
[pr850]: https://github.com/teloxide/teloxide/pull/850
## 0.9.1 - 2023-02-15
### Fixed
- `Update::user` now handles channel posts, chat member changes and chat join request updates correctly ([#835][pr835])
- In cases when `teloxide` can't deserialize an update, error now includes the full json value ([#826][pr826])
- Deserialization of topic messages ([#830][pr830])
[pr835]: https://github.com/teloxide/teloxide/pull/835
[pr826]: https://github.com/teloxide/teloxide/pull/826
[pr830]: https://github.com/teloxide/teloxide/pull/830
### Added
- `ApiError::ImageProcessFailed` ([#825][pr825])
[pr825]: https://github.com/teloxide/teloxide/pull/825
## 0.9.0 - 2023-01-17

View file

@ -1,6 +1,6 @@
[package]
name = "teloxide-core"
version = "0.9.0"
version = "0.9.1"
description = "Core part of the `teloxide` library - telegram bot API client"
rust-version.workspace = true

View file

@ -206,7 +206,7 @@ impl Bot {
) -> impl Future<Output = ResponseResult<P::Output>> + 'static
where
P: Payload + Serialize,
P::Output: DeserializeOwned,
P::Output: DeserializeOwned + 'static,
{
let client = self.client.clone();
let token = Arc::clone(&self.token);
@ -237,7 +237,7 @@ impl Bot {
) -> impl Future<Output = ResponseResult<P::Output>>
where
P: MultipartPayload + Serialize,
P::Output: DeserializeOwned,
P::Output: DeserializeOwned + 'static,
{
let client = self.client.clone();
let token = Arc::clone(&self.token);
@ -267,7 +267,7 @@ impl Bot {
) -> impl Future<Output = ResponseResult<P::Output>>
where
P: MultipartPayload + Serialize,
P::Output: DeserializeOwned,
P::Output: DeserializeOwned + 'static,
{
let client = self.client.clone();
let token = Arc::clone(&self.token);

View file

@ -16,6 +16,7 @@ pub enum RequestError {
/// The group has been migrated to a supergroup with the specified
/// identifier.
#[error("The group has been migrated to a supergroup with ID #{0}")]
// FIXME: change to `ChatId` :|
MigrateToChatId(i64),
/// In case of exceeding flood control, the number of seconds left to wait

View file

@ -5,7 +5,7 @@
//! asynchronous and built using [`tokio`].
//!
//!```toml
//! teloxide_core = "0.8"
//! teloxide_core = "0.9"
//! ```
//! _Compiler support: requires rustc 1.64+_.
//!
@ -117,6 +117,7 @@ mod bot;
// implementation details
mod serde_multipart;
mod util;
#[cfg(test)]
mod codegen;

View file

@ -1,4 +1,4 @@
use std::time::Duration;
use std::{any::TypeId, time::Duration};
use reqwest::{
header::{HeaderValue, CONTENT_TYPE},
@ -19,7 +19,7 @@ pub async fn request_multipart<T>(
_timeout_hint: Option<Duration>,
) -> ResponseResult<T>
where
T: DeserializeOwned,
T: DeserializeOwned + 'static,
{
// Workaround for [#460]
//
@ -58,7 +58,7 @@ pub async fn request_json<T>(
_timeout_hint: Option<Duration>,
) -> ResponseResult<T>
where
T: DeserializeOwned,
T: DeserializeOwned + 'static,
{
// Workaround for [#460]
//
@ -91,7 +91,7 @@ where
async fn process_response<T>(response: Response) -> ResponseResult<T>
where
T: DeserializeOwned,
T: DeserializeOwned + 'static,
{
if response.status().is_server_error() {
tokio::time::sleep(DELAY_ON_SERVER_ERROR).await;
@ -99,7 +99,176 @@ where
let text = response.text().await?;
deserialize_response(text)
}
fn deserialize_response<T>(text: String) -> Result<T, RequestError>
where
T: DeserializeOwned + 'static,
{
serde_json::from_str::<TelegramResponse<T>>(&text)
.map(|mut response| {
use crate::types::{Update, UpdateKind};
use std::{any::Any, iter::zip};
// HACK: Fill-in error information into `UpdateKind::Error`.
//
// Why? Well, we need `Update` deserialization to be reliable,
// even if Telegram breaks something in their Bot API, we want
// 1. Deserialization to """succeed"""
// 2. Get the `update.id`
//
// Both of these points are required for `get_updates(...) -> Vec<Update>`
// to behave well after Telegram introduces updates that we can't parse.
// (1.) makes it so only some of the updates in a butch need to be skipped
// (otherwise serde'll stop on the first error). (2.) allows us to issue
// the next `get_updates` call with the right offset, even if the last
// update in the batch didn't deserialize well.
//
// serde's interface doesn't allows us to implement `Deserialize` in such
// a way, that we could keep the data we couldn't parse, so our
// `Deserialize` impl for `UpdateKind` just returns
// `UpdateKind::Error(/* some empty-ish value */)`. Here, through some
// terrible hacks and downcasting, we fill-in the data we couldn't parse
// so that our users can make actionable bug reports.
//
// We specifically handle `Vec<Update>` here, because that's the return
// type of the only method that returns updates.
if TypeId::of::<T>() == TypeId::of::<Vec<Update>>() {
if let TelegramResponse::Ok { response, .. } = &mut response {
if let Some(updates) =
(response as &mut T as &mut dyn Any).downcast_mut::<Vec<Update>>()
{
if updates.iter().any(|u| matches!(u.kind, UpdateKind::Error(_))) {
let re_parsed = serde_json::from_str(&text);
if let Ok(TelegramResponse::Ok { response: values, .. }) = re_parsed {
for (update, value) in zip::<_, Vec<_>>(updates, values) {
if let UpdateKind::Error(dest) = &mut update.kind {
*dest = value;
}
}
}
}
}
}
}
response
})
.map_err(|source| RequestError::InvalidJson { source, raw: text.into() })?
.into()
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use cool_asserts::assert_matches;
use crate::{
net::request::deserialize_response,
types::{True, Update, UpdateKind},
ApiError, RequestError,
};
#[test]
fn smoke_ok() {
let json = r#"{"ok":true,"result":true}"#.to_owned();
let res = deserialize_response::<True>(json);
assert_matches!(res, Ok(True));
}
#[test]
fn smoke_err() {
let json =
r#"{"ok":false,"description":"Forbidden: bot was blocked by the user"}"#.to_owned();
let res = deserialize_response::<True>(json);
assert_matches!(res, Err(RequestError::Api(ApiError::BotBlocked)));
}
#[test]
fn migrate() {
let json = r#"{"ok":false,"description":"this string is ignored","parameters":{"migrate_to_chat_id":123456}}"#.to_owned();
let res = deserialize_response::<True>(json);
assert_matches!(res, Err(RequestError::MigrateToChatId(123456)));
}
#[test]
fn retry_after() {
let json = r#"{"ok":false,"description":"this string is ignored","parameters":{"retry_after":123456}}"#.to_owned();
let res = deserialize_response::<True>(json);
assert_matches!(res, Err(RequestError::RetryAfter(duration)) if duration == Duration::from_secs(123456));
}
#[test]
fn update_ok() {
let json = r#"{
"ok":true,
"result":[
{
"update_id":0,
"poll_answer":{
"poll_id":"POLL_ID",
"user": {"id":42,"is_bot":false,"first_name":"blah"},
"option_ids": []
}
}
]
}"#
.to_owned();
let res = deserialize_response::<Vec<Update>>(json).unwrap();
assert_matches!(res, [Update { id: 0, kind: UpdateKind::PollAnswer(_) }]);
}
/// Check that `get_updates` can work with malformed updates.
#[test]
fn update_err() {
let json = r#"{
"ok":true,
"result":[
{
"update_id":0,
"poll_answer":{
"poll_id":"POLL_ID",
"user": {"id":42,"is_bot":false,"first_name":"blah"},
"option_ids": []
}
},
{
"update_id":1,
"something unknown to us":17
},
{
"update_id":2,
"poll_answer":{
"poll_id":"POLL_ID",
"user": {"id":42,"is_bot":false,"first_name":"blah"},
"option_ids": [3, 4, 8]
}
},
{
"update_id":3,
"message":{"some fields are missing":true}
}
]
}"#
.to_owned();
let res = deserialize_response::<Vec<Update>>(json).unwrap();
assert_matches!(
res,
[
Update { id: 0, kind: UpdateKind::PollAnswer(_) },
Update { id: 1, kind: UpdateKind::Error(v) } if v.is_object(),
Update { id: 2, kind: UpdateKind::PollAnswer(_) },
Update { id: 3, kind: UpdateKind::Error(v) } if v.is_object(),
]
);
}
}

View file

@ -461,6 +461,13 @@ pub(crate) mod serde_rgb {
{
Ok(from_u32(v))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_u32(v.try_into().map_err(|_| E::custom("rgb value doesn't fit u32"))?)
}
}
d.deserialize_u32(V)
}
@ -481,5 +488,16 @@ pub(crate) mod serde_rgb {
}
#[test]
fn json() {}
fn json() {
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct Struct {
#[serde(with = "self")]
color: [u8; 3],
}
let json = format!(r#"{{"color":{}}}"#, 0x00AABBCC);
let Struct { color } = serde_json::from_str(&json).unwrap();
assert_eq!(color, [0xAA, 0xBB, 0xCC])
}
}

View file

@ -40,8 +40,7 @@ pub struct CallbackQuery {
/// [games]: https://core.telegram.org/bots/api#games
pub chat_instance: String,
/// A data associated with the callback button. Be aware that a bad client
/// can send arbitrary data in this field.
/// A data associated with the callback button.
pub data: Option<String>,
/// A short name of a Game to be returned, serves as the unique identifier
@ -49,6 +48,20 @@ pub struct CallbackQuery {
pub game_short_name: Option<String>,
}
impl CallbackQuery {
/// Returns all users that are "contained" in this `CallbackQuery`
/// structure.
///
/// This might be useful to track information about users.
/// Note that this function can return duplicate users.
pub fn mentioned_users(&self) -> impl Iterator<Item = &User> {
use crate::util::flatten;
use std::iter::once;
once(&self.from).chain(flatten(self.message.as_ref().map(Message::mentioned_users)))
}
}
#[cfg(test)]
mod tests {
use crate::types::UserId;

View file

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::types::{ChatId, ChatLocation, ChatPermissions, ChatPhoto, Message, True};
use crate::types::{ChatId, ChatLocation, ChatPermissions, ChatPhoto, Message, True, User};
/// This object represents a chat.
///
@ -493,6 +493,23 @@ impl Chat {
_ => None,
}
}
/// Returns all users that are "contained" in this `Chat`
/// structure.
///
/// This might be useful to track information about users.
///
/// Note that this function can return duplicate users.
pub fn mentioned_users(&self) -> impl Iterator<Item = &User> {
crate::util::flatten(self.pinned_message.as_ref().map(|m| m.mentioned_users()))
}
/// `{Message, Chat}::mentioned_users` are mutually recursive, as such we
/// can't use `->impl Iterator` everywhere, as it would make an infinite
/// type. So we need to box somewhere.
pub(crate) fn mentioned_users_rec(&self) -> impl Iterator<Item = &User> {
crate::util::flatten(self.pinned_message.as_ref().map(|m| m.mentioned_users_rec()))
}
}
mod serde_helper {

View file

@ -18,3 +18,15 @@ pub struct ChatJoinRequest {
/// Chat invite link that was used by the user to send the join request
pub invite_link: Option<ChatInviteLink>,
}
impl ChatJoinRequest {
/// Returns all users that are "contained" in this `ChatJoinRequest`
/// structure.
///
/// This might be useful to track information about users.
///
/// Note that this function can return duplicate users.
pub fn mentioned_users(&self) -> impl Iterator<Item = &User> {
std::iter::once(&self.from).chain(self.chat.mentioned_users())
}
}

View file

@ -20,3 +20,21 @@ pub struct ChatMemberUpdated {
/// joining by invite link events only.
pub invite_link: Option<ChatInviteLink>,
}
impl ChatMemberUpdated {
/// Returns all users that are "contained" in this `ChatMemberUpdated`
/// structure.
///
/// This might be useful to track information about users.
///
/// Note that this function can return duplicate users.
pub fn mentioned_users(&self) -> impl Iterator<Item = &User> {
[
&self.from,
/* ignore `old_chat_member.user`, it should always be the same as the new one */
&self.new_chat_member.user,
]
.into_iter()
.chain(self.chat.mentioned_users())
}
}

View file

@ -84,7 +84,70 @@ bitflags::bitflags! {
}
}
// FIXME: add `can_*` methods for convinience
impl ChatPermissions {
/// Checks for [`SEND_MESSAGES`] permission.
///
/// [`SEND_MESSAGES`]: ChatPermissions::SEND_MESSAGES
pub fn can_send_messages(&self) -> bool {
self.contains(ChatPermissions::SEND_MESSAGES)
}
/// Checks for [`SEND_MEDIA_MESSAGES`] permission.
///
/// [`SEND_MEDIA_MESSAGES`]: ChatPermissions::SEND_MEDIA_MESSAGES
pub fn can_send_media_messages(&self) -> bool {
self.contains(ChatPermissions::SEND_MEDIA_MESSAGES)
}
/// Checks for [`SEND_POLLS`] permission.
///
/// [`SEND_POLLS`]: ChatPermissions::SEND_POLLS
pub fn can_send_polls(&self) -> bool {
self.contains(ChatPermissions::SEND_POLLS)
}
/// Checks for [`SEND_OTHER_MESSAGES`] permission.
///
/// [`SEND_OTHER_MESSAGES`]: ChatPermissions::SEND_OTHER_MESSAGES
pub fn can_send_other_messages(&self) -> bool {
self.contains(ChatPermissions::SEND_OTHER_MESSAGES)
}
/// Checks for [`ADD_WEB_PAGE_PREVIEWS`] permission.
///
/// [`ADD_WEB_PAGE_PREVIEWS`]: ChatPermissions::ADD_WEB_PAGE_PREVIEWS
pub fn can_add_web_page_previews(&self) -> bool {
self.contains(ChatPermissions::ADD_WEB_PAGE_PREVIEWS)
}
/// Checks for [`CHANGE_INFO`] permission.
///
/// [`CHANGE_INFO`]: ChatPermissions::CHANGE_INFO
pub fn can_change_info(&self) -> bool {
self.contains(ChatPermissions::CHANGE_INFO)
}
/// Checks for [`INVITE_USERS`] permission.
///
/// [`INVITE_USERS`]: ChatPermissions::INVITE_USERS
pub fn can_invite_users(&self) -> bool {
self.contains(ChatPermissions::INVITE_USERS)
}
/// Checks for [`PIN_MESSAGES`] permission.
///
/// [`PIN_MESSAGES`]: ChatPermissions::PIN_MESSAGES
pub fn can_pin_messages(&self) -> bool {
self.contains(ChatPermissions::PIN_MESSAGES)
}
/// Checks for [`MANAGE_TOPICS`] permission.
///
/// [`MANAGE_TOPICS`]: ChatPermissions::MANAGE_TOPICS
pub fn can_manage_topics(&self) -> bool {
self.contains(ChatPermissions::MANAGE_TOPICS)
}
}
/// Helper for (de)serialization
#[derive(Serialize, Deserialize)]
@ -124,15 +187,15 @@ struct ChatPermissionsRaw {
impl From<ChatPermissions> for ChatPermissionsRaw {
fn from(this: ChatPermissions) -> Self {
Self {
can_send_messages: this.contains(ChatPermissions::SEND_MESSAGES),
can_send_media_messages: this.contains(ChatPermissions::SEND_MEDIA_MESSAGES),
can_send_polls: this.contains(ChatPermissions::SEND_POLLS),
can_send_other_messages: this.contains(ChatPermissions::SEND_OTHER_MESSAGES),
can_add_web_page_previews: this.contains(ChatPermissions::ADD_WEB_PAGE_PREVIEWS),
can_change_info: this.contains(ChatPermissions::CHANGE_INFO),
can_invite_users: this.contains(ChatPermissions::INVITE_USERS),
can_pin_messages: this.contains(ChatPermissions::PIN_MESSAGES),
can_manage_topics: this.contains(ChatPermissions::MANAGE_TOPICS),
can_send_messages: this.can_send_messages(),
can_send_media_messages: this.can_send_media_messages(),
can_send_polls: this.can_send_polls(),
can_send_other_messages: this.can_send_other_messages(),
can_add_web_page_previews: this.can_add_web_page_previews(),
can_change_info: this.can_change_info(),
can_invite_users: this.can_invite_users(),
can_pin_messages: this.can_pin_messages(),
can_manage_topics: this.can_manage_topics(),
}
}
}

View file

@ -19,3 +19,20 @@ pub struct ForumTopicCreated {
// FIXME: CustomEmojiId
pub icon_custom_emoji_id: Option<String>,
}
#[cfg(test)]
mod tests {
use crate::types::ForumTopicCreated;
#[test]
fn deserialization() {
let json =
r#"{"icon_color":9367192,"icon_custom_emoji_id":"5312536423851630001","name":"???"}"#;
let event = serde_json::from_str::<ForumTopicCreated>(json).unwrap();
assert_eq!(event.name, "???");
assert_eq!(event.icon_color, [0x8E, 0xEE, 0x98]);
assert_eq!(event.icon_custom_emoji_id.as_deref(), Some("5312536423851630001"));
}
}

View file

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::types::{Animation, MessageEntity, PhotoSize};
use crate::types::{Animation, MessageEntity, PhotoSize, User};
/// This object represents a game.
///
@ -39,3 +39,17 @@ pub struct Game {
/// [@Botfather]: https://t.me/botfather
pub animation: Option<Animation>,
}
impl Game {
/// Returns all users that are "contained" in this `Game`
/// structure.
///
/// This might be useful to track information about users.
///
/// Note that this function can return duplicate users.
pub fn mentioned_users(&self) -> impl Iterator<Item = &User> {
use crate::util::{flatten, mentioned_users_from_entities};
flatten(self.text_entities.as_deref().map(mentioned_users_from_entities))
}
}

View file

@ -123,8 +123,8 @@ impl InputFile {
///
/// This is used to coordinate with `attach://`.
pub(crate) fn id(&self) -> &str {
// FIXME: remove extra alloc
self.id.get_or_init(|| uuid::Uuid::new_v4().to_string().into())
let random = || Arc::from(&*uuid::Uuid::new_v4().as_simple().encode_lower(&mut [0; 32]));
self.id.get_or_init(random)
}
/// Returns `true` if this file needs an attachment i.e. it's not a file_id

View file

@ -26,6 +26,7 @@ pub struct Message {
/// Unique identifier of a message thread to which the message belongs; for
/// supergroups only.
// FIXME: MessageThreadId or such
#[serde(rename = "message_thread_id")]
pub thread_id: Option<i32>,
/// Date the message was sent in Unix time.
@ -116,6 +117,8 @@ pub struct MessageCommon {
pub reply_markup: Option<InlineKeyboardMarkup>,
/// `true`, if the message is sent to a forum topic.
// FIXME: `is_topic_message` is included even in service messages, like ForumTopicCreated.
// more this to `Message`
#[serde(default)]
pub is_topic_message: bool,
@ -612,7 +615,7 @@ mod getters {
MessageGroupChatCreated, MessageInvoice, MessageLeftChatMember, MessageNewChatMembers,
MessageNewChatPhoto, MessageNewChatTitle, MessagePassportData, MessagePinned,
MessageProximityAlertTriggered, MessageSuccessfulPayment, MessageSupergroupChatCreated,
PhotoSize, True, User,
MessageVideoChatParticipantsInvited, PhotoSize, True, User,
};
/// Getters for [Message] fields from [telegram docs].
@ -620,6 +623,7 @@ mod getters {
/// [Message]: crate::types::Message
/// [telegram docs]: https://core.telegram.org/bots/api#message
impl Message {
/// Returns the user who sent the message.
#[must_use]
pub fn from(&self) -> Option<&User> {
match &self.kind {
@ -1174,6 +1178,18 @@ mod getters {
}
}
#[must_use]
pub fn video_chat_participants_invited(
&self,
) -> Option<&types::VideoChatParticipantsInvited> {
match &self.kind {
VideoChatParticipantsInvited(MessageVideoChatParticipantsInvited {
video_chat_participants_invited,
}) => Some(video_chat_participants_invited),
_ => None,
}
}
#[must_use]
pub fn reply_markup(&self) -> Option<&types::InlineKeyboardMarkup> {
match &self.kind {
@ -1390,6 +1406,42 @@ impl Message {
pub fn parse_caption_entities(&self) -> Option<Vec<MessageEntityRef<'_>>> {
self.caption().zip(self.caption_entities()).map(|(t, e)| MessageEntityRef::parse(t, e))
}
/// Returns all users that are "contained" in this `Message` structure.
///
/// This might be useful to track information about users.
///
/// Note that this function may return quite a few users as it scans
/// replies, pinned messages, message entities and more. Also note that this
/// function can return duplicate users.
pub fn mentioned_users(&self) -> impl Iterator<Item = &User> {
use crate::util::{flatten, mentioned_users_from_entities};
// Lets just hope we didn't forget something here...
self.from()
.into_iter()
.chain(self.via_bot.as_ref())
.chain(self.chat.mentioned_users_rec())
.chain(flatten(self.reply_to_message().map(Self::mentioned_users_rec)))
.chain(flatten(self.new_chat_members()))
.chain(self.left_chat_member())
.chain(self.forward_from_user())
.chain(flatten(self.forward_from_chat().map(Chat::mentioned_users_rec)))
.chain(flatten(self.game().map(Game::mentioned_users)))
.chain(flatten(self.entities().map(mentioned_users_from_entities)))
.chain(flatten(self.caption_entities().map(mentioned_users_from_entities)))
.chain(flatten(self.poll().map(Poll::mentioned_users)))
.chain(flatten(self.proximity_alert_triggered().map(|a| [&a.traveler, &a.watcher])))
.chain(flatten(self.video_chat_participants_invited().and_then(|i| i.users.as_deref())))
}
/// `Message::mentioned_users` is recursive (due to replies), as such we
/// can't use `->impl Iterator` everywhere, as it would make an infinite
/// type. So we need to box somewhere.
pub(crate) fn mentioned_users_rec(&self) -> Box<dyn Iterator<Item = &User> + Send + Sync + '_> {
Box::new(self.mentioned_users())
}
}
#[cfg(test)]
@ -1836,4 +1888,36 @@ mod tests {
assert!(!entities.is_empty());
assert_eq!(entities[0].kind().clone(), MessageEntityKind::Url);
}
#[test]
fn topic_created() {
let json = r#"{
"chat":{"id":-1001847508954,"is_forum":true,"title":"twest","type":"supergroup"},
"date":1675229139,
"forum_topic_created":{
"icon_color":9367192,
"icon_custom_emoji_id":"5312536423851630001",
"name":"???"
},
"from":{
"first_name":"вафель'",
"id":1253681278,
"is_bot":false,
"language_code":"en",
"username":"wafflelapkin"
},
"is_topic_message":true,
"message_id":4,
"message_thread_id":4
}"#;
let _: Message = serde_json::from_str(json).unwrap();
}
#[test]
fn topic_message() {
let json = r#"{"chat":{"id":-1001847508954,"is_forum":true,"title":"twest","type":"supergroup"},"date":1675229140,"from":{"first_name":"вафель'","id":1253681278,"is_bot":false,"language_code":"en","username":"wafflelapkin"},"is_topic_message":true,"message_id":5,"message_thread_id":4,"reply_to_message":{"chat":{"id":-1001847508954,"is_forum":true,"title":"twest","type":"supergroup"},"date":1675229139,"forum_topic_created":{"icon_color":9367192,"icon_custom_emoji_id":"5312536423851630001","name":"???"},"from":{"first_name":"вафель'","id":1253681278,"is_bot":false,"language_code":"en","username":"wafflelapkin"},"is_topic_message":true,"message_id":4,"message_thread_id":4},"text":"blah"}"#;
let _: Message = serde_json::from_str(json).unwrap();
}
}

View file

@ -1,4 +1,4 @@
use crate::types::{MessageEntity, PollType};
use crate::types::{MessageEntity, PollType, User};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
@ -67,6 +67,20 @@ pub struct PollOption {
pub voter_count: i32,
}
impl Poll {
/// Returns all users that are "contained" in this `Poll`
/// structure.
///
/// This might be useful to track information about users.
///
/// Note that this function can return duplicate users.
pub fn mentioned_users(&self) -> impl Iterator<Item = &User> {
use crate::util::{flatten, mentioned_users_from_entities};
flatten(self.explanation_entities.as_deref().map(mentioned_users_from_entities))
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -29,7 +29,7 @@ pub struct Update {
}
impl Update {
// FIXME: rename user => from, add mentioned_users -> impl Iterator<&User>
// FIXME: add mentioned_users -> impl Iterator<&User>
/// Returns the user that performed the action that caused this update, if
/// known.
@ -37,7 +37,7 @@ impl Update {
/// This is generally the `from` field (except for `PollAnswer` where it's
/// `user` and `Poll` with `Error` which don't have such field at all).
#[must_use]
pub fn user(&self) -> Option<&User> {
pub fn from(&self) -> Option<&User> {
use UpdateKind::*;
let from = match &self.kind {
@ -59,6 +59,48 @@ impl Update {
Some(from)
}
/// Returns all users that are "contained" in this `Update` structure.
///
/// This might be useful to track information about users.
///
/// Note that this function may return quite a few users as it scans
/// replies, pinned messages, message entities, "via bot" fields and more.
/// Also note that this function can return duplicate users.
pub fn mentioned_users(&self) -> impl Iterator<Item = &User> {
use either::Either::{Left, Right};
use std::iter::{empty, once};
let i0 = Left;
let i1 = |x| Right(Left(x));
let i2 = |x| Right(Right(Left(x)));
let i3 = |x| Right(Right(Right(Left(x))));
let i4 = |x| Right(Right(Right(Right(Left(x)))));
let i5 = |x| Right(Right(Right(Right(Right(Left(x))))));
let i6 = |x| Right(Right(Right(Right(Right(Right(x))))));
match &self.kind {
UpdateKind::Message(message)
| UpdateKind::EditedMessage(message)
| UpdateKind::ChannelPost(message)
| UpdateKind::EditedChannelPost(message) => i0(message.mentioned_users()),
UpdateKind::InlineQuery(query) => i1(once(&query.from)),
UpdateKind::ChosenInlineResult(query) => i1(once(&query.from)),
UpdateKind::CallbackQuery(query) => i2(query.mentioned_users()),
UpdateKind::ShippingQuery(query) => i1(once(&query.from)),
UpdateKind::PreCheckoutQuery(query) => i1(once(&query.from)),
UpdateKind::Poll(poll) => i3(poll.mentioned_users()),
UpdateKind::PollAnswer(answer) => i1(once(&answer.user)),
UpdateKind::MyChatMember(member) | UpdateKind::ChatMember(member) => {
i4(member.mentioned_users())
}
UpdateKind::ChatJoinRequest(request) => i5(request.mentioned_users()),
UpdateKind::Error(_) => i6(empty()),
}
}
/// Returns the chat in which is update has happened, if any.
#[must_use]
pub fn chat(&self) -> Option<&Chat> {
@ -82,6 +124,11 @@ impl Update {
Some(chat)
}
#[deprecated(note = "renamed to `from`", since = "0.10.0")]
pub fn user(&self) -> Option<&User> {
self.from()
}
}
#[derive(Clone, Debug, PartialEq)]
@ -156,7 +203,10 @@ pub enum UpdateKind {
/// An error that happened during deserialization.
///
/// This allows `teloxide` to continue working even if telegram adds a new
/// kind of updates.
/// kinds of updates.
///
/// **Note that deserialize implementation always returns an empty value**,
/// teloxide fills in the data when doing deserialization.
Error(Value),
}
@ -182,94 +232,63 @@ impl<'de> Deserialize<'de> for UpdateKind {
// Try to deserialize a borrowed-str key, or else try deserializing an owned
// string key
let k = map.next_key::<&str>().or_else(|_| {
let key = map.next_key::<&str>().or_else(|_| {
map.next_key::<String>().map(|k| {
tmp = k;
tmp.as_deref()
})
});
if let Ok(Some(k)) = k {
let res = match k {
"message" => {
map.next_value::<Message>().map(UpdateKind::Message).map_err(|_| false)
let this = key
.ok()
.flatten()
.and_then(|key| match key {
"message" => map.next_value::<Message>().ok().map(UpdateKind::Message),
"edited_message" => {
map.next_value::<Message>().ok().map(UpdateKind::EditedMessage)
}
"channel_post" => {
map.next_value::<Message>().ok().map(UpdateKind::ChannelPost)
}
"edited_channel_post" => {
map.next_value::<Message>().ok().map(UpdateKind::EditedChannelPost)
}
"inline_query" => {
map.next_value::<InlineQuery>().ok().map(UpdateKind::InlineQuery)
}
"edited_message" => map
.next_value::<Message>()
.map(UpdateKind::EditedMessage)
.map_err(|_| false),
"channel_post" => map
.next_value::<Message>()
.map(UpdateKind::ChannelPost)
.map_err(|_| false),
"edited_channel_post" => map
.next_value::<Message>()
.map(UpdateKind::EditedChannelPost)
.map_err(|_| false),
"inline_query" => map
.next_value::<InlineQuery>()
.map(UpdateKind::InlineQuery)
.map_err(|_| false),
"chosen_inline_result" => map
.next_value::<ChosenInlineResult>()
.map(UpdateKind::ChosenInlineResult)
.map_err(|_| false),
"callback_query" => map
.next_value::<CallbackQuery>()
.map(UpdateKind::CallbackQuery)
.map_err(|_| false),
"shipping_query" => map
.next_value::<ShippingQuery>()
.map(UpdateKind::ShippingQuery)
.map_err(|_| false),
.ok()
.map(UpdateKind::ChosenInlineResult),
"callback_query" => {
map.next_value::<CallbackQuery>().ok().map(UpdateKind::CallbackQuery)
}
"shipping_query" => {
map.next_value::<ShippingQuery>().ok().map(UpdateKind::ShippingQuery)
}
"pre_checkout_query" => map
.next_value::<PreCheckoutQuery>()
.map(UpdateKind::PreCheckoutQuery)
.map_err(|_| false),
"poll" => map.next_value::<Poll>().map(UpdateKind::Poll).map_err(|_| false),
"poll_answer" => map
.next_value::<PollAnswer>()
.map(UpdateKind::PollAnswer)
.map_err(|_| false),
"my_chat_member" => map
.next_value::<ChatMemberUpdated>()
.map(UpdateKind::MyChatMember)
.map_err(|_| false),
"chat_member" => map
.next_value::<ChatMemberUpdated>()
.map(UpdateKind::ChatMember)
.map_err(|_| false),
.ok()
.map(UpdateKind::PreCheckoutQuery),
"poll" => map.next_value::<Poll>().ok().map(UpdateKind::Poll),
"poll_answer" => {
map.next_value::<PollAnswer>().ok().map(UpdateKind::PollAnswer)
}
"my_chat_member" => {
map.next_value::<ChatMemberUpdated>().ok().map(UpdateKind::MyChatMember)
}
"chat_member" => {
map.next_value::<ChatMemberUpdated>().ok().map(UpdateKind::ChatMember)
}
"chat_join_request" => map
.next_value::<ChatJoinRequest>()
.map(UpdateKind::ChatJoinRequest)
.map_err(|_| false),
.ok()
.map(UpdateKind::ChatJoinRequest),
_ => Some(empty_error()),
})
.unwrap_or_else(empty_error);
_ => Err(true),
};
let value_available = match res {
Ok(ok) => return Ok(ok),
Err(e) => e,
};
let mut value = serde_json::Map::new();
value.insert(
k.to_owned(),
if value_available {
map.next_value::<Value>().unwrap_or(Value::Null)
} else {
Value::Null
},
);
while let Ok(Some((k, v))) = map.next_entry::<_, Value>() {
value.insert(k, v);
}
return Ok(UpdateKind::Error(Value::Object(value)));
}
Ok(UpdateKind::Error(Value::Object(<_>::default())))
Ok(this)
}
}
@ -319,6 +338,10 @@ impl Serialize for UpdateKind {
}
}
fn empty_error() -> UpdateKind {
UpdateKind::Error(Value::Object(<_>::default()))
}
#[cfg(test)]
mod test {
use crate::types::{

View file

@ -0,0 +1,56 @@
use crate::types::{MessageEntity, User};
/// Converts an optional iterator to a flattened iterator.
pub(crate) fn flatten<I>(opt: Option<I>) -> impl Iterator<Item = I::Item>
where
I: IntoIterator,
{
struct Flat<I>(Option<I>);
impl<I> Iterator for Flat<I>
where
I: Iterator,
{
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
self.0.as_mut()?.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
match &self.0 {
None => (0, Some(0)),
Some(i) => i.size_hint(),
}
}
}
Flat(opt.map(<_>::into_iter))
}
pub(crate) fn mentioned_users_from_entities(
entities: &[MessageEntity],
) -> impl Iterator<Item = &User> {
use crate::types::MessageEntityKind::*;
entities.iter().filter_map(|entity| match &entity.kind {
TextMention { user } => Some(user),
Mention
| Hashtag
| Cashtag
| BotCommand
| Url
| Email
| PhoneNumber
| Bold
| Italic
| Underline
| Strikethrough
| Spoiler
| Code
| Pre { language: _ }
| TextLink { url: _ }
| CustomEmoji { custom_emoji_id: _ } => None,
})
}

View file

@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## unreleased
### Fixed
- Fix `split` parser for tuple variants with len < 2 ([issue #834](https://github.com/teloxide/teloxide/issues/834))
## 0.7.1 - 2023-01-17
### Fixed

View file

@ -125,8 +125,8 @@ fn parser_with_separator<'a>(
})?;
<#types>::from_str(s).map_err(|e| teloxide::utils::command::ParseError::IncorrectFormat(e.into()))?
}
),*
},
)*
)
}
};
@ -139,12 +139,12 @@ fn parser_with_separator<'a>(
let res = #res;
match splitted.next() {
Some(d) => ::std::result::Result::Err(teloxide::utils::command::ParseError::TooManyArguments {
Some(d) if !s.is_empty() => ::std::result::Result::Err(teloxide::utils::command::ParseError::TooManyArguments {
expected: #expected,
found: #expected + 1,
found: #expected + 1 + splitted.count(),
message: format!("Excess argument: {}", d),
}),
None => ::std::result::Result::Ok(res)
_ => ::std::result::Result::Ok(res)
}
}
)

View file

@ -1,6 +1,6 @@
[package]
name = "teloxide"
version = "0.12.0"
version = "0.12.2"
description = "An elegant Telegram bots framework for Rust"
rust-version.workspace = true
@ -22,7 +22,9 @@ default = ["native-tls", "ctrlc_handler", "teloxide-core/default", "auto-send"]
webhooks = ["rand"]
webhooks-axum = ["webhooks", "axum", "tower", "tower-http"]
sqlite-storage = ["sqlx"]
# FIXME: rename `sqlite-storage` -> `sqlite-storage-nativetls`
sqlite-storage = ["sqlx", "sqlx/runtime-tokio-native-tls", "native-tls"]
sqlite-storage-rustls = ["sqlx", "sqlx/runtime-tokio-rustls", "rustls"]
redis-storage = ["redis"]
cbor-serializer = ["serde_cbor"]
bincode-serializer = ["bincode"]
@ -35,7 +37,7 @@ native-tls = ["teloxide-core/native-tls"]
rustls = ["teloxide-core/rustls"]
auto-send = ["teloxide-core/auto_send"]
throttle = ["teloxide-core/throttle"]
cache-me = ["teloxide-core/cache_me"]
cache-me = ["teloxide-core/cache_me"] # FIXME: why teloxide and core use - _ differently?
trace-adaptor = ["teloxide-core/trace_adaptor"]
erased = ["teloxide-core/erased"]
@ -44,8 +46,11 @@ erased = ["teloxide-core/erased"]
nightly = ["teloxide-core/nightly"]
full = [
"webhooks",
"webhooks-axum",
"sqlite-storage",
# "sqlite-storage-rustls" is explicitly ommited here,
# since it conflicts with "sqlite-storage"
"redis-storage",
"cbor-serializer",
"bincode-serializer",
@ -63,7 +68,7 @@ full = [
[dependencies]
teloxide-core = { version = "0.9.0", path = "../teloxide-core", default-features = false }
teloxide-core = { version = "0.9.1", path = "../teloxide-core", default-features = false }
teloxide-macros = { version = "0.7.1", path = "../teloxide-macros", optional = true }
serde_json = "1.0"
@ -91,7 +96,6 @@ serde_with_macros = "1.4"
aquamarine = "0.1.11"
sqlx = { version = "0.6", optional = true, default-features = false, features = [
"runtime-tokio-native-tls",
"macros",
"sqlite",
] }
@ -116,7 +120,8 @@ tokio-stream = "0.1"
[package.metadata.docs.rs]
all-features = true
# NB: can't use `all-features = true`, because `sqlite-storage` conflicts with `sqlite-storage-rustls`
features = ["full", "nightly"]
# FIXME: Add back "-Znormalize-docs" when https://github.com/rust-lang/rust/issues/93703 is fixed
rustdoc-args = ["--cfg", "docsrs"]
rustc-args = ["--cfg", "dep_docsrs"]

View file

@ -207,12 +207,12 @@
//! [default]: DispatcherBuilder#method.default_handler
//! [error]: DispatcherBuilder#method.error_handler
//! [dialogues]: dialogue
//! [`examples/purchase.rs`]: https://github.com/teloxide/teloxide/blob/master/examples/purchase.rs
//! [`examples/purchase.rs`]: https://github.com/teloxide/teloxide/blob/master/crates/teloxide/examples/purchase.rs
//! [`Update::filter_message`]: crate::types::Update::filter_message
//! [`Update::filter_callback_query`]: crate::types::Update::filter_callback_query
//! [chain of responsibility]: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern
//! [dependency injection (DI)]: https://en.wikipedia.org/wiki/Dependency_injection
//! [`examples/dispatching_features.rs`]: https://github.com/teloxide/teloxide/blob/master/examples/dispatching_features.rs
//! [`examples/dispatching_features.rs`]: https://github.com/teloxide/teloxide/blob/master/crates/teloxide/examples/dispatching_features.rs
//! [`Update`]: crate::types::Update
pub mod dialogue;

View file

@ -91,7 +91,7 @@
//! }
//! ```
//!
//! [`examples/dialogue.rs`]: https://github.com/teloxide/teloxide/blob/master/examples/dialogue.rs
//! [`examples/dialogue.rs`]: https://github.com/teloxide/teloxide/blob/master/crates/teloxide/examples/dialogue.rs
#[cfg(feature = "redis-storage")]
pub use self::{RedisStorage, RedisStorageError};

View file

@ -16,7 +16,8 @@
| `native-tls` | Enables the [`native-tls`] TLS implementation (**enabled by default**). |
| `rustls` | Enables the [`rustls`] TLS implementation. |
| `redis-storage` | Enables the [Redis] storage support for dialogues. |
| `sqlite-storage` | Enables the [Sqlite] storage support for dialogues. |
| `sqlite-storage` | Enables the [Sqlite] storage support for dialogues (depends on `native-tls`). |
| `sqlite-storage-rustls` | Enables the [Sqlite] storage support for dialogues (depends on `rustls`, conflicts with `sqlite-storage`). |
| `cbor-serializer` | Enables the [CBOR] serializer for dialogues. |
| `bincode-serializer` | Enables the [Bincode] serializer for dialogues. |

View file

@ -4,7 +4,7 @@
//!
//! For a high-level overview, see [our GitHub repository](https://github.com/teloxide/teloxide).
//!
//! [[`examples/throw_dice.rs`](https://github.com/teloxide/teloxide/blob/master/examples/throw_dice.rs)]
//! [[`examples/throw_dice.rs`](https://github.com/teloxide/teloxide/blob/master/crates/teloxide/examples/throw_dice.rs)]
//! ```no_run
//! # #[cfg(feature = "ctrlc_handler")]
//! use teloxide::prelude::*;

View file

@ -9,7 +9,7 @@ use tokio::sync::mpsc;
use crate::{
requests::Requester,
stop::StopFlag,
types::Update,
types::{Update, UpdateKind},
update_listeners::{webhooks::Options, UpdateListener},
};
@ -186,8 +186,14 @@ pub fn axum_no_setup(
Some(tx) => tx,
};
match serde_json::from_str(&input) {
Ok(update) => {
match serde_json::from_str::<Update>(&input) {
Ok(mut update) => {
// See HACK comment in
// `teloxide_core::net::request::process_response::{closure#0}`
if let UpdateKind::Error(value) = &mut update.kind {
*value = serde_json::from_str(&input).unwrap_or_default();
}
tx.send(Ok(update)).expect("Cannot send an incoming update from the webhook")
}
Err(error) => {

View file

@ -45,9 +45,9 @@
//! assert_eq!(args, vec!["3", "hours"]);
//! ```
//!
//! See [examples/admin_bot] as a more complicated examples.
//! See [examples/admin] as a more complicated examples.
//!
//! [examples/admin_bot]: https://github.com/teloxide/teloxide/blob/master/examples/admin_bot/
//! [examples/admin]: https://github.com/teloxide/teloxide/blob/master/crates/teloxide/examples/admin.rs
use core::fmt;
use std::{

View file

@ -117,7 +117,7 @@ fn parse_with_split() {
assert_eq!(
DefaultCommands::Start(10, "hello".to_string()),
DefaultCommands::parse("/start 10 hello", "").unwrap()
DefaultCommands::parse("/start 10 hello", "").unwrap(),
);
}
@ -138,6 +138,34 @@ fn parse_with_split2() {
);
}
#[test]
#[cfg(feature = "macros")]
fn parse_with_split3() {
#[derive(BotCommands, Debug, PartialEq)]
#[command(rename_rule = "lowercase")]
#[command(parse_with = "split")]
enum DefaultCommands {
Start(u8),
Help,
}
assert_eq!(DefaultCommands::Start(10), DefaultCommands::parse("/start 10", "").unwrap(),);
}
#[test]
#[cfg(feature = "macros")]
fn parse_with_split4() {
#[derive(BotCommands, Debug, PartialEq)]
#[command(rename_rule = "lowercase")]
#[command(parse_with = "split")]
enum DefaultCommands {
Start(),
Help,
}
assert_eq!(DefaultCommands::Start(), DefaultCommands::parse("/start", "").unwrap(),);
}
#[test]
#[cfg(feature = "macros")]
fn parse_custom_parser() {