Skip to content

Commit

Permalink
Add new example and rename old
Browse files Browse the repository at this point in the history
  • Loading branch information
Desiders committed Apr 28, 2024
1 parent ef3aaec commit d37f7d1
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 3 deletions.
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions examples/fsm_and_business_connection/Cargo.toml
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"] }
223 changes: 223 additions & 0 deletions examples/fsm_and_business_connection/src/main.rs
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"),
}
}
2 changes: 1 addition & 1 deletion telers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit d37f7d1

Please sign in to comment.