mirror of
https://github.com/teloxide/teloxide.git
synced 2025-03-14 11:44:04 +01:00
Merge pull request #211 from teloxide/improve-dialogues
Reduce boilerplate in dialogues
This commit is contained in:
commit
ac9c7a35f2
27 changed files with 644 additions and 635 deletions
|
@ -44,6 +44,7 @@ async-trait = "0.1.22"
|
|||
futures = "0.3.1"
|
||||
pin-project = "0.4.6"
|
||||
serde_with_macros = "1.0.1"
|
||||
frunk = "0.3.1"
|
||||
|
||||
teloxide-macros = "0.2.1"
|
||||
|
||||
|
|
234
README.md
234
README.md
|
@ -23,12 +23,11 @@
|
|||
|
||||
## Table of contents
|
||||
- [Features](https://github.com/teloxide/teloxide#features)
|
||||
- [Getting started](https://github.com/teloxide/teloxide#getting-started)
|
||||
- [Examples](https://github.com/teloxide/teloxide#examples)
|
||||
- [Setting up your environment](https://github.com/teloxide/teloxide#setting-up-your-environment)
|
||||
- [API overview](https://github.com/teloxide/teloxide#api-overview)
|
||||
- [The ping-pong bot](https://github.com/teloxide/teloxide#the-ping-pong-bot)
|
||||
- [Commands](https://github.com/teloxide/teloxide#commands)
|
||||
- [Guess a number](https://github.com/teloxide/teloxide#guess-a-number)
|
||||
- [More examples!](https://github.com/teloxide/teloxide#more-examples)
|
||||
- [Dialogues](https://github.com/teloxide/teloxide#dialogues)
|
||||
- [Recommendations](https://github.com/teloxide/teloxide#recommendations)
|
||||
- [FAQ](https://github.com/teloxide/teloxide#faq)
|
||||
- [Where I can ask questions?](https://github.com/teloxide/teloxide#where-i-can-ask-questions)
|
||||
|
@ -57,9 +56,10 @@ All the API <a href="https://docs.rs/teloxide/latest/teloxide/types/index.html">
|
|||
Dialogues management is independent of how/where they are stored: just replace one line and make them <a href="https://en.wikipedia.org/wiki/Persistence_(computer_science)">persistent</a> (for example, store on a disk, transmit through a network), without affecting the actual <a href="https://en.wikipedia.org/wiki/Finite-state_machine">FSM</a> algorithm. By default, teloxide stores all user dialogues in RAM. Default database implementations <a href="https://github.com/teloxide/teloxide/issues/183">are coming</a>!
|
||||
</p>
|
||||
|
||||
## Getting started
|
||||
1. Create a new bot using [@Botfather](https://t.me/botfather) to get a token in the format `123456789:blablabla`.
|
||||
2. Initialise the `TELOXIDE_TOKEN` environmental variable to your token:
|
||||
## Setting up your environment
|
||||
1. [Download Rust](http://rustup.rs/).
|
||||
2. Create a new bot using [@Botfather](https://t.me/botfather) to get a token in the format `123456789:blablabla`.
|
||||
3. Initialise the `TELOXIDE_TOKEN` environmental variable to your token:
|
||||
```bash
|
||||
# Unix-like
|
||||
$ export TELOXIDE_TOKEN=<Your token here>
|
||||
|
@ -67,7 +67,7 @@ $ export TELOXIDE_TOKEN=<Your token here>
|
|||
# Windows
|
||||
$ set TELOXIDE_TOKEN=<Your token here>
|
||||
```
|
||||
3. Be sure that you are up to date:
|
||||
4. Be sure that you are up to date:
|
||||
```bash
|
||||
# If you're using stable
|
||||
$ rustup update stable
|
||||
|
@ -78,7 +78,7 @@ $ rustup update nightly
|
|||
$ rustup override set nightly
|
||||
```
|
||||
|
||||
4. Execute `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`:
|
||||
5. Execute `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`:
|
||||
```toml
|
||||
[dependencies]
|
||||
teloxide = "0.2.0"
|
||||
|
@ -87,7 +87,9 @@ tokio = "0.2.11"
|
|||
pretty_env_logger = "0.4.0"
|
||||
```
|
||||
|
||||
## The ping-pong bot
|
||||
## API overview
|
||||
|
||||
### The ping-pong bot
|
||||
This bot has a single message handler, which answers "pong" to each incoming message:
|
||||
|
||||
([Full](https://github.com/teloxide/teloxide/blob/master/examples/ping_pong_bot/src/main.rs))
|
||||
|
@ -119,7 +121,7 @@ async fn main() {
|
|||
</kbd>
|
||||
</div>
|
||||
|
||||
## Commands
|
||||
### Commands
|
||||
Commands are defined similar to how we define CLI using [structopt](https://docs.rs/structopt/0.3.9/structopt/). This bot says "I am a cat! Meow!" on `/meow`, generates a random number within [0; 1) on `/generate`, and shows the usage guide on `/help`:
|
||||
|
||||
([Full](https://github.com/teloxide/teloxide/blob/master/examples/simple_commands_bot/src/main.rs))
|
||||
|
@ -185,84 +187,182 @@ See? The dispatcher gives us a stream of messages, so we can handle it as we wan
|
|||
|
||||
- ... And lots of [others](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html) and [others](https://docs.rs/teloxide/latest/teloxide/dispatching/trait.DispatcherHandlerRxExt.html) and [others](https://docs.rs/tokio/0.2.13/tokio/sync/index.html)!
|
||||
|
||||
## Guess a number
|
||||
Wanna see more? This is a bot, which starts a game on each incoming message. You must guess a number from 1 to 10 (inclusively):
|
||||
### Dialogues
|
||||
Wanna see more? This is how dialogues management is made in teloxide.
|
||||
|
||||
([Full](https://github.com/teloxide/teloxide/blob/master/examples/guess_a_number_bot/src/main.rs))
|
||||
([dialogue_bot/src/states.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states.rs))
|
||||
```rust
|
||||
// Imports are omitted...
|
||||
|
||||
#[derive(SmartDefault)]
|
||||
enum Dialogue {
|
||||
#[default]
|
||||
Start,
|
||||
ReceiveAttempt(u8),
|
||||
pub struct StartState;
|
||||
|
||||
pub struct ReceiveFullNameState {
|
||||
rest: StartState,
|
||||
}
|
||||
|
||||
type Cx<State> = DialogueDispatcherHandlerCx<Message, State>;
|
||||
type Res = ResponseResult<DialogueStage<Dialogue>>;
|
||||
|
||||
async fn start(cx: Cx<()>) -> Res {
|
||||
cx.answer("Let's play a game! Guess a number from 1 to 10 (inclusively).")
|
||||
.send()
|
||||
.await?;
|
||||
next(Dialogue::ReceiveAttempt(thread_rng().gen_range(1, 11)))
|
||||
pub struct ReceiveAgeState {
|
||||
rest: ReceiveFullNameState,
|
||||
full_name: String,
|
||||
}
|
||||
|
||||
async fn receive_attempt(cx: Cx<u8>) -> Res {
|
||||
let secret = cx.dialogue;
|
||||
pub struct ReceiveFavouriteMusicState {
|
||||
rest: ReceiveAgeState,
|
||||
age: u8,
|
||||
}
|
||||
|
||||
match cx.update.text() {
|
||||
None => {
|
||||
cx.answer("Oh, please, send me a text message!").send().await?;
|
||||
next(Dialogue::ReceiveAttempt(secret))
|
||||
#[derive(Display)]
|
||||
#[display(
|
||||
"Your full name: {rest.rest.full_name}, your age: {rest.age}, your \
|
||||
favourite music: {favourite_music}"
|
||||
)]
|
||||
pub struct ExitState {
|
||||
rest: ReceiveFavouriteMusicState,
|
||||
favourite_music: FavouriteMusic,
|
||||
}
|
||||
|
||||
up!(
|
||||
StartState -> ReceiveFullNameState,
|
||||
ReceiveFullNameState + [full_name: String] -> ReceiveAgeState,
|
||||
ReceiveAgeState + [age: u8] -> ReceiveFavouriteMusicState,
|
||||
ReceiveFavouriteMusicState + [favourite_music: FavouriteMusic] -> ExitState,
|
||||
);
|
||||
|
||||
pub type Dialogue = Coprod!(
|
||||
StartState,
|
||||
ReceiveFullNameState,
|
||||
ReceiveAgeState,
|
||||
ReceiveFavouriteMusicState,
|
||||
);
|
||||
|
||||
wrap_dialogue!(
|
||||
Wrapper(Dialogue),
|
||||
default Self(Dialogue::inject(StartState)),
|
||||
);
|
||||
```
|
||||
|
||||
The [`wrap_dialogue!`](https://docs.rs/teloxide/latest/teloxide/macro.wrap_dialogue.html) macro generates a new-type of `Dialogue` with a default implementation.
|
||||
|
||||
([dialogue_bot/src/transitions.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/transitions.rs))
|
||||
```rust
|
||||
// Imports are omitted...
|
||||
|
||||
pub type In<State> = TransitionIn<State, std::convert::Infallible>;
|
||||
pub type Out = TransitionOut<Wrapper>;
|
||||
|
||||
pub async fn start(cx: In<StartState>) -> Out {
|
||||
let (cx, dialogue) = cx.unpack();
|
||||
|
||||
cx.answer_str("Let's start! First, what's your full name?").await?;
|
||||
next(dialogue.up())
|
||||
}
|
||||
|
||||
pub async fn receive_full_name(cx: In<ReceiveFullNameState>) -> Out {
|
||||
let (cx, dialogue) = cx.unpack();
|
||||
|
||||
match cx.update.text_owned() {
|
||||
Some(full_name) => {
|
||||
cx.answer_str("What a wonderful name! Your age?").await?;
|
||||
next(dialogue.up(full_name))
|
||||
}
|
||||
_ => {
|
||||
cx.answer_str("Please, enter a text message!").await?;
|
||||
next(dialogue)
|
||||
}
|
||||
Some(text) => match text.parse::<u8>() {
|
||||
Ok(attempt) => {
|
||||
if attempt == secret {
|
||||
cx.answer("Congratulations! You won!").send().await?;
|
||||
exit()
|
||||
} else {
|
||||
cx.answer("No.").send().await?;
|
||||
next(Dialogue::ReceiveAttempt(secret))
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
cx.answer("Oh, please, send me a number in the range [1; 10]!")
|
||||
.send()
|
||||
.await?;
|
||||
next(Dialogue::ReceiveAttempt(secret))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_message(
|
||||
cx: DialogueDispatcherHandlerCx<Message, Dialogue>,
|
||||
) -> Res {
|
||||
// Match is omitted...
|
||||
pub async fn receive_age(cx: In<ReceiveAgeState>) -> Out {
|
||||
let (cx, dialogue) = cx.unpack();
|
||||
|
||||
match cx.update.text().map(str::parse) {
|
||||
Some(Ok(age)) => {
|
||||
cx.answer("Good. Now choose your favourite music:")
|
||||
.reply_markup(FavouriteMusic::markup())
|
||||
.send()
|
||||
.await?;
|
||||
next(dialogue.up(age))
|
||||
}
|
||||
_ => {
|
||||
cx.answer_str("Please, enter a number!").await?;
|
||||
next(dialogue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Setup is omitted...
|
||||
pub async fn receive_favourite_music(
|
||||
cx: In<ReceiveFavouriteMusicState>,
|
||||
) -> Out {
|
||||
let (cx, dialogue) = cx.unpack();
|
||||
|
||||
match cx.update.text().map(str::parse) {
|
||||
Some(Ok(favourite_music)) => {
|
||||
cx.answer_str(format!("Fine. {}", dialogue.up(favourite_music)))
|
||||
.await?;
|
||||
exit()
|
||||
}
|
||||
_ => {
|
||||
cx.answer_str("Please, enter from the keyboard!").await?;
|
||||
next(dialogue)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<div align="center">
|
||||
<kbd>
|
||||
<img src=https://github.com/teloxide/teloxide/raw/master/media/GUESS_A_NUMBER_BOT.png width="600" />
|
||||
</kbd>
|
||||
<br/><br/>
|
||||
</div>
|
||||
([dialogue_bot/src/favourite_music.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/favourite_music.rs))
|
||||
```rust
|
||||
// Imports are omitted...
|
||||
|
||||
Our [finite automaton](https://en.wikipedia.org/wiki/Finite-state_machine), designating a user dialogue, cannot be in an invalid state, and this is why it is called "type-safe". We could use `enum` + `Option`s instead, but it would lead us to lots of unpleasant `.unwrap()`s.
|
||||
#[derive(Copy, Clone, Display, FromStr)]
|
||||
pub enum FavouriteMusic {
|
||||
Rock,
|
||||
Metal,
|
||||
Pop,
|
||||
Other,
|
||||
}
|
||||
|
||||
Remember that a classical [finite automaton](https://en.wikipedia.org/wiki/Finite-state_machine) is defined by its initial state, a list of its possible states and a transition function? We can think that `Dialogue` is a finite automaton with a context type at each state (`Dialogue::Start` has `()`, `Dialogue::ReceiveAttempt` has `u8`).
|
||||
impl FavouriteMusic {
|
||||
pub fn markup() -> ReplyKeyboardMarkup {
|
||||
ReplyKeyboardMarkup::default().append_row(vec![
|
||||
KeyboardButton::new("Rock"),
|
||||
KeyboardButton::new("Metal"),
|
||||
KeyboardButton::new("Pop"),
|
||||
KeyboardButton::new("Other"),
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [examples/dialogue_bot](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/main.rs) to see a bit more complicated bot with dialogues.
|
||||
|
||||
## [More examples!](https://github.com/teloxide/teloxide/tree/master/examples)
|
||||
([dialogue_bot/src/main.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/main.rs))
|
||||
```rust
|
||||
// Imports are omitted...
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
teloxide::enable_logging!();
|
||||
log::info!("Starting dialogue_bot!");
|
||||
|
||||
let bot = Bot::from_env();
|
||||
|
||||
Dispatcher::new(bot)
|
||||
.messages_handler(DialogueDispatcher::new(|cx| async move {
|
||||
let DialogueWithCx { cx, dialogue } = cx;
|
||||
|
||||
// Unwrap without panic because of std::convert::Infallible.
|
||||
let Wrapper(dialogue) = dialogue.unwrap();
|
||||
|
||||
dispatch!(
|
||||
[cx, dialogue] ->
|
||||
[start, receive_full_name, receive_age, receive_favourite_music]
|
||||
)
|
||||
.expect("Something wrong with the bot!")
|
||||
}))
|
||||
.dispatch()
|
||||
.await;
|
||||
}
|
||||
```
|
||||
|
||||
[More examples!](https://github.com/teloxide/teloxide/tree/master/examples)
|
||||
|
||||
## Recommendations
|
||||
- Use this pattern:
|
||||
|
|
|
@ -10,9 +10,9 @@ edition = "2018"
|
|||
log = "0.4.8"
|
||||
tokio = "0.2.9"
|
||||
pretty_env_logger = "0.4.0"
|
||||
smart-default = "0.6.0"
|
||||
parse-display = "0.1.1"
|
||||
teloxide = { path = "../../" }
|
||||
frunk = "0.3.1"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
22
examples/dialogue_bot/src/favourite_music.rs
Normal file
22
examples/dialogue_bot/src/favourite_music.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use parse_display::{Display, FromStr};
|
||||
|
||||
use teloxide::types::{KeyboardButton, ReplyKeyboardMarkup};
|
||||
|
||||
#[derive(Copy, Clone, Display, FromStr)]
|
||||
pub enum FavouriteMusic {
|
||||
Rock,
|
||||
Metal,
|
||||
Pop,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl FavouriteMusic {
|
||||
pub fn markup() -> ReplyKeyboardMarkup {
|
||||
ReplyKeyboardMarkup::default().append_row(vec![
|
||||
KeyboardButton::new("Rock"),
|
||||
KeyboardButton::new("Metal"),
|
||||
KeyboardButton::new("Pop"),
|
||||
KeyboardButton::new("Other"),
|
||||
])
|
||||
}
|
||||
}
|
|
@ -15,167 +15,16 @@
|
|||
// ```
|
||||
|
||||
#![allow(clippy::trivial_regex)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate smart_default;
|
||||
mod favourite_music;
|
||||
mod states;
|
||||
mod transitions;
|
||||
|
||||
use std::convert::Infallible;
|
||||
use teloxide::{
|
||||
prelude::*,
|
||||
types::{KeyboardButton, ReplyKeyboardMarkup},
|
||||
};
|
||||
use states::*;
|
||||
use transitions::*;
|
||||
|
||||
use parse_display::{Display, FromStr};
|
||||
|
||||
// ============================================================================
|
||||
// [Favourite music kinds]
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Copy, Clone, Display, FromStr)]
|
||||
enum FavouriteMusic {
|
||||
Rock,
|
||||
Metal,
|
||||
Pop,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl FavouriteMusic {
|
||||
fn markup() -> ReplyKeyboardMarkup {
|
||||
ReplyKeyboardMarkup::default().append_row(vec![
|
||||
KeyboardButton::new("Rock"),
|
||||
KeyboardButton::new("Metal"),
|
||||
KeyboardButton::new("Pop"),
|
||||
KeyboardButton::new("Other"),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// [A type-safe finite automaton]
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ReceiveAgeState {
|
||||
full_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ReceiveFavouriteMusicState {
|
||||
data: ReceiveAgeState,
|
||||
age: u8,
|
||||
}
|
||||
|
||||
#[derive(Display)]
|
||||
#[display(
|
||||
"Your full name: {data.data.full_name}, your age: {data.age}, your \
|
||||
favourite music: {favourite_music}"
|
||||
)]
|
||||
struct ExitState {
|
||||
data: ReceiveFavouriteMusicState,
|
||||
favourite_music: FavouriteMusic,
|
||||
}
|
||||
|
||||
#[derive(SmartDefault)]
|
||||
enum Dialogue {
|
||||
#[default]
|
||||
Start,
|
||||
ReceiveFullName,
|
||||
ReceiveAge(ReceiveAgeState),
|
||||
ReceiveFavouriteMusic(ReceiveFavouriteMusicState),
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// [Control a dialogue]
|
||||
// ============================================================================
|
||||
|
||||
type Cx<State> = DialogueDispatcherHandlerCx<Message, State, Infallible>;
|
||||
type Res = ResponseResult<DialogueStage<Dialogue>>;
|
||||
|
||||
async fn start(cx: Cx<()>) -> Res {
|
||||
cx.answer("Let's start! First, what's your full name?").send().await?;
|
||||
next(Dialogue::ReceiveFullName)
|
||||
}
|
||||
|
||||
async fn full_name(cx: Cx<()>) -> Res {
|
||||
match cx.update.text() {
|
||||
None => {
|
||||
cx.answer("Please, send me a text message!").send().await?;
|
||||
next(Dialogue::ReceiveFullName)
|
||||
}
|
||||
Some(full_name) => {
|
||||
cx.answer("What a wonderful name! Your age?").send().await?;
|
||||
next(Dialogue::ReceiveAge(ReceiveAgeState {
|
||||
full_name: full_name.to_owned(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn age(cx: Cx<ReceiveAgeState>) -> Res {
|
||||
match cx.update.text().unwrap().parse() {
|
||||
Ok(age) => {
|
||||
cx.answer("Good. Now choose your favourite music:")
|
||||
.reply_markup(FavouriteMusic::markup())
|
||||
.send()
|
||||
.await?;
|
||||
next(Dialogue::ReceiveFavouriteMusic(ReceiveFavouriteMusicState {
|
||||
data: cx.dialogue.unwrap(),
|
||||
age,
|
||||
}))
|
||||
}
|
||||
Err(_) => {
|
||||
cx.answer("Oh, please, enter a number!").send().await?;
|
||||
next(Dialogue::ReceiveAge(cx.dialogue.unwrap()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn favourite_music(cx: Cx<ReceiveFavouriteMusicState>) -> Res {
|
||||
match cx.update.text().unwrap().parse() {
|
||||
Ok(favourite_music) => {
|
||||
cx.answer(format!(
|
||||
"Fine. {}",
|
||||
ExitState {
|
||||
data: cx.dialogue.clone().unwrap(),
|
||||
favourite_music
|
||||
}
|
||||
))
|
||||
.send()
|
||||
.await?;
|
||||
exit()
|
||||
}
|
||||
Err(_) => {
|
||||
cx.answer("Oh, please, enter from the keyboard!").send().await?;
|
||||
next(Dialogue::ReceiveFavouriteMusic(cx.dialogue.unwrap()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_message(cx: Cx<Dialogue>) -> Res {
|
||||
let DialogueDispatcherHandlerCx { bot, update, dialogue } = cx;
|
||||
|
||||
// You need handle the error instead of panicking in real-world code, maybe
|
||||
// send diagnostics to a development chat.
|
||||
match dialogue.expect("Failed to get dialogue info from storage") {
|
||||
Dialogue::Start => {
|
||||
start(DialogueDispatcherHandlerCx::new(bot, update, ())).await
|
||||
}
|
||||
Dialogue::ReceiveFullName => {
|
||||
full_name(DialogueDispatcherHandlerCx::new(bot, update, ())).await
|
||||
}
|
||||
Dialogue::ReceiveAge(s) => {
|
||||
age(DialogueDispatcherHandlerCx::new(bot, update, s)).await
|
||||
}
|
||||
Dialogue::ReceiveFavouriteMusic(s) => {
|
||||
favourite_music(DialogueDispatcherHandlerCx::new(bot, update, s))
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// [Run!]
|
||||
// ============================================================================
|
||||
use teloxide::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
|
@ -190,7 +39,16 @@ async fn run() {
|
|||
|
||||
Dispatcher::new(bot)
|
||||
.messages_handler(DialogueDispatcher::new(|cx| async move {
|
||||
handle_message(cx).await.expect("Something wrong with the bot!")
|
||||
let DialogueWithCx { cx, dialogue } = cx;
|
||||
|
||||
// Unwrap without panic because of std::convert::Infallible.
|
||||
let Wrapper(dialogue) = dialogue.unwrap();
|
||||
|
||||
dispatch!(
|
||||
[cx, dialogue] ->
|
||||
[start, receive_full_name, receive_age, receive_favourite_music]
|
||||
)
|
||||
.expect("Something wrong with the bot!")
|
||||
}))
|
||||
.dispatch()
|
||||
.await;
|
||||
|
|
49
examples/dialogue_bot/src/states.rs
Normal file
49
examples/dialogue_bot/src/states.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use teloxide::prelude::*;
|
||||
|
||||
use super::favourite_music::FavouriteMusic;
|
||||
use parse_display::Display;
|
||||
|
||||
pub struct StartState;
|
||||
|
||||
pub struct ReceiveFullNameState {
|
||||
rest: StartState,
|
||||
}
|
||||
|
||||
pub struct ReceiveAgeState {
|
||||
rest: ReceiveFullNameState,
|
||||
full_name: String,
|
||||
}
|
||||
|
||||
pub struct ReceiveFavouriteMusicState {
|
||||
rest: ReceiveAgeState,
|
||||
age: u8,
|
||||
}
|
||||
|
||||
#[derive(Display)]
|
||||
#[display(
|
||||
"Your full name: {rest.rest.full_name}, your age: {rest.age}, your \
|
||||
favourite music: {favourite_music}"
|
||||
)]
|
||||
pub struct ExitState {
|
||||
rest: ReceiveFavouriteMusicState,
|
||||
favourite_music: FavouriteMusic,
|
||||
}
|
||||
|
||||
up!(
|
||||
StartState -> ReceiveFullNameState,
|
||||
ReceiveFullNameState + [full_name: String] -> ReceiveAgeState,
|
||||
ReceiveAgeState + [age: u8] -> ReceiveFavouriteMusicState,
|
||||
ReceiveFavouriteMusicState + [favourite_music: FavouriteMusic] -> ExitState,
|
||||
);
|
||||
|
||||
pub type Dialogue = Coprod!(
|
||||
StartState,
|
||||
ReceiveFullNameState,
|
||||
ReceiveAgeState,
|
||||
ReceiveFavouriteMusicState,
|
||||
);
|
||||
|
||||
wrap_dialogue!(
|
||||
Wrapper(Dialogue),
|
||||
default Self(Dialogue::inject(StartState)),
|
||||
);
|
64
examples/dialogue_bot/src/transitions.rs
Normal file
64
examples/dialogue_bot/src/transitions.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use teloxide::prelude::*;
|
||||
|
||||
use super::{favourite_music::FavouriteMusic, states::*};
|
||||
|
||||
pub type In<State> = TransitionIn<State, std::convert::Infallible>;
|
||||
pub type Out = TransitionOut<Wrapper>;
|
||||
|
||||
pub async fn start(cx: In<StartState>) -> Out {
|
||||
let (cx, dialogue) = cx.unpack();
|
||||
|
||||
cx.answer_str("Let's start! First, what's your full name?").await?;
|
||||
next(dialogue.up())
|
||||
}
|
||||
|
||||
pub async fn receive_full_name(cx: In<ReceiveFullNameState>) -> Out {
|
||||
let (cx, dialogue) = cx.unpack();
|
||||
|
||||
match cx.update.text_owned() {
|
||||
Some(full_name) => {
|
||||
cx.answer_str("What a wonderful name! Your age?").await?;
|
||||
next(dialogue.up(full_name))
|
||||
}
|
||||
_ => {
|
||||
cx.answer_str("Please, enter a text message!").await?;
|
||||
next(dialogue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn receive_age(cx: In<ReceiveAgeState>) -> Out {
|
||||
let (cx, dialogue) = cx.unpack();
|
||||
|
||||
match cx.update.text().map(str::parse) {
|
||||
Some(Ok(age)) => {
|
||||
cx.answer("Good. Now choose your favourite music:")
|
||||
.reply_markup(FavouriteMusic::markup())
|
||||
.send()
|
||||
.await?;
|
||||
next(dialogue.up(age))
|
||||
}
|
||||
_ => {
|
||||
cx.answer_str("Please, enter a number!").await?;
|
||||
next(dialogue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn receive_favourite_music(
|
||||
cx: In<ReceiveFavouriteMusicState>,
|
||||
) -> Out {
|
||||
let (cx, dialogue) = cx.unpack();
|
||||
|
||||
match cx.update.text().map(str::parse) {
|
||||
Some(Ok(favourite_music)) => {
|
||||
cx.answer_str(format!("Fine. {}", dialogue.up(favourite_music)))
|
||||
.await?;
|
||||
exit()
|
||||
}
|
||||
_ => {
|
||||
cx.answer_str("Please, enter from the keyboard!").await?;
|
||||
next(dialogue)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
[package]
|
||||
name = "guess_a_number_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]
|
||||
log = "0.4.8"
|
||||
tokio = "0.2.9"
|
||||
smart-default = "0.6.0"
|
||||
rand = "0.7.3"
|
||||
pretty_env_logger = "0.4.0"
|
||||
teloxide = { path = "../../" }
|
|
@ -1,121 +0,0 @@
|
|||
// This is a guess-a-number game!
|
||||
//
|
||||
// # Example
|
||||
// ```
|
||||
// - Hello
|
||||
// - Let's play a game! Guess a number from 1 to 10 (inclusively).
|
||||
// - 4
|
||||
// - No.
|
||||
// - 3
|
||||
// - No.
|
||||
// - Blablabla
|
||||
// - Oh, please, send me a text message!
|
||||
// - 111
|
||||
// - Oh, please, send me a number in the range [1; 10]!
|
||||
// - 5
|
||||
// - Congratulations! You won!
|
||||
// ```
|
||||
|
||||
#[macro_use]
|
||||
extern crate smart_default;
|
||||
|
||||
use teloxide::prelude::*;
|
||||
|
||||
use rand::{thread_rng, Rng};
|
||||
use std::convert::Infallible;
|
||||
|
||||
// ============================================================================
|
||||
// [A type-safe finite automaton]
|
||||
// ============================================================================
|
||||
|
||||
#[derive(SmartDefault)]
|
||||
enum Dialogue {
|
||||
#[default]
|
||||
Start,
|
||||
ReceiveAttempt(u8),
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// [Control a dialogue]
|
||||
// ============================================================================
|
||||
|
||||
type Cx<State> = DialogueDispatcherHandlerCx<Message, State, Infallible>;
|
||||
type Res = ResponseResult<DialogueStage<Dialogue>>;
|
||||
|
||||
async fn start(cx: Cx<()>) -> Res {
|
||||
cx.answer("Let's play a game! Guess a number from 1 to 10 (inclusively).")
|
||||
.send()
|
||||
.await?;
|
||||
next(Dialogue::ReceiveAttempt(thread_rng().gen_range(1, 11)))
|
||||
}
|
||||
|
||||
async fn receive_attempt(cx: Cx<u8>) -> Res {
|
||||
let secret = cx.dialogue.unwrap();
|
||||
|
||||
match cx.update.text() {
|
||||
None => {
|
||||
cx.answer("Oh, please, send me a text message!").send().await?;
|
||||
next(Dialogue::ReceiveAttempt(secret))
|
||||
}
|
||||
Some(text) => match text.parse::<u8>() {
|
||||
Ok(attempt) => {
|
||||
if attempt == secret {
|
||||
cx.answer("Congratulations! You won!").send().await?;
|
||||
exit()
|
||||
} else {
|
||||
cx.answer("No.").send().await?;
|
||||
next(Dialogue::ReceiveAttempt(secret))
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
cx.answer("Oh, please, send me a number in the range [1; 10]!")
|
||||
.send()
|
||||
.await?;
|
||||
next(Dialogue::ReceiveAttempt(secret))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_message(
|
||||
cx: DialogueDispatcherHandlerCx<Message, Dialogue, Infallible>,
|
||||
) -> Res {
|
||||
let DialogueDispatcherHandlerCx { bot, update, dialogue } = cx;
|
||||
|
||||
// You need handle the error instead of panicking in real-world code, maybe
|
||||
// send diagnostics to a development chat.
|
||||
match dialogue.expect("Failed to get dialogue info from storage") {
|
||||
Dialogue::Start => {
|
||||
start(DialogueDispatcherHandlerCx::new(bot, update, ())).await
|
||||
}
|
||||
Dialogue::ReceiveAttempt(secret) => {
|
||||
receive_attempt(DialogueDispatcherHandlerCx::new(
|
||||
bot, update, secret,
|
||||
))
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// [Run!]
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
run().await;
|
||||
}
|
||||
|
||||
async fn run() {
|
||||
teloxide::enable_logging!();
|
||||
log::info!("Starting guess_a_number_bot!");
|
||||
|
||||
let bot = Bot::from_env();
|
||||
|
||||
Dispatcher::new(bot)
|
||||
.messages_handler(DialogueDispatcher::new(|cx| async move {
|
||||
handle_message(cx).await.expect("Something wrong with the bot!")
|
||||
}))
|
||||
.dispatch()
|
||||
.await;
|
||||
}
|
|
@ -11,7 +11,7 @@ log = "0.4.8"
|
|||
futures = "0.3.4"
|
||||
tokio = "0.2.9"
|
||||
pretty_env_logger = "0.4.0"
|
||||
teloxide = "0.2.0"
|
||||
teloxide = { path = "../../" }
|
||||
|
||||
# Used to setup a webhook
|
||||
warp = "0.2.2"
|
||||
|
|
|
@ -14,14 +14,19 @@ async fn main() {
|
|||
run().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> {
|
||||
log::error!("Cannot process the request due to: {:?}", error);
|
||||
Ok(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
pub async fn webhook<'a>(bot: Arc<Bot>) -> impl update_listeners::UpdateListener<Infallible> {
|
||||
pub async fn webhook<'a>(
|
||||
bot: Arc<Bot>,
|
||||
) -> impl update_listeners::UpdateListener<Infallible> {
|
||||
// Heroku defines auto defines a port value
|
||||
let teloxide_token = env::var("TELOXIDE_TOKEN").expect("TELOXIDE_TOKEN env variable missing");
|
||||
let teloxide_token = env::var("TELOXIDE_TOKEN")
|
||||
.expect("TELOXIDE_TOKEN env variable missing");
|
||||
let port: u16 = env::var("PORT")
|
||||
.expect("PORT env variable missing")
|
||||
.parse()
|
||||
|
@ -31,10 +36,7 @@ pub async fn webhook<'a>(bot: Arc<Bot>) -> impl update_listeners::UpdateListener
|
|||
let path = format!("bot{}", teloxide_token);
|
||||
let url = format!("https://{}/{}", host, path);
|
||||
|
||||
bot.set_webhook(url)
|
||||
.send()
|
||||
.await
|
||||
.expect("Cannot setup a webhook");
|
||||
bot.set_webhook(url).send().await.expect("Cannot setup a webhook");
|
||||
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
|
||||
|
@ -80,12 +82,14 @@ async fn run() {
|
|||
Dispatcher::new(Arc::clone(&bot))
|
||||
.messages_handler(|rx: DispatcherHandlerRx<Message>| {
|
||||
rx.for_each(|message| async move {
|
||||
message.answer("pong").send().await.log_on_error().await;
|
||||
message.answer_str("pong").await.log_on_error().await;
|
||||
})
|
||||
})
|
||||
.dispatch_with_listener(
|
||||
webhook(bot).await,
|
||||
LoggingErrorHandler::with_custom_text("An error from the update listener"),
|
||||
LoggingErrorHandler::with_custom_text(
|
||||
"An error from the update listener",
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ async fn run() {
|
|||
Dispatcher::new(bot)
|
||||
.messages_handler(|rx: DispatcherHandlerRx<Message>| {
|
||||
rx.for_each(|message| async move {
|
||||
message.answer("pong").send().await.log_on_error().await;
|
||||
message.answer_str("pong").await.log_on_error().await;
|
||||
})
|
||||
})
|
||||
.dispatch()
|
||||
|
|
|
@ -26,11 +26,10 @@ async fn run() {
|
|||
let previous = MESSAGES_TOTAL.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
message
|
||||
.answer(format!(
|
||||
.answer_str(format!(
|
||||
"I received {} messages in total.",
|
||||
previous
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.log_on_error()
|
||||
.await;
|
||||
|
|
|
@ -18,7 +18,7 @@ fn generate() -> String {
|
|||
}
|
||||
|
||||
async fn answer(
|
||||
cx: DispatcherHandlerCx<Message>,
|
||||
cx: UpdateWithCx<Message>,
|
||||
command: Command,
|
||||
) -> ResponseResult<()> {
|
||||
match command {
|
||||
|
|
|
@ -63,7 +63,7 @@ async fn run() {
|
|||
Dispatcher::new(Arc::clone(&bot))
|
||||
.messages_handler(|rx: DispatcherHandlerRx<Message>| {
|
||||
rx.for_each(|message| async move {
|
||||
message.answer("pong").send().await.log_on_error().await;
|
||||
message.answer_str("pong").await.log_on_error().await;
|
||||
})
|
||||
})
|
||||
.dispatch_with_listener(
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use crate::dispatching::{
|
||||
dialogue::{
|
||||
DialogueDispatcherHandler, DialogueDispatcherHandlerCx, DialogueStage,
|
||||
GetChatId, InMemStorage, Storage,
|
||||
DialogueDispatcherHandler, DialogueStage, DialogueWithCx, GetChatId,
|
||||
InMemStorage, Storage,
|
||||
},
|
||||
DispatcherHandler, DispatcherHandlerCx,
|
||||
DispatcherHandler, UpdateWithCx,
|
||||
};
|
||||
use std::{convert::Infallible, marker::PhantomData};
|
||||
|
||||
|
@ -34,7 +34,7 @@ pub struct DialogueDispatcher<D, S, H, Upd> {
|
|||
/// A value is the TX part of an unbounded asynchronous MPSC channel. A
|
||||
/// handler that executes updates from the same chat ID sequentially
|
||||
/// handles the RX part.
|
||||
senders: Arc<Map<i64, mpsc::UnboundedSender<DispatcherHandlerCx<Upd>>>>,
|
||||
senders: Arc<Map<i64, mpsc::UnboundedSender<UpdateWithCx<Upd>>>>,
|
||||
}
|
||||
|
||||
impl<D, H, Upd> DialogueDispatcher<D, InMemStorage<D>, H, Upd>
|
||||
|
@ -78,14 +78,14 @@ where
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
fn new_tx(&self) -> mpsc::UnboundedSender<DispatcherHandlerCx<Upd>> {
|
||||
fn new_tx(&self) -> mpsc::UnboundedSender<UpdateWithCx<Upd>> {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
|
||||
let storage = Arc::clone(&self.storage);
|
||||
let handler = Arc::clone(&self.handler);
|
||||
let senders = Arc::clone(&self.senders);
|
||||
|
||||
tokio::spawn(rx.for_each(move |cx: DispatcherHandlerCx<Upd>| {
|
||||
tokio::spawn(rx.for_each(move |cx: UpdateWithCx<Upd>| {
|
||||
let storage = Arc::clone(&storage);
|
||||
let handler = Arc::clone(&handler);
|
||||
let senders = Arc::clone(&senders);
|
||||
|
@ -98,14 +98,7 @@ where
|
|||
.await
|
||||
.map(Option::unwrap_or_default);
|
||||
|
||||
match handler
|
||||
.handle(DialogueDispatcherHandlerCx {
|
||||
bot: cx.bot,
|
||||
update: cx.update,
|
||||
dialogue,
|
||||
})
|
||||
.await
|
||||
{
|
||||
match handler.handle(DialogueWithCx { cx, dialogue }).await {
|
||||
DialogueStage::Next(new_dialogue) => {
|
||||
if let Ok(Some(_)) =
|
||||
storage.update_dialogue(chat_id, new_dialogue).await
|
||||
|
@ -144,10 +137,10 @@ where
|
|||
{
|
||||
fn handle(
|
||||
self,
|
||||
updates: mpsc::UnboundedReceiver<DispatcherHandlerCx<Upd>>,
|
||||
updates: mpsc::UnboundedReceiver<UpdateWithCx<Upd>>,
|
||||
) -> BoxFuture<'static, ()>
|
||||
where
|
||||
DispatcherHandlerCx<Upd>: 'static,
|
||||
UpdateWithCx<Upd>: 'static,
|
||||
{
|
||||
let this = Arc::new(self);
|
||||
|
||||
|
@ -221,10 +214,10 @@ mod tests {
|
|||
}
|
||||
|
||||
let dispatcher = DialogueDispatcher::new(
|
||||
|cx: DialogueDispatcherHandlerCx<MyUpdate, (), Infallible>| async move {
|
||||
|cx: DialogueWithCx<MyUpdate, (), Infallible>| async move {
|
||||
delay_for(Duration::from_millis(300)).await;
|
||||
|
||||
match cx.update {
|
||||
match cx.cx.update {
|
||||
MyUpdate { chat_id: 1, unique_number } => {
|
||||
SEQ1.lock().await.push(unique_number);
|
||||
}
|
||||
|
@ -266,11 +259,11 @@ mod tests {
|
|||
MyUpdate::new(3, 1611),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|update| DispatcherHandlerCx {
|
||||
.map(|update| UpdateWithCx {
|
||||
update,
|
||||
bot: Bot::new("Doesn't matter here"),
|
||||
})
|
||||
.collect::<Vec<DispatcherHandlerCx<MyUpdate>>>(),
|
||||
.collect::<Vec<UpdateWithCx<MyUpdate>>>(),
|
||||
);
|
||||
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::prelude::{DialogueDispatcherHandlerCx, DialogueStage};
|
||||
use crate::prelude::{DialogueStage, DialogueWithCx};
|
||||
use futures::future::BoxFuture;
|
||||
use std::{future::Future, sync::Arc};
|
||||
|
||||
|
@ -12,26 +12,23 @@ pub trait DialogueDispatcherHandler<Upd, D, E> {
|
|||
#[must_use]
|
||||
fn handle(
|
||||
self: Arc<Self>,
|
||||
cx: DialogueDispatcherHandlerCx<Upd, D, E>,
|
||||
cx: DialogueWithCx<Upd, D, E>,
|
||||
) -> BoxFuture<'static, DialogueStage<D>>
|
||||
where
|
||||
DialogueDispatcherHandlerCx<Upd, D, E>: Send + 'static;
|
||||
DialogueWithCx<Upd, D, E>: Send + 'static;
|
||||
}
|
||||
|
||||
impl<Upd, D, E, F, Fut> DialogueDispatcherHandler<Upd, D, E> for F
|
||||
where
|
||||
F: Fn(DialogueDispatcherHandlerCx<Upd, D, E>) -> Fut
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
F: Fn(DialogueWithCx<Upd, D, E>) -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = DialogueStage<D>> + Send + 'static,
|
||||
{
|
||||
fn handle(
|
||||
self: Arc<Self>,
|
||||
cx: DialogueDispatcherHandlerCx<Upd, D, E>,
|
||||
cx: DialogueWithCx<Upd, D, E>,
|
||||
) -> BoxFuture<'static, Fut::Output>
|
||||
where
|
||||
DialogueDispatcherHandlerCx<Upd, D, E>: Send + 'static,
|
||||
DialogueWithCx<Upd, D, E>: Send + 'static,
|
||||
{
|
||||
Box::pin(async move { self(cx).await })
|
||||
}
|
||||
|
|
|
@ -1,186 +0,0 @@
|
|||
use crate::{
|
||||
dispatching::dialogue::GetChatId,
|
||||
requests::{
|
||||
DeleteMessage, EditMessageCaption, EditMessageText, ForwardMessage,
|
||||
PinChatMessage, SendAnimation, SendAudio, SendContact, SendDocument,
|
||||
SendLocation, SendMediaGroup, SendMessage, SendPhoto, SendSticker,
|
||||
SendVenue, SendVideo, SendVideoNote, SendVoice,
|
||||
},
|
||||
types::{ChatId, ChatOrInlineMessage, InputFile, InputMedia, Message},
|
||||
Bot,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A context of a [`DialogueDispatcher`]'s message handler.
|
||||
///
|
||||
/// See [the module-level documentation for the design
|
||||
/// overview](crate::dispatching::dialogue).
|
||||
///
|
||||
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
|
||||
#[derive(Debug)]
|
||||
pub struct DialogueDispatcherHandlerCx<Upd, D, E> {
|
||||
pub bot: Arc<Bot>,
|
||||
pub update: Upd,
|
||||
pub dialogue: Result<D, E>,
|
||||
}
|
||||
|
||||
impl<Upd, D, E> DialogueDispatcherHandlerCx<Upd, D, E> {
|
||||
/// Creates a new instance with the provided fields.
|
||||
pub fn new(bot: Arc<Bot>, update: Upd, dialogue: D) -> Self {
|
||||
Self { bot, update, dialogue: Ok(dialogue) }
|
||||
}
|
||||
|
||||
/// Creates a new instance by substituting a dialogue and preserving
|
||||
/// `self.bot` and `self.update`.
|
||||
pub fn with_new_dialogue<Nd, Ne>(
|
||||
self,
|
||||
new_dialogue: Result<Nd, Ne>,
|
||||
) -> DialogueDispatcherHandlerCx<Upd, Nd, Ne> {
|
||||
DialogueDispatcherHandlerCx {
|
||||
bot: self.bot,
|
||||
update: self.update,
|
||||
dialogue: new_dialogue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Upd, D, E> GetChatId for DialogueDispatcherHandlerCx<Upd, D, E>
|
||||
where
|
||||
Upd: GetChatId,
|
||||
{
|
||||
fn chat_id(&self) -> i64 {
|
||||
self.update.chat_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, E> DialogueDispatcherHandlerCx<Message, D, E> {
|
||||
pub fn answer<T>(&self, text: T) -> SendMessage
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
self.bot.send_message(self.chat_id(), text)
|
||||
}
|
||||
|
||||
pub fn reply_to<T>(&self, text: T) -> SendMessage
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
self.bot
|
||||
.send_message(self.chat_id(), text)
|
||||
.reply_to_message_id(self.update.id)
|
||||
}
|
||||
|
||||
pub fn answer_photo(&self, photo: InputFile) -> SendPhoto {
|
||||
self.bot.send_photo(self.update.chat.id, photo)
|
||||
}
|
||||
|
||||
pub fn answer_audio(&self, audio: InputFile) -> SendAudio {
|
||||
self.bot.send_audio(self.update.chat.id, audio)
|
||||
}
|
||||
|
||||
pub fn answer_animation(&self, animation: InputFile) -> SendAnimation {
|
||||
self.bot.send_animation(self.update.chat.id, animation)
|
||||
}
|
||||
|
||||
pub fn answer_document(&self, document: InputFile) -> SendDocument {
|
||||
self.bot.send_document(self.update.chat.id, document)
|
||||
}
|
||||
|
||||
pub fn answer_video(&self, video: InputFile) -> SendVideo {
|
||||
self.bot.send_video(self.update.chat.id, video)
|
||||
}
|
||||
|
||||
pub fn answer_voice(&self, voice: InputFile) -> SendVoice {
|
||||
self.bot.send_voice(self.update.chat.id, voice)
|
||||
}
|
||||
|
||||
pub fn answer_media_group<T>(&self, media_group: T) -> SendMediaGroup
|
||||
where
|
||||
T: Into<Vec<InputMedia>>,
|
||||
{
|
||||
self.bot.send_media_group(self.update.chat.id, media_group)
|
||||
}
|
||||
|
||||
pub fn answer_location(
|
||||
&self,
|
||||
latitude: f32,
|
||||
longitude: f32,
|
||||
) -> SendLocation {
|
||||
self.bot.send_location(self.update.chat.id, latitude, longitude)
|
||||
}
|
||||
|
||||
pub fn answer_venue<T, U>(
|
||||
&self,
|
||||
latitude: f32,
|
||||
longitude: f32,
|
||||
title: T,
|
||||
address: U,
|
||||
) -> SendVenue
|
||||
where
|
||||
T: Into<String>,
|
||||
U: Into<String>,
|
||||
{
|
||||
self.bot.send_venue(
|
||||
self.update.chat.id,
|
||||
latitude,
|
||||
longitude,
|
||||
title,
|
||||
address,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn answer_video_note(&self, video_note: InputFile) -> SendVideoNote {
|
||||
self.bot.send_video_note(self.update.chat.id, video_note)
|
||||
}
|
||||
|
||||
pub fn answer_contact<T, U>(
|
||||
&self,
|
||||
phone_number: T,
|
||||
first_name: U,
|
||||
) -> SendContact
|
||||
where
|
||||
T: Into<String>,
|
||||
U: Into<String>,
|
||||
{
|
||||
self.bot.send_contact(self.chat_id(), phone_number, first_name)
|
||||
}
|
||||
|
||||
pub fn answer_sticker<T>(&self, sticker: InputFile) -> SendSticker {
|
||||
self.bot.send_sticker(self.update.chat.id, sticker)
|
||||
}
|
||||
|
||||
pub fn forward_to<T>(&self, chat_id: T) -> ForwardMessage
|
||||
where
|
||||
T: Into<ChatId>,
|
||||
{
|
||||
self.bot.forward_message(chat_id, self.update.chat.id, self.update.id)
|
||||
}
|
||||
|
||||
pub fn edit_message_text<T>(&self, text: T) -> EditMessageText
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
self.bot.edit_message_text(
|
||||
ChatOrInlineMessage::Chat {
|
||||
chat_id: self.update.chat.id.into(),
|
||||
message_id: self.update.id,
|
||||
},
|
||||
text,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn edit_message_caption(&self) -> EditMessageCaption {
|
||||
self.bot.edit_message_caption(ChatOrInlineMessage::Chat {
|
||||
chat_id: self.update.chat.id.into(),
|
||||
message_id: self.update.id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_message(&self) -> DeleteMessage {
|
||||
self.bot.delete_message(self.update.chat.id, self.update.id)
|
||||
}
|
||||
|
||||
pub fn pin_message(&self) -> PinChatMessage {
|
||||
self.bot.pin_chat_message(self.update.chat.id, self.update.id)
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
use crate::dispatching::dialogue::TransitionOut;
|
||||
use frunk::coproduct::CoprodInjector;
|
||||
|
||||
/// Continue or terminate a dialogue.
|
||||
///
|
||||
/// See [the module-level documentation for the design
|
||||
|
@ -8,18 +11,29 @@ pub enum DialogueStage<D> {
|
|||
Exit,
|
||||
}
|
||||
|
||||
/// A shortcut for `Ok(DialogueStage::Next(dialogue))`.
|
||||
///
|
||||
/// See [the module-level documentation for the design
|
||||
/// overview](crate::dispatching::dialogue).
|
||||
pub fn next<E, D>(dialogue: D) -> Result<DialogueStage<D>, E> {
|
||||
Ok(DialogueStage::Next(dialogue))
|
||||
/// A dialogue wrapper to bypass orphan rules.
|
||||
pub trait DialogueWrapper<D> {
|
||||
fn new(dialogue: D) -> Self;
|
||||
}
|
||||
|
||||
/// A shortcut for `Ok(DialogueStage::Exit)`.
|
||||
/// Returns a new dialogue state.
|
||||
///
|
||||
/// See [the module-level documentation for the design
|
||||
/// overview](crate::dispatching::dialogue).
|
||||
pub fn exit<E, D>() -> Result<DialogueStage<D>, E> {
|
||||
pub fn next<Dialogue, State, Index, DWrapper>(
|
||||
new_state: State,
|
||||
) -> TransitionOut<DWrapper>
|
||||
where
|
||||
Dialogue: CoprodInjector<State, Index>,
|
||||
DWrapper: DialogueWrapper<Dialogue>,
|
||||
{
|
||||
Ok(DialogueStage::Next(DWrapper::new(Dialogue::inject(new_state))))
|
||||
}
|
||||
|
||||
/// Exits a dialogue.
|
||||
///
|
||||
/// See [the module-level documentation for the design
|
||||
/// overview](crate::dispatching::dialogue).
|
||||
pub fn exit<DWrapper>() -> TransitionOut<DWrapper> {
|
||||
Ok(DialogueStage::Exit)
|
||||
}
|
||||
|
|
49
src/dispatching/dialogue/dialogue_with_cx.rs
Normal file
49
src/dispatching/dialogue/dialogue_with_cx.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use crate::dispatching::{dialogue::GetChatId, UpdateWithCx};
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// A context of a [`DialogueDispatcher`]'s message handler.
|
||||
///
|
||||
/// See [the module-level documentation for the design
|
||||
/// overview](crate::dispatching::dialogue).
|
||||
///
|
||||
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
|
||||
#[derive(Debug)]
|
||||
pub struct DialogueWithCx<Upd, D, E> {
|
||||
pub cx: UpdateWithCx<Upd>,
|
||||
pub dialogue: Result<D, E>,
|
||||
}
|
||||
|
||||
impl<Upd, D, E> DialogueWithCx<Upd, D, E> {
|
||||
/// Returns the inner `UpdateWithCx<Upd>` and an unwrapped dialogue.
|
||||
pub fn unpack(self) -> (UpdateWithCx<Upd>, D)
|
||||
where
|
||||
E: Debug,
|
||||
{
|
||||
(self.cx, self.dialogue.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Upd, D, E> DialogueWithCx<Upd, D, E> {
|
||||
/// Creates a new instance with the provided fields.
|
||||
pub fn new(cx: UpdateWithCx<Upd>, dialogue: D) -> Self {
|
||||
Self { cx, dialogue: Ok(dialogue) }
|
||||
}
|
||||
|
||||
/// Creates a new instance by substituting a dialogue and preserving
|
||||
/// `self.bot` and `self.update`.
|
||||
pub fn with_new_dialogue<Nd, Ne>(
|
||||
self,
|
||||
new_dialogue: Result<Nd, Ne>,
|
||||
) -> DialogueWithCx<Upd, Nd, Ne> {
|
||||
DialogueWithCx { cx: self.cx, dialogue: new_dialogue }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Upd, D, E> GetChatId for DialogueWithCx<Upd, D, E>
|
||||
where
|
||||
Upd: GetChatId,
|
||||
{
|
||||
fn chat_id(&self) -> i64 {
|
||||
self.cx.update.chat_id()
|
||||
}
|
||||
}
|
|
@ -36,22 +36,195 @@
|
|||
//! [`Dispatcher::messages_handler`]:
|
||||
//! crate::dispatching::Dispatcher::messages_handler
|
||||
//! [`UpdateKind::Message(message)`]: crate::types::UpdateKind::Message
|
||||
//! [`DialogueDispatcherHandlerCx<YourUpdate, D>`]:
|
||||
//! crate::dispatching::dialogue::DialogueDispatcherHandlerCx
|
||||
//! [`DialogueWithCx<YourUpdate, D>`]:
|
||||
//! crate::dispatching::dialogue::DialogueWithCx
|
||||
//! [examples/dialogue_bot]: https://github.com/teloxide/teloxide/tree/master/examples/dialogue_bot
|
||||
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
mod dialogue_dispatcher;
|
||||
mod dialogue_dispatcher_handler;
|
||||
mod dialogue_dispatcher_handler_cx;
|
||||
mod dialogue_stage;
|
||||
mod dialogue_with_cx;
|
||||
mod get_chat_id;
|
||||
mod storage;
|
||||
|
||||
use crate::{requests::ResponseResult, types::Message};
|
||||
pub use dialogue_dispatcher::DialogueDispatcher;
|
||||
pub use dialogue_dispatcher_handler::DialogueDispatcherHandler;
|
||||
pub use dialogue_dispatcher_handler_cx::DialogueDispatcherHandlerCx;
|
||||
pub use dialogue_stage::{exit, next, DialogueStage};
|
||||
pub use dialogue_stage::{exit, next, DialogueStage, DialogueWrapper};
|
||||
pub use dialogue_with_cx::DialogueWithCx;
|
||||
pub use get_chat_id::GetChatId;
|
||||
pub use storage::{InMemStorage, Storage};
|
||||
|
||||
/// Dispatches a dialogue state into transition functions.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use teloxide::prelude::*;
|
||||
///
|
||||
/// pub struct StartState;
|
||||
/// pub struct ReceiveWordState;
|
||||
/// pub struct ReceiveNumberState;
|
||||
/// pub struct ExitState;
|
||||
///
|
||||
/// pub type Dialogue = Coprod!(
|
||||
/// StartState,
|
||||
/// ReceiveWordState,
|
||||
/// ReceiveNumberState,
|
||||
/// );
|
||||
///
|
||||
/// wrap_dialogue!(
|
||||
/// Wrapper(Dialogue),
|
||||
/// default Self(Dialogue::inject(StartState)),
|
||||
/// );
|
||||
///
|
||||
/// pub type In<State> = TransitionIn<State, std::convert::Infallible>;
|
||||
/// pub type Out = TransitionOut<Wrapper>;
|
||||
///
|
||||
/// pub async fn start(cx: In<StartState>) -> Out { todo!() }
|
||||
/// pub async fn receive_word(cx: In<ReceiveWordState>) -> Out { todo!() }
|
||||
/// pub async fn receive_number(cx: In<ReceiveNumberState>) -> Out { todo!() }
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// let cx: In<Dialogue> = todo!();
|
||||
/// let (cx, dialogue) = cx.unpack();
|
||||
///
|
||||
/// // StartState -> start
|
||||
/// // ReceiveWordState -> receive_word
|
||||
/// // ReceiveNumberState -> receive_number
|
||||
/// let stage = dispatch!(
|
||||
/// [cx, dialogue] ->
|
||||
/// [start, receive_word, receive_number]
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! dispatch {
|
||||
([$cx:ident, $dialogue:ident] -> [$transition:ident, $($transitions:ident),+]) => {
|
||||
match $dialogue {
|
||||
Coproduct::Inl(state) => {
|
||||
$transition(teloxide::dispatching::dialogue::DialogueWithCx::new($cx, state)).await
|
||||
}
|
||||
Coproduct::Inr(another) => { dispatch!([$cx, another] -> [$($transitions),+]) }
|
||||
}
|
||||
};
|
||||
|
||||
([$cx:ident, $dialogue:ident] -> [$transition:ident]) => {
|
||||
match $dialogue {
|
||||
Coproduct::Inl(state) => {
|
||||
$transition(teloxide::dispatching::dialogue::DialogueWithCx::new($cx, state)).await
|
||||
}
|
||||
Coproduct::Inr(_absurd) => unreachable!(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates a dialogue wrapper and implements `Default` for it.
|
||||
///
|
||||
/// The reason is to bypass orphan rules to be able to pass a user-defined
|
||||
/// dialogue into [`DialogueDispatcher`]. Since a dialogue is
|
||||
/// [`frunk::Coproduct`], we cannot directly satisfy the `D: Default`
|
||||
/// constraint.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use teloxide::prelude::*;
|
||||
///
|
||||
/// struct StartState;
|
||||
/// struct ReceiveWordState;
|
||||
/// struct ReceiveNumberState;
|
||||
/// struct ExitState;
|
||||
///
|
||||
/// type Dialogue = Coprod!(
|
||||
/// StartState,
|
||||
/// ReceiveWordState,
|
||||
/// ReceiveNumberState,
|
||||
/// );
|
||||
///
|
||||
/// wrap_dialogue!(
|
||||
/// Wrapper(Dialogue),
|
||||
/// default Self(Dialogue::inject(StartState)),
|
||||
/// );
|
||||
///
|
||||
/// let start_state = Wrapper::default();
|
||||
/// ```
|
||||
///
|
||||
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
|
||||
/// [`frunk::Coproduct`]: https://docs.rs/frunk/0.3.1/frunk/coproduct/enum.Coproduct.html
|
||||
#[macro_export]
|
||||
macro_rules! wrap_dialogue {
|
||||
($name:ident($dialogue:ident), default $default_block:expr, ) => {
|
||||
pub struct $name(pub $dialogue);
|
||||
|
||||
impl teloxide::dispatching::dialogue::DialogueWrapper<$dialogue>
|
||||
for $name
|
||||
{
|
||||
fn new(d: $dialogue) -> Wrapper {
|
||||
$name(d)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for $name {
|
||||
fn default() -> $name {
|
||||
$default_block
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates `.up(field)` methods for dialogue states.
|
||||
///
|
||||
/// Given inductively defined states, this macro generates `.up(field)` methods
|
||||
/// from `Sn` to `Sn+1`.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use teloxide::prelude::*;
|
||||
///
|
||||
/// struct StartState;
|
||||
///
|
||||
/// struct ReceiveWordState {
|
||||
/// rest: StartState,
|
||||
/// }
|
||||
///
|
||||
/// struct ReceiveNumberState {
|
||||
/// rest: ReceiveWordState,
|
||||
/// word: String,
|
||||
/// }
|
||||
///
|
||||
/// struct ExitState {
|
||||
/// rest: ReceiveNumberState,
|
||||
/// number: i32,
|
||||
/// }
|
||||
///
|
||||
/// up!(
|
||||
/// StartState -> ReceiveWordState,
|
||||
/// ReceiveWordState + [word: String] -> ReceiveNumberState,
|
||||
/// ReceiveNumberState + [number: i32] -> ExitState,
|
||||
/// );
|
||||
///
|
||||
/// let start_state = StartState;
|
||||
/// let receive_word_state = start_state.up();
|
||||
/// let receive_number_state = receive_word_state.up("Hello".to_owned());
|
||||
/// let exit_state = receive_number_state.up(123);
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! up {
|
||||
( $( $from:ident $(+ [$field_name:ident : $field_type:ty])? -> $to:ident ),+, ) => {
|
||||
$(
|
||||
impl $from {
|
||||
pub fn up(self, $( $field_name: $field_type )?) -> $to {
|
||||
$to { rest: self, $($field_name)? }
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
/// A type passed into a FSM transition function.
|
||||
pub type TransitionIn<State, E> = DialogueWithCx<Message, State, E>;
|
||||
|
||||
// A type returned from a FSM transition function.
|
||||
pub type TransitionOut<DWrapper> = ResponseResult<DialogueStage<DWrapper>>;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
dispatching::{
|
||||
update_listeners, update_listeners::UpdateListener, DispatcherHandler,
|
||||
DispatcherHandlerCx,
|
||||
UpdateWithCx,
|
||||
},
|
||||
error_handlers::{ErrorHandler, LoggingErrorHandler},
|
||||
types::{
|
||||
|
@ -14,7 +14,7 @@ use futures::StreamExt;
|
|||
use std::{fmt::Debug, sync::Arc};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
type Tx<Upd> = Option<mpsc::UnboundedSender<DispatcherHandlerCx<Upd>>>;
|
||||
type Tx<Upd> = Option<mpsc::UnboundedSender<UpdateWithCx<Upd>>>;
|
||||
|
||||
#[macro_use]
|
||||
mod macros {
|
||||
|
@ -36,7 +36,7 @@ fn send<'a, Upd>(
|
|||
{
|
||||
if let Some(tx) = tx {
|
||||
if let Err(error) =
|
||||
tx.send(DispatcherHandlerCx { bot: Arc::clone(&bot), update })
|
||||
tx.send(UpdateWithCx { bot: Arc::clone(&bot), update })
|
||||
{
|
||||
log::error!(
|
||||
"The RX part of the {} channel is closed, but an update is \
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::future::Future;
|
||||
|
||||
use crate::dispatching::{DispatcherHandlerCx, DispatcherHandlerRx};
|
||||
use crate::dispatching::{DispatcherHandlerRx, UpdateWithCx};
|
||||
use futures::future::BoxFuture;
|
||||
|
||||
/// An asynchronous handler of a stream of updates used in [`Dispatcher`].
|
||||
|
@ -16,7 +16,7 @@ pub trait DispatcherHandler<Upd> {
|
|||
updates: DispatcherHandlerRx<Upd>,
|
||||
) -> BoxFuture<'static, ()>
|
||||
where
|
||||
DispatcherHandlerCx<Upd>: Send + 'static;
|
||||
UpdateWithCx<Upd>: Send + 'static;
|
||||
}
|
||||
|
||||
impl<Upd, F, Fut> DispatcherHandler<Upd> for F
|
||||
|
@ -26,7 +26,7 @@ where
|
|||
{
|
||||
fn handle(self, updates: DispatcherHandlerRx<Upd>) -> BoxFuture<'static, ()>
|
||||
where
|
||||
DispatcherHandlerCx<Upd>: Send + 'static,
|
||||
UpdateWithCx<Upd>: Send + 'static,
|
||||
{
|
||||
Box::pin(async move { self(updates).await })
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
prelude::DispatcherHandlerCx, types::Message, utils::command::BotCommand,
|
||||
prelude::UpdateWithCx, types::Message, utils::command::BotCommand,
|
||||
};
|
||||
use futures::{stream::BoxStream, Stream, StreamExt};
|
||||
|
||||
|
@ -10,18 +10,18 @@ pub trait DispatcherHandlerRxExt {
|
|||
/// Extracts only text messages from this stream of arbitrary messages.
|
||||
fn text_messages(
|
||||
self,
|
||||
) -> BoxStream<'static, (DispatcherHandlerCx<Message>, String)>
|
||||
) -> BoxStream<'static, (UpdateWithCx<Message>, String)>
|
||||
where
|
||||
Self: Stream<Item = DispatcherHandlerCx<Message>>;
|
||||
Self: Stream<Item = UpdateWithCx<Message>>;
|
||||
|
||||
/// Extracts only commands with their arguments from this stream of
|
||||
/// arbitrary messages.
|
||||
fn commands<C, N>(
|
||||
self,
|
||||
bot_name: N,
|
||||
) -> BoxStream<'static, (DispatcherHandlerCx<Message>, C, Vec<String>)>
|
||||
) -> BoxStream<'static, (UpdateWithCx<Message>, C, Vec<String>)>
|
||||
where
|
||||
Self: Stream<Item = DispatcherHandlerCx<Message>>,
|
||||
Self: Stream<Item = UpdateWithCx<Message>>,
|
||||
C: BotCommand,
|
||||
N: Into<String> + Send;
|
||||
}
|
||||
|
@ -32,9 +32,9 @@ where
|
|||
{
|
||||
fn text_messages(
|
||||
self,
|
||||
) -> BoxStream<'static, (DispatcherHandlerCx<Message>, String)>
|
||||
) -> BoxStream<'static, (UpdateWithCx<Message>, String)>
|
||||
where
|
||||
Self: Stream<Item = DispatcherHandlerCx<Message>>,
|
||||
Self: Stream<Item = UpdateWithCx<Message>>,
|
||||
{
|
||||
Box::pin(self.filter_map(|cx| async move {
|
||||
cx.update.text_owned().map(|text| (cx, text))
|
||||
|
@ -44,9 +44,9 @@ where
|
|||
fn commands<C, N>(
|
||||
self,
|
||||
bot_name: N,
|
||||
) -> BoxStream<'static, (DispatcherHandlerCx<Message>, C, Vec<String>)>
|
||||
) -> BoxStream<'static, (UpdateWithCx<Message>, C, Vec<String>)>
|
||||
where
|
||||
Self: Stream<Item = DispatcherHandlerCx<Message>>,
|
||||
Self: Stream<Item = UpdateWithCx<Message>>,
|
||||
C: BotCommand,
|
||||
N: Into<String> + Send,
|
||||
{
|
||||
|
|
|
@ -80,17 +80,17 @@
|
|||
pub mod dialogue;
|
||||
mod dispatcher;
|
||||
mod dispatcher_handler;
|
||||
mod dispatcher_handler_cx;
|
||||
mod dispatcher_handler_rx_ext;
|
||||
pub mod update_listeners;
|
||||
mod update_with_cx;
|
||||
|
||||
pub use dispatcher::Dispatcher;
|
||||
pub use dispatcher_handler::DispatcherHandler;
|
||||
pub use dispatcher_handler_cx::DispatcherHandlerCx;
|
||||
pub use dispatcher_handler_rx_ext::DispatcherHandlerRxExt;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
pub use update_with_cx::UpdateWithCx;
|
||||
|
||||
/// A type of a stream, consumed by [`Dispatcher`]'s handlers.
|
||||
///
|
||||
/// [`Dispatcher`]: crate::dispatching::Dispatcher
|
||||
pub type DispatcherHandlerRx<Upd> = UnboundedReceiver<DispatcherHandlerCx<Upd>>;
|
||||
pub type DispatcherHandlerRx<Upd> = UnboundedReceiver<UpdateWithCx<Upd>>;
|
||||
|
|
|
@ -2,9 +2,9 @@ use crate::{
|
|||
dispatching::dialogue::GetChatId,
|
||||
requests::{
|
||||
DeleteMessage, EditMessageCaption, EditMessageText, ForwardMessage,
|
||||
PinChatMessage, SendAnimation, SendAudio, SendContact, SendDocument,
|
||||
SendLocation, SendMediaGroup, SendMessage, SendPhoto, SendSticker,
|
||||
SendVenue, SendVideo, SendVideoNote, SendVoice,
|
||||
PinChatMessage, Request, ResponseResult, SendAnimation, SendAudio,
|
||||
SendContact, SendDocument, SendLocation, SendMediaGroup, SendMessage,
|
||||
SendPhoto, SendSticker, SendVenue, SendVideo, SendVideoNote, SendVoice,
|
||||
},
|
||||
types::{ChatId, ChatOrInlineMessage, InputFile, InputMedia, Message},
|
||||
Bot,
|
||||
|
@ -18,12 +18,12 @@ use std::sync::Arc;
|
|||
///
|
||||
/// [`Dispatcher`]: crate::dispatching::Dispatcher
|
||||
#[derive(Debug)]
|
||||
pub struct DispatcherHandlerCx<Upd> {
|
||||
pub struct UpdateWithCx<Upd> {
|
||||
pub bot: Arc<Bot>,
|
||||
pub update: Upd,
|
||||
}
|
||||
|
||||
impl<Upd> GetChatId for DispatcherHandlerCx<Upd>
|
||||
impl<Upd> GetChatId for UpdateWithCx<Upd>
|
||||
where
|
||||
Upd: GetChatId,
|
||||
{
|
||||
|
@ -32,7 +32,14 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl DispatcherHandlerCx<Message> {
|
||||
impl UpdateWithCx<Message> {
|
||||
pub async fn answer_str<T>(&self, text: T) -> ResponseResult<Message>
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
self.answer(text).send().await
|
||||
}
|
||||
|
||||
pub fn answer<T>(&self, text: T) -> SendMessage
|
||||
where
|
||||
T: Into<String>,
|
|
@ -1,20 +1,21 @@
|
|||
//! Commonly used items.
|
||||
|
||||
pub use crate::{
|
||||
dispatch,
|
||||
dispatching::{
|
||||
dialogue::{
|
||||
exit, next, DialogueDispatcher, DialogueDispatcherHandlerCx,
|
||||
DialogueStage, GetChatId,
|
||||
exit, next, DialogueDispatcher, DialogueStage, DialogueWithCx,
|
||||
DialogueWrapper, GetChatId, TransitionIn, TransitionOut,
|
||||
},
|
||||
Dispatcher, DispatcherHandlerCx, DispatcherHandlerRx,
|
||||
DispatcherHandlerRxExt,
|
||||
Dispatcher, DispatcherHandlerRx, DispatcherHandlerRxExt, UpdateWithCx,
|
||||
},
|
||||
error_handlers::{LoggingErrorHandler, OnError},
|
||||
requests::{Request, ResponseResult},
|
||||
types::{Message, Update},
|
||||
Bot, RequestError,
|
||||
up, wrap_dialogue, Bot, RequestError,
|
||||
};
|
||||
|
||||
pub use frunk::{Coprod, Coproduct};
|
||||
pub use tokio::sync::mpsc::UnboundedReceiver;
|
||||
|
||||
pub use futures::StreamExt;
|
||||
|
|
Loading…
Add table
Reference in a new issue