mirror of
https://github.com/teloxide/teloxide.git
synced 2024-12-22 14:35:36 +01:00
A nicer approach to manage dialogues via #[derive(Transition)] + #[teloxide(transition)]
This commit is contained in:
parent
840fe93b61
commit
bf114de249
12 changed files with 120 additions and 114 deletions
|
@ -7,8 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## [0.3.0] - ???
|
||||
### Added
|
||||
- `BotBuilder`, which allows setting a default `ParseMode`.
|
||||
- The `BotDialogue` trait.
|
||||
- Automatic `dispatch` function generation via `#[derive(BotDialogue)]` + `#[transition(transition_fn)]`.
|
||||
- The `Transition`, `SubTransition`, `SubTransitionOutputType` traits.
|
||||
- A nicer approach to manage dialogues via `#[derive(Transition)]` + `#[teloxide(transition)]`.
|
||||
|
||||
### Deprecated
|
||||
- `Bot::{from_env_with_client, new, with_client}`.
|
||||
|
|
35
README.md
35
README.md
|
@ -195,19 +195,12 @@ States and transition functions are placed into separated modules. For example,
|
|||
```rust
|
||||
// Imports are omitted...
|
||||
|
||||
#[derive(BotDialogue, SmartDefault, From)]
|
||||
#[derive(Transition, SmartDefault, From)]
|
||||
pub enum Dialogue {
|
||||
#[default]
|
||||
#[transition(start)]
|
||||
Start(StartState),
|
||||
|
||||
#[transition(receive_days_of_week)]
|
||||
ReceiveDaysOfWeek(ReceiveDaysOfWeekState),
|
||||
|
||||
#[transition(receive_10x5_answer)]
|
||||
Receive10x5Answer(Receive10x5AnswerState),
|
||||
|
||||
#[transition(receive_gandalf_alternative_name)]
|
||||
ReceiveGandalfAlternativeName(ReceiveGandalfAlternativeNameState),
|
||||
}
|
||||
|
||||
|
@ -228,16 +221,10 @@ pub struct ReceiveGandalfAlternativeNameState {
|
|||
_10x5_answer: u8,
|
||||
}
|
||||
|
||||
pub struct ExitState {
|
||||
rest: ReceiveGandalfAlternativeNameState,
|
||||
gandalf_alternative_name: String,
|
||||
}
|
||||
|
||||
up!(
|
||||
StartState -> ReceiveDaysOfWeekState,
|
||||
ReceiveDaysOfWeekState + [days_of_week: u8] -> Receive10x5AnswerState,
|
||||
Receive10x5AnswerState + [_10x5_answer: u8] -> ReceiveGandalfAlternativeNameState,
|
||||
ReceiveGandalfAlternativeNameState + [gandalf_alternative_name: String] -> ExitState,
|
||||
);
|
||||
```
|
||||
|
||||
|
@ -249,15 +236,17 @@ The handy `up!` macro automatically generates functions that complete one state
|
|||
|
||||
pub type Out = TransitionOut<Dialogue>;
|
||||
|
||||
pub async fn start(cx: TransitionIn, state: StartState) -> Out {
|
||||
#[teloxide(transition)]
|
||||
async fn start(state: StartState, cx: TransitionIn) -> Out {
|
||||
cx.answer_str("Let's start our test! How many days per week are there?")
|
||||
.await?;
|
||||
next(state.up())
|
||||
}
|
||||
|
||||
pub async fn receive_days_of_week(
|
||||
cx: TransitionIn,
|
||||
#[teloxide(transition)]
|
||||
async fn receive_days_of_week(
|
||||
state: ReceiveDaysOfWeekState,
|
||||
cx: TransitionIn,
|
||||
) -> Out {
|
||||
match cx.update.text().map(str::parse) {
|
||||
Some(Ok(ans)) if ans == 7 => {
|
||||
|
@ -271,9 +260,10 @@ pub async fn receive_days_of_week(
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn receive_10x5_answer(
|
||||
cx: TransitionIn,
|
||||
#[teloxide(transition)]
|
||||
async fn receive_10x5_answer(
|
||||
state: Receive10x5AnswerState,
|
||||
cx: TransitionIn,
|
||||
) -> Out {
|
||||
match cx.update.text().map(str::parse) {
|
||||
Some(Ok(ans)) if ans == 50 => {
|
||||
|
@ -287,9 +277,10 @@ pub async fn receive_10x5_answer(
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn receive_gandalf_alternative_name(
|
||||
cx: TransitionIn,
|
||||
#[teloxide(transition)]
|
||||
async fn receive_gandalf_alternative_name(
|
||||
state: ReceiveGandalfAlternativeNameState,
|
||||
cx: TransitionIn,
|
||||
) -> Out {
|
||||
match cx.update.text() {
|
||||
Some(ans) if ans == "Mithrandir" => {
|
||||
|
@ -327,7 +318,7 @@ async fn main() {
|
|||
input
|
||||
.dialogue
|
||||
.unwrap()
|
||||
.dispatch(input.cx)
|
||||
.react(input.cx)
|
||||
.await
|
||||
.expect("Something wrong with the bot!")
|
||||
},
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
// This is a bot that asks your full name, your age, your favourite kind of
|
||||
// music and sends all the gathered information back.
|
||||
// This is a bot that asks you three questions, e.g. a simple test.
|
||||
//
|
||||
// # Example
|
||||
// ```
|
||||
// - Let's start! First, what's your full name?
|
||||
// - Luke Skywalker
|
||||
// - What a wonderful name! Your age?
|
||||
// - 26
|
||||
// - Good. Now choose your favourite music
|
||||
// *A keyboard of music kinds is displayed*
|
||||
// *You select Metal*
|
||||
// - Metal
|
||||
// - Fine. Your full name: Luke Skywalker, your age: 26, your favourite music: Metal
|
||||
// - Let's start our test! How many days per week are there?
|
||||
// - 7
|
||||
// - 10*5 = ?
|
||||
// - 50
|
||||
// - What's an alternative name of Gandalf?
|
||||
// - Mithrandir
|
||||
// - Congratulations! You've successfully passed the test!
|
||||
// ```
|
||||
|
||||
#![allow(clippy::trivial_regex)]
|
||||
|
@ -21,8 +18,6 @@
|
|||
extern crate smart_default;
|
||||
#[macro_use]
|
||||
extern crate derive_more;
|
||||
#[macro_use]
|
||||
extern crate teloxide_macros;
|
||||
|
||||
mod states;
|
||||
mod transitions;
|
||||
|
@ -50,7 +45,7 @@ async fn run() {
|
|||
input
|
||||
.dialogue
|
||||
.unwrap()
|
||||
.dispatch(input.cx)
|
||||
.react(input.cx)
|
||||
.await
|
||||
.expect("Something wrong with the bot!")
|
||||
},
|
||||
|
|
|
@ -1,23 +1,12 @@
|
|||
use teloxide::prelude::*;
|
||||
use teloxide_macros::Transition;
|
||||
|
||||
use super::transitions::{
|
||||
receive_10x5_answer, receive_days_of_week,
|
||||
receive_gandalf_alternative_name, start,
|
||||
};
|
||||
|
||||
#[derive(BotDialogue, SmartDefault, From)]
|
||||
#[derive(Transition, SmartDefault, From)]
|
||||
pub enum Dialogue {
|
||||
#[default]
|
||||
#[transition(start)]
|
||||
Start(StartState),
|
||||
|
||||
#[transition(receive_days_of_week)]
|
||||
ReceiveDaysOfWeek(ReceiveDaysOfWeekState),
|
||||
|
||||
#[transition(receive_10x5_answer)]
|
||||
Receive10x5Answer(Receive10x5AnswerState),
|
||||
|
||||
#[transition(receive_gandalf_alternative_name)]
|
||||
ReceiveGandalfAlternativeName(ReceiveGandalfAlternativeNameState),
|
||||
}
|
||||
|
||||
|
@ -38,14 +27,8 @@ pub struct ReceiveGandalfAlternativeNameState {
|
|||
_10x5_answer: u8,
|
||||
}
|
||||
|
||||
pub struct ExitState {
|
||||
rest: ReceiveGandalfAlternativeNameState,
|
||||
gandalf_alternative_name: String,
|
||||
}
|
||||
|
||||
up!(
|
||||
StartState -> ReceiveDaysOfWeekState,
|
||||
ReceiveDaysOfWeekState + [days_of_week: u8] -> Receive10x5AnswerState,
|
||||
Receive10x5AnswerState + [_10x5_answer: u8] -> ReceiveGandalfAlternativeNameState,
|
||||
ReceiveGandalfAlternativeNameState + [gandalf_alternative_name: String] -> ExitState,
|
||||
);
|
||||
|
|
|
@ -2,19 +2,23 @@ use crate::states::{
|
|||
Dialogue, Receive10x5AnswerState, ReceiveDaysOfWeekState,
|
||||
ReceiveGandalfAlternativeNameState, StartState,
|
||||
};
|
||||
|
||||
use teloxide::prelude::*;
|
||||
use teloxide_macros::teloxide;
|
||||
|
||||
pub type Out = TransitionOut<Dialogue>;
|
||||
|
||||
pub async fn start(cx: TransitionIn, state: StartState) -> Out {
|
||||
#[teloxide(transition)]
|
||||
async fn start(state: StartState, cx: TransitionIn) -> Out {
|
||||
cx.answer_str("Let's start our test! How many days per week are there?")
|
||||
.await?;
|
||||
next(state.up())
|
||||
}
|
||||
|
||||
pub async fn receive_days_of_week(
|
||||
cx: TransitionIn,
|
||||
#[teloxide(transition)]
|
||||
async fn receive_days_of_week(
|
||||
state: ReceiveDaysOfWeekState,
|
||||
cx: TransitionIn,
|
||||
) -> Out {
|
||||
match cx.update.text().map(str::parse) {
|
||||
Some(Ok(ans)) if ans == 7 => {
|
||||
|
@ -28,9 +32,10 @@ pub async fn receive_days_of_week(
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn receive_10x5_answer(
|
||||
cx: TransitionIn,
|
||||
#[teloxide(transition)]
|
||||
async fn receive_10x5_answer(
|
||||
state: Receive10x5AnswerState,
|
||||
cx: TransitionIn,
|
||||
) -> Out {
|
||||
match cx.update.text().map(str::parse) {
|
||||
Some(Ok(ans)) if ans == 50 => {
|
||||
|
@ -44,9 +49,10 @@ pub async fn receive_10x5_answer(
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn receive_gandalf_alternative_name(
|
||||
cx: TransitionIn,
|
||||
#[teloxide(transition)]
|
||||
async fn receive_gandalf_alternative_name(
|
||||
state: ReceiveGandalfAlternativeNameState,
|
||||
cx: TransitionIn,
|
||||
) -> Out {
|
||||
match cx.update.text() {
|
||||
Some(ans) if ans == "Mithrandir" => {
|
||||
|
|
|
@ -39,7 +39,7 @@ async fn run() {
|
|||
let (cx, dialogue) = input.unpack();
|
||||
|
||||
dialogue
|
||||
.dispatch(cx)
|
||||
.react(cx)
|
||||
.await
|
||||
.expect("Something is wrong with the bot!")
|
||||
},
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use teloxide::prelude::*;
|
||||
use teloxide_macros::BotDialogue;
|
||||
|
||||
use super::transitions::{have_number, start};
|
||||
use teloxide_macros::Transition;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -18,12 +16,9 @@ up!(
|
|||
StartState + [number: i32] -> HaveNumberState,
|
||||
);
|
||||
|
||||
#[derive(BotDialogue, SmartDefault, From, Serialize, Deserialize)]
|
||||
#[derive(Transition, SmartDefault, From, Serialize, Deserialize)]
|
||||
pub enum Dialogue {
|
||||
#[default]
|
||||
#[transition(start)]
|
||||
Start(StartState),
|
||||
|
||||
#[transition(have_number)]
|
||||
HaveNumber(HaveNumberState),
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use teloxide::prelude::*;
|
||||
use teloxide_macros::teloxide;
|
||||
|
||||
use super::states::*;
|
||||
|
||||
|
@ -17,7 +18,8 @@ macro_rules! extract_text {
|
|||
|
||||
pub type Out = TransitionOut<Dialogue>;
|
||||
|
||||
pub async fn start(cx: TransitionIn, state: StartState) -> Out {
|
||||
#[teloxide(transition)]
|
||||
async fn start(state: StartState, cx: TransitionIn) -> Out {
|
||||
let text = extract_text!(cx);
|
||||
|
||||
if let Ok(number) = text.parse() {
|
||||
|
@ -33,7 +35,8 @@ pub async fn start(cx: TransitionIn, state: StartState) -> Out {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn have_number(cx: TransitionIn, state: HaveNumberState) -> Out {
|
||||
#[teloxide(transition)]
|
||||
async fn have_number(state: HaveNumberState, cx: TransitionIn) -> Out {
|
||||
let text = extract_text!(cx);
|
||||
let num = state.number;
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
use crate::dispatching::dialogue::{TransitionIn, TransitionOut};
|
||||
use futures::future::BoxFuture;
|
||||
|
||||
/// Represents a dialogue FSM.
|
||||
pub trait BotDialogue: Default {
|
||||
/// Turns itself into another state, depending on the input message.
|
||||
fn dispatch(
|
||||
self,
|
||||
cx: TransitionIn,
|
||||
) -> BoxFuture<'static, TransitionOut<Self>>;
|
||||
}
|
|
@ -2,9 +2,11 @@
|
|||
//!
|
||||
//! There are three main components:
|
||||
//!
|
||||
//! 1. Your type `D` (typically an enumeration), implementing [`BotDialogue`].
|
||||
//! 1. Your type `D` (typically an enumeration), implementing [`Transition`].
|
||||
//! It is essentially a [FSM]: its variants are possible dialogue states and
|
||||
//! [`BotDialogue::dispatch`] is a transition function.
|
||||
//! [`Transition::react`] is a transition function.
|
||||
//!
|
||||
//! 2. State types, forming `D`. They implement [`SubTransition`].
|
||||
//!
|
||||
//! 2. [`Storage<D>`], which encapsulates all the dialogues.
|
||||
//!
|
||||
|
@ -30,7 +32,7 @@
|
|||
//! use std::convert::Infallible;
|
||||
//!
|
||||
//! use teloxide::prelude::*;
|
||||
//! use teloxide_macros::BotDialogue;
|
||||
//! use teloxide_macros::{teloxide, Transition};
|
||||
//!
|
||||
//! struct _1State;
|
||||
//! struct _2State;
|
||||
|
@ -38,23 +40,25 @@
|
|||
//!
|
||||
//! type Out = TransitionOut<D>;
|
||||
//!
|
||||
//! async fn _1_transition(_cx: TransitionIn, _state: _1State) -> Out {
|
||||
//! todo!()
|
||||
//! }
|
||||
//! async fn _2_transition(_cx: TransitionIn, _state: _2State) -> Out {
|
||||
//! todo!()
|
||||
//! }
|
||||
//! async fn _3_transition(_cx: TransitionIn, _state: _3State) -> Out {
|
||||
//! #[teloxide(transition)]
|
||||
//! async fn _1_transition(_state: _1State, _cx: TransitionIn) -> Out {
|
||||
//! todo!()
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(BotDialogue)]
|
||||
//! #[teloxide(transition)]
|
||||
//! async fn _2_transition(_state: _2State, _cx: TransitionIn) -> Out {
|
||||
//! todo!()
|
||||
//! }
|
||||
//!
|
||||
//! #[teloxide(transition)]
|
||||
//! async fn _3_transition(_state: _3State, _cx: TransitionIn) -> Out {
|
||||
//! todo!()
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Transition)]
|
||||
//! enum D {
|
||||
//! #[transition(_1_transition)]
|
||||
//! _1(_1State),
|
||||
//! #[transition(_2_transition)]
|
||||
//! _2(_2State),
|
||||
//! #[transition(_3_transition)]
|
||||
//! _3(_3State),
|
||||
//! }
|
||||
//!
|
||||
|
@ -94,9 +98,10 @@
|
|||
//!
|
||||
//! See [examples/dialogue_bot] as a real example.
|
||||
//!
|
||||
//! [`BotDialogue`]: crate::dispatching::dialogue::BotDialogue
|
||||
//! [`BotDialogue::dispatch`]:
|
||||
//! crate::dispatching::dialogue::BotDialogue::dispatch
|
||||
//! [`Transition`]: crate::dispatching::dialogue::Transition
|
||||
//! [`SubTransition`]: crate::dispatching::dialogue::SubTransition
|
||||
//! [`Transition::react`]:
|
||||
//! crate::dispatching::dialogue::Transition::react
|
||||
//! [FSM]: https://en.wikipedia.org/wiki/Finite-state_machine
|
||||
//!
|
||||
//! [`Storage<D>`]: crate::dispatching::dialogue::Storage
|
||||
|
@ -122,26 +127,27 @@
|
|||
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
mod bot_dialogue;
|
||||
mod dialogue_dispatcher;
|
||||
mod dialogue_dispatcher_handler;
|
||||
mod dialogue_stage;
|
||||
mod dialogue_with_cx;
|
||||
mod get_chat_id;
|
||||
mod storage;
|
||||
mod transition;
|
||||
|
||||
use crate::{requests::ResponseResult, types::Message};
|
||||
pub use bot_dialogue::BotDialogue;
|
||||
pub use dialogue_dispatcher::DialogueDispatcher;
|
||||
pub use dialogue_dispatcher_handler::DialogueDispatcherHandler;
|
||||
pub use dialogue_stage::{exit, next, DialogueStage};
|
||||
pub use dialogue_with_cx::DialogueWithCx;
|
||||
pub use get_chat_id::GetChatId;
|
||||
pub use transition::{
|
||||
SubTransition, SubTransitionOutputType, Transition, TransitionIn,
|
||||
TransitionOut,
|
||||
};
|
||||
|
||||
#[cfg(feature = "redis-storage")]
|
||||
pub use storage::{RedisStorage, RedisStorageError};
|
||||
|
||||
use crate::dispatching::UpdateWithCx;
|
||||
pub use storage::{serializer, InMemStorage, Serializer, Storage};
|
||||
|
||||
/// Generates `.up(field)` methods for dialogue states.
|
||||
|
@ -192,9 +198,3 @@ macro_rules! up {
|
|||
)+
|
||||
};
|
||||
}
|
||||
|
||||
/// An input passed into a FSM transition function.
|
||||
pub type TransitionIn = UpdateWithCx<Message>;
|
||||
|
||||
/// A type returned from a FSM transition function.
|
||||
pub type TransitionOut<D> = ResponseResult<DialogueStage<D>>;
|
||||
|
|
44
src/dispatching/dialogue/transition.rs
Normal file
44
src/dispatching/dialogue/transition.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use crate::{
|
||||
dispatching::{dialogue::DialogueStage, UpdateWithCx},
|
||||
requests::ResponseResult,
|
||||
types::Message,
|
||||
};
|
||||
use futures::future::BoxFuture;
|
||||
|
||||
/// Represents a transition function of a dialogue FSM.
|
||||
pub trait Transition: Sized {
|
||||
/// Turns itself into another state, depending on the input message.
|
||||
fn react(self, cx: TransitionIn)
|
||||
-> BoxFuture<'static, TransitionOut<Self>>;
|
||||
}
|
||||
|
||||
/// Like [`Transition`], but from `StateN` -> `Dialogue`.
|
||||
///
|
||||
/// [`Transition`]: crate::dispatching::dialogue::Transition
|
||||
pub trait SubTransition<Dialogue>
|
||||
where
|
||||
Dialogue: Transition,
|
||||
{
|
||||
/// Turns itself into another state, depending on the input message.
|
||||
fn react(
|
||||
self,
|
||||
cx: TransitionIn,
|
||||
) -> BoxFuture<'static, TransitionOut<Dialogue>>;
|
||||
}
|
||||
|
||||
/// A type returned from a FSM subtransition function.
|
||||
///
|
||||
/// Now it is used only inside `#[teloxide(transition)]` for type inference.
|
||||
pub trait SubTransitionOutputType {
|
||||
type Output;
|
||||
}
|
||||
|
||||
impl<D> SubTransitionOutputType for TransitionOut<D> {
|
||||
type Output = D;
|
||||
}
|
||||
|
||||
/// An input passed into a FSM (sub)transition function.
|
||||
pub type TransitionIn = UpdateWithCx<Message>;
|
||||
|
||||
/// A type returned from a FSM (sub)transition function.
|
||||
pub type TransitionOut<D> = ResponseResult<DialogueStage<D>>;
|
|
@ -3,8 +3,8 @@
|
|||
pub use crate::{
|
||||
dispatching::{
|
||||
dialogue::{
|
||||
exit, next, BotDialogue, DialogueDispatcher, DialogueStage,
|
||||
DialogueWithCx, GetChatId, TransitionIn, TransitionOut,
|
||||
exit, next, DialogueDispatcher, DialogueStage, DialogueWithCx,
|
||||
GetChatId, Transition, TransitionIn, TransitionOut,
|
||||
},
|
||||
Dispatcher, DispatcherHandlerRx, DispatcherHandlerRxExt, UpdateWithCx,
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue