Merge pull request #508 from teloxide/dev

Merge v0.6.0
This commit is contained in:
Hirrolot 2022-02-06 18:10:55 +06:00 committed by GitHub
commit 66023ee56c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 2909 additions and 1626 deletions

View file

@ -18,8 +18,3 @@ Instead, this happened: _explanation_
## Meta
- `teloxide` version: <!-- (e.g.: `0.3.1`) -->
- rustc version:
```
<version>
```
<!-- use `rustc --version --verbose` to get it -->

View file

@ -19,11 +19,6 @@ When using `<...>` method I've got `RequestError::InvalidJson` error with the f
## Meta
- `teloxide` version: <!-- (e.g.: `0.3.1`) -->
- rustc version:
```
<version>
```
<!-- use `rustc --version --verbose` to get it -->
### Additional context

View file

@ -19,11 +19,6 @@ When using `<...>` method I've got `ApiError::Unknown` error with the following
## Meta
- `teloxide` version: <!-- (e.g.: `0.3.1`) -->
- rustc version:
```
<version>
```
<!-- use `rustc --version --verbose` to get it -->
### Additional context

View file

@ -15,7 +15,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
toolchain: nightly-2022-02-02
override: true
components: rustfmt
@ -34,7 +34,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
toolchain: nightly-2022-02-02
override: true
components: clippy
@ -91,18 +91,6 @@ jobs:
build-example:
runs-on: ubuntu-latest
strategy:
matrix:
example: [
admin_bot,
dialogue_bot,
heroku_ping_pong_bot,
ngrok_ping_pong_bot,
dices_bot,
shared_state_bot,
simple_commands_bot,
redis_remember_bot,
]
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
@ -110,5 +98,5 @@ jobs:
profile: minimal
toolchain: stable
override: true
- name: Check the example
run: cd examples && cd ${{ matrix.example }} && cargo check
- name: Check the examples
run: cargo check --examples --features="full"

View file

@ -4,7 +4,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [unreleased]
## unreleased
## 0.6.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`.
### Changed
- Require that `AsUpdateStream::Stream` is `Send`.
- Restrict a user crate by `CARGO_CRATE_NAME` instead of `CARGO_PKG_NAME` in `enable_logging!` and `enable_logging_with_filter!`.
- Updated `teloxide-core` to v0.4.0, see [its changelog](https://github.com/teloxide/teloxide-core/blob/master/CHANGELOG.md#040---2022-02-03).
### Deprecated
- The `dispatching` and `prelude` modules.
### Fixed
- Infinite retries while stopping polling listener ([issue 496](https://github.com/teloxide/teloxide/issues/496))
- `polling{,_default}` and it's `Stream` and `StopToken` not being `Send` (and by extension fix the same problem with `repl`s)
## 0.5.3 - 2021-10-25
@ -12,19 +34,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Compilation when the `ctrlc_handler` feature is disabled ([issue 462](https://github.com/teloxide/teloxide/issues/462))
## [0.5.2] - 2021-08-25
## 0.5.2 - 2021-08-25
### Fixed
- Depend on a correct `futures` version (v0.3.15).
## [0.5.1] - 2021-08-05
## 0.5.1 - 2021-08-05
### Changed
- Improved log messages when `^C` is received with `^C` handler set up
## [0.5.0] - 2021-07-08
## 0.5.0 - 2021-07-08
### Added
@ -70,7 +92,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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
## 0.4.0 - 2021-03-22
### Added
- Integrate [teloxide-core].
@ -107,28 +129,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[issue 253]: https://github.com/teloxide/teloxide/issues/253
[pr 257]: https://github.com/teloxide/teloxide/pull/257
## [0.3.4] - 2020-01-13
## 0.3.4 - 2020-01-13
### Fixed
- Failing compilation with `serde::export` ([issue 328](https://github.com/teloxide/teloxide/issues/328)).
## [0.3.3] - 2020-10-30
## 0.3.3 - 2020-10-30
### Fixed
- The `dice` field from `MessageDice` is public now ([issue 306](https://github.com/teloxide/teloxide/issues/306))
## [0.3.2] - 2020-10-23
## 0.3.2 - 2020-10-23
### Added
- `LoginUrl::new` ([issue 298](https://github.com/teloxide/teloxide/issues/298))
## [0.3.1] - 2020-08-25
## 0.3.1 - 2020-08-25
### Added
- `Bot::builder` method ([PR 269](https://github.com/teloxide/teloxide/pull/269)).
## [0.3.0] - 2020-07-31
## 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`.
@ -165,7 +187,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Now methods which can send file to Telegram returns `tokio::io::Result<T>`. 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
## 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`.
@ -180,6 +202,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [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
## 0.1.0 - 2020-02-19
### Added
- This project.

View file

@ -1,6 +1,6 @@
[package]
name = "teloxide"
version = "0.5.3"
version = "0.6.0"
edition = "2018"
description = "An elegant Telegram bots framework for Rust"
repository = "https://github.com/teloxide/teloxide"
@ -10,22 +10,11 @@ keywords = ["teloxide", "telegram", "telegram-bot", "telegram-bot-api"]
categories = ["web-programming", "api-bindings", "asynchronous"]
license = "MIT"
exclude = ["media"]
authors = [
"Hirrolot <hirrolot@gmail.com>",
"Waffle Lapkin <waffle.lapkin@gmail.com>",
"p0lunin <dmytro.polunin@gmail.com>",
"Mishko torop'izhko",
"Mr-Andersen",
"Sergey Levitin <selevit@gmail.com>",
"Rustem B. <bakirov.com@yandex.ru>",
"Alexey Fedechkin <aleksey-fedechkin@rambler.ru>"
]
[badges]
maintenance = { status = "actively-developed" }
[features]
default = ["native-tls", "ctrlc_handler", "teloxide-core/default"]
default = ["native-tls", "ctrlc_handler", "teloxide-core/default", "auto-send", "cache-me", "dispatching2"]
dispatching2 = ["dptree"]
sqlite-storage = ["sqlx"]
redis-storage = ["redis"]
@ -68,13 +57,14 @@ full = [
]
[dependencies]
teloxide-core = { version = "0.3.3", default-features = false }
#teloxide-core = { git = "https://github.com/teloxide/teloxide-core.git", rev = "...", default-features = false }
teloxide-macros = { version = "0.4", optional = true }
teloxide-core = { version = "0.4", default-features = false }
teloxide-macros = { version = "0.5", optional = true }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
dptree = { version = "0.1.0", optional = true }
tokio = { version = "1.8", features = ["fs"] }
tokio-util = "0.6"
tokio-stream = "0.1"
@ -100,18 +90,27 @@ redis = { version = "0.20", features = ["tokio-comp"], optional = true }
serde_cbor = { version = "0.11", optional = true }
bincode = { version = "1.3", optional = true }
frunk = { version = "0.4", optional = true }
aquamarine = "0.1.11"
[dev-dependencies]
smart-default = "0.6.0"
rand = "0.8.3"
pretty_env_logger = "0.4.0"
once_cell = "1.9.0"
lazy_static = "1.4.0"
anyhow = "1.0.52"
serde = "1"
serde_json = "1"
tokio = { version = "1.8", features = ["fs", "rt-multi-thread", "macros"] }
warp = "0.3.0"
reqwest = "0.10.4"
chrono = "0.4"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs", "-Znormalize-docs"]
rustc-args = ["--cfg", "dep_docsrs"]
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples=examples"]
[[test]]
name = "redis"
@ -122,3 +121,35 @@ required-features = ["redis-storage", "cbor-serializer", "bincode-serializer"]
name = "sqlite"
path = "tests/sqlite.rs"
required-features = ["sqlite-storage", "cbor-serializer", "bincode-serializer"]
[[example]]
name = "dialogue"
required-features = ["macros"]
[[example]]
name = "sqlite_remember"
required-features = ["sqlite-storage", "bincode-serializer", "redis-storage", "macros"]
[[example]]
name = "simple_commands"
required-features = ["macros"]
[[example]]
name = "redis_remember"
required-features = ["redis-storage", "bincode-serializer", "macros"]
[[example]]
name = "inline"
required-features = ["macros"]
[[example]]
name = "buttons"
required-features = ["macros"]
[[example]]
name = "admin"
required-features = ["macros"]
[[example]]
name = "dispatching2_features"
required-features = ["macros"]

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019-2020 teloxide
Copyright (c) 2019-2022 teloxide
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,6 +1,33 @@
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.5 -> 0.6
### core
- `InputFile` now can't be created like `InputFile::Url(url)` or matched on, use constructors like `InputFile::url`, `InputFile::file`, etc.
- `RequestError` and `DownloadError` error variants were slightly renamed
- `ChatPermissions` is now bitflags.
### teloxide
v0.6 of teloxide introduces a new dispatching model based on the [chain of responsibility pattern]. To use it, you need to replace `prelude` with `prelude2` and `dispatching` with `dispatching2`. Instead of using old REPLs, you should now use `teloxide::repls2`.
The whole design is different than the previous one based on Tokio streams. In this section, we are only to address the most common usage scenarios.
First of all, now there are no streams. Instead of using streams, you use [`dptree`], which is a more suitable alternative for our purposes. Thus, if you previously used `Dispatcher::messages_handler`, now you should use `Update::filter_message()`, and so on.
Secondly, `Dispatcher` has been split into two separate abstractions: `DispatcherBuilder` and `Dispatcher`. The calling sequence is simple: you call `Dispatcher::builder(bot, handler)`, set up your stuff, and then call `.build()` to obtain `Dispatcher`. Later, you can `.setup_ctrlc_handler()` on it and finally `.dispatch()` (or `.dispatch_with_listener()`).
Lastly, the dialogue management system has been greatly simplified. Just compare the [new `examples/dialogue.rs`](https://github.com/teloxide/teloxide/blob/25f863402d4f377f573ce2ba394f5b768ee8052e/examples/dialogue.rs) with the [old one](https://github.com/teloxide/teloxide/tree/2a6067fe94773a0015627a6aaa1930b8f88b6da0/examples/dialogue_bot/src) to see the difference. Now you don't need `TransitionIn`, `TransitionOut`, `#[teloxide(subtransition)]`, etc. All you need is to derive `DialogueState` for your FSM enumeration, call `.enter_dialogue()` and write handlers for each of a dialogue's states. Instead of supplying dependencies in the `aux` parameter of `Transition::react`, you can just call `.dependencies()` while setting up the dispatcher and all the dependencies will be passed to your handler functions as parameters.
For more information, please look at the appropriate documentation pages and the [updated examples](https://github.com/teloxide/teloxide/tree/master/examples). Note that, in one of the upcoming releases, the old dispatching model will be removed, so we highly encourage you to migrate your bots to the new one.
Thanks for using teloxide!
[chain of responsibility pattern]: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern
[`dptree`]: https://github.com/p0lunin/dptree
## 0.4 -> 0.5
### core

376
README.md
View file

@ -1,4 +1,4 @@
[_v0.4.0 => v0.5.0 migration guide >>_](MIGRATION_GUIDE.md#04---05)
> [v0.5 -> v0.6 migration guide >>](MIGRATION_GUIDE.md#05---06)
<div align="center">
<img src="ICON.png" width="250"/>
@ -16,7 +16,7 @@
<img src="https://img.shields.io/crates/v/teloxide.svg">
</a>
<a href="https://core.telegram.org/bots/api">
<img src="https://img.shields.io/badge/API coverage-Up to 5.3 (inclusively)-green.svg">
<img src="https://img.shields.io/badge/API%20coverage-Up%20to%205.7%20(inclusively)-green.svg">
</a>
<a href="https://t.me/teloxide">
<img src="https://img.shields.io/badge/official%20chat-t.me%2Fteloxide-blueviolet">
@ -27,23 +27,24 @@
## Highlights
- **Functional reactive design.** teloxide follows [functional reactive design], allowing you to declaratively manipulate streams of updates from Telegram using filters, maps, folds, zips, and a lot of [other adaptors].
- **Declarative design.** teloxide is based upon [`dptree`], a functional-style [chain of responsibility] pattern that allows you to express pipelines of message processing in a highly declarative and extensible style.
[functional reactive design]: https://en.wikipedia.org/wiki/Functional_reactive_programming
[other adaptors]: https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html
[`dptree`]: https://github.com/p0lunin/dptree
[chain of responsibility]: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern
- **Dialogues management subsystem.** We have designed our dialogues management subsystem to be easy-to-use, and, furthermore, to be 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].
- **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].
[persistence]: https://en.wikipedia.org/wiki/Persistence_(computer_science)
[Redis]: https://redis.io/
[Sqlite]: https://www.sqlite.org
- **Strongly typed bot 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.** 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`].
[structopt]: https://github.com/TeXitoi/structopt
[serde-json]: https://github.com/serde-rs/json
[`structopt`]: https://github.com/TeXitoi/structopt
[`serde-json`]: https://github.com/serde-rs/json
## Setting up your environment
1. [Download Rust](http://rustup.rs/).
2. Create a new bot using [@Botfather](https://t.me/botfather) to get a token in the format `123456789:blablabla`.
3. Initialise the `TELOXIDE_TOKEN` environmental variable to your token:
@ -51,8 +52,12 @@
# Unix-like
$ export TELOXIDE_TOKEN=<Your token here>
# Windows
# Windows command line
$ set TELOXIDE_TOKEN=<Your token here>
# Windows PowerShell
$ $env:TELOXIDE_TOKEN=<Your token here>
```
4. Make sure that your Rust compiler is up to date:
```bash
@ -68,20 +73,22 @@ $ 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.4", features = ["auto-send", "macros"] }
log = "0.4.8"
teloxide = { version = "0.5", features = ["macros", "auto-send"] }
log = "0.4"
pretty_env_logger = "0.4.0"
tokio = { version = "1.3", features = ["rt-multi-thread", "macros"] }
tokio = { version = "1.8", features = ["rt-multi-thread", "macros"] }
```
## API overview
### The dices bot
This bot replies with a dice throw to each received message:
([Full](./examples/dices_bot/src/main.rs))
([Full](examples/dices.rs))
```rust,no_run
use teloxide::prelude::*;
use teloxide::prelude2::*;
#[tokio::main]
async fn main() {
@ -90,8 +97,8 @@ async fn main() {
let bot = Bot::from_env().auto_send();
teloxide::repl(bot, |message| async move {
message.answer_dice().await?;
teloxide::repls2::repl(bot, |message: Message, bot: AutoSend<Bot>| async move {
bot.send_dice(message.chat.id).await?;
respond(())
})
.await;
@ -105,6 +112,7 @@ async fn main() {
</div>
### Commands
Commands are strongly typed and defined declaratively, similar to how we define CLI using [structopt] and JSON structures in [serde-json]. The following bot accepts these commands:
- `/username <your username>`
@ -114,13 +122,14 @@ Commands are strongly typed and defined declaratively, similar to how we define
[structopt]: https://docs.rs/structopt/0.3.9/structopt/
[serde-json]: https://github.com/serde-rs/json
([Full](./examples/simple_commands_bot/src/main.rs))
([Full](examples/simple_commands.rs))
```rust,no_run
use teloxide::{prelude::*, utils::command::BotCommand};
use teloxide::{prelude2::*, utils::command::BotCommand};
use std::error::Error;
#[derive(BotCommand)]
#[derive(BotCommand, Clone)]
#[command(rename = "lowercase", description = "These commands are supported:")]
enum Command {
#[command(description = "display this text.")]
@ -132,16 +141,21 @@ enum Command {
}
async fn answer(
cx: UpdateWithCx<AutoSend<Bot>, Message>,
bot: AutoSend<Bot>,
message: Message,
command: Command,
) -> Result<(), Box<dyn Error + Send + Sync>> {
match command {
Command::Help => cx.answer(Command::descriptions()).await?,
Command::Help => bot.send_message(message.chat.id, Command::descriptions()).await?,
Command::Username(username) => {
cx.answer(format!("Your username is @{}.", username)).await?
bot.send_message(message.chat.id, format!("Your username is @{}.", username)).await?
}
Command::UsernameAndAge { username, age } => {
cx.answer(format!("Your username is @{} and age is {}.", username, age)).await?
bot.send_message(
message.chat.id,
format!("Your username is @{} and age is {}.", username, age),
)
.await?
}
};
@ -155,8 +169,7 @@ async fn main() {
let bot = Bot::from_env().auto_send();
let bot_name: String = panic!("Your bot's name here");
teloxide::commands_repl(bot, bot_name, answer).await;
teloxide::repls2::commands_repl(bot, answer, Command::ty()).await;
}
```
@ -167,145 +180,41 @@ async fn main() {
</div>
### Dialogues management
A dialogue is described by an enumeration where each variant is one of possible dialogue's states. There are also _subtransition functions_, which turn a dialogue from one state to another, thereby forming an [FSM].
A dialogue is typically described by an enumeration where each variant is one of possible dialogue's states. There are also _state handler functions_, which may turn a dialogue from one state to another, thereby forming an [FSM].
[FSM]: https://en.wikipedia.org/wiki/Finite-state_machine
Below is a bot that asks you three questions and then sends the answers back to you. First, let's start with an enumeration (a collection of our dialogue's states):
Below is a bot that asks you three questions and then sends the answers back to you:
([Full](examples/dialogue.rs))
([dialogue_bot/src/dialogue/mod.rs](./examples/dialogue_bot/src/dialogue/mod.rs))
```rust,ignore
// Imports are omitted...
use teloxide::{dispatching2::dialogue::InMemStorage, macros::DialogueState, prelude2::*};
#[derive(Transition, From)]
pub enum Dialogue {
Start(StartState),
ReceiveFullName(ReceiveFullNameState),
ReceiveAge(ReceiveAgeState),
ReceiveLocation(ReceiveLocationState),
type MyDialogue = Dialogue<State, InMemStorage<State>>;
#[derive(DialogueState, Clone)]
#[handler_out(anyhow::Result<()>)]
pub enum State {
#[handler(handle_start)]
Start,
#[handler(handle_receive_full_name)]
ReceiveFullName,
#[handler(handle_receive_age)]
ReceiveAge { full_name: String },
#[handler(handle_receive_location)]
ReceiveLocation { full_name: String, age: u8 },
}
impl Default for Dialogue {
impl Default for State {
fn default() -> Self {
Self::Start(StartState)
Self::Start
}
}
```
When a user sends a message to our bot and such a dialogue does not exist yet, a `Dialogue::default()` is invoked, which is a `Dialogue::Start` in this case. Every time a message is received, an associated dialogue is extracted and then passed to a corresponding subtransition function:
<details>
<summary>Dialogue::Start</summary>
([dialogue_bot/src/dialogue/states/start.rs](./examples/dialogue_bot/src/dialogue/states/start.rs))
```rust,ignore
// Imports are omitted...
pub struct StartState;
#[teloxide(subtransition)]
async fn start(
_state: StartState,
cx: TransitionIn<AutoSend<Bot>>,
_ans: String,
) -> TransitionOut<Dialogue> {
cx.answer("Let's start! What's your full name?").await?;
next(ReceiveFullNameState)
}
```
</details>
<details>
<summary>Dialogue::ReceiveFullName</summary>
([dialogue_bot/src/dialogue/states/receive_full_name.rs](./examples/dialogue_bot/src/dialogue/states/receive_full_name.rs))
```rust,ignore
// Imports are omitted...
#[derive(Generic)]
pub struct ReceiveFullNameState;
#[teloxide(subtransition)]
async fn receive_full_name(
state: ReceiveFullNameState,
cx: TransitionIn<AutoSend<Bot>>,
ans: String,
) -> TransitionOut<Dialogue> {
cx.answer("How old are you?").await?;
next(ReceiveAgeState::up(state, ans))
}
```
</details>
<details>
<summary>Dialogue::ReceiveAge</summary>
([dialogue_bot/src/dialogue/states/receive_age.rs](./examples/dialogue_bot/src/dialogue/states/receive_age.rs))
```rust,ignore
// Imports are omitted...
#[derive(Generic)]
pub struct ReceiveAgeState {
pub full_name: String,
}
#[teloxide(subtransition)]
async fn receive_age_state(
state: ReceiveAgeState,
cx: TransitionIn<AutoSend<Bot>>,
ans: String,
) -> TransitionOut<Dialogue> {
match ans.parse::<u8>() {
Ok(ans) => {
cx.answer("What's your location?").await?;
next(ReceiveLocationState::up(state, ans))
}
_ => {
cx.answer("Send me a number.").await?;
next(state)
}
}
}
```
</details>
<details>
<summary>Dialogue::ReceiveLocation</summary>
([dialogue_bot/src/dialogue/states/receive_location.rs](./examples/dialogue_bot/src/dialogue/states/receive_location.rs))
```rust,ignore
// Imports are omitted...
#[derive(Generic)]
pub struct ReceiveLocationState {
pub full_name: String,
pub age: u8,
}
#[teloxide(subtransition)]
async fn receive_location(
state: ReceiveLocationState,
cx: TransitionIn<AutoSend<Bot>>,
ans: String,
) -> TransitionOut<Dialogue> {
cx.answer(format!("Full name: {}\nAge: {}\nLocation: {}", state.full_name, state.age, ans))
.await?;
exit()
}
```
</details>
All these subtransition functions accept a corresponding state (one of the many variants of `Dialogue`), a context, and a textual message. They return `TransitionOut<Dialogue>`, e.g. a mapping from `<your state type>` to `Dialogue`.
Finally, the `main` function looks like this:
([dialogue_bot/src/main.rs](./examples/dialogue_bot/src/main.rs))
```rust,ignore
// Imports are omitted...
#[tokio::main]
async fn main() {
@ -314,23 +223,84 @@ async fn main() {
let bot = Bot::from_env().auto_send();
teloxide::dialogues_repl(bot, |message, dialogue| async move {
handle_message(message, dialogue).await.expect("Something wrong with the bot!")
})
Dispatcher::builder(
bot,
Update::filter_message()
.enter_dialogue::<Message, InMemStorage<State>, State>()
.dispatch_by::<State>(),
)
.dependencies(dptree::deps![InMemStorage::<State>::new()])
.build()
.setup_ctrlc_handler()
.dispatch()
.await;
}
async fn handle_message(
cx: UpdateWithCx<AutoSend<Bot>, Message>,
dialogue: Dialogue,
) -> TransitionOut<Dialogue> {
match cx.update.text().map(ToOwned::to_owned) {
None => {
cx.answer("Send me a text message.").await?;
next(dialogue)
async fn handle_start(
bot: AutoSend<Bot>,
msg: Message,
dialogue: MyDialogue,
) -> anyhow::Result<()> {
bot.send_message(msg.chat.id, "Let's start! What's your full name?").await?;
dialogue.update(State::ReceiveFullName).await?;
Ok(())
}
async fn handle_receive_full_name(
bot: AutoSend<Bot>,
msg: Message,
dialogue: MyDialogue,
) -> anyhow::Result<()> {
match msg.text() {
Some(text) => {
bot.send_message(msg.chat.id, "How old are you?").await?;
dialogue.update(State::ReceiveAge { full_name: text.into() }).await?;
}
None => {
bot.send_message(msg.chat.id, "Send me plain text.").await?;
}
Some(ans) => dialogue.react(cx, ans).await,
}
Ok(())
}
async fn handle_receive_age(
bot: AutoSend<Bot>,
msg: Message,
dialogue: MyDialogue,
(full_name,): (String,), // Available from `State::ReceiveAge`.
) -> anyhow::Result<()> {
match msg.text().map(|text| text.parse::<u8>()) {
Some(Ok(age)) => {
bot.send_message(msg.chat.id, "What's your location?").await?;
dialogue.update(State::ReceiveLocation { full_name, age }).await?;
}
_ => {
bot.send_message(msg.chat.id, "Send me a number.").await?;
}
}
Ok(())
}
async fn handle_receive_location(
bot: AutoSend<Bot>,
msg: Message,
dialogue: MyDialogue,
(full_name, age): (String, u8), // Available from `State::ReceiveLocation`.
) -> anyhow::Result<()> {
match msg.text() {
Some(location) => {
let message = format!("Full name: {}\nAge: {}\nLocation: {}", full_name, age, location);
bot.send_message(msg.chat.id, message).await?;
dialogue.exit().await?;
}
None => {
bot.send_message(msg.chat.id, "Send me plain text.").await?;
}
}
Ok(())
}
```
@ -340,60 +310,29 @@ async fn handle_message(
</kbd>
</div>
[More examples!](./examples)
## Recommendations
- Use this pattern:
```rust
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
// Your logic here...
}
```
Instead of this:
```rust
#[tokio::main]
async fn main() {
// Your logic here...
}
```
The second one produces very strange compiler messages due to the `#[tokio::main]` macro. However, the examples in this README use the second variant for brevity.
[More examples >>](examples/)
## FAQ
**Q: Where I can ask questions?**
A: [Issues](https://github.com/teloxide/teloxide/issues) is a good place for well-formed questions, for example, about:
A:
- the library design;
- enhancements;
- bug reports;
- ...
- [Issues] is a good place for well-formed questions about the library design, enhancements, and bug reports.
- [GitHub Discussions] is a place where you can ask us for help in a less formal manner.
- If you need quick help in real-time, you should ask a question in [our official Telegram group].
If you can't compile your bot due to compilation errors and need quick help, feel free to ask in [our official Telegram group](https://t.me/teloxide).
[Issues]: https://github.com/teloxide/teloxide/issues
[our official Telegram group]: https://t.me/teloxide
[GitHub Discussions]: https://github.com/teloxide/teloxide/discussions
**Q: Do you support the Telegram API for clients?**
A: No, only the bots API.
**Q: Why Rust?**
A: Most programming languages have their own implementations of Telegram bots frameworks, so why not Rust? We think Rust provides a good enough ecosystem and the language for it to be suitable for writing bots.
UPD: The current design relies on wide and deep trait bounds, thereby increasing cognitive complexity. It can be avoided using [mux-stream], but currently the stable Rust channel doesn't support necessary features to use [mux-stream] conveniently. Furthermore, the [mux-stream] could help to make a library out of teloxide, not a framework, since the design in this case could be defined by just combining streams of updates.
[mux-stream]: https://github.com/Hirrolot/mux-stream
**Q: Can I use webhooks?**
A: teloxide doesn't provide special API for working with webhooks due to their nature with lots of subtle settings. Instead, you should setup your webhook by yourself, as shown in [`examples/ngrok_ping_pong_bot`](./examples/ngrok_ping_pong_bot/src/main.rs) and [`examples/heroku_ping_pong_bot`](./examples/heroku_ping_pong_bot/src/main.rs).
A: teloxide doesn't provide special API for working with webhooks due to their nature with lots of subtle settings. Instead, you should setup your webhook by yourself, as shown in [`examples/ngrok_ping_pong_bot`](examples/ngrok_ping_pong_bot/src/main.rs) and [`examples/heroku_ping_pong_bot`](examples/heroku_ping_pong_bot/src/main.rs).
Associated links:
- [Marvin's Marvellous Guide to All Things Webhook](https://core.telegram.org/bots/webhooks)
@ -409,24 +348,29 @@ A: Yes. You can setup any logger, for example, [fern], e.g. teloxide has no spec
[`enable_logging_with_filter!`]: https://docs.rs/teloxide/latest/teloxide/macro.enable_logging_with_filter.html
## Community bots
Feel free to propose your own bot to our collection!
- [WaffleLapkin/crate_upd_bot](https://github.com/WaffleLapkin/crate_upd_bot) -- A bot that notifies about crate updates.
- [dracarys18/grpmr-rs](https://github.com/dracarys18/grpmr-rs) -- A telegram group manager bot with variety of extra features.
- [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.
- [Hermitter/tepe](https://github.com/Hermitter/tepe) -- A CLI to command a bot to send messages and files over Telegram.
- [dracarys18/grpmr-rs](https://github.com/dracarys18/grpmr-rs) -- A Telegram group manager bot with variety of extra features.
- [steadylearner/subreddit_reader](https://github.com/steadylearner/Rust-Full-Stack/tree/master/commits/teloxide/subreddit_reader) -- A bot that shows the latest posts at Rust subreddit.
- [myblackbeard/basketball-betting-bot](https://github.com/myblackbeard/basketball-betting-bot) -- The bot lets you bet on NBA games against your buddies.
- [ArtHome12/vzmuinebot](https://github.com/ArtHome12/vzmuinebot) -- Telegram bot for food menu navigate.
- [ArtHome12/cognito_bot](https://github.com/ArtHome12/cognito_bot) -- The bot is designed to anonymize messages to a group.
- [Hermitter/tepe](https://github.com/Hermitter/tepe) -- A CLI to command a bot to send messages and files over Telegram.
- [pro-vim/tg-vimhelpbot](https://github.com/pro-vim/tg-vimhelpbot) -- Link `:help` for Vim in Telegram.
- [sschiz/janitor-bot](https://github.com/sschiz/janitor-bot) -- A bot that removes users trying to join to a chat that is designed for comments.
- [myblackbeard/basketball-betting-bot](https://github.com/myblackbeard/basketball-betting-bot) -- The bot lets you bet on NBA games against your buddies.
- [slondr/BeerHolderBot](https://gitlab.com/slondr/BeerHolderBot) -- A bot that holds your beer.
- [mxseev/logram](https://github.com/mxseev/logram) -- Utility that takes logs from anywhere and sends them to Telegram.
- [msfjarvis/walls-bot-rs](https://github.com/msfjarvis/walls-bot-rs) -- Telegram bot for my wallpapers collection, in Rust.
- [MustafaSalih1993/Miss-Vodka-Telegram-Bot](https://github.com/MustafaSalih1993/Miss-Vodka-Telegram-Bot) -- A telegram bot written in rust using "Teloxide" library.
- [MustafaSalih1993/Miss-Vodka-Telegram-Bot](https://github.com/MustafaSalih1993/Miss-Vodka-Telegram-Bot) -- A Telegram bot written in rust using "Teloxide" library.
- [x13a/tg-prompt](https://github.com/x13a/tg-prompt) -- Telegram prompt.
- [magnickolas/remindee-bot](https://github.com/magnickolas/remindee-bot) -- Telegram bot for managing reminders.
- [cyberknight777/knight-bot](https://gitlab.com/cyberknight777/knight-bot) -- A telegram bot with variety of fun features.
- [cyberknight777/knight-bot](https://gitlab.com/cyberknight777/knight-bot) -- A Telegram bot with variety of fun features.
- [wa7sa34cx/the-black-box-bot](https://github.com/wa7sa34cx/the-black-box-bot) -- This is the Black Box Telegram bot. You can hold any items in it.
- [crapstone/hsctt](https://codeberg.org/crapstones-bots/hsctt) -- A Telegram bot that searches for HTTP status codes in all messages and replies with the text form.
- [alenpaul2001/AurSearchBot](https://gitlab.com/alenpaul2001/aursearchbot) -- Telegram bot for searching AUR in inline mode.
## Contributing
See [CONRIBUTING.md](https://github.com/teloxide/teloxide/blob/master/CONTRIBUTING.md).
See [`CONRIBUTING.md`](CONTRIBUTING.md).

View file

@ -1,12 +1,7 @@
# Examples
Just enter the directory (for example, `cd dialogue_bot`) and execute `cargo run` to run an example. Don't forget to initialise the `TELOXIDE_TOKEN` environmental variable.
| Bot | Description |
|---|-----------|
| [dices_bot](dices_bot) | Throws a dice on each incoming message. |
| [ngrok_ping_pong_bot](ngrok_ping_pong_bot) | The ngrok version of ping-pong-bot that uses webhooks. |
| [heroku_ping_pong_bot](heroku_ping_pong_bot) | The Heroku version of ping-pong-bot that uses webhooks. |
| [simple_commands_bot](simple_commands_bot) | Shows how to deal with bot's commands. |
| [redis_remember_bot](redis_remember_bot) | Uses `RedisStorage` instead of `InMemStorage`. |
| [dialogue_bot](dialogue_bot) | How to deal with dialogues. |
| [admin_bot](admin_bot) | Ban, kick, and mute on a command. |
| [shared_state_bot](shared_state_bot) | How to deal with shared state. |
# Usage
```
$ cargo run --example <example name> --features="<features required by the example>"
```
Don't forget to initialise the `TELOXIDE_TOKEN` environmental variable.

153
examples/admin.rs Normal file
View file

@ -0,0 +1,153 @@
use std::{error::Error, str::FromStr};
use chrono::Duration;
use teloxide::{prelude2::*, types::ChatPermissions, utils::command::BotCommand};
// Derive BotCommand to parse text with a command into this enumeration.
//
// 1. rename = "lowercase" turns all the commands into lowercase letters.
// 2. `description = "..."` specifies a text before all the commands.
//
// That is, you can just call Command::descriptions() to get a description of
// your commands in this format:
// %GENERAL-DESCRIPTION%
// %PREFIX%%COMMAND% - %DESCRIPTION%
#[derive(BotCommand, Clone)]
#[command(
rename = "lowercase",
description = "Use commands in format /%command% %num% %unit%",
parse_with = "split"
)]
enum Command {
#[command(description = "kick user from chat.")]
Kick,
#[command(description = "ban user in chat.")]
Ban {
time: u64,
unit: UnitOfTime,
},
#[command(description = "mute user in chat.")]
Mute {
time: u64,
unit: UnitOfTime,
},
Help,
}
#[derive(Clone)]
enum UnitOfTime {
Seconds,
Minutes,
Hours,
}
impl FromStr for UnitOfTime {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
match s {
"h" | "hours" => Ok(UnitOfTime::Hours),
"m" | "minutes" => Ok(UnitOfTime::Minutes),
"s" | "seconds" => Ok(UnitOfTime::Seconds),
_ => Err("Allowed units: h, m, s"),
}
}
}
// Calculates time of user restriction.
fn calc_restrict_time(time: u64, unit: UnitOfTime) -> Duration {
match unit {
UnitOfTime::Hours => Duration::hours(time as i64),
UnitOfTime::Minutes => Duration::minutes(time as i64),
UnitOfTime::Seconds => Duration::seconds(time as i64),
}
}
type Bot = AutoSend<teloxide::Bot>;
// Kick a user with a replied message.
async fn kick_user(bot: Bot, msg: Message) -> Result<(), Box<dyn Error + Send + Sync>> {
match msg.reply_to_message() {
Some(replied) => {
// bot.unban_chat_member can also kicks a user from a group chat.
bot.unban_chat_member(msg.chat.id, replied.from().unwrap().id).await?;
}
None => {
bot.send_message(msg.chat.id, "Use this command in reply to another message").await?;
}
}
Ok(())
}
// Mute a user with a replied message.
async fn mute_user(
bot: Bot,
msg: Message,
time: Duration,
) -> Result<(), Box<dyn Error + Send + Sync>> {
match msg.reply_to_message() {
Some(replied) => {
bot.restrict_chat_member(
msg.chat.id,
replied.from().expect("Must be MessageKind::Common").id,
ChatPermissions::empty(),
)
.until_date(msg.date + time)
.await?;
}
None => {
bot.send_message(msg.chat.id, "Use this command in a reply to another message!")
.await?;
}
}
Ok(())
}
// Ban a user with replied message.
async fn ban_user(
bot: Bot,
msg: Message,
time: Duration,
) -> Result<(), Box<dyn Error + Send + Sync>> {
match msg.reply_to_message() {
Some(replied) => {
bot.kick_chat_member(
msg.chat.id,
replied.from().expect("Must be MessageKind::Common").id,
)
.until_date(msg.date + time)
.await?;
}
None => {
bot.send_message(msg.chat.id, "Use this command in a reply to another message!")
.await?;
}
}
Ok(())
}
async fn action(
bot: Bot,
msg: Message,
command: Command,
) -> Result<(), Box<dyn Error + Send + Sync>> {
match command {
Command::Help => {
bot.send_message(msg.chat.id, Command::descriptions()).await?;
}
Command::Kick => kick_user(bot, msg).await?,
Command::Ban { time, unit } => ban_user(bot, msg, calc_restrict_time(time, unit)).await?,
Command::Mute { time, unit } => mute_user(bot, msg, calc_restrict_time(time, unit)).await?,
};
Ok(())
}
#[tokio::main]
async fn main() {
teloxide::enable_logging!();
log::info!("Starting admin_bot...");
let bot = teloxide::Bot::from_env().auto_send();
teloxide::repls2::commands_repl(bot, action, Command::ty()).await;
}

View file

@ -1,17 +0,0 @@
[package]
name = "admin_bot"
version = "0.1.0"
authors = ["p0lunin <dmytro.polunin@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
teloxide = { path = "../../", features = ["macros", "auto-send"] }
log = "0.4.8"
pretty_env_logger = "0.4.0"
tokio = { version = "1.3.0", features = ["rt-multi-thread", "macros"] }
chrono = "0.4"
[profile.release]
lto = true

View file

@ -1,157 +0,0 @@
use std::{error::Error, str::FromStr};
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
use teloxide::{prelude::*, types::{ChatPermissions, Me}, utils::command::BotCommand};
// Derive BotCommand to parse text with a command into this enumeration.
//
// 1. rename = "lowercase" turns all the commands into lowercase letters.
// 2. `description = "..."` specifies a text before all the commands.
//
// That is, you can just call Command::descriptions() to get a description of
// your commands in this format:
// %GENERAL-DESCRIPTION%
// %PREFIX%%COMMAND% - %DESCRIPTION%
#[derive(BotCommand)]
#[command(
rename = "lowercase",
description = "Use commands in format /%command% %num% %unit%",
parse_with = "split"
)]
enum Command {
#[command(description = "kick user from chat.")]
Kick,
#[command(description = "ban user in chat.")]
Ban {
time: u64,
unit: UnitOfTime,
},
#[command(description = "mute user in chat.")]
Mute {
time: u64,
unit: UnitOfTime,
},
Help,
}
enum UnitOfTime {
Seconds,
Minutes,
Hours,
}
impl FromStr for UnitOfTime {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
match s {
"h" | "hours" => Ok(UnitOfTime::Hours),
"m" | "minutes" => Ok(UnitOfTime::Minutes),
"s" | "seconds" => Ok(UnitOfTime::Seconds),
_ => Err("Allowed units: h, m, s"),
}
}
}
// Calculates time of user restriction.
fn calc_restrict_time(time: u64, unit: UnitOfTime) -> Duration {
match unit {
UnitOfTime::Hours => Duration::hours(time as i64),
UnitOfTime::Minutes => Duration::minutes(time as i64),
UnitOfTime::Seconds => Duration::seconds(time as i64),
}
}
type Cx = UpdateWithCx<AutoSend<Bot>, Message>;
// Mute a user with a replied message.
async fn mute_user(cx: &Cx, time: Duration) -> Result<(), Box<dyn Error + Send + Sync>> {
match cx.update.reply_to_message() {
Some(msg1) => {
cx.requester
.restrict_chat_member(
cx.update.chat_id(),
msg1.from().expect("Must be MessageKind::Common").id,
ChatPermissions::default(),
)
.until_date(
DateTime::<Utc>::from_utc(
NaiveDateTime::from_timestamp(cx.update.date as i64, 0),
Utc,
) + time,
)
.await?;
}
None => {
cx.reply_to("Use this command in reply to another message").send().await?;
}
}
Ok(())
}
// Kick a user with a replied message.
async fn kick_user(cx: &Cx) -> Result<(), Box<dyn Error + Send + Sync>> {
match cx.update.reply_to_message() {
Some(mes) => {
// bot.unban_chat_member can also kicks a user from a group chat.
cx.requester
.unban_chat_member(cx.update.chat_id(), mes.from().unwrap().id)
.send()
.await?;
}
None => {
cx.reply_to("Use this command in reply to another message").send().await?;
}
}
Ok(())
}
// Ban a user with replied message.
async fn ban_user(cx: &Cx, time: Duration) -> Result<(), Box<dyn Error + Send + Sync>> {
match cx.update.reply_to_message() {
Some(message) => {
cx.requester
.kick_chat_member(
cx.update.chat_id(),
message.from().expect("Must be MessageKind::Common").id,
)
.until_date(
DateTime::<Utc>::from_utc(
NaiveDateTime::from_timestamp(cx.update.date as i64, 0),
Utc,
) + time,
)
.await?;
}
None => {
cx.reply_to("Use this command in a reply to another message!").send().await?;
}
}
Ok(())
}
async fn action(cx: Cx, command: Command) -> Result<(), Box<dyn Error + Send + Sync>> {
match command {
Command::Help => cx.answer(Command::descriptions()).send().await.map(|_| ())?,
Command::Kick => kick_user(&cx).await?,
Command::Ban { time, unit } => ban_user(&cx, calc_restrict_time(time, unit)).await?,
Command::Mute { time, unit } => mute_user(&cx, calc_restrict_time(time, unit)).await?,
};
Ok(())
}
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting admin_bot...");
let bot = Bot::from_env().auto_send();
let Me { user: bot_user, .. } = bot.get_me().await.unwrap();
let bot_name = bot_user.username.expect("Bots must have usernames");
teloxide::commands_repl(bot, bot_name, action).await;
}

132
examples/buttons.rs Normal file
View file

@ -0,0 +1,132 @@
use std::error::Error;
use teloxide::{
payloads::SendMessageSetters,
prelude2::*,
types::{
InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultArticle, InputMessageContent,
InputMessageContentText,
},
utils::command::BotCommand,
};
#[derive(BotCommand)]
#[command(rename = "lowercase", description = "These commands are supported:")]
enum Command {
#[command(description = "Display this text")]
Help,
#[command(description = "Start")]
Start,
}
/// Creates a keyboard made by buttons in a big column.
fn make_keyboard() -> InlineKeyboardMarkup {
let mut keyboard: Vec<Vec<InlineKeyboardButton>> = vec![];
let debian_versions = [
"Buzz", "Rex", "Bo", "Hamm", "Slink", "Potato", "Woody", "Sarge", "Etch", "Lenny",
"Squeeze", "Wheezy", "Jessie", "Stretch", "Buster", "Bullseye",
];
for versions in debian_versions.chunks(3) {
let row = versions
.iter()
.map(|&version| InlineKeyboardButton::callback(version.to_owned(), version.to_owned()))
.collect();
keyboard.push(row);
}
InlineKeyboardMarkup::new(keyboard)
}
/// Parse the text wrote on Telegram and check if that text is a valid command
/// 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>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
if let Some(text) = m.text() {
match BotCommand::parse(text, "buttons") {
Ok(Command::Help) => {
// Just send the description of all commands.
bot.send_message(m.chat.id, Command::descriptions()).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?;
}
Err(_) => {
bot.send_message(m.chat.id, "Command not found!").await?;
}
}
}
Ok(())
}
async fn inline_query_handler(
q: InlineQuery,
bot: AutoSend<Bot>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let choose_debian_version = InlineQueryResultArticle::new(
"0",
"Chose debian version",
InputMessageContent::Text(InputMessageContentText::new("Debian versions:")),
)
.reply_markup(make_keyboard());
bot.answer_inline_query(q.id, vec![choose_debian_version.into()]).await?;
Ok(())
}
/// When it receives a callback from a button it edits the message with all
/// those buttons writing a text with the selected Debian version.
///
/// **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<Bot>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
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?;
}
}
}
log::info!("You chose: {}", version);
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
teloxide::enable_logging!();
log::info!("Starting bot...");
let bot = Bot::from_env().auto_send();
let handler = dptree::entry()
.branch(Update::filter_message().endpoint(message_handler))
.branch(Update::filter_callback_query().endpoint(callback_handler))
.branch(Update::filter_inline_query().endpoint(inline_query_handler));
Dispatcher::builder(bot, handler).build().setup_ctrlc_handler().dispatch().await;
log::info!("Closing bot... Goodbye!");
Ok(())
}

127
examples/dialogue.rs Normal file
View file

@ -0,0 +1,127 @@
// This is a bot that asks you three questions, e.g. a simple test.
//
// # Example
// ```
// - Hey
// - Let's start! What's your full name?
// - Gandalf the Grey
// - How old are you?
// - 223
// - What's your location?
// - Middle-earth
// - Full name: Gandalf the Grey
// Age: 223
// Location: Middle-earth
// ```
use teloxide::{dispatching2::dialogue::InMemStorage, macros::DialogueState, prelude2::*};
type MyDialogue = Dialogue<State, InMemStorage<State>>;
#[derive(DialogueState, Clone)]
#[handler_out(anyhow::Result<()>)]
pub enum State {
#[handler(handle_start)]
Start,
#[handler(handle_receive_full_name)]
ReceiveFullName,
#[handler(handle_receive_age)]
ReceiveAge { full_name: String },
#[handler(handle_receive_location)]
ReceiveLocation { full_name: String, age: u8 },
}
impl Default for State {
fn default() -> Self {
Self::Start
}
}
#[tokio::main]
async fn main() {
teloxide::enable_logging!();
log::info!("Starting dialogue_bot...");
let bot = Bot::from_env().auto_send();
Dispatcher::builder(
bot,
Update::filter_message()
.enter_dialogue::<Message, InMemStorage<State>, State>()
.dispatch_by::<State>(),
)
.dependencies(dptree::deps![InMemStorage::<State>::new()])
.build()
.setup_ctrlc_handler()
.dispatch()
.await;
}
async fn handle_start(
bot: AutoSend<Bot>,
msg: Message,
dialogue: MyDialogue,
) -> anyhow::Result<()> {
bot.send_message(msg.chat.id, "Let's start! What's your full name?").await?;
dialogue.update(State::ReceiveFullName).await?;
Ok(())
}
async fn handle_receive_full_name(
bot: AutoSend<Bot>,
msg: Message,
dialogue: MyDialogue,
) -> anyhow::Result<()> {
match msg.text() {
Some(text) => {
bot.send_message(msg.chat.id, "How old are you?").await?;
dialogue.update(State::ReceiveAge { full_name: text.into() }).await?;
}
None => {
bot.send_message(msg.chat.id, "Send me plain text.").await?;
}
}
Ok(())
}
async fn handle_receive_age(
bot: AutoSend<Bot>,
msg: Message,
dialogue: MyDialogue,
(full_name,): (String,), // Available from `State::ReceiveAge`.
) -> anyhow::Result<()> {
match msg.text().map(|text| text.parse::<u8>()) {
Some(Ok(age)) => {
bot.send_message(msg.chat.id, "What's your location?").await?;
dialogue.update(State::ReceiveLocation { full_name, age }).await?;
}
_ => {
bot.send_message(msg.chat.id, "Send me a number.").await?;
}
}
Ok(())
}
async fn handle_receive_location(
bot: AutoSend<Bot>,
msg: Message,
dialogue: MyDialogue,
(full_name, age): (String, u8), // Available from `State::ReceiveLocation`.
) -> anyhow::Result<()> {
match msg.text() {
Some(location) => {
let message = format!("Full name: {}\nAge: {}\nLocation: {}", full_name, age, location);
bot.send_message(msg.chat.id, message).await?;
dialogue.exit().await?;
}
None => {
bot.send_message(msg.chat.id, "Send me plain text.").await?;
}
}
Ok(())
}

View file

@ -1,23 +0,0 @@
[package]
name = "dialogue_bot"
version = "0.1.0"
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
teloxide = { path = "../../", features = ["frunk", "macros", "auto-send"] }
futures = "0.3.5"
tokio = { version = "1.3.0", features = ["rt-multi-thread", "macros"] }
log = "0.4.8"
pretty_env_logger = "0.4.0"
derive_more = "0.99.9"
frunk = "0.4"
frunk_core = "0.4"
[profile.release]
lto = true

View file

@ -1,21 +0,0 @@
mod states;
use crate::dialogue::states::{
ReceiveAgeState, ReceiveFullNameState, ReceiveLocationState, StartState,
};
use derive_more::From;
use teloxide::macros::Transition;
#[derive(Transition, Clone, From)]
pub enum Dialogue {
Start(StartState),
ReceiveFullName(ReceiveFullNameState),
ReceiveAge(ReceiveAgeState),
ReceiveLocation(ReceiveLocationState),
}
impl Default for Dialogue {
fn default() -> Self {
Self::Start(StartState)
}
}

View file

@ -1,9 +0,0 @@
mod receive_age;
mod receive_full_name;
mod receive_location;
mod start;
pub use receive_age::ReceiveAgeState;
pub use receive_full_name::ReceiveFullNameState;
pub use receive_location::ReceiveLocationState;
pub use start::StartState;

View file

@ -1,25 +0,0 @@
use crate::dialogue::{states::receive_location::ReceiveLocationState, Dialogue};
use teloxide::prelude::*;
#[derive(Clone, Generic)]
pub struct ReceiveAgeState {
pub full_name: String,
}
#[teloxide(subtransition)]
async fn receive_age_state(
state: ReceiveAgeState,
cx: TransitionIn<AutoSend<Bot>>,
ans: String,
) -> TransitionOut<Dialogue> {
match ans.parse::<u8>() {
Ok(ans) => {
cx.answer("What's your location?").await?;
next(ReceiveLocationState::up(state, ans))
}
_ => {
cx.answer("Send me a number.").await?;
next(state)
}
}
}

View file

@ -1,15 +0,0 @@
use crate::dialogue::{states::receive_age::ReceiveAgeState, Dialogue};
use teloxide::prelude::*;
#[derive(Clone, Generic)]
pub struct ReceiveFullNameState;
#[teloxide(subtransition)]
async fn receive_full_name(
state: ReceiveFullNameState,
cx: TransitionIn<AutoSend<Bot>>,
ans: String,
) -> TransitionOut<Dialogue> {
cx.answer("How old are you?").await?;
next(ReceiveAgeState::up(state, ans))
}

View file

@ -1,19 +0,0 @@
use crate::dialogue::Dialogue;
use teloxide::prelude::*;
#[derive(Clone, Generic)]
pub struct ReceiveLocationState {
pub full_name: String,
pub age: u8,
}
#[teloxide(subtransition)]
async fn receive_location(
state: ReceiveLocationState,
cx: TransitionIn<AutoSend<Bot>>,
ans: String,
) -> TransitionOut<Dialogue> {
cx.answer(format!("Full name: {}\nAge: {}\nLocation: {}", state.full_name, state.age, ans))
.await?;
exit()
}

View file

@ -1,15 +0,0 @@
use crate::dialogue::{states::ReceiveFullNameState, Dialogue};
use teloxide::prelude::*;
#[derive(Clone)]
pub struct StartState;
#[teloxide(subtransition)]
async fn start(
_state: StartState,
cx: TransitionIn<AutoSend<Bot>>,
_ans: String,
) -> TransitionOut<Dialogue> {
cx.answer("Let's start! What's your full name?").await?;
next(ReceiveFullNameState)
}

View file

@ -1,56 +0,0 @@
// This is a bot that asks you three questions, e.g. a simple test.
//
// # Example
// ```
// - Hey
// - Let's start! What's your full name?
// - Gandalf the Grey
// - How old are you?
// - 223
// - What's your location?
// - Middle-earth
// - Full name: Gandalf the Grey
// Age: 223
// Location: Middle-earth
// ```
#![allow(clippy::trivial_regex)]
#![allow(dead_code)]
#[macro_use]
extern crate frunk;
mod dialogue;
use crate::dialogue::Dialogue;
use teloxide::prelude::*;
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting dialogue_bot...");
let bot = Bot::from_env().auto_send();
teloxide::dialogues_repl(bot, |message, dialogue| async move {
handle_message(message, dialogue).await.expect("Something wrong with the bot!")
})
.await;
}
async fn handle_message(
cx: UpdateWithCx<AutoSend<Bot>, Message>,
dialogue: Dialogue,
) -> TransitionOut<Dialogue> {
match cx.update.text().map(ToOwned::to_owned) {
None => {
cx.answer("Send me a text message.").await?;
next(dialogue)
}
Some(ans) => dialogue.react(cx, ans).await,
}
}

View file

@ -1,20 +1,16 @@
// This bot throws a dice on each incoming message.
use teloxide::prelude::*;
use teloxide::prelude2::*;
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting dices_bot...");
let bot = Bot::from_env().auto_send();
teloxide::repl(bot, |message| async move {
message.answer_dice().await?;
teloxide::repls2::repl(bot, |message: Message, bot: AutoSend<Bot>| async move {
bot.send_dice(message.chat.id).await?;
respond(())
})
.await;

View file

@ -1,16 +0,0 @@
[package]
name = "dices_bot"
version = "0.1.0"
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
teloxide = { path = "../../", features = ["auto-send"] }
log = "0.4.8"
pretty_env_logger = "0.4.0"
tokio = { version = "1.3.0", features = ["rt-multi-thread", "macros"] }
[profile.release]
lto = true

View file

@ -0,0 +1,153 @@
// This example provide a quick overview of the new features in the
// `dispatching2` module.
use rand::Rng;
// You need to import `prelude2` because `prelude` contains items from the old
// dispatching system, which will be deprecated in the future.
use teloxide::{
prelude2::*,
types::{Dice, Update},
utils::command::BotCommand,
};
#[tokio::main]
async fn main() {
teloxide::enable_logging!();
log::info!("Starting dispatching2_features_bot...");
let bot = Bot::from_env().auto_send();
let parameters = ConfigParameters {
bot_maintainer: 268486177, // Paste your ID to run this bot.
maintainer_username: None,
};
let handler = Update::filter_message()
// You can use branching to define multiple ways in which an update will be handled. If the
// first branch fails, an update will be passed to the second branch, and so on.
.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<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(())
}),
)
.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<Bot>| async move {
bot.send_message(msg.chat.id, format!("Dice value: {}", dice.value))
.reply_to_message_id(msg.id)
.await?;
Ok(())
},
),
)
.branch(
dptree::entry()
// Filter commands: the next handlers will receive a parsed `SimpleCommand`.
.filter_command::<SimpleCommand>()
// If a command parsing fails, this handler will not be executed.
.endpoint(simple_commands_handler),
)
.branch(
// Filter a maintainer by a used ID.
dptree::filter(|msg: Message, cfg: ConfigParameters| {
msg.from().map(|user| user.id == cfg.bot_maintainer).unwrap_or_default()
})
.filter_command::<MaintainerCommands>()
.endpoint(
|msg: Message, bot: AutoSend<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(())
}
}
},
),
);
Dispatcher::builder(bot, handler)
// Here you specify initial dependencies that all handlers will receive; they can be
// database connections, configurations, and other auxiliary arguments. It is similar to
// `actix_web::Extensions`.
.dependencies(dptree::deps![parameters])
// If no handler succeeded to handle an update, this closure will be called.
.default_handler(|upd| async move {
log::warn!("Unhandled update: {:?}", upd);
})
// If the dispatcher fails for some reason, execute this handler.
.error_handler(LoggingErrorHandler::with_custom_text(
"An error has occurred in the dispatcher",
))
.build()
.setup_ctrlc_handler()
.dispatch()
.await;
}
#[derive(Clone)]
struct ConfigParameters {
bot_maintainer: i64,
maintainer_username: Option<String>,
}
#[derive(BotCommand, Clone)]
#[command(rename = "lowercase", description = "Simple commands")]
enum SimpleCommand {
#[command(description = "shows this message.")]
Help,
#[command(description = "shows maintainer info.")]
Maintainer,
#[command(description = "shows your ID.")]
MyId,
}
#[derive(BotCommand, Clone)]
#[command(rename = "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<Bot>,
cmd: SimpleCommand,
cfg: ConfigParameters,
) -> Result<(), teloxide::RequestError> {
let text = match cmd {
SimpleCommand::Help => {
if msg.from().unwrap().id == cfg.bot_maintainer {
format!("{}\n{}", SimpleCommand::descriptions(), MaintainerCommands::descriptions())
} else {
SimpleCommand::descriptions()
}
}
SimpleCommand::Maintainer => {
if msg.from().unwrap().id == cfg.bot_maintainer {
"Maintainer is you!".into()
} else if let Some(username) = cfg.maintainer_username {
format!("Maintainer is @{}", username)
} else {
format!("Maintainer ID is {}", cfg.bot_maintainer)
}
}
SimpleCommand::MyId => {
format!("{}", msg.from().unwrap().id)
}
};
bot.send_message(msg.chat.id, text).await?;
Ok(())
}

View file

@ -1,7 +1,31 @@
// The version of Heroku ping-pong-bot, which uses a webhook to receive updates
// from Telegram, instead of long polling.
//
// You will need to configure the buildpack for heroku. We will be using Heroku
// rust buildpack [1]. Configuration was done by using heroku CLI.
//
// If you're creating a new Heroku application, run this:
//
// ```
// heroku create --buildpack emk/rust
// ```
//
// To set buildpack for existing applicaton:
//
// ```
// heroku buildpacks:set emk/rust
// ```
//
// [1] https://github.com/emk/heroku-buildpack-rust
use teloxide::{dispatching::{update_listeners::{self, StatefulListener}, stop_token::AsyncStopToken}, prelude::*, types::Update};
use teloxide::{
dispatching::{
stop_token::AsyncStopToken,
update_listeners::{self, StatefulListener},
},
prelude2::*,
types::Update,
};
use std::{convert::Infallible, env, net::SocketAddr};
use tokio::sync::mpsc;
@ -12,7 +36,20 @@ use reqwest::{StatusCode, Url};
#[tokio::main]
async fn main() {
run().await;
teloxide::enable_logging!();
log::info!("Starting heroku_ping_pong_bot...");
let bot = Bot::from_env().auto_send();
teloxide::repls2::repl_with_listener(
bot.clone(),
|msg: Message, bot: AutoSend<Bot>| async move {
bot.send_message(msg.chat.id, "pong").await?;
respond(())
},
webhook(bot).await,
)
.await;
}
async fn handle_rejection(error: warp::Rejection) -> Result<impl warp::Reply, Infallible> {
@ -39,10 +76,8 @@ pub async fn webhook(bot: AutoSend<Bot>) -> impl update_listeners::UpdateListene
let server = warp::post()
.and(warp::path(path))
.and(warp::body::json())
.map(move |json: serde_json::Value| {
if let Ok(update) = Update::try_parse(&json) {
tx.send(Ok(update)).expect("Cannot send an incoming update from the webhook")
}
.map(move |update: Update| {
tx.send(Ok(update)).expect("Cannot send an incoming update from the webhook");
StatusCode::OK
})
@ -60,25 +95,11 @@ pub async fn webhook(bot: AutoSend<Bot>) -> impl update_listeners::UpdateListene
tokio::spawn(fut);
let stream = UnboundedReceiverStream::new(rx);
fn streamf<S, T>(state: &mut (S, T)) -> &mut S { &mut state.0 }
StatefulListener::new((stream, stop_token), streamf, |state: &mut (_, AsyncStopToken)| state.1.clone())
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting heroku_ping_pong_bot...");
let bot = Bot::from_env().auto_send();
let cloned_bot = bot.clone();
teloxide::repl_with_listener(
bot,
|message| async move {
message.answer("pong").await?;
respond(())
},
webhook(cloned_bot).await,
)
.await;
fn streamf<S, T>(state: &mut (S, T)) -> &mut S {
&mut state.0
}
StatefulListener::new((stream, stop_token), streamf, |state: &mut (_, AsyncStopToken)| {
state.1.clone()
})
}

View file

@ -1,19 +0,0 @@
[package]
name = "heroku_ping_pong_bot"
version = "0.1.0"
authors = ["Pedro Lopes <ordepi@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
teloxide = { path = "../../", features = ["auto-send"] }
log = "0.4.8"
pretty_env_logger = "0.4.0"
tokio = { version = "1.3.0", features = ["rt-multi-thread", "macros"] }
tokio-stream = "0.1.4"
# Used to setup a webhook
warp = "0.3.0"
reqwest = "0.10.4"
serde_json = "1.0.50"

View file

@ -1,15 +0,0 @@
# Heroku example
This is an example project on how to deploy `webhook_ping_pong_bot` to heroku.
You will need to configure the buildpack for heroku. We will be using [Heroku rust buildpack](https://github.com/emk/heroku-buildpack-rust). Configuration was done by using `heroku` CLI.
If you're creating a new Heroku application, run this command inside example
```
heroku create --buildpack emk/rust
```
To set buildpack for existing applicaton:
```
heroku buildpacks:set emk/rust
```

64
examples/inline.rs Normal file
View file

@ -0,0 +1,64 @@
use teloxide::{
prelude2::*,
types::{
InlineQueryResult, InlineQueryResultArticle, InputMessageContent, InputMessageContentText,
},
Bot,
};
#[tokio::main]
async fn main() {
teloxide::enable_logging!();
log::info!("Starting inline_bot...");
let bot = Bot::from_env().auto_send();
let handler = Update::filter_inline_query().branch(dptree::endpoint(
|query: InlineQuery, bot: AutoSend<Bot>| 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
// items. These can be whatever, as long as they don't
// conflict.
"01".to_string(),
// What the user will actually see
"Google Search",
// What message will be sent when clicked/tapped
InputMessageContent::Text(InputMessageContentText::new(format!(
"https://www.google.com/search?q={}",
query.query,
))),
);
// While constructing them from the struct itself is possible, it is preferred
// to use the builder pattern if you wish to add more
// information to your result. Please refer to the documentation
// for more detailed information about each field. https://docs.rs/teloxide/latest/teloxide/types/struct.InlineQueryResultArticle.html
let ddg_search = InlineQueryResultArticle::new(
"02".to_string(),
"DuckDuckGo Search".to_string(),
InputMessageContent::Text(InputMessageContentText::new(format!(
"https://duckduckgo.com/?q={}",
query.query
))),
)
.description("DuckDuckGo Search")
.thumb_url("https://duckduckgo.com/assets/logo_header.v108.png".parse().unwrap())
.url("https://duckduckgo.com/about".parse().unwrap()); // Note: This is the url that will open if they click the thumbnail
let results = vec![
InlineQueryResult::Article(google_search),
InlineQueryResult::Article(ddg_search),
];
// 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;
if let Err(err) = response {
log::error!("Error in handler: {:?}", err);
}
respond(())
},
));
Dispatcher::builder(bot, handler).build().setup_ctrlc_handler().dispatch().await;
}

View file

@ -1,14 +0,0 @@
[package]
name = "inline_bot"
version = "0.1.0"
authors = ["Colin Diener <colin@colind.me>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
teloxide = { path = "../../", features = ["macros", "auto-send"] }
log = "0.4.8"
pretty_env_logger = "0.4.0"
tokio = { version = "1.3.0", features = ["rt-multi-thread", "macros"] }
tokio-stream = "0.1.3"

View file

@ -1,65 +0,0 @@
use teloxide::{
prelude::*,
types::{
InlineQueryResult, InlineQueryResultArticle, InputMessageContent, InputMessageContentText,
},
Bot,
};
use tokio_stream::wrappers::UnboundedReceiverStream;
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
let bot = Bot::from_env().auto_send();
// Create a new dispatcher to handle incoming queries
Dispatcher::new(bot)
.inline_queries_handler(|rx: DispatcherHandlerRx<AutoSend<Bot>, InlineQuery>| {
UnboundedReceiverStream::new(rx).for_each_concurrent(None, |query| 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 items.
// These can be whatever, as long as they don't conflict.
"01".to_string(),
// What the user will actually see
"Google Search",
// What message will be sent when clicked/tapped
InputMessageContent::Text(InputMessageContentText::new(format!(
"https://www.google.com/search?q={}",
query.update.query,
))),
);
// While constructing them from the struct itself is possible, it is preferred to use
// the builder pattern if you wish to add more information to your result.
// Please refer to the documentation for more detailed information about each field.
// https://docs.rs/teloxide/latest/teloxide/types/struct.InlineQueryResultArticle.html
let ddg_search = InlineQueryResultArticle::new(
"02".to_string(),
"DuckDuckGo Search".to_string(),
InputMessageContent::Text(InputMessageContentText::new(format!(
"https://duckduckgo.com/?q={}",
query.update.query.to_string()
))),
)
.description("DuckDuckGo Search")
.thumb_url("https://duckduckgo.com/assets/logo_header.v108.png")
.url("https://duckduckgo.com/about"); // Note: This is the url that will open if they click the thumbnail
let results = vec![
InlineQueryResult::Article(google_search),
InlineQueryResult::Article(ddg_search),
];
// Send it off! One thing to note -- the ID we use here must be of the query we're responding to.
let response =
query.requester.answer_inline_query(&query.update.id, results).send().await;
if let Err(err) = response {
log::error!("Error in handler: {:?}", err);
}
})
})
.dispatch()
.await;
}

View file

@ -1,7 +1,14 @@
// The version of ngrok ping-pong-bot, which uses a webhook to receive updates
// from Telegram, instead of long polling.
use teloxide::{dispatching::{update_listeners::{self, StatefulListener}, stop_token::AsyncStopToken}, prelude::*, types::Update};
use teloxide::{
dispatching::{
stop_token::AsyncStopToken,
update_listeners::{self, StatefulListener},
},
prelude2::*,
types::Update,
};
use std::{convert::Infallible, net::SocketAddr};
use tokio::sync::mpsc;
@ -12,7 +19,20 @@ use reqwest::{StatusCode, Url};
#[tokio::main]
async fn main() {
run().await;
teloxide::enable_logging!();
log::info!("Starting heroku_ping_pong_bot...");
let bot = Bot::from_env().auto_send();
teloxide::repls2::repl_with_listener(
bot.clone(),
|msg: Message, bot: AutoSend<Bot>| async move {
bot.send_message(msg.chat.id, "pong").await?;
respond(())
},
webhook(bot).await,
)
.await;
}
async fn handle_rejection(error: warp::Rejection) -> Result<impl warp::Reply, Infallible> {
@ -25,18 +45,14 @@ pub async fn webhook(bot: AutoSend<Bot>) -> impl update_listeners::UpdateListene
// You might want to specify a self-signed certificate via .certificate
// method on SetWebhook.
bot.set_webhook(url)
.await
.expect("Cannot setup a webhook");
bot.set_webhook(url).await.expect("Cannot setup a webhook");
let (tx, rx) = mpsc::unbounded_channel();
let server = warp::post()
.and(warp::body::json())
.map(move |json: serde_json::Value| {
if let Ok(update) = Update::try_parse(&json) {
tx.send(Ok(update)).expect("Cannot send an incoming update from the webhook")
}
.map(move |update: Update| {
tx.send(Ok(update)).expect("Cannot send an incoming update from the webhook");
StatusCode::OK
})
@ -54,25 +70,11 @@ pub async fn webhook(bot: AutoSend<Bot>) -> impl update_listeners::UpdateListene
tokio::spawn(fut);
let stream = UnboundedReceiverStream::new(rx);
fn streamf<S, T>(state: &mut (S, T)) -> &mut S { &mut state.0 }
StatefulListener::new((stream, stop_token), streamf, |state: &mut (_, AsyncStopToken)| state.1.clone())
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting ngrok_ping_pong_bot...");
let bot = Bot::from_env().auto_send();
let cloned_bot = bot.clone();
teloxide::repl_with_listener(
bot,
|message| async move {
message.answer("pong").await?;
respond(())
},
webhook(cloned_bot).await,
)
.await;
fn streamf<S, T>(state: &mut (S, T)) -> &mut S {
&mut state.0
}
StatefulListener::new((stream, stop_token), streamf, |state: &mut (_, AsyncStopToken)| {
state.1.clone()
})
}

View file

@ -1,19 +0,0 @@
[package]
name = "webhook_ping_pong_bot"
version = "0.1.0"
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
teloxide = { path = "../../", features = ["auto-send"] }
log = "0.4.8"
pretty_env_logger = "0.4.0"
tokio = { version = "1.3.0", features = ["rt-multi-thread", "macros"] }
tokio-stream = "0.1.4"
# Used to setup a webhook
warp = "0.3.0"
reqwest = "0.10.4"
serde_json = "1.0.50"

116
examples/redis_remember.rs Normal file
View file

@ -0,0 +1,116 @@
use teloxide::{
dispatching2::dialogue::{serializer::Bincode, RedisStorage, Storage},
macros::DialogueState,
prelude2::*,
types::Me,
utils::command::BotCommand,
RequestError,
};
use thiserror::Error;
type MyDialogue = Dialogue<State, RedisStorage<Bincode>>;
type StorageError = <RedisStorage<Bincode> as Storage<State>>::Error;
#[derive(Debug, Error)]
enum Error {
#[error("error from Telegram: {0}")]
TelegramError(#[from] RequestError),
#[error("error from storage: {0}")]
StorageError(#[from] StorageError),
}
#[derive(DialogueState, Clone, serde::Serialize, serde::Deserialize)]
#[handler_out(anyhow::Result<()>)]
pub enum State {
#[handler(handle_start)]
Start,
#[handler(handle_got_number)]
GotNumber(i32),
}
impl Default for State {
fn default() -> Self {
Self::Start
}
}
#[derive(BotCommand)]
#[command(rename = "lowercase", description = "These commands are supported:")]
pub enum Command {
#[command(description = "get your number.")]
Get,
#[command(description = "reset your number.")]
Reset,
}
#[tokio::main]
async fn main() {
let bot = Bot::from_env().auto_send();
// You can also choose serializer::JSON or serializer::CBOR
// All serializers but JSON require enabling feature
// "serializer-<name>", e. g. "serializer-cbor"
// or "serializer-bincode"
let storage = RedisStorage::open("redis://127.0.0.1:6379", Bincode).await.unwrap();
let handler = Update::filter_message()
.enter_dialogue::<Message, RedisStorage<Bincode>, State>()
.dispatch_by::<State>();
Dispatcher::builder(bot, handler)
.dependencies(dptree::deps![storage])
.build()
.setup_ctrlc_handler()
.dispatch()
.await;
}
async fn handle_start(
bot: AutoSend<Bot>,
msg: Message,
dialogue: MyDialogue,
) -> anyhow::Result<()> {
match msg.text().unwrap().parse() {
Ok(number) => {
dialogue.update(State::GotNumber(number)).await?;
bot.send_message(
msg.chat.id,
format!("Remembered number {}. Now use /get or /reset", number),
)
.await?;
}
_ => {
bot.send_message(msg.chat.id, "Please, send me a number").await?;
}
}
Ok(())
}
async fn handle_got_number(
bot: AutoSend<Bot>,
msg: Message,
dialogue: MyDialogue,
num: i32,
me: Me,
) -> anyhow::Result<()> {
let ans = msg.text().unwrap();
let bot_name = me.user.username.unwrap();
match Command::parse(ans, bot_name) {
Ok(cmd) => match cmd {
Command::Get => {
bot.send_message(msg.chat.id, format!("Here is your number: {}", num)).await?;
}
Command::Reset => {
dialogue.reset().await?;
bot.send_message(msg.chat.id, "Number resetted").await?;
}
},
Err(_) => {
bot.send_message(msg.chat.id, "Please, send /get or /reset").await?;
}
}
Ok(())
}

View file

@ -1,19 +0,0 @@
[package]
name = "redis_remember_bot"
version = "0.1.0"
authors = ["Maximilian Siling <mouse-art@ya.ru>"]
edition = "2018"
[dependencies]
# You can also choose "cbor-serializer" or built-in JSON serializer
teloxide = { path = "../../", features = ["redis-storage", "bincode-serializer", "macros", "auto-send"] }
log = "0.4.8"
pretty_env_logger = "0.4.0"
tokio = { version = "1.3.0", features = ["rt-multi-thread", "macros"] }
serde = "1.0.104"
futures = "0.3.5"
thiserror = "1.0.15"
derive_more = "0.99.9"

View file

@ -1,62 +0,0 @@
#[macro_use]
extern crate derive_more;
mod states;
mod transitions;
use states::*;
use teloxide::{
dispatching::dialogue::{serializer::Bincode, RedisStorage, Storage},
prelude::*,
RequestError,
};
use thiserror::Error;
type StorageError = <RedisStorage<Bincode> as Storage<Dialogue>>::Error;
#[derive(Debug, Error)]
enum Error {
#[error("error from Telegram: {0}")]
TelegramError(#[from] RequestError),
#[error("error from storage: {0}")]
StorageError(#[from] StorageError),
}
type In = DialogueWithCx<AutoSend<Bot>, Message, Dialogue, StorageError>;
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
let bot = Bot::from_env().auto_send();
Dispatcher::new(bot)
.messages_handler(DialogueDispatcher::with_storage(
|DialogueWithCx { cx, dialogue }: In| async move {
let dialogue = dialogue.expect("std::convert::Infallible");
handle_message(cx, dialogue).await.expect("Something wrong with the bot!")
},
// You can also choose serializer::JSON or serializer::CBOR
// All serializers but JSON require enabling feature
// "serializer-<name>", e. g. "serializer-cbor"
// or "serializer-bincode"
RedisStorage::open("redis://127.0.0.1:6379", Bincode).await.unwrap(),
))
.dispatch()
.await;
}
async fn handle_message(
cx: UpdateWithCx<AutoSend<Bot>, Message>,
dialogue: Dialogue,
) -> TransitionOut<Dialogue> {
match cx.update.text().map(ToOwned::to_owned) {
None => {
cx.answer("Send me a text message.").await?;
next(dialogue)
}
Some(ans) => dialogue.react(cx, ans).await,
}
}

View file

@ -1,23 +0,0 @@
use teloxide::dispatching::dialogue::Transition;
use serde::{Deserialize, Serialize};
#[derive(Transition, From, Serialize, Deserialize)]
pub enum Dialogue {
Start(StartState),
HaveNumber(HaveNumberState),
}
impl Default for Dialogue {
fn default() -> Self {
Self::Start(StartState)
}
}
#[derive(Serialize, Deserialize)]
pub struct StartState;
#[derive(Serialize, Deserialize)]
pub struct HaveNumberState {
pub number: i32,
}

View file

@ -1,38 +0,0 @@
use teloxide::prelude::*;
use super::states::*;
#[teloxide(subtransition)]
async fn start(
state: StartState,
cx: TransitionIn<AutoSend<Bot>>,
ans: String,
) -> TransitionOut<Dialogue> {
if let Ok(number) = ans.parse() {
cx.answer(format!("Remembered number {}. Now use /get or /reset", number)).await?;
next(HaveNumberState { number })
} else {
cx.answer("Please, send me a number").await?;
next(state)
}
}
#[teloxide(subtransition)]
async fn have_number(
state: HaveNumberState,
cx: TransitionIn<AutoSend<Bot>>,
ans: String,
) -> TransitionOut<Dialogue> {
let num = state.number;
if ans.starts_with("/get") {
cx.answer(format!("Here is your number: {}", num)).await?;
next(state)
} else if ans.starts_with("/reset") {
cx.answer("Resetted number").await?;
next(StartState)
} else {
cx.answer("Please, send /get or /reset").await?;
next(state)
}
}

27
examples/shared_state.rs Normal file
View file

@ -0,0 +1,27 @@
// This bot answers how many messages it received in total on every message.
use std::sync::atomic::{AtomicU64, Ordering};
use once_cell::sync::Lazy;
use teloxide::prelude2::*;
static MESSAGES_TOTAL: Lazy<AtomicU64> = Lazy::new(AtomicU64::default);
#[tokio::main]
async fn main() {
teloxide::enable_logging!();
log::info!("Starting shared_state_bot...");
let bot = Bot::from_env().auto_send();
let handler = Update::filter_message().branch(dptree::endpoint(
|msg: Message, bot: AutoSend<Bot>| async move {
let previous = MESSAGES_TOTAL.fetch_add(1, Ordering::Relaxed);
bot.send_message(msg.chat.id, format!("I received {} messages in total.", previous))
.await?;
respond(())
},
));
Dispatcher::builder(bot, handler).build().setup_ctrlc_handler().dispatch().await;
}

View file

@ -1,15 +0,0 @@
[package]
name = "shared_state_bot"
version = "0.1.0"
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
teloxide = { path = "../../", features = ["auto-send"] }
log = "0.4.8"
pretty_env_logger = "0.4.0"
tokio = { version = "1.3.0", features = ["rt-multi-thread", "macros"] }
tokio-stream = "0.1.3"
lazy_static = "1.4.0"

View file

@ -1,38 +0,0 @@
// This bot answers how many messages it received in total on every message.
use std::sync::atomic::{AtomicU64, Ordering};
use lazy_static::lazy_static;
use teloxide::prelude::*;
use tokio_stream::wrappers::UnboundedReceiverStream;
lazy_static! {
static ref MESSAGES_TOTAL: AtomicU64 = AtomicU64::new(0);
}
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting shared_state_bot...");
let bot = Bot::from_env().auto_send();
Dispatcher::new(bot)
.messages_handler(|rx: DispatcherHandlerRx<AutoSend<Bot>, Message>| {
UnboundedReceiverStream::new(rx).for_each_concurrent(None, |message| async move {
let previous = MESSAGES_TOTAL.fetch_add(1, Ordering::Relaxed);
message
.answer(format!("I received {} messages in total.", previous))
.await
.log_on_error()
.await;
})
})
.dispatch()
.await;
}

View file

@ -1,8 +1,8 @@
use teloxide::{prelude::*, utils::command::BotCommand};
use teloxide::{prelude2::*, utils::command::BotCommand};
use std::error::Error;
#[derive(BotCommand)]
#[derive(BotCommand, Clone)]
#[command(rename = "lowercase", description = "These commands are supported:")]
enum Command {
#[command(description = "display this text.")]
@ -14,16 +14,21 @@ enum Command {
}
async fn answer(
cx: UpdateWithCx<AutoSend<Bot>, Message>,
bot: AutoSend<Bot>,
message: Message,
command: Command,
) -> Result<(), Box<dyn Error + Send + Sync>> {
match command {
Command::Help => cx.answer(Command::descriptions()).await?,
Command::Help => bot.send_message(message.chat.id, Command::descriptions()).await?,
Command::Username(username) => {
cx.answer(format!("Your username is @{}.", username)).await?
bot.send_message(message.chat.id, format!("Your username is @{}.", username)).await?
}
Command::UsernameAndAge { username, age } => {
cx.answer(format!("Your username is @{} and age is {}.", username, age)).await?
bot.send_message(
message.chat.id,
format!("Your username is @{} and age is {}.", username, age),
)
.await?
}
};
@ -32,15 +37,10 @@ async fn answer(
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting simple_commands_bot...");
let bot = Bot::from_env().auto_send();
let bot_name: String = panic!("Your bot's name here");
teloxide::commands_repl(bot, bot_name, answer).await;
teloxide::repls2::commands_repl(bot, answer, Command::ty()).await;
}

View file

@ -1,13 +0,0 @@
[package]
name = "simple_commands_bot"
version = "0.1.0"
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
teloxide = { path = "../../", features = ["macros", "auto-send"] }
log = "0.4.8"
pretty_env_logger = "0.4.0"
tokio = { version = "1.3.0", features = ["rt-multi-thread", "macros"] }

113
examples/sqlite_remember.rs Normal file
View file

@ -0,0 +1,113 @@
use teloxide::{
dispatching2::dialogue::{serializer::Json, SqliteStorage, Storage},
macros::DialogueState,
prelude2::*,
types::Me,
utils::command::BotCommand,
RequestError,
};
use thiserror::Error;
type MyDialogue = Dialogue<State, SqliteStorage<Json>>;
type StorageError = <SqliteStorage<Json> as Storage<State>>::Error;
#[derive(Debug, Error)]
enum Error {
#[error("error from Telegram: {0}")]
TelegramError(#[from] RequestError),
#[error("error from storage: {0}")]
StorageError(#[from] StorageError),
}
#[derive(DialogueState, Clone, serde::Serialize, serde::Deserialize)]
#[handler_out(anyhow::Result<()>)]
pub enum State {
#[handler(handle_start)]
Start,
#[handler(handle_got_number)]
GotNumber(i32),
}
impl Default for State {
fn default() -> Self {
Self::Start
}
}
#[derive(BotCommand)]
#[command(rename = "lowercase", description = "These commands are supported:")]
pub enum Command {
#[command(description = "get your number.")]
Get,
#[command(description = "reset your number.")]
Reset,
}
#[tokio::main]
async fn main() {
let bot = Bot::from_env().auto_send();
let storage = SqliteStorage::open("db.sqlite", Json).await.unwrap();
let handler = Update::filter_message()
.enter_dialogue::<Message, SqliteStorage<Json>, State>()
.dispatch_by::<State>();
Dispatcher::builder(bot, handler)
.dependencies(dptree::deps![storage])
.build()
.setup_ctrlc_handler()
.dispatch()
.await;
}
async fn handle_start(
bot: AutoSend<Bot>,
msg: Message,
dialogue: MyDialogue,
) -> anyhow::Result<()> {
match msg.text().unwrap().parse() {
Ok(number) => {
dialogue.update(State::GotNumber(number)).await?;
bot.send_message(
msg.chat.id,
format!("Remembered number {}. Now use /get or /reset", number),
)
.await?;
}
_ => {
bot.send_message(msg.chat.id, "Please, send me a number").await?;
}
}
Ok(())
}
async fn handle_got_number(
bot: AutoSend<Bot>,
msg: Message,
dialogue: MyDialogue,
num: i32,
me: Me,
) -> anyhow::Result<()> {
let ans = msg.text().unwrap();
let bot_name = me.user.username.unwrap();
match Command::parse(ans, bot_name) {
Ok(cmd) => match cmd {
Command::Get => {
bot.send_message(msg.chat.id, format!("Here is your number: {}", num)).await?;
}
Command::Reset => {
dialogue.reset().await?;
bot.send_message(msg.chat.id, "Number resetted").await?;
}
},
Err(_) => {
bot.send_message(msg.chat.id, "Please, send /get or /reset").await?;
}
}
Ok(())
}

View file

@ -1,19 +0,0 @@
[package]
name = "sqlite_remember_bot"
version = "0.1.0"
authors = ["Maximilian Siling <mouse-art@ya.ru>", "Sergey Levitin <selevit@gmail.com>"]
edition = "2018"
[dependencies]
# You can also choose "cbor-serializer" or built-in JSON serializer
teloxide = { path = "../../", features = ["sqlite-storage", "bincode-serializer", "redis-storage", "macros", "auto-send"] }
log = "0.4.8"
pretty_env_logger = "0.4.0"
tokio = { version = "1.3.0", features = ["rt-multi-thread", "macros"] }
serde = "1.0.104"
futures = "0.3.5"
thiserror = "1.0.15"
derive_more = "0.99.9"

View file

@ -1,55 +0,0 @@
#[macro_use]
extern crate derive_more;
mod states;
mod transitions;
use states::*;
use teloxide::{
dispatching::dialogue::{serializer::Json, SqliteStorage, Storage},
prelude::*,
RequestError,
};
use thiserror::Error;
type StorageError = <SqliteStorage<Json> as Storage<Dialogue>>::Error;
#[derive(Debug, Error)]
enum Error {
#[error("error from Telegram: {0}")]
TelegramError(#[from] RequestError),
#[error("error from storage: {0}")]
StorageError(#[from] StorageError),
}
type In = DialogueWithCx<AutoSend<Bot>, Message, Dialogue, StorageError>;
async fn handle_message(
cx: UpdateWithCx<AutoSend<Bot>, Message>,
dialogue: Dialogue,
) -> TransitionOut<Dialogue> {
match cx.update.text().map(ToOwned::to_owned) {
None => {
cx.answer("Send me a text message.").await?;
next(dialogue)
}
Some(ans) => dialogue.react(cx, ans).await,
}
}
#[tokio::main]
async fn main() {
let bot = Bot::from_env().auto_send();
Dispatcher::new(bot)
.messages_handler(DialogueDispatcher::with_storage(
|DialogueWithCx { cx, dialogue }: In| async move {
let dialogue = dialogue.expect("std::convert::Infallible");
handle_message(cx, dialogue).await.expect("Something wrong with the bot!")
},
SqliteStorage::open("db.sqlite", Json).await.unwrap(),
))
.dispatch()
.await;
}

View file

@ -1,23 +0,0 @@
use teloxide::macros::Transition;
use serde::{Deserialize, Serialize};
#[derive(Transition, From, Serialize, Deserialize)]
pub enum Dialogue {
Start(StartState),
HaveNumber(HaveNumberState),
}
impl Default for Dialogue {
fn default() -> Self {
Self::Start(StartState)
}
}
#[derive(Serialize, Deserialize)]
pub struct StartState;
#[derive(Serialize, Deserialize)]
pub struct HaveNumberState {
pub number: i32,
}

View file

@ -1,39 +0,0 @@
use teloxide::prelude::*;
use teloxide::macros::teloxide;
use super::states::*;
#[teloxide(subtransition)]
async fn start(
state: StartState,
cx: TransitionIn<AutoSend<Bot>>,
ans: String,
) -> TransitionOut<Dialogue> {
if let Ok(number) = ans.parse() {
cx.answer(format!("Remembered number {}. Now use /get or /reset", number)).await?;
next(HaveNumberState { number })
} else {
cx.answer("Please, send me a number").await?;
next(state)
}
}
#[teloxide(subtransition)]
async fn have_number(
state: HaveNumberState,
cx: TransitionIn<AutoSend<Bot>>,
ans: String,
) -> TransitionOut<Dialogue> {
let num = state.number;
if ans.starts_with("/get") {
cx.answer(format!("Here is your number: {}", num)).await?;
next(state)
} else if ans.starts_with("/reset") {
cx.answer("Resetted number").await?;
next(StartState)
} else {
cx.answer("Please, send /get or /reset").await?;
next(state)
}
}

View file

@ -1,10 +0,0 @@
##!/bin/sh
for example in */; do
echo Testing $example...
cd $example
cargo check &
cd ..
done
wait

4
rust-toolchain.toml Normal file
View file

@ -0,0 +1,4 @@
[toolchain]
channel = "nightly-2022-02-02"
components = ["rustfmt", "clippy"]
profile = "minimal"

View file

@ -30,6 +30,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
/// [`DispatcherHandler`]: crate::dispatching::DispatcherHandler
#[deprecated(note = "Use dispatching2 instead")]
pub struct DialogueDispatcher<R, D, S, H, Upd> {
storage: Arc<S>,
handler: Arc<H>,

View file

@ -1,4 +1,4 @@
use crate::prelude::{DialogueStage, DialogueWithCx};
use crate::dispatching::dialogue::{DialogueStage, DialogueWithCx};
use futures::future::BoxFuture;
use std::{future::Future, sync::Arc};
@ -8,6 +8,7 @@ use std::{future::Future, sync::Arc};
/// overview](crate::dispatching::dialogue).
///
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
#[deprecated(note = "Use dispatching2 instead")]
pub trait DialogueDispatcherHandler<R, Upd, D, E> {
#[must_use]
fn handle(

View file

@ -5,6 +5,7 @@ use crate::dispatching::dialogue::TransitionOut;
/// See [the module-level documentation for the design
/// overview](crate::dispatching::dialogue).
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
#[deprecated(note = "Use dispatching2 instead")]
pub enum DialogueStage<D> {
Next(D),
Exit,
@ -21,6 +22,7 @@ pub enum DialogueStage<D> {
///
/// [`From`]: std::convert::From
/// [derive-more]: https://crates.io/crates/derive_more
#[deprecated(note = "Use dispatching2 instead")]
pub fn next<Dialogue, State, E>(new_state: State) -> TransitionOut<Dialogue, E>
where
Dialogue: From<State>,
@ -32,6 +34,7 @@ where
///
/// See [the module-level documentation for the design
/// overview](crate::dispatching::dialogue).
#[deprecated(note = "Use dispatching2 instead")]
pub fn exit<D, E>() -> TransitionOut<D, E> {
Ok(DialogueStage::Exit)
}

View file

@ -9,6 +9,7 @@ use teloxide_core::requests::Requester;
///
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
#[derive(Debug)]
#[deprecated(note = "Use dispatching2 instead")]
pub struct DialogueWithCx<R, Upd, D, E> {
pub cx: UpdateWithCx<R, Upd>,
pub dialogue: Result<D, E>,

View file

@ -1,6 +1,7 @@
use teloxide_core::types::Message;
/// Something that has a chat ID.
#[deprecated(note = "Use dispatching2 instead")]
pub trait GetChatId {
#[must_use]
fn chat_id(&self) -> i64;

View file

@ -80,10 +80,6 @@
//!
//! #[tokio::main]
//! async fn main() {
//! run().await;
//! }
//!
//! async fn run() {
//! teloxide::enable_logging!();
//! log::info!("Starting dialogue_bot!");
//!

View file

@ -3,6 +3,7 @@ use futures::future::BoxFuture;
use teloxide_core::types::Message;
/// Represents a transition function of a dialogue FSM.
#[deprecated(note = "Use dispatching2 instead")]
pub trait Transition: Sized {
type Aux;
type Error;
@ -21,6 +22,7 @@ pub trait Transition: Sized {
/// Like [`Transition`], but from `StateN` -> `Dialogue`.
///
/// [`Transition`]: crate::dispatching::dialogue::Transition
#[deprecated(note = "Use dispatching2 instead")]
pub trait Subtransition
where
Self::Dialogue: Transition<Aux = Self::Aux>,
@ -45,6 +47,7 @@ where
///
/// Now it is used only inside `#[teloxide(subtransition)]` for type inference.
#[doc(hidden)]
#[deprecated(note = "Use dispatching2 instead")]
pub trait SubtransitionOutputType {
type Output;
type Error;
@ -56,7 +59,9 @@ impl<D, E> SubtransitionOutputType for TransitionOut<D, E> {
}
/// An input passed into a FSM (sub)transition function.
#[deprecated(note = "Use dispatching2 instead")]
pub type TransitionIn<R> = UpdateWithCx<R, Message>;
/// A type returned from a FSM (sub)transition function.
#[deprecated(note = "Use dispatching2 instead")]
pub type TransitionOut<D, E = crate::RequestError> = Result<DialogueStage<D>, E>;

View file

@ -1,11 +1,4 @@
use std::{
fmt::{self, Debug},
sync::{
atomic::{AtomicU8, Ordering},
Arc,
},
time::Duration,
};
use std::{fmt::Debug, sync::Arc};
use crate::{
dispatching::{
@ -14,21 +7,21 @@ use crate::{
DispatcherHandler, UpdateWithCx,
},
error_handlers::{ErrorHandler, LoggingErrorHandler},
utils::shutdown_token::shutdown_check_timeout_for,
};
use futures::{stream::FuturesUnordered, Future, StreamExt};
use futures::{stream::FuturesUnordered, StreamExt};
use teloxide_core::{
requests::Requester,
types::{
AllowedUpdate, CallbackQuery, ChatMemberUpdated, ChosenInlineResult, InlineQuery, Message,
Poll, PollAnswer, PreCheckoutQuery, ShippingQuery, Update, UpdateKind,
AllowedUpdate, CallbackQuery, ChatJoinRequest, ChatMemberUpdated, ChosenInlineResult,
InlineQuery, Message, Poll, PollAnswer, PreCheckoutQuery, ShippingQuery, Update,
UpdateKind,
},
};
use tokio::{
sync::{mpsc, Notify},
task::JoinHandle,
time::timeout,
};
use tokio::{sync::mpsc, task::JoinHandle, time::timeout};
use crate::utils::shutdown_token::ShutdownToken;
type Tx<Upd, R> = Option<mpsc::UnboundedSender<UpdateWithCx<Upd, R>>>;
@ -36,6 +29,7 @@ type Tx<Upd, R> = Option<mpsc::UnboundedSender<UpdateWithCx<Upd, R>>>;
///
/// See the [module-level documentation](crate::dispatching) for the design
/// overview.
#[deprecated(note = "Use dispatching2 instead")]
pub struct Dispatcher<R> {
requester: R,
@ -52,11 +46,11 @@ pub struct Dispatcher<R> {
poll_answers_queue: Tx<R, PollAnswer>,
my_chat_members_queue: Tx<R, ChatMemberUpdated>,
chat_members_queue: Tx<R, ChatMemberUpdated>,
chat_join_requests_queue: Tx<R, ChatJoinRequest>,
running_handlers: FuturesUnordered<JoinHandle<()>>,
state: Arc<DispatcherState>,
shutdown_notify_back: Arc<Notify>,
state: ShutdownToken,
}
impl<R> Dispatcher<R>
@ -81,9 +75,9 @@ where
poll_answers_queue: None,
my_chat_members_queue: None,
chat_members_queue: None,
chat_join_requests_queue: None,
running_handlers: FuturesUnordered::new(),
state: <_>::default(),
shutdown_notify_back: <_>::default(),
state: ShutdownToken::new(),
}
}
@ -106,22 +100,21 @@ where
///
/// [`shutdown`]: ShutdownToken::shutdown
#[cfg(feature = "ctrlc_handler")]
#[cfg_attr(docsrs, doc(cfg(feature = "ctrlc_handler")))]
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "ctrlc_handler")))]
#[must_use]
pub fn setup_ctrlc_handler(self) -> Self {
let state = Arc::clone(&self.state);
let token = self.state.clone();
tokio::spawn(async move {
loop {
tokio::signal::ctrl_c().await.expect("Failed to listen for ^C");
match shutdown_inner(&state) {
Ok(()) => log::info!("^C received, trying to shutdown the dispatcher..."),
Err(Ok(AlreadyShuttingDown)) => {
log::info!(
"^C received, the dispatcher is already shutting down, ignoring the \
signal"
)
match token.shutdown() {
Ok(f) => {
log::info!("^C received, trying to shutdown the dispatcher...");
f.await;
log::info!("dispatcher is shutdown...");
}
Err(Err(IdleShutdownError)) => {
Err(_) => {
log::info!("^C received, the dispatcher isn't running, ignoring the signal")
}
}
@ -263,7 +256,7 @@ where
pub async fn dispatch(&mut self)
where
R: Requester + Clone,
<R as Requester>::GetUpdatesFaultTolerant: Send,
<R as Requester>::GetUpdates: Send,
{
let listener = update_listeners::polling_default(self.requester.clone()).await;
let error_handler =
@ -292,19 +285,12 @@ where
ListenerE: Debug,
R: Requester + Clone,
{
use ShutdownState::*;
self.hint_allowed_updates(&mut update_listener);
let shutdown_check_timeout = shutdown_check_timeout_for(&update_listener);
let mut stop_token = Some(update_listener.stop_token());
if let Err(actual) = self.state.compare_exchange(Idle, Running) {
unreachable!(
"Dispatching is already running: expected `{:?}` state, found `{:?}`",
Idle, actual
);
}
self.state.start_dispatching();
{
let stream = update_listener.as_stream();
@ -320,7 +306,7 @@ where
}
}
if let ShuttingDown = self.state.load() {
if self.state.is_shutting_down() {
if let Some(token) = stop_token.take() {
log::debug!("Start shutting down dispatching...");
token.stop();
@ -330,27 +316,13 @@ where
}
self.wait_for_handlers().await;
if let ShuttingDown = self.state.load() {
// Stopped because of a `shutdown` call.
// Notify `shutdown`s that we finished
self.shutdown_notify_back.notify_waiters();
log::info!("Dispatching has been shut down.");
} else {
log::info!("Dispatching has been stopped (listener returned `None`).");
}
self.state.store(Idle);
self.state.done();
}
/// Returns a shutdown token, which can later be used to shutdown
/// dispatching.
pub fn shutdown_token(&self) -> ShutdownToken {
ShutdownToken {
dispatcher_state: Arc::clone(&self.state),
shutdown_notify_back: Arc::clone(&self.shutdown_notify_back),
}
self.state.clone()
}
async fn process_update<ListenerE, Eh>(
@ -446,6 +418,20 @@ where
chat_member_updated,
"UpdateKind::MyChatMember",
),
UpdateKind::ChatJoinRequest(chat_join_request) => send(
&self.requester,
&self.chat_join_requests_queue,
chat_join_request,
"UpdateKind::ChatJoinRequest",
),
UpdateKind::Error(err) => {
log::error!(
"Cannot parse an update.\nError: {:?}\n\
This is a bug in teloxide-core, please open an issue here: \
https://github.com/teloxide/teloxide-core/issues.",
err,
);
}
}
}
}
@ -528,127 +514,6 @@ where
}
}
/// This error is returned from [`ShutdownToken::shutdown`] when trying to
/// shutdown an idle [`Dispatcher`].
#[derive(Debug)]
pub struct IdleShutdownError;
impl fmt::Display for IdleShutdownError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Dispatcher was idle and as such couldn't be shut down")
}
}
impl std::error::Error for IdleShutdownError {}
/// A token which used to shutdown [`Dispatcher`].
#[derive(Clone)]
pub struct ShutdownToken {
dispatcher_state: Arc<DispatcherState>,
shutdown_notify_back: Arc<Notify>,
}
impl ShutdownToken {
/// Tries to shutdown dispatching.
///
/// Returns an error if the dispatcher is idle at the moment.
///
/// If you don't need to wait for shutdown, the returned future can be
/// ignored.
pub fn shutdown(&self) -> Result<impl Future<Output = ()> + '_, IdleShutdownError> {
match shutdown_inner(&self.dispatcher_state) {
Ok(()) | Err(Ok(AlreadyShuttingDown)) => Ok(async move {
log::info!("Trying to shutdown the dispatcher...");
self.shutdown_notify_back.notified().await
}),
Err(Err(err)) => Err(err),
}
}
}
struct DispatcherState {
inner: AtomicU8,
}
impl DispatcherState {
fn load(&self) -> ShutdownState {
ShutdownState::from_u8(self.inner.load(Ordering::SeqCst))
}
fn store(&self, new: ShutdownState) {
self.inner.store(new as _, Ordering::SeqCst)
}
fn compare_exchange(
&self,
current: ShutdownState,
new: ShutdownState,
) -> Result<ShutdownState, ShutdownState> {
self.inner
.compare_exchange(current as _, new as _, Ordering::SeqCst, Ordering::SeqCst)
.map(ShutdownState::from_u8)
.map_err(ShutdownState::from_u8)
}
}
impl Default for DispatcherState {
fn default() -> Self {
Self { inner: AtomicU8::new(ShutdownState::Idle as _) }
}
}
#[repr(u8)]
#[derive(Debug)]
enum ShutdownState {
Running,
ShuttingDown,
Idle,
}
impl ShutdownState {
fn from_u8(n: u8) -> Self {
const RUNNING: u8 = ShutdownState::Running as u8;
const SHUTTING_DOWN: u8 = ShutdownState::ShuttingDown as u8;
const IDLE: u8 = ShutdownState::Idle as u8;
match n {
RUNNING => ShutdownState::Running,
SHUTTING_DOWN => ShutdownState::ShuttingDown,
IDLE => ShutdownState::Idle,
_ => unreachable!(),
}
}
}
fn shutdown_check_timeout_for<E>(update_listener: &impl UpdateListener<E>) -> Duration {
const MIN_SHUTDOWN_CHECK_TIMEOUT: Duration = Duration::from_secs(1);
// FIXME: replace this by just Duration::ZERO once 1.53 will be released
const DZERO: Duration = Duration::from_secs(0);
let shutdown_check_timeout = update_listener.timeout_hint().unwrap_or(DZERO);
// FIXME: replace this by just saturating_add once 1.53 will be released
shutdown_check_timeout.checked_add(MIN_SHUTDOWN_CHECK_TIMEOUT).unwrap_or(shutdown_check_timeout)
}
struct AlreadyShuttingDown;
fn shutdown_inner(
state: &DispatcherState,
) -> Result<(), Result<AlreadyShuttingDown, IdleShutdownError>> {
use ShutdownState::*;
let res = state.compare_exchange(Running, ShuttingDown);
match res {
Ok(_) => Ok(()),
Err(ShuttingDown) => Err(Ok(AlreadyShuttingDown)),
Err(Idle) => Err(Err(IdleShutdownError)),
Err(Running) => unreachable!(),
}
}
fn send<'a, R, Upd>(requester: &'a R, tx: &'a Tx<R, Upd>, update: Upd, variant: &'static str)
where
Upd: Debug,

View file

@ -9,6 +9,7 @@ use futures::future::BoxFuture;
/// overview.
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
#[deprecated(note = "Use dispatching2 instead")]
pub trait DispatcherHandler<R, Upd> {
#[must_use]
fn handle(self, updates: DispatcherHandlerRx<R, Upd>) -> BoxFuture<'static, ()>

View file

@ -8,6 +8,7 @@ use teloxide_core::types::Message;
/// overview.
///
/// [`DispatcherHandlerRx`]: crate::dispatching::DispatcherHandlerRx
#[deprecated(note = "Use dispatching2 instead")]
pub trait DispatcherHandlerRxExt<R> {
/// Extracts only text messages from this stream of arbitrary messages.
fn text_messages(self) -> BoxStream<'static, (UpdateWithCx<R, Message>, String)>

View file

@ -1,4 +1,5 @@
//! Updates dispatching.
//! Old updates dispatching (**DEPRECATED**: use [`crate::dispatching2`]
//! instead).
//!
//! The key type here is [`Dispatcher`]. It encapsulates [`Bot`] and handlers
//! for [all the update kinds].
@ -45,11 +46,12 @@
//! [`tokio::sync::mpsc::UnboundedReceiver`]: https://docs.rs/tokio/0.2.11/tokio/sync/mpsc/struct.UnboundedReceiver.html
//! [examples/dialogue_bot]: https://github.com/teloxide/teloxide/tree/master/examples/dialogue_bot
#![allow(deprecated)]
pub mod dialogue;
pub mod stop_token;
pub mod update_listeners;
#[cfg(feature = "ctrlc_handler")]
pub(crate) mod repls;
mod dispatcher;
@ -57,7 +59,8 @@ mod dispatcher_handler;
mod dispatcher_handler_rx_ext;
mod update_with_cx;
pub use dispatcher::{Dispatcher, IdleShutdownError, ShutdownToken};
pub use crate::utils::shutdown_token::{IdleShutdownError, ShutdownToken};
pub use dispatcher::Dispatcher;
pub use dispatcher_handler::DispatcherHandler;
pub use dispatcher_handler_rx_ext::DispatcherHandlerRxExt;
use tokio::sync::mpsc::UnboundedReceiver;
@ -66,4 +69,5 @@ pub use update_with_cx::{UpdateWithCx, UpdateWithCxRequesterType};
/// A type of a stream, consumed by [`Dispatcher`]'s handlers.
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
#[deprecated(note = "Use dispatching2 instead")]
pub type DispatcherHandlerRx<R, Upd> = UnboundedReceiver<UpdateWithCx<R, Upd>>;

View file

@ -31,7 +31,7 @@ where
HandlerE: Debug + Send,
N: Into<String> + Send + 'static,
R: Requester + Send + Clone + 'static,
<R as Requester>::GetUpdatesFaultTolerant: Send,
<R as Requester>::GetUpdates: Send,
{
let cloned_requester = requester.clone();

View file

@ -29,7 +29,7 @@ where
D: Clone + Default + Send + 'static,
Fut: Future<Output = DialogueStage<D>> + Send + 'static,
R: Requester + Send + Clone + 'static,
<R as Requester>::GetUpdatesFaultTolerant: Send,
<R as Requester>::GetUpdates: Send,
{
let cloned_requester = requester.clone();

View file

@ -28,7 +28,7 @@ where
Result<(), E>: OnError<E>,
E: Debug + Send,
R: Requester + Send + Clone + 'static,
<R as Requester>::GetUpdatesFaultTolerant: Send,
<R as Requester>::GetUpdates: Send,
{
let cloned_requester = requester.clone();
repl_with_listener(
@ -83,3 +83,12 @@ pub async fn repl_with_listener<'a, R, H, Fut, E, L, ListenerE>(
)
.await;
}
#[test]
fn repl_is_send() {
let bot = crate::Bot::new("");
let repl = crate::repl(bot, |_| async { crate::respond(()) });
assert_send(&repl);
fn assert_send(_: &impl Send) {}
}

View file

@ -1,107 +1,27 @@
//! Receiving updates from Telegram.
//!
//! The key trait here is [`UpdateListener`]. You can get it by these functions:
//! The key trait here is [`UpdateListener`]. You can get its implementation
//! using one these functions:
//!
//! - [`polling_default`], which returns a default long polling listener.
//! - [`polling`], which returns a long/short polling listener with your
//! configuration.
//! - [`polling_default`], which returns a default long polling listener.
//! - [`polling`], which returns a long polling listener with your
//! configuration.
//!
//! And then you can extract updates from it and pass them directly to a
//! dispatcher.
//! And then you can extract updates from it or pass them directly to a
//! [`Dispatcher`].
//!
//! Telegram supports two ways of [getting updates]: [long]/[short] polling and
//! [webhook].
//!
//! # Long Polling
//!
//! In long polling, you just call [`Box::get_updates`] every N seconds.
//!
//! ## Example
//!
//! <pre>
//! tg bot
//! | |
//! |<---------------------------| Updates? (Bot::get_updates call)
//! ↑ ↑
//! | timeout<a id="1b" href="#1">^1</a> |
//! ↓ ↓
//! Nope |--------------------------->|
//! ↑ ↑
//! | delay between Bot::get_updates<a id="2b" href="#2">^2</a> |
//! ↓ ↓
//! |<---------------------------| Updates?
//! ↑ ↑
//! | timeout<a id="3b" href="#3">^3</a> |
//! ↓ ↓
//! Yes |-------[updates 0, 1]------>|
//! ↑ ↑
//! | delay |
//! ↓ ↓
//! |<-------[offset = 1]--------| Updates?<a id="4b" href="#4">^4</a>
//! ↑ ↑
//! | timeout |
//! ↓ ↓
//! Yes |---------[update 2]-------->|
//! ↑ ↑
//! | delay |
//! ↓ ↓
//! |<-------[offset = 2]--------| Updates?
//! ↑ ↑
//! | timeout |
//! ↓ ↓
//! Nope |--------------------------->|
//! ↑ ↑
//! | delay |
//! ↓ ↓
//! |<-------[offset = 2]--------| Updates?
//! ↑ ↑
//! | timeout |
//! ↓ ↓
//! Nope |--------------------------->|
//! ↑ ↑
//! | delay |
//! ↓ ↓
//! |<-------[offset = 2]--------| Updates?
//! ↑ ↑
//! | timeout |
//! ↓ ↓
//! Yes |-------[updates 2..5]------>|
//! ↑ ↑
//! | delay |
//! ↓ ↓
//! |<-------[offset = 5]--------| Updates?
//! ↑ ↑
//! | timeout |
//! ↓ ↓
//! Nope |--------------------------->|
//! | |
//! ~ and so on, and so on ~
//! </pre>
//!
//! <a id="1" href="#1b">^1</a> A timeout can be even 0
//! (this is also called short polling),
//! but you should use it **only** for testing purposes.
//!
//! <a id="2" href="#2b">^2</a> Large delays will cause in bot lags,
//! so delay shouldn't exceed second.
//!
//! <a id="3" href="#3b">^3</a> Note that if Telegram already have updates for
//! you it will answer you **without** waiting for a timeout.
//!
//! <a id="4" href="#4b">^4</a> `offset = N` means that we've already received
//! updates `0..=N`.
//!
//! # Webhooks
//! See the [README FAQ about webhooks](https://github.com/teloxide/teloxide/blob/master/README.md#faq).
//! Telegram supports two ways of [getting updates]: [long polling] and
//! [webhooks]. Currently, only the former one is implemented (see [`polling()`]
//! and [`polling_default`]). See also [README FAQ about webhooks](https://github.com/teloxide/teloxide/blob/master/README.md#faq).
//!
//! [`UpdateListener`]: UpdateListener
//! [`polling_default`]: polling_default
//! [`polling`]: polling()
//! [`Dispatcher`]: crate::dispatching::Dispatcher
//! [`Box::get_updates`]: crate::requests::Requester::get_updates
//! [getting updates]: https://core.telegram.org/bots/api#getting-updates
//! [long]: https://en.wikipedia.org/wiki/Push_technology#Long_polling
//! [short]: https://en.wikipedia.org/wiki/Polling_(computer_science)
//! [webhook]: https://en.wikipedia.org/wiki/Webhook
//! [long polling]: https://en.wikipedia.org/wiki/Push_technology#Long_polling
//! [webhooks]: https://en.wikipedia.org/wiki/Webhook
use futures::Stream;
@ -122,19 +42,15 @@ pub use self::{
/// An update listener.
///
/// Implementors of this trait allow getting updates from Telegram.
///
/// Currently Telegram has 2 ways of getting updates -- [polling] and
/// [webhooks]. Currently, only the former one is implemented (see [`polling()`]
/// and [`polling_default`])
/// Implementors of this trait allow getting updates from Telegram. See
/// [module-level documentation] for more.
///
/// Some functions of this trait are located in the supertrait
/// ([`AsUpdateStream`]), see also:
/// - [`AsUpdateStream::Stream`]
/// - [`AsUpdateStream::as_stream`]
///
/// [polling]: self#long-polling
/// [webhooks]: self#webhooks
/// [module-level documentation]: mod@self
pub trait UpdateListener<E>: for<'a> AsUpdateStream<'a, E> {
/// The type of token which allows to stop this listener.
type StopToken: StopToken;
@ -150,8 +66,8 @@ pub trait UpdateListener<E>: for<'a> AsUpdateStream<'a, E> {
/// Implementors of this function are encouraged to stop listening for
/// updates as soon as possible and return `None` from the update stream as
/// 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"]
#[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;
/// Hint which updates should the listener listen for.
@ -188,7 +104,14 @@ pub trait UpdateListener<E>: for<'a> AsUpdateStream<'a, E> {
/// This trait is a workaround to not require GAT.
pub trait AsUpdateStream<'a, E> {
/// The stream of updates from Telegram.
type Stream: Stream<Item = Result<Update, E>> + 'a;
// HACK: There is currently no way to write something like
// `-> impl for<'a> AsUpdateStream<'a, E, Stream: Send>`. Since we return
// `impl UpdateListener<E>` 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<Item = Result<Update, E>> + Send + 'a;
/// Creates the update [`Stream`].
///

View file

@ -7,12 +7,12 @@ use futures::{
use crate::{
dispatching::{
stop_token::{AsyncStopFlag, AsyncStopToken},
stop_token::{AsyncStopFlag, AsyncStopToken, StopToken},
update_listeners::{stateful_listener::StatefulListener, UpdateListener},
},
payloads::GetUpdates,
payloads::{GetUpdates, GetUpdatesSetters as _},
requests::{HasPayload, Request, Requester},
types::{AllowedUpdate, SemiparsedVec, Update},
types::{AllowedUpdate, Update},
};
/// Returns a long polling update listener with `timeout` of 10 seconds.
@ -22,36 +22,118 @@ use crate::{
/// ## Notes
///
/// This function will automatically delete a webhook if it was set up.
pub async fn polling_default<R>(requester: R) -> impl UpdateListener<R::Err>
pub async fn polling_default<R>(
requester: R,
) -> impl UpdateListener<R::Err, StopToken = impl Send + StopToken>
where
R: Requester + 'static,
<R as Requester>::GetUpdatesFaultTolerant: Send,
R: Requester + Send + 'static,
<R as Requester>::GetUpdates: Send,
{
delete_webhook_if_setup(&requester).await;
polling(requester, Some(Duration::from_secs(10)), None, None)
}
/// Returns a long/short polling update listener with some additional options.
#[cfg_attr(doc, aquamarine::aquamarine)]
/// Returns a long polling update listener with some additional options.
///
/// - `bot`: Using this bot, the returned update listener will receive updates.
/// - `timeout`: A timeout for polling.
/// - `timeout`: A timeout in seconds for polling.
/// - `limit`: Limits the number of updates to be retrieved at once. Values
/// between 1—100 are accepted.
/// - `allowed_updates`: A list the types of updates you want to receive.
///
/// See [`GetUpdates`] for defaults.
///
/// See also: [`polling_default`](polling_default).
///
/// [`GetUpdates`]: crate::payloads::GetUpdates
/// ## Notes
///
/// - `timeout` should not be bigger than http client timeout, see
/// [`default_reqwest_settings`] for default http client settings.
/// - [`repl`]s and [`Dispatcher`] use [`hint_allowed_updates`] to set
/// `allowed_updates`, so you rarely need to pass `allowed_updates`
/// explicitly.
///
/// [`default_reqwest_settings`]: teloxide::net::default_reqwest_settings
/// [`repl`]: fn@crate::repl
/// [`Dispatcher`]: crate::dispatching::Dispatcher
/// [`hint_allowed_updates`]:
/// crate::dispatching::update_listeners::UpdateListener::hint_allowed_updates
///
/// ## How it works
///
/// Long polling works by repeatedly calling [`Bot::get_updates`][get_updates].
/// If telegram has any updates, it returns them immediately, otherwise it waits
/// until either it has any updates or `timeout` expires.
///
/// Each [`get_updates`][get_updates] call includes an `offset` parameter equal
/// to the latest update id + one, that allows to only receive updates that has
/// not been received before.
///
/// When telegram receives a [`get_updates`][get_updates] request with `offset =
/// N` it forgets any updates with id < `N`. When `polling` listener is stopped,
/// it sends [`get_updates`][get_updates] with `timeout = 0, limit = 1` and
/// appropriate `offset`, so future bot restarts won't see updates that were
/// already seen.
///
/// Consumers of a `polling` update listener then need to repeatedly call
/// [`futures::StreamExt::next`] to get the updates.
///
/// Here is an example diagram that shows these interactions between consumers
/// like [`Dispatcher`], `polling` update listener and telegram.
///
/// ```mermaid
/// sequenceDiagram
/// participant C as Consumer
/// participant P as polling
/// participant T as Telegram
///
/// link C: Dispatcher @ ../struct.Dispatcher.html
/// link C: repl @ ../../fn.repl.html
///
/// C->>P: next
///
/// P->>+T: Updates? (offset = 0)
/// Note right of T: timeout
/// T->>-P: None
///
/// P->>+T: Updates? (offset = 0)
/// Note right of T: <= timeout
/// T->>-P: updates with ids [3, 4]
///
/// P->>C: update(3)
///
/// C->>P: next
/// P->>C: update(4)
///
/// C->>P: next
///
/// P->>+T: Updates? (offset = 5)
/// Note right of T: <= timeout
/// T->>-P: updates with ids [5]
///
/// C->>P: stop signal
///
/// P->>C: update(5)
///
/// C->>P: next
///
/// P->>T: *Acknolegment of update(5)*
/// T->>P: ok
///
/// P->>C: None
/// ```
///
/// [get_updates]: crate::requests::Requester::get_updates
pub fn polling<R>(
requester: R,
bot: R,
timeout: Option<Duration>,
limit: Option<u8>,
allowed_updates: Option<Vec<AllowedUpdate>>,
) -> impl UpdateListener<R::Err>
) -> impl UpdateListener<R::Err, StopToken = impl Send + StopToken>
where
R: Requester + 'static,
<R as Requester>::GetUpdatesFaultTolerant: Send,
R: Requester + Send + 'static,
<R as Requester>::GetUpdates: Send,
{
struct State<B: Requester> {
bot: B,
@ -61,73 +143,57 @@ where
offset: i32,
flag: AsyncStopFlag,
token: AsyncStopToken,
force_stop: bool,
}
fn stream<B>(st: &mut State<B>) -> impl Stream<Item = Result<Update, B::Err>> + '_
fn stream<B>(st: &mut State<B>) -> impl Stream<Item = Result<Update, B::Err>> + Send + '_
where
B: Requester,
B: Requester + Send,
<B as Requester>::GetUpdates: Send,
{
stream::unfold(st, move |state| async move {
let State { timeout, limit, allowed_updates, bot, offset, flag, .. } = &mut *state;
let State { timeout, limit, allowed_updates, bot, offset, flag, force_stop, .. } =
&mut *state;
if *force_stop {
return None;
}
if flag.is_stopped() {
let mut req = bot.get_updates_fault_tolerant();
req.payload_mut().0 = GetUpdates {
offset: Some(*offset),
timeout: Some(0),
limit: Some(1),
allowed_updates: allowed_updates.take(),
};
let mut req = bot.get_updates().offset(*offset).timeout(0).limit(1);
req.payload_mut().allowed_updates = allowed_updates.take();
return match req.send().await {
Ok(_) => None,
Err(err) => Some((Either::Left(stream::once(ready(Err(err)))), state)),
Err(err) => {
// Prevents infinite retries, see https://github.com/teloxide/teloxide/issues/496
*force_stop = true;
Some((Either::Left(stream::once(ready(Err(err)))), state))
}
};
}
let mut req = bot.get_updates_fault_tolerant();
req.payload_mut().0 = GetUpdates {
let mut req = bot.get_updates();
*req.payload_mut() = GetUpdates {
offset: Some(*offset),
timeout: *timeout,
limit: *limit,
allowed_updates: allowed_updates.take(),
};
let updates = match req.send().await {
Err(err) => return Some((Either::Left(stream::once(ready(Err(err)))), state)),
Ok(SemiparsedVec(updates)) => {
match req.send().await {
Ok(updates) => {
// Set offset to the last update's id + 1
if let Some(upd) = updates.last() {
let id: i32 = match upd {
Ok(ok) => ok.id,
Err((value, _)) => value["update_id"]
.as_i64()
.expect("The 'update_id' field must always exist in Update")
.try_into()
.expect("update_id must be i32"),
};
*offset = id + 1;
*offset = upd.id + 1;
}
for update in &updates {
if let Err((value, e)) = update {
log::error!(
"Cannot parse an update.\nError: {:?}\nValue: {}\n\
This is a bug in teloxide-core, please open an issue here: \
https://github.com/teloxide/teloxide-core/issues.",
e,
value
);
}
}
updates.into_iter().filter_map(Result::ok).map(Ok)
let updates = updates.into_iter().map(Ok);
Some((Either::Right(stream::iter(updates)), state))
}
};
Some((Either::Right(stream::iter(updates)), state))
Err(err) => Some((Either::Left(stream::once(ready(Err(err)))), state)),
}
})
.flatten()
}
@ -135,13 +201,14 @@ where
let (token, flag) = AsyncStopToken::new_pair();
let state = State {
bot: requester,
bot,
timeout: timeout.map(|t| t.as_secs().try_into().expect("timeout is too big")),
limit,
allowed_updates,
offset: 0,
flag,
token,
force_stop: false,
};
let stop_token = |st: &mut State<_>| st.token.clone();
@ -169,7 +236,7 @@ where
}
};
let is_webhook_setup = !webhook_info.url.is_empty();
let is_webhook_setup = webhook_info.url.is_some();
if is_webhook_setup {
if let Err(e) = requester.delete_webhook().send().await {
@ -177,3 +244,17 @@ where
}
}
}
#[test]
fn polling_is_send() {
use crate::dispatching::update_listeners::AsUpdateStream;
let bot = crate::Bot::new("TOKEN");
let mut polling = polling(bot, None, None, None);
assert_send(&polling);
assert_send(&polling.as_stream());
assert_send(&polling.stop_token());
fn assert_send(_: &impl Send) {}
}

View file

@ -25,25 +25,23 @@ pub struct StatefulListener<St, Assf, Sf, Hauf, Thf> {
/// The function used as [`AsUpdateStream::as_stream`].
///
/// Must be of type `for<'a> &'a mut St -> impl Stream + 'a` and callable by
/// `&mut`.
/// Must implement `for<'a> FnMut(&'a mut St) -> impl Stream + 'a`.
pub stream: Assf,
/// The function used as [`UpdateListener::stop_token`].
///
/// Must be of type `for<'a> &'a mut St -> impl StopToken`.
/// Must implement `FnMut(&mut St) -> impl StopToken`.
pub stop_token: Sf,
/// The function used as [`UpdateListener::hint_allowed_updates`].
///
/// Must be of type `for<'a, 'b> &'a mut St, &'b mut dyn Iterator<Item =
/// AllowedUpdate> -> ()`.
/// Must implement `FnMut(&mut St, &mut dyn Iterator<Item =
/// AllowedUpdate>)`.
pub hint_allowed_updates: Option<Hauf>,
/// The function used as [`UpdateListener::timeout_hint`].
///
/// Must be of type `for<'a> &'a St -> Option<Duration>` and callable by
/// `&`.
/// Must implement `Fn(&St) -> Option<Duration>`.
pub timeout_hint: Option<Thf>,
}
@ -79,7 +77,7 @@ impl<S, E>
Thfn<S>,
>
where
S: Stream<Item = Result<Update, E>> + Unpin + 'static,
S: Stream<Item = Result<Update, E>> + Unpin + Send + 'static,
{
/// Creates a new update listener from a stream of updates which ignores
/// stop signals.
@ -109,6 +107,7 @@ impl<'a, St, Assf, Sf, Hauf, Thf, Strm, E> AsUpdateStream<'a, E>
for StatefulListener<St, Assf, Hauf, Sf, Thf>
where
(St, Strm): 'a,
Strm: Send,
Assf: FnMut(&'a mut St) -> Strm,
Strm: Stream<Item = Result<Update, E>>,
{

View file

@ -17,6 +17,7 @@ use teloxide_core::{
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
#[derive(Debug)]
#[deprecated(note = "Use dispatching2 instead")]
pub struct UpdateWithCx<R, Upd> {
pub requester: R,
pub update: Upd,

View file

@ -0,0 +1,20 @@
use crate::types::CallbackQuery;
use teloxide_core::types::Message;
/// Something that may has a chat ID.
pub trait GetChatId {
#[must_use]
fn chat_id(&self) -> Option<i64>;
}
impl GetChatId for Message {
fn chat_id(&self) -> Option<i64> {
Some(self.chat.id)
}
}
impl GetChatId for CallbackQuery {
fn chat_id(&self) -> Option<i64> {
self.message.as_ref().map(|mes| mes.chat.id)
}
}

View file

@ -0,0 +1,177 @@
//! Support for user dialogues.
//!
//! The main type is (surprise!) [`Dialogue`]. Under the hood, it is just a
//! wrapper over [`Storage`] and a chat ID. All it does is provides convenient
//! method for manipulating the dialogue state. [`Storage`] is where all
//! dialogue states are stored; it can be either [`InMemStorage`], which is a
//! simple hash map, or database wrappers such as [`SqliteStorage`]. In the
//! latter case, your dialogues are _persistent_, meaning that you can safely
//! restart your bot and all dialogues will remain in the database -- this is a
//! preferred method for production bots.
//!
//! [`examples/dialogue.rs`] clearly demonstrates the typical usage of
//! dialogues. Your dialogue state can be represented as an enumeration:
//!
//! ```ignore
//! #[derive(DialogueState, Clone)]
//! #[handler_out(anyhow::Result<()>)]
//! pub enum State {
//! #[handler(handle_start)]
//! Start,
//!
//! #[handler(handle_receive_full_name)]
//! ReceiveFullName,
//!
//! #[handler(handle_receive_age)]
//! ReceiveAge { full_name: String },
//!
//! #[handler(handle_receive_location)]
//! ReceiveLocation { full_name: String, age: u8 },
//! }
//! ```
//!
//! Each state is associated with its respective handler: e.g., when a dialogue
//! state is `ReceiveAge`, `handle_receive_age` is invoked:
//!
//! ```ignore
//! async fn handle_receive_age(
//! bot: AutoSend<Bot>,
//! msg: Message,
//! dialogue: MyDialogue,
//! (full_name,): (String,), // Available from `State::ReceiveAge`.
//! ) -> anyhow::Result<()> {
//! match msg.text().map(|text| text.parse::<u8>()) {
//! Some(Ok(age)) => {
//! bot.send_message(msg.chat.id, "What's your location?").await?;
//! dialogue.update(State::ReceiveLocation { full_name, age }).await?;
//! }
//! _ => {
//! bot.send_message(msg.chat.id, "Send me a number.").await?;
//! }
//! }
//!
//! Ok(())
//! }
//! ```
//!
//! Variant's fields are passed to state handlers as tuples: `(full_name,):
//! (String,)`. Using [`Dialogue::update`], you can update the dialogue with a
//! new state, in our case -- `State::ReceiveLocation { full_name, age }`. To
//! exit the dialogue, just call [`Dialogue::exit`] and it will be removed from
//! the inner storage:
//!
//! ```ignore
//! async fn handle_receive_location(
//! bot: AutoSend<Bot>,
//! msg: Message,
//! dialogue: MyDialogue,
//! (full_name, age): (String, u8), // Available from `State::ReceiveLocation`.
//! ) -> anyhow::Result<()> {
//! match msg.text() {
//! Some(location) => {
//! let message =
//! format!("Full name: {}\nAge: {}\nLocation: {}", full_name, age, location);
//! bot.send_message(msg.chat.id, message).await?;
//! dialogue.exit().await?;
//! }
//! None => {
//! bot.send_message(msg.chat.id, "Send me a text message.").await?;
//! }
//! }
//!
//! Ok(())
//! }
//! ```
//!
//! [`examples/dialogue.rs`]: https://github.com/teloxide/teloxide/blob/master/examples/dialogue.rs
#[cfg(feature = "redis-storage")]
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "redis-storage")))]
pub use crate::dispatching::dialogue::{RedisStorage, RedisStorageError};
#[cfg(feature = "sqlite-storage")]
pub use crate::dispatching::dialogue::{SqliteStorage, SqliteStorageError};
pub use crate::dispatching::dialogue::{
serializer, InMemStorage, InMemStorageError, Serializer, Storage, TraceStorage,
};
pub use get_chat_id::GetChatId;
use std::{marker::PhantomData, sync::Arc};
mod get_chat_id;
/// A handle for controlling dialogue state.
#[derive(Debug)]
pub struct Dialogue<D, S> {
storage: Arc<S>,
chat_id: i64,
_phantom: PhantomData<D>,
}
// `#[derive]` requires generics to implement `Clone`, but `S` is wrapped around
// `Arc`, and `D` is wrapped around PhantomData.
impl<D, S> Clone for Dialogue<D, S> {
fn clone(&self) -> Self {
Dialogue { storage: self.storage.clone(), chat_id: self.chat_id, _phantom: PhantomData }
}
}
impl<D, S> Dialogue<D, S>
where
D: Send + 'static,
S: Storage<D>,
{
/// Constructs a new dialogue with `storage` (where dialogues are stored)
/// and `chat_id` of a current dialogue.
pub fn new(storage: Arc<S>, chat_id: i64) -> Self {
Self { storage, chat_id, _phantom: PhantomData }
}
/// Retrieves the current state of the dialogue or `None` if there is no
/// dialogue.
pub async fn get(&self) -> Result<Option<D>, S::Error> {
self.storage.clone().get_dialogue(self.chat_id).await
}
/// Like [`Dialogue::get`] but returns a default value if there is no
/// dialogue.
pub async fn get_or_default(&self) -> Result<D, S::Error>
where
D: Default,
{
match self.get().await? {
Some(d) => Ok(d),
None => {
self.storage.clone().update_dialogue(self.chat_id, D::default()).await?;
Ok(D::default())
}
}
}
/// Updates the dialogue state.
///
/// The dialogue type `D` must implement `From<State>` to allow implicit
/// conversion from `State` to `D`.
pub async fn update<State>(&self, state: State) -> Result<(), S::Error>
where
D: From<State>,
{
let new_dialogue = state.into();
self.storage.clone().update_dialogue(self.chat_id, new_dialogue).await?;
Ok(())
}
/// Updates the dialogue with a default value.
pub async fn reset(&self) -> Result<(), S::Error>
where
D: Default,
{
self.update(D::default()).await
}
/// Removes the dialogue from the storage provided to [`Dialogue::new`].
pub async fn exit(&self) -> Result<(), S::Error> {
self.storage.clone().remove_dialogue(self.chat_id).await
}
}

View file

@ -0,0 +1,274 @@
use crate::{
adaptors::CacheMe,
dispatching::{
stop_token::StopToken, update_listeners, update_listeners::UpdateListener, ShutdownToken,
},
error_handlers::{ErrorHandler, LoggingErrorHandler},
requests::Requester,
types::{AllowedUpdate, Update},
utils::shutdown_token::shutdown_check_timeout_for,
};
use dptree::di::{DependencyMap, DependencySupplier};
use futures::{future::BoxFuture, StreamExt};
use std::{collections::HashSet, fmt::Debug, ops::ControlFlow, sync::Arc};
use teloxide_core::requests::{Request, RequesterExt};
use tokio::time::timeout;
use std::future::Future;
/// The builder for [`Dispatcher`].
pub struct DispatcherBuilder<R, Err> {
bot: R,
dependencies: DependencyMap,
handler: UpdateHandler<Err>,
default_handler: DefaultHandler,
error_handler: Arc<dyn ErrorHandler<Err>>,
}
impl<R, Err> DispatcherBuilder<R, Err>
where
R: Clone + Requester + Clone + Send + Sync + 'static,
Err: Debug + Send + Sync + 'static,
{
/// Specifies a handler that will be called for an unhandled update.
///
/// By default, it is a mere [`log::warn`].
#[must_use]
pub fn default_handler<H, Fut>(self, handler: H) -> Self
where
H: Fn(Arc<Update>) -> Fut + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let handler = Arc::new(handler);
Self {
default_handler: Box::new(move |upd| {
let handler = Arc::clone(&handler);
Box::pin(handler(upd))
}),
..self
}
}
/// Specifies a handler that will be called on a handler error.
///
/// By default, it is [`LoggingErrorHandler`].
#[must_use]
pub fn error_handler(self, handler: Arc<dyn ErrorHandler<Err>>) -> Self {
Self { error_handler: handler, ..self }
}
/// Specifies dependencies that can be used inside of handlers.
///
/// By default, there is no dependencies.
#[must_use]
pub fn dependencies(self, dependencies: DependencyMap) -> Self {
Self { dependencies, ..self }
}
/// Constructs [`Dispatcher`].
#[must_use]
pub fn build(self) -> Dispatcher<R, Err> {
Dispatcher {
bot: self.bot.clone(),
cache_me_bot: self.bot.cache_me(),
dependencies: self.dependencies,
handler: self.handler,
default_handler: self.default_handler,
error_handler: self.error_handler,
allowed_updates: Default::default(),
state: ShutdownToken::new(),
}
}
}
/// The base for update dispatching.
pub struct Dispatcher<R, Err> {
bot: R,
cache_me_bot: CacheMe<R>,
dependencies: DependencyMap,
handler: UpdateHandler<Err>,
default_handler: DefaultHandler,
error_handler: Arc<dyn ErrorHandler<Err>>,
// TODO: respect allowed_udpates
allowed_updates: HashSet<AllowedUpdate>,
state: ShutdownToken,
}
// TODO: it is allowed to return message as response on telegram request in
// webhooks, so we can allow this too. See more there: https://core.telegram.org/bots/api#making-requests-when-getting-updates
/// A handler that processes updates from Telegram.
pub type UpdateHandler<Err> = dptree::Handler<'static, DependencyMap, Result<(), Err>>;
type DefaultHandler = Box<dyn Fn(Arc<Update>) -> BoxFuture<'static, ()>>;
impl<R, Err> Dispatcher<R, Err>
where
R: Requester + Clone + Send + Sync + 'static,
Err: Send + Sync + 'static,
{
/// Constructs a new [`DispatcherBuilder`] with `bot` and `handler`.
#[must_use]
pub fn builder(bot: R, handler: UpdateHandler<Err>) -> DispatcherBuilder<R, Err>
where
Err: Debug,
{
DispatcherBuilder {
bot,
dependencies: DependencyMap::new(),
handler,
default_handler: Box::new(|upd| {
log::warn!("Unhandled update: {:?}", upd);
Box::pin(async {})
}),
error_handler: LoggingErrorHandler::new(),
}
}
/// Starts your bot with the default parameters.
///
/// The default parameters are a long polling update listener and log all
/// errors produced by this listener.
///
/// Each time a handler is invoked, [`Dispatcher`] adds the following
/// dependencies (in addition to those passed to
/// [`DispatcherBuilder::dependencies`]):
///
/// - Your bot passed to [`Dispatcher::builder`];
/// - An update from Telegram;
/// - [`crate::types::Me`] (can be used in [`HandlerExt::filter_command`]).
///
/// [`shutdown`]: ShutdownToken::shutdown
/// [a ctrlc signal]: Dispatcher::setup_ctrlc_handler
/// [`HandlerExt::filter_command`]: crate::dispatching2::HandlerExt::filter_command
pub async fn dispatch(&mut self)
where
R: Requester + Clone,
<R as Requester>::GetUpdates: Send,
{
let listener = update_listeners::polling_default(self.bot.clone()).await;
let error_handler =
LoggingErrorHandler::with_custom_text("An error from the update listener");
self.dispatch_with_listener(listener, error_handler).await;
}
/// Starts your bot with custom `update_listener` and
/// `update_listener_error_handler`.
///
/// This method adds the same dependencies as [`Dispatcher::dispatch`].
///
/// [`shutdown`]: ShutdownToken::shutdown
/// [a ctrlc signal]: Dispatcher::setup_ctrlc_handler
pub async fn dispatch_with_listener<'a, UListener, ListenerE, Eh>(
&'a mut self,
mut update_listener: UListener,
update_listener_error_handler: Arc<Eh>,
) where
UListener: UpdateListener<ListenerE> + 'a,
Eh: ErrorHandler<ListenerE> + 'a,
ListenerE: Debug,
{
update_listener.hint_allowed_updates(&mut self.allowed_updates.clone().into_iter());
let shutdown_check_timeout = shutdown_check_timeout_for(&update_listener);
let mut stop_token = Some(update_listener.stop_token());
self.state.start_dispatching();
{
let stream = update_listener.as_stream();
tokio::pin!(stream);
loop {
// False positive
#[allow(clippy::collapsible_match)]
if let Ok(upd) = timeout(shutdown_check_timeout, stream.next()).await {
match upd {
None => break,
Some(upd) => self.process_update(upd, &update_listener_error_handler).await,
}
}
if self.state.is_shutting_down() {
if let Some(token) = stop_token.take() {
log::debug!("Start shutting down dispatching...");
token.stop();
break;
}
}
}
}
// TODO: wait for executing handlers?
self.state.done();
}
async fn process_update<LErr, LErrHandler>(
&self,
update: Result<Update, LErr>,
err_handler: &Arc<LErrHandler>,
) where
LErrHandler: ErrorHandler<LErr>,
{
match update {
Ok(upd) => {
let mut deps = self.dependencies.clone();
deps.insert(upd);
deps.insert(self.bot.clone());
deps.insert(
self.cache_me_bot.get_me().send().await.expect("Failed to retrieve 'me'"),
);
match self.handler.dispatch(deps).await {
ControlFlow::Break(Ok(())) => {}
ControlFlow::Break(Err(err)) => {
self.error_handler.clone().handle_error(err).await
}
ControlFlow::Continue(deps) => {
let upd = deps.get();
(self.default_handler)(upd).await;
}
}
}
Err(err) => err_handler.clone().handle_error(err).await,
}
}
/// Setups the `^C` handler that [`shutdown`]s dispatching.
///
/// [`shutdown`]: ShutdownToken::shutdown
#[cfg(feature = "ctrlc_handler")]
#[cfg_attr(docsrs, doc(cfg(feature = "ctrlc_handler")))]
pub fn setup_ctrlc_handler(&mut self) -> &mut Self {
let token = self.state.clone();
tokio::spawn(async move {
loop {
tokio::signal::ctrl_c().await.expect("Failed to listen for ^C");
match token.shutdown() {
Ok(f) => {
log::info!("^C received, trying to shutdown the dispatcher...");
f.await;
log::info!("dispatcher is shutdown...");
}
Err(_) => {
log::info!("^C received, the dispatcher isn't running, ignoring the signal")
}
}
}
});
self
}
/// Returns a shutdown token, which can later be used to shutdown
/// dispatching.
pub fn shutdown_token(&self) -> ShutdownToken {
self.state.clone()
}
}

View file

@ -0,0 +1,108 @@
#![allow(clippy::redundant_closure_call)]
use dptree::{di::DependencyMap, Handler};
use teloxide_core::types::{Message, Update, UpdateKind};
macro_rules! define_ext {
($ext_name:ident, $for_ty:ty => $( ($func:ident, $proj_fn:expr, $fn_doc:expr) ,)*) => {
#[doc = concat!("Filter methods for [`", stringify!($for_ty), "`].")]
pub trait $ext_name<Out>: private::Sealed {
$( define_ext!(@sig $func, $fn_doc); )*
}
impl<Out> $ext_name<Out> for $for_ty
where
Out: Send + Sync + 'static,
{
$( define_ext!(@impl $for_ty, $func, $proj_fn); )*
}
};
(@sig $func:ident, $fn_doc:expr) => {
#[doc = $fn_doc]
fn $func() -> Handler<'static, DependencyMap, Out>;
};
(@impl $for_ty:ty, $func:ident, $proj_fn:expr) => {
fn $func() -> Handler<'static, DependencyMap, Out> {
dptree::filter_map(move |input: $for_ty| {
$proj_fn(input)
})
}
};
}
mod private {
use teloxide_core::types::{Message, Update};
pub trait Sealed {}
impl Sealed for Update {}
impl Sealed for Message {}
}
macro_rules! define_message_ext {
($( ($func:ident, $fn_name:path) ,)*) => {
define_ext! {
MessageFilterExt, crate::types::Message =>
$((
$func,
(|x| $fn_name(&x).map(ToOwned::to_owned)),
concat!("Applies the [`crate::types::", stringify!($fn_name), "`] filter.")
),)*
}
}
}
// May be expanded in the future.
define_message_ext! {
(filter_from, Message::from),
(filter_animation, Message::animation),
(filter_audio, Message::audio),
(filter_contact, Message::contact),
(filter_document, Message::document),
(filter_location, Message::location),
(filter_photo, Message::photo),
(filter_poll, Message::poll),
(filter_sticker, Message::sticker),
(filter_text, Message::text),
(filter_reply_to_message, Message::reply_to_message),
(filter_forward_from, Message::forward_from),
(filter_new_chat_members, Message::new_chat_members),
(filter_left_chat_member, Message::left_chat_member),
(filter_pinned, Message::pinned_message),
(filter_dice, Message::dice),
}
macro_rules! define_update_ext {
($( ($func:ident, $kind:path) ,)*) => {
define_ext! {
UpdateFilterExt, crate::types::Update =>
$((
$func,
|update: Update| match update.kind {
$kind(x) => Some(x),
_ => None,
},
concat!("Filters out [`crate::types::", stringify!($kind), "`] objects.")
),)*
}
}
}
// May be expanded in the future.
define_update_ext! {
(filter_message, UpdateKind::Message),
(filter_edited_message, UpdateKind::EditedMessage),
(filter_channel_post, UpdateKind::ChannelPost),
(filter_edited_channel_post, UpdateKind::EditedChannelPost),
(filter_inline_query, UpdateKind::InlineQuery),
(filter_chosen_inline_result, UpdateKind::ChosenInlineResult),
(filter_callback_query, UpdateKind::CallbackQuery),
(filter_shipping_query, UpdateKind::ShippingQuery),
(filter_pre_checkout_query, UpdateKind::PreCheckoutQuery),
(filter_poll, UpdateKind::Poll),
(filter_poll_answer, UpdateKind::PollAnswer),
(filter_my_chat_member, UpdateKind::MyChatMember),
(filter_chat_member, UpdateKind::ChatMember),
}

View file

@ -0,0 +1,101 @@
use std::sync::Arc;
use crate::{
dispatching2::{
dialogue::{Dialogue, GetChatId, Storage},
HandlerFactory,
},
types::{Me, Message},
utils::command::BotCommand,
};
use dptree::{di::DependencyMap, Handler};
use std::fmt::Debug;
/// Extension methods for working with `dptree` handlers.
pub trait HandlerExt<Output> {
/// Returns a handler that accepts a parsed command `C`.
///
/// ## Dependency requirements
///
/// - [`crate::types::Message`]
/// - [`crate::types::Me`]
#[must_use]
fn filter_command<C>(self) -> Self
where
C: BotCommand + Send + Sync + 'static;
/// Passes [`Dialogue<D, S>`] and `D` as handler dependencies.
///
/// It does so by the following steps:
///
/// 1. If an incoming update has no chat ID ([`GetChatId::chat_id`] returns
/// `None`), the rest of the chain will not be executed. Otherwise, passes
/// `Dialogue::new(storage, chat_id)` forwards.
/// 2. If [`Dialogue::get_or_default`] on the passed dialogue returns `Ok`,
/// passes the dialogue state forwards. Otherwise, logs an error and the
/// rest of the chain is not executed.
///
/// ## Dependency requirements
///
/// - `Arc<S>`
/// - `Upd`
///
/// [`Dialogue<D, S>`]: Dialogue
#[must_use]
fn enter_dialogue<Upd, S, D>(self) -> Self
where
S: Storage<D> + Send + Sync + 'static,
<S as Storage<D>>::Error: Debug + Send,
D: Default + Send + Sync + 'static,
Upd: GetChatId + Clone + Send + Sync + 'static;
#[must_use]
fn dispatch_by<F>(self) -> Self
where
F: HandlerFactory<Out = Output>;
}
impl<Output> HandlerExt<Output> for Handler<'static, DependencyMap, Output>
where
Output: Send + Sync + 'static,
{
fn filter_command<C>(self) -> Self
where
C: BotCommand + Send + Sync + 'static,
{
self.chain(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())
}))
}
fn enter_dialogue<Upd, S, D>(self) -> Self
where
S: Storage<D> + Send + Sync + 'static,
<S as Storage<D>>::Error: Debug + Send,
D: Default + Send + Sync + 'static,
Upd: GetChatId + Clone + Send + Sync + 'static,
{
self.chain(dptree::filter_map(|storage: Arc<S>, upd: Upd| {
let chat_id = upd.chat_id()?;
Some(Dialogue::new(storage, chat_id))
}))
.chain(dptree::filter_map_async(|dialogue: Dialogue<D, S>| async move {
match dialogue.get_or_default().await {
Ok(dialogue) => Some(dialogue),
Err(err) => {
log::error!("dialogue.get_or_default() failed: {:?}", err);
None
}
}
}))
}
fn dispatch_by<F>(self) -> Self
where
F: HandlerFactory<Out = Output>,
{
self.chain(F::handler())
}
}

View file

@ -0,0 +1,8 @@
use dptree::{di::DependencyMap, Handler};
/// Something that can construct a handler.
pub trait HandlerFactory {
type Out;
fn handler() -> Handler<'static, DependencyMap, Self::Out>;
}

109
src/dispatching2/mod.rs Normal file
View file

@ -0,0 +1,109 @@
//! A new dispatching model based on [`dptree`].
//!
//! In teloxide, updates are dispatched by a pipleine. The central type is
//! [`dptree::Handler`] -- it represents a handler of an update; since the API
//! is highly declarative, you can combine handlers with each other via such
//! methods as [`dptree::Handler::chain`] and [`dptree::Handler::branch`]. The
//! former method pipes one handler to another one, whilst the latter creates a
//! new node, as communicated by the name. For more information, please refer to
//! the documentation of [`dptree`].
//!
//! The pattern itself is called [chain of responsibility], a well-known design
//! technique across OOP developers. But unlike typical object-oriented design,
//! we employ declarative FP-style functions like [`dptree::filter`],
//! [`dptree::filter_map`], and [`dptree::endpoint`]; these functions create
//! special forms of [`dptree::Handler`]; for more information, please refer to
//! their respective documentation. Each of these higher-order functions accept
//! a closure that is made into a handler -- this closure can take any
//! additional parameters, which must be supplied while creating [`Dispatcher`]
//! (see [`DispatcherBuilder::dependencies`]).
//!
//! The [`Dispatcher`] type puts all these things together: it only provides
//! [`Dispatcher::dispatch`] and a handful of other methods. Once you call
//! `.dispatch()`, it will retrieve updates from the Telegram server and pass
//! them to your handler, which is a parameter of [`Dispatcher::builder`].
//!
//! Let us look at a simple example:
//!
//!
//! ([Full](https://github.com/teloxide/teloxide/blob/master/examples/shared_state.rs))
//!
//! ```no_run
//! use std::sync::atomic::{AtomicU64, Ordering};
//!
//! use once_cell::sync::Lazy;
//! use teloxide::prelude2::*;
//!
//! static MESSAGES_TOTAL: Lazy<AtomicU64> = Lazy::new(AtomicU64::default);
//!
//! # #[tokio::main]
//! # async fn main() {
//! teloxide::enable_logging!();
//! log::info!("Starting shared_state_bot...");
//!
//! let bot = Bot::from_env().auto_send();
//!
//! let handler = Update::filter_message().branch(dptree::endpoint(
//! |msg: Message, bot: AutoSend<Bot>| async move {
//! let previous = MESSAGES_TOTAL.fetch_add(1, Ordering::Relaxed);
//! bot.send_message(msg.chat.id, format!("I received {} messages in total.", previous))
//! .await?;
//! respond(())
//! },
//! ));
//!
//! Dispatcher::builder(bot, handler).build().setup_ctrlc_handler().dispatch().await;
//! # }
//! ```
//!
//! 1. First, we create the bot: `let bot = Bot::from_env().auto_send()`.
//! 2. Then we construct an update handler. While it is possible to handle all
//! kinds of [`crate::types::Update`], here we are only interested in
//! [`crate::types::Message`]: [`UpdateFilterExt::filter_message`] create a
//! handler object which filters all messages out of a generic update.
//! 3. By doing `.branch(dptree::endpoint(...))`, we set up a custom handling
//! closure that receives `msg: Message` and `bot: AutoSend<Bot>`. There are
//! called dependencies: `msg` is supplied by
//! [`UpdateFilterExt::filter_message`], while `bot` is supplied by
//! [`Dispatcher`].
//!
//! That being said, if we receive a message, the dispatcher will call our
//! handler, but if we receive something other than a message (e.g., a channel
//! post), you will see an unhandled update notice in your terminal.
//!
//! This is a very limited example of update pipelining facilities. In more
//! involved scenarios, there are multiple branches and chains; if one element
//! of a chain fails to handle an update, the update will be passed forwards; if
//! no handler succeeds at handling the update, [`Dispatcher`] will invoke a
//! default handler set up via [`DispatcherBuilder::default_handler`].
//!
//! Update pipelining provides several advantages over the typical `match
//! (update.kind) { ... }` approach:
//!
//! 1. It supports _extension_: e.g., you
//! can define extension filters or some other handlers and then combine them in
//! a single place, thus facilitating loose coupling.
//! 2. Pipelining exhibits a natural syntax for expressing message processing.
//! 3. Lastly, it provides a primitive form of [dependency injection (DI)],
//! which allows you to deal with such objects as a bot and various update types
//! easily.
//!
//! For a more involved example, see [`examples/dispatching2_features.rs`](https://github.com/teloxide/teloxide/blob/master/examples/dispatching2_features.rs).
//!
//! TODO: explain a more involved example with multiple branches.
//!
//! [chain of responsibility]: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern
//! [dependency injection (DI)]: https://en.wikipedia.org/wiki/Dependency_injection
pub mod repls;
pub mod dialogue;
mod dispatcher;
mod filter_ext;
mod handler_ext;
mod handler_factory;
pub use dispatcher::{Dispatcher, DispatcherBuilder, UpdateHandler};
pub use filter_ext::{MessageFilterExt, UpdateFilterExt};
pub use handler_ext::HandlerExt;
pub use handler_factory::HandlerFactory;

View file

@ -0,0 +1,97 @@
use crate::{
dispatching::{update_listeners, update_listeners::UpdateListener},
dispatching2::{HandlerExt, UpdateFilterExt},
error_handlers::LoggingErrorHandler,
types::Update,
utils::command::BotCommand,
};
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.
///
/// ## Caution
/// **DO NOT** use this function together with [`Dispatcher`] and other REPLs,
/// because Telegram disallow multiple requests at the same time from the same
/// bot.
///
/// ## Dependency requirements
///
/// - Those of [`HandlerExt::filter_command`].
///
/// [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<Cmd>)
where
Cmd: BotCommand + Send + Sync + 'static,
H: Injectable<DependencyMap, Result<(), E>, Args> + Send + Sync + 'static,
R: Requester + Clone + Send + Sync + 'static,
<R as Requester>::GetUpdates: Send,
E: Debug + Send + Sync + 'static,
{
let cloned_bot = bot.clone();
commands_repl_with_listener(
bot,
handler,
update_listeners::polling_default(cloned_bot).await,
cmd,
)
.await;
}
/// Like [`commands_repl`], but with a custom [`UpdateListener`].
///
/// All errors from an update listener and handler will be logged.
///
/// ## Caution
/// **DO NOT** use this function together with [`Dispatcher`] and other REPLs,
/// because Telegram disallow multiple requests at the same time from the same
/// bot.
///
/// ## Dependency requirements
///
/// - Those of [`HandlerExt::filter_command`].
///
/// [`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>(
bot: R,
handler: H,
listener: L,
_cmd: PhantomData<Cmd>,
) where
Cmd: BotCommand + Send + Sync + 'static,
H: Injectable<DependencyMap, Result<(), E>, Args> + Send + Sync + 'static,
L: UpdateListener<ListenerE> + Send + 'a,
ListenerE: Debug + Send + 'a,
R: Requester + Clone + Send + Sync + 'static,
E: Debug + Send + Sync + 'static,
{
use crate::dispatching2::Dispatcher;
let mut dispatcher = Dispatcher::builder(
bot,
Update::filter_message().filter_command::<Cmd>().branch(dptree::endpoint(handler)),
)
.build();
#[cfg(feature = "ctrlc_handler")]
dispatcher.setup_ctrlc_handler();
// To make mutable var from immutable.
let mut dispatcher = dispatcher;
dispatcher
.dispatch_with_listener(
listener,
LoggingErrorHandler::with_custom_text("An error from the update listener"),
)
.await;
}

View file

@ -0,0 +1,96 @@
use crate::{
dispatching::{
dialogue::{DialogueDispatcher, DialogueStage, DialogueWithCx, InMemStorageError},
update_listeners,
update_listeners::UpdateListener,
Dispatcher, UpdateWithCx,
},
error_handlers::LoggingErrorHandler,
};
use std::{fmt::Debug, future::Future, sync::Arc};
use teloxide_core::{requests::Requester, types::Message};
/// A [REPL] for dialogues.
///
/// All errors from an update listener and handler will be logged. This function
/// uses [`InMemStorage`].
///
/// # Caution
/// **DO NOT** use this function together with [`Dispatcher`] and other REPLs,
/// because Telegram disallow multiple requests at the same time from the same
/// bot.
///
/// [REPL]: https://en.wikipedia.org/wiki/Read-eval-print_loop
/// [`Dispatcher`]: crate::dispatching::Dispatcher
/// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage
#[cfg(feature = "ctrlc_handler")]
pub async fn dialogues_repl<'a, R, H, D, Fut>(requester: R, handler: H)
where
H: Fn(UpdateWithCx<R, Message>, D) -> Fut + Send + Sync + 'static,
D: Clone + Default + Send + 'static,
Fut: Future<Output = DialogueStage<D>> + Send + 'static,
R: Requester + Send + Clone + 'static,
<R as Requester>::GetUpdatesFaultTolerant: Send,
{
let cloned_requester = requester.clone();
dialogues_repl_with_listener(
requester,
handler,
update_listeners::polling_default(cloned_requester).await,
)
.await;
}
/// Like [`dialogues_repl`], but with a custom [`UpdateListener`].
///
/// All errors from an update listener and handler will be logged. This function
/// uses [`InMemStorage`].
///
/// # Caution
/// **DO NOT** use this function together with [`Dispatcher`] and other REPLs,
/// because Telegram disallow multiple requests at the same time from the same
/// bot.
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
/// [`dialogues_repl`]: crate::dispatching::repls::dialogues_repl()
/// [`UpdateListener`]: crate::dispatching::update_listeners::UpdateListener
/// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage
#[cfg(feature = "ctrlc_handler")]
pub async fn dialogues_repl_with_listener<'a, R, H, D, Fut, L, ListenerE>(
requester: R,
handler: H,
listener: L,
) where
H: Fn(UpdateWithCx<R, Message>, D) -> Fut + Send + Sync + 'static,
D: Clone + Default + Send + 'static,
Fut: Future<Output = DialogueStage<D>> + Send + 'static,
L: UpdateListener<ListenerE> + Send + 'a,
ListenerE: Debug + Send + 'a,
R: Requester + Send + Clone + 'static,
{
let handler = Arc::new(handler);
Dispatcher::new(requester)
.messages_handler(DialogueDispatcher::new(
move |DialogueWithCx { cx, dialogue }: DialogueWithCx<
R,
Message,
D,
InMemStorageError,
>| {
let handler = Arc::clone(&handler);
async move {
let dialogue = dialogue.expect("std::convert::Infallible");
handler(cx, dialogue).await
}
},
))
.setup_ctrlc_handler()
.dispatch_with_listener(
listener,
LoggingErrorHandler::with_custom_text("An error from the update listener"),
)
.await;
}

View file

@ -0,0 +1,9 @@
//! REPLs for dispatching updates.
//mod dialogues_repl;
mod commands_repl;
mod repl;
pub use commands_repl::{commands_repl, commands_repl_with_listener};
//pub use dialogues_repl::{dialogues_repl, dialogues_repl_with_listener};
pub use repl::{repl, repl_with_listener};

View file

@ -0,0 +1,73 @@
use crate::{
dispatching::{update_listeners, update_listeners::UpdateListener},
dispatching2::UpdateFilterExt,
error_handlers::{LoggingErrorHandler, OnError},
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.
///
/// # Caution
/// **DO NOT** use this function together with [`Dispatcher`] and other REPLs,
/// because Telegram disallow multiple requests at the same time from the same
/// bot.
///
/// [REPL]: https://en.wikipedia.org/wiki/Read-eval-print_loop
/// [`Dispatcher`]: crate::dispatching::Dispatcher
#[cfg(feature = "ctrlc_handler")]
pub async fn repl<R, H, E, Args>(bot: R, handler: H)
where
H: Injectable<DependencyMap, Result<(), E>, Args> + Send + Sync + 'static,
Result<(), E>: OnError<E>,
E: Debug + Send + Sync + 'static,
R: Requester + Send + Sync + Clone + 'static,
<R as Requester>::GetUpdates: Send,
{
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`].
///
/// All errors from an update listener and handler will be logged.
///
/// # Caution
/// **DO NOT** use this function together with [`Dispatcher`] and other REPLs,
/// because Telegram disallow multiple requests at the same time from the same
/// bot.
///
/// [`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)
where
H: Injectable<DependencyMap, Result<(), E>, Args> + Send + Sync + 'static,
L: UpdateListener<ListenerE> + Send + 'a,
ListenerE: Debug,
Result<(), E>: OnError<E>,
E: Debug + Send + Sync + 'static,
R: Requester + Clone + Send + Sync + 'static,
{
use crate::dispatching2::Dispatcher;
#[allow(unused_mut)]
let mut dispatcher =
Dispatcher::builder(bot, Update::filter_message().branch(dptree::endpoint(handler)))
.build();
#[cfg(feature = "ctrlc_handler")]
dispatcher.setup_ctrlc_handler();
dispatcher
.dispatch_with_listener(
listener,
LoggingErrorHandler::with_custom_text("An error from the update listener"),
)
.await;
}

View file

@ -10,8 +10,11 @@
| `native-tls` | Enables the [`native-tls`] TLS implementation (enabled by default). |
| `rustls` | Enables the [`rustls`] TLS implementation. |
| `ctrlc_handler` | Enables the [`Dispatcher::setup_ctrlc_handler`](dispatching::Dispatcher::setup_ctrlc_handler) function. |
| `auto-send` | Enables the `AutoSend` bot adaptor. |
| `cache-me` | Enables the `CacheMe` bot adaptor. |
| `auto-send` | Enables the [`AutoSend`](adaptors::AutoSend) bot adaptor. |
| `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. |
| `frunk` | Enables [`teloxide::utils::UpState`]. |
| `full` | Enables all the features except `nightly`. |
| `nightly` | Enables nightly-only features (see the [teloxide-core features]). |

View file

@ -6,17 +6,17 @@
//!
//! ([Full](https://github.com/teloxide/teloxide/blob/master/examples/dices_bot/src/main.rs))
//! ```no_run
//! use teloxide::prelude::*;
//! use teloxide::prelude2::*;
//!
//! # #[tokio::main]
//! # async fn main_() {
//! # async fn main() {
//! teloxide::enable_logging!();
//! log::info!("Starting dices_bot...");
//!
//! let bot = Bot::from_env().auto_send();
//!
//! teloxide::repl(bot, |message| async move {
//! message.answer_dice().await?;
//! teloxide::repls2::repl(bot, |message: Message, bot: AutoSend<Bot>| async move {
//! bot.send_dice(message.chat.id).await?;
//! respond(())
//! })
//! .await;
@ -44,8 +44,6 @@
html_logo_url = "https://github.com/teloxide/teloxide/raw/master/ICON.png",
html_favicon_url = "https://github.com/teloxide/teloxide/raw/master/ICON.png"
)]
#![allow(clippy::match_bool)]
#![forbid(unsafe_code)]
// We pass "--cfg docsrs" when building docs to add `This is supported on
// feature="..." only.`
//
@ -56,21 +54,33 @@
// $ RUSTFLAGS="--cfg dep_docsrs" RUSTDOCFLAGS="--cfg docsrs -Znormalize-docs" cargo +nightly doc --open --all-features
// ```
#![cfg_attr(all(docsrs, feature = "nightly"), feature(doc_cfg))]
#![forbid(unsafe_code)]
#![warn(rustdoc::broken_intra_doc_links)]
#![allow(clippy::match_bool)]
#![allow(clippy::redundant_pattern_matching)]
// https://github.com/rust-lang/rust-clippy/issues/7422
#![allow(clippy::nonstandard_macro_braces)]
#[cfg(feature = "ctrlc_handler")]
pub use dispatching::repls::{
commands_repl, commands_repl_with_listener, dialogues_repl, dialogues_repl_with_listener, repl,
repl_with_listener,
};
#[cfg(feature = "dispatching2")]
pub use dispatching2::repls as repls2;
mod logging;
// Things from this module is also used for the dispatching2 module.
pub mod dispatching;
#[cfg(feature = "dispatching2")]
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "dispatching2")))]
pub mod dispatching2;
pub mod error_handlers;
pub mod prelude;
#[cfg(feature = "dispatching2")]
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "dispatching2")))]
pub mod prelude2;
pub mod utils;
#[doc(inline)]
@ -80,6 +90,9 @@ pub use teloxide_core::*;
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "macros")))]
pub use teloxide_macros as macros;
#[cfg(feature = "dispatching2")]
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "dispatching2")))]
pub use dptree;
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "macros")))]
#[cfg(feature = "macros")]
pub use teloxide_macros::teloxide;

View file

@ -3,12 +3,8 @@
/// A logger will **only** print errors, warnings, and general information from
/// teloxide and **all** logs from your program.
///
/// # Example
/// ```no_compile
/// teloxide::enable_logging!();
/// ```
///
/// # Note
///
/// Calling this macro **is not mandatory**; you can setup if your own logger if
/// you want.
///
@ -27,6 +23,7 @@ macro_rules! enable_logging {
/// 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):
///
@ -35,6 +32,7 @@ macro_rules! enable_logging {
/// ```
///
/// # Note
///
/// Calling this macro **is not mandatory**; you can setup if your own logger if
/// you want.
///
@ -45,7 +43,7 @@ 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_PKG_NAME").replace("-", "_")), $filter)
.filter(Some(&env!("CARGO_CRATE_NAME").replace("-", "_")), $filter)
.filter(Some("teloxide"), log::LevelFilter::Info)
.init();
};

View file

@ -1,17 +1,21 @@
//! Commonly used items.
#![deprecated(note = "Use dispatching2 instead")]
#![allow(deprecated)]
pub use crate::{
dispatching::{
dialogue::{
exit, next, DialogueDispatcher, DialogueStage, DialogueWithCx, GetChatId, Transition,
TransitionIn, TransitionOut,
},
Dispatcher, DispatcherHandlerRx, DispatcherHandlerRxExt, UpdateWithCx,
},
error_handlers::{LoggingErrorHandler, OnError},
respond,
};
pub use crate::dispatching::{
dialogue::{
exit, next, DialogueDispatcher, DialogueStage, DialogueWithCx, GetChatId, Transition,
TransitionIn, TransitionOut,
},
Dispatcher, DispatcherHandlerRx, DispatcherHandlerRxExt, UpdateWithCx,
};
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "macros")))]
#[cfg(feature = "macros")]
pub use crate::teloxide;

27
src/prelude2.rs Normal file
View file

@ -0,0 +1,27 @@
//! Commonly used items (`dispatching2`).
pub use crate::{
error_handlers::{LoggingErrorHandler, OnError},
respond,
};
pub use crate::dispatching2::{
dialogue::Dialogue, Dispatcher, HandlerExt as _, MessageFilterExt as _, UpdateFilterExt as _,
};
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "macros")))]
#[cfg(feature = "macros")]
pub use crate::teloxide;
pub use teloxide_core::types::{
CallbackQuery, ChatMemberUpdated, ChosenInlineResult, InlineQuery, Message, Poll, PollAnswer,
PreCheckoutQuery, ShippingQuery, Update,
};
#[cfg(feature = "auto-send")]
pub use crate::adaptors::AutoSend;
#[doc(no_inline)]
pub use teloxide_core::prelude::*;
pub use dptree::{self, prelude::*};

View file

@ -51,6 +51,7 @@ use std::{
fmt::{Display, Formatter},
};
use std::marker::PhantomData;
#[cfg(feature = "macros")]
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "macros")))]
pub use teloxide_macros::BotCommand;
@ -208,6 +209,10 @@ pub trait BotCommand: Sized {
fn parse<N>(s: &str, bot_name: N) -> Result<Self, ParseError>
where
N: Into<String>;
fn ty() -> PhantomData<Self> {
PhantomData
}
fn bot_commands() -> Vec<crate::types::BotCommand>;
}
pub type PrefixedBotCommand = String;

View file

@ -61,7 +61,7 @@ pub fn code_block(code: &str) -> String {
pub fn code_block_with_lang(code: &str, lang: &str) -> String {
format!(
"<pre><code class=\"language-{}\">{}</code></pre>",
escape(lang).replace("\"", "&quot;"),
escape(lang).replace('"', "&quot;"),
escape(code)
)
}
@ -81,7 +81,7 @@ pub fn code_inline(s: &str) -> String {
///
/// [spec]: https://core.telegram.org/bots/api#html-style
pub fn escape(s: &str) -> String {
s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
}
pub fn user_mention_or_link(user: &User) -> String {

View file

@ -89,36 +89,36 @@ pub fn code_inline(s: &str) -> String {
///
/// [spec]: https://core.telegram.org/bots/api#html-style
pub fn escape(s: &str) -> String {
s.replace("_", r"\_")
.replace("*", r"\*")
.replace("[", r"\[")
.replace("]", r"\]")
.replace("(", r"\(")
.replace(")", r"\)")
.replace("~", r"\~")
.replace("`", r"\`")
.replace(">", r"\>")
.replace("#", r"\#")
.replace("+", r"\+")
.replace("-", r"\-")
.replace("=", r"\=")
.replace("|", r"\|")
.replace("{", r"\{")
.replace("}", r"\}")
.replace(".", r"\.")
.replace("!", r"\!")
s.replace('_', r"\_")
.replace('*', r"\*")
.replace('[', r"\[")
.replace(']', r"\]")
.replace('(', r"\(")
.replace(')', r"\)")
.replace('~', r"\~")
.replace('`', r"\`")
.replace('>', r"\>")
.replace('#', r"\#")
.replace('+', r"\+")
.replace('-', r"\-")
.replace('=', r"\=")
.replace('|', r"\|")
.replace('{', r"\{")
.replace('}', r"\}")
.replace('.', r"\.")
.replace('!', r"\!")
}
/// Escapes all markdown special characters specific for the inline link URL
/// (``` and `)`).
pub fn escape_link_url(s: &str) -> String {
s.replace("`", r"\`").replace(")", r"\)")
s.replace('`', r"\`").replace(')', r"\)")
}
/// Escapes all markdown special characters specific for the code block (``` and
/// `\`).
pub fn escape_code(s: &str) -> String {
s.replace(r"\", r"\\").replace("`", r"\`")
s.replace('\\', r"\\").replace('`', r"\`")
}
pub fn user_mention_or_link(user: &User) -> String {

View file

@ -3,6 +3,7 @@
pub mod command;
pub mod html;
pub mod markdown;
pub(crate) mod shutdown_token;
mod up_state;
pub use teloxide_core::net::client_from_env;

165
src/utils/shutdown_token.rs Normal file
View file

@ -0,0 +1,165 @@
use std::{
fmt,
future::Future,
sync::{
atomic::{AtomicU8, Ordering},
Arc,
},
time::Duration,
};
use tokio::sync::Notify;
use crate::dispatching::update_listeners::UpdateListener;
/// A token which used to shutdown [`Dispatcher`].
#[derive(Clone)]
pub struct ShutdownToken {
dispatcher_state: Arc<DispatcherState>,
shutdown_notify_back: Arc<Notify>,
}
/// This error is returned from [`ShutdownToken::shutdown`] when trying to
/// shutdown an idle [`Dispatcher`].
#[derive(Debug)]
pub struct IdleShutdownError;
impl ShutdownToken {
/// Tries to shutdown dispatching.
///
/// Returns an error if the dispatcher is idle at the moment.
///
/// If you don't need to wait for shutdown, the returned future can be
/// ignored.
pub fn shutdown(&self) -> Result<impl Future<Output = ()> + '_, IdleShutdownError> {
match shutdown_inner(&self.dispatcher_state) {
Ok(()) | Err(Ok(AlreadyShuttingDown)) => Ok(async move {
log::info!("Trying to shutdown the dispatcher...");
self.shutdown_notify_back.notified().await
}),
Err(Err(err)) => Err(err),
}
}
pub(crate) fn new() -> Self {
Self {
dispatcher_state: Arc::new(DispatcherState {
inner: AtomicU8::new(ShutdownState::Idle as _),
}),
shutdown_notify_back: <_>::default(),
}
}
pub(crate) fn start_dispatching(&self) {
if let Err(actual) =
self.dispatcher_state.compare_exchange(ShutdownState::Idle, ShutdownState::Running)
{
panic!(
"Dispatching is already running: expected `{:?}` state, found `{:?}`",
ShutdownState::Idle,
actual
);
}
}
pub(crate) fn is_shutting_down(&self) -> bool {
matches!(self.dispatcher_state.load(), ShutdownState::ShuttingDown)
}
pub(crate) fn done(&self) {
if self.is_shutting_down() {
// Stopped because of a `shutdown` call.
// Notify `shutdown`s that we finished
self.shutdown_notify_back.notify_waiters();
log::info!("Dispatching has been shut down.");
} else {
log::info!("Dispatching has been stopped (listener returned `None`).");
}
self.dispatcher_state.store(ShutdownState::Idle);
}
}
impl fmt::Display for IdleShutdownError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Dispatcher was idle and as such couldn't be shut down")
}
}
impl std::error::Error for IdleShutdownError {}
pub(crate) fn shutdown_check_timeout_for<E>(update_listener: &impl UpdateListener<E>) -> Duration {
const MIN_SHUTDOWN_CHECK_TIMEOUT: Duration = Duration::from_secs(1);
const DZERO: Duration = Duration::ZERO;
let shutdown_check_timeout = update_listener.timeout_hint().unwrap_or(DZERO);
shutdown_check_timeout.saturating_add(MIN_SHUTDOWN_CHECK_TIMEOUT)
}
struct DispatcherState {
inner: AtomicU8,
}
impl DispatcherState {
// Ordering::Relaxed: only one atomic variable, nothing to synchronize.
fn load(&self) -> ShutdownState {
ShutdownState::from_u8(self.inner.load(Ordering::Relaxed))
}
fn store(&self, new: ShutdownState) {
self.inner.store(new as _, Ordering::Relaxed)
}
fn compare_exchange(
&self,
current: ShutdownState,
new: ShutdownState,
) -> Result<ShutdownState, ShutdownState> {
self.inner
.compare_exchange(current as _, new as _, Ordering::Relaxed, Ordering::Relaxed)
.map(ShutdownState::from_u8)
.map_err(ShutdownState::from_u8)
}
}
#[repr(u8)]
#[derive(Debug)]
enum ShutdownState {
Running,
ShuttingDown,
Idle,
}
impl ShutdownState {
fn from_u8(n: u8) -> Self {
const RUNNING: u8 = ShutdownState::Running as u8;
const SHUTTING_DOWN: u8 = ShutdownState::ShuttingDown as u8;
const IDLE: u8 = ShutdownState::Idle as u8;
match n {
RUNNING => ShutdownState::Running,
SHUTTING_DOWN => ShutdownState::ShuttingDown,
IDLE => ShutdownState::Idle,
_ => unreachable!(),
}
}
}
struct AlreadyShuttingDown;
fn shutdown_inner(
state: &DispatcherState,
) -> Result<(), Result<AlreadyShuttingDown, IdleShutdownError>> {
use ShutdownState::*;
let res = state.compare_exchange(Running, ShuttingDown);
match res {
Ok(_) => Ok(()),
Err(ShuttingDown) => Err(Ok(AlreadyShuttingDown)),
Err(Idle) => Err(Err(IdleShutdownError)),
Err(Running) => unreachable!(),
}
}

62
tests/dialogue_state.rs Normal file
View file

@ -0,0 +1,62 @@
#[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<X: Clone + Send + Sync + 'static> {
#[handler(handle_start)]
Start,
#[handler(handle_have_data)]
HaveData(X),
}
impl<X: Clone + Send + Sync + 'static> Default for State<X> {
fn default() -> Self {
Self::Start
}
}
async fn handle_start() -> Result<(), teloxide::RequestError> {
Ok(())
}
async fn handle_have_data() -> Result<(), teloxide::RequestError> {
Ok(())
}
}