-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
237 additions
and
3 deletions.
There are no files selected for viewing
2 changes: 1 addition & 1 deletion
2
examples/finite_state_machine/Cargo.toml → examples/fsm/Cargo.toml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
[package] | ||
name = "finite_state_machine" | ||
name = "fsm" | ||
version = "0.1.0" | ||
edition = "2021" | ||
publish = false | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
[package] | ||
name = "fsm_and_business_connection" | ||
version = "0.1.0" | ||
edition = "2021" | ||
publish = false | ||
|
||
[dependencies] | ||
telers = { path = "../../telers", features = ["memory-storage"] } | ||
tokio = { version = "1.36", features = ["macros"] } | ||
tracing = "0.1" | ||
tracing-subscriber = { version = "0.3", features = ["env-filter"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
//! This example shows how to use [`FSMContextMiddleware`] and [`StateFilter`] to use a finite state machine with a user in business connection. | ||
//! In this example we will ask user for his name and language, | ||
//! if languase isn't "acceptable", we will ask him to choose another one. | ||
//! After that all steps will be finished and we will send a message with user's name and language to him and | ||
//! finish conversation. | ||
//! | ||
//! In this example we will use [`MemoryStorage`] as storage for [`FSMContextMiddleware`], but you can use any storage, | ||
//! which implements [`Storage`] trait. | ||
//! This storage isn't recommended for production use, because it doesn't persist data between restarts, but it's | ||
//! useful for testing and example purposes and easy to use. | ||
//! We the same use [`StateFilter`] to filter states and call handlers only when state is equal to some value. | ||
//! | ||
//! More information about FSM you can find in [`telers::fsm`] and [`FSMContextMiddleware`] documentation. | ||
//! | ||
//! You can run this example by setting `BOT_TOKEN` and optional `RUST_LOG` environment variable and running: | ||
//! ```bash | ||
//! RUST_LOG={log_level} BOT_TOKEN={your_bot_token} cargo run --package fsm_and_business_connection | ||
//! ``` | ||
use std::borrow::Cow; | ||
use telers::{ | ||
enums::ContentType as ContentTypeEnum, | ||
enums::UpdateType, | ||
event::{telegram::HandlerResult, EventReturn, ToServiceProvider as _}, | ||
filters::{Command, ContentType, State as StateFilter}, | ||
fsm::{Context as FSMContext, MemoryStorage, Storage, Strategy}, | ||
methods::SendMessage, | ||
middlewares::outer::FSMContext as FSMContextMiddleware, | ||
types::{Message, MessageText}, | ||
Bot, Dispatcher, Router, | ||
}; | ||
use tracing::{event, Level}; | ||
use tracing_subscriber::{fmt, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter}; | ||
|
||
/// State of conversation. | ||
/// | ||
/// We use it to determine what we should ask user next and implement [`From<State>`] for [`Cow<'static, str>`] | ||
/// for possible save this state in [`Storage`]. | ||
/// We also implement [`PartialEq<&str>`] for comparing states with other in [`StateFilter`]. | ||
#[derive(Clone)] | ||
enum State { | ||
/// User is asked for his name | ||
Name, | ||
/// User is asked for his language | ||
Language, | ||
} | ||
|
||
impl State { | ||
const fn as_str(&self) -> &'static str { | ||
match self { | ||
State::Name => "name", | ||
State::Language => "language", | ||
} | ||
} | ||
} | ||
|
||
// Implementation `PartialEq<&str>` and `From<State> for Cow<'static, str>` for `State` is optional, | ||
// but it's useful for using enum as state without boilerplate code as `State::Name.as_str()`, | ||
// because we can use `State::Name` directly. | ||
impl PartialEq<&str> for State { | ||
fn eq(&self, other: &&str) -> bool { | ||
self.as_str() == *other | ||
} | ||
} | ||
|
||
impl From<State> for Cow<'static, str> { | ||
fn from(state: State) -> Self { | ||
Cow::Borrowed(state.as_str()) | ||
} | ||
} | ||
|
||
async fn start_handler<S: Storage>( | ||
bot: Bot, | ||
message: Message, | ||
fsm: FSMContext<S>, | ||
) -> HandlerResult { | ||
bot.send(SendMessage::new( | ||
message.chat().id(), | ||
"Hello! What's your name?", | ||
).business_connection_id(message.business_connection_id().expect( | ||
"Business connection id should be set, because we regitered this handler for business connections only", | ||
))) | ||
.await?; | ||
|
||
// We set state to `State::Name` to point that we are waiting for user's name. | ||
// `name_handler` will be called when user will send message, | ||
// because we set `State::Name` as state and this handler is registered for this state | ||
fsm.set_state(State::Name).await.map_err(Into::into)?; | ||
|
||
Ok(EventReturn::Finish) | ||
} | ||
|
||
async fn name_handler<S: Storage>( | ||
bot: Bot, | ||
message: MessageText, | ||
fsm: FSMContext<S>, | ||
) -> HandlerResult { | ||
let name = message.text; | ||
|
||
// Save name to FSM storage, because we will need it in `language_handler` | ||
fsm.set_value("name", name.clone()) | ||
.await | ||
.map_err(Into::into)?; | ||
// Set state to `State::Language` to point that we are waiting for user's language | ||
fsm.set_state(State::Language).await.map_err(Into::into)?; | ||
|
||
// Usually state and data set to FSM storage before sending message to user, | ||
// because we want to be sure that we will receive message from user in the same state | ||
// (user can send message to bot before we set state and data to FSM storage, but it's rare case) | ||
|
||
bot.send( | ||
SendMessage::new( | ||
message.chat.id(), | ||
format!("Nice to meet you, {name}! What's your native language?"), | ||
) | ||
.business_connection_id(message.business_connection_id.expect( | ||
"Business connection id should be set, because we regitered this handler for business connections only", | ||
)), | ||
) | ||
.await?; | ||
|
||
Ok(EventReturn::Finish) | ||
} | ||
|
||
async fn language_handler<S: Storage>( | ||
bot: Bot, | ||
message: MessageText, | ||
fsm: FSMContext<S>, | ||
) -> HandlerResult { | ||
let language = message.text; | ||
|
||
// Get user's name from FSM storage | ||
// TODO: Add validation, e.g. check that name isn't empty | ||
let name: Box<str> = fsm | ||
.get_value("name") | ||
.await | ||
.map_err(Into::into)? | ||
.expect("Name should be set"); | ||
|
||
// Check if user's language is acceptable | ||
match language.to_lowercase().as_str() { | ||
"english" | "en" => { | ||
bot.send(SendMessage::new( | ||
message.chat.id(), | ||
format!("{name}, let's talk!"), | ||
).business_connection_id(message.business_connection_id.expect( | ||
"Business connection id should be set, because we regitered this handler for business connections only", | ||
))) | ||
.await?; | ||
|
||
// Remove state and data from FSM storage, because we don't need them anymore | ||
fsm.finish().await.map_err(Into::into)?; | ||
} | ||
_ => { | ||
bot.send(SendMessage::new( | ||
message.chat.id(), | ||
format!("{name}, I don't speak your language. Please, choose another :(",), | ||
).business_connection_id(message.business_connection_id.expect( | ||
"Business connection id should be set, because we regitered this handler for business connections only", | ||
))) | ||
.await?; | ||
|
||
// We don't need this, because `State::Language` is already set and doesn't change automatically | ||
// fsm.set_state(State::Language).await.map_err(Into::into)?; | ||
} | ||
}; | ||
|
||
Ok(EventReturn::Finish) | ||
} | ||
|
||
#[tokio::main(flavor = "current_thread")] | ||
async fn main() { | ||
tracing_subscriber::registry() | ||
.with(fmt::layer()) | ||
.with(EnvFilter::from_env("RUST_LOG")) | ||
.init(); | ||
|
||
let bot = Bot::from_env_by_key("BOT_TOKEN"); | ||
|
||
// You can use any storage, which implements `Storage` trait | ||
let storage = MemoryStorage::new(); | ||
|
||
let mut router = Router::new("main"); | ||
|
||
// Register fsm middleware for possible managing states and fsm data (e.g. user's name and language for this example) | ||
router | ||
.update | ||
.outer_middlewares | ||
// We use here `Strategy::UserInChatAndConnection` to have different states for business connections and other chats | ||
.register(FSMContextMiddleware::new(storage).strategy(Strategy::UserInChatAndConnection)); | ||
|
||
router | ||
.business_message | ||
.register(start_handler::<MemoryStorage>) | ||
.filter(Command::one("start")) | ||
.filter(StateFilter::none()); | ||
router | ||
.business_message | ||
.register(name_handler::<MemoryStorage>) | ||
.filter(ContentType::one(ContentTypeEnum::Text)) | ||
.filter(StateFilter::one(State::Name)); | ||
router | ||
.business_message | ||
.register(language_handler::<MemoryStorage>) | ||
.filter(ContentType::one(ContentTypeEnum::Text)) | ||
.filter(StateFilter::one(State::Language)); | ||
|
||
let dispatcher = Dispatcher::builder() | ||
.main_router(router) | ||
.bot(bot) | ||
.allowed_update(UpdateType::BusinessMessage) | ||
.build(); | ||
|
||
match dispatcher | ||
.to_service_provider_default() | ||
.unwrap() | ||
.run_polling() | ||
.await | ||
{ | ||
Ok(()) => event!(Level::INFO, "Bot stopped"), | ||
Err(err) => event!(Level::ERROR, error = %err, "Bot stopped"), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters