From d37f7d1f38c216288c15e35b798846d3cbba0e88 Mon Sep 17 00:00:00 2001 From: Desiders Date: Sun, 28 Apr 2024 16:40:42 +0300 Subject: [PATCH] Add new example and rename old --- .../{finite_state_machine => fsm}/Cargo.toml | 2 +- .../{finite_state_machine => fsm}/src/main.rs | 2 +- .../fsm_and_business_connection/Cargo.toml | 11 + .../fsm_and_business_connection/src/main.rs | 223 ++++++++++++++++++ telers/README.md | 2 +- 5 files changed, 237 insertions(+), 3 deletions(-) rename examples/{finite_state_machine => fsm}/Cargo.toml (90%) rename examples/{finite_state_machine => fsm}/src/main.rs (99%) create mode 100644 examples/fsm_and_business_connection/Cargo.toml create mode 100644 examples/fsm_and_business_connection/src/main.rs diff --git a/examples/finite_state_machine/Cargo.toml b/examples/fsm/Cargo.toml similarity index 90% rename from examples/finite_state_machine/Cargo.toml rename to examples/fsm/Cargo.toml index f190d0f..7e2c50b 100644 --- a/examples/finite_state_machine/Cargo.toml +++ b/examples/fsm/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "finite_state_machine" +name = "fsm" version = "0.1.0" edition = "2021" publish = false diff --git a/examples/finite_state_machine/src/main.rs b/examples/fsm/src/main.rs similarity index 99% rename from examples/finite_state_machine/src/main.rs rename to examples/fsm/src/main.rs index 59e8d78..6b29eca 100644 --- a/examples/finite_state_machine/src/main.rs +++ b/examples/fsm/src/main.rs @@ -14,7 +14,7 @@ //! //! 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 finite_state_machine +//! RUST_LOG={log_level} BOT_TOKEN={your_bot_token} cargo run --package fsm //! ``` use std::borrow::Cow; diff --git a/examples/fsm_and_business_connection/Cargo.toml b/examples/fsm_and_business_connection/Cargo.toml new file mode 100644 index 0000000..905ff02 --- /dev/null +++ b/examples/fsm_and_business_connection/Cargo.toml @@ -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"] } \ No newline at end of file diff --git a/examples/fsm_and_business_connection/src/main.rs b/examples/fsm_and_business_connection/src/main.rs new file mode 100644 index 0000000..801b7ef --- /dev/null +++ b/examples/fsm_and_business_connection/src/main.rs @@ -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`] 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 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 for Cow<'static, str> { + fn from(state: State) -> Self { + Cow::Borrowed(state.as_str()) + } +} + +async fn start_handler( + bot: Bot, + message: Message, + fsm: FSMContext, +) -> 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( + bot: Bot, + message: MessageText, + fsm: FSMContext, +) -> 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( + bot: Bot, + message: MessageText, + fsm: FSMContext, +) -> 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 = 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::) + .filter(Command::one("start")) + .filter(StateFilter::none()); + router + .business_message + .register(name_handler::) + .filter(ContentType::one(ContentTypeEnum::Text)) + .filter(StateFilter::one(State::Name)); + router + .business_message + .register(language_handler::) + .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"), + } +} diff --git a/telers/README.md b/telers/README.md index 7ba0aa7..5b715fd 100644 --- a/telers/README.md +++ b/telers/README.md @@ -38,7 +38,7 @@ Before you start, make sure that you have a basic understanding of the [Telegram - [Stats updates middleware][examples/stats_incoming_updates_middleware]. This example shows how to create a middleware that count incoming updates. - [Context][examples/from_event_and_context]. This example shows how to extract data from event and context and use it in handlers. - [Input file][examples/input_file]. This example shows how to send files by the bot. - - [Finite state machine][examples/finite_state_machine]. This example shows how to use a finite state machine (conversation). + - [Finite state machine][examples/fsm]. This example shows how to use a finite state machine (conversation). - [Router tree][examples/router_tree]. This example shows how to create a router tree. - [Bot http client][examples/bot_http_client]. This example shows how to set a custom bot HTTP client. - [Axum and echo bot][examples/axum_and_echo_bot]. This example shows how to create an echo bot and run it concurrently with polling `axum` server.