diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2ccac7ea..e83969932 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,12 +15,12 @@ env: CARGO_NET_RETRY: 10 RUSTUP_MAX_RETRIES: 10 - rust_nightly: nightly-2022-07-01 + rust_nightly: nightly-2022-09-01 # When updating this, also update: # - README.md # - src/lib.rs # - down below in a matrix - rust_msrv: 1.58.0 + rust_msrv: 1.64.0 jobs: # Depends on all action that are required for a "successful" CI run. @@ -82,10 +82,10 @@ jobs: toolchain: beta features: "--features full" - rust: nightly - toolchain: nightly-2022-07-01 + toolchain: nightly-2022-09-01 features: "--all-features" - rust: msrv - toolchain: 1.58.0 + toolchain: 1.64.0 features: "--features full" steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a82450e9..4ea743ff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,35 +6,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased +## 0.11.0 - 2022-10-07 + +### Changed + +- Updated `teloxide-macros` to v0.7.0; see its [changelog](https://github.com/teloxide/teloxide-macros/blob/master/CHANGELOG.md#070---2022-10-06) for more +- Updated `teloxide-core` to v0.8.0; see its [changelog](https://github.com/teloxide/teloxide-core/blob/master/CHANGELOG.md#080---2022-10-03) for more +- `UpdateListener` now has an associated type `Err` instead of a generic +- `AsUpdateStream` now has an associated type `StreamErr` instead of a generic +- Rename `dispatching::stop_token::{AsyncStopToken, AsyncStopFlag}` => `stop::{StopToken, StopFlag}` +- Replace the generic error type `E` with `RequestError` for REPLs (`repl(_with_listener)`, `commands_repl(_with_listener)`) +- The following functions are now `#[must_use]`: + - `BotCommands::ty`. + - `CommandDescriptions::{new, global_description, username, username_from_me}`. + - `teloxide::filter_command`. + - `teloxide::dispatching::dialogue::enter`. +- `BotCommands::parse` now accept `bot_username` as `&str` + +### Added + +- `requests::ResponseResult` to `prelude` + +### Removed + +- `dispatching::stop_token::StopToken` trait (all uses are replaced with `stop::StopToken` structure) +- Some previously deprecated items + - `enable_logging!`, `enable_logging_with_filter!` + - `HandlerFactory`, `HandlerExt::dispatch_by` + ## 0.10.1 - 2022-07-22 ### Fixed - - Mark the following functions with `#[must_use]` ([PR 457](https://github.com/teloxide/teloxide/pull/457)): - - `TraceStorage::into_inner`. - - `AsyncStopToken::new_pair`. - - `AsyncStopFlag::is_stopped`. - - All from `crate::utils::{html, markdown}`. - - Rendering of GIFs in lib.rs and crates.io ([PR 681](https://github.com/teloxide/teloxide/pull/681)). +- Mark the following functions with `#[must_use]` ([PR 457](https://github.com/teloxide/teloxide/pull/457)): + - `TraceStorage::into_inner`. + - `AsyncStopToken::new_pair`. + - `AsyncStopFlag::is_stopped`. + - All from `crate::utils::{html, markdown}`. +- Rendering of GIFs in lib.rs and crates.io ([PR 681](https://github.com/teloxide/teloxide/pull/681)). ## 0.10.0 - 2022-07-21 ### Added - - Security checks based on `secret_token` param of `set_webhook` to built-in webhooks. - - `dispatching::update_listeners::{PollingBuilder, Polling, PollingStream}`. - - `DispatcherBuilder::enable_ctrlc_handler` method. +- Security checks based on `secret_token` param of `set_webhook` to built-in webhooks. +- `dispatching::update_listeners::{PollingBuilder, Polling, PollingStream}`. +- `DispatcherBuilder::enable_ctrlc_handler` method. ### Fixed - - `Dispatcher` no longer "leaks" memory for every inactive user ([PR 657](https://github.com/teloxide/teloxide/pull/657)). - - Allow specifying a path to a custom command parser in `parse_with` ([issue 668](https://github.com/teloxide/teloxide/issues/668)). +- `Dispatcher` no longer "leaks" memory for every inactive user ([PR 657](https://github.com/teloxide/teloxide/pull/657)). +- Allow specifying a path to a custom command parser in `parse_with` ([issue 668](https://github.com/teloxide/teloxide/issues/668)). ### Changed - - Add the `Key: Clone` requirement for `impl Dispatcher` [**BC**]. - - `dispatching::update_listeners::{polling_default, polling}` now return a named, `Polling<_>` type. - - Update teloxide-core to v0.7.0 with Bot API 6.1 support, see [its changelog][core07c] for more information [**BC**]. +- Add the `Key: Clone` requirement for `impl Dispatcher` [**BC**]. +- `dispatching::update_listeners::{polling_default, polling}` now return a named, `Polling<_>` type. +- Update `teloxide-core` to v0.7.0 with Bot API 6.1 support, see [its changelog][core07c] for more information [**BC**]. [core07c]: https://github.com/teloxide/teloxide-core/blob/master/CHANGELOG.md#070---2022-07-19 @@ -47,24 +75,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - - Fix Api Unknown error (Can't parse entities) on message created with `utils::markdown::user_mention_or_link` if user full name contaiins some escapable symbols eg '.' +- Fix Api Unknown error (Can't parse entities) on message created with `utils::markdown::user_mention_or_link` if user full name contaiins some escapable symbols eg '.' ## 0.9.1 - 2022-05-27 ### Fixed - - Fix `#[command(rename = "...")]` for custom command names ([issue 633](https://github.com/teloxide/teloxide/issues/633)). +- Fix `#[command(rename = "...")]` for custom command names ([issue 633](https://github.com/teloxide/teloxide/issues/633)). ## 0.9.0 - 2022-04-27 ### Added - - The `dispatching::filter_command` function (also accessible as `teloxide::filter_command`) as a shortcut for `dptree::entry().filter_command()`. - - Re-export `dptree::case!` as `teloxide::handler!` (the former is preferred for new code). +- The `dispatching::filter_command` function (also accessible as `teloxide::filter_command`) as a shortcut for `dptree::entry().filter_command()`. +- Re-export `dptree::case!` as `teloxide::handler!` (the former is preferred for new code). ### Changed - - Update teloxide-core to v0.6.0 with [Bot API 6.0] support [**BC**]. +- Update `teloxide-core` to v0.6.0 with [Bot API 6.0] support [**BC**]. [Bot API 6.0]: https://core.telegram.org/bots/api#april-16-2022 @@ -72,46 +100,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - - Fix the broken `#[derive(DialogueState)]` (function return type `dptree::Handler`). +- Fix the broken `#[derive(DialogueState)]` (function return type `dptree::Handler`). ## 0.8.1 - 2022-04-24 ### Added - - Implement `GetChatId` for `Update`. - - The `dialogue::enter()` function as a shortcut for `dptree::entry().enter_dialogue()`. +- Implement `GetChatId` for `Update`. +- The `dialogue::enter()` function as a shortcut for `dptree::entry().enter_dialogue()`. ## 0.8.0 - 2022-04-18 ### Removed - - The old dispatching system and related stuff: `dispatching`, `utils::UpState`, `prelude`, `repls2`, `crate::{dialogues_repl, dialogues_repl_with_listener}`, and `#[teloxide(subtransition)]` [**BC**]. +- The old dispatching system and related stuff: `dispatching`, `utils::UpState`, `prelude`, `repls2`, `crate::{dialogues_repl, dialogues_repl_with_listener}`, and `#[teloxide(subtransition)]` [**BC**]. ### Added - - The new API for dialogue handlers: `teloxide::handler!` ([issue 567](https://github.com/teloxide/teloxide/issues/567)). - - Built-in webhooks support via `teloxide::dispatching::update_listeners::webhooks` module. - - `Dialogue::chat_id` for retrieving a chat ID from a dialogue. +- The new API for dialogue handlers: `teloxide::handler!` ([issue 567](https://github.com/teloxide/teloxide/issues/567)). +- Built-in webhooks support via `teloxide::dispatching::update_listeners::webhooks` module. +- `Dialogue::chat_id` for retrieving a chat ID from a dialogue. ### Changed - - Updated `teloxide-core` from version `0.4.5` to version [`0.5.0`](https://github.com/teloxide/teloxide-core/releases/tag/v0.5.0) [**BC**] - - Rename `dispatching2` => `dispatching` [**BC**]. - - Rename `prelude2` => `prelude` [**BC**]. - - Move `update_listeners`, `stop_token`, `IdleShutdownError`, and `ShutdownToken` from the old `dispatching` to the new `dispatching` (previously `dispatching2`). - - Replace `crate::{commands_repl, commands_repl_with_listener, repl, repl_with_listener}` with those of the new `dispatching` [**BC**]. - - `UpdateListener::StopToken` is now always `Send` [**BC**]. - - Rename `BotCommand` trait to `BotCommands` [**BC**]. - - `BotCommands::descriptions` now returns `CommandDescriptions` instead of `String` [**BC**]. - - Mark `Dialogue::new` as `#[must_use]`. +- Updated `teloxide-core` from version `0.4.5` to version [`0.5.0`](https://github.com/teloxide/teloxide-core/releases/tag/v0.5.0) [**BC**] +- Rename `dispatching2` => `dispatching` [**BC**]. +- Rename `prelude2` => `prelude` [**BC**]. +- Move `update_listeners`, `stop_token`, `IdleShutdownError`, and `ShutdownToken` from the old `dispatching` to the new `dispatching` (previously `dispatching2`). +- Replace `crate::{commands_repl, commands_repl_with_listener, repl, repl_with_listener}` with those of the new `dispatching` [**BC**]. +- `UpdateListener::StopToken` is now always `Send` [**BC**]. +- Rename `BotCommand` trait to `BotCommands` [**BC**]. +- `BotCommands::descriptions` now returns `CommandDescriptions` instead of `String` [**BC**]. +- Mark `Dialogue::new` as `#[must_use]`. ### Fixed - - Concurrent update handling in the new dispatcher ([issue 536](https://github.com/teloxide/teloxide/issues/536)). +- Concurrent update handling in the new dispatcher ([issue 536](https://github.com/teloxide/teloxide/issues/536)). ### Deprecated - - `HandlerFactory` and `HandlerExt::dispatch_by` in favour of `teloxide::handler!`. +- `HandlerFactory` and `HandlerExt::dispatch_by` in favour of `teloxide::handler!`. ## 0.7.3 - 2022-04-03 @@ -159,8 +187,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - - `BotCommand::bot_commands` to obtain Telegram API commands ([issue 262](https://github.com/teloxide/teloxide/issues/262)). - - The `dispatching2` and `prelude2` modules. They presents a new dispatching model based on `dptree`. +- `BotCommand::bot_commands` to obtain Telegram API commands ([issue 262](https://github.com/teloxide/teloxide/issues/262)). +- The `dispatching2` and `prelude2` modules. They presents a new dispatching model based on `dptree`. ### Changed @@ -199,81 +227,83 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - - `Storage::get_dialogue` to obtain a dialogue indexed by a chat ID. - - `InMemStorageError` with a single variant `DialogueNotFound` to be returned from `InMemStorage::remove_dialogue`. - - `RedisStorageError::DialogueNotFound` and `SqliteStorageError::DialogueNotFound` to be returned from `Storage::remove_dialogue`. - - A way to `shutdown` dispatcher - - `Dispatcher::shutdown_token` function. - - `ShutdownToken` with a `shutdown` function. - - `Dispatcher::setup_ctrlc_handler` function ([issue 153](https://github.com/teloxide/teloxide/issues/153)). - - `IdleShutdownError` - - Automatic update filtering ([issue 389](https://github.com/teloxide/teloxide/issues/389)). - - Added reply shortcut to every kind of messages ([PR 404](https://github.com/teloxide/teloxide/pull/404)). +- `Storage::get_dialogue` to obtain a dialogue indexed by a chat ID. +- `InMemStorageError` with a single variant `DialogueNotFound` to be returned from `InMemStorage::remove_dialogue`. +- `RedisStorageError::DialogueNotFound` and `SqliteStorageError::DialogueNotFound` to be returned from `Storage::remove_dialogue`. +- A way to `shutdown` dispatcher + - `Dispatcher::shutdown_token` function. + - `ShutdownToken` with a `shutdown` function. +- `Dispatcher::setup_ctrlc_handler` function ([issue 153](https://github.com/teloxide/teloxide/issues/153)). +- `IdleShutdownError` +- Automatic update filtering ([issue 389](https://github.com/teloxide/teloxide/issues/389)). +- Added reply shortcut to every kind of messages ([PR 404](https://github.com/teloxide/teloxide/pull/404)). ### Changed - - Do not return a dialogue from `Storage::{remove_dialogue, update_dialogue}`. - - Return an error from `Storage::remove_dialogue` if a dialogue does not exist. - - Require `D: Clone` in `dialogues_repl(_with_listener)` and `InMemStorage`. - - Automatically delete a webhook if it was set up in `update_listeners::polling_default` (thereby making it `async`, [issue 319](https://github.com/teloxide/teloxide/issues/319)). - - `polling` and `polling_default` now require `R: 'static` - - Refactor `UpdateListener` trait: - - Add a `StopToken` associated type. - - It must implement a new `StopToken` trait which has the only function `fn stop(self);` - - Add a `stop_token` function that returns `Self::StopToken` and allows stopping the listener later ([issue 166](https://github.com/teloxide/teloxide/issues/166)). - - Remove blanked implementation. - - Remove `Stream` from super traits. - - Add `AsUpdateStream` to super traits. - - Add an `AsUpdateStream` trait that allows turning implementors into streams of updates (GAT workaround). - - Add a `timeout_hint` function (with a default implementation). - - `Dispatcher::dispatch` and `Dispatcher::dispatch_with_listener` now require mutable reference to self. - - Repls can now be stopped by `^C` signal. - - `Noop` and `AsyncStopToken`stop tokens. - - `StatefulListener`. - - Emit not only errors but also warnings and general information from teloxide, when set up by `enable_logging!`. - - Use `i64` instead of `i32` for `user_id` in `html::user_mention` and `markdown::user_mention`. - - Updated to `teloxide-core` `v0.3.0` (see it's [changelog](https://github.com/teloxide/teloxide-core/blob/master/CHANGELOG.md#030---2021-07-05) for more) +- Do not return a dialogue from `Storage::{remove_dialogue, update_dialogue}`. +- Return an error from `Storage::remove_dialogue` if a dialogue does not exist. +- Require `D: Clone` in `dialogues_repl(_with_listener)` and `InMemStorage`. +- Automatically delete a webhook if it was set up in `update_listeners::polling_default` (thereby making it `async`, [issue 319](https://github.com/teloxide/teloxide/issues/319)). +- `polling` and `polling_default` now require `R: 'static` +- Refactor `UpdateListener` trait: + - Add a `StopToken` associated type. + - It must implement a new `StopToken` trait which has the only function `fn stop(self);` + - Add a `stop_token` function that returns `Self::StopToken` and allows stopping the listener later ([issue 166](https://github.com/teloxide/teloxide/issues/166)). + - Remove blanked implementation. + - Remove `Stream` from super traits. + - Add `AsUpdateStream` to super traits. + - Add an `AsUpdateStream` trait that allows turning implementors into streams of updates (GAT workaround). + - Add a `timeout_hint` function (with a default implementation). +- `Dispatcher::dispatch` and `Dispatcher::dispatch_with_listener` now require mutable reference to self. +- Repls can now be stopped by `^C` signal. +- `Noop` and `AsyncStopToken`stop tokens. +- `StatefulListener`. +- Emit not only errors but also warnings and general information from teloxide, when set up by `enable_logging!`. +- Use `i64` instead of `i32` for `user_id` in `html::user_mention` and `markdown::user_mention`. +- Updated to `teloxide-core` `v0.3.0` (see it's [changelog](https://github.com/teloxide/teloxide-core/blob/master/CHANGELOG.md#030---2021-07-05) for more) ### Fixed - - Remove the `reqwest` dependency. It's not needed after the [teloxide-core] integration. - - A storage persistency bug ([issue 304](https://github.com/teloxide/teloxide/issues/304)). - - Log errors from `Storage::{remove_dialogue, update_dialogue}` in `DialogueDispatcher` ([issue 302](https://github.com/teloxide/teloxide/issues/302)). - - Mark all the functions of `Storage` as `#[must_use]`. +- Remove the `reqwest` dependency. It's not needed after the [teloxide-core] integration. +- A storage persistency bug ([issue 304](https://github.com/teloxide/teloxide/issues/304)). +- Log errors from `Storage::{remove_dialogue, update_dialogue}` in `DialogueDispatcher` ([issue 302](https://github.com/teloxide/teloxide/issues/302)). +- Mark all the functions of `Storage` as `#[must_use]`. ## 0.4.0 - 2021-03-22 ### Added - - Integrate [teloxide-core]. - - Allow arbitrary error types to be returned from (sub)transitions ([issue 242](https://github.com/teloxide/teloxide/issues/242)). - - The `respond` function, a shortcut for `ResponseResult::Ok(())`. - - The `sqlite-storage` feature -- enables SQLite support. - - `Dispatcher::{my_chat_members_handler, chat_members_handler}` + +- Integrate [teloxide-core]. +- Allow arbitrary error types to be returned from (sub)transitions ([issue 242](https://github.com/teloxide/teloxide/issues/242)). +- The `respond` function, a shortcut for `ResponseResult::Ok(())`. +- The `sqlite-storage` feature -- enables SQLite support. +- `Dispatcher::{my_chat_members_handler, chat_members_handler}` [teloxide-core]: https://github.com/teloxide/teloxide-core ### Deprecated - - `UpdateWithCx::answer_str` +- `UpdateWithCx::answer_str` ### Fixed - - Hide `SubtransitionOutputType` from the docs. +- Hide `SubtransitionOutputType` from the docs. ### Changed - - Export `teloxide_macros::teloxide` in `prelude`. - - `dispatching::dialogue::serializer::{JSON -> Json, CBOR -> Cbor}` - - Allow `bot_name` be `N`, where `N: Into + ...` in `commands_repl` & `commands_repl_with_listener`. - - 'Edit methods' (namely `edit_message_live_location`, `stop_message_live_location`, `edit_message_text`, + +- Export `teloxide_macros::teloxide` in `prelude`. +- `dispatching::dialogue::serializer::{JSON -> Json, CBOR -> Cbor}` +- Allow `bot_name` be `N`, where `N: Into + ...` in `commands_repl` & `commands_repl_with_listener`. +- 'Edit methods' (namely `edit_message_live_location`, `stop_message_live_location`, `edit_message_text`, `edit_message_caption`, `edit_message_media` and `edit_message_reply_markup`) are split into common and inline versions (e.g.: `edit_message_text` and `edit_inline_message_text`). Instead of `ChatOrInlineMessage` common versions accept `chat_id: impl Into` and `message_id: i32` whereas inline versions accept `inline_message_id: impl Into`. Also note that return type of inline versions is `True` ([issue 253], [pr 257]) - - `ChatOrInlineMessage` is renamed to `TargetMessage`, it's `::Chat` variant is renamed to `::Common`, +- `ChatOrInlineMessage` is renamed to `TargetMessage`, it's `::Chat` variant is renamed to `::Common`, `#[non_exhaustive]` annotation is removed from the enum, type of `TargetMessage::Inline::inline_message_id` changed `i32` => `String`. `TargetMessage` now implements `From`, `get_game_high_scores` and `set_game_score` use `Into` to accept `String`s. ([issue 253], [pr 257]) - - Remove `ResponseResult` from `prelude`. +- Remove `ResponseResult` from `prelude`. [issue 253]: https://github.com/teloxide/teloxide/issues/253 [pr 257]: https://github.com/teloxide/teloxide/pull/257 @@ -282,75 +312,88 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - - Failing compilation with `serde::export` ([issue 328](https://github.com/teloxide/teloxide/issues/328)). +- Failing compilation with `serde::export` ([issue 328](https://github.com/teloxide/teloxide/issues/328)). ## 0.3.3 - 2020-10-30 ### Fixed - - The `dice` field from `MessageDice` is public now ([issue 306](https://github.com/teloxide/teloxide/issues/306)) + +- The `dice` field from `MessageDice` is public now ([issue 306](https://github.com/teloxide/teloxide/issues/306)) ## 0.3.2 - 2020-10-23 ### Added - - `LoginUrl::new` ([issue 298](https://github.com/teloxide/teloxide/issues/298)) + +- `LoginUrl::new` ([issue 298](https://github.com/teloxide/teloxide/issues/298)) ## 0.3.1 - 2020-08-25 ### Added - - `Bot::builder` method ([PR 269](https://github.com/teloxide/teloxide/pull/269)). + +- `Bot::builder` method ([PR 269](https://github.com/teloxide/teloxide/pull/269)). ## 0.3.0 - 2020-07-31 + ### Added - - Support for typed bot commands ([issue 152](https://github.com/teloxide/teloxide/issues/152)). - - `BotBuilder`, which allows setting a default `ParseMode`. - - The `Transition`, `Subtransition`, `SubtransitionOutputType` traits. - - A nicer approach to manage dialogues via `#[derive(Transition)]` + `#[teloxide(subtransition)]` (see [`examples/dialogue_bot`](https://github.com/teloxide/teloxide/tree/af2aa218e7bfc442ab4475023a1c661834f576fc/examples/dialogue_bot)). - - The `redis-storage` feature -- enables the Redis support. - - The `cbor-serializer` feature -- enables the `CBOR` serializer for dialogues. - - The `bincode-serializer` feature -- enables the `Bincode` serializer for dialogues. - - The `frunk` feature -- enables `teloxide::utils::UpState`, which allows mapping from a structure of `field1, ..., fieldN` to a structure of `field1, ..., fieldN, fieldN+1`. - - Upgrade to v4.9 Telegram bots API. - - `teloxide::utils::client_from_env` -- constructs a client from the `TELOXIDE_TOKEN` environmental variable. - - Import `Transition`, `TransitionIn`, `TransitionOut`, `UpState` to `teloxide::prelude`. - - Import `repl`, `commands_repl` to `teloxide`. - - Let users inspect an unknown API error using `ApiErrorKind::Unknown(String)`. All the known API errors are placed into `KnownApiErrorKind`. - - Setters to all the API types. - - `teloxide::dispatching::dialogue::serializer` -- various serializers for memory storages. The `Serializer` trait, `Bincode`, `CBOR`, `JSON`. - - `teloxide::{repl, repl_with_listener, commands_repl, commands_repl_with_listener, dialogues_repl, dialogues_repl_with_listener}` - - `InputFile::Memory` - - Option to hide a command from description ([issue 217](https://github.com/teloxide/teloxide/issues/217)). - - Respect the `TELOXIDE_PROXY` environment variable in `Bot::from_env`. + +- Support for typed bot commands ([issue 152](https://github.com/teloxide/teloxide/issues/152)). +- `BotBuilder`, which allows setting a default `ParseMode`. +- The `Transition`, `Subtransition`, `SubtransitionOutputType` traits. +- A nicer approach to manage dialogues via `#[derive(Transition)]` + `#[teloxide(subtransition)]` (see [`examples/dialogue_bot`](https://github.com/teloxide/teloxide/tree/af2aa218e7bfc442ab4475023a1c661834f576fc/examples/dialogue_bot)). +- The `redis-storage` feature -- enables the Redis support. +- The `cbor-serializer` feature -- enables the `CBOR` serializer for dialogues. +- The `bincode-serializer` feature -- enables the `Bincode` serializer for dialogues. +- The `frunk` feature -- enables `teloxide::utils::UpState`, which allows mapping from a structure of `field1, ..., fieldN` to a structure of `field1, ..., fieldN, fieldN+1`. +- Upgrade to v4.9 Telegram bots API. +- `teloxide::utils::client_from_env` -- constructs a client from the `TELOXIDE_TOKEN` environmental variable. +- Import `Transition`, `TransitionIn`, `TransitionOut`, `UpState` to `teloxide::prelude`. +- Import `repl`, `commands_repl` to `teloxide`. +- Let users inspect an unknown API error using `ApiErrorKind::Unknown(String)`. All the known API errors are placed into `KnownApiErrorKind`. +- Setters to all the API types. +- `teloxide::dispatching::dialogue::serializer` -- various serializers for memory storages. The `Serializer` trait, `Bincode`, `CBOR`, `JSON`. +- `teloxide::{repl, repl_with_listener, commands_repl, commands_repl_with_listener, dialogues_repl, dialogues_repl_with_listener}` +- `InputFile::Memory` +- Option to hide a command from description ([issue 217](https://github.com/teloxide/teloxide/issues/217)). +- Respect the `TELOXIDE_PROXY` environment variable in `Bot::from_env`. ### Deprecated - - `Bot::{from_env_with_client, new, with_client}` + +- `Bot::{from_env_with_client, new, with_client}` ### Changed - - `DialogueDispatcherHandlerCx` -> `DialogueWithCx`. - - `DispatcherHandlerCx` -> `UpdateWithCx`. - - Now provided description of unknown telegram error, by splitting ApiErrorKind at `ApiErrorKind` and `ApiErrorKindKnown` enums ([issue 199](https://github.com/teloxide/teloxide/issues/199)). - - Extract `Bot` from `Arc` ([issue 216](https://github.com/teloxide/teloxide/issues/230)). - - Mark all the API types as `#[non_exhaustive]`. - - Replace all `mime_type: String` with `MimeWrapper`. + +- `DialogueDispatcherHandlerCx` -> `DialogueWithCx`. +- `DispatcherHandlerCx` -> `UpdateWithCx`. +- Now provided description of unknown telegram error, by splitting ApiErrorKind at `ApiErrorKind` and `ApiErrorKindKnown` enums ([issue 199](https://github.com/teloxide/teloxide/issues/199)). +- Extract `Bot` from `Arc` ([issue 216](https://github.com/teloxide/teloxide/issues/230)). +- Mark all the API types as `#[non_exhaustive]`. +- Replace all `mime_type: String` with `MimeWrapper`. ### Fixed - - Now methods which can send file to Telegram returns `tokio::io::Result`. Early its could panic ([issue 216](https://github.com/teloxide/teloxide/issues/216)). - - If a bot wasn't triggered for several days, it stops responding ([issue 223](https://github.com/teloxide/teloxide/issues/223)). + +- Now methods which can send file to Telegram returns `tokio::io::Result`. Early its could panic ([issue 216](https://github.com/teloxide/teloxide/issues/216)). +- If a bot wasn't triggered for several days, it stops responding ([issue 223](https://github.com/teloxide/teloxide/issues/223)). ## 0.2.0 - 2020-02-25 + ### Added - - The functionality to parse commands only with a correct bot's name (breaks backwards compatibility) ([Issue 168](https://github.com/teloxide/teloxide/issues/168)). - - This `CHANGELOG.md`. + +- The functionality to parse commands only with a correct bot's name (breaks backwards compatibility) ([Issue 168](https://github.com/teloxide/teloxide/issues/168)). +- This `CHANGELOG.md`. ### Fixed - - Fix parsing a pinned message ([Issue 167](https://github.com/teloxide/teloxide/issues/167)). - - Replace `LanguageCode` with `String`, because [the official Telegram documentation](https://core.telegram.org/bots/api#getchat) doesn't specify a concrete version of IETF language tag. - - Problems with the `poll_type` field ([Issue 178](https://github.com/teloxide/teloxide/issues/178)). - - Make `polling_default` actually a long polling update listener ([PR 182](https://github.com/teloxide/teloxide/pull/182)). + +- Fix parsing a pinned message ([Issue 167](https://github.com/teloxide/teloxide/issues/167)). +- Replace `LanguageCode` with `String`, because [the official Telegram documentation](https://core.telegram.org/bots/api#getchat) doesn't specify a concrete version of IETF language tag. +- Problems with the `poll_type` field ([Issue 178](https://github.com/teloxide/teloxide/issues/178)). +- Make `polling_default` actually a long polling update listener ([PR 182](https://github.com/teloxide/teloxide/pull/182)). ### Removed - - [either](https://crates.io/crates/either) from the dependencies in `Cargo.toml`. - - `teloxide-macros` migrated into [the separate repository](https://github.com/teloxide/teloxide-macros) to easier releases and testing. + +- [either](https://crates.io/crates/either) from the dependencies in `Cargo.toml`. +- `teloxide-macros` migrated into [the separate repository](https://github.com/teloxide/teloxide-macros) to easier releases and testing. ## 0.1.0 - 2020-02-19 + ### Added - - This project. +- This project. diff --git a/CODE_STYLE.md b/CODE_STYLE.md index 0fa0756b1..40b65d862 100644 --- a/CODE_STYLE.md +++ b/CODE_STYLE.md @@ -1,127 +1,408 @@ # Code style -This is a description of a coding style that every contributor must follow. Please, read the whole document before you start pushing code. + +This is a description of a coding style that every contributor must follow. +Please, read the whole document before you start pushing code. ## Generics -Generics are always written with `where`. -Bad: +All trait bounds should be written in `where`: ```rust - pub fn new, - T: Into, - P: Into, - E: Into> +// GOOD +pub fn new(user_id: i32, name: N, title: T, png_sticker: P, emojis: E) -> Self +where + N: Into, + T: Into, + P: Into, + E: Into, +{ ... } + +// BAD +pub fn new, + T: Into, + P: Into, + E: Into> (user_id: i32, name: N, title: T, png_sticker: P, emojis: E) -> Self { ... } ``` +```rust +// GOOD +impl Trait for Wrap +where + T: Trait +{ ... } + +// BAD +impl Trait for Wrap { ... } +``` + +**Rationale:** +- `where` clauses are easier to read when there are a lot of bounds +- uniformity + +## Documentation comments + +1. Documentation must describe _what_ your code does and mustn't describe _how_ your code does it and bla-bla-bla. +2. Be sure that your comments follow the grammar, including punctuation, the first capital letter and so on: + ```rust + // GOOD + /// This function makes a request to Telegram. + pub fn make_request(url: &str) -> String { ... } + + // BAD + /// this function make request to telegram + pub fn make_request(url: &str) -> String { ... } + ``` +3. Do not use ending punctuation in short list items (usually containing just one phrase or sentence): + ```md + + - Handle different kinds of Update + - Pass dependencies to handlers + - Disable a default Ctrl-C handling + + + - Handle different kinds of Update. + - Pass dependencies to handlers. + - Disable a default Ctrl-C handling. -Good: + + - Handle different kinds of Update; + - Pass dependencies to handlers; + - Disable a default Ctrl-C handling; + ``` +3. Link resources in your comments when possible: + ```rust + /// Download a file from Telegram. + /// + /// `path` can be obtained from the [`Bot::get_file`]. + /// + /// To download into [`AsyncWrite`] (e.g. [`tokio::fs::File`]), see + /// [`Bot::download_file`]. + /// + /// [`Bot::get_file`]: crate::bot::Bot::get_file + /// [`AsyncWrite`]: tokio::io::AsyncWrite + /// [`tokio::fs::File`]: tokio::fs::File + /// [`Bot::download_file`]: crate::Bot::download_file + ``` +4. Write `teloxide`, `teloxide-macros`, and `teloxide-core`, not "teloxide", "Teloxide", "teloxide-macros" or any other variant. + +## Use `Self` where possible + +When referring to the type for which block is implemented, prefer using `Self`, rather than the name of the type: + +```rust +impl ErrorKind { + // GOOD + fn print(&self) { + Self::Io => println!("Io"), + Self::Network => println!("Network"), + Self::Json => println!("Json"), + } + // BAD + fn print(&self) { + ErrorKind::Io => println!("Io"), + ErrorKind::Network => println!("Network"), + ErrorKind::Json => println!("Json"), + } +} +``` ```rust - pub fn new(user_id: i32, name: N, title: T, png_sticker: P, emojis: E) -> Self +impl<'a> AnswerCallbackQuery<'a> { + // GOOD + fn new(bot: &'a Bot, callback_query_id: C) -> Self where - N: Into, - T: Into, - P: Into, - E: Into { ... } + C: Into, + { ... } + + // BAD + fn new(bot: &'a Bot, callback_query_id: C) -> AnswerCallbackQuery<'a> + where + C: Into, + { ... } +} ``` -## Comments - 1. Comments must describe what your code does and mustn't describe how your code does it and bla-bla-bla. Be sure that your comments follow the grammar, including punctuation, the first capital letter and so on. +**Rationale:** `Self` is generally shorter and it's easier to copy-paste code or rename the type. -Bad: +## Avoid duplication in fields names ```rust -/// this function make request to telegram -pub fn make_request(url: &str) -> String { ... } +struct Message { + // GOOD + #[serde(rename = "message_id")] + id: MessageId, + + // BAD + message_id: MessageId, +} ``` -Good: +**Rationale:** duplication blurs the focus of code, making it unnecessarily longer. + +## Conventional generic names + +Use a generic parameter name `S` for streams, `Fut` for futures, `F` for functions (where possible). + +**Rationale:** uniformity. + +## Deriving traits + +Derive `Copy`, `Clone`, `Eq`, `PartialEq`, `Hash` and `Debug` for public types when possible. + +**Rationale:** these traits can be useful for users and can be implemented for most types. + +Derive `Default` when there is a reasonable default value for the type. + +**Rationale:** `Default` plays nicely with generic code (for example, `mem::take`). + +## `Into`-polymorphism + +Use `T: Into` when this can simplify user code. +I.e. when there are types that implement `Into` that are likely to be passed to this function. + +**Rationale:** conversions unnecessarily complicate caller code and can be confusing for beginners. + +## `must_use` + +Always mark functions as `#[must_use]` if they don't have side effects and the only reason to call them is to get the result: ```rust -/// This function makes a request to Telegram. -pub fn make_request(url: &str) -> String { ... } +impl User { + // GOOD + #[must_use] + fn full_name(&self) -> String { + format!("{} {}", user.first_name, user.last_name) + } +} +``` + +**Rationale:** users will get warnings if they forgot to do something with the result, potentially preventing bugs. + +## Creating boxed futures + +Prefer `Box::pin(async { ... })` instead of `async { ... }.boxed()`. + +**Rationale:** the former is generally formatted better by rustfmt. + +## Full paths for logging + +Always write `log::!(...)` instead of importing `use log::;` and invoking `!(...)`. + +```rust +// GOOD +log::warn!("Everything is on fire"); + +// BAD +use log::warn; + +warn!("Everything is on fire"); +``` + +**Rationale:** +- Less polluted import blocks +- Uniformity + +## `&str` -> `String` conversion + +Prefer using `.to_owned()`, rather than `.to_string()`, `.into()`, `String::from`, etc. + +**Rationale:** uniformity, intent clarity. + +## Order of imports + +Separate import groups with blank lines. Use one use per crate. + +Module declarations come before the imports. +Order them in "suggested reading order" for a person new to the code base. + +```rust +mod x; +mod y; + +// First std. +use std::{ ... } + +// Second, external crates (both crates.io crates and other rust-analyzer crates). +use crate_foo::{ ... } +use crate_bar::{ ... } + +// Then current crate. +use crate::{} + +// Finally, parent and child modules, but prefer `use crate::`. +use super::{} + +// Re-exports are treated as item definitions rather than imports, so they go +// after imports and modules. Use them sparingly. +pub use crate::x::Z; ``` - 2. Also, link resources in your comments when possible: +**Rationale:** +- Reading order is important for new contributors +- Grouping by crate allows spotting unwanted dependencies easier +- Consistency + +## Import Style + +When implementing traits from `std::fmt` import the module: ```rust -/// Download a file from Telegram. -/// -/// `path` can be obtained from the [`Bot::get_file`]. -/// -/// To download into [`AsyncWrite`] (e.g. [`tokio::fs::File`]), see -/// [`Bot::download_file`]. -/// -/// [`Bot::get_file`]: crate::bot::Bot::get_file -/// [`AsyncWrite`]: tokio::io::AsyncWrite -/// [`tokio::fs::File`]: tokio::fs::File -/// [`Bot::download_file`]: crate::Bot::download_file -#[cfg(feature = "unstable-stream")] -pub async fn download_file_stream( - &self, - path: &str, -) -> Result>, reqwest::Error> -{ - download_file_stream(&self.client, &self.token, path).await +// GOOD +use std::fmt; + +impl fmt::Display for RenameError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { .. } +} + +// BAD +impl std::fmt::Display for RenameError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { .. } } ``` -## Use Self where possible -Bad: +**Rationale:** +- Makes it clear that a trait is implemented, rather than used +- Less typing + +Prefer `use crate::foo::bar` to `use super::bar` or `use self::bar::baz`. **Rationale:** +- Works in all cases +- Consistency + +## Order of Items +Optimize for the reader who sees the file for the first time, and wants to get a general idea about what's going on. People read things from top to bottom, so place most important things first. + +Specifically, if all items except one are private, always put the non-private item on top: ```rust -impl ErrorKind { - fn print(&self) { - ErrorKind::Io => println!("Io"), - ErrorKind::Network => println!("Network"), - ErrorKind::Json => println!("Json"), +// GOOD +pub(crate) fn frobnicate() { + Helper::act() +} + +#[derive(Default)] +struct Helper { stuff: i32 } + +impl Helper { + fn act(&self) { + + } +} + +// BAD +#[derive(Default)] +struct Helper { stuff: i32 } + +pub(crate) fn frobnicate() { + Helper::act() +} + +impl Helper { + fn act(&self) { + } } ``` -Good: +If there's a mixture of private and public items, put public items first. + +Put structs and enums first, functions and impls last. Order type declarations in a top-down manner: + ```rust -impl ErrorKind { - fn print(&self) { - Self::Io => println!("Io"), - Self::Network => println!("Network"), - Self::Json => println!("Json"), - } +// GOOD +struct Parent { + children: Vec +} + +struct Child; + +impl Parent { +} + +impl Child { +} + +// BAD +struct Child; + +impl Child { +} + +struct Parent { + children: Vec +} + +impl Parent { } ``` -
- More examples - -Bad: - +**Rationale:** +- Easier to get a sense of the API by visually scanning the file +- If function bodies are folded in the editor, the source code should be read as documentation for the public API + +## Early Returns + +Do use early returns: + ```rust -impl<'a> AnswerCallbackQuery<'a> { - pub(crate) fn new(bot: &'a Bot, callback_query_id: C) -> AnswerCallbackQuery<'a> - where -C: Into, { ... } +// GOOD +fn foo() -> Option { + if !condition() { + return None; + } + + Some(...) +} + +// BAD +fn foo() -> Option { + if condition() { + Some(...) + } else { + None + } +} ``` -Good: - +**Rationale:** reduce cognitive stack usage. + +## If-let + +Avoid the `if let ... { } else { }` construct, use `match` instead: + ```rust -impl<'a> AnswerCallbackQuery<'a> { - pub(crate) fn new(bot: &'a Bot, callback_query_id: C) -> Self - where -C: Into, { ... } +// GOOD +match ctx.expected_type.as_ref() { + Some(expected_type) => completion_ty == expected_type && !expected_type.is_unit(), + None => false, +} + +// BAD +if let Some(expected_type) = ctx.expected_type.as_ref() { + completion_ty == expected_type && !expected_type.is_unit() +} else { + false +} ``` -
-## Naming - 1. Avoid unnecessary duplication (`Message::message_id` -> `Message::id` using `#[serde(rename = "message_id")]`). - 2. Use a generic parameter name `S` for streams, `Fut` for futures, `F` for functions (where possible). +**Rationale:** +- `match` is almost always more compact +- The `else` branch can get a more precise pattern: `None` or `Err(_)` instead of `_` + +## Empty Match Arms + +Use `=> (),` when a match arm is intentionally empty: +```rust +// GOOD +match result { + Ok(_) => (), + Err(err) => error!("{}", err), +} -## Deriving - 1. Derive `Copy`, `Eq`, `Hash`, `PartialEq`, `Clone`, `Debug` for public types when possible (note: if the default `Debug` implementation is weird, you should manually implement it by yourself). - 2. Derive `Default` when there is an algorithm to get a default value for your type. +// BAD +match result { + Ok(_) => {} + Err(err) => error!("{}", err), +} +``` -## Misc - 1. Use `Into<...>` only where there exists at least one conversion **and** it will be logically to use. - 2. Always mark a function as `#[must_use]` if its return value **must** be used. - 3. `Box::pin(async [move] { ... })` instead of `async [move] { ... }.boxed()`. - 4. Always write `log::!(...)` instead of importing `use log::;` and invoking `!(...)`. For example, write `log::info!("blah")`. +**Rationale:** consistency. diff --git a/Cargo.toml b/Cargo.toml index 076616913..65e5135ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "teloxide" -version = "0.10.1" +version = "0.11.0" edition = "2021" description = "An elegant Telegram bots framework for Rust" repository = "https://github.com/teloxide/teloxide" @@ -57,8 +57,8 @@ full = [ ] [dependencies] -teloxide-core = { version = "0.7.0", default-features = false } -teloxide-macros = { version = "0.6.3", optional = true } +teloxide-core = { version = "0.8.0", default-features = false } +teloxide-macros = { version = "0.7.0", optional = true } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } @@ -66,8 +66,8 @@ serde = { version = "1.0", features = ["derive"] } dptree = "0.3.0" # These lines are used only for development. -# teloxide-core = { git = "https://github.com/teloxide/teloxide-core", rev = "b13393d", default-features = false } -# teloxide-macros = { git = "https://github.com/teloxide/teloxide-macros", rev = "44d91c5", optional = true } +# teloxide-core = { git = "https://github.com/teloxide/teloxide-core", rev = "00165e6", default-features = false } +# teloxide-macros = { git = "https://github.com/teloxide/teloxide-macros", rev = "e715105", optional = true } # dptree = { git = "https://github.com/teloxide/dptree", rev = "df578e4" } tokio = { version = "1.8", features = ["fs"] } diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 78003f486..83ea0829e 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -1,6 +1,108 @@ This document describes breaking changes of `teloxide` crate, as well as the ways to update code. Note that the list of required changes is not fully exhaustive and it may lack something in rare cases. +## 0.10 -> 0.11 + +### core + +Requests can now be `.await`ed directly, without need of `.send()` or `AutoSend`. +If you previously used `AutoSend` adaptor, you can safely remove it: + +```diff,rust +-let bot = Bot::from_env().auto_send(); ++let bot = Bot::from_env(); +``` + +```diff,rust +-async fn start(bot: AutoSend, dialogue: MyDialogue, msg: Message) -> HandlerResult { ++async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult { +``` + +`File`'s and `FileMeta`'s fields now don't have `file_` prefix. +If you previously accessed the fields, you'll need to change remove the prefix: + +```diff +-_ = file.file_size; ++_ = file.size; +``` + +`Animation`, `Audio`, `Document`, `PassportFile`, `PhotoSize`, `Video`, `VideoNote` and `Voice` now contain `FileMeta` instead of its fields. +Together with rename of `FileMeta`'s fields, you'll need to change `_` to `.`: + +```diff +-_ = animation.file_size; ++_ = animation.file.size; +``` + +Message id fields and parameters now use `MessageId` type, instead of `i32`. +You may need to change code accordingly: + +```diff +-let id: i32 = message.id; ++let id: MessageId = message.id; +``` +```diff,rust +let (cid, mid): (ChatId, i32) = get_message_to_delete_from_db(); +-bot.delete_message(cid, mid).await?; ++bot.delete_message(cid, MessageId(mid)).await?; +``` + +Note that at the same time `MessageId` is now a tuple struct. +If you've accessed its only field you'll need to change it too: + +```diff,rust +-let MessageId { message_id } = bot.copy_message(dst_chat, src_chat, mid).await?; ++let MessageId(message_id) = bot.copy_message(dst_chat, src_chat, mid).await?; +save_to_db(message_id); +``` + +Because of API updates `Sticker` type was refactored again. +You may need to change code accordingly. +See `Sticker` documentation for more information about the new structure. + +### teloxide + +You can now write `Ok(())` instead of `respond(())` at the end of closures provided to RELPs: + +```diff,rust +teloxide::repl(bot, |bot: Bot, msg: Message| async move { + bot.send_dice(msg.chat.id).await?; +- respond(()) ++ Ok(()) +}) +.await; +``` + +This is because REPLs now require the closure to return `RequestError` instead of a generic error type, so type inference works perfectly for a return value. If you use something other than `RequestError`, you can transfer your code to `teloxide::dispatching`, which still permits a generic error type. + +### macros + +`parse_with` now accepts a Rust _path_ to a custom parser function instead of a string: + +```diff,rust +fn custom_parser(input: String) -> Result<(u8,), ParseError> { + todo!() +} + +#[derive(BotCommands)] +enum Command { +- #[command(parse_with = "custom_parser")] ++ #[command(parse_with = custom_parser)] + Num(u8), +} +``` + +`rename` now only renames a command literally; use `rename_rule` to change the case of a command: + +```diff,rust +#[derive(BotCommands)] +- #[command(rename = "lowercase", description = "These commands are supported:")] ++ #[command(rename_rule = "lowercase", description = "These commands are supported:")] +enum Command { + // ... +} +``` + ## 0.9 -> 0.10 ### core diff --git a/README.md b/README.md index 8ece9b1b9..2fb0010a5 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -> [v0.9 -> v0.10 migration guide >>](MIGRATION_GUIDE.md#09---010) +> [v0.10 -> v0.11 migration guide >>](MIGRATION_GUIDE.md#010---011)
-

teloxide

+

teloxide

@@ -13,7 +13,7 @@ - + @@ -24,18 +24,20 @@ ## Highlights - - **Declarative design.** teloxide is based upon [`dptree`], a functional [chain of responsibility] pattern that allows you to express pipelines of message processing in a highly declarative and extensible style. + - **Declarative design.** `teloxide` is based upon [`dptree`], a functional [chain of responsibility] pattern that allows you to express pipelines of message processing in a highly declarative and extensible style. [`dptree`]: https://github.com/teloxide/dptree [chain of responsibility]: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern - - **Dialogues management subsystem.** Our dialogues management subsystem is simple and easy-to-use, and, furthermore, is agnostic of how/where dialogues are stored. For example, you can just replace a one line to achieve [persistence]. Out-of-the-box storages include [Redis] and [Sqlite]. + - **Feature-rich.** You can use both long polling and webhooks, configure an underlying HTTPS client, set a custom URL of a Telegram API server, and much more. + + - **Simple dialogues.** Our dialogues subsystem is simple and easy-to-use, and, furthermore, is agnostic of how/where dialogues are stored. For example, you can just replace a one line to achieve [persistence]. Out-of-the-box storages include [Redis] and [Sqlite]. [persistence]: https://en.wikipedia.org/wiki/Persistence_(computer_science) [Redis]: https://redis.io/ [Sqlite]: https://www.sqlite.org - - **Strongly typed commands.** You can describe bot commands as enumerations, and then they'll be automatically constructed from strings — just like JSON structures in [`serde-json`] and command-line arguments in [`structopt`]. + - **Strongly typed commands.** Define bot commands as an `enum` and teloxide will parse them automatically — just like JSON structures in [`serde-json`] and command-line arguments in [`structopt`]. [`structopt`]: https://github.com/TeXitoi/structopt [`serde-json`]: https://github.com/serde-rs/json @@ -54,9 +56,9 @@ $ set TELOXIDE_TOKEN= # Windows PowerShell $ $env:TELOXIDE_TOKEN= - ``` - 4. Make sure that your Rust compiler is up to date (teloxide currently requires rustc at least version 1.58): + + 4. Make sure that your Rust compiler is up to date (`teloxide` currently requires rustc at least version 1.64): ```bash # If you're using stable $ rustup update stable @@ -70,7 +72,7 @@ $ rustup override set nightly 5. Run `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`: ```toml [dependencies] -teloxide = { version = "0.10", features = ["macros", "auto-send"] } +teloxide = { version = "0.11", features = ["macros", "auto-send"] } log = "0.4" pretty_env_logger = "0.4" tokio = { version = "1.8", features = ["rt-multi-thread", "macros"] } @@ -92,11 +94,11 @@ async fn main() { pretty_env_logger::init(); log::info!("Starting throw dice bot..."); - let bot = Bot::from_env().auto_send(); + let bot = Bot::from_env(); - teloxide::repl(bot, |message: Message, bot: AutoSend| async move { - bot.send_dice(message.chat.id).await?; - respond(()) + teloxide::repl(bot, |bot: Bot, msg: Message| async move { + bot.send_dice(msg.chat.id).await?; + Ok(()) }) .await; } @@ -122,20 +124,18 @@ Commands are strongly typed and defined declaratively, similar to how we define ```rust,no_run use teloxide::{prelude::*, utils::command::BotCommands}; -use std::error::Error; - #[tokio::main] async fn main() { pretty_env_logger::init(); log::info!("Starting command bot..."); - let bot = Bot::from_env().auto_send(); + let bot = Bot::from_env(); teloxide::commands_repl(bot, answer, Command::ty()).await; } #[derive(BotCommands, Clone)] -#[command(rename = "lowercase", description = "These commands are supported:")] +#[command(rename_rule = "lowercase", description = "These commands are supported:")] enum Command { #[command(description = "display this text.")] Help, @@ -145,24 +145,15 @@ enum Command { UsernameAndAge { username: String, age: u8 }, } -async fn answer( - bot: AutoSend, - message: Message, - command: Command, -) -> Result<(), Box> { - match command { - Command::Help => { - bot.send_message(message.chat.id, Command::descriptions().to_string()).await? - } +async fn answer(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> { + match cmd { + Command::Help => bot.send_message(msg.chat.id, Command::descriptions().to_string()).await?, Command::Username(username) => { - bot.send_message(message.chat.id, format!("Your username is @{username}.")).await? + bot.send_message(msg.chat.id, format!("Your username is @{username}.")).await? } Command::UsernameAndAge { username, age } => { - bot.send_message( - message.chat.id, - format!("Your username is @{username} and age is {age}."), - ) - .await? + bot.send_message(msg.chat.id, format!("Your username is @{username} and age is {age}.")) + .await? } }; @@ -190,18 +181,18 @@ use teloxide::{dispatching::dialogue::InMemStorage, prelude::*}; type MyDialogue = Dialogue>; type HandlerResult = Result<(), Box>; -#[derive(Clone)] +#[derive(Clone, Default)] pub enum State { + #[default] Start, ReceiveFullName, - ReceiveAge { full_name: String }, - ReceiveLocation { full_name: String, age: u8 }, -} - -impl Default for State { - fn default() -> Self { - Self::Start - } + ReceiveAge { + full_name: String, + }, + ReceiveLocation { + full_name: String, + age: u8, + }, } #[tokio::main] @@ -209,7 +200,7 @@ async fn main() { pretty_env_logger::init(); log::info!("Starting dialogue bot..."); - let bot = Bot::from_env().auto_send(); + let bot = Bot::from_env(); Dispatcher::builder( bot, @@ -229,17 +220,13 @@ async fn main() { .await; } -async fn start(bot: AutoSend, msg: Message, dialogue: MyDialogue) -> HandlerResult { +async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult { bot.send_message(msg.chat.id, "Let's start! What's your full name?").await?; dialogue.update(State::ReceiveFullName).await?; Ok(()) } -async fn receive_full_name( - bot: AutoSend, - msg: Message, - dialogue: MyDialogue, -) -> HandlerResult { +async fn receive_full_name(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult { match msg.text() { Some(text) => { bot.send_message(msg.chat.id, "How old are you?").await?; @@ -254,10 +241,10 @@ async fn receive_full_name( } async fn receive_age( - bot: AutoSend, - msg: Message, + bot: Bot, dialogue: MyDialogue, full_name: String, // Available from `State::ReceiveAge`. + msg: Message, ) -> HandlerResult { match msg.text().map(|text| text.parse::()) { Some(Ok(age)) => { @@ -273,15 +260,15 @@ async fn receive_age( } async fn receive_location( - bot: AutoSend, - msg: Message, + bot: Bot, dialogue: MyDialogue, (full_name, age): (String, u8), // Available from `State::ReceiveLocation`. + msg: Message, ) -> HandlerResult { match msg.text() { Some(location) => { - let message = format!("Full name: {full_name}\nAge: {age}\nLocation: {location}"); - bot.send_message(msg.chat.id, message).await?; + let report = format!("Full name: {full_name}\nAge: {age}\nLocation: {location}"); + bot.send_message(msg.chat.id, report).await?; dialogue.exit().await?; } None => { @@ -319,7 +306,7 @@ A: No, only the bots API. **Q: Can I use webhooks?** -A: You can! Teloxide has a built-in support for webhooks in `dispatching::update_listeners::webhooks` module. See how it's used in [`examples/ngrok_ping_pong_bot`](examples/ngrok_ping_pong.rs) and [`examples/heroku_ping_pong_bot`](examples/heroku_ping_pong.rs). +A: You can! `teloxide` has a built-in support for webhooks in `dispatching::update_listeners::webhooks` module. See how it's used in [`examples/ngrok_ping_pong_bot`](examples/ngrok_ping_pong.rs) and [`examples/heroku_ping_pong_bot`](examples/heroku_ping_pong.rs). **Q: Can I handle both callback queries and messages within a single dialogue?** @@ -343,7 +330,7 @@ Feel free to propose your own bot to our collection! - [`zamazan4ik/npaperbot-telegram`](https://github.com/zamazan4ik/npaperbot-telegram) — Telegram bot for searching via C++ proposals.
-Show bots using teloxide older than v0.6.0 +Show bots using `teloxide` older than v0.6.0 - [`mxseev/logram`](https://github.com/mxseev/logram) — Utility that takes logs from anywhere and sends them to Telegram. - [`alexkonovalov/PedigreeBot`](https://github.com/alexkonovalov/PedigreeBot) — A Telegram bot for building family trees. @@ -355,7 +342,7 @@ Feel free to propose your own bot to our collection!
-See [600+ other public repositories using teloxide >>](https://github.com/teloxide/teloxide/network/dependents) +See [700+ other public repositories using `teloxide` >>](https://github.com/teloxide/teloxide/network/dependents) ## Contributing diff --git a/examples/admin.rs b/examples/admin.rs index 298d62b52..4be9a5430 100644 --- a/examples/admin.rs +++ b/examples/admin.rs @@ -1,4 +1,4 @@ -use std::{error::Error, str::FromStr}; +use std::str::FromStr; use chrono::Duration; use teloxide::{prelude::*, types::ChatPermissions, utils::command::BotCommands}; @@ -14,7 +14,7 @@ use teloxide::{prelude::*, types::ChatPermissions, utils::command::BotCommands}; // %PREFIX%%COMMAND% - %DESCRIPTION% #[derive(BotCommands, Clone)] #[command( - rename = "lowercase", + rename_rule = "lowercase", description = "Use commands in format /%command% %num% %unit%", parse_with = "split" )] @@ -58,19 +58,13 @@ async fn main() { pretty_env_logger::init(); log::info!("Starting admin bot..."); - let bot = teloxide::Bot::from_env().auto_send(); + let bot = teloxide::Bot::from_env(); teloxide::commands_repl(bot, action, Command::ty()).await; } -type Bot = AutoSend; - -async fn action( - bot: Bot, - msg: Message, - command: Command, -) -> Result<(), Box> { - match command { +async fn action(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> { + match cmd { Command::Help => { bot.send_message(msg.chat.id, Command::descriptions().to_string()).await?; } @@ -83,7 +77,7 @@ async fn action( } // Kick a user with a replied message. -async fn kick_user(bot: Bot, msg: Message) -> Result<(), Box> { +async fn kick_user(bot: Bot, msg: Message) -> ResponseResult<()> { match msg.reply_to_message() { Some(replied) => { // bot.unban_chat_member can also kicks a user from a group chat. @@ -97,11 +91,7 @@ async fn kick_user(bot: Bot, msg: Message) -> Result<(), Box Result<(), Box> { +async fn ban_user(bot: Bot, msg: Message, time: Duration) -> ResponseResult<()> { match msg.reply_to_message() { Some(replied) => { bot.kick_chat_member( @@ -120,11 +110,7 @@ async fn ban_user( } // Mute a user with a replied message. -async fn mute_user( - bot: Bot, - msg: Message, - time: Duration, -) -> Result<(), Box> { +async fn mute_user(bot: Bot, msg: Message, time: Duration) -> ResponseResult<()> { match msg.reply_to_message() { Some(replied) => { bot.restrict_chat_member( diff --git a/examples/buttons.rs b/examples/buttons.rs index b510e24ac..ffb9846b0 100644 --- a/examples/buttons.rs +++ b/examples/buttons.rs @@ -4,13 +4,13 @@ use teloxide::{ prelude::*, types::{ InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultArticle, InputMessageContent, - InputMessageContentText, + InputMessageContentText, Me, }, utils::command::BotCommands, }; #[derive(BotCommands)] -#[command(rename = "lowercase", description = "These commands are supported:")] +#[command(rename_rule = "lowercase", description = "These commands are supported:")] enum Command { #[command(description = "Display this text")] Help, @@ -23,7 +23,7 @@ async fn main() -> Result<(), Box> { pretty_env_logger::init(); log::info!("Starting buttons bot..."); - let bot = Bot::from_env().auto_send(); + let bot = Bot::from_env(); let handler = dptree::entry() .branch(Update::filter_message().endpoint(message_handler)) @@ -59,23 +59,24 @@ fn make_keyboard() -> InlineKeyboardMarkup { /// or not, then match the command. If the command is `/start` it writes a /// markup with the `InlineKeyboardMarkup`. async fn message_handler( - m: Message, - bot: AutoSend, + bot: Bot, + msg: Message, + me: Me, ) -> Result<(), Box> { - if let Some(text) = m.text() { - match BotCommands::parse(text, "buttons") { + if let Some(text) = msg.text() { + match BotCommands::parse(text, me.username()) { Ok(Command::Help) => { // Just send the description of all commands. - bot.send_message(m.chat.id, Command::descriptions().to_string()).await?; + bot.send_message(msg.chat.id, Command::descriptions().to_string()).await?; } Ok(Command::Start) => { // Create a list of buttons and send them. let keyboard = make_keyboard(); - bot.send_message(m.chat.id, "Debian versions:").reply_markup(keyboard).await?; + bot.send_message(msg.chat.id, "Debian versions:").reply_markup(keyboard).await?; } Err(_) => { - bot.send_message(m.chat.id, "Command not found!").await?; + bot.send_message(msg.chat.id, "Command not found!").await?; } } } @@ -84,8 +85,8 @@ async fn message_handler( } async fn inline_query_handler( + bot: Bot, q: InlineQuery, - bot: AutoSend, ) -> Result<(), Box> { let choose_debian_version = InlineQueryResultArticle::new( "0", @@ -104,22 +105,21 @@ async fn inline_query_handler( /// /// **IMPORTANT**: do not send privacy-sensitive data this way!!! /// Anyone can read data stored in the callback button. -async fn callback_handler( - q: CallbackQuery, - bot: AutoSend, -) -> Result<(), Box> { +async fn callback_handler(bot: Bot, q: CallbackQuery) -> Result<(), Box> { if let Some(version) = q.data { let text = format!("You chose: {version}"); - match q.message { - Some(Message { id, chat, .. }) => { - bot.edit_message_text(chat.id, id, text).await?; - } - None => { - if let Some(id) = q.inline_message_id { - bot.edit_message_text_inline(id, text).await?; - } - } + // Tell telegram that we've seen this query, to remove 🕑 icons from the + // + // clients. You could also use `answer_callback_query`'s optional + // parameters to tweak what happens on the client side. + bot.answer_callback_query(q.id).await?; + + // Edit text of the message to which the buttons were attached + if let Some(Message { id, chat, .. }) = q.message { + bot.edit_message_text(chat.id, id, text).await?; + } else if let Some(id) = q.inline_message_id { + bot.edit_message_text_inline(id, text).await?; } log::info!("You chose: {}", version); diff --git a/examples/command.rs b/examples/command.rs index 5b30240a6..00f443154 100644 --- a/examples/command.rs +++ b/examples/command.rs @@ -1,19 +1,17 @@ use teloxide::{prelude::*, utils::command::BotCommands}; -use std::error::Error; - #[tokio::main] async fn main() { pretty_env_logger::init(); log::info!("Starting command bot..."); - let bot = Bot::from_env().auto_send(); + let bot = Bot::from_env(); teloxide::commands_repl(bot, answer, Command::ty()).await; } #[derive(BotCommands, Clone)] -#[command(rename = "lowercase", description = "These commands are supported:")] +#[command(rename_rule = "lowercase", description = "These commands are supported:")] enum Command { #[command(description = "display this text.")] Help, @@ -23,24 +21,15 @@ enum Command { UsernameAndAge { username: String, age: u8 }, } -async fn answer( - bot: AutoSend, - message: Message, - command: Command, -) -> Result<(), Box> { - match command { - Command::Help => { - bot.send_message(message.chat.id, Command::descriptions().to_string()).await? - } +async fn answer(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> { + match cmd { + Command::Help => bot.send_message(msg.chat.id, Command::descriptions().to_string()).await?, Command::Username(username) => { - bot.send_message(message.chat.id, format!("Your username is @{username}.")).await? + bot.send_message(msg.chat.id, format!("Your username is @{username}.")).await? } Command::UsernameAndAge { username, age } => { - bot.send_message( - message.chat.id, - format!("Your username is @{username} and age is {age}."), - ) - .await? + bot.send_message(msg.chat.id, format!("Your username is @{username} and age is {age}.")) + .await? } }; diff --git a/examples/db_remember.rs b/examples/db_remember.rs index 1aed2808b..de09db0d6 100644 --- a/examples/db_remember.rs +++ b/examples/db_remember.rs @@ -22,7 +22,7 @@ pub enum State { } #[derive(Clone, BotCommands)] -#[command(rename = "lowercase", description = "These commands are supported:")] +#[command(rename_rule = "lowercase", description = "These commands are supported:")] pub enum Command { #[command(description = "get your number.")] Get, @@ -35,7 +35,7 @@ async fn main() { pretty_env_logger::init(); log::info!("Starting DB remember bot..."); - let bot = Bot::from_env().auto_send(); + let bot = Bot::from_env(); let storage: MyStorage = if std::env::var("DB_REMEMBER_REDIS").is_ok() { RedisStorage::open("redis://127.0.0.1:6379", Bincode).await.unwrap().erase() @@ -60,7 +60,7 @@ async fn main() { .await; } -async fn start(bot: AutoSend, msg: Message, dialogue: MyDialogue) -> HandlerResult { +async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult { match msg.text().map(|text| text.parse::()) { Some(Ok(n)) => { dialogue.update(State::GotNumber(n)).await?; @@ -79,10 +79,10 @@ async fn start(bot: AutoSend, msg: Message, dialogue: MyDialogue) -> Handle } async fn got_number( - bot: AutoSend, - msg: Message, + bot: Bot, dialogue: MyDialogue, - num: i32, + num: i32, // Available from `State::GotNumber`. + msg: Message, cmd: Command, ) -> HandlerResult { match cmd { @@ -97,7 +97,7 @@ async fn got_number( Ok(()) } -async fn invalid_command(bot: AutoSend, msg: Message) -> HandlerResult { +async fn invalid_command(bot: Bot, msg: Message) -> HandlerResult { bot.send_message(msg.chat.id, "Please, send /get or /reset.").await?; Ok(()) } diff --git a/examples/dialogue.rs b/examples/dialogue.rs index 8e30219de..72ea6b2e0 100644 --- a/examples/dialogue.rs +++ b/examples/dialogue.rs @@ -37,7 +37,7 @@ async fn main() { pretty_env_logger::init(); log::info!("Starting dialogue bot..."); - let bot = Bot::from_env().auto_send(); + let bot = Bot::from_env(); Dispatcher::builder( bot, @@ -57,17 +57,13 @@ async fn main() { .await; } -async fn start(bot: AutoSend, msg: Message, dialogue: MyDialogue) -> HandlerResult { +async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult { bot.send_message(msg.chat.id, "Let's start! What's your full name?").await?; dialogue.update(State::ReceiveFullName).await?; Ok(()) } -async fn receive_full_name( - bot: AutoSend, - msg: Message, - dialogue: MyDialogue, -) -> HandlerResult { +async fn receive_full_name(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult { match msg.text() { Some(text) => { bot.send_message(msg.chat.id, "How old are you?").await?; @@ -82,10 +78,10 @@ async fn receive_full_name( } async fn receive_age( - bot: AutoSend, - msg: Message, + bot: Bot, dialogue: MyDialogue, full_name: String, // Available from `State::ReceiveAge`. + msg: Message, ) -> HandlerResult { match msg.text().map(|text| text.parse::()) { Some(Ok(age)) => { @@ -101,15 +97,15 @@ async fn receive_age( } async fn receive_location( - bot: AutoSend, - msg: Message, + bot: Bot, dialogue: MyDialogue, (full_name, age): (String, u8), // Available from `State::ReceiveLocation`. + msg: Message, ) -> HandlerResult { match msg.text() { Some(location) => { - let message = format!("Full name: {full_name}\nAge: {age}\nLocation: {location}"); - bot.send_message(msg.chat.id, message).await?; + let report = format!("Full name: {full_name}\nAge: {age}\nLocation: {location}"); + bot.send_message(msg.chat.id, report).await?; dialogue.exit().await?; } None => { diff --git a/examples/dispatching_features.rs b/examples/dispatching_features.rs index dcaf4fcc5..983f00027 100644 --- a/examples/dispatching_features.rs +++ b/examples/dispatching_features.rs @@ -14,7 +14,7 @@ async fn main() { pretty_env_logger::init(); log::info!("Starting dispatching features bot..."); - let bot = Bot::from_env().auto_send(); + let bot = Bot::from_env(); let parameters = ConfigParameters { bot_maintainer: UserId(0), // Paste your ID to run this bot. @@ -33,29 +33,27 @@ async fn main() { ) .branch( // Filter a maintainer by a used ID. - dptree::filter(|msg: Message, cfg: ConfigParameters| { + dptree::filter(|cfg: ConfigParameters, msg: Message| { msg.from().map(|user| user.id == cfg.bot_maintainer).unwrap_or_default() }) .filter_command::() - .endpoint( - |msg: Message, bot: AutoSend, cmd: MaintainerCommands| async move { - match cmd { - MaintainerCommands::Rand { from, to } => { - let mut rng = rand::rngs::OsRng::default(); - let value: u64 = rng.gen_range(from..=to); + .endpoint(|msg: Message, bot: Bot, cmd: MaintainerCommands| async move { + match cmd { + MaintainerCommands::Rand { from, to } => { + let mut rng = rand::rngs::OsRng::default(); + let value: u64 = rng.gen_range(from..=to); - bot.send_message(msg.chat.id, value.to_string()).await?; - Ok(()) - } + bot.send_message(msg.chat.id, value.to_string()).await?; + Ok(()) } - }, - ), + } + }), ) .branch( // Filtering allow you to filter updates by some condition. dptree::filter(|msg: Message| msg.chat.is_group() || msg.chat.is_supergroup()) // An endpoint is the last update handler. - .endpoint(|msg: Message, bot: AutoSend| async move { + .endpoint(|msg: Message, bot: Bot| async move { log::info!("Received a message from a group chat."); bot.send_message(msg.chat.id, "This is a group chat.").await?; respond(()) @@ -64,14 +62,12 @@ async fn main() { .branch( // There are some extension filtering functions on `Message`. The following filter will // filter only messages with dices. - Message::filter_dice().endpoint( - |msg: Message, dice: Dice, bot: AutoSend| async move { - bot.send_message(msg.chat.id, format!("Dice value: {}", dice.value)) - .reply_to_message_id(msg.id) - .await?; - Ok(()) - }, - ), + Message::filter_dice().endpoint(|bot: Bot, msg: Message, dice: Dice| async move { + bot.send_message(msg.chat.id, format!("Dice value: {}", dice.value)) + .reply_to_message_id(msg.id) + .await?; + Ok(()) + }), ); Dispatcher::builder(bot, handler) @@ -100,7 +96,7 @@ struct ConfigParameters { } #[derive(BotCommands, Clone)] -#[command(rename = "lowercase", description = "Simple commands")] +#[command(rename_rule = "lowercase", description = "Simple commands")] enum SimpleCommand { #[command(description = "shows this message.")] Help, @@ -111,18 +107,18 @@ enum SimpleCommand { } #[derive(BotCommands, Clone)] -#[command(rename = "lowercase", description = "Maintainer commands")] +#[command(rename_rule = "lowercase", description = "Maintainer commands")] enum MaintainerCommands { #[command(parse_with = "split", description = "generate a number within range")] Rand { from: u64, to: u64 }, } async fn simple_commands_handler( - msg: Message, - bot: AutoSend, - cmd: SimpleCommand, cfg: ConfigParameters, + bot: Bot, me: teloxide::types::Me, + msg: Message, + cmd: SimpleCommand, ) -> Result<(), teloxide::RequestError> { let text = match cmd { SimpleCommand::Help => { diff --git a/examples/heroku_ping_pong.rs b/examples/heroku_ping_pong.rs index 8238dd9ae..6fe508473 100644 --- a/examples/heroku_ping_pong.rs +++ b/examples/heroku_ping_pong.rs @@ -21,15 +21,13 @@ use std::env; use teloxide::{dispatching::update_listeners::webhooks, prelude::*}; -use url::Url; #[tokio::main] async fn main() { pretty_env_logger::init(); log::info!("Starting Heroku ping-pong bot..."); - let bot = Bot::from_env().auto_send(); - let token = bot.inner().token(); + let bot = Bot::from_env(); // Heroku auto defines a port value let port: u16 = env::var("PORT") @@ -41,7 +39,7 @@ async fn main() { // Heroku host example: "heroku-ping-pong-bot.herokuapp.com" let host = env::var("HOST").expect("HOST env variable is not set"); - let url = Url::parse(&format!("https://{host}/webhooks/{token}")).unwrap(); + let url = format!("https://{host}/webhook").parse().unwrap(); let listener = webhooks::axum(bot.clone(), webhooks::Options::new(addr, url)) .await @@ -49,9 +47,9 @@ async fn main() { teloxide::repl_with_listener( bot, - |msg: Message, bot: AutoSend| async move { + |bot: Bot, msg: Message| async move { bot.send_message(msg.chat.id, "pong").await?; - respond(()) + Ok(()) }, listener, ) diff --git a/examples/inline.rs b/examples/inline.rs index 837fa30d2..444e78589 100644 --- a/examples/inline.rs +++ b/examples/inline.rs @@ -11,10 +11,10 @@ async fn main() { pretty_env_logger::init(); log::info!("Starting inline bot..."); - let bot = Bot::from_env().auto_send(); + let bot = Bot::from_env(); let handler = Update::filter_inline_query().branch(dptree::endpoint( - |query: InlineQuery, bot: AutoSend| async move { + |bot: Bot, q: InlineQuery| async move { // First, create your actual response let google_search = InlineQueryResultArticle::new( // Each item needs a unique ID, as well as the response container for the @@ -26,7 +26,7 @@ async fn main() { // What message will be sent when clicked/tapped InputMessageContent::Text(InputMessageContentText::new(format!( "https://www.google.com/search?q={}", - query.query, + q.query, ))), ); // While constructing them from the struct itself is possible, it is preferred @@ -38,7 +38,7 @@ async fn main() { "DuckDuckGo Search".to_string(), InputMessageContent::Text(InputMessageContentText::new(format!( "https://duckduckgo.com/?q={}", - query.query + q.query ))), ) .description("DuckDuckGo Search") @@ -52,7 +52,7 @@ async fn main() { // Send it off! One thing to note -- the ID we use here must be of the query // we're responding to. - let response = bot.answer_inline_query(&query.id, results).send().await; + let response = bot.answer_inline_query(&q.id, results).send().await; if let Err(err) = response { log::error!("Error in handler: {:?}", err); } diff --git a/examples/ngrok_ping_pong.rs b/examples/ngrok_ping_pong.rs index f44086c6f..3ea157ed0 100644 --- a/examples/ngrok_ping_pong.rs +++ b/examples/ngrok_ping_pong.rs @@ -8,7 +8,7 @@ async fn main() { pretty_env_logger::init(); log::info!("Starting ngrok ping-pong bot..."); - let bot = Bot::from_env().auto_send(); + let bot = Bot::from_env(); let addr = ([127, 0, 0, 1], 8443).into(); let url = "Your HTTPS ngrok URL here. Get it by `ngrok http 8443`".parse().unwrap(); @@ -18,9 +18,9 @@ async fn main() { teloxide::repl_with_listener( bot, - |msg: Message, bot: AutoSend| async move { + |bot: Bot, msg: Message| async move { bot.send_message(msg.chat.id, "pong").await?; - respond(()) + Ok(()) }, listener, ) diff --git a/examples/purchase.rs b/examples/purchase.rs index 0eac1ad34..daf27cddc 100644 --- a/examples/purchase.rs +++ b/examples/purchase.rs @@ -33,7 +33,7 @@ pub enum State { } #[derive(BotCommands, Clone)] -#[command(rename = "lowercase", description = "These commands are supported:")] +#[command(rename_rule = "lowercase", description = "These commands are supported:")] enum Command { #[command(description = "display this text.")] Help, @@ -48,7 +48,7 @@ async fn main() { pretty_env_logger::init(); log::info!("Starting purchase bot..."); - let bot = Bot::from_env().auto_send(); + let bot = Bot::from_env(); Dispatcher::builder(bot, schema()) .dependencies(dptree::deps![InMemStorage::::new()]) @@ -83,34 +83,30 @@ fn schema() -> UpdateHandler> .branch(callback_query_handler) } -async fn start(bot: AutoSend, msg: Message, dialogue: MyDialogue) -> HandlerResult { +async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult { bot.send_message(msg.chat.id, "Let's start! What's your full name?").await?; dialogue.update(State::ReceiveFullName).await?; Ok(()) } -async fn help(bot: AutoSend, msg: Message) -> HandlerResult { +async fn help(bot: Bot, msg: Message) -> HandlerResult { bot.send_message(msg.chat.id, Command::descriptions().to_string()).await?; Ok(()) } -async fn cancel(bot: AutoSend, msg: Message, dialogue: MyDialogue) -> HandlerResult { +async fn cancel(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult { bot.send_message(msg.chat.id, "Cancelling the dialogue.").await?; dialogue.exit().await?; Ok(()) } -async fn invalid_state(bot: AutoSend, msg: Message) -> HandlerResult { +async fn invalid_state(bot: Bot, msg: Message) -> HandlerResult { bot.send_message(msg.chat.id, "Unable to handle the message. Type /help to see the usage.") .await?; Ok(()) } -async fn receive_full_name( - bot: AutoSend, - msg: Message, - dialogue: MyDialogue, -) -> HandlerResult { +async fn receive_full_name(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult { match msg.text().map(ToOwned::to_owned) { Some(full_name) => { let products = ["Apple", "Banana", "Orange", "Potato"] @@ -130,10 +126,10 @@ async fn receive_full_name( } async fn receive_product_selection( - bot: AutoSend, - q: CallbackQuery, + bot: Bot, dialogue: MyDialogue, - full_name: String, + full_name: String, // Available from `State::ReceiveProductChoice`. + q: CallbackQuery, ) -> HandlerResult { if let Some(product) = &q.data { bot.send_message( diff --git a/examples/shared_state.rs b/examples/shared_state.rs index 21a0fcc0b..bb8ef8faa 100644 --- a/examples/shared_state.rs +++ b/examples/shared_state.rs @@ -12,11 +12,11 @@ async fn main() { pretty_env_logger::init(); log::info!("Starting shared state bot..."); - let bot = Bot::from_env().auto_send(); + let bot = Bot::from_env(); let messages_total = Arc::new(AtomicU64::new(0)); let handler = Update::filter_message().endpoint( - |msg: Message, bot: AutoSend, messages_total: Arc| async move { + |bot: Bot, messages_total: Arc, msg: Message| async move { let previous = messages_total.fetch_add(1, Ordering::Relaxed); bot.send_message(msg.chat.id, format!("I received {previous} messages in total.")) .await?; diff --git a/examples/throw_dice.rs b/examples/throw_dice.rs index 758778b18..71f850a4b 100644 --- a/examples/throw_dice.rs +++ b/examples/throw_dice.rs @@ -7,11 +7,11 @@ async fn main() { pretty_env_logger::init(); log::info!("Starting throw dice bot..."); - let bot = Bot::from_env().auto_send(); + let bot = Bot::from_env(); - teloxide::repl(bot, |message: Message, bot: AutoSend| async move { - bot.send_dice(message.chat.id).await?; - respond(()) + teloxide::repl(bot, |bot: Bot, msg: Message| async move { + bot.send_dice(msg.chat.id).await?; + Ok(()) }) .await; } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index ea79a4343..eca557709 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly-2022-07-01" +channel = "nightly-2022-09-01" components = ["rustfmt", "clippy"] profile = "minimal" diff --git a/src/dispatching.rs b/src/dispatching.rs index e0440705b..e65eca20d 100644 --- a/src/dispatching.rs +++ b/src/dispatching.rs @@ -25,7 +25,7 @@ //! ```no_run //! # use teloxide::utils::command::BotCommands; //! #[derive(BotCommands, Clone)] -//! #[command(rename = "lowercase", description = "These commands are supported:")] +//! #[command(rename_rule = "lowercase", description = "These commands are supported:")] //! enum Command { //! #[command(description = "display this text.")] //! Help, @@ -102,10 +102,10 @@ //! -- no problem, reuse [`dptree::Handler::filter`], [`dptree::case!`], and //! other combinators in the same way! //! -//! Finally, we define our endpoints like this: +//! Finally, we define our endpoints: //! //! ```no_run -//! # use teloxide::{Bot, adaptors::AutoSend}; +//! # use teloxide::Bot; //! # use teloxide::types::{Message, CallbackQuery}; //! # use teloxide::dispatching::dialogue::{InMemStorage, Dialogue}; //! # enum State{} @@ -113,47 +113,38 @@ //! type MyDialogue = Dialogue>; //! type HandlerResult = Result<(), Box>; //! -//! async fn start(bot: AutoSend, msg: Message, dialogue: MyDialogue) -> HandlerResult { +//! async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult { //! todo!() //! } -//! -//! async fn help(bot: AutoSend, msg: Message) -> HandlerResult { +//! async fn help(bot: Bot, msg: Message) -> HandlerResult { //! todo!() //! } -//! -//! async fn cancel(bot: AutoSend, msg: Message, dialogue: MyDialogue) -> HandlerResult { +//! async fn cancel(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult { //! todo!() //! } -//! -//! async fn invalid_state(bot: AutoSend, msg: Message) -> HandlerResult { +//! async fn invalid_state(bot: Bot, msg: Message) -> HandlerResult { //! todo!() //! } -//! -//! async fn receive_full_name( -//! bot: AutoSend, -//! msg: Message, -//! dialogue: MyDialogue, -//! ) -> HandlerResult { +//! async fn receive_full_name(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult { //! todo!() //! } -//! //! async fn receive_product_selection( -//! bot: AutoSend, -//! q: CallbackQuery, +//! bot: Bot, //! dialogue: MyDialogue, -//! full_name: String, +//! full_name: String, // Available from `State::ReceiveProductChoice`. +//! q: CallbackQuery, //! ) -> HandlerResult { //! todo!() //! } //! ``` //! -//! Each parameter is supplied as a dependency by teloxide. In particular: -//! - `bot: AutoSend` comes from the dispatcher (see below); -//! - `msg: Message` comes from [`Update::filter_message`]; -//! - `q: CallbackQuery` comes from [`Update::filter_callback_query`]; -//! - `dialogue: MyDialogue` comes from [`dialogue::enter`]; +//! Each parameter is supplied as a dependency by `teloxide`. In particular: +//! - `bot: Bot` comes from the dispatcher (see below) +//! - `msg: Message` comes from [`Update::filter_message`] +//! - `q: CallbackQuery` comes from [`Update::filter_callback_query`] +//! - `dialogue: MyDialogue` comes from [`dialogue::enter`] //! - `full_name: String` comes from `dptree::case![State::ReceiveProductChoice -//! { full_name }]`. +//! { full_name }]` //! //! Inside `main`, we plug the schema into [`Dispatcher`] like this: //! @@ -165,7 +156,7 @@ //! # fn schema() -> teloxide::dispatching::UpdateHandler> { teloxide::dptree::entry() } //! #[tokio::main] //! async fn main() { -//! let bot = Bot::from_env().auto_send(); +//! let bot = Bot::from_env(); //! //! Dispatcher::builder(bot, schema()) //! .dependencies(dptree::deps![InMemStorage::::new()]) @@ -187,12 +178,36 @@ //! useful features. See [`examples/dispatching_features.rs`] as a more involved //! example. //! +//! ## Dispatching or REPLs? +//! +//! The difference between dispatching and the REPLs ([`crate::repl`] & co) is +//! that dispatching gives you a greater degree of flexibility at the cost of a +//! bit more complicated setup. +//! +//! Here are things that dispatching can do, but REPLs can't: +//! - Handle different kinds of [`Update`] +//! - [Pass dependencies] to handlers +//! - Disable a [default Ctrl-C handling] +//! - Control your [default] and [error] handlers +//! - Use [dialogues] +//! - Use [`dptree`]-related functionality +//! - Probably more +//! +//! Thus, REPLs are good for simple bots and rapid prototyping, but for more +//! involved scenarios, we recommend using dispatching over REPLs. +//! +//! [Pass dependencies]: DispatcherBuilder#method.dependencies +//! [default Ctrl-C handling]: DispatcherBuilder#method.enable_ctrlc_handler +//! [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 //! [`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 +//! [`Update`]: crate::types::Update #[cfg(all(feature = "ctrlc_handler"))] pub mod repls; @@ -203,8 +218,6 @@ mod distribution; mod filter_ext; mod handler_description; mod handler_ext; -mod handler_factory; -pub mod stop_token; pub mod update_listeners; pub use crate::utils::shutdown_token::{IdleShutdownError, ShutdownToken}; @@ -213,5 +226,3 @@ pub use distribution::DefaultKey; pub use filter_ext::{MessageFilterExt, UpdateFilterExt}; pub use handler_description::DpHandlerDescription; pub use handler_ext::{filter_command, HandlerExt}; -#[allow(deprecated)] -pub use handler_factory::HandlerFactory; diff --git a/src/dispatching/dialogue.rs b/src/dispatching/dialogue.rs index 3af1ecc0a..89d163265 100644 --- a/src/dispatching/dialogue.rs +++ b/src/dispatching/dialogue.rs @@ -13,26 +13,35 @@ //! [`examples/dialogue.rs`] clearly demonstrates the typical usage of //! dialogues. Your dialogue state can be represented as an enumeration: //! -//! ```ignore +//! ```no_run //! #[derive(Clone, Default)] //! pub enum State { //! #[default] //! Start, //! ReceiveFullName, -//! ReceiveAge { full_name: String }, -//! ReceiveLocation { full_name: String, age: u8 }, +//! ReceiveAge { +//! full_name: String, +//! }, +//! ReceiveLocation { +//! full_name: String, +//! age: u8, +//! }, //! } //! ``` //! //! Each state is associated with its respective handler: e.g., when a dialogue //! state is `ReceiveAge`, `receive_age` is invoked: //! -//! ```ignore +//! ```no_run +//! # use teloxide::{dispatching::dialogue::InMemStorage, prelude::*}; +//! # type MyDialogue = Dialogue>; +//! # type HandlerResult = Result<(), Box>; +//! # #[derive(Clone, Debug)] enum State { ReceiveLocation { full_name: String, age: u8 } } //! async fn receive_age( -//! bot: AutoSend, -//! msg: Message, +//! bot: Bot, //! dialogue: MyDialogue, //! full_name: String, // Available from `State::ReceiveAge`. +//! msg: Message, //! ) -> HandlerResult { //! match msg.text().map(|text| text.parse::()) { //! Some(Ok(age)) => { @@ -55,13 +64,17 @@ //! the dialogue, just call [`Dialogue::exit`] and it will be removed from the //! underlying storage: //! -//! ```ignore +//! ```no_run +//! # use teloxide::{dispatching::dialogue::InMemStorage, prelude::*}; +//! # type MyDialogue = Dialogue>; +//! # type HandlerResult = Result<(), Box>; +//! # #[derive(Clone, Debug)] enum State {} //! async fn receive_location( -//! bot: AutoSend, -//! msg: Message, +//! bot: Bot, //! dialogue: MyDialogue, //! (full_name, age): (String, u8), // Available from `State::ReceiveLocation`. -//! ) -> anyhow::Result<()> { +//! msg: Message, +//! ) -> HandlerResult { //! match msg.text() { //! Some(location) => { //! let message = @@ -198,6 +211,7 @@ where /// - `Upd` /// /// [`HandlerExt::enter_dialogue`]: super::HandlerExt::enter_dialogue +#[must_use] pub fn enter() -> Handler<'static, DependencyMap, Output, DpHandlerDescription> where S: Storage + ?Sized + Send + Sync + 'static, @@ -206,18 +220,17 @@ where Upd: GetChatId + Clone + Send + Sync + 'static, Output: Send + Sync + 'static, { - dptree::entry() - .chain(dptree::filter_map(|storage: Arc, upd: Upd| { - let chat_id = upd.chat_id()?; - Some(Dialogue::new(storage, chat_id)) - })) - .chain(dptree::filter_map_async(|dialogue: Dialogue| async move { - match dialogue.get_or_default().await { - Ok(dialogue) => Some(dialogue), - Err(err) => { - log::error!("dialogue.get_or_default() failed: {:?}", err); - None - } + dptree::filter_map(|storage: Arc, upd: Upd| { + let chat_id = upd.chat_id()?; + Some(Dialogue::new(storage, chat_id)) + }) + .filter_map_async(|dialogue: Dialogue| async move { + match dialogue.get_or_default().await { + Ok(dialogue) => Some(dialogue), + Err(err) => { + log::error!("dialogue.get_or_default() failed: {:?}", err); + None } - })) + } + }) } diff --git a/src/dispatching/dispatcher.rs b/src/dispatching/dispatcher.rs index 6ff568e66..c44ac97e2 100644 --- a/src/dispatching/dispatcher.rs +++ b/src/dispatching/dispatcher.rs @@ -1,6 +1,6 @@ use crate::{ dispatching::{ - distribution::default_distribution_function, stop_token::StopToken, update_listeners, + distribution::default_distribution_function, update_listeners, update_listeners::UpdateListener, DefaultKey, DpHandlerDescription, ShutdownToken, }, error_handlers::{ErrorHandler, LoggingErrorHandler}, @@ -27,6 +27,9 @@ use std::{ }; /// The builder for [`Dispatcher`]. +/// +/// See also: ["Dispatching or +/// REPLs?"](../dispatching/index.html#dispatching-or-repls) pub struct DispatcherBuilder { bot: R, dependencies: DependencyMap, @@ -171,11 +174,14 @@ where /// The base for update dispatching. /// -/// Updates from different chats are handles concurrently, whereas updates from +/// Updates from different chats are handled concurrently, whereas updates from /// the same chats are handled sequentially. If the dispatcher is unable to /// determine a chat ID of an incoming update, it will be handled concurrently. /// Note that this behaviour can be altered with [`distribution_function`]. /// +/// See also: ["Dispatching or +/// REPLs?"](../dispatching/index.html#dispatching-or-repls) +/// /// [`distribution_function`]: DispatcherBuilder::distribution_function pub struct Dispatcher { bot: R, @@ -281,14 +287,14 @@ where /// This method adds the same dependencies as [`Dispatcher::dispatch`]. /// /// [`shutdown`]: ShutdownToken::shutdown - pub async fn dispatch_with_listener<'a, UListener, ListenerE, Eh>( + pub async fn dispatch_with_listener<'a, UListener, Eh>( &'a mut self, mut update_listener: UListener, update_listener_error_handler: Arc, ) where - UListener: UpdateListener + 'a, - Eh: ErrorHandler + 'a, - ListenerE: Debug, + UListener: UpdateListener + 'a, + Eh: ErrorHandler + 'a, + UListener::Err: Debug, { // FIXME: there should be a way to check if dependency is already inserted let me = self.bot.get_me().send().await.expect("Failed to retrieve 'me'"); diff --git a/src/dispatching/filter_ext.rs b/src/dispatching/filter_ext.rs index 69fb9bb71..029a52459 100644 --- a/src/dispatching/filter_ext.rs +++ b/src/dispatching/filter_ext.rs @@ -56,11 +56,11 @@ mod private { macro_rules! define_message_ext { ($( ($func:ident, $fn_name:path) ,)*) => { define_ext! { - MessageFilterExt, crate::types::Message => + MessageFilterExt, Message => $(( $func, (|x| $fn_name(&x).map(ToOwned::to_owned)), - concat!("Applies the [`crate::types::", stringify!($fn_name), "`] filter.") + concat!("Applies the [`", stringify!($fn_name), "`] filter.") ),)* } } @@ -89,14 +89,14 @@ define_message_ext! { macro_rules! define_update_ext { ($( ($func:ident, $kind:path, $Allowed:ident) ,)*) => { define_ext! { - UpdateFilterExt, crate::types::Update => + UpdateFilterExt, Update => $(( $func, |update: Update| match update.kind { $kind(x) => Some(x), _ => None, }, - concat!("Filters out [`crate::types::", stringify!($kind), "`] objects."), + concat!("Filters out [`", stringify!($kind), "`] objects."), $Allowed ),)* } diff --git a/src/dispatching/handler_description.rs b/src/dispatching/handler_description.rs index 3ed2f6219..92bbda38c 100644 --- a/src/dispatching/handler_description.rs +++ b/src/dispatching/handler_description.rs @@ -86,7 +86,7 @@ mod tests { use crate as teloxide; // fixup for the `BotCommands` macro #[derive(BotCommands, Clone)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum Cmd { B, } diff --git a/src/dispatching/handler_ext.rs b/src/dispatching/handler_ext.rs index 11ebc6bff..0013acb45 100644 --- a/src/dispatching/handler_ext.rs +++ b/src/dispatching/handler_ext.rs @@ -8,9 +8,6 @@ use crate::{ }; use dptree::{di::DependencyMap, Handler}; -#[allow(deprecated)] -use crate::dispatching::HandlerFactory; - use std::fmt::Debug; /// Extension methods for working with `dptree` handlers. @@ -51,13 +48,6 @@ pub trait HandlerExt { >::Error: Debug + Send, D: Default + Send + Sync + 'static, Upd: GetChatId + Clone + Send + Sync + 'static; - - #[must_use] - #[deprecated(note = "Use the teloxide::handler! API")] - #[allow(deprecated)] - fn dispatch_by(self) -> Self - where - F: HandlerFactory; } impl HandlerExt for Handler<'static, DependencyMap, Output, DpHandlerDescription> @@ -80,14 +70,6 @@ where { self.chain(super::dialogue::enter::()) } - - #[allow(deprecated)] - fn dispatch_by(self) -> Self - where - F: HandlerFactory, - { - self.chain(F::handler()) - } } /// Returns a handler that accepts a parsed command `C`. @@ -100,13 +82,14 @@ where /// /// - [`crate::types::Message`] /// - [`crate::types::Me`] +#[must_use] pub fn filter_command() -> Handler<'static, DependencyMap, Output, DpHandlerDescription> where C: BotCommands + Send + Sync + 'static, Output: Send + Sync + 'static, { - dptree::entry().chain(dptree::filter_map(move |message: Message, me: Me| { + dptree::filter_map(move |message: Message, me: Me| { let bot_name = me.user.username.expect("Bots must have a username"); - message.text().and_then(|text| C::parse(text, bot_name).ok()) - })) + message.text().and_then(|text| C::parse(text, &bot_name).ok()) + }) } diff --git a/src/dispatching/handler_factory.rs b/src/dispatching/handler_factory.rs deleted file mode 100644 index 25122f338..000000000 --- a/src/dispatching/handler_factory.rs +++ /dev/null @@ -1,11 +0,0 @@ -use dptree::{di::DependencyMap, Handler}; - -use crate::dispatching::DpHandlerDescription; - -/// Something that can construct a handler. -#[deprecated(note = "Use the teloxide::handler! API")] -pub trait HandlerFactory { - type Out; - - fn handler() -> Handler<'static, DependencyMap, Self::Out, DpHandlerDescription>; -} diff --git a/src/dispatching/repls.rs b/src/dispatching/repls.rs index b0f450a38..71d703430 100644 --- a/src/dispatching/repls.rs +++ b/src/dispatching/repls.rs @@ -1,4 +1,12 @@ -//! REPLs for dispatching updates. +//! [REPL]s for dispatching updates. +//! +//! This module provides utilities for easy update handling. They accept a +//! single "handler" function that processes all updates of a certain kind. Note +//! that REPLs are meant to be used for simple scenarios, such as prototyping, +//! inasmuch they lack configuration and some [advanced features]. +//! +//! [REPL]: https://en.wikipedia.org/wiki/Read-eval-print_loop +//! [advanced features]: crate::dispatching#dispatching-or-repls mod commands_repl; mod repl; diff --git a/src/dispatching/repls/caution.md b/src/dispatching/repls/caution.md new file mode 100644 index 000000000..a5968cb36 --- /dev/null +++ b/src/dispatching/repls/caution.md @@ -0,0 +1,2 @@ +**DO NOT** use this function together with [`Dispatcher`] and other REPLs, +because Telegram disallow multiple requests at the same time from the same bot. diff --git a/src/dispatching/repls/commands_repl.rs b/src/dispatching/repls/commands_repl.rs index 7f7db10fc..fe9d9fb7a 100644 --- a/src/dispatching/repls/commands_repl.rs +++ b/src/dispatching/repls/commands_repl.rs @@ -3,41 +3,68 @@ use crate::{ update_listeners, update_listeners::UpdateListener, HandlerExt, UpdateFilterExt, }, error_handlers::LoggingErrorHandler, + requests::{Requester, ResponseResult}, types::Update, utils::command::BotCommands, }; use dptree::di::{DependencyMap, Injectable}; use std::{fmt::Debug, marker::PhantomData}; -use teloxide_core::requests::Requester; /// A [REPL] for commands. +// /// -/// All errors from an update listener and handler will be logged. +// +#[doc = include_str!("preamble.md")] /// -/// REPLs are meant only for simple bots and rapid prototyping. If you need to -/// supply dependencies or describe more complex dispatch logic, please use -/// [`Dispatcher`]. +/// [REPL]: https://en.wikipedia.org/wiki/Read-eval-print_loop /// -/// ## Caution +/// ## Signature /// -/// **DO NOT** use this function together with [`Dispatcher`] and other REPLs, -/// because Telegram disallow multiple requests at the same time from the same -/// bot. +/// Don't be scared by many trait bounds in the signature, in essence they +/// require: /// -/// ## Dependency requirements +/// 1. `bot` is a bot, client for the Telegram bot API. It is represented via +/// the [`Requester`] trait. +/// 2. `handler` is an `async` function that takes arguments from +/// [`DependencyMap`] (see below) and returns [`ResponseResult`]. +/// 3. `cmd` is a type hint for your command enumeration +/// `MyCommand`: just write `MyCommand::ty()`. Note that `MyCommand` must +/// implement the [`BotCommands`] trait, typically via +/// `#[derive(BotCommands)]`. /// -/// - Those of [`HandlerExt::filter_command`]. +/// All the other requirements are about thread safety and data validity and can +/// be ignored for most of the time. +/// +/// ## Handler arguments +/// +/// `teloxide` provides the following types to the `handler`: +/// - [`Message`] +/// - `R` (type of the `bot`) +/// - `Cmd` (type of the parsed command) +/// - [`Me`] +/// +/// Each of these types can be accepted as a handler parameter. Note that they +/// aren't all required at the same time: e.g., you can take only the bot and +/// the command without [`Me`] and [`Message`]. +/// +/// [`Me`]: crate::types::Me +/// [`Message`]: crate::types::Message +/// +/// ## Stopping +// +#[doc = include_str!("stopping.md")] +/// +/// ## Caution +// +#[doc = include_str!("caution.md")] /// -/// [REPL]: https://en.wikipedia.org/wiki/Read-eval-print_loop -/// [`Dispatcher`]: crate::dispatching::Dispatcher #[cfg(feature = "ctrlc_handler")] -pub async fn commands_repl<'a, R, Cmd, H, E, Args>(bot: R, handler: H, cmd: PhantomData) +pub async fn commands_repl<'a, R, Cmd, H, Args>(bot: R, handler: H, cmd: PhantomData) where - Cmd: BotCommands + Send + Sync + 'static, - H: Injectable, Args> + Send + Sync + 'static, R: Requester + Clone + Send + Sync + 'static, ::GetUpdates: Send, - E: Debug + Send + Sync + 'static, + H: Injectable, Args> + Send + Sync + 'static, + Cmd: BotCommands + Send + Sync + 'static, { let cloned_bot = bot.clone(); @@ -50,57 +77,83 @@ where .await; } -/// Like [`commands_repl`], but with a custom [`UpdateListener`]. +/// A [REPL] for commands, with a custom [`UpdateListener`]. +// +/// +// +#[doc = include_str!("preamble.md")] /// -/// All errors from an update listener and handler will be logged. +/// [REPL]: https://en.wikipedia.org/wiki/Read-eval-print_loop /// -/// REPLs are meant only for simple bots and rapid prototyping. If you need to -/// supply dependencies or describe more complex dispatch logic, please use -/// [`Dispatcher`]. +/// ## Signature /// -/// ## Caution +/// Don't be scared by many trait bounds in the signature, in essence they +/// require: +/// +/// 1. `bot` is a bot, client for the Telegram bot API. It is represented via +/// the [`Requester`] trait. +/// 2. `handler` is an `async` function that takes arguments from +/// [`DependencyMap`] (see below) and returns [`ResponseResult`]. +/// 3. `listener` is something that takes updates from a Telegram server and +/// implements [`UpdateListener`]. +/// 4. `cmd` is a type hint for your command enumeration `MyCommand`: just +/// write `MyCommand::ty()`. Note that `MyCommand` must implement the +/// [`BotCommands`] trait, typically via `#[derive(BotCommands)]`. +/// +/// All the other requirements are about thread safety and data validity and can +/// be ignored for most of the time. /// -/// **DO NOT** use this function together with [`Dispatcher`] and other REPLs, -/// because Telegram disallow multiple requests at the same time from the same -/// bot. +/// ## Handler arguments /// -/// ## Dependency requirements +/// `teloxide` provides the following types to the `handler`: +/// - [`Message`] +/// - `R` (type of the `bot`) +/// - `Cmd` (type of the parsed command) +/// - [`Me`] /// -/// - Those of [`HandlerExt::filter_command`]. +/// Each of these types can be accepted as a handler parameter. Note that they +/// aren't all required at the same time: e.g., you can take only the bot and +/// the command without [`Me`] and [`Message`]. +/// +/// [`Me`]: crate::types::Me +/// [`Message`]: crate::types::Message +/// +/// ## Stopping +// +#[doc = include_str!("stopping.md")] +/// +/// ## Caution +// +#[doc = include_str!("caution.md")] /// -/// [`Dispatcher`]: crate::dispatching::Dispatcher -/// [`commands_repl`]: crate::dispatching::repls::commands_repl() -/// [`UpdateListener`]: crate::dispatching::update_listeners::UpdateListener #[cfg(feature = "ctrlc_handler")] -pub async fn commands_repl_with_listener<'a, R, Cmd, H, L, ListenerE, E, Args>( +pub async fn commands_repl_with_listener<'a, R, Cmd, H, L, Args>( bot: R, handler: H, listener: L, - _cmd: PhantomData, + cmd: PhantomData, ) where Cmd: BotCommands + Send + Sync + 'static, - H: Injectable, Args> + Send + Sync + 'static, - L: UpdateListener + Send + 'a, - ListenerE: Debug + Send + 'a, + H: Injectable, Args> + Send + Sync + 'static, + L: UpdateListener + Send + 'a, + L::Err: Debug + Send + 'a, R: Requester + Clone + Send + Sync + 'static, - E: Debug + Send + Sync + 'static, { use crate::dispatching::Dispatcher; + let _ = cmd; + // Other update types are of no interest to use since this REPL is only for // commands. See . let ignore_update = |_upd| Box::pin(async {}); - Dispatcher::builder( - bot, - Update::filter_message().filter_command::().chain(dptree::endpoint(handler)), - ) - .default_handler(ignore_update) - .enable_ctrlc_handler() - .build() - .dispatch_with_listener( - listener, - LoggingErrorHandler::with_custom_text("An error from the update listener"), - ) - .await; + Dispatcher::builder(bot, Update::filter_message().filter_command::().endpoint(handler)) + .default_handler(ignore_update) + .enable_ctrlc_handler() + .build() + .dispatch_with_listener( + listener, + LoggingErrorHandler::with_custom_text("An error from the update listener"), + ) + .await; } diff --git a/src/dispatching/repls/preamble.md b/src/dispatching/repls/preamble.md new file mode 100644 index 000000000..f434a3256 --- /dev/null +++ b/src/dispatching/repls/preamble.md @@ -0,0 +1,7 @@ +REPLs are meant only for simple bots and rapid prototyping. +If you need to supply dependencies or describe more complex dispatch logic, please use [`Dispatcher`]. +See also: ["Dispatching or REPLs?"](dispatching/index.html#dispatching-or-repls). + +[`Dispatcher`]: crate::dispatching::Dispatcher + +All errors from the handler and update listener will be logged. diff --git a/src/dispatching/repls/repl.rs b/src/dispatching/repls/repl.rs index cecf90ade..b352d8e3b 100644 --- a/src/dispatching/repls/repl.rs +++ b/src/dispatching/repls/repl.rs @@ -1,67 +1,113 @@ use crate::{ dispatching::{update_listeners, update_listeners::UpdateListener, UpdateFilterExt}, - error_handlers::{LoggingErrorHandler, OnError}, + error_handlers::LoggingErrorHandler, + requests::{Requester, ResponseResult}, types::Update, }; use dptree::di::{DependencyMap, Injectable}; use std::fmt::Debug; -use teloxide_core::requests::Requester; /// A [REPL] for messages. +// /// -/// All errors from an update listener and a handler will be logged. +// +#[doc = include_str!("preamble.md")] /// -/// REPLs are meant only for simple bots and rapid prototyping. If you need to -/// supply dependencies or describe more complex dispatch logic, please use -/// [`Dispatcher`]. +/// [REPL]: https://en.wikipedia.org/wiki/Read-eval-print_loop /// -/// ## Caution +/// ## Signature /// -/// **DO NOT** use this function together with [`Dispatcher`] and other REPLs, -/// because Telegram disallow multiple requests at the same time from the same -/// bot. +/// Don't be scared by many trait bounds in the signature, in essence they +/// require: +/// +/// 1. `bot` is a bot, client for the Telegram bot API. It is represented via +/// the [`Requester`] trait. +/// 2. `handler` is an `async` function that takes arguments from +/// [`DependencyMap`] (see below) and returns [`ResponseResult`]. +/// +/// ## Handler arguments +/// +/// `teloxide` provides the following types to the `handler`: +/// - [`Message`] +/// - `R` (type of the `bot`) +/// - [`Me`] +/// +/// Each of these types can be accepted as a handler parameter. Note that they +/// aren't all required at the same time: e.g., you can take only the bot and +/// the message without [`Me`]. +/// +/// [`Me`]: crate::types::Me +/// [`Message`]: crate::types::Message +/// +/// ## Stopping +// +#[doc = include_str!("stopping.md")] +/// +/// ## Caution +// +#[doc = include_str!("caution.md")] /// -/// [REPL]: https://en.wikipedia.org/wiki/Read-eval-print_loop -/// [`Dispatcher`]: crate::dispatching::Dispatcher #[cfg(feature = "ctrlc_handler")] -pub async fn repl(bot: R, handler: H) +pub async fn repl(bot: R, handler: H) where - H: Injectable, Args> + Send + Sync + 'static, - Result<(), E>: OnError, - E: Debug + Send + Sync + 'static, R: Requester + Send + Sync + Clone + 'static, ::GetUpdates: Send, + H: Injectable, Args> + Send + Sync + 'static, { let cloned_bot = bot.clone(); repl_with_listener(bot, handler, update_listeners::polling_default(cloned_bot).await).await; } -/// Like [`repl`], but with a custom [`UpdateListener`]. +/// A [REPL] for messages, with a custom [`UpdateListener`]. +// /// -/// All errors from an update listener and handler will be logged. +// +#[doc = include_str!("preamble.md")] /// -/// REPLs are meant only for simple bots and rapid prototyping. If you need to -/// supply dependencies or describe more complex dispatch logic, please use -/// [`Dispatcher`]. +/// [REPL]: https://en.wikipedia.org/wiki/Read-eval-print_loop +/// [`UpdateListener`]: crate::dispatching::update_listeners::UpdateListener /// -/// # Caution +/// ## Signature /// -/// **DO NOT** use this function together with [`Dispatcher`] and other REPLs, -/// because Telegram disallow multiple requests at the same time from the same -/// bot. +/// Don't be scared by many trait bounds in the signature, in essence they +/// require: +/// +/// 1. `bot` is a bot, client for the Telegram bot API. It is represented via +/// the [`Requester`] trait. +/// 2. `handler` is an `async` function that takes arguments from +/// [`DependencyMap`] (see below) and returns [`ResponseResult`]. +/// 3. `listener` is something that takes updates from a Telegram server and +/// implements [`UpdateListener`]. +/// +/// ## Handler arguments +/// +/// `teloxide` provides the following types to the `handler`: +/// - [`Message`] +/// - `R` (type of the `bot`) +/// - [`Me`] +/// +/// Each of these types can be accepted as a handler parameter. Note that they +/// aren't all required at the same time: e.g., you can take only the bot and +/// the message without [`Me`]. +/// +/// [`Me`]: crate::types::Me +/// [`Message`]: crate::types::Message +/// +/// ## Stopping +// +#[doc = include_str!("stopping.md")] +/// +/// ## Caution +// +#[doc = include_str!("caution.md")] /// -/// [`Dispatcher`]: crate::dispatching::Dispatcher -/// [`repl`]: crate::dispatching::repls::repl() -/// [`UpdateListener`]: crate::dispatching::update_listeners::UpdateListener #[cfg(feature = "ctrlc_handler")] -pub async fn repl_with_listener<'a, R, H, E, L, ListenerE, Args>(bot: R, handler: H, listener: L) +pub async fn repl_with_listener(bot: R, handler: H, listener: L) where - H: Injectable, Args> + Send + Sync + 'static, - L: UpdateListener + Send + 'a, - ListenerE: Debug, - Result<(), E>: OnError, - E: Debug + Send + Sync + 'static, R: Requester + Clone + Send + Sync + 'static, + H: Injectable, Args> + Send + Sync + 'static, + L: UpdateListener + Send, + L::Err: Debug, { use crate::dispatching::Dispatcher; @@ -69,7 +115,7 @@ where // messages. See . let ignore_update = |_upd| Box::pin(async {}); - Dispatcher::builder(bot, Update::filter_message().chain(dptree::endpoint(handler))) + Dispatcher::builder(bot, Update::filter_message().endpoint(handler)) .default_handler(ignore_update) .enable_ctrlc_handler() .build() @@ -83,7 +129,7 @@ where #[test] fn repl_is_send() { let bot = crate::Bot::new(""); - let repl = crate::repl(bot, || async { crate::respond(()) }); + let repl = crate::repl(bot, || async { Ok(()) }); assert_send(&repl); fn assert_send(_: &impl Send) {} diff --git a/src/dispatching/repls/stopping.md b/src/dispatching/repls/stopping.md new file mode 100644 index 000000000..b5733e842 --- /dev/null +++ b/src/dispatching/repls/stopping.md @@ -0,0 +1,4 @@ +To stop a REPL, simply press `Ctrl`+`C` in the terminal where you run the program. +Note that graceful shutdown may take some time (we plan to improve this, see [#711]). + +[#711]: https://github.com/teloxide/teloxide/issues/711 diff --git a/src/dispatching/stop_token.rs b/src/dispatching/stop_token.rs deleted file mode 100644 index f9c25aff3..000000000 --- a/src/dispatching/stop_token.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! A stop token used to stop a listener. - -use std::{future::Future, pin::Pin, task}; - -use futures::future::{pending, AbortHandle, Abortable, Pending}; - -/// A stop token allows you to stop a listener. -/// -/// See also: [`UpdateListener::stop_token`]. -/// -/// [`UpdateListener::stop_token`]: -/// crate::dispatching::update_listeners::UpdateListener::stop_token -pub trait StopToken { - /// Stop the listener linked to this token. - fn stop(self); -} - -/// A stop token which does nothing. May be used in prototyping or in cases -/// where you do not care about graceful shutdowning. -pub struct Noop; - -impl StopToken for Noop { - fn stop(self) {} -} - -/// A stop token which corresponds to [`AsyncStopFlag`]. -#[derive(Clone)] -pub struct AsyncStopToken(AbortHandle); - -/// A flag which corresponds to [`AsyncStopToken`]. -/// -/// To know if the stop token was used you can either repeatedly call -/// [`is_stopped`] or use this type as a `Future`. -/// -/// [`is_stopped`]: AsyncStopFlag::is_stopped -#[pin_project::pin_project] -#[derive(Clone)] -pub struct AsyncStopFlag(#[pin] Abortable>); - -impl AsyncStopToken { - /// Create a new token/flag pair. - #[must_use = "This function is pure, that is does nothing unless its output is used"] - pub fn new_pair() -> (Self, AsyncStopFlag) { - let (handle, reg) = AbortHandle::new_pair(); - let token = Self(handle); - let flag = AsyncStopFlag(Abortable::new(pending(), reg)); - - (token, flag) - } -} - -impl StopToken for AsyncStopToken { - fn stop(self) { - self.0.abort() - } -} - -impl AsyncStopFlag { - /// Returns true if the stop token linked to `self` was used. - #[must_use = "This function is pure, that is does nothing unless its output is used"] - pub fn is_stopped(&self) -> bool { - self.0.is_aborted() - } -} - -/// This future resolves when a stop token was used. -impl Future for AsyncStopFlag { - type Output = (); - - fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { - self.project().0.poll(cx).map(|res| { - debug_assert!( - res.is_err(), - "Pending Future can't ever be resolved, so Abortable is only resolved when \ - canceled" - ); - }) - } -} diff --git a/src/dispatching/update_listeners.rs b/src/dispatching/update_listeners.rs index 9c4abc2fb..4103e3984 100644 --- a/src/dispatching/update_listeners.rs +++ b/src/dispatching/update_listeners.rs @@ -35,7 +35,7 @@ use futures::Stream; use std::time::Duration; use crate::{ - dispatching::stop_token::StopToken, + stop::StopToken, types::{AllowedUpdate, Update}, }; @@ -59,9 +59,11 @@ pub use self::{ /// - [`AsUpdateStream::as_stream`] /// /// [module-level documentation]: mod@self -pub trait UpdateListener: for<'a> AsUpdateStream<'a, E> { - /// The type of token which allows to stop this listener. - type StopToken: StopToken + Send; +pub trait UpdateListener: + for<'a> AsUpdateStream<'a, StreamErr = ::Err> +{ + /// The type of errors that can be returned from this listener. + type Err; /// Returns a token which stops this listener. /// @@ -76,7 +78,7 @@ pub trait UpdateListener: for<'a> AsUpdateStream<'a, E> { /// soon as all cached updates are returned. #[must_use = "This function doesn't stop listening, to stop listening you need to call `stop` \ on the returned token"] - fn stop_token(&mut self) -> Self::StopToken; + fn stop_token(&mut self) -> StopToken; /// Hint which updates should the listener listen for. /// @@ -110,16 +112,19 @@ pub trait UpdateListener: for<'a> AsUpdateStream<'a, E> { /// [`UpdateListener`]'s supertrait/extension. /// /// This trait is a workaround to not require GAT. -pub trait AsUpdateStream<'a, E> { +pub trait AsUpdateStream<'a> { + /// Error that can be returned from the [`Stream`] + /// + /// [`Stream`]: AsUpdateStream::Stream + // NB: This should be named differently to `UpdateListener::Err`, so that it's + // unambiguous + type StreamErr; + /// The stream of updates from Telegram. - // HACK: There is currently no way to write something like - // `-> impl for<'a> AsUpdateStream<'a, E, Stream: Send>`. Since we return - // `impl UpdateListener` from `polling`, we need to have `Send` bound here, - // to make the stream `Send`. - // - // Without this it's, for example, impossible to spawn a tokio task with - // teloxide polling. - type Stream: Stream> + Send + 'a; + // NB: `Send` is not strictly required here, but it makes it easier to return + // `impl AsUpdateStream` and also you want `Send` streams almost (?) always + // anyway. + type Stream: Stream> + Send + 'a; /// Creates the update [`Stream`]. /// @@ -128,9 +133,9 @@ pub trait AsUpdateStream<'a, E> { } #[inline(always)] -pub(crate) fn assert_update_listener(listener: L) -> L +pub(crate) fn assert_update_listener(listener: L) -> L where - L: UpdateListener, + L: UpdateListener, { listener } diff --git a/src/dispatching/update_listeners/polling.rs b/src/dispatching/update_listeners/polling.rs index 118fc02ec..5fb863f07 100644 --- a/src/dispatching/update_listeners/polling.rs +++ b/src/dispatching/update_listeners/polling.rs @@ -13,11 +13,9 @@ use std::{ use futures::{ready, stream::Stream}; use crate::{ - dispatching::{ - stop_token::{AsyncStopFlag, AsyncStopToken}, - update_listeners::{assert_update_listener, AsUpdateStream, UpdateListener}, - }, + dispatching::update_listeners::{assert_update_listener, AsUpdateStream, UpdateListener}, requests::{HasPayload, Request, Requester}, + stop::{mk_stop_token, StopFlag, StopToken}, types::{AllowedUpdate, Update}, }; @@ -69,7 +67,7 @@ where /// /// ## Note /// - /// Teloxide normally (when using [`Dispatcher`] or [`repl`]s) sets this + /// `teloxide` normally (when using [`Dispatcher`] or [`repl`]s) sets this /// automatically via [`hint_allowed_updates`], so you rarely need to use /// `allowed_updates` explicitly. /// @@ -98,7 +96,7 @@ where /// See also: [`polling_default`], [`Polling`]. pub fn build(self) -> Polling { let Self { bot, timeout, limit, allowed_updates, drop_pending_updates } = self; - let (token, flag) = AsyncStopToken::new_pair(); + let (token, flag) = mk_stop_token(); let polling = Polling { bot, timeout, limit, allowed_updates, drop_pending_updates, flag, token }; @@ -242,8 +240,8 @@ pub struct Polling { limit: Option, allowed_updates: Option>, drop_pending_updates: bool, - flag: AsyncStopFlag, - token: AsyncStopToken, + flag: StopFlag, + token: StopToken, } impl Polling @@ -291,10 +289,10 @@ pub struct PollingStream<'a, B: Requester> { in_flight: Option<::Send>, } -impl UpdateListener for Polling { - type StopToken = AsyncStopToken; +impl UpdateListener for Polling { + type Err = B::Err; - fn stop_token(&mut self) -> Self::StopToken { + fn stop_token(&mut self) -> StopToken { self.token.clone() } @@ -309,7 +307,8 @@ impl UpdateListener for Polling { } } -impl<'a, B: Requester + Send + 'a> AsUpdateStream<'a, B::Err> for Polling { +impl<'a, B: Requester + Send + 'a> AsUpdateStream<'a> for Polling { + type StreamErr = B::Err; type Stream = PollingStream<'a, B>; fn as_stream(&'a mut self) -> Self::Stream { diff --git a/src/dispatching/update_listeners/stateful_listener.rs b/src/dispatching/update_listeners/stateful_listener.rs index e37a9efc1..0eec921da 100644 --- a/src/dispatching/update_listeners/stateful_listener.rs +++ b/src/dispatching/update_listeners/stateful_listener.rs @@ -3,10 +3,8 @@ use std::time::Duration; use futures::Stream; use crate::{ - dispatching::{ - stop_token::{self, StopToken}, - update_listeners::{AsUpdateStream, UpdateListener}, - }, + dispatching::update_listeners::{AsUpdateStream, UpdateListener}, + stop::StopToken, types::{AllowedUpdate, Update}, }; @@ -30,7 +28,7 @@ pub struct StatefulListener { /// The function used as [`UpdateListener::stop_token`]. /// - /// Must implement `FnMut(&mut St) -> impl StopToken`. + /// Must implement `FnMut(&mut St) -> StopToken`. pub stop_token: Sf, /// The function used as [`UpdateListener::hint_allowed_updates`]. @@ -68,42 +66,7 @@ impl StatefulListener { } } -impl - StatefulListener< - S, - for<'a> fn(&'a mut S) -> &'a mut S, - for<'a> fn(&'a mut S) -> stop_token::Noop, - Haufn, - Thfn, - > -where - S: Stream> + Unpin + Send + 'static, -{ - /// Creates a new update listener from a stream of updates which ignores - /// stop signals. - /// - /// It won't be possible to ever stop this listener with a stop token. - pub fn from_stream_without_graceful_shutdown(stream: S) -> Self { - let this = Self::new_with_hints( - stream, - |s| s, - |_| stop_token::Noop, - None, - Some(|_| { - // FIXME: replace this by just Duration::MAX once 1.53 releases - // be released - const NANOS_PER_SEC: u32 = 1_000_000_000; - let dmax = Duration::new(u64::MAX, NANOS_PER_SEC - 1); - - Some(dmax) - }), - ); - - assert_update_listener(this) - } -} - -impl<'a, St, Assf, Sf, Hauf, Thf, Strm, E> AsUpdateStream<'a, E> +impl<'a, St, Assf, Sf, Hauf, Thf, Strm, E> AsUpdateStream<'a> for StatefulListener where (St, Strm): 'a, @@ -111,6 +74,7 @@ where Assf: FnMut(&'a mut St) -> Strm, Strm: Stream>, { + type StreamErr = E; type Stream = Strm; fn as_stream(&'a mut self) -> Self::Stream { @@ -118,18 +82,16 @@ where } } -impl UpdateListener - for StatefulListener +impl UpdateListener for StatefulListener where - Self: for<'a> AsUpdateStream<'a, E>, - Sf: FnMut(&mut St) -> Stt, - Stt: StopToken + Send, + Self: for<'a> AsUpdateStream<'a, StreamErr = E>, + Sf: FnMut(&mut St) -> StopToken, Hauf: FnMut(&mut St, &mut dyn Iterator), Thf: Fn(&St) -> Option, { - type StopToken = Stt; + type Err = E; - fn stop_token(&mut self) -> Stt { + fn stop_token(&mut self) -> StopToken { (self.stop_token)(&mut self.state) } @@ -143,10 +105,3 @@ where self.timeout_hint.as_ref().and_then(|f| f(&self.state)) } } - -fn assert_update_listener(l: L) -> L -where - L: UpdateListener, -{ - l -} diff --git a/src/dispatching/update_listeners/webhooks.rs b/src/dispatching/update_listeners/webhooks.rs index ef94fec46..e327016b1 100644 --- a/src/dispatching/update_listeners/webhooks.rs +++ b/src/dispatching/update_listeners/webhooks.rs @@ -49,7 +49,7 @@ pub struct Options { /// `a-z`, `0-9`, `_` and `-` are allowed. The header is useful to ensure /// that the request comes from a webhook set by you. /// - /// Default - teloxide will generate a random token. + /// Default - `teloxide` will generate a random token. pub secret_token: Option, } diff --git a/src/dispatching/update_listeners/webhooks/axum.rs b/src/dispatching/update_listeners/webhooks/axum.rs index 6c16bdf0d..7bbe03bae 100644 --- a/src/dispatching/update_listeners/webhooks/axum.rs +++ b/src/dispatching/update_listeners/webhooks/axum.rs @@ -6,11 +6,9 @@ use axum::{ }; use crate::{ - dispatching::{ - stop_token::{AsyncStopFlag, StopToken}, - update_listeners::{webhooks::Options, UpdateListener}, - }, + dispatching::update_listeners::{webhooks::Options, UpdateListener}, requests::Requester, + stop::StopFlag, }; /// Webhook implementation based on the [mod@axum] framework. @@ -22,7 +20,7 @@ use crate::{ /// /// [`set_webhook`]: crate::payloads::SetWebhook /// [`delete_webhook`]: crate::payloads::DeleteWebhook -/// [`stop`]: StopToken::stop +/// [`stop`]: crate::stop::StopToken::stop /// /// ## Panics /// @@ -38,7 +36,10 @@ use crate::{ /// /// [`axum_to_router`] and [`axum_no_setup`] for lower-level versions of this /// function. -pub async fn axum(bot: R, options: Options) -> Result, R::Err> +pub async fn axum( + bot: R, + options: Options, +) -> Result, R::Err> where R: Requester + Send + 'static, ::DeleteWebhook: Send, @@ -85,7 +86,7 @@ where /// /// [`set_webhook`]: crate::payloads::SetWebhook /// [`delete_webhook`]: crate::payloads::DeleteWebhook -/// [`stop`]: StopToken::stop +/// [`stop`]: crate::stop::StopToken::stop /// [`options.address`]: Options::address /// [`with_graceful_shutdown`]: axum::Server::with_graceful_shutdown /// @@ -107,7 +108,10 @@ where pub async fn axum_to_router( bot: R, mut options: Options, -) -> Result<(impl UpdateListener, impl Future + Send, axum::Router), R::Err> +) -> Result< + (impl UpdateListener, impl Future + Send, axum::Router), + R::Err, +> where R: Requester + Send, ::DeleteWebhook: Send, @@ -148,12 +152,10 @@ where /// function. pub fn axum_no_setup( options: Options, -) -> (impl UpdateListener, impl Future, axum::Router) { +) -> (impl UpdateListener, impl Future, axum::Router) { use crate::{ - dispatching::{ - stop_token::AsyncStopToken, - update_listeners::{self, webhooks::tuple_first_mut}, - }, + dispatching::update_listeners::{self, webhooks::tuple_first_mut}, + stop::{mk_stop_token, StopToken}, types::Update, }; use axum::{extract::Extension, response::IntoResponse, routing::post}; @@ -172,7 +174,7 @@ pub fn axum_no_setup( secret_header: XTelegramBotApiSecretToken, secret: Extension>, tx: Extension, - flag: Extension, + flag: Extension, ) -> impl IntoResponse { // FIXME: use constant time comparison here if secret_header.0.as_deref() != secret.as_deref().map(str::as_bytes) { @@ -208,7 +210,7 @@ pub fn axum_no_setup( StatusCode::OK } - let (stop_token, stop_flag) = AsyncStopToken::new_pair(); + let (stop_token, stop_flag) = mk_stop_token(); let app = axum::Router::new().route(options.url.path(), post(telegram_request)).layer( ServiceBuilder::new() @@ -225,7 +227,7 @@ pub fn axum_no_setup( let listener = update_listeners::StatefulListener::new( (stream, stop_token), tuple_first_mut, - |state: &mut (_, AsyncStopToken)| state.1.clone(), + |state: &mut (_, StopToken)| state.1.clone(), ); (listener, stop_flag, app) diff --git a/src/features.md b/src/features.md index 2c4b2951e..1545af787 100644 --- a/src/features.md +++ b/src/features.md @@ -6,13 +6,13 @@ | `webhooks-axum` | Enables webhook implementation based on axum framework | | `macros` | Re-exports macros from [`teloxide-macros`]. | | `ctrlc_handler` | Enables the [`DispatcherBuilder::enable_ctrlc_handler`] function (**enabled by default**). | -| `auto-send` | Enables the [`AutoSend`](adaptors::AutoSend) bot adaptor (**enabled by default**). | +| `auto-send` | Enables the [`AutoSend`](adaptors::AutoSend) bot adaptor (**enabled by default; DEPRECATED**). | | `throttle` | Enables the [`Throttle`](adaptors::Throttle) bot adaptor. | | `cache-me` | Enables the [`CacheMe`](adaptors::CacheMe) bot adaptor. | | `trace-adaptor` | Enables the [`Trace`](adaptors::Trace) bot adaptor. | | `erased` | Enables the [`ErasedRequester`](adaptors::ErasedRequester) bot adaptor. | | `full` | Enables all the features except `nightly`. | -| `nightly` | Enables nightly-only features (see the [teloxide-core features]). | +| `nightly` | Enables nightly-only features (see the [`teloxide-core` features]). | | `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. | @@ -29,6 +29,6 @@ [`native-tls`]: https://docs.rs/native-tls [`rustls`]: https://docs.rs/rustls [`teloxide::utils::UpState`]: utils::UpState -[teloxide-core features]: https://docs.rs/teloxide-core/latest/teloxide_core/#cargo-features +[`teloxide-core` features]: https://docs.rs/teloxide-core/latest/teloxide_core/#cargo-features [`DispatcherBuilder::enable_ctrlc_handler`]: dispatching::DispatcherBuilder::enable_ctrlc_handler \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 2964cd746..80237a9dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,11 +13,11 @@ //! pretty_env_logger::init(); //! log::info!("Starting throw dice bot..."); //! -//! let bot = Bot::from_env().auto_send(); +//! let bot = Bot::from_env(); //! -//! teloxide::repl(bot, |message: Message, bot: AutoSend| async move { -//! bot.send_dice(message.chat.id).await?; -//! respond(()) +//! teloxide::repl(bot, |bot: Bot, msg: Message| async move { +//! bot.send_dice(msg.chat.id).await?; +//! Ok(()) //! }) //! .await; //! # } @@ -62,11 +62,10 @@ pub use dispatching::repls::{ commands_repl, commands_repl_with_listener, repl, repl_with_listener, }; -mod logging; - pub mod dispatching; pub mod error_handlers; pub mod prelude; +pub mod stop; pub mod utils; #[doc(inline)] diff --git a/src/logging.rs b/src/logging.rs deleted file mode 100644 index 770c17a73..000000000 --- a/src/logging.rs +++ /dev/null @@ -1,53 +0,0 @@ -/// Enables logging through [pretty-env-logger]. -/// -/// A logger will **only** print errors, warnings, and general information from -/// teloxide and **all** logs from your program. -/// -/// # Note -/// -/// Calling this macro **is not mandatory**; you can setup if your own logger if -/// you want. -/// -/// [pretty-env-logger]: https://crates.io/crates/pretty_env_logger -#[macro_export] -#[deprecated = "Choose logging implementation yourself"] -macro_rules! enable_logging { - () => { - #[allow(deprecated)] - teloxide::enable_logging_with_filter!(log::LevelFilter::Trace); - }; -} - -/// Enables logging through [pretty-env-logger] with a custom filter for your -/// program. -/// -/// A logger will **only** print errors, warnings, and general information from -/// teloxide and restrict logs from your program by the specified filter. -/// -/// # Example -/// -/// Allow printing all logs from your program up to [`LevelFilter::Debug`] (i.e. -/// do not print traces): -/// -/// ```no_compile -/// teloxide::enable_logging_with_filter!(log::LevelFilter::Debug); -/// ``` -/// -/// # Note -/// -/// Calling this macro **is not mandatory**; you can setup if your own logger if -/// you want. -/// -/// [pretty-env-logger]: https://crates.io/crates/pretty_env_logger -/// [`LevelFilter::Debug`]: https://docs.rs/log/0.4.10/log/enum.LevelFilter.html -#[macro_export] -#[deprecated = "Choose logging implementation yourself"] -macro_rules! enable_logging_with_filter { - ($filter:expr) => { - pretty_env_logger::formatted_builder() - .write_style(pretty_env_logger::env_logger::WriteStyle::Auto) - .filter(Some(&env!("CARGO_CRATE_NAME").replace("-", "_")), $filter) - .filter(Some("teloxide"), log::LevelFilter::Info) - .init(); - }; -} diff --git a/src/prelude.rs b/src/prelude.rs index 3cbf6ca52..6dbe94df2 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,20 +1,24 @@ //! Commonly used items. -pub use crate::{ - error_handlers::{LoggingErrorHandler, OnError}, - respond, -}; +pub use crate::error_handlers::{LoggingErrorHandler, OnError}; + +#[allow(deprecated)] +pub use crate::respond; pub use crate::dispatching::{ dialogue::Dialogue, Dispatcher, HandlerExt as _, MessageFilterExt as _, UpdateFilterExt as _, }; -pub use teloxide_core::types::{ - CallbackQuery, ChatMemberUpdated, ChosenInlineResult, InlineQuery, Message, Poll, PollAnswer, - PreCheckoutQuery, ShippingQuery, Update, +pub use teloxide_core::{ + requests::ResponseResult, + types::{ + CallbackQuery, ChatMemberUpdated, ChosenInlineResult, InlineQuery, Message, Poll, + PollAnswer, PreCheckoutQuery, ShippingQuery, Update, + }, }; #[cfg(feature = "auto-send")] +#[allow(deprecated)] pub use crate::adaptors::AutoSend; #[doc(no_inline)] diff --git a/src/stop.rs b/src/stop.rs new file mode 100644 index 000000000..f61caf3d1 --- /dev/null +++ b/src/stop.rs @@ -0,0 +1,64 @@ +//! This module contains stop [token] and stop [flag] that are used to stop +//! async tasks, for example [listeners]. +//! +//! [token]: StopToken +//! [flag]: StopFlag +//! [listeners]: crate::dispatching::update_listeners + +use std::{convert::Infallible, future::Future, pin::Pin, task}; + +use futures::future::{pending, AbortHandle, Abortable, Pending}; + +/// Create a new token/flag pair. +#[must_use] +pub fn mk_stop_token() -> (StopToken, StopFlag) { + let (handle, reg) = AbortHandle::new_pair(); + let token = StopToken(handle); + let flag = StopFlag(Abortable::new(pending(), reg)); + + (token, flag) +} + +/// A stop token which corresponds to a [`StopFlag`]. +#[derive(Clone)] +pub struct StopToken(AbortHandle); + +/// A flag which corresponds to [`StopToken`]. +/// +/// To know if the stop token was used you can either repeatedly call +/// [`is_stopped`] or use this type as a `Future`. +/// +/// [`is_stopped`]: StopFlag::is_stopped +#[pin_project::pin_project] +#[derive(Clone)] +pub struct StopFlag(#[pin] Abortable>); + +impl StopToken { + /// "Stops" the flag associated with this token. + /// + /// Note that calling this function multiple times does nothing, only the + /// first call changes the state. + pub fn stop(&self) { + self.0.abort() + } +} + +impl StopFlag { + /// Returns true if the stop token linked to `self` was used. + #[must_use] + pub fn is_stopped(&self) -> bool { + self.0.is_aborted() + } +} + +/// This future resolves when a stop token was used. +impl Future for StopFlag { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { + self.project().0.poll(cx).map(|res| match res { + Err(_aborted) => (), + Ok(unreachable) => match unreachable {}, + }) + } +} diff --git a/src/utils/command.rs b/src/utils/command.rs index 8d29ee7bb..24e001cfa 100644 --- a/src/utils/command.rs +++ b/src/utils/command.rs @@ -13,7 +13,7 @@ //! type UnitOfTime = u8; //! //! #[derive(BotCommands, PartialEq, Debug)] -//! #[command(rename = "lowercase", parse_with = "split")] +//! #[command(rename_rule = "lowercase", parse_with = "split")] //! enum AdminCommand { //! Mute(UnitOfTime, char), //! Ban(UnitOfTime, char), @@ -70,7 +70,7 @@ pub use teloxide_macros::BotCommands; /// type UnitOfTime = u8; /// /// #[derive(BotCommands, PartialEq, Debug)] -/// #[command(rename = "lowercase", parse_with = "split")] +/// #[command(rename_rule = "lowercase", parse_with = "split")] /// enum AdminCommand { /// Mute(UnitOfTime, char), /// Ban(UnitOfTime, char), @@ -82,11 +82,10 @@ pub use teloxide_macros::BotCommands; /// ``` /// /// # Enum attributes -/// 1. `#[command(rename = "rule")]` -/// Rename all commands by `rule`. If you will not use this attribute, commands -/// will be parsed by their original names. Allowed rules are `lowercase`, -/// `UPPERCASE`, `PascalCase`, `camelCase`, `snake_case`, -/// `SCREAMING_SNAKE_CASE`, `kebab-case`, and `SCREAMING-KEBAB-CASE`. +/// 1. `#[command(rename_rule = "rule")]` +/// Rename all commands by `rule`. Allowed rules are `lowercase`, `UPPERCASE`, +/// `PascalCase`, `camelCase`, `snake_case`, `SCREAMING_SNAKE_CASE`, +/// `kebab-case`, and `SCREAMING-KEBAB-CASE`. /// /// 2. `#[command(prefix = "prefix")]` /// Change a prefix for all commands (the default is `/`). @@ -106,7 +105,7 @@ pub use teloxide_macros::BotCommands; /// use teloxide::utils::command::BotCommands; /// /// #[derive(BotCommands, PartialEq, Debug)] -/// #[command(rename = "lowercase")] +/// #[command(rename_rule = "lowercase")] /// enum Command { /// Text(String), /// } @@ -126,7 +125,7 @@ pub use teloxide_macros::BotCommands; /// use teloxide::utils::command::BotCommands; /// /// #[derive(BotCommands, PartialEq, Debug)] -/// #[command(rename = "lowercase", parse_with = "split")] +/// #[command(rename_rule = "lowercase", parse_with = "split")] /// enum Command { /// Nums(u8, u16, i32), /// } @@ -146,7 +145,7 @@ pub use teloxide_macros::BotCommands; /// use teloxide::utils::command::BotCommands; /// /// #[derive(BotCommands, PartialEq, Debug)] -/// #[command(rename = "lowercase", parse_with = "split", separator = "|")] +/// #[command(rename_rule = "lowercase", parse_with = "split", separator = "|")] /// enum Command { /// Nums(u8, u16, i32), /// } @@ -159,21 +158,23 @@ pub use teloxide_macros::BotCommands; /// # Variant attributes /// All variant attributes override the corresponding `enum` attributes. /// -/// 1. `#[command(rename = "rule")]` +/// 1. `#[command(rename_rule = "rule")]` /// Rename one command by a rule. Allowed rules are `lowercase`, `UPPERCASE`, /// `PascalCase`, `camelCase`, `snake_case`, `SCREAMING_SNAKE_CASE`, -/// `kebab-case`, `SCREAMING-KEBAB-CASE`, and `%some_name%`, where `%some_name%` -/// is any string, a new name. +/// `kebab-case`, `SCREAMING-KEBAB-CASE`. +/// +/// 2. `#[command(rename = "name")]` +/// Rename one command to `name` (literal renaming; do not confuse with +/// `rename_rule`). /// -/// 2. `#[command(description = "description")]` +/// 3. `#[command(description = "description")]` /// Give your command a description. Write `"off"` for `"description"` to hide a /// command. /// -/// 3. `#[command(parse_with = "parser")]` -/// One more option is available for variants. -/// - `custom_parser` - your own parser of the signature `fn(String) -> -/// Result`, where `Tuple` corresponds to the variant's -/// arguments. +/// 4. `#[command(parse_with = "parser")]` +/// Parse arguments of one command with a given parser. `parser` must be a +/// function of the signature `fn(String) -> Result`, where +/// `Tuple` corresponds to the variant's arguments. /// /// ## Example /// ``` @@ -191,9 +192,9 @@ pub use teloxide_macros::BotCommands; /// } /// /// #[derive(BotCommands, PartialEq, Debug)] -/// #[command(rename = "lowercase")] +/// #[command(rename_rule = "lowercase")] /// enum Command { -/// #[command(parse_with = "accept_two_digits")] +/// #[command(parse_with = accept_two_digits)] /// Num(u8), /// } /// @@ -204,8 +205,8 @@ pub use teloxide_macros::BotCommands; /// # } /// ``` /// -/// 4. `#[command(prefix = "prefix")]` -/// 5. `#[command(separator = "sep")]` +/// 5. `#[command(prefix = "prefix")]` +/// 6. `#[command(separator = "sep")]` /// /// These attributes just override the corresponding `enum` attributes for a /// specific variant. @@ -217,9 +218,7 @@ pub trait BotCommands: Sized { /// /// `bot_username` is required to parse commands like /// `/cmd@username_of_the_bot`. - fn parse(s: &str, bot_username: N) -> Result - where - N: Into; + fn parse(s: &str, bot_username: &str) -> Result; /// Returns descriptions of the commands suitable to be shown to the user /// (for example when `/help` command is used). @@ -235,6 +234,7 @@ pub trait BotCommands: Sized { /// Returns `PhantomData` that is used as a param of [`commands_repl`] /// /// [`commands_repl`]: (crate::repls2::commands_repl) + #[must_use] fn ty() -> PhantomData { PhantomData } @@ -296,11 +296,13 @@ pub struct CommandDescription<'a> { impl<'a> CommandDescriptions<'a> { /// Creates new [`CommandDescriptions`] from a list of command descriptions. + #[must_use] pub fn new(descriptions: &'a [CommandDescription<'a>]) -> Self { Self { global_description: None, descriptions, bot_username: None } } /// Sets the global description of these commands. + #[must_use] pub fn global_description(self, global_description: &'a str) -> Self { Self { global_description: Some(global_description), ..self } } @@ -328,6 +330,7 @@ impl<'a> CommandDescriptions<'a> { /// message" /// ); /// ``` + #[must_use] pub fn username(self, bot_username: &'a str) -> Self { Self { bot_username: Some(bot_username), ..self } } @@ -338,6 +341,7 @@ impl<'a> CommandDescriptions<'a> { /// method to get the username. /// /// [`username`]: self::CommandDescriptions::username + #[must_use] pub fn username_from_me(self, me: &'a Me) -> CommandDescriptions<'a> { self.username(me.user.username.as_deref().expect("Bots must have usernames")) } diff --git a/src/utils/shutdown_token.rs b/src/utils/shutdown_token.rs index 2447f6fd2..998a64ae1 100644 --- a/src/utils/shutdown_token.rs +++ b/src/utils/shutdown_token.rs @@ -93,7 +93,7 @@ impl fmt::Display for IdleShutdownError { impl std::error::Error for IdleShutdownError {} -pub(crate) fn shutdown_check_timeout_for(update_listener: &impl UpdateListener) -> Duration { +pub(crate) fn shutdown_check_timeout_for(update_listener: &impl UpdateListener) -> Duration { const MIN_SHUTDOWN_CHECK_TIMEOUT: Duration = Duration::from_secs(1); const DZERO: Duration = Duration::ZERO; diff --git a/tests/command.rs b/tests/command.rs index 4b59ac485..cac62e790 100644 --- a/tests/command.rs +++ b/tests/command.rs @@ -11,7 +11,7 @@ use teloxide::utils::command::BotCommands; #[cfg(feature = "macros")] fn parse_command_with_args() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { Start(String), Help, @@ -27,7 +27,7 @@ fn parse_command_with_args() { #[cfg(feature = "macros")] fn parse_command_with_non_string_arg() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { Start(i32), Help, @@ -43,7 +43,7 @@ fn parse_command_with_non_string_arg() { #[cfg(feature = "macros")] fn attribute_prefix() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { #[command(prefix = "!")] Start(String), @@ -60,7 +60,7 @@ fn attribute_prefix() { #[cfg(feature = "macros")] fn many_attributes() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { #[command(prefix = "!", description = "desc")] Start, @@ -75,7 +75,7 @@ fn many_attributes() { #[cfg(feature = "macros")] fn global_attributes() { #[derive(BotCommands, Debug, PartialEq)] - #[command(prefix = "!", rename = "lowercase", description = "Bot commands")] + #[command(prefix = "!", rename_rule = "lowercase", description = "Bot commands")] enum DefaultCommands { #[command(prefix = "/")] Start, @@ -91,7 +91,7 @@ fn global_attributes() { #[cfg(feature = "macros")] fn parse_command_with_bot_name() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { #[command(prefix = "/")] Start, @@ -108,7 +108,7 @@ fn parse_command_with_bot_name() { #[cfg(feature = "macros")] fn parse_with_split() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] #[command(parse_with = "split")] enum DefaultCommands { Start(u8, String), @@ -125,7 +125,7 @@ fn parse_with_split() { #[cfg(feature = "macros")] fn parse_with_split2() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] #[command(parse_with = "split", separator = "|")] enum DefaultCommands { Start(u8, String), @@ -159,13 +159,13 @@ fn parse_custom_parser() { use parser::custom_parse_function; #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { - #[command(parse_with = "custom_parse_function")] + #[command(parse_with = custom_parse_function)] Start(u8, String), // Test . - #[command(parse_with = "parser::custom_parse_function")] + #[command(parse_with = parser::custom_parse_function)] TestPath(u8, String), Help, @@ -185,7 +185,7 @@ fn parse_custom_parser() { #[cfg(feature = "macros")] fn parse_named_fields() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] #[command(parse_with = "split")] enum DefaultCommands { Start { num: u8, data: String }, @@ -202,7 +202,7 @@ fn parse_named_fields() { #[cfg(feature = "macros")] fn descriptions_off() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { #[command(description = "off")] Start, @@ -217,21 +217,21 @@ fn descriptions_off() { fn rename_rules() { #[derive(BotCommands, Debug, PartialEq)] enum DefaultCommands { - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] AaaAaa, - #[command(rename = "UPPERCASE")] + #[command(rename_rule = "UPPERCASE")] BbbBbb, - #[command(rename = "PascalCase")] + #[command(rename_rule = "PascalCase")] CccCcc, - #[command(rename = "camelCase")] + #[command(rename_rule = "camelCase")] DddDdd, - #[command(rename = "snake_case")] + #[command(rename_rule = "snake_case")] EeeEee, - #[command(rename = "SCREAMING_SNAKE_CASE")] + #[command(rename_rule = "SCREAMING_SNAKE_CASE")] FffFff, - #[command(rename = "kebab-case")] + #[command(rename_rule = "kebab-case")] GggGgg, - #[command(rename = "SCREAMING-KEBAB-CASE")] + #[command(rename_rule = "SCREAMING-KEBAB-CASE")] HhhHhh, #[command(rename = "Bar")] Foo, diff --git a/tests/dialogue_state.rs b/tests/dialogue_state.rs deleted file mode 100644 index 567d8f51f..000000000 --- a/tests/dialogue_state.rs +++ /dev/null @@ -1,64 +0,0 @@ -#![allow(deprecated)] - -#[cfg(feature = "macros")] -use teloxide::macros::DialogueState; -// We put tests here because macro expand in unit tests in the crate was a -// failure - -#[test] -#[cfg(feature = "macros")] -fn compile_test() { - #[allow(dead_code)] - #[derive(DialogueState, Clone)] - #[handler_out(Result<(), teloxide::RequestError>)] - enum State { - #[handler(handle_start)] - Start, - - #[handler(handle_have_data)] - HaveData(String), - } - - impl Default for State { - fn default() -> Self { - Self::Start - } - } - - async fn handle_start() -> Result<(), teloxide::RequestError> { - Ok(()) - } - - async fn handle_have_data() -> Result<(), teloxide::RequestError> { - Ok(()) - } -} - -#[test] -#[cfg(feature = "macros")] -fn compile_test_generics() { - #[allow(dead_code)] - #[derive(DialogueState, Clone)] - #[handler_out(Result<(), teloxide::RequestError>)] - enum State { - #[handler(handle_start)] - Start, - - #[handler(handle_have_data)] - HaveData(X), - } - - impl Default for State { - fn default() -> Self { - Self::Start - } - } - - async fn handle_start() -> Result<(), teloxide::RequestError> { - Ok(()) - } - - async fn handle_have_data() -> Result<(), teloxide::RequestError> { - Ok(()) - } -}