Merge pull request #211 from teloxide/improve-dialogues

Reduce boilerplate in dialogues
This commit is contained in:
Temirkhan Myrzamadi 2020-05-29 18:03:49 +03:00 committed by GitHub
commit ac9c7a35f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 644 additions and 635 deletions

View file

@ -44,6 +44,7 @@ async-trait = "0.1.22"
futures = "0.3.1"
pin-project = "0.4.6"
serde_with_macros = "1.0.1"
frunk = "0.3.1"
teloxide-macros = "0.2.1"

234
README.md
View file

@ -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)
@ -57,9 +56,10 @@ All the API <a href="https://docs.rs/teloxide/latest/teloxide/types/index.html">
Dialogues management is independent of how/where they are stored: just replace one line and make them <a href="https://en.wikipedia.org/wiki/Persistence_(computer_science)">persistent</a> (for example, store on a disk, transmit through a network), without affecting the actual <a href="https://en.wikipedia.org/wiki/Finite-state_machine">FSM</a> algorithm. By default, teloxide stores all user dialogues in RAM. Default database implementations <a href="https://github.com/teloxide/teloxide/issues/183">are coming</a>!
</p>
## 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-like
$ export TELOXIDE_TOKEN=<Your token here>
@ -67,7 +67,7 @@ $ export TELOXIDE_TOKEN=<Your token here>
# Windows
$ set TELOXIDE_TOKEN=<Your token here>
```
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
@ -78,7 +78,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"
@ -87,7 +87,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))
@ -119,7 +121,7 @@ async fn main() {
</kbd>
</div>
## 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))
@ -185,84 +187,182 @@ 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<State> = DialogueDispatcherHandlerCx<Message, State>;
type Res = ResponseResult<DialogueStage<Dialogue>>;
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<u8>) -> 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)),
);
```
The [`wrap_dialogue!`](https://docs.rs/teloxide/latest/teloxide/macro.wrap_dialogue.html) macro generates a new-type of `Dialogue` with a default implementation.
([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<State> = TransitionIn<State, std::convert::Infallible>;
pub type Out = TransitionOut<Wrapper>;
pub async fn start(cx: In<StartState>) -> 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<ReceiveFullNameState>) -> 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::<u8>() {
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<Message, Dialogue>,
) -> Res {
// Match is omitted...
pub async fn receive_age(cx: In<ReceiveAgeState>) -> 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<ReceiveFavouriteMusicState>,
) -> 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)
}
}
}
```
<div align="center">
<kbd>
<img src=https://github.com/teloxide/teloxide/raw/master/media/GUESS_A_NUMBER_BOT.png width="600" />
</kbd>
<br/><br/>
</div>
([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:

View file

@ -10,9 +10,9 @@ edition = "2018"
log = "0.4.8"
tokio = "0.2.9"
pretty_env_logger = "0.4.0"
smart-default = "0.6.0"
parse-display = "0.1.1"
teloxide = { path = "../../" }
frunk = "0.3.1"
[profile.release]
lto = true

View file

@ -0,0 +1,22 @@
use parse_display::{Display, FromStr};
use teloxide::types::{KeyboardButton, ReplyKeyboardMarkup};
#[derive(Copy, Clone, Display, FromStr)]
pub enum FavouriteMusic {
Rock,
Metal,
Pop,
Other,
}
impl FavouriteMusic {
pub fn markup() -> ReplyKeyboardMarkup {
ReplyKeyboardMarkup::default().append_row(vec![
KeyboardButton::new("Rock"),
KeyboardButton::new("Metal"),
KeyboardButton::new("Pop"),
KeyboardButton::new("Other"),
])
}
}

View file

@ -15,167 +15,16 @@
// ```
#![allow(clippy::trivial_regex)]
#![allow(dead_code)]
#[macro_use]
extern crate smart_default;
mod favourite_music;
mod states;
mod transitions;
use std::convert::Infallible;
use teloxide::{
prelude::*,
types::{KeyboardButton, ReplyKeyboardMarkup},
};
use states::*;
use transitions::*;
use parse_display::{Display, FromStr};
// ============================================================================
// [Favourite music kinds]
// ============================================================================
#[derive(Copy, Clone, Display, FromStr)]
enum FavouriteMusic {
Rock,
Metal,
Pop,
Other,
}
impl FavouriteMusic {
fn markup() -> ReplyKeyboardMarkup {
ReplyKeyboardMarkup::default().append_row(vec![
KeyboardButton::new("Rock"),
KeyboardButton::new("Metal"),
KeyboardButton::new("Pop"),
KeyboardButton::new("Other"),
])
}
}
// ============================================================================
// [A type-safe finite automaton]
// ============================================================================
#[derive(Clone)]
struct ReceiveAgeState {
full_name: String,
}
#[derive(Clone)]
struct ReceiveFavouriteMusicState {
data: ReceiveAgeState,
age: u8,
}
#[derive(Display)]
#[display(
"Your full name: {data.data.full_name}, your age: {data.age}, your \
favourite music: {favourite_music}"
)]
struct ExitState {
data: ReceiveFavouriteMusicState,
favourite_music: FavouriteMusic,
}
#[derive(SmartDefault)]
enum Dialogue {
#[default]
Start,
ReceiveFullName,
ReceiveAge(ReceiveAgeState),
ReceiveFavouriteMusic(ReceiveFavouriteMusicState),
}
// ============================================================================
// [Control a dialogue]
// ============================================================================
type Cx<State> = DialogueDispatcherHandlerCx<Message, State, Infallible>;
type Res = ResponseResult<DialogueStage<Dialogue>>;
async fn start(cx: Cx<()>) -> Res {
cx.answer("Let's start! First, what's your full name?").send().await?;
next(Dialogue::ReceiveFullName)
}
async fn full_name(cx: Cx<()>) -> Res {
match cx.update.text() {
None => {
cx.answer("Please, send me a text message!").send().await?;
next(Dialogue::ReceiveFullName)
}
Some(full_name) => {
cx.answer("What a wonderful name! Your age?").send().await?;
next(Dialogue::ReceiveAge(ReceiveAgeState {
full_name: full_name.to_owned(),
}))
}
}
}
async fn age(cx: Cx<ReceiveAgeState>) -> Res {
match cx.update.text().unwrap().parse() {
Ok(age) => {
cx.answer("Good. Now choose your favourite music:")
.reply_markup(FavouriteMusic::markup())
.send()
.await?;
next(Dialogue::ReceiveFavouriteMusic(ReceiveFavouriteMusicState {
data: cx.dialogue.unwrap(),
age,
}))
}
Err(_) => {
cx.answer("Oh, please, enter a number!").send().await?;
next(Dialogue::ReceiveAge(cx.dialogue.unwrap()))
}
}
}
async fn favourite_music(cx: Cx<ReceiveFavouriteMusicState>) -> Res {
match cx.update.text().unwrap().parse() {
Ok(favourite_music) => {
cx.answer(format!(
"Fine. {}",
ExitState {
data: cx.dialogue.clone().unwrap(),
favourite_music
}
))
.send()
.await?;
exit()
}
Err(_) => {
cx.answer("Oh, please, enter from the keyboard!").send().await?;
next(Dialogue::ReceiveFavouriteMusic(cx.dialogue.unwrap()))
}
}
}
async fn handle_message(cx: Cx<Dialogue>) -> Res {
let DialogueDispatcherHandlerCx { bot, update, dialogue } = cx;
// You need handle the error instead of panicking in real-world code, maybe
// send diagnostics to a development chat.
match dialogue.expect("Failed to get dialogue info from storage") {
Dialogue::Start => {
start(DialogueDispatcherHandlerCx::new(bot, update, ())).await
}
Dialogue::ReceiveFullName => {
full_name(DialogueDispatcherHandlerCx::new(bot, update, ())).await
}
Dialogue::ReceiveAge(s) => {
age(DialogueDispatcherHandlerCx::new(bot, update, s)).await
}
Dialogue::ReceiveFavouriteMusic(s) => {
favourite_music(DialogueDispatcherHandlerCx::new(bot, update, s))
.await
}
}
}
// ============================================================================
// [Run!]
// ============================================================================
use teloxide::prelude::*;
#[tokio::main]
async fn main() {
@ -190,7 +39,16 @@ async fn run() {
Dispatcher::new(bot)
.messages_handler(DialogueDispatcher::new(|cx| async move {
handle_message(cx).await.expect("Something wrong with the bot!")
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;

View file

@ -0,0 +1,49 @@
use teloxide::prelude::*;
use super::favourite_music::FavouriteMusic;
use parse_display::Display;
pub struct StartState;
pub struct ReceiveFullNameState {
rest: StartState,
}
pub struct ReceiveAgeState {
rest: ReceiveFullNameState,
full_name: String,
}
pub struct ReceiveFavouriteMusicState {
rest: ReceiveAgeState,
age: u8,
}
#[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)),
);

View file

@ -0,0 +1,64 @@
use teloxide::prelude::*;
use super::{favourite_music::FavouriteMusic, states::*};
pub type In<State> = TransitionIn<State, std::convert::Infallible>;
pub type Out = TransitionOut<Wrapper>;
pub async fn start(cx: In<StartState>) -> 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<ReceiveFullNameState>) -> 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)
}
}
}
pub async fn receive_age(cx: In<ReceiveAgeState>) -> 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)
}
}
}
pub async fn receive_favourite_music(
cx: In<ReceiveFavouriteMusicState>,
) -> 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)
}
}
}

View file

@ -1,15 +0,0 @@
[package]
name = "guess_a_number_bot"
version = "0.1.0"
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.8"
tokio = "0.2.9"
smart-default = "0.6.0"
rand = "0.7.3"
pretty_env_logger = "0.4.0"
teloxide = { path = "../../" }

View file

@ -1,121 +0,0 @@
// This is a guess-a-number game!
//
// # Example
// ```
// - Hello
// - Let's play a game! Guess a number from 1 to 10 (inclusively).
// - 4
// - No.
// - 3
// - No.
// - Blablabla
// - Oh, please, send me a text message!
// - 111
// - Oh, please, send me a number in the range [1; 10]!
// - 5
// - Congratulations! You won!
// ```
#[macro_use]
extern crate smart_default;
use teloxide::prelude::*;
use rand::{thread_rng, Rng};
use std::convert::Infallible;
// ============================================================================
// [A type-safe finite automaton]
// ============================================================================
#[derive(SmartDefault)]
enum Dialogue {
#[default]
Start,
ReceiveAttempt(u8),
}
// ============================================================================
// [Control a dialogue]
// ============================================================================
type Cx<State> = DialogueDispatcherHandlerCx<Message, State, Infallible>;
type Res = ResponseResult<DialogueStage<Dialogue>>;
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)))
}
async fn receive_attempt(cx: Cx<u8>) -> Res {
let secret = cx.dialogue.unwrap();
match cx.update.text() {
None => {
cx.answer("Oh, please, send me a text message!").send().await?;
next(Dialogue::ReceiveAttempt(secret))
}
Some(text) => match text.parse::<u8>() {
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<Message, Dialogue, Infallible>,
) -> Res {
let DialogueDispatcherHandlerCx { bot, update, dialogue } = cx;
// You need handle the error instead of panicking in real-world code, maybe
// send diagnostics to a development chat.
match dialogue.expect("Failed to get dialogue info from storage") {
Dialogue::Start => {
start(DialogueDispatcherHandlerCx::new(bot, update, ())).await
}
Dialogue::ReceiveAttempt(secret) => {
receive_attempt(DialogueDispatcherHandlerCx::new(
bot, update, secret,
))
.await
}
}
}
// ============================================================================
// [Run!]
// ============================================================================
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting guess_a_number_bot!");
let bot = Bot::from_env();
Dispatcher::new(bot)
.messages_handler(DialogueDispatcher::new(|cx| async move {
handle_message(cx).await.expect("Something wrong with the bot!")
}))
.dispatch()
.await;
}

View file

@ -11,7 +11,7 @@ log = "0.4.8"
futures = "0.3.4"
tokio = "0.2.9"
pretty_env_logger = "0.4.0"
teloxide = "0.2.0"
teloxide = { path = "../../" }
# Used to setup a webhook
warp = "0.2.2"

View file

@ -14,14 +14,19 @@ async fn main() {
run().await;
}
async fn handle_rejection(error: warp::Rejection) -> Result<impl warp::Reply, Infallible> {
async fn handle_rejection(
error: warp::Rejection,
) -> Result<impl warp::Reply, Infallible> {
log::error!("Cannot process the request due to: {:?}", error);
Ok(StatusCode::INTERNAL_SERVER_ERROR)
}
pub async fn webhook<'a>(bot: Arc<Bot>) -> impl update_listeners::UpdateListener<Infallible> {
pub async fn webhook<'a>(
bot: Arc<Bot>,
) -> impl update_listeners::UpdateListener<Infallible> {
// Heroku defines auto defines a port value
let teloxide_token = env::var("TELOXIDE_TOKEN").expect("TELOXIDE_TOKEN env variable missing");
let teloxide_token = env::var("TELOXIDE_TOKEN")
.expect("TELOXIDE_TOKEN env variable missing");
let port: u16 = env::var("PORT")
.expect("PORT env variable missing")
.parse()
@ -31,10 +36,7 @@ pub async fn webhook<'a>(bot: Arc<Bot>) -> impl update_listeners::UpdateListener
let path = format!("bot{}", teloxide_token);
let url = format!("https://{}/{}", host, path);
bot.set_webhook(url)
.send()
.await
.expect("Cannot setup a webhook");
bot.set_webhook(url).send().await.expect("Cannot setup a webhook");
let (tx, rx) = mpsc::unbounded_channel();
@ -80,12 +82,14 @@ async fn run() {
Dispatcher::new(Arc::clone(&bot))
.messages_handler(|rx: DispatcherHandlerRx<Message>| {
rx.for_each(|message| async move {
message.answer("pong").send().await.log_on_error().await;
message.answer_str("pong").await.log_on_error().await;
})
})
.dispatch_with_listener(
webhook(bot).await,
LoggingErrorHandler::with_custom_text("An error from the update listener"),
LoggingErrorHandler::with_custom_text(
"An error from the update listener",
),
)
.await;
}

View file

@ -16,7 +16,7 @@ async fn run() {
Dispatcher::new(bot)
.messages_handler(|rx: DispatcherHandlerRx<Message>| {
rx.for_each(|message| async move {
message.answer("pong").send().await.log_on_error().await;
message.answer_str("pong").await.log_on_error().await;
})
})
.dispatch()

View file

@ -26,11 +26,10 @@ async fn run() {
let previous = MESSAGES_TOTAL.fetch_add(1, Ordering::Relaxed);
message
.answer(format!(
.answer_str(format!(
"I received {} messages in total.",
previous
))
.send()
.await
.log_on_error()
.await;

View file

@ -18,7 +18,7 @@ fn generate() -> String {
}
async fn answer(
cx: DispatcherHandlerCx<Message>,
cx: UpdateWithCx<Message>,
command: Command,
) -> ResponseResult<()> {
match command {

View file

@ -63,7 +63,7 @@ async fn run() {
Dispatcher::new(Arc::clone(&bot))
.messages_handler(|rx: DispatcherHandlerRx<Message>| {
rx.for_each(|message| async move {
message.answer("pong").send().await.log_on_error().await;
message.answer_str("pong").await.log_on_error().await;
})
})
.dispatch_with_listener(

View file

@ -1,9 +1,9 @@
use crate::dispatching::{
dialogue::{
DialogueDispatcherHandler, DialogueDispatcherHandlerCx, DialogueStage,
GetChatId, InMemStorage, Storage,
DialogueDispatcherHandler, DialogueStage, DialogueWithCx, GetChatId,
InMemStorage, Storage,
},
DispatcherHandler, DispatcherHandlerCx,
DispatcherHandler, UpdateWithCx,
};
use std::{convert::Infallible, marker::PhantomData};
@ -34,7 +34,7 @@ pub struct DialogueDispatcher<D, S, H, Upd> {
/// A value is the TX part of an unbounded asynchronous MPSC channel. A
/// handler that executes updates from the same chat ID sequentially
/// handles the RX part.
senders: Arc<Map<i64, mpsc::UnboundedSender<DispatcherHandlerCx<Upd>>>>,
senders: Arc<Map<i64, mpsc::UnboundedSender<UpdateWithCx<Upd>>>>,
}
impl<D, H, Upd> DialogueDispatcher<D, InMemStorage<D>, H, Upd>
@ -78,14 +78,14 @@ where
}
#[must_use]
fn new_tx(&self) -> mpsc::UnboundedSender<DispatcherHandlerCx<Upd>> {
fn new_tx(&self) -> mpsc::UnboundedSender<UpdateWithCx<Upd>> {
let (tx, rx) = mpsc::unbounded_channel();
let storage = Arc::clone(&self.storage);
let handler = Arc::clone(&self.handler);
let senders = Arc::clone(&self.senders);
tokio::spawn(rx.for_each(move |cx: DispatcherHandlerCx<Upd>| {
tokio::spawn(rx.for_each(move |cx: UpdateWithCx<Upd>| {
let storage = Arc::clone(&storage);
let handler = Arc::clone(&handler);
let senders = Arc::clone(&senders);
@ -98,14 +98,7 @@ where
.await
.map(Option::unwrap_or_default);
match handler
.handle(DialogueDispatcherHandlerCx {
bot: cx.bot,
update: cx.update,
dialogue,
})
.await
{
match handler.handle(DialogueWithCx { cx, dialogue }).await {
DialogueStage::Next(new_dialogue) => {
if let Ok(Some(_)) =
storage.update_dialogue(chat_id, new_dialogue).await
@ -144,10 +137,10 @@ where
{
fn handle(
self,
updates: mpsc::UnboundedReceiver<DispatcherHandlerCx<Upd>>,
updates: mpsc::UnboundedReceiver<UpdateWithCx<Upd>>,
) -> BoxFuture<'static, ()>
where
DispatcherHandlerCx<Upd>: 'static,
UpdateWithCx<Upd>: 'static,
{
let this = Arc::new(self);
@ -221,10 +214,10 @@ mod tests {
}
let dispatcher = DialogueDispatcher::new(
|cx: DialogueDispatcherHandlerCx<MyUpdate, (), Infallible>| async move {
|cx: DialogueWithCx<MyUpdate, (), Infallible>| async move {
delay_for(Duration::from_millis(300)).await;
match cx.update {
match cx.cx.update {
MyUpdate { chat_id: 1, unique_number } => {
SEQ1.lock().await.push(unique_number);
}
@ -266,11 +259,11 @@ mod tests {
MyUpdate::new(3, 1611),
]
.into_iter()
.map(|update| DispatcherHandlerCx {
.map(|update| UpdateWithCx {
update,
bot: Bot::new("Doesn't matter here"),
})
.collect::<Vec<DispatcherHandlerCx<MyUpdate>>>(),
.collect::<Vec<UpdateWithCx<MyUpdate>>>(),
);
let (tx, rx) = mpsc::unbounded_channel();

View file

@ -1,4 +1,4 @@
use crate::prelude::{DialogueDispatcherHandlerCx, DialogueStage};
use crate::prelude::{DialogueStage, DialogueWithCx};
use futures::future::BoxFuture;
use std::{future::Future, sync::Arc};
@ -12,26 +12,23 @@ pub trait DialogueDispatcherHandler<Upd, D, E> {
#[must_use]
fn handle(
self: Arc<Self>,
cx: DialogueDispatcherHandlerCx<Upd, D, E>,
cx: DialogueWithCx<Upd, D, E>,
) -> BoxFuture<'static, DialogueStage<D>>
where
DialogueDispatcherHandlerCx<Upd, D, E>: Send + 'static;
DialogueWithCx<Upd, D, E>: Send + 'static;
}
impl<Upd, D, E, F, Fut> DialogueDispatcherHandler<Upd, D, E> for F
where
F: Fn(DialogueDispatcherHandlerCx<Upd, D, E>) -> Fut
+ Send
+ Sync
+ 'static,
F: Fn(DialogueWithCx<Upd, D, E>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = DialogueStage<D>> + Send + 'static,
{
fn handle(
self: Arc<Self>,
cx: DialogueDispatcherHandlerCx<Upd, D, E>,
cx: DialogueWithCx<Upd, D, E>,
) -> BoxFuture<'static, Fut::Output>
where
DialogueDispatcherHandlerCx<Upd, D, E>: Send + 'static,
DialogueWithCx<Upd, D, E>: Send + 'static,
{
Box::pin(async move { self(cx).await })
}

View file

@ -1,186 +0,0 @@
use crate::{
dispatching::dialogue::GetChatId,
requests::{
DeleteMessage, EditMessageCaption, EditMessageText, ForwardMessage,
PinChatMessage, SendAnimation, SendAudio, SendContact, SendDocument,
SendLocation, SendMediaGroup, SendMessage, SendPhoto, SendSticker,
SendVenue, SendVideo, SendVideoNote, SendVoice,
},
types::{ChatId, ChatOrInlineMessage, InputFile, InputMedia, Message},
Bot,
};
use std::sync::Arc;
/// A context of a [`DialogueDispatcher`]'s message handler.
///
/// See [the module-level documentation for the design
/// overview](crate::dispatching::dialogue).
///
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
#[derive(Debug)]
pub struct DialogueDispatcherHandlerCx<Upd, D, E> {
pub bot: Arc<Bot>,
pub update: Upd,
pub dialogue: Result<D, E>,
}
impl<Upd, D, E> DialogueDispatcherHandlerCx<Upd, D, E> {
/// Creates a new instance with the provided fields.
pub fn new(bot: Arc<Bot>, update: Upd, dialogue: D) -> Self {
Self { bot, update, dialogue: Ok(dialogue) }
}
/// Creates a new instance by substituting a dialogue and preserving
/// `self.bot` and `self.update`.
pub fn with_new_dialogue<Nd, Ne>(
self,
new_dialogue: Result<Nd, Ne>,
) -> DialogueDispatcherHandlerCx<Upd, Nd, Ne> {
DialogueDispatcherHandlerCx {
bot: self.bot,
update: self.update,
dialogue: new_dialogue,
}
}
}
impl<Upd, D, E> GetChatId for DialogueDispatcherHandlerCx<Upd, D, E>
where
Upd: GetChatId,
{
fn chat_id(&self) -> i64 {
self.update.chat_id()
}
}
impl<D, E> DialogueDispatcherHandlerCx<Message, D, E> {
pub fn answer<T>(&self, text: T) -> SendMessage
where
T: Into<String>,
{
self.bot.send_message(self.chat_id(), text)
}
pub fn reply_to<T>(&self, text: T) -> SendMessage
where
T: Into<String>,
{
self.bot
.send_message(self.chat_id(), text)
.reply_to_message_id(self.update.id)
}
pub fn answer_photo(&self, photo: InputFile) -> SendPhoto {
self.bot.send_photo(self.update.chat.id, photo)
}
pub fn answer_audio(&self, audio: InputFile) -> SendAudio {
self.bot.send_audio(self.update.chat.id, audio)
}
pub fn answer_animation(&self, animation: InputFile) -> SendAnimation {
self.bot.send_animation(self.update.chat.id, animation)
}
pub fn answer_document(&self, document: InputFile) -> SendDocument {
self.bot.send_document(self.update.chat.id, document)
}
pub fn answer_video(&self, video: InputFile) -> SendVideo {
self.bot.send_video(self.update.chat.id, video)
}
pub fn answer_voice(&self, voice: InputFile) -> SendVoice {
self.bot.send_voice(self.update.chat.id, voice)
}
pub fn answer_media_group<T>(&self, media_group: T) -> SendMediaGroup
where
T: Into<Vec<InputMedia>>,
{
self.bot.send_media_group(self.update.chat.id, media_group)
}
pub fn answer_location(
&self,
latitude: f32,
longitude: f32,
) -> SendLocation {
self.bot.send_location(self.update.chat.id, latitude, longitude)
}
pub fn answer_venue<T, U>(
&self,
latitude: f32,
longitude: f32,
title: T,
address: U,
) -> SendVenue
where
T: Into<String>,
U: Into<String>,
{
self.bot.send_venue(
self.update.chat.id,
latitude,
longitude,
title,
address,
)
}
pub fn answer_video_note(&self, video_note: InputFile) -> SendVideoNote {
self.bot.send_video_note(self.update.chat.id, video_note)
}
pub fn answer_contact<T, U>(
&self,
phone_number: T,
first_name: U,
) -> SendContact
where
T: Into<String>,
U: Into<String>,
{
self.bot.send_contact(self.chat_id(), phone_number, first_name)
}
pub fn answer_sticker<T>(&self, sticker: InputFile) -> SendSticker {
self.bot.send_sticker(self.update.chat.id, sticker)
}
pub fn forward_to<T>(&self, chat_id: T) -> ForwardMessage
where
T: Into<ChatId>,
{
self.bot.forward_message(chat_id, self.update.chat.id, self.update.id)
}
pub fn edit_message_text<T>(&self, text: T) -> EditMessageText
where
T: Into<String>,
{
self.bot.edit_message_text(
ChatOrInlineMessage::Chat {
chat_id: self.update.chat.id.into(),
message_id: self.update.id,
},
text,
)
}
pub fn edit_message_caption(&self) -> EditMessageCaption {
self.bot.edit_message_caption(ChatOrInlineMessage::Chat {
chat_id: self.update.chat.id.into(),
message_id: self.update.id,
})
}
pub fn delete_message(&self) -> DeleteMessage {
self.bot.delete_message(self.update.chat.id, self.update.id)
}
pub fn pin_message(&self) -> PinChatMessage {
self.bot.pin_chat_message(self.update.chat.id, self.update.id)
}
}

View file

@ -1,3 +1,6 @@
use crate::dispatching::dialogue::TransitionOut;
use frunk::coproduct::CoprodInjector;
/// Continue or terminate a dialogue.
///
/// See [the module-level documentation for the design
@ -8,18 +11,29 @@ pub enum DialogueStage<D> {
Exit,
}
/// A shortcut for `Ok(DialogueStage::Next(dialogue))`.
///
/// See [the module-level documentation for the design
/// overview](crate::dispatching::dialogue).
pub fn next<E, D>(dialogue: D) -> Result<DialogueStage<D>, E> {
Ok(DialogueStage::Next(dialogue))
/// A dialogue wrapper to bypass orphan rules.
pub trait DialogueWrapper<D> {
fn new(dialogue: D) -> Self;
}
/// A shortcut for `Ok(DialogueStage::Exit)`.
/// Returns a new dialogue state.
///
/// See [the module-level documentation for the design
/// overview](crate::dispatching::dialogue).
pub fn exit<E, D>() -> Result<DialogueStage<D>, E> {
pub fn next<Dialogue, State, Index, DWrapper>(
new_state: State,
) -> TransitionOut<DWrapper>
where
Dialogue: CoprodInjector<State, Index>,
DWrapper: DialogueWrapper<Dialogue>,
{
Ok(DialogueStage::Next(DWrapper::new(Dialogue::inject(new_state))))
}
/// Exits a dialogue.
///
/// See [the module-level documentation for the design
/// overview](crate::dispatching::dialogue).
pub fn exit<DWrapper>() -> TransitionOut<DWrapper> {
Ok(DialogueStage::Exit)
}

View file

@ -0,0 +1,49 @@
use crate::dispatching::{dialogue::GetChatId, UpdateWithCx};
use std::fmt::Debug;
/// A context of a [`DialogueDispatcher`]'s message handler.
///
/// See [the module-level documentation for the design
/// overview](crate::dispatching::dialogue).
///
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
#[derive(Debug)]
pub struct DialogueWithCx<Upd, D, E> {
pub cx: UpdateWithCx<Upd>,
pub dialogue: Result<D, E>,
}
impl<Upd, D, E> DialogueWithCx<Upd, D, E> {
/// Returns the inner `UpdateWithCx<Upd>` and an unwrapped dialogue.
pub fn unpack(self) -> (UpdateWithCx<Upd>, D)
where
E: Debug,
{
(self.cx, self.dialogue.unwrap())
}
}
impl<Upd, D, E> DialogueWithCx<Upd, D, E> {
/// Creates a new instance with the provided fields.
pub fn new(cx: UpdateWithCx<Upd>, dialogue: D) -> Self {
Self { cx, dialogue: Ok(dialogue) }
}
/// Creates a new instance by substituting a dialogue and preserving
/// `self.bot` and `self.update`.
pub fn with_new_dialogue<Nd, Ne>(
self,
new_dialogue: Result<Nd, Ne>,
) -> DialogueWithCx<Upd, Nd, Ne> {
DialogueWithCx { cx: self.cx, dialogue: new_dialogue }
}
}
impl<Upd, D, E> GetChatId for DialogueWithCx<Upd, D, E>
where
Upd: GetChatId,
{
fn chat_id(&self) -> i64 {
self.cx.update.chat_id()
}
}

View file

@ -36,22 +36,195 @@
//! [`Dispatcher::messages_handler`]:
//! crate::dispatching::Dispatcher::messages_handler
//! [`UpdateKind::Message(message)`]: crate::types::UpdateKind::Message
//! [`DialogueDispatcherHandlerCx<YourUpdate, D>`]:
//! crate::dispatching::dialogue::DialogueDispatcherHandlerCx
//! [`DialogueWithCx<YourUpdate, D>`]:
//! crate::dispatching::dialogue::DialogueWithCx
//! [examples/dialogue_bot]: https://github.com/teloxide/teloxide/tree/master/examples/dialogue_bot
#![allow(clippy::type_complexity)]
mod dialogue_dispatcher;
mod dialogue_dispatcher_handler;
mod dialogue_dispatcher_handler_cx;
mod dialogue_stage;
mod dialogue_with_cx;
mod get_chat_id;
mod storage;
use crate::{requests::ResponseResult, types::Message};
pub use dialogue_dispatcher::DialogueDispatcher;
pub use dialogue_dispatcher_handler::DialogueDispatcherHandler;
pub use dialogue_dispatcher_handler_cx::DialogueDispatcherHandlerCx;
pub use dialogue_stage::{exit, next, DialogueStage};
pub use dialogue_stage::{exit, next, DialogueStage, DialogueWrapper};
pub use dialogue_with_cx::DialogueWithCx;
pub use get_chat_id::GetChatId;
pub use storage::{InMemStorage, Storage};
/// Dispatches a dialogue state into transition functions.
///
/// # Example
/// ```no_run
/// use teloxide::prelude::*;
///
/// pub struct StartState;
/// pub struct ReceiveWordState;
/// pub struct ReceiveNumberState;
/// pub struct ExitState;
///
/// pub type Dialogue = Coprod!(
/// StartState,
/// ReceiveWordState,
/// ReceiveNumberState,
/// );
///
/// wrap_dialogue!(
/// Wrapper(Dialogue),
/// default Self(Dialogue::inject(StartState)),
/// );
///
/// pub type In<State> = TransitionIn<State, std::convert::Infallible>;
/// pub type Out = TransitionOut<Wrapper>;
///
/// pub async fn start(cx: In<StartState>) -> Out { todo!() }
/// pub async fn receive_word(cx: In<ReceiveWordState>) -> Out { todo!() }
/// pub async fn receive_number(cx: In<ReceiveNumberState>) -> Out { todo!() }
///
/// # #[tokio::main]
/// # async fn main() {
/// let cx: In<Dialogue> = todo!();
/// let (cx, dialogue) = cx.unpack();
///
/// // StartState -> start
/// // ReceiveWordState -> receive_word
/// // ReceiveNumberState -> receive_number
/// let stage = dispatch!(
/// [cx, dialogue] ->
/// [start, receive_word, receive_number]
/// );
/// # }
/// ```
#[macro_export]
macro_rules! dispatch {
([$cx:ident, $dialogue:ident] -> [$transition:ident, $($transitions:ident),+]) => {
match $dialogue {
Coproduct::Inl(state) => {
$transition(teloxide::dispatching::dialogue::DialogueWithCx::new($cx, state)).await
}
Coproduct::Inr(another) => { dispatch!([$cx, another] -> [$($transitions),+]) }
}
};
([$cx:ident, $dialogue:ident] -> [$transition:ident]) => {
match $dialogue {
Coproduct::Inl(state) => {
$transition(teloxide::dispatching::dialogue::DialogueWithCx::new($cx, state)).await
}
Coproduct::Inr(_absurd) => unreachable!(),
}
};
}
/// Generates a dialogue wrapper and implements `Default` for it.
///
/// The reason is to bypass orphan rules to be able to pass a user-defined
/// dialogue into [`DialogueDispatcher`]. Since a dialogue is
/// [`frunk::Coproduct`], we cannot directly satisfy the `D: Default`
/// constraint.
///
/// # Examples
/// ```
/// use teloxide::prelude::*;
///
/// struct StartState;
/// struct ReceiveWordState;
/// struct ReceiveNumberState;
/// struct ExitState;
///
/// type Dialogue = Coprod!(
/// StartState,
/// ReceiveWordState,
/// ReceiveNumberState,
/// );
///
/// wrap_dialogue!(
/// Wrapper(Dialogue),
/// default Self(Dialogue::inject(StartState)),
/// );
///
/// let start_state = Wrapper::default();
/// ```
///
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
/// [`frunk::Coproduct`]: https://docs.rs/frunk/0.3.1/frunk/coproduct/enum.Coproduct.html
#[macro_export]
macro_rules! wrap_dialogue {
($name:ident($dialogue:ident), default $default_block:expr, ) => {
pub struct $name(pub $dialogue);
impl teloxide::dispatching::dialogue::DialogueWrapper<$dialogue>
for $name
{
fn new(d: $dialogue) -> Wrapper {
$name(d)
}
}
impl Default for $name {
fn default() -> $name {
$default_block
}
}
};
}
/// Generates `.up(field)` methods for dialogue states.
///
/// Given inductively defined states, this macro generates `.up(field)` methods
/// from `Sn` to `Sn+1`.
///
/// # Examples
/// ```
/// use teloxide::prelude::*;
///
/// struct StartState;
///
/// struct ReceiveWordState {
/// rest: StartState,
/// }
///
/// struct ReceiveNumberState {
/// rest: ReceiveWordState,
/// word: String,
/// }
///
/// struct ExitState {
/// rest: ReceiveNumberState,
/// number: i32,
/// }
///
/// up!(
/// StartState -> ReceiveWordState,
/// ReceiveWordState + [word: String] -> ReceiveNumberState,
/// ReceiveNumberState + [number: i32] -> ExitState,
/// );
///
/// let start_state = StartState;
/// let receive_word_state = start_state.up();
/// let receive_number_state = receive_word_state.up("Hello".to_owned());
/// let exit_state = receive_number_state.up(123);
/// ```
#[macro_export]
macro_rules! up {
( $( $from:ident $(+ [$field_name:ident : $field_type:ty])? -> $to:ident ),+, ) => {
$(
impl $from {
pub fn up(self, $( $field_name: $field_type )?) -> $to {
$to { rest: self, $($field_name)? }
}
}
)+
};
}
/// A type passed into a FSM transition function.
pub type TransitionIn<State, E> = DialogueWithCx<Message, State, E>;
// A type returned from a FSM transition function.
pub type TransitionOut<DWrapper> = ResponseResult<DialogueStage<DWrapper>>;

View file

@ -1,7 +1,7 @@
use crate::{
dispatching::{
update_listeners, update_listeners::UpdateListener, DispatcherHandler,
DispatcherHandlerCx,
UpdateWithCx,
},
error_handlers::{ErrorHandler, LoggingErrorHandler},
types::{
@ -14,7 +14,7 @@ use futures::StreamExt;
use std::{fmt::Debug, sync::Arc};
use tokio::sync::mpsc;
type Tx<Upd> = Option<mpsc::UnboundedSender<DispatcherHandlerCx<Upd>>>;
type Tx<Upd> = Option<mpsc::UnboundedSender<UpdateWithCx<Upd>>>;
#[macro_use]
mod macros {
@ -36,7 +36,7 @@ fn send<'a, Upd>(
{
if let Some(tx) = tx {
if let Err(error) =
tx.send(DispatcherHandlerCx { bot: Arc::clone(&bot), update })
tx.send(UpdateWithCx { bot: Arc::clone(&bot), update })
{
log::error!(
"The RX part of the {} channel is closed, but an update is \

View file

@ -1,6 +1,6 @@
use std::future::Future;
use crate::dispatching::{DispatcherHandlerCx, DispatcherHandlerRx};
use crate::dispatching::{DispatcherHandlerRx, UpdateWithCx};
use futures::future::BoxFuture;
/// An asynchronous handler of a stream of updates used in [`Dispatcher`].
@ -16,7 +16,7 @@ pub trait DispatcherHandler<Upd> {
updates: DispatcherHandlerRx<Upd>,
) -> BoxFuture<'static, ()>
where
DispatcherHandlerCx<Upd>: Send + 'static;
UpdateWithCx<Upd>: Send + 'static;
}
impl<Upd, F, Fut> DispatcherHandler<Upd> for F
@ -26,7 +26,7 @@ where
{
fn handle(self, updates: DispatcherHandlerRx<Upd>) -> BoxFuture<'static, ()>
where
DispatcherHandlerCx<Upd>: Send + 'static,
UpdateWithCx<Upd>: Send + 'static,
{
Box::pin(async move { self(updates).await })
}

View file

@ -1,5 +1,5 @@
use crate::{
prelude::DispatcherHandlerCx, types::Message, utils::command::BotCommand,
prelude::UpdateWithCx, types::Message, utils::command::BotCommand,
};
use futures::{stream::BoxStream, Stream, StreamExt};
@ -10,18 +10,18 @@ pub trait DispatcherHandlerRxExt {
/// Extracts only text messages from this stream of arbitrary messages.
fn text_messages(
self,
) -> BoxStream<'static, (DispatcherHandlerCx<Message>, String)>
) -> BoxStream<'static, (UpdateWithCx<Message>, String)>
where
Self: Stream<Item = DispatcherHandlerCx<Message>>;
Self: Stream<Item = UpdateWithCx<Message>>;
/// Extracts only commands with their arguments from this stream of
/// arbitrary messages.
fn commands<C, N>(
self,
bot_name: N,
) -> BoxStream<'static, (DispatcherHandlerCx<Message>, C, Vec<String>)>
) -> BoxStream<'static, (UpdateWithCx<Message>, C, Vec<String>)>
where
Self: Stream<Item = DispatcherHandlerCx<Message>>,
Self: Stream<Item = UpdateWithCx<Message>>,
C: BotCommand,
N: Into<String> + Send;
}
@ -32,9 +32,9 @@ where
{
fn text_messages(
self,
) -> BoxStream<'static, (DispatcherHandlerCx<Message>, String)>
) -> BoxStream<'static, (UpdateWithCx<Message>, String)>
where
Self: Stream<Item = DispatcherHandlerCx<Message>>,
Self: Stream<Item = UpdateWithCx<Message>>,
{
Box::pin(self.filter_map(|cx| async move {
cx.update.text_owned().map(|text| (cx, text))
@ -44,9 +44,9 @@ where
fn commands<C, N>(
self,
bot_name: N,
) -> BoxStream<'static, (DispatcherHandlerCx<Message>, C, Vec<String>)>
) -> BoxStream<'static, (UpdateWithCx<Message>, C, Vec<String>)>
where
Self: Stream<Item = DispatcherHandlerCx<Message>>,
Self: Stream<Item = UpdateWithCx<Message>>,
C: BotCommand,
N: Into<String> + Send,
{

View file

@ -80,17 +80,17 @@
pub mod dialogue;
mod dispatcher;
mod dispatcher_handler;
mod dispatcher_handler_cx;
mod dispatcher_handler_rx_ext;
pub mod update_listeners;
mod update_with_cx;
pub use dispatcher::Dispatcher;
pub use dispatcher_handler::DispatcherHandler;
pub use dispatcher_handler_cx::DispatcherHandlerCx;
pub use dispatcher_handler_rx_ext::DispatcherHandlerRxExt;
use tokio::sync::mpsc::UnboundedReceiver;
pub use update_with_cx::UpdateWithCx;
/// A type of a stream, consumed by [`Dispatcher`]'s handlers.
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
pub type DispatcherHandlerRx<Upd> = UnboundedReceiver<DispatcherHandlerCx<Upd>>;
pub type DispatcherHandlerRx<Upd> = UnboundedReceiver<UpdateWithCx<Upd>>;

View file

@ -2,9 +2,9 @@ use crate::{
dispatching::dialogue::GetChatId,
requests::{
DeleteMessage, EditMessageCaption, EditMessageText, ForwardMessage,
PinChatMessage, SendAnimation, SendAudio, SendContact, SendDocument,
SendLocation, SendMediaGroup, SendMessage, SendPhoto, SendSticker,
SendVenue, SendVideo, SendVideoNote, SendVoice,
PinChatMessage, Request, ResponseResult, SendAnimation, SendAudio,
SendContact, SendDocument, SendLocation, SendMediaGroup, SendMessage,
SendPhoto, SendSticker, SendVenue, SendVideo, SendVideoNote, SendVoice,
},
types::{ChatId, ChatOrInlineMessage, InputFile, InputMedia, Message},
Bot,
@ -18,12 +18,12 @@ use std::sync::Arc;
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
#[derive(Debug)]
pub struct DispatcherHandlerCx<Upd> {
pub struct UpdateWithCx<Upd> {
pub bot: Arc<Bot>,
pub update: Upd,
}
impl<Upd> GetChatId for DispatcherHandlerCx<Upd>
impl<Upd> GetChatId for UpdateWithCx<Upd>
where
Upd: GetChatId,
{
@ -32,7 +32,14 @@ where
}
}
impl DispatcherHandlerCx<Message> {
impl UpdateWithCx<Message> {
pub async fn answer_str<T>(&self, text: T) -> ResponseResult<Message>
where
T: Into<String>,
{
self.answer(text).send().await
}
pub fn answer<T>(&self, text: T) -> SendMessage
where
T: Into<String>,

View file

@ -1,20 +1,21 @@
//! Commonly used items.
pub use crate::{
dispatch,
dispatching::{
dialogue::{
exit, next, DialogueDispatcher, DialogueDispatcherHandlerCx,
DialogueStage, GetChatId,
exit, next, DialogueDispatcher, DialogueStage, DialogueWithCx,
DialogueWrapper, GetChatId, TransitionIn, TransitionOut,
},
Dispatcher, DispatcherHandlerCx, DispatcherHandlerRx,
DispatcherHandlerRxExt,
Dispatcher, DispatcherHandlerRx, DispatcherHandlerRxExt, UpdateWithCx,
},
error_handlers::{LoggingErrorHandler, OnError},
requests::{Request, ResponseResult},
types::{Message, Update},
Bot, RequestError,
up, wrap_dialogue, Bot, RequestError,
};
pub use frunk::{Coprod, Coproduct};
pub use tokio::sync::mpsc::UnboundedReceiver;
pub use futures::StreamExt;