mirror of
https://github.com/teloxide/teloxide.git
synced 2024-12-22 14:35:36 +01:00
Merge remote-tracking branch 'master' into api_errors_custom_deserialize
This commit is contained in:
commit
081f75546c
31 changed files with 735 additions and 129 deletions
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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::{
|
||||
|
|
56
crates/teloxide-core/src/util.rs
Normal file
56
crates/teloxide-core/src/util.rs
Normal 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,
|
||||
})
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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. |
|
||||
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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::{
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in a new issue