mirror of
https://github.com/teloxide/teloxide.git
synced 2025-01-08 19:33:53 +01:00
commit
66023ee56c
92 changed files with 2909 additions and 1626 deletions
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -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 -->
|
||||
|
|
5
.github/ISSUE_TEMPLATE/parse-error.md
vendored
5
.github/ISSUE_TEMPLATE/parse-error.md
vendored
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
|
@ -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"
|
||||
|
|
46
CHANGELOG.md
46
CHANGELOG.md
|
@ -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.
|
||||
|
|
67
Cargo.toml
67
Cargo.toml
|
@ -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"]
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -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
|
||||
|
|
|
@ -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
376
README.md
|
@ -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).
|
||||
|
|
|
@ -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
153
examples/admin.rs
Normal 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;
|
||||
}
|
|
@ -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
|
|
@ -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
132
examples/buttons.rs
Normal 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
127
examples/dialogue.rs
Normal 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(())
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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
|
153
examples/dispatching2_features.rs
Normal file
153
examples/dispatching2_features.rs
Normal 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(())
|
||||
}
|
|
@ -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()
|
||||
})
|
||||
}
|
|
@ -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"
|
|
@ -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
64
examples/inline.rs
Normal 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;
|
||||
}
|
|
@ -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"
|
|
@ -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;
|
||||
}
|
|
@ -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()
|
||||
})
|
||||
}
|
|
@ -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
116
examples/redis_remember.rs
Normal 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(())
|
||||
}
|
|
@ -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"
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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
27
examples/shared_state.rs
Normal 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;
|
||||
}
|
|
@ -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"
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
113
examples/sqlite_remember.rs
Normal 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(())
|
||||
}
|
|
@ -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"
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
4
rust-toolchain.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "nightly-2022-02-02"
|
||||
components = ["rustfmt", "clippy"]
|
||||
profile = "minimal"
|
|
@ -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>,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -80,10 +80,6 @@
|
|||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() {
|
||||
//! run().await;
|
||||
//! }
|
||||
//!
|
||||
//! async fn run() {
|
||||
//! teloxide::enable_logging!();
|
||||
//! log::info!("Starting dialogue_bot!");
|
||||
//!
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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, ()>
|
||||
|
|
|
@ -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)>
|
||||
|
|
|
@ -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>>;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
@ -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`].
|
||||
///
|
||||
|
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
@ -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>>,
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
|
|
20
src/dispatching2/dialogue/get_chat_id.rs
Normal file
20
src/dispatching2/dialogue/get_chat_id.rs
Normal 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)
|
||||
}
|
||||
}
|
177
src/dispatching2/dialogue/mod.rs
Normal file
177
src/dispatching2/dialogue/mod.rs
Normal 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
|
||||
}
|
||||
}
|
274
src/dispatching2/dispatcher.rs
Normal file
274
src/dispatching2/dispatcher.rs
Normal 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()
|
||||
}
|
||||
}
|
108
src/dispatching2/filter_ext.rs
Normal file
108
src/dispatching2/filter_ext.rs
Normal 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),
|
||||
}
|
101
src/dispatching2/handler_ext.rs
Normal file
101
src/dispatching2/handler_ext.rs
Normal 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())
|
||||
}
|
||||
}
|
8
src/dispatching2/handler_factory.rs
Normal file
8
src/dispatching2/handler_factory.rs
Normal 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
109
src/dispatching2/mod.rs
Normal 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;
|
97
src/dispatching2/repls/commands_repl.rs
Normal file
97
src/dispatching2/repls/commands_repl.rs
Normal 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;
|
||||
}
|
96
src/dispatching2/repls/dialogues_repl.rs
Normal file
96
src/dispatching2/repls/dialogues_repl.rs
Normal 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;
|
||||
}
|
9
src/dispatching2/repls/mod.rs
Normal file
9
src/dispatching2/repls/mod.rs
Normal 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};
|
73
src/dispatching2/repls/repl.rs
Normal file
73
src/dispatching2/repls/repl.rs
Normal 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;
|
||||
}
|
|
@ -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]). |
|
||||
|
|
27
src/lib.rs
27
src/lib.rs
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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
27
src/prelude2.rs
Normal 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::*};
|
|
@ -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;
|
||||
|
|
|
@ -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("\"", """),
|
||||
escape(lang).replace('"', """),
|
||||
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("&", "&").replace("<", "<").replace(">", ">")
|
||||
s.replace('&', "&").replace('<', "<").replace('>', ">")
|
||||
}
|
||||
|
||||
pub fn user_mention_or_link(user: &User) -> String {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
165
src/utils/shutdown_token.rs
Normal 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
62
tests/dialogue_state.rs
Normal 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(())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue