mirror of
https://github.com/teloxide/teloxide.git
synced 2025-01-09 11:43:57 +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
|
## Meta
|
||||||
|
|
||||||
- `teloxide` version: <!-- (e.g.: `0.3.1`) -->
|
- `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
|
## Meta
|
||||||
|
|
||||||
- `teloxide` version: <!-- (e.g.: `0.3.1`) -->
|
- `teloxide` version: <!-- (e.g.: `0.3.1`) -->
|
||||||
- rustc version:
|
|
||||||
```
|
|
||||||
<version>
|
|
||||||
```
|
|
||||||
<!-- use `rustc --version --verbose` to get it -->
|
|
||||||
|
|
||||||
### Additional context
|
### Additional context
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,6 @@ When using `<...>` method I've got `ApiError::Unknown` error with the following
|
||||||
## Meta
|
## Meta
|
||||||
|
|
||||||
- `teloxide` version: <!-- (e.g.: `0.3.1`) -->
|
- `teloxide` version: <!-- (e.g.: `0.3.1`) -->
|
||||||
- rustc version:
|
|
||||||
```
|
|
||||||
<version>
|
|
||||||
```
|
|
||||||
<!-- use `rustc --version --verbose` to get it -->
|
|
||||||
|
|
||||||
### Additional context
|
### Additional context
|
||||||
|
|
||||||
|
|
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
toolchain: nightly
|
toolchain: nightly-2022-02-02
|
||||||
override: true
|
override: true
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ jobs:
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
toolchain: nightly
|
toolchain: nightly-2022-02-02
|
||||||
override: true
|
override: true
|
||||||
components: clippy
|
components: clippy
|
||||||
|
|
||||||
|
@ -91,18 +91,6 @@ jobs:
|
||||||
|
|
||||||
build-example:
|
build-example:
|
||||||
runs-on: ubuntu-latest
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
|
@ -110,5 +98,5 @@ jobs:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
override: true
|
override: true
|
||||||
- name: Check the example
|
- name: Check the examples
|
||||||
run: cd examples && cd ${{ matrix.example }} && cargo check
|
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/),
|
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).
|
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
|
## 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))
|
- 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
|
### Fixed
|
||||||
|
|
||||||
- Depend on a correct `futures` version (v0.3.15).
|
- Depend on a correct `futures` version (v0.3.15).
|
||||||
|
|
||||||
## [0.5.1] - 2021-08-05
|
## 0.5.1 - 2021-08-05
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Improved log messages when `^C` is received with `^C` handler set up
|
- 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
|
### 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)).
|
- 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]`.
|
- Mark all the functions of `Storage` as `#[must_use]`.
|
||||||
|
|
||||||
## [0.4.0] - 2021-03-22
|
## 0.4.0 - 2021-03-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Integrate [teloxide-core].
|
- 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
|
[issue 253]: https://github.com/teloxide/teloxide/issues/253
|
||||||
[pr 257]: https://github.com/teloxide/teloxide/pull/257
|
[pr 257]: https://github.com/teloxide/teloxide/pull/257
|
||||||
|
|
||||||
## [0.3.4] - 2020-01-13
|
## 0.3.4 - 2020-01-13
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Failing compilation with `serde::export` ([issue 328](https://github.com/teloxide/teloxide/issues/328)).
|
- Failing compilation with `serde::export` ([issue 328](https://github.com/teloxide/teloxide/issues/328)).
|
||||||
|
|
||||||
## [0.3.3] - 2020-10-30
|
## 0.3.3 - 2020-10-30
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- The `dice` field from `MessageDice` is public now ([issue 306](https://github.com/teloxide/teloxide/issues/306))
|
- The `dice` field from `MessageDice` is public now ([issue 306](https://github.com/teloxide/teloxide/issues/306))
|
||||||
|
|
||||||
## [0.3.2] - 2020-10-23
|
## 0.3.2 - 2020-10-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- `LoginUrl::new` ([issue 298](https://github.com/teloxide/teloxide/issues/298))
|
- `LoginUrl::new` ([issue 298](https://github.com/teloxide/teloxide/issues/298))
|
||||||
|
|
||||||
## [0.3.1] - 2020-08-25
|
## 0.3.1 - 2020-08-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- `Bot::builder` method ([PR 269](https://github.com/teloxide/teloxide/pull/269)).
|
- `Bot::builder` method ([PR 269](https://github.com/teloxide/teloxide/pull/269)).
|
||||||
|
|
||||||
## [0.3.0] - 2020-07-31
|
## 0.3.0 - 2020-07-31
|
||||||
### Added
|
### Added
|
||||||
- Support for typed bot commands ([issue 152](https://github.com/teloxide/teloxide/issues/152)).
|
- Support for typed bot commands ([issue 152](https://github.com/teloxide/teloxide/issues/152)).
|
||||||
- `BotBuilder`, which allows setting a default `ParseMode`.
|
- `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)).
|
- 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)).
|
- 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
|
### 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)).
|
- 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`.
|
- 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`.
|
- [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.
|
- `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
|
### Added
|
||||||
- This project.
|
- This project.
|
||||||
|
|
67
Cargo.toml
67
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "teloxide"
|
name = "teloxide"
|
||||||
version = "0.5.3"
|
version = "0.6.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "An elegant Telegram bots framework for Rust"
|
description = "An elegant Telegram bots framework for Rust"
|
||||||
repository = "https://github.com/teloxide/teloxide"
|
repository = "https://github.com/teloxide/teloxide"
|
||||||
|
@ -10,22 +10,11 @@ keywords = ["teloxide", "telegram", "telegram-bot", "telegram-bot-api"]
|
||||||
categories = ["web-programming", "api-bindings", "asynchronous"]
|
categories = ["web-programming", "api-bindings", "asynchronous"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
exclude = ["media"]
|
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]
|
[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"]
|
sqlite-storage = ["sqlx"]
|
||||||
redis-storage = ["redis"]
|
redis-storage = ["redis"]
|
||||||
|
@ -68,13 +57,14 @@ full = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
teloxide-core = { version = "0.3.3", default-features = false }
|
teloxide-core = { version = "0.4", default-features = false }
|
||||||
#teloxide-core = { git = "https://github.com/teloxide/teloxide-core.git", rev = "...", default-features = false }
|
teloxide-macros = { version = "0.5", optional = true }
|
||||||
teloxide-macros = { version = "0.4", optional = true }
|
|
||||||
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
||||||
|
dptree = { version = "0.1.0", optional = true }
|
||||||
|
|
||||||
tokio = { version = "1.8", features = ["fs"] }
|
tokio = { version = "1.8", features = ["fs"] }
|
||||||
tokio-util = "0.6"
|
tokio-util = "0.6"
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
@ -100,18 +90,27 @@ redis = { version = "0.20", features = ["tokio-comp"], optional = true }
|
||||||
serde_cbor = { version = "0.11", optional = true }
|
serde_cbor = { version = "0.11", optional = true }
|
||||||
bincode = { version = "1.3", optional = true }
|
bincode = { version = "1.3", optional = true }
|
||||||
frunk = { version = "0.4", optional = true }
|
frunk = { version = "0.4", optional = true }
|
||||||
|
aquamarine = "0.1.11"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
smart-default = "0.6.0"
|
smart-default = "0.6.0"
|
||||||
rand = "0.8.3"
|
rand = "0.8.3"
|
||||||
pretty_env_logger = "0.4.0"
|
pretty_env_logger = "0.4.0"
|
||||||
|
once_cell = "1.9.0"
|
||||||
lazy_static = "1.4.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"] }
|
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]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs", "-Znormalize-docs"]
|
rustdoc-args = ["--cfg", "docsrs", "-Znormalize-docs"]
|
||||||
rustc-args = ["--cfg", "dep_docsrs"]
|
rustc-args = ["--cfg", "dep_docsrs"]
|
||||||
|
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples=examples"]
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
|
@ -122,3 +121,35 @@ required-features = ["redis-storage", "cbor-serializer", "bincode-serializer"]
|
||||||
name = "sqlite"
|
name = "sqlite"
|
||||||
path = "tests/sqlite.rs"
|
path = "tests/sqlite.rs"
|
||||||
required-features = ["sqlite-storage", "cbor-serializer", "bincode-serializer"]
|
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
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
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.
|
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.
|
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
|
## 0.4 -> 0.5
|
||||||
|
|
||||||
### core
|
### 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">
|
<div align="center">
|
||||||
<img src="ICON.png" width="250"/>
|
<img src="ICON.png" width="250"/>
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
<img src="https://img.shields.io/crates/v/teloxide.svg">
|
<img src="https://img.shields.io/crates/v/teloxide.svg">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://core.telegram.org/bots/api">
|
<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>
|
||||||
<a href="https://t.me/teloxide">
|
<a href="https://t.me/teloxide">
|
||||||
<img src="https://img.shields.io/badge/official%20chat-t.me%2Fteloxide-blueviolet">
|
<img src="https://img.shields.io/badge/official%20chat-t.me%2Fteloxide-blueviolet">
|
||||||
|
@ -27,23 +27,24 @@
|
||||||
|
|
||||||
## Highlights
|
## 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
|
[`dptree`]: https://github.com/p0lunin/dptree
|
||||||
[other adaptors]: https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html
|
[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)
|
[persistence]: https://en.wikipedia.org/wiki/Persistence_(computer_science)
|
||||||
[Redis]: https://redis.io/
|
[Redis]: https://redis.io/
|
||||||
[Sqlite]: https://www.sqlite.org
|
[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
|
[`structopt`]: https://github.com/TeXitoi/structopt
|
||||||
[serde-json]: https://github.com/serde-rs/json
|
[`serde-json`]: https://github.com/serde-rs/json
|
||||||
|
|
||||||
## Setting up your environment
|
## Setting up your environment
|
||||||
|
|
||||||
1. [Download Rust](http://rustup.rs/).
|
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`.
|
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:
|
3. Initialise the `TELOXIDE_TOKEN` environmental variable to your token:
|
||||||
|
@ -51,8 +52,12 @@
|
||||||
# Unix-like
|
# Unix-like
|
||||||
$ export TELOXIDE_TOKEN=<Your token here>
|
$ export TELOXIDE_TOKEN=<Your token here>
|
||||||
|
|
||||||
# Windows
|
# Windows command line
|
||||||
$ set TELOXIDE_TOKEN=<Your token here>
|
$ 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:
|
4. Make sure that your Rust compiler is up to date:
|
||||||
```bash
|
```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`:
|
5. Run `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`:
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
teloxide = { version = "0.4", features = ["auto-send", "macros"] }
|
teloxide = { version = "0.5", features = ["macros", "auto-send"] }
|
||||||
log = "0.4.8"
|
log = "0.4"
|
||||||
pretty_env_logger = "0.4.0"
|
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
|
## API overview
|
||||||
|
|
||||||
### The dices bot
|
### The dices bot
|
||||||
|
|
||||||
This bot replies with a dice throw to each received message:
|
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
|
```rust,no_run
|
||||||
use teloxide::prelude::*;
|
use teloxide::prelude2::*;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
@ -90,8 +97,8 @@ async fn main() {
|
||||||
|
|
||||||
let bot = Bot::from_env().auto_send();
|
let bot = Bot::from_env().auto_send();
|
||||||
|
|
||||||
teloxide::repl(bot, |message| async move {
|
teloxide::repls2::repl(bot, |message: Message, bot: AutoSend<Bot>| async move {
|
||||||
message.answer_dice().await?;
|
bot.send_dice(message.chat.id).await?;
|
||||||
respond(())
|
respond(())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -105,6 +112,7 @@ async fn main() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Commands
|
### 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:
|
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>`
|
- `/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/
|
[structopt]: https://docs.rs/structopt/0.3.9/structopt/
|
||||||
[serde-json]: https://github.com/serde-rs/json
|
[serde-json]: https://github.com/serde-rs/json
|
||||||
|
|
||||||
([Full](./examples/simple_commands_bot/src/main.rs))
|
([Full](examples/simple_commands.rs))
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
use teloxide::{prelude::*, utils::command::BotCommand};
|
use teloxide::{prelude2::*, utils::command::BotCommand};
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
#[derive(BotCommand)]
|
#[derive(BotCommand, Clone)]
|
||||||
#[command(rename = "lowercase", description = "These commands are supported:")]
|
#[command(rename = "lowercase", description = "These commands are supported:")]
|
||||||
enum Command {
|
enum Command {
|
||||||
#[command(description = "display this text.")]
|
#[command(description = "display this text.")]
|
||||||
|
@ -132,16 +141,21 @@ enum Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn answer(
|
async fn answer(
|
||||||
cx: UpdateWithCx<AutoSend<Bot>, Message>,
|
bot: AutoSend<Bot>,
|
||||||
|
message: Message,
|
||||||
command: Command,
|
command: Command,
|
||||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
match command {
|
match command {
|
||||||
Command::Help => cx.answer(Command::descriptions()).await?,
|
Command::Help => bot.send_message(message.chat.id, Command::descriptions()).await?,
|
||||||
Command::Username(username) => {
|
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 } => {
|
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 = Bot::from_env().auto_send();
|
||||||
|
|
||||||
let bot_name: String = panic!("Your bot's name here");
|
teloxide::repls2::commands_repl(bot, answer, Command::ty()).await;
|
||||||
teloxide::commands_repl(bot, bot_name, answer).await;
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -167,145 +180,41 @@ async fn main() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Dialogues management
|
### 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
|
[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
|
```rust,ignore
|
||||||
// Imports are omitted...
|
use teloxide::{dispatching2::dialogue::InMemStorage, macros::DialogueState, prelude2::*};
|
||||||
|
|
||||||
#[derive(Transition, From)]
|
type MyDialogue = Dialogue<State, InMemStorage<State>>;
|
||||||
pub enum Dialogue {
|
|
||||||
Start(StartState),
|
#[derive(DialogueState, Clone)]
|
||||||
ReceiveFullName(ReceiveFullNameState),
|
#[handler_out(anyhow::Result<()>)]
|
||||||
ReceiveAge(ReceiveAgeState),
|
pub enum State {
|
||||||
ReceiveLocation(ReceiveLocationState),
|
#[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 {
|
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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
@ -314,23 +223,84 @@ async fn main() {
|
||||||
|
|
||||||
let bot = Bot::from_env().auto_send();
|
let bot = Bot::from_env().auto_send();
|
||||||
|
|
||||||
teloxide::dialogues_repl(bot, |message, dialogue| async move {
|
Dispatcher::builder(
|
||||||
handle_message(message, dialogue).await.expect("Something wrong with the bot!")
|
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;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_message(
|
async fn handle_start(
|
||||||
cx: UpdateWithCx<AutoSend<Bot>, Message>,
|
bot: AutoSend<Bot>,
|
||||||
dialogue: Dialogue,
|
msg: Message,
|
||||||
) -> TransitionOut<Dialogue> {
|
dialogue: MyDialogue,
|
||||||
match cx.update.text().map(ToOwned::to_owned) {
|
) -> anyhow::Result<()> {
|
||||||
None => {
|
bot.send_message(msg.chat.id, "Let's start! What's your full name?").await?;
|
||||||
cx.answer("Send me a text message.").await?;
|
dialogue.update(State::ReceiveFullName).await?;
|
||||||
next(dialogue)
|
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>
|
</kbd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[More examples!](./examples)
|
[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.
|
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
**Q: Where I can ask questions?**
|
**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;
|
- [Issues] is a good place for well-formed questions about the library design, enhancements, and bug reports.
|
||||||
- enhancements;
|
- [GitHub Discussions] is a place where you can ask us for help in a less formal manner.
|
||||||
- bug reports;
|
- 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?**
|
**Q: Do you support the Telegram API for clients?**
|
||||||
|
|
||||||
A: No, only the bots API.
|
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?**
|
**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:
|
Associated links:
|
||||||
- [Marvin's Marvellous Guide to All Things Webhook](https://core.telegram.org/bots/webhooks)
|
- [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
|
[`enable_logging_with_filter!`]: https://docs.rs/teloxide/latest/teloxide/macro.enable_logging_with_filter.html
|
||||||
|
|
||||||
## Community bots
|
## Community bots
|
||||||
|
|
||||||
Feel free to propose your own bot to our collection!
|
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.
|
- [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.
|
- [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/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.
|
- [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.
|
- [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.
|
- [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.
|
- [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.
|
- [MustafaSalih1993/Miss-Vodka-Telegram-Bot](https://github.com/MustafaSalih1993/Miss-Vodka-Telegram-Bot) -- A Telegram bot written in rust using "Teloxide" library.
|
||||||
- [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.
|
|
||||||
- [x13a/tg-prompt](https://github.com/x13a/tg-prompt) -- Telegram prompt.
|
- [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.
|
- [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
|
## Contributing
|
||||||
See [CONRIBUTING.md](https://github.com/teloxide/teloxide/blob/master/CONTRIBUTING.md).
|
|
||||||
|
See [`CONRIBUTING.md`](CONTRIBUTING.md).
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
# Examples
|
# Usage
|
||||||
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 |
|
```
|
||||||
|---|-----------|
|
$ cargo run --example <example name> --features="<features required by the example>"
|
||||||
| [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. |
|
Don't forget to initialise the `TELOXIDE_TOKEN` environmental variable.
|
||||||
| [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. |
|
|
||||||
|
|
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.
|
// This bot throws a dice on each incoming message.
|
||||||
|
|
||||||
use teloxide::prelude::*;
|
use teloxide::prelude2::*;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
run().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run() {
|
|
||||||
teloxide::enable_logging!();
|
teloxide::enable_logging!();
|
||||||
log::info!("Starting dices_bot...");
|
log::info!("Starting dices_bot...");
|
||||||
|
|
||||||
let bot = Bot::from_env().auto_send();
|
let bot = Bot::from_env().auto_send();
|
||||||
|
|
||||||
teloxide::repl(bot, |message| async move {
|
teloxide::repls2::repl(bot, |message: Message, bot: AutoSend<Bot>| async move {
|
||||||
message.answer_dice().await?;
|
bot.send_dice(message.chat.id).await?;
|
||||||
respond(())
|
respond(())
|
||||||
})
|
})
|
||||||
.await;
|
.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
|
// The version of Heroku ping-pong-bot, which uses a webhook to receive updates
|
||||||
// from Telegram, instead of long polling.
|
// 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 std::{convert::Infallible, env, net::SocketAddr};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
@ -12,7 +36,20 @@ use reqwest::{StatusCode, Url};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn 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> {
|
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()
|
let server = warp::post()
|
||||||
.and(warp::path(path))
|
.and(warp::path(path))
|
||||||
.and(warp::body::json())
|
.and(warp::body::json())
|
||||||
.map(move |json: serde_json::Value| {
|
.map(move |update: Update| {
|
||||||
if let Ok(update) = Update::try_parse(&json) {
|
tx.send(Ok(update)).expect("Cannot send an incoming update from the webhook");
|
||||||
tx.send(Ok(update)).expect("Cannot send an incoming update from the webhook")
|
|
||||||
}
|
|
||||||
|
|
||||||
StatusCode::OK
|
StatusCode::OK
|
||||||
})
|
})
|
||||||
|
@ -60,25 +95,11 @@ pub async fn webhook(bot: AutoSend<Bot>) -> impl update_listeners::UpdateListene
|
||||||
tokio::spawn(fut);
|
tokio::spawn(fut);
|
||||||
let stream = UnboundedReceiverStream::new(rx);
|
let stream = UnboundedReceiverStream::new(rx);
|
||||||
|
|
||||||
fn streamf<S, T>(state: &mut (S, T)) -> &mut S { &mut state.0 }
|
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())
|
}
|
||||||
}
|
|
||||||
|
StatefulListener::new((stream, stop_token), streamf, |state: &mut (_, AsyncStopToken)| {
|
||||||
async fn run() {
|
state.1.clone()
|
||||||
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;
|
|
||||||
}
|
}
|
|
@ -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
|
// The version of ngrok ping-pong-bot, which uses a webhook to receive updates
|
||||||
// from Telegram, instead of long polling.
|
// 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 std::{convert::Infallible, net::SocketAddr};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
@ -12,7 +19,20 @@ use reqwest::{StatusCode, Url};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn 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> {
|
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
|
// You might want to specify a self-signed certificate via .certificate
|
||||||
// method on SetWebhook.
|
// method on SetWebhook.
|
||||||
bot.set_webhook(url)
|
bot.set_webhook(url).await.expect("Cannot setup a webhook");
|
||||||
.await
|
|
||||||
.expect("Cannot setup a webhook");
|
|
||||||
|
|
||||||
let (tx, rx) = mpsc::unbounded_channel();
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
let server = warp::post()
|
let server = warp::post()
|
||||||
.and(warp::body::json())
|
.and(warp::body::json())
|
||||||
.map(move |json: serde_json::Value| {
|
.map(move |update: Update| {
|
||||||
if let Ok(update) = Update::try_parse(&json) {
|
tx.send(Ok(update)).expect("Cannot send an incoming update from the webhook");
|
||||||
tx.send(Ok(update)).expect("Cannot send an incoming update from the webhook")
|
|
||||||
}
|
|
||||||
|
|
||||||
StatusCode::OK
|
StatusCode::OK
|
||||||
})
|
})
|
||||||
|
@ -54,25 +70,11 @@ pub async fn webhook(bot: AutoSend<Bot>) -> impl update_listeners::UpdateListene
|
||||||
tokio::spawn(fut);
|
tokio::spawn(fut);
|
||||||
let stream = UnboundedReceiverStream::new(rx);
|
let stream = UnboundedReceiverStream::new(rx);
|
||||||
|
|
||||||
fn streamf<S, T>(state: &mut (S, T)) -> &mut S { &mut state.0 }
|
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())
|
}
|
||||||
}
|
|
||||||
|
StatefulListener::new((stream, stop_token), streamf, |state: &mut (_, AsyncStopToken)| {
|
||||||
async fn run() {
|
state.1.clone()
|
||||||
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;
|
|
||||||
}
|
}
|
|
@ -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;
|
use std::error::Error;
|
||||||
|
|
||||||
#[derive(BotCommand)]
|
#[derive(BotCommand, Clone)]
|
||||||
#[command(rename = "lowercase", description = "These commands are supported:")]
|
#[command(rename = "lowercase", description = "These commands are supported:")]
|
||||||
enum Command {
|
enum Command {
|
||||||
#[command(description = "display this text.")]
|
#[command(description = "display this text.")]
|
||||||
|
@ -14,16 +14,21 @@ enum Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn answer(
|
async fn answer(
|
||||||
cx: UpdateWithCx<AutoSend<Bot>, Message>,
|
bot: AutoSend<Bot>,
|
||||||
|
message: Message,
|
||||||
command: Command,
|
command: Command,
|
||||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
match command {
|
match command {
|
||||||
Command::Help => cx.answer(Command::descriptions()).await?,
|
Command::Help => bot.send_message(message.chat.id, Command::descriptions()).await?,
|
||||||
Command::Username(username) => {
|
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 } => {
|
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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
run().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run() {
|
|
||||||
teloxide::enable_logging!();
|
teloxide::enable_logging!();
|
||||||
log::info!("Starting simple_commands_bot...");
|
log::info!("Starting simple_commands_bot...");
|
||||||
|
|
||||||
let bot = Bot::from_env().auto_send();
|
let bot = Bot::from_env().auto_send();
|
||||||
|
|
||||||
let bot_name: String = panic!("Your bot's name here");
|
teloxide::repls2::commands_repl(bot, answer, Command::ty()).await;
|
||||||
teloxide::commands_repl(bot, bot_name, answer).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
|
/// [`Dispatcher`]: crate::dispatching::Dispatcher
|
||||||
/// [`DispatcherHandler`]: crate::dispatching::DispatcherHandler
|
/// [`DispatcherHandler`]: crate::dispatching::DispatcherHandler
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub struct DialogueDispatcher<R, D, S, H, Upd> {
|
pub struct DialogueDispatcher<R, D, S, H, Upd> {
|
||||||
storage: Arc<S>,
|
storage: Arc<S>,
|
||||||
handler: Arc<H>,
|
handler: Arc<H>,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::prelude::{DialogueStage, DialogueWithCx};
|
use crate::dispatching::dialogue::{DialogueStage, DialogueWithCx};
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use std::{future::Future, sync::Arc};
|
use std::{future::Future, sync::Arc};
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ use std::{future::Future, sync::Arc};
|
||||||
/// overview](crate::dispatching::dialogue).
|
/// overview](crate::dispatching::dialogue).
|
||||||
///
|
///
|
||||||
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
|
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub trait DialogueDispatcherHandler<R, Upd, D, E> {
|
pub trait DialogueDispatcherHandler<R, Upd, D, E> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn handle(
|
fn handle(
|
||||||
|
|
|
@ -5,6 +5,7 @@ use crate::dispatching::dialogue::TransitionOut;
|
||||||
/// See [the module-level documentation for the design
|
/// See [the module-level documentation for the design
|
||||||
/// overview](crate::dispatching::dialogue).
|
/// overview](crate::dispatching::dialogue).
|
||||||
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
|
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub enum DialogueStage<D> {
|
pub enum DialogueStage<D> {
|
||||||
Next(D),
|
Next(D),
|
||||||
Exit,
|
Exit,
|
||||||
|
@ -21,6 +22,7 @@ pub enum DialogueStage<D> {
|
||||||
///
|
///
|
||||||
/// [`From`]: std::convert::From
|
/// [`From`]: std::convert::From
|
||||||
/// [derive-more]: https://crates.io/crates/derive_more
|
/// [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>
|
pub fn next<Dialogue, State, E>(new_state: State) -> TransitionOut<Dialogue, E>
|
||||||
where
|
where
|
||||||
Dialogue: From<State>,
|
Dialogue: From<State>,
|
||||||
|
@ -32,6 +34,7 @@ where
|
||||||
///
|
///
|
||||||
/// See [the module-level documentation for the design
|
/// See [the module-level documentation for the design
|
||||||
/// overview](crate::dispatching::dialogue).
|
/// overview](crate::dispatching::dialogue).
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub fn exit<D, E>() -> TransitionOut<D, E> {
|
pub fn exit<D, E>() -> TransitionOut<D, E> {
|
||||||
Ok(DialogueStage::Exit)
|
Ok(DialogueStage::Exit)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ use teloxide_core::requests::Requester;
|
||||||
///
|
///
|
||||||
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
|
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub struct DialogueWithCx<R, Upd, D, E> {
|
pub struct DialogueWithCx<R, Upd, D, E> {
|
||||||
pub cx: UpdateWithCx<R, Upd>,
|
pub cx: UpdateWithCx<R, Upd>,
|
||||||
pub dialogue: Result<D, E>,
|
pub dialogue: Result<D, E>,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use teloxide_core::types::Message;
|
use teloxide_core::types::Message;
|
||||||
|
|
||||||
/// Something that has a chat ID.
|
/// Something that has a chat ID.
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub trait GetChatId {
|
pub trait GetChatId {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn chat_id(&self) -> i64;
|
fn chat_id(&self) -> i64;
|
||||||
|
|
|
@ -80,10 +80,6 @@
|
||||||
//!
|
//!
|
||||||
//! #[tokio::main]
|
//! #[tokio::main]
|
||||||
//! async fn main() {
|
//! async fn main() {
|
||||||
//! run().await;
|
|
||||||
//! }
|
|
||||||
//!
|
|
||||||
//! async fn run() {
|
|
||||||
//! teloxide::enable_logging!();
|
//! teloxide::enable_logging!();
|
||||||
//! log::info!("Starting dialogue_bot!");
|
//! log::info!("Starting dialogue_bot!");
|
||||||
//!
|
//!
|
||||||
|
|
|
@ -3,6 +3,7 @@ use futures::future::BoxFuture;
|
||||||
use teloxide_core::types::Message;
|
use teloxide_core::types::Message;
|
||||||
|
|
||||||
/// Represents a transition function of a dialogue FSM.
|
/// Represents a transition function of a dialogue FSM.
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub trait Transition: Sized {
|
pub trait Transition: Sized {
|
||||||
type Aux;
|
type Aux;
|
||||||
type Error;
|
type Error;
|
||||||
|
@ -21,6 +22,7 @@ pub trait Transition: Sized {
|
||||||
/// Like [`Transition`], but from `StateN` -> `Dialogue`.
|
/// Like [`Transition`], but from `StateN` -> `Dialogue`.
|
||||||
///
|
///
|
||||||
/// [`Transition`]: crate::dispatching::dialogue::Transition
|
/// [`Transition`]: crate::dispatching::dialogue::Transition
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub trait Subtransition
|
pub trait Subtransition
|
||||||
where
|
where
|
||||||
Self::Dialogue: Transition<Aux = Self::Aux>,
|
Self::Dialogue: Transition<Aux = Self::Aux>,
|
||||||
|
@ -45,6 +47,7 @@ where
|
||||||
///
|
///
|
||||||
/// Now it is used only inside `#[teloxide(subtransition)]` for type inference.
|
/// Now it is used only inside `#[teloxide(subtransition)]` for type inference.
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub trait SubtransitionOutputType {
|
pub trait SubtransitionOutputType {
|
||||||
type Output;
|
type Output;
|
||||||
type Error;
|
type Error;
|
||||||
|
@ -56,7 +59,9 @@ impl<D, E> SubtransitionOutputType for TransitionOut<D, E> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An input passed into a FSM (sub)transition function.
|
/// An input passed into a FSM (sub)transition function.
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub type TransitionIn<R> = UpdateWithCx<R, Message>;
|
pub type TransitionIn<R> = UpdateWithCx<R, Message>;
|
||||||
|
|
||||||
/// A type returned from a FSM (sub)transition function.
|
/// 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>;
|
pub type TransitionOut<D, E = crate::RequestError> = Result<DialogueStage<D>, E>;
|
||||||
|
|
|
@ -1,11 +1,4 @@
|
||||||
use std::{
|
use std::{fmt::Debug, sync::Arc};
|
||||||
fmt::{self, Debug},
|
|
||||||
sync::{
|
|
||||||
atomic::{AtomicU8, Ordering},
|
|
||||||
Arc,
|
|
||||||
},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
dispatching::{
|
dispatching::{
|
||||||
|
@ -14,21 +7,21 @@ use crate::{
|
||||||
DispatcherHandler, UpdateWithCx,
|
DispatcherHandler, UpdateWithCx,
|
||||||
},
|
},
|
||||||
error_handlers::{ErrorHandler, LoggingErrorHandler},
|
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::{
|
use teloxide_core::{
|
||||||
requests::Requester,
|
requests::Requester,
|
||||||
types::{
|
types::{
|
||||||
AllowedUpdate, CallbackQuery, ChatMemberUpdated, ChosenInlineResult, InlineQuery, Message,
|
AllowedUpdate, CallbackQuery, ChatJoinRequest, ChatMemberUpdated, ChosenInlineResult,
|
||||||
Poll, PollAnswer, PreCheckoutQuery, ShippingQuery, Update, UpdateKind,
|
InlineQuery, Message, Poll, PollAnswer, PreCheckoutQuery, ShippingQuery, Update,
|
||||||
|
UpdateKind,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::{sync::mpsc, task::JoinHandle, time::timeout};
|
||||||
sync::{mpsc, Notify},
|
|
||||||
task::JoinHandle,
|
use crate::utils::shutdown_token::ShutdownToken;
|
||||||
time::timeout,
|
|
||||||
};
|
|
||||||
|
|
||||||
type Tx<Upd, R> = Option<mpsc::UnboundedSender<UpdateWithCx<Upd, R>>>;
|
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
|
/// See the [module-level documentation](crate::dispatching) for the design
|
||||||
/// overview.
|
/// overview.
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub struct Dispatcher<R> {
|
pub struct Dispatcher<R> {
|
||||||
requester: R,
|
requester: R,
|
||||||
|
|
||||||
|
@ -52,11 +46,11 @@ pub struct Dispatcher<R> {
|
||||||
poll_answers_queue: Tx<R, PollAnswer>,
|
poll_answers_queue: Tx<R, PollAnswer>,
|
||||||
my_chat_members_queue: Tx<R, ChatMemberUpdated>,
|
my_chat_members_queue: Tx<R, ChatMemberUpdated>,
|
||||||
chat_members_queue: Tx<R, ChatMemberUpdated>,
|
chat_members_queue: Tx<R, ChatMemberUpdated>,
|
||||||
|
chat_join_requests_queue: Tx<R, ChatJoinRequest>,
|
||||||
|
|
||||||
running_handlers: FuturesUnordered<JoinHandle<()>>,
|
running_handlers: FuturesUnordered<JoinHandle<()>>,
|
||||||
|
|
||||||
state: Arc<DispatcherState>,
|
state: ShutdownToken,
|
||||||
shutdown_notify_back: Arc<Notify>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R> Dispatcher<R>
|
impl<R> Dispatcher<R>
|
||||||
|
@ -81,9 +75,9 @@ where
|
||||||
poll_answers_queue: None,
|
poll_answers_queue: None,
|
||||||
my_chat_members_queue: None,
|
my_chat_members_queue: None,
|
||||||
chat_members_queue: None,
|
chat_members_queue: None,
|
||||||
|
chat_join_requests_queue: None,
|
||||||
running_handlers: FuturesUnordered::new(),
|
running_handlers: FuturesUnordered::new(),
|
||||||
state: <_>::default(),
|
state: ShutdownToken::new(),
|
||||||
shutdown_notify_back: <_>::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,22 +100,21 @@ where
|
||||||
///
|
///
|
||||||
/// [`shutdown`]: ShutdownToken::shutdown
|
/// [`shutdown`]: ShutdownToken::shutdown
|
||||||
#[cfg(feature = "ctrlc_handler")]
|
#[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 {
|
pub fn setup_ctrlc_handler(self) -> Self {
|
||||||
let state = Arc::clone(&self.state);
|
let token = self.state.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
tokio::signal::ctrl_c().await.expect("Failed to listen for ^C");
|
tokio::signal::ctrl_c().await.expect("Failed to listen for ^C");
|
||||||
|
|
||||||
match shutdown_inner(&state) {
|
match token.shutdown() {
|
||||||
Ok(()) => log::info!("^C received, trying to shutdown the dispatcher..."),
|
Ok(f) => {
|
||||||
Err(Ok(AlreadyShuttingDown)) => {
|
log::info!("^C received, trying to shutdown the dispatcher...");
|
||||||
log::info!(
|
f.await;
|
||||||
"^C received, the dispatcher is already shutting down, ignoring the \
|
log::info!("dispatcher is shutdown...");
|
||||||
signal"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Err(Err(IdleShutdownError)) => {
|
Err(_) => {
|
||||||
log::info!("^C received, the dispatcher isn't running, ignoring the signal")
|
log::info!("^C received, the dispatcher isn't running, ignoring the signal")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -263,7 +256,7 @@ where
|
||||||
pub async fn dispatch(&mut self)
|
pub async fn dispatch(&mut self)
|
||||||
where
|
where
|
||||||
R: Requester + Clone,
|
R: Requester + Clone,
|
||||||
<R as Requester>::GetUpdatesFaultTolerant: Send,
|
<R as Requester>::GetUpdates: Send,
|
||||||
{
|
{
|
||||||
let listener = update_listeners::polling_default(self.requester.clone()).await;
|
let listener = update_listeners::polling_default(self.requester.clone()).await;
|
||||||
let error_handler =
|
let error_handler =
|
||||||
|
@ -292,19 +285,12 @@ where
|
||||||
ListenerE: Debug,
|
ListenerE: Debug,
|
||||||
R: Requester + Clone,
|
R: Requester + Clone,
|
||||||
{
|
{
|
||||||
use ShutdownState::*;
|
|
||||||
|
|
||||||
self.hint_allowed_updates(&mut update_listener);
|
self.hint_allowed_updates(&mut update_listener);
|
||||||
|
|
||||||
let shutdown_check_timeout = shutdown_check_timeout_for(&update_listener);
|
let shutdown_check_timeout = shutdown_check_timeout_for(&update_listener);
|
||||||
let mut stop_token = Some(update_listener.stop_token());
|
let mut stop_token = Some(update_listener.stop_token());
|
||||||
|
|
||||||
if let Err(actual) = self.state.compare_exchange(Idle, Running) {
|
self.state.start_dispatching();
|
||||||
unreachable!(
|
|
||||||
"Dispatching is already running: expected `{:?}` state, found `{:?}`",
|
|
||||||
Idle, actual
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let stream = update_listener.as_stream();
|
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() {
|
if let Some(token) = stop_token.take() {
|
||||||
log::debug!("Start shutting down dispatching...");
|
log::debug!("Start shutting down dispatching...");
|
||||||
token.stop();
|
token.stop();
|
||||||
|
@ -330,27 +316,13 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
self.wait_for_handlers().await;
|
self.wait_for_handlers().await;
|
||||||
|
self.state.done();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a shutdown token, which can later be used to shutdown
|
/// Returns a shutdown token, which can later be used to shutdown
|
||||||
/// dispatching.
|
/// dispatching.
|
||||||
pub fn shutdown_token(&self) -> ShutdownToken {
|
pub fn shutdown_token(&self) -> ShutdownToken {
|
||||||
ShutdownToken {
|
self.state.clone()
|
||||||
dispatcher_state: Arc::clone(&self.state),
|
|
||||||
shutdown_notify_back: Arc::clone(&self.shutdown_notify_back),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process_update<ListenerE, Eh>(
|
async fn process_update<ListenerE, Eh>(
|
||||||
|
@ -446,6 +418,20 @@ where
|
||||||
chat_member_updated,
|
chat_member_updated,
|
||||||
"UpdateKind::MyChatMember",
|
"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)
|
fn send<'a, R, Upd>(requester: &'a R, tx: &'a Tx<R, Upd>, update: Upd, variant: &'static str)
|
||||||
where
|
where
|
||||||
Upd: Debug,
|
Upd: Debug,
|
||||||
|
|
|
@ -9,6 +9,7 @@ use futures::future::BoxFuture;
|
||||||
/// overview.
|
/// overview.
|
||||||
///
|
///
|
||||||
/// [`Dispatcher`]: crate::dispatching::Dispatcher
|
/// [`Dispatcher`]: crate::dispatching::Dispatcher
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub trait DispatcherHandler<R, Upd> {
|
pub trait DispatcherHandler<R, Upd> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn handle(self, updates: DispatcherHandlerRx<R, Upd>) -> BoxFuture<'static, ()>
|
fn handle(self, updates: DispatcherHandlerRx<R, Upd>) -> BoxFuture<'static, ()>
|
||||||
|
|
|
@ -8,6 +8,7 @@ use teloxide_core::types::Message;
|
||||||
/// overview.
|
/// overview.
|
||||||
///
|
///
|
||||||
/// [`DispatcherHandlerRx`]: crate::dispatching::DispatcherHandlerRx
|
/// [`DispatcherHandlerRx`]: crate::dispatching::DispatcherHandlerRx
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub trait DispatcherHandlerRxExt<R> {
|
pub trait DispatcherHandlerRxExt<R> {
|
||||||
/// Extracts only text messages from this stream of arbitrary messages.
|
/// Extracts only text messages from this stream of arbitrary messages.
|
||||||
fn text_messages(self) -> BoxStream<'static, (UpdateWithCx<R, Message>, String)>
|
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
|
//! The key type here is [`Dispatcher`]. It encapsulates [`Bot`] and handlers
|
||||||
//! for [all the update kinds].
|
//! 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
|
//! [`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
|
//! [examples/dialogue_bot]: https://github.com/teloxide/teloxide/tree/master/examples/dialogue_bot
|
||||||
|
|
||||||
|
#![allow(deprecated)]
|
||||||
|
|
||||||
pub mod dialogue;
|
pub mod dialogue;
|
||||||
pub mod stop_token;
|
pub mod stop_token;
|
||||||
pub mod update_listeners;
|
pub mod update_listeners;
|
||||||
|
|
||||||
#[cfg(feature = "ctrlc_handler")]
|
|
||||||
pub(crate) mod repls;
|
pub(crate) mod repls;
|
||||||
|
|
||||||
mod dispatcher;
|
mod dispatcher;
|
||||||
|
@ -57,7 +59,8 @@ mod dispatcher_handler;
|
||||||
mod dispatcher_handler_rx_ext;
|
mod dispatcher_handler_rx_ext;
|
||||||
mod update_with_cx;
|
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::DispatcherHandler;
|
||||||
pub use dispatcher_handler_rx_ext::DispatcherHandlerRxExt;
|
pub use dispatcher_handler_rx_ext::DispatcherHandlerRxExt;
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
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.
|
/// A type of a stream, consumed by [`Dispatcher`]'s handlers.
|
||||||
///
|
///
|
||||||
/// [`Dispatcher`]: crate::dispatching::Dispatcher
|
/// [`Dispatcher`]: crate::dispatching::Dispatcher
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub type DispatcherHandlerRx<R, Upd> = UnboundedReceiver<UpdateWithCx<R, Upd>>;
|
pub type DispatcherHandlerRx<R, Upd> = UnboundedReceiver<UpdateWithCx<R, Upd>>;
|
||||||
|
|
|
@ -31,7 +31,7 @@ where
|
||||||
HandlerE: Debug + Send,
|
HandlerE: Debug + Send,
|
||||||
N: Into<String> + Send + 'static,
|
N: Into<String> + Send + 'static,
|
||||||
R: Requester + Send + Clone + 'static,
|
R: Requester + Send + Clone + 'static,
|
||||||
<R as Requester>::GetUpdatesFaultTolerant: Send,
|
<R as Requester>::GetUpdates: Send,
|
||||||
{
|
{
|
||||||
let cloned_requester = requester.clone();
|
let cloned_requester = requester.clone();
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ where
|
||||||
D: Clone + Default + Send + 'static,
|
D: Clone + Default + Send + 'static,
|
||||||
Fut: Future<Output = DialogueStage<D>> + Send + 'static,
|
Fut: Future<Output = DialogueStage<D>> + Send + 'static,
|
||||||
R: Requester + Send + Clone + 'static,
|
R: Requester + Send + Clone + 'static,
|
||||||
<R as Requester>::GetUpdatesFaultTolerant: Send,
|
<R as Requester>::GetUpdates: Send,
|
||||||
{
|
{
|
||||||
let cloned_requester = requester.clone();
|
let cloned_requester = requester.clone();
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ where
|
||||||
Result<(), E>: OnError<E>,
|
Result<(), E>: OnError<E>,
|
||||||
E: Debug + Send,
|
E: Debug + Send,
|
||||||
R: Requester + Send + Clone + 'static,
|
R: Requester + Send + Clone + 'static,
|
||||||
<R as Requester>::GetUpdatesFaultTolerant: Send,
|
<R as Requester>::GetUpdates: Send,
|
||||||
{
|
{
|
||||||
let cloned_requester = requester.clone();
|
let cloned_requester = requester.clone();
|
||||||
repl_with_listener(
|
repl_with_listener(
|
||||||
|
@ -83,3 +83,12 @@ pub async fn repl_with_listener<'a, R, H, Fut, E, L, ListenerE>(
|
||||||
)
|
)
|
||||||
.await;
|
.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.
|
//! 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_default`], which returns a default long polling listener.
|
||||||
//! - [`polling`], which returns a long/short polling listener with your
|
//! - [`polling`], which returns a long polling listener with your
|
||||||
//! configuration.
|
//! configuration.
|
||||||
//!
|
//!
|
||||||
//! And then you can extract updates from it and pass them directly to a
|
//! And then you can extract updates from it or pass them directly to a
|
||||||
//! dispatcher.
|
//! [`Dispatcher`].
|
||||||
//!
|
//!
|
||||||
//! Telegram supports two ways of [getting updates]: [long]/[short] polling and
|
//! Telegram supports two ways of [getting updates]: [long polling] and
|
||||||
//! [webhook].
|
//! [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).
|
||||||
//! # 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).
|
|
||||||
//!
|
//!
|
||||||
//! [`UpdateListener`]: UpdateListener
|
//! [`UpdateListener`]: UpdateListener
|
||||||
//! [`polling_default`]: polling_default
|
//! [`polling_default`]: polling_default
|
||||||
//! [`polling`]: polling()
|
//! [`polling`]: polling()
|
||||||
|
//! [`Dispatcher`]: crate::dispatching::Dispatcher
|
||||||
//! [`Box::get_updates`]: crate::requests::Requester::get_updates
|
//! [`Box::get_updates`]: crate::requests::Requester::get_updates
|
||||||
//! [getting updates]: https://core.telegram.org/bots/api#getting-updates
|
//! [getting updates]: https://core.telegram.org/bots/api#getting-updates
|
||||||
//! [long]: https://en.wikipedia.org/wiki/Push_technology#Long_polling
|
//! [long polling]: https://en.wikipedia.org/wiki/Push_technology#Long_polling
|
||||||
//! [short]: https://en.wikipedia.org/wiki/Polling_(computer_science)
|
//! [webhooks]: https://en.wikipedia.org/wiki/Webhook
|
||||||
//! [webhook]: https://en.wikipedia.org/wiki/Webhook
|
|
||||||
|
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
|
||||||
|
@ -122,19 +42,15 @@ pub use self::{
|
||||||
|
|
||||||
/// An update listener.
|
/// An update listener.
|
||||||
///
|
///
|
||||||
/// Implementors of this trait allow getting updates from Telegram.
|
/// Implementors of this trait allow getting updates from Telegram. See
|
||||||
///
|
/// [module-level documentation] for more.
|
||||||
/// Currently Telegram has 2 ways of getting updates -- [polling] and
|
|
||||||
/// [webhooks]. Currently, only the former one is implemented (see [`polling()`]
|
|
||||||
/// and [`polling_default`])
|
|
||||||
///
|
///
|
||||||
/// Some functions of this trait are located in the supertrait
|
/// Some functions of this trait are located in the supertrait
|
||||||
/// ([`AsUpdateStream`]), see also:
|
/// ([`AsUpdateStream`]), see also:
|
||||||
/// - [`AsUpdateStream::Stream`]
|
/// - [`AsUpdateStream::Stream`]
|
||||||
/// - [`AsUpdateStream::as_stream`]
|
/// - [`AsUpdateStream::as_stream`]
|
||||||
///
|
///
|
||||||
/// [polling]: self#long-polling
|
/// [module-level documentation]: mod@self
|
||||||
/// [webhooks]: self#webhooks
|
|
||||||
pub trait UpdateListener<E>: for<'a> AsUpdateStream<'a, E> {
|
pub trait UpdateListener<E>: for<'a> AsUpdateStream<'a, E> {
|
||||||
/// The type of token which allows to stop this listener.
|
/// The type of token which allows to stop this listener.
|
||||||
type StopToken: StopToken;
|
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
|
/// Implementors of this function are encouraged to stop listening for
|
||||||
/// updates as soon as possible and return `None` from the update stream as
|
/// updates as soon as possible and return `None` from the update stream as
|
||||||
/// soon as all cached updates are returned.
|
/// soon as all cached updates are returned.
|
||||||
#[must_use = "This function doesn't stop listening, to stop listening you need to call stop on \
|
#[must_use = "This function doesn't stop listening, to stop listening you need to call `stop` \
|
||||||
the returned token"]
|
on the returned token"]
|
||||||
fn stop_token(&mut self) -> Self::StopToken;
|
fn stop_token(&mut self) -> Self::StopToken;
|
||||||
|
|
||||||
/// Hint which updates should the listener listen for.
|
/// 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.
|
/// This trait is a workaround to not require GAT.
|
||||||
pub trait AsUpdateStream<'a, E> {
|
pub trait AsUpdateStream<'a, E> {
|
||||||
/// The stream of updates from Telegram.
|
/// 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`].
|
/// Creates the update [`Stream`].
|
||||||
///
|
///
|
||||||
|
|
|
@ -7,12 +7,12 @@ use futures::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
dispatching::{
|
dispatching::{
|
||||||
stop_token::{AsyncStopFlag, AsyncStopToken},
|
stop_token::{AsyncStopFlag, AsyncStopToken, StopToken},
|
||||||
update_listeners::{stateful_listener::StatefulListener, UpdateListener},
|
update_listeners::{stateful_listener::StatefulListener, UpdateListener},
|
||||||
},
|
},
|
||||||
payloads::GetUpdates,
|
payloads::{GetUpdates, GetUpdatesSetters as _},
|
||||||
requests::{HasPayload, Request, Requester},
|
requests::{HasPayload, Request, Requester},
|
||||||
types::{AllowedUpdate, SemiparsedVec, Update},
|
types::{AllowedUpdate, Update},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Returns a long polling update listener with `timeout` of 10 seconds.
|
/// Returns a long polling update listener with `timeout` of 10 seconds.
|
||||||
|
@ -22,36 +22,118 @@ use crate::{
|
||||||
/// ## Notes
|
/// ## Notes
|
||||||
///
|
///
|
||||||
/// This function will automatically delete a webhook if it was set up.
|
/// 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
|
where
|
||||||
R: Requester + 'static,
|
R: Requester + Send + 'static,
|
||||||
<R as Requester>::GetUpdatesFaultTolerant: Send,
|
<R as Requester>::GetUpdates: Send,
|
||||||
{
|
{
|
||||||
delete_webhook_if_setup(&requester).await;
|
delete_webhook_if_setup(&requester).await;
|
||||||
polling(requester, Some(Duration::from_secs(10)), None, None)
|
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.
|
/// - `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
|
/// - `limit`: Limits the number of updates to be retrieved at once. Values
|
||||||
/// between 1—100 are accepted.
|
/// between 1—100 are accepted.
|
||||||
/// - `allowed_updates`: A list the types of updates you want to receive.
|
/// - `allowed_updates`: A list the types of updates you want to receive.
|
||||||
|
///
|
||||||
/// See [`GetUpdates`] for defaults.
|
/// See [`GetUpdates`] for defaults.
|
||||||
///
|
///
|
||||||
/// See also: [`polling_default`](polling_default).
|
/// 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>(
|
pub fn polling<R>(
|
||||||
requester: R,
|
bot: R,
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
limit: Option<u8>,
|
limit: Option<u8>,
|
||||||
allowed_updates: Option<Vec<AllowedUpdate>>,
|
allowed_updates: Option<Vec<AllowedUpdate>>,
|
||||||
) -> impl UpdateListener<R::Err>
|
) -> impl UpdateListener<R::Err, StopToken = impl Send + StopToken>
|
||||||
where
|
where
|
||||||
R: Requester + 'static,
|
R: Requester + Send + 'static,
|
||||||
<R as Requester>::GetUpdatesFaultTolerant: Send,
|
<R as Requester>::GetUpdates: Send,
|
||||||
{
|
{
|
||||||
struct State<B: Requester> {
|
struct State<B: Requester> {
|
||||||
bot: B,
|
bot: B,
|
||||||
|
@ -61,73 +143,57 @@ where
|
||||||
offset: i32,
|
offset: i32,
|
||||||
flag: AsyncStopFlag,
|
flag: AsyncStopFlag,
|
||||||
token: AsyncStopToken,
|
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
|
where
|
||||||
B: Requester,
|
B: Requester + Send,
|
||||||
|
<B as Requester>::GetUpdates: Send,
|
||||||
{
|
{
|
||||||
stream::unfold(st, move |state| async move {
|
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() {
|
if flag.is_stopped() {
|
||||||
let mut req = bot.get_updates_fault_tolerant();
|
let mut req = bot.get_updates().offset(*offset).timeout(0).limit(1);
|
||||||
|
req.payload_mut().allowed_updates = allowed_updates.take();
|
||||||
req.payload_mut().0 = GetUpdates {
|
|
||||||
offset: Some(*offset),
|
|
||||||
timeout: Some(0),
|
|
||||||
limit: Some(1),
|
|
||||||
allowed_updates: allowed_updates.take(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return match req.send().await {
|
return match req.send().await {
|
||||||
Ok(_) => None,
|
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();
|
let mut req = bot.get_updates();
|
||||||
req.payload_mut().0 = GetUpdates {
|
*req.payload_mut() = GetUpdates {
|
||||||
offset: Some(*offset),
|
offset: Some(*offset),
|
||||||
timeout: *timeout,
|
timeout: *timeout,
|
||||||
limit: *limit,
|
limit: *limit,
|
||||||
allowed_updates: allowed_updates.take(),
|
allowed_updates: allowed_updates.take(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let updates = match req.send().await {
|
match req.send().await {
|
||||||
Err(err) => return Some((Either::Left(stream::once(ready(Err(err)))), state)),
|
Ok(updates) => {
|
||||||
Ok(SemiparsedVec(updates)) => {
|
|
||||||
// Set offset to the last update's id + 1
|
// Set offset to the last update's id + 1
|
||||||
if let Some(upd) = updates.last() {
|
if let Some(upd) = updates.last() {
|
||||||
let id: i32 = match upd {
|
*offset = upd.id + 1;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for update in &updates {
|
let updates = updates.into_iter().map(Ok);
|
||||||
if let Err((value, e)) = update {
|
Some((Either::Right(stream::iter(updates)), state))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
};
|
Err(err) => Some((Either::Left(stream::once(ready(Err(err)))), state)),
|
||||||
|
}
|
||||||
Some((Either::Right(stream::iter(updates)), state))
|
|
||||||
})
|
})
|
||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
|
@ -135,13 +201,14 @@ where
|
||||||
let (token, flag) = AsyncStopToken::new_pair();
|
let (token, flag) = AsyncStopToken::new_pair();
|
||||||
|
|
||||||
let state = State {
|
let state = State {
|
||||||
bot: requester,
|
bot,
|
||||||
timeout: timeout.map(|t| t.as_secs().try_into().expect("timeout is too big")),
|
timeout: timeout.map(|t| t.as_secs().try_into().expect("timeout is too big")),
|
||||||
limit,
|
limit,
|
||||||
allowed_updates,
|
allowed_updates,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
flag,
|
flag,
|
||||||
token,
|
token,
|
||||||
|
force_stop: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let stop_token = |st: &mut State<_>| st.token.clone();
|
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 is_webhook_setup {
|
||||||
if let Err(e) = requester.delete_webhook().send().await {
|
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`].
|
/// The function used as [`AsUpdateStream::as_stream`].
|
||||||
///
|
///
|
||||||
/// Must be of type `for<'a> &'a mut St -> impl Stream + 'a` and callable by
|
/// Must implement `for<'a> FnMut(&'a mut St) -> impl Stream + 'a`.
|
||||||
/// `&mut`.
|
|
||||||
pub stream: Assf,
|
pub stream: Assf,
|
||||||
|
|
||||||
/// The function used as [`UpdateListener::stop_token`].
|
/// 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,
|
pub stop_token: Sf,
|
||||||
|
|
||||||
/// The function used as [`UpdateListener::hint_allowed_updates`].
|
/// The function used as [`UpdateListener::hint_allowed_updates`].
|
||||||
///
|
///
|
||||||
/// Must be of type `for<'a, 'b> &'a mut St, &'b mut dyn Iterator<Item =
|
/// Must implement `FnMut(&mut St, &mut dyn Iterator<Item =
|
||||||
/// AllowedUpdate> -> ()`.
|
/// AllowedUpdate>)`.
|
||||||
pub hint_allowed_updates: Option<Hauf>,
|
pub hint_allowed_updates: Option<Hauf>,
|
||||||
|
|
||||||
/// The function used as [`UpdateListener::timeout_hint`].
|
/// 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>,
|
pub timeout_hint: Option<Thf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +77,7 @@ impl<S, E>
|
||||||
Thfn<S>,
|
Thfn<S>,
|
||||||
>
|
>
|
||||||
where
|
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
|
/// Creates a new update listener from a stream of updates which ignores
|
||||||
/// stop signals.
|
/// 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>
|
for StatefulListener<St, Assf, Hauf, Sf, Thf>
|
||||||
where
|
where
|
||||||
(St, Strm): 'a,
|
(St, Strm): 'a,
|
||||||
|
Strm: Send,
|
||||||
Assf: FnMut(&'a mut St) -> Strm,
|
Assf: FnMut(&'a mut St) -> Strm,
|
||||||
Strm: Stream<Item = Result<Update, E>>,
|
Strm: Stream<Item = Result<Update, E>>,
|
||||||
{
|
{
|
||||||
|
|
|
@ -17,6 +17,7 @@ use teloxide_core::{
|
||||||
///
|
///
|
||||||
/// [`Dispatcher`]: crate::dispatching::Dispatcher
|
/// [`Dispatcher`]: crate::dispatching::Dispatcher
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[deprecated(note = "Use dispatching2 instead")]
|
||||||
pub struct UpdateWithCx<R, Upd> {
|
pub struct UpdateWithCx<R, Upd> {
|
||||||
pub requester: R,
|
pub requester: R,
|
||||||
pub update: Upd,
|
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). |
|
| `native-tls` | Enables the [`native-tls`] TLS implementation (enabled by default). |
|
||||||
| `rustls` | Enables the [`rustls`] TLS implementation. |
|
| `rustls` | Enables the [`rustls`] TLS implementation. |
|
||||||
| `ctrlc_handler` | Enables the [`Dispatcher::setup_ctrlc_handler`](dispatching::Dispatcher::setup_ctrlc_handler) function. |
|
| `ctrlc_handler` | Enables the [`Dispatcher::setup_ctrlc_handler`](dispatching::Dispatcher::setup_ctrlc_handler) function. |
|
||||||
| `auto-send` | Enables the `AutoSend` bot adaptor. |
|
| `auto-send` | Enables the [`AutoSend`](adaptors::AutoSend) bot adaptor. |
|
||||||
| `cache-me` | Enables the `CacheMe` 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`]. |
|
| `frunk` | Enables [`teloxide::utils::UpState`]. |
|
||||||
| `full` | Enables all the features except `nightly`. |
|
| `full` | Enables all the features except `nightly`. |
|
||||||
| `nightly` | Enables nightly-only features (see the [teloxide-core features]). |
|
| `nightly` | Enables nightly-only features (see the [teloxide-core features]). |
|
||||||
|
|
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))
|
//! ([Full](https://github.com/teloxide/teloxide/blob/master/examples/dices_bot/src/main.rs))
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! use teloxide::prelude::*;
|
//! use teloxide::prelude2::*;
|
||||||
//!
|
//!
|
||||||
//! # #[tokio::main]
|
//! # #[tokio::main]
|
||||||
//! # async fn main_() {
|
//! # async fn main() {
|
||||||
//! teloxide::enable_logging!();
|
//! teloxide::enable_logging!();
|
||||||
//! log::info!("Starting dices_bot...");
|
//! log::info!("Starting dices_bot...");
|
||||||
//!
|
//!
|
||||||
//! let bot = Bot::from_env().auto_send();
|
//! let bot = Bot::from_env().auto_send();
|
||||||
//!
|
//!
|
||||||
//! teloxide::repl(bot, |message| async move {
|
//! teloxide::repls2::repl(bot, |message: Message, bot: AutoSend<Bot>| async move {
|
||||||
//! message.answer_dice().await?;
|
//! bot.send_dice(message.chat.id).await?;
|
||||||
//! respond(())
|
//! respond(())
|
||||||
//! })
|
//! })
|
||||||
//! .await;
|
//! .await;
|
||||||
|
@ -44,8 +44,6 @@
|
||||||
html_logo_url = "https://github.com/teloxide/teloxide/raw/master/ICON.png",
|
html_logo_url = "https://github.com/teloxide/teloxide/raw/master/ICON.png",
|
||||||
html_favicon_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
|
// We pass "--cfg docsrs" when building docs to add `This is supported on
|
||||||
// feature="..." only.`
|
// feature="..." only.`
|
||||||
//
|
//
|
||||||
|
@ -56,21 +54,33 @@
|
||||||
// $ RUSTFLAGS="--cfg dep_docsrs" RUSTDOCFLAGS="--cfg docsrs -Znormalize-docs" cargo +nightly doc --open --all-features
|
// $ RUSTFLAGS="--cfg dep_docsrs" RUSTDOCFLAGS="--cfg docsrs -Znormalize-docs" cargo +nightly doc --open --all-features
|
||||||
// ```
|
// ```
|
||||||
#![cfg_attr(all(docsrs, feature = "nightly"), feature(doc_cfg))]
|
#![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)]
|
#![allow(clippy::redundant_pattern_matching)]
|
||||||
// https://github.com/rust-lang/rust-clippy/issues/7422
|
// https://github.com/rust-lang/rust-clippy/issues/7422
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
#![allow(clippy::nonstandard_macro_braces)]
|
||||||
|
|
||||||
#[cfg(feature = "ctrlc_handler")]
|
|
||||||
pub use dispatching::repls::{
|
pub use dispatching::repls::{
|
||||||
commands_repl, commands_repl_with_listener, dialogues_repl, dialogues_repl_with_listener, repl,
|
commands_repl, commands_repl_with_listener, dialogues_repl, dialogues_repl_with_listener, repl,
|
||||||
repl_with_listener,
|
repl_with_listener,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "dispatching2")]
|
||||||
|
pub use dispatching2::repls as repls2;
|
||||||
|
|
||||||
mod logging;
|
mod logging;
|
||||||
|
|
||||||
|
// Things from this module is also used for the dispatching2 module.
|
||||||
pub mod dispatching;
|
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 error_handlers;
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
#[cfg(feature = "dispatching2")]
|
||||||
|
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "dispatching2")))]
|
||||||
|
pub mod prelude2;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
|
@ -80,6 +90,9 @@ pub use teloxide_core::*;
|
||||||
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "macros")))]
|
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "macros")))]
|
||||||
pub use teloxide_macros as 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_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "macros")))]
|
||||||
#[cfg(feature = "macros")]
|
#[cfg(feature = "macros")]
|
||||||
pub use teloxide_macros::teloxide;
|
pub use teloxide_macros::teloxide;
|
||||||
|
|
|
@ -3,12 +3,8 @@
|
||||||
/// A logger will **only** print errors, warnings, and general information from
|
/// A logger will **only** print errors, warnings, and general information from
|
||||||
/// teloxide and **all** logs from your program.
|
/// teloxide and **all** logs from your program.
|
||||||
///
|
///
|
||||||
/// # Example
|
|
||||||
/// ```no_compile
|
|
||||||
/// teloxide::enable_logging!();
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Note
|
/// # Note
|
||||||
|
///
|
||||||
/// Calling this macro **is not mandatory**; you can setup if your own logger if
|
/// Calling this macro **is not mandatory**; you can setup if your own logger if
|
||||||
/// you want.
|
/// you want.
|
||||||
///
|
///
|
||||||
|
@ -27,6 +23,7 @@ macro_rules! enable_logging {
|
||||||
/// teloxide and restrict logs from your program by the specified filter.
|
/// teloxide and restrict logs from your program by the specified filter.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// Allow printing all logs from your program up to [`LevelFilter::Debug`] (i.e.
|
/// Allow printing all logs from your program up to [`LevelFilter::Debug`] (i.e.
|
||||||
/// do not print traces):
|
/// do not print traces):
|
||||||
///
|
///
|
||||||
|
@ -35,6 +32,7 @@ macro_rules! enable_logging {
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Note
|
/// # Note
|
||||||
|
///
|
||||||
/// Calling this macro **is not mandatory**; you can setup if your own logger if
|
/// Calling this macro **is not mandatory**; you can setup if your own logger if
|
||||||
/// you want.
|
/// you want.
|
||||||
///
|
///
|
||||||
|
@ -45,7 +43,7 @@ macro_rules! enable_logging_with_filter {
|
||||||
($filter:expr) => {
|
($filter:expr) => {
|
||||||
pretty_env_logger::formatted_builder()
|
pretty_env_logger::formatted_builder()
|
||||||
.write_style(pretty_env_logger::env_logger::WriteStyle::Auto)
|
.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)
|
.filter(Some("teloxide"), log::LevelFilter::Info)
|
||||||
.init();
|
.init();
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
//! Commonly used items.
|
//! Commonly used items.
|
||||||
|
|
||||||
|
#![deprecated(note = "Use dispatching2 instead")]
|
||||||
|
#![allow(deprecated)]
|
||||||
|
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
dispatching::{
|
|
||||||
dialogue::{
|
|
||||||
exit, next, DialogueDispatcher, DialogueStage, DialogueWithCx, GetChatId, Transition,
|
|
||||||
TransitionIn, TransitionOut,
|
|
||||||
},
|
|
||||||
Dispatcher, DispatcherHandlerRx, DispatcherHandlerRxExt, UpdateWithCx,
|
|
||||||
},
|
|
||||||
error_handlers::{LoggingErrorHandler, OnError},
|
error_handlers::{LoggingErrorHandler, OnError},
|
||||||
respond,
|
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_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "macros")))]
|
||||||
#[cfg(feature = "macros")]
|
#[cfg(feature = "macros")]
|
||||||
pub use crate::teloxide;
|
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},
|
fmt::{Display, Formatter},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use std::marker::PhantomData;
|
||||||
#[cfg(feature = "macros")]
|
#[cfg(feature = "macros")]
|
||||||
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "macros")))]
|
#[cfg_attr(all(docsrs, feature = "nightly"), doc(cfg(feature = "macros")))]
|
||||||
pub use teloxide_macros::BotCommand;
|
pub use teloxide_macros::BotCommand;
|
||||||
|
@ -208,6 +209,10 @@ pub trait BotCommand: Sized {
|
||||||
fn parse<N>(s: &str, bot_name: N) -> Result<Self, ParseError>
|
fn parse<N>(s: &str, bot_name: N) -> Result<Self, ParseError>
|
||||||
where
|
where
|
||||||
N: Into<String>;
|
N: Into<String>;
|
||||||
|
fn ty() -> PhantomData<Self> {
|
||||||
|
PhantomData
|
||||||
|
}
|
||||||
|
fn bot_commands() -> Vec<crate::types::BotCommand>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PrefixedBotCommand = String;
|
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 {
|
pub fn code_block_with_lang(code: &str, lang: &str) -> String {
|
||||||
format!(
|
format!(
|
||||||
"<pre><code class=\"language-{}\">{}</code></pre>",
|
"<pre><code class=\"language-{}\">{}</code></pre>",
|
||||||
escape(lang).replace("\"", """),
|
escape(lang).replace('"', """),
|
||||||
escape(code)
|
escape(code)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ pub fn code_inline(s: &str) -> String {
|
||||||
///
|
///
|
||||||
/// [spec]: https://core.telegram.org/bots/api#html-style
|
/// [spec]: https://core.telegram.org/bots/api#html-style
|
||||||
pub fn escape(s: &str) -> String {
|
pub fn escape(s: &str) -> String {
|
||||||
s.replace("&", "&").replace("<", "<").replace(">", ">")
|
s.replace('&', "&").replace('<', "<").replace('>', ">")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn user_mention_or_link(user: &User) -> String {
|
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
|
/// [spec]: https://core.telegram.org/bots/api#html-style
|
||||||
pub fn escape(s: &str) -> String {
|
pub fn escape(s: &str) -> String {
|
||||||
s.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"\#")
|
.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
|
/// Escapes all markdown special characters specific for the inline link URL
|
||||||
/// (``` and `)`).
|
/// (``` and `)`).
|
||||||
pub fn escape_link_url(s: &str) -> String {
|
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
|
/// Escapes all markdown special characters specific for the code block (``` and
|
||||||
/// `\`).
|
/// `\`).
|
||||||
pub fn escape_code(s: &str) -> String {
|
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 {
|
pub fn user_mention_or_link(user: &User) -> String {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
pub mod command;
|
pub mod command;
|
||||||
pub mod html;
|
pub mod html;
|
||||||
pub mod markdown;
|
pub mod markdown;
|
||||||
|
pub(crate) mod shutdown_token;
|
||||||
mod up_state;
|
mod up_state;
|
||||||
|
|
||||||
pub use teloxide_core::net::client_from_env;
|
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