mirror of
https://github.com/teloxide/teloxide.git
synced 2025-01-03 09:49:07 +01:00
Add an auxiliary parameter to (sub)transitions
This commit is contained in:
parent
1da19f3d30
commit
54a6bf440b
14 changed files with 246 additions and 192 deletions
138
README.md
138
README.md
|
@ -191,7 +191,7 @@ A dialogue is described by an enumeration, where each variant is one of possible
|
|||
|
||||
Below is a bot, which asks you three questions and then sends the answers back to you. Here's possible states for a dialogue:
|
||||
|
||||
([dialogue_bot/src/states.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states.rs))
|
||||
([dialogue_bot/src/states/mod.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states/mod.rs))
|
||||
```rust
|
||||
// Imports are omitted...
|
||||
|
||||
|
@ -203,60 +203,43 @@ pub enum Dialogue {
|
|||
ReceiveAge(ReceiveAgeState),
|
||||
ReceiveLocation(ReceiveLocationState),
|
||||
}
|
||||
```
|
||||
|
||||
([dialogue_bot/src/states/start.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states/start.rs))
|
||||
```rust
|
||||
// Imports are omitted...
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct StartState;
|
||||
|
||||
#[derive(Generic)]
|
||||
pub struct ReceiveFullNameState;
|
||||
#[teloxide(transition)]
|
||||
async fn start(
|
||||
_state: StartState,
|
||||
cx: TransitionIn,
|
||||
_ans: String,
|
||||
) -> TransitionOut<Dialogue> {
|
||||
cx.answer_str("Let's start! What's your full name?").await?;
|
||||
next(ReceiveFullNameState)
|
||||
}
|
||||
```
|
||||
|
||||
([dialogue_bot/src/states/receive_age.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states/receive_age.rs))
|
||||
```rust
|
||||
// Imports are omitted...
|
||||
|
||||
#[derive(Generic)]
|
||||
pub struct ReceiveAgeState {
|
||||
pub full_name: String,
|
||||
}
|
||||
|
||||
#[derive(Generic)]
|
||||
pub struct ReceiveLocationState {
|
||||
pub full_name: String,
|
||||
pub age: u8,
|
||||
}
|
||||
```
|
||||
|
||||
... and here are the transition functions, which turn one state into another:
|
||||
|
||||
([dialogue_bot/src/transitions.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/transitions.rs))
|
||||
```rust
|
||||
// Imports are omitted...
|
||||
|
||||
pub type Out = TransitionOut<Dialogue>;
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn start(_state: StartState, cx: TransitionIn) -> Out {
|
||||
cx.answer_str("Let's start! What's your full name?").await?;
|
||||
next(ReceiveFullNameState)
|
||||
}
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn receive_full_name(
|
||||
state: ReceiveFullNameState,
|
||||
async fn receive_age_state(
|
||||
state: ReceiveAgeState,
|
||||
cx: TransitionIn,
|
||||
) -> Out {
|
||||
match cx.update.text_owned() {
|
||||
Some(ans) => {
|
||||
cx.answer_str("How old are you?").await?;
|
||||
next(ReceiveAgeState::up(state, ans))
|
||||
}
|
||||
_ => {
|
||||
cx.answer_str("Send me a text message.").await?;
|
||||
next(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn receive_age_state(state: ReceiveAgeState, cx: TransitionIn) -> Out {
|
||||
match cx.update.text().map(str::parse::<u8>) {
|
||||
Some(Ok(ans)) => {
|
||||
ans: String,
|
||||
) -> TransitionOut<Dialogue> {
|
||||
match ans.parse::<u8>() {
|
||||
Ok(ans) => {
|
||||
cx.answer_str("What's your location?").await?;
|
||||
next(ReceiveLocationState::up(state, ans))
|
||||
}
|
||||
|
@ -266,26 +249,48 @@ async fn receive_age_state(state: ReceiveAgeState, cx: TransitionIn) -> Out {
|
|||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
([dialogue_bot/src/states/receive_full_name.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states/receive_full_name.rs))
|
||||
```rust
|
||||
// Imports are omitted...
|
||||
|
||||
#[derive(Generic)]
|
||||
pub struct ReceiveFullNameState;
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn receive_full_name(
|
||||
state: ReceiveFullNameState,
|
||||
cx: TransitionIn,
|
||||
ans: String,
|
||||
) -> TransitionOut<Dialogue> {
|
||||
cx.answer_str("How old are you?").await?;
|
||||
next(ReceiveAgeState::up(state, ans))
|
||||
}
|
||||
```
|
||||
|
||||
([dialogue_bot/src/states/receive_location.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states/receive_location.rs))
|
||||
```rust
|
||||
// Imports are omitted...
|
||||
|
||||
#[derive(Generic)]
|
||||
pub struct ReceiveLocationState {
|
||||
pub full_name: String,
|
||||
pub age: u8,
|
||||
}
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn receive_location(
|
||||
state: ReceiveLocationState,
|
||||
cx: TransitionIn,
|
||||
) -> Out {
|
||||
match cx.update.text() {
|
||||
Some(ans) => {
|
||||
cx.answer_str(format!(
|
||||
"Full name: {}\nAge: {}\nLocation: {}",
|
||||
state.full_name, state.age, ans
|
||||
))
|
||||
.await?;
|
||||
exit()
|
||||
}
|
||||
_ => {
|
||||
cx.answer_str("Send me a text message.").await?;
|
||||
next(state)
|
||||
}
|
||||
}
|
||||
ans: String,
|
||||
) -> TransitionOut<Dialogue> {
|
||||
cx.answer_str(format!(
|
||||
"Full name: {}\nAge: {}\nLocation: {}",
|
||||
state.full_name, state.age, ans
|
||||
))
|
||||
.await?;
|
||||
exit()
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -309,12 +314,27 @@ async fn main() {
|
|||
|DialogueWithCx { cx, dialogue }: In| async move {
|
||||
// No panic because of std::convert::Infallible.
|
||||
let dialogue = dialogue.unwrap();
|
||||
dialogue.react(cx).await.expect("Something wrong with the bot!")
|
||||
handle_message(cx, dialogue)
|
||||
.await
|
||||
.expect("Something wrong with the bot!")
|
||||
},
|
||||
))
|
||||
.dispatch()
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn handle_message(
|
||||
cx: UpdateWithCx<Message>,
|
||||
dialogue: Dialogue,
|
||||
) -> TransitionOut<Dialogue> {
|
||||
match cx.update.text_owned() {
|
||||
None => {
|
||||
cx.answer_str("Send me a text message.").await?;
|
||||
next(dialogue)
|
||||
}
|
||||
Some(ans) => dialogue.react(cx, ans).await,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<div align="center">
|
||||
|
|
15
examples/dialogue_bot/src/dialogue/mod.rs
Normal file
15
examples/dialogue_bot/src/dialogue/mod.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
mod states;
|
||||
|
||||
use crate::dialogue::states::{
|
||||
ReceiveAgeState, ReceiveFullNameState, ReceiveLocationState, StartState,
|
||||
};
|
||||
use teloxide_macros::Transition;
|
||||
|
||||
#[derive(Transition, SmartDefault, From)]
|
||||
pub enum Dialogue {
|
||||
#[default]
|
||||
Start(StartState),
|
||||
ReceiveFullName(ReceiveFullNameState),
|
||||
ReceiveAge(ReceiveAgeState),
|
||||
ReceiveLocation(ReceiveLocationState),
|
||||
}
|
9
examples/dialogue_bot/src/dialogue/states/mod.rs
Normal file
9
examples/dialogue_bot/src/dialogue/states/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
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;
|
27
examples/dialogue_bot/src/dialogue/states/receive_age.rs
Normal file
27
examples/dialogue_bot/src/dialogue/states/receive_age.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use crate::dialogue::{
|
||||
states::receive_location::ReceiveLocationState, Dialogue,
|
||||
};
|
||||
use teloxide::prelude::*;
|
||||
|
||||
#[derive(Generic)]
|
||||
pub struct ReceiveAgeState {
|
||||
pub full_name: String,
|
||||
}
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn receive_age_state(
|
||||
state: ReceiveAgeState,
|
||||
cx: TransitionIn,
|
||||
ans: String,
|
||||
) -> TransitionOut<Dialogue> {
|
||||
match ans.parse::<u8>() {
|
||||
Ok(ans) => {
|
||||
cx.answer_str("What's your location?").await?;
|
||||
next(ReceiveLocationState::up(state, ans))
|
||||
}
|
||||
_ => {
|
||||
cx.answer_str("Send me a number.").await?;
|
||||
next(state)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
use crate::dialogue::{states::receive_age::ReceiveAgeState, Dialogue};
|
||||
use teloxide::prelude::*;
|
||||
|
||||
#[derive(Generic)]
|
||||
pub struct ReceiveFullNameState;
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn receive_full_name(
|
||||
state: ReceiveFullNameState,
|
||||
cx: TransitionIn,
|
||||
ans: String,
|
||||
) -> TransitionOut<Dialogue> {
|
||||
cx.answer_str("How old are you?").await?;
|
||||
next(ReceiveAgeState::up(state, ans))
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
use crate::dialogue::Dialogue;
|
||||
use teloxide::prelude::*;
|
||||
|
||||
#[derive(Generic)]
|
||||
pub struct ReceiveLocationState {
|
||||
pub full_name: String,
|
||||
pub age: u8,
|
||||
}
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn receive_location(
|
||||
state: ReceiveLocationState,
|
||||
cx: TransitionIn,
|
||||
ans: String,
|
||||
) -> TransitionOut<Dialogue> {
|
||||
cx.answer_str(format!(
|
||||
"Full name: {}\nAge: {}\nLocation: {}",
|
||||
state.full_name, state.age, ans
|
||||
))
|
||||
.await?;
|
||||
exit()
|
||||
}
|
15
examples/dialogue_bot/src/dialogue/states/start.rs
Normal file
15
examples/dialogue_bot/src/dialogue/states/start.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use crate::dialogue::{states::ReceiveFullNameState, Dialogue};
|
||||
use teloxide::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct StartState;
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn start(
|
||||
_state: StartState,
|
||||
cx: TransitionIn,
|
||||
_ans: String,
|
||||
) -> TransitionOut<Dialogue> {
|
||||
cx.answer_str("Let's start! What's your full name?").await?;
|
||||
next(ReceiveFullNameState)
|
||||
}
|
|
@ -27,11 +27,9 @@ extern crate frunk_core;
|
|||
#[macro_use]
|
||||
extern crate teloxide_macros;
|
||||
|
||||
mod states;
|
||||
mod transitions;
|
||||
|
||||
use states::*;
|
||||
mod dialogue;
|
||||
|
||||
use crate::dialogue::Dialogue;
|
||||
use std::convert::Infallible;
|
||||
use teloxide::prelude::*;
|
||||
|
||||
|
@ -53,9 +51,24 @@ async fn run() {
|
|||
|DialogueWithCx { cx, dialogue }: In| async move {
|
||||
// No panic because of std::convert::Infallible.
|
||||
let dialogue = dialogue.unwrap();
|
||||
dialogue.react(cx).await.expect("Something wrong with the bot!")
|
||||
handle_message(cx, dialogue)
|
||||
.await
|
||||
.expect("Something wrong with the bot!")
|
||||
},
|
||||
))
|
||||
.dispatch()
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn handle_message(
|
||||
cx: UpdateWithCx<Message>,
|
||||
dialogue: Dialogue,
|
||||
) -> TransitionOut<Dialogue> {
|
||||
match cx.update.text_owned() {
|
||||
None => {
|
||||
cx.answer_str("Send me a text message.").await?;
|
||||
next(dialogue)
|
||||
}
|
||||
Some(ans) => dialogue.react(cx, ans).await,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
use teloxide::prelude::*;
|
||||
use teloxide_macros::Transition;
|
||||
|
||||
#[derive(Transition, SmartDefault, From)]
|
||||
pub enum Dialogue {
|
||||
#[default]
|
||||
Start(StartState),
|
||||
ReceiveFullName(ReceiveFullNameState),
|
||||
ReceiveAge(ReceiveAgeState),
|
||||
ReceiveLocation(ReceiveLocationState),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct StartState;
|
||||
|
||||
#[derive(Generic)]
|
||||
pub struct ReceiveFullNameState;
|
||||
|
||||
#[derive(Generic)]
|
||||
pub struct ReceiveAgeState {
|
||||
pub full_name: String,
|
||||
}
|
||||
|
||||
#[derive(Generic)]
|
||||
pub struct ReceiveLocationState {
|
||||
pub full_name: String,
|
||||
pub age: u8,
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
use crate::states::{
|
||||
Dialogue, ReceiveAgeState, ReceiveFullNameState, ReceiveLocationState,
|
||||
StartState,
|
||||
};
|
||||
|
||||
use teloxide::prelude::*;
|
||||
|
||||
pub type Out = TransitionOut<Dialogue>;
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn start(_state: StartState, cx: TransitionIn) -> Out {
|
||||
cx.answer_str("Let's start! What's your full name?").await?;
|
||||
next(ReceiveFullNameState)
|
||||
}
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn receive_full_name(
|
||||
state: ReceiveFullNameState,
|
||||
cx: TransitionIn,
|
||||
) -> Out {
|
||||
match cx.update.text_owned() {
|
||||
Some(ans) => {
|
||||
cx.answer_str("How old are you?").await?;
|
||||
next(ReceiveAgeState::up(state, ans))
|
||||
}
|
||||
_ => {
|
||||
cx.answer_str("Send me a text message.").await?;
|
||||
next(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn receive_age_state(state: ReceiveAgeState, cx: TransitionIn) -> Out {
|
||||
match cx.update.text().map(str::parse::<u8>) {
|
||||
Some(Ok(ans)) => {
|
||||
cx.answer_str("What's your location?").await?;
|
||||
next(ReceiveLocationState::up(state, ans))
|
||||
}
|
||||
_ => {
|
||||
cx.answer_str("Send me a number.").await?;
|
||||
next(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn receive_location(
|
||||
state: ReceiveLocationState,
|
||||
cx: TransitionIn,
|
||||
) -> Out {
|
||||
match cx.update.text() {
|
||||
Some(ans) => {
|
||||
cx.answer_str(format!(
|
||||
"Full name: {}\nAge: {}\nLocation: {}",
|
||||
state.full_name, state.age, ans
|
||||
))
|
||||
.await?;
|
||||
exit()
|
||||
}
|
||||
_ => {
|
||||
cx.answer_str("Send me a text message.").await?;
|
||||
next(state)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,11 +38,9 @@ async fn run() {
|
|||
|DialogueWithCx { cx, dialogue }: In| async move {
|
||||
// No panic because of std::convert::Infallible.
|
||||
let dialogue = dialogue.unwrap();
|
||||
|
||||
dialogue
|
||||
.react(cx)
|
||||
handle_message(cx, dialogue)
|
||||
.await
|
||||
.expect("Something is wrong with the bot!")
|
||||
.expect("Something wrong with the bot!")
|
||||
},
|
||||
// You can also choose serializer::JSON or serializer::CBOR
|
||||
// All serializers but JSON require enabling feature
|
||||
|
@ -55,3 +53,16 @@ async fn run() {
|
|||
.dispatch()
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn handle_message(
|
||||
cx: UpdateWithCx<Message>,
|
||||
dialogue: Dialogue,
|
||||
) -> TransitionOut<Dialogue> {
|
||||
match cx.update.text_owned() {
|
||||
None => {
|
||||
cx.answer_str("Send me a text message.").await?;
|
||||
next(dialogue)
|
||||
}
|
||||
Some(ans) => dialogue.react(cx, ans).await,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use teloxide::prelude::*;
|
||||
use teloxide_macros::Transition;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
|
@ -3,26 +3,13 @@ use teloxide_macros::teloxide;
|
|||
|
||||
use super::states::*;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! extract_text {
|
||||
($cx:ident) => {
|
||||
match $cx.update.text_owned() {
|
||||
Some(text) => text,
|
||||
None => {
|
||||
$cx.answer_str("Please, send me a text message").await?;
|
||||
return next(StartState);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub type Out = TransitionOut<Dialogue>;
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn start(state: StartState, cx: TransitionIn) -> Out {
|
||||
let text = extract_text!(cx);
|
||||
|
||||
if let Ok(number) = text.parse() {
|
||||
async fn start(
|
||||
state: StartState,
|
||||
cx: TransitionIn,
|
||||
ans: String,
|
||||
) -> TransitionOut<Dialogue> {
|
||||
if let Ok(number) = ans.parse() {
|
||||
cx.answer_str(format!(
|
||||
"Remembered number {}. Now use /get or /reset",
|
||||
number
|
||||
|
@ -36,14 +23,17 @@ async fn start(state: StartState, cx: TransitionIn) -> Out {
|
|||
}
|
||||
|
||||
#[teloxide(transition)]
|
||||
async fn have_number(state: HaveNumberState, cx: TransitionIn) -> Out {
|
||||
let text = extract_text!(cx);
|
||||
async fn have_number(
|
||||
state: HaveNumberState,
|
||||
cx: TransitionIn,
|
||||
ans: String,
|
||||
) -> TransitionOut<Dialogue> {
|
||||
let num = state.number;
|
||||
|
||||
if text.starts_with("/get") {
|
||||
if ans.starts_with("/get") {
|
||||
cx.answer_str(format!("Here is your number: {}", num)).await?;
|
||||
next(state)
|
||||
} else if text.starts_with("/reset") {
|
||||
} else if ans.starts_with("/reset") {
|
||||
cx.answer_str("Resetted number").await?;
|
||||
next(StartState)
|
||||
} else {
|
||||
|
|
|
@ -6,24 +6,36 @@ use crate::{
|
|||
use futures::future::BoxFuture;
|
||||
|
||||
/// Represents a transition function of a dialogue FSM.
|
||||
pub trait Transition: Sized {
|
||||
pub trait Transition<T>: Sized {
|
||||
/// Turns itself into another state, depending on the input message.
|
||||
fn react(self, cx: TransitionIn)
|
||||
-> BoxFuture<'static, TransitionOut<Self>>;
|
||||
///
|
||||
/// `aux` will be passed to each subtransition function.
|
||||
fn react(
|
||||
self,
|
||||
cx: TransitionIn,
|
||||
aux: T,
|
||||
) -> BoxFuture<'static, TransitionOut<Self>>;
|
||||
}
|
||||
|
||||
/// Like [`Transition`], but from `StateN` -> `Dialogue`.
|
||||
///
|
||||
/// [`Transition`]: crate::dispatching::dialogue::Transition
|
||||
pub trait SubTransition<Dialogue>
|
||||
pub trait SubTransition
|
||||
where
|
||||
Dialogue: Transition,
|
||||
Self::Dialogue: Transition<Self::Aux>,
|
||||
{
|
||||
type Aux;
|
||||
type Dialogue;
|
||||
|
||||
/// Turns itself into another state, depending on the input message.
|
||||
///
|
||||
/// `aux` is something that is provided by the call side, for example, a
|
||||
/// message's text.
|
||||
fn react(
|
||||
self,
|
||||
cx: TransitionIn,
|
||||
) -> BoxFuture<'static, TransitionOut<Dialogue>>;
|
||||
aux: Self::Aux,
|
||||
) -> BoxFuture<'static, TransitionOut<Self::Dialogue>>;
|
||||
}
|
||||
|
||||
/// A type returned from a FSM subtransition function.
|
||||
|
|
Loading…
Reference in a new issue