From 6e11427ed1561cc3add6e8655bbdba520bcc8d86 Mon Sep 17 00:00:00 2001 From: Temirkhan Myrzamadi Date: Tue, 26 May 2020 17:03:24 +0600 Subject: [PATCH] Update README.md --- README.md | 232 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 165 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index c8d5f37e..4c4e5917 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,11 @@ ## Table of contents - [Features](https://github.com/teloxide/teloxide#features) - - [Getting started](https://github.com/teloxide/teloxide#getting-started) - - [Examples](https://github.com/teloxide/teloxide#examples) + - [Setting up your environment](https://github.com/teloxide/teloxide#setting-up-your-environment) + - [API overview](https://github.com/teloxide/teloxide#api-overview) - [The ping-pong bot](https://github.com/teloxide/teloxide#the-ping-pong-bot) - [Commands](https://github.com/teloxide/teloxide#commands) - - [Guess a number](https://github.com/teloxide/teloxide#guess-a-number) - - [More examples!](https://github.com/teloxide/teloxide#more-examples) + - [Dialogues](https://github.com/teloxide/teloxide#dialogues) - [Recommendations](https://github.com/teloxide/teloxide#recommendations) - [FAQ](https://github.com/teloxide/teloxide#faq) - [Where I can ask questions?](https://github.com/teloxide/teloxide#where-i-can-ask-questions) @@ -64,9 +63,10 @@ Dialogues management is independent of how/where they are stored: just replace o Define a type-safe finite automaton and transition functions to drive a user dialogue with ease (see the guess-a-number example below).

-## Getting started - 1. Create a new bot using [@Botfather](https://t.me/botfather) to get a token in the format `123456789:blablabla`. - 2. Initialise the `TELOXIDE_TOKEN` environmental variable to your token: +## Setting up your environment + 1. [Download Rust](http://rustup.rs/). + 2. Create a new bot using [@Botfather](https://t.me/botfather) to get a token in the format `123456789:blablabla`. + 3. Initialise the `TELOXIDE_TOKEN` environmental variable to your token: ```bash # Unix $ export TELOXIDE_TOKEN= @@ -74,7 +74,7 @@ $ export TELOXIDE_TOKEN= # Windows $ set TELOXIDE_TOKEN= ``` - 3. Be sure that you are up to date: + 4. Be sure that you are up to date: ```bash # If you're using stable $ rustup update stable @@ -85,7 +85,7 @@ $ rustup update nightly $ rustup override set nightly ``` - 4. Execute `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`: + 5. Execute `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`: ```toml [dependencies] teloxide = "0.2.0" @@ -94,7 +94,9 @@ tokio = "0.2.11" pretty_env_logger = "0.4.0" ``` -## The ping-pong bot +## API overview + +### The ping-pong bot This bot has a single message handler, which answers "pong" to each incoming message: ([Full](https://github.com/teloxide/teloxide/blob/master/examples/ping_pong_bot/src/main.rs)) @@ -126,7 +128,7 @@ async fn main() { -## Commands +### Commands Commands are defined similar to how we define CLI using [structopt](https://docs.rs/structopt/0.3.9/structopt/). This bot says "I am a cat! Meow!" on `/meow`, generates a random number within [0; 1) on `/generate`, and shows the usage guide on `/help`: ([Full](https://github.com/teloxide/teloxide/blob/master/examples/simple_commands_bot/src/main.rs)) @@ -192,84 +194,180 @@ See? The dispatcher gives us a stream of messages, so we can handle it as we wan - ... And lots of [others](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html) and [others](https://docs.rs/teloxide/latest/teloxide/dispatching/trait.DispatcherHandlerRxExt.html) and [others](https://docs.rs/tokio/0.2.13/tokio/sync/index.html)! -## Guess a number -Wanna see more? This is a bot, which starts a game on each incoming message. You must guess a number from 1 to 10 (inclusively): +### Dialogues +Wanna see more? This is how dialogues management is made in teloxide. -([Full](https://github.com/teloxide/teloxide/blob/master/examples/guess_a_number_bot/src/main.rs)) +([dialogue_bot/src/states.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/states.rs)) ```rust // Imports are omitted... -#[derive(SmartDefault)] -enum Dialogue { - #[default] - Start, - ReceiveAttempt(u8), +pub struct StartState; + +pub struct ReceiveFullNameState { + rest: StartState, } -type Cx = DialogueDispatcherHandlerCx; -type Res = ResponseResult>; - -async fn start(cx: Cx<()>) -> Res { - cx.answer("Let's play a game! Guess a number from 1 to 10 (inclusively).") - .send() - .await?; - next(Dialogue::ReceiveAttempt(thread_rng().gen_range(1, 11))) +pub struct ReceiveAgeState { + rest: ReceiveFullNameState, + full_name: String, } -async fn receive_attempt(cx: Cx) -> Res { - let secret = cx.dialogue; +pub struct ReceiveFavouriteMusicState { + rest: ReceiveAgeState, + age: u8, +} - match cx.update.text() { - None => { - cx.answer("Oh, please, send me a text message!").send().await?; - next(Dialogue::ReceiveAttempt(secret)) +#[derive(Display)] +#[display( + "Your full name: {rest.rest.full_name}, your age: {rest.age}, your \ + favourite music: {favourite_music}" +)] +pub struct ExitState { + rest: ReceiveFavouriteMusicState, + favourite_music: FavouriteMusic, +} + +up!( + StartState -> ReceiveFullNameState, + ReceiveFullNameState + [full_name: String] -> ReceiveAgeState, + ReceiveAgeState + [age: u8] -> ReceiveFavouriteMusicState, + ReceiveFavouriteMusicState + [favourite_music: FavouriteMusic] -> ExitState, +); + +pub type Dialogue = Coprod!( + StartState, + ReceiveFullNameState, + ReceiveAgeState, + ReceiveFavouriteMusicState, +); + +wrap_dialogue!( + Wrapper(Dialogue), + default Self(Dialogue::inject(StartState)), +); +``` + +([dialogue_bot/src/transitions.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/transitions.rs)) +```rust +// Imports are omitted... + +pub type In = TransitionIn; +pub type Out = TransitionOut; + +pub async fn start(cx: In) -> Out { + let (cx, dialogue) = cx.unpack(); + + cx.answer_str("Let's start! First, what's your full name?").await?; + next(dialogue.up()) +} + +pub async fn receive_full_name(cx: In) -> Out { + let (cx, dialogue) = cx.unpack(); + + match cx.update.text_owned() { + Some(full_name) => { + cx.answer_str("What a wonderful name! Your age?").await?; + next(dialogue.up(full_name)) + } + _ => { + cx.answer_str("Please, enter a text message!").await?; + next(dialogue) } - Some(text) => match text.parse::() { - Ok(attempt) => { - if attempt == secret { - cx.answer("Congratulations! You won!").send().await?; - exit() - } else { - cx.answer("No.").send().await?; - next(Dialogue::ReceiveAttempt(secret)) - } - } - Err(_) => { - cx.answer("Oh, please, send me a number in the range [1; 10]!") - .send() - .await?; - next(Dialogue::ReceiveAttempt(secret)) - } - }, } } -async fn handle_message( - cx: DialogueDispatcherHandlerCx, -) -> Res { - // Match is omitted... +pub async fn receive_age(cx: In) -> Out { + let (cx, dialogue) = cx.unpack(); + + match cx.update.text().map(str::parse) { + Some(Ok(age)) => { + cx.answer("Good. Now choose your favourite music:") + .reply_markup(FavouriteMusic::markup()) + .send() + .await?; + next(dialogue.up(age)) + } + _ => { + cx.answer_str("Please, enter a number!").await?; + next(dialogue) + } + } } -#[tokio::main] -async fn main() { - // Setup is omitted... +pub async fn receive_favourite_music( + cx: In, +) -> Out { + let (cx, dialogue) = cx.unpack(); + + match cx.update.text().map(str::parse) { + Some(Ok(favourite_music)) => { + cx.answer_str(format!("Fine. {}", dialogue.up(favourite_music))) + .await?; + exit() + } + _ => { + cx.answer_str("Please, enter from the keyboard!").await?; + next(dialogue) + } + } } ``` -
- - - -

-
+([dialogue_bot/src/favourite_music.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/favourite_music.rs)) +```rust +// Imports are omitted... -Our [finite automaton](https://en.wikipedia.org/wiki/Finite-state_machine), designating a user dialogue, cannot be in an invalid state, and this is why it is called "type-safe". We could use `enum` + `Option`s instead, but it would lead us to lots of unpleasant `.unwrap()`s. +#[derive(Copy, Clone, Display, FromStr)] +pub enum FavouriteMusic { + Rock, + Metal, + Pop, + Other, +} -Remember that a classical [finite automaton](https://en.wikipedia.org/wiki/Finite-state_machine) is defined by its initial state, a list of its possible states and a transition function? We can think that `Dialogue` is a finite automaton with a context type at each state (`Dialogue::Start` has `()`, `Dialogue::ReceiveAttempt` has `u8`). +impl FavouriteMusic { + pub fn markup() -> ReplyKeyboardMarkup { + ReplyKeyboardMarkup::default().append_row(vec![ + KeyboardButton::new("Rock"), + KeyboardButton::new("Metal"), + KeyboardButton::new("Pop"), + KeyboardButton::new("Other"), + ]) + } +} +``` -See [examples/dialogue_bot](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/main.rs) to see a bit more complicated bot with dialogues. -## [More examples!](https://github.com/teloxide/teloxide/tree/master/examples) +([dialogue_bot/src/main.rs](https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/main.rs)) +```rust +// Imports are omitted... + +#[tokio::main] +async fn main() { + teloxide::enable_logging!(); + log::info!("Starting dialogue_bot!"); + + let bot = Bot::from_env(); + + Dispatcher::new(bot) + .messages_handler(DialogueDispatcher::new(|cx| async move { + let DialogueWithCx { cx, dialogue } = cx; + + // Unwrap without panic because of std::convert::Infallible. + let Wrapper(dialogue) = dialogue.unwrap(); + + dispatch!( + [cx, dialogue] -> + [start, receive_full_name, receive_age, receive_favourite_music] + ) + .expect("Something wrong with the bot!") + })) + .dispatch() + .await; +} +``` + +[More examples!](https://github.com/teloxide/teloxide/tree/master/examples) ## Recommendations - Use this pattern: