This commit is contained in:
Temirkhan Myrzamadi 2020-02-19 04:54:41 +06:00
parent f20932a730
commit fa554a8252
58 changed files with 774 additions and 757 deletions

129
README.md
View file

@ -16,15 +16,15 @@
</div>
## Features
- **Type-safe.** teloxide leverages the Rust's type system with two serious implications: resistance to human mistakes and tight integration with IDEs. Write fast, avoid debugging as possible.
- **Type-safe.** teloxide leverages the Rust's type system with two serious implications: resistance to human mistakes and tight integration with IDEs. Write fast, avoid debugging as much as possible.
- **Flexible API.** teloxide gives you the power of [streams](https://docs.rs/futures/0.3.4/futures/stream/index.html): you can combine [all 30+ patterns](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html) when working with updates from Telegram.
- **Persistency.** By default, teloxide stores all user dialogues in RAM, but you can store them somewhere else (for example, in DB) just by implementing 2 functions.
- **Convenient dialogues system.** Define a type-safe [finite automaton](https://en.wikipedia.org/wiki/Finite-state_machine)
and transition functions to drive a user dialogue with ease (see the examples below).
and transition functions to drive a user dialogue with ease (see [the guess-a-number example](#guess-a-number) below).
- **Convenient API.** Automatic conversions are used to avoid boilerplate. For example, functions accept `Into<String>`, rather than `&str` or `String`, so you can call them without `.to_string()`/`.as_str()`/etc.
## 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:
@ -45,6 +45,7 @@ $ rustup update stable
[dependencies]
teloxide = "0.1.0"
log = "0.4.8"
futures = "0.3.4"
tokio = "0.2.11"
pretty_env_logger = "0.4.0"
```
@ -59,22 +60,24 @@ use teloxide::prelude::*;
#[tokio::main]
async fn main() {
teloxide::enable_logging!();
log::info!("Starting the ping-pong bot!");
log::info!("Starting ping_pong_bot!");
let bot = Bot::from_env();
Dispatcher::<RequestError>::new(bot)
.message_handler(&|ctx: DispatcherHandlerCtx<Message>| async move {
ctx.answer("pong").send().await?;
Ok(())
Dispatcher::new(bot)
.messages_handler(|rx: DispatcherHandlerRx<Message>| {
rx.for_each(|message| async move {
message.answer("pong").send().await.log_on_error().await;
})
})
.dispatch()
.await;
}
```
<details>
<summary>Run this!</summary>
<summary>Click here to run it!</summary>
```bash
git clone https://github.com/teloxide/teloxide.git
@ -106,47 +109,44 @@ enum Command {
Generate,
}
async fn handle_command(
ctx: DispatcherHandlerCtx<Message>,
) -> Result<(), RequestError> {
let text = match ctx.update.text() {
Some(text) => text,
None => {
log::info!("Received a message, but not text.");
return Ok(());
}
};
let command = match Command::parse(text) {
Some((command, _)) => command,
None => {
log::info!("Received a text message, but not a command.");
return Ok(());
}
};
fn generate() -> String {
thread_rng().gen_range(0.0, 1.0).to_string()
}
async fn answer(
cx: DispatcherHandlerCx<Message>,
command: Command,
) -> ResponseResult<()> {
match command {
Command::Help => ctx.answer(Command::descriptions()).send().await?,
Command::Generate => {
ctx.answer(thread_rng().gen_range(0.0, 1.0).to_string())
.send()
.await?
}
Command::Meow => ctx.answer("I am a cat! Meow!").send().await?,
Command::Help => cx.answer(Command::descriptions()).send().await?,
Command::Generate => cx.answer(generate()).send().await?,
Command::Meow => cx.answer("I am a cat! Meow!").send().await?,
};
Ok(())
}
async fn handle_command(rx: DispatcherHandlerRx<Message>) {
rx.filter_map(|cx| {
future::ready(cx.update.text_owned().map(|text| (cx, text)))
})
.filter_map(|(cx, text)| {
future::ready(Command::parse(&text).map(|(command, _)| (cx, command)))
})
.for_each_concurrent(None, |(cx, command)| async move {
answer(cx, command).await.log_on_error().await;
})
.await;
}
#[tokio::main]
async fn main() {
// Setup is omitted...
}
```
<details>
<summary>Run this!</summary>
<summary>Click here to run it!</summary>
```bash
git clone https://github.com/teloxide/teloxide.git
@ -160,12 +160,24 @@ TELOXIDE_TOKEN=MyAwesomeToken cargo run
<img src=https://github.com/teloxide/teloxide/raw/master/media/SIMPLE_COMMANDS_BOT.png width="400" />
</div>
See? The dispatcher gives us a stream of messages, so we can handle it as we want! Here we use [`.filter_map()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.filter_map) and [`.for_each_concurrent()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.for_each_concurrent), but others are also available:
- [`.flatten()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.flatten)
- [`.left_stream()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.left_stream)
- [`.scan()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.scan)
- [`.skip_while()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.skip_while)
- [`.zip()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.zip)
- [`.select_next_some()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.select_next_some)
- [`.fold()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.fold)
- [`.inspect()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.inspect)
- ... And lots of [others](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.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):
([Full](https://github.com/teloxide/teloxide/blob/master/examples/guess_a_number_bot/src/main.rs))
```rust
// Imports are omitted...
// Setup is omitted...
#[derive(SmartDefault)]
enum Dialogue {
@ -175,51 +187,49 @@ enum Dialogue {
}
async fn handle_message(
ctx: DialogueHandlerCtx<Message, Dialogue>,
) -> Result<DialogueStage<Dialogue>, RequestError> {
match ctx.dialogue {
cx: DialogueDispatcherHandlerCx<Message, Dialogue>,
) -> ResponseResult<DialogueStage<Dialogue>> {
match cx.dialogue {
Dialogue::Start => {
ctx.answer(
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)))
}
Dialogue::ReceiveAttempt(secret) => match ctx.update.text() {
Dialogue::ReceiveAttempt(secret) => match cx.update.text() {
None => {
ctx.answer("Oh, please, send me a text message!")
.send()
.await?;
next(ctx.dialogue)
cx.answer("Oh, please, send me a text message!").send().await?;
next(cx.dialogue)
}
Some(text) => match text.parse::<u8>() {
Ok(attempt) => match attempt {
x if !(1..=10).contains(&x) => {
ctx.answer(
cx.answer(
"Oh, please, send me a number in the range [1; \
10]!",
)
.send()
.await?;
next(ctx.dialogue)
next(cx.dialogue)
}
x if x == secret => {
ctx.answer("Congratulations! You won!").send().await?;
cx.answer("Congratulations! You won!").send().await?;
exit()
}
_ => {
ctx.answer("No.").send().await?;
next(ctx.dialogue)
cx.answer("No.").send().await?;
next(cx.dialogue)
}
},
Err(_) => {
ctx.answer(
cx.answer(
"Oh, please, send me a number in the range [1; 10]!",
)
.send()
.await?;
next(ctx.dialogue)
next(cx.dialogue)
}
},
},
@ -229,20 +239,11 @@ async fn handle_message(
#[tokio::main]
async fn main() {
// Setup is omitted...
Dispatcher::new(bot)
.message_handler(&DialogueDispatcher::new(|ctx| async move {
handle_message(ctx)
.await
.expect("Something wrong with the bot!")
}))
.dispatch()
.await;
}
```
<details>
<summary>Run this!</summary>
<summary>Click here to run it!</summary>
```bash
git clone https://github.com/teloxide/teloxide.git

View file

@ -6,4 +6,4 @@ Just enter the directory (for example, `cd dialogue_bot`) and execute `cargo run
- [guess_a_number_bot](guess_a_number_bot) - The "guess a number" game.
- [dialogue_bot](dialogue_bot) - Drive a dialogue with a user using a type-safe finite automaton.
- [admin_bot](admin_bot) - A bot, which can ban, kick, and mute on a command.
- [multiple_handlers_bot](multiple_handlers_bot) - Shows how multiple dispatcher's handlers relate to each other.

View file

@ -8,6 +8,7 @@ edition = "2018"
[dependencies]
log = "0.4.8"
futures = "0.3.4"
tokio = "0.2.9"
pretty_env_logger = "0.4.0"
teloxide = { path = "../../" }

View file

@ -4,6 +4,8 @@ use teloxide::{
prelude::*, types::ChatPermissions, utils::command::BotCommand,
};
use futures::future;
// Derive BotCommand to parse text with a command into this enumeration.
//
// 1. rename = "lowercase" turns all the commands into lowercase letters.
@ -40,7 +42,7 @@ fn calc_restrict_time(num: i32, unit: &str) -> Result<i32, &str> {
}
// Parse arguments after a command.
fn parse_args(args: Vec<&str>) -> Result<(i32, &str), &str> {
fn parse_args(args: &[String]) -> Result<(i32, &str), &str> {
let num = match args.get(0) {
Some(s) => s,
None => return Err("Use command in format /%command% %num% %unit%"),
@ -57,34 +59,34 @@ fn parse_args(args: Vec<&str>) -> Result<(i32, &str), &str> {
}
// Parse arguments into a user restriction duration.
fn parse_time_restrict(args: Vec<&str>) -> Result<i32, &str> {
fn parse_time_restrict(args: &[String]) -> Result<i32, &str> {
let (num, unit) = parse_args(args)?;
calc_restrict_time(num, unit)
}
type Ctx = DispatcherHandlerCtx<Message>;
type Cx = DispatcherHandlerCx<Message>;
// Mute a user with a replied message.
async fn mute_user(ctx: &Ctx, args: Vec<&str>) -> Result<(), RequestError> {
match ctx.update.reply_to_message() {
async fn mute_user(cx: &Cx, args: &[String]) -> ResponseResult<()> {
match cx.update.reply_to_message() {
Some(msg1) => match parse_time_restrict(args) {
// Mute user temporarily...
Ok(time) => {
ctx.bot
cx.bot
.restrict_chat_member(
ctx.update.chat_id(),
cx.update.chat_id(),
msg1.from().expect("Must be MessageKind::Common").id,
ChatPermissions::default(),
)
.until_date(ctx.update.date + time)
.until_date(cx.update.date + time)
.send()
.await?;
}
// ...or permanently
Err(_) => {
ctx.bot
cx.bot
.restrict_chat_member(
ctx.update.chat_id(),
cx.update.chat_id(),
msg1.from().unwrap().id,
ChatPermissions::default(),
)
@ -93,7 +95,7 @@ async fn mute_user(ctx: &Ctx, args: Vec<&str>) -> Result<(), RequestError> {
}
},
None => {
ctx.reply_to("Use this command in reply to another message")
cx.reply_to("Use this command in reply to another message")
.send()
.await?;
}
@ -102,17 +104,17 @@ async fn mute_user(ctx: &Ctx, args: Vec<&str>) -> Result<(), RequestError> {
}
// Kick a user with a replied message.
async fn kick_user(ctx: &Ctx) -> Result<(), RequestError> {
match ctx.update.reply_to_message() {
async fn kick_user(cx: &Cx) -> ResponseResult<()> {
match cx.update.reply_to_message() {
Some(mes) => {
// bot.unban_chat_member can also kicks a user from a group chat.
ctx.bot
.unban_chat_member(ctx.update.chat_id(), mes.from().unwrap().id)
cx.bot
.unban_chat_member(cx.update.chat_id(), mes.from().unwrap().id)
.send()
.await?;
}
None => {
ctx.reply_to("Use this command in reply to another message")
cx.reply_to("Use this command in reply to another message")
.send()
.await?;
}
@ -121,25 +123,25 @@ async fn kick_user(ctx: &Ctx) -> Result<(), RequestError> {
}
// Ban a user with replied message.
async fn ban_user(ctx: &Ctx, args: Vec<&str>) -> Result<(), RequestError> {
match ctx.update.reply_to_message() {
async fn ban_user(cx: &Cx, args: &[String]) -> ResponseResult<()> {
match cx.update.reply_to_message() {
Some(message) => match parse_time_restrict(args) {
// Mute user temporarily...
Ok(time) => {
ctx.bot
cx.bot
.kick_chat_member(
ctx.update.chat_id(),
cx.update.chat_id(),
message.from().expect("Must be MessageKind::Common").id,
)
.until_date(ctx.update.date + time)
.until_date(cx.update.date + time)
.send()
.await?;
}
// ...or permanently
Err(_) => {
ctx.bot
cx.bot
.kick_chat_member(
ctx.update.chat_id(),
cx.update.chat_id(),
message.from().unwrap().id,
)
.send()
@ -147,7 +149,7 @@ async fn ban_user(ctx: &Ctx, args: Vec<&str>) -> Result<(), RequestError> {
}
},
None => {
ctx.reply_to("Use this command in a reply to another message!")
cx.reply_to("Use this command in a reply to another message!")
.send()
.await?;
}
@ -155,49 +157,56 @@ async fn ban_user(ctx: &Ctx, args: Vec<&str>) -> Result<(), RequestError> {
Ok(())
}
// Handle all messages.
async fn handle_command(ctx: Ctx) -> Result<(), RequestError> {
if ctx.update.chat.is_group() {
// The same as DispatcherHandlerResult::exit(Ok(())). If you have more
// handlers, use DispatcherHandlerResult::next(...)
return Ok(());
}
if let Some(text) = ctx.update.text() {
// Parse text into a command with args.
let (command, args): (Command, Vec<&str>) = match Command::parse(text) {
Some(tuple) => tuple,
None => return Ok(()),
};
match command {
Command::Help => {
ctx.answer(Command::descriptions()).send().await?;
}
Command::Kick => {
kick_user(&ctx).await?;
}
Command::Ban => {
ban_user(&ctx, args).await?;
}
Command::Mute => {
mute_user(&ctx, args).await?;
}
};
}
async fn action(
cx: DispatcherHandlerCx<Message>,
command: Command,
args: &[String],
) -> ResponseResult<()> {
match command {
Command::Help => {
cx.answer(Command::descriptions()).send().await.map(|_| ())?
}
Command::Kick => kick_user(&cx).await?,
Command::Ban => ban_user(&cx, args).await?,
Command::Mute => mute_user(&cx, args).await?,
};
Ok(())
}
// Handle all messages.
async fn handle_commands(rx: DispatcherHandlerRx<Message>) {
rx.filter(|cx| future::ready(!cx.update.chat.is_group()))
.filter_map(|cx| {
future::ready(cx.update.text_owned().map(|text| (cx, text)))
})
.filter_map(|(cx, text)| {
future::ready(Command::parse(&text).map(|(command, args)| {
(
cx,
command,
args.into_iter()
.map(ToOwned::to_owned)
.collect::<Vec<String>>(),
)
}))
})
.for_each_concurrent(None, |(cx, command, args)| async move {
action(cx, command, &args).await.log_on_error().await;
})
.await;
}
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting admin_bot!");
let bot = Bot::from_env();
Dispatcher::new(bot)
.message_handler(&handle_command)
.dispatch()
.await
Dispatcher::new(bot).messages_handler(handle_commands).dispatch().await
}

View file

@ -87,26 +87,22 @@ enum Dialogue {
// [Control a dialogue]
// ============================================================================
type Ctx<State> = DialogueHandlerCtx<Message, State>;
type Res = Result<DialogueStage<Dialogue>, RequestError>;
type Cx<State> = DialogueDispatcherHandlerCx<Message, State>;
type Res = ResponseResult<DialogueStage<Dialogue>>;
async fn start(ctx: Ctx<()>) -> Res {
ctx.answer("Let's start! First, what's your full name?")
.send()
.await?;
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(ctx: Ctx<()>) -> Res {
match ctx.update.text() {
async fn full_name(cx: Cx<()>) -> Res {
match cx.update.text() {
None => {
ctx.answer("Please, send me a text message!").send().await?;
cx.answer("Please, send me a text message!").send().await?;
next(Dialogue::ReceiveFullName)
}
Some(full_name) => {
ctx.answer("What a wonderful name! Your age?")
.send()
.await?;
cx.answer("What a wonderful name! Your age?").send().await?;
next(Dialogue::ReceiveAge(ReceiveAgeState {
full_name: full_name.to_owned(),
}))
@ -114,72 +110,68 @@ async fn full_name(ctx: Ctx<()>) -> Res {
}
}
async fn age(ctx: Ctx<ReceiveAgeState>) -> Res {
match ctx.update.text().unwrap().parse() {
async fn age(cx: Cx<ReceiveAgeState>) -> Res {
match cx.update.text().unwrap().parse() {
Ok(age) => {
ctx.answer("Good. Now choose your favourite music:")
cx.answer("Good. Now choose your favourite music:")
.reply_markup(FavouriteMusic::markup())
.send()
.await?;
next(Dialogue::ReceiveFavouriteMusic(
ReceiveFavouriteMusicState {
data: ctx.dialogue,
age,
},
))
next(Dialogue::ReceiveFavouriteMusic(ReceiveFavouriteMusicState {
data: cx.dialogue,
age,
}))
}
Err(_) => {
ctx.answer("Oh, please, enter a number!").send().await?;
next(Dialogue::ReceiveAge(ctx.dialogue))
cx.answer("Oh, please, enter a number!").send().await?;
next(Dialogue::ReceiveAge(cx.dialogue))
}
}
}
async fn favourite_music(ctx: Ctx<ReceiveFavouriteMusicState>) -> Res {
match ctx.update.text().unwrap().parse() {
async fn favourite_music(cx: Cx<ReceiveFavouriteMusicState>) -> Res {
match cx.update.text().unwrap().parse() {
Ok(favourite_music) => {
ctx.answer(format!(
cx.answer(format!(
"Fine. {}",
ExitState {
data: ctx.dialogue.clone(),
favourite_music
}
ExitState { data: cx.dialogue.clone(), favourite_music }
))
.send()
.await?;
exit()
}
Err(_) => {
ctx.answer("Oh, please, enter from the keyboard!")
.send()
.await?;
next(Dialogue::ReceiveFavouriteMusic(ctx.dialogue))
cx.answer("Oh, please, enter from the keyboard!").send().await?;
next(Dialogue::ReceiveFavouriteMusic(cx.dialogue))
}
}
}
async fn handle_message(ctx: Ctx<Dialogue>) -> Res {
match ctx {
DialogueHandlerCtx {
async fn handle_message(cx: Cx<Dialogue>) -> Res {
match cx {
DialogueDispatcherHandlerCx {
bot,
update,
dialogue: Dialogue::Start,
} => start(DialogueHandlerCtx::new(bot, update, ())).await,
DialogueHandlerCtx {
} => start(DialogueDispatcherHandlerCx::new(bot, update, ())).await,
DialogueDispatcherHandlerCx {
bot,
update,
dialogue: Dialogue::ReceiveFullName,
} => full_name(DialogueHandlerCtx::new(bot, update, ())).await,
DialogueHandlerCtx {
} => full_name(DialogueDispatcherHandlerCx::new(bot, update, ())).await,
DialogueDispatcherHandlerCx {
bot,
update,
dialogue: Dialogue::ReceiveAge(s),
} => age(DialogueHandlerCtx::new(bot, update, s)).await,
DialogueHandlerCtx {
} => age(DialogueDispatcherHandlerCx::new(bot, update, s)).await,
DialogueDispatcherHandlerCx {
bot,
update,
dialogue: Dialogue::ReceiveFavouriteMusic(s),
} => favourite_music(DialogueHandlerCtx::new(bot, update, s)).await,
} => {
favourite_music(DialogueDispatcherHandlerCx::new(bot, update, s))
.await
}
}
}
@ -199,10 +191,8 @@ async fn run() {
let bot = Bot::from_env();
Dispatcher::new(bot)
.message_handler(&DialogueDispatcher::new(|ctx| async move {
handle_message(ctx)
.await
.expect("Something wrong with the bot!")
.messages_handler(DialogueDispatcher::new(|cx| async move {
handle_message(cx).await.expect("Something wrong with the bot!")
}))
.dispatch()
.await;

View file

@ -39,51 +39,49 @@ enum Dialogue {
// ============================================================================
async fn handle_message(
ctx: DialogueHandlerCtx<Message, Dialogue>,
) -> Result<DialogueStage<Dialogue>, RequestError> {
match ctx.dialogue {
cx: DialogueDispatcherHandlerCx<Message, Dialogue>,
) -> ResponseResult<DialogueStage<Dialogue>> {
match cx.dialogue {
Dialogue::Start => {
ctx.answer(
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)))
}
Dialogue::ReceiveAttempt(secret) => match ctx.update.text() {
Dialogue::ReceiveAttempt(secret) => match cx.update.text() {
None => {
ctx.answer("Oh, please, send me a text message!")
.send()
.await?;
next(ctx.dialogue)
cx.answer("Oh, please, send me a text message!").send().await?;
next(cx.dialogue)
}
Some(text) => match text.parse::<u8>() {
Ok(attempt) => match attempt {
x if !(1..=10).contains(&x) => {
ctx.answer(
cx.answer(
"Oh, please, send me a number in the range [1; \
10]!",
)
.send()
.await?;
next(ctx.dialogue)
next(cx.dialogue)
}
x if x == secret => {
ctx.answer("Congratulations! You won!").send().await?;
cx.answer("Congratulations! You won!").send().await?;
exit()
}
_ => {
ctx.answer("No.").send().await?;
next(ctx.dialogue)
cx.answer("No.").send().await?;
next(cx.dialogue)
}
},
Err(_) => {
ctx.answer(
cx.answer(
"Oh, please, send me a number in the range [1; 10]!",
)
.send()
.await?;
next(ctx.dialogue)
next(cx.dialogue)
}
},
},
@ -106,10 +104,8 @@ async fn run() {
let bot = Bot::from_env();
Dispatcher::new(bot)
.message_handler(&DialogueDispatcher::new(|ctx| async move {
handle_message(ctx)
.await
.expect("Something wrong with the bot!")
.messages_handler(DialogueDispatcher::new(|cx| async move {
handle_message(cx).await.expect("Something wrong with the bot!")
}))
.dispatch()
.await;

View file

@ -1,13 +0,0 @@
[package]
name = "multiple_handlers_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"
pretty_env_logger = "0.4.0"
teloxide = { path = "../../" }

View file

@ -1,49 +0,0 @@
// This example demonstrates the ability of Dispatcher to deal with multiple
// handlers.
use teloxide::prelude::*;
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting multiple_handlers_bot!");
let bot = Bot::from_env();
// Create a dispatcher with multiple handlers of different types. This will
// print One! and Two! on every incoming UpdateKind::Message.
Dispatcher::<RequestError>::new(bot)
// This is the first UpdateKind::Message handler, which will be called
// after the Update handler below.
.message_handler(&|ctx: DispatcherHandlerCtx<Message>| async move {
log::info!("Two!");
DispatcherHandlerResult::next(ctx.update, Ok(()))
})
// Remember: handler of Update are called first.
.update_handler(&|ctx: DispatcherHandlerCtx<Update>| async move {
log::info!("One!");
DispatcherHandlerResult::next(ctx.update, Ok(()))
})
// This handler will be called right after the first UpdateKind::Message
// handler, because it is registered after.
.message_handler(&|_ctx: DispatcherHandlerCtx<Message>| async move {
// The same as DispatcherHandlerResult::exit(Ok(()))
Ok(())
})
// This handler will never be called, because the UpdateKind::Message
// handler above terminates the pipeline.
.message_handler(&|ctx: DispatcherHandlerCtx<Message>| async move {
log::info!("This will never be printed!");
DispatcherHandlerResult::next(ctx.update, Ok(()))
})
.dispatch()
.await;
// Note: if this bot receive, for example, UpdateKind::ChannelPost, it will
// only print "One!", because the UpdateKind::Message handlers will not be
// called.
}

View file

@ -1,3 +1,5 @@
// This bot just answers "pong" to each incoming UpdateKind::Message.
use teloxide::prelude::*;
#[tokio::main]
@ -12,13 +14,9 @@ async fn run() {
let bot = Bot::from_env();
Dispatcher::new(bot)
.messages_handler(|messages: DispatcherHandlerRx<Message>| {
messages.for_each_concurrent(None, |message| async move {
if let Err(error) = message.answer("pong").send().await {
let foo = LoggingErrorHandler::new("Cannot send");
foo.handle_error(error)
.await;
}
.messages_handler(|rx: DispatcherHandlerRx<Message>| {
rx.for_each(|message| async move {
message.answer("pong").send().await.log_on_error().await;
})
})
.dispatch()

View file

@ -8,6 +8,7 @@ edition = "2018"
[dependencies]
log = "0.4.8"
futures = "0.3.4"
tokio = "0.2.9"
rand = "0.7.3"
pretty_env_logger = "0.4.0"

View file

@ -1,5 +1,6 @@
use teloxide::{prelude::*, utils::command::BotCommand};
use futures::future;
use rand::{thread_rng, Rng};
#[derive(BotCommand)]
@ -13,38 +14,36 @@ enum Command {
Generate,
}
async fn handle_command(
ctx: DispatcherHandlerCtx<Message>,
) -> Result<(), RequestError> {
let text = match ctx.update.text() {
Some(text) => text,
None => {
log::info!("Received a message, but not text.");
return Ok(());
}
};
let command = match Command::parse(text) {
Some((command, _)) => command,
None => {
log::info!("Received a text message, but not a command.");
return Ok(());
}
};
fn generate() -> String {
thread_rng().gen_range(0.0, 1.0).to_string()
}
async fn answer(
cx: DispatcherHandlerCx<Message>,
command: Command,
) -> ResponseResult<()> {
match command {
Command::Help => ctx.answer(Command::descriptions()).send().await?,
Command::Generate => {
ctx.answer(thread_rng().gen_range(0.0, 1.0).to_string())
.send()
.await?
}
Command::Meow => ctx.answer("I am a cat! Meow!").send().await?,
Command::Help => cx.answer(Command::descriptions()).send().await?,
Command::Generate => cx.answer(generate()).send().await?,
Command::Meow => cx.answer("I am a cat! Meow!").send().await?,
};
Ok(())
}
async fn handle_command(rx: DispatcherHandlerRx<Message>) {
rx.filter_map(|cx| {
future::ready(cx.update.text_owned().map(|text| (cx, text)))
})
.filter_map(|(cx, text)| {
future::ready(Command::parse(&text).map(|(command, _)| (cx, command)))
})
.for_each_concurrent(None, |(cx, command)| async move {
answer(cx, command).await.log_on_error().await;
})
.await;
}
#[tokio::main]
async fn main() {
run().await;
@ -56,8 +55,5 @@ async fn run() {
let bot = Bot::from_env();
Dispatcher::<RequestError>::new(bot)
.message_handler(&handle_command)
.dispatch()
.await;
Dispatcher::new(bot).messages_handler(handle_command).dispatch().await;
}

View file

@ -2,4 +2,6 @@ format_code_in_doc_comments = true
wrap_comments = true
format_strings = true
max_width = 80
merge_imports = true
merge_imports = true
use_small_heuristics = "Max"
use_field_init_shorthand = true

View file

@ -60,10 +60,7 @@ impl Bot {
where
S: Into<String>,
{
Arc::new(Self {
token: token.into(),
client,
})
Arc::new(Self { token: token.into(), client })
}
}

View file

@ -1,9 +1,9 @@
use crate::dispatching::{
dialogue::{
DialogueDispatcherHandler, DialogueDispatcherHandlerCtx, DialogueStage,
DialogueDispatcherHandler, DialogueDispatcherHandlerCx, DialogueStage,
GetChatId, InMemStorage, Storage,
},
DispatcherHandler, DispatcherHandlerCtx,
DispatcherHandler, DispatcherHandlerCx,
};
use std::{future::Future, pin::Pin};
@ -33,7 +33,7 @@ pub struct DialogueDispatcher<D, 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<DispatcherHandlerCtx<Upd>>>>,
senders: Arc<Map<i64, mpsc::UnboundedSender<DispatcherHandlerCx<Upd>>>>,
}
impl<D, H, Upd> DialogueDispatcher<D, H, Upd>
@ -69,20 +69,20 @@ where
}
#[must_use]
fn new_tx(&self) -> mpsc::UnboundedSender<DispatcherHandlerCtx<Upd>> {
fn new_tx(&self) -> mpsc::UnboundedSender<DispatcherHandlerCx<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 |ctx: DispatcherHandlerCtx<Upd>| {
tokio::spawn(rx.for_each(move |cx: DispatcherHandlerCx<Upd>| {
let storage = Arc::clone(&storage);
let handler = Arc::clone(&handler);
let senders = Arc::clone(&senders);
async move {
let chat_id = ctx.update.chat_id();
let chat_id = cx.update.chat_id();
let dialogue = Arc::clone(&storage)
.remove_dialogue(chat_id)
@ -90,9 +90,9 @@ where
.unwrap_or_default();
match handler
.handle(DialogueDispatcherHandlerCtx {
bot: ctx.bot,
update: ctx.update,
.handle(DialogueDispatcherHandlerCx {
bot: cx.bot,
update: cx.update,
dialogue,
})
.await
@ -129,11 +129,7 @@ async fn update_dialogue<D>(
) where
D: 'static + Send,
{
if storage
.update_dialogue(chat_id, new_dialogue)
.await
.is_some()
{
if storage.update_dialogue(chat_id, new_dialogue).await.is_some() {
panic!(
"Oops, you have an bug in your Storage: update_dialogue returns \
Some after remove_dialogue"
@ -149,21 +145,21 @@ where
{
fn handle(
self,
updates: mpsc::UnboundedReceiver<DispatcherHandlerCtx<Upd>>,
updates: mpsc::UnboundedReceiver<DispatcherHandlerCx<Upd>>,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>>
where
DispatcherHandlerCtx<Upd>: 'static,
DispatcherHandlerCx<Upd>: 'static,
{
let this = Arc::new(self);
Box::pin(updates.for_each(move |ctx| {
Box::pin(updates.for_each(move |cx| {
let this = Arc::clone(&this);
let chat_id = ctx.update.chat_id();
let chat_id = cx.update.chat_id();
match this.senders.get(&chat_id) {
// An old dialogue
Some(tx) => {
if let Err(_) = tx.1.send(ctx) {
if let Err(_) = tx.1.send(cx) {
panic!(
"We are not dropping a receiver or call .close() \
on it",
@ -172,7 +168,7 @@ where
}
None => {
let tx = this.new_tx();
if let Err(_) = tx.send(ctx) {
if let Err(_) = tx.send(cx) {
panic!(
"We are not dropping a receiver or call .close() \
on it",
@ -209,10 +205,7 @@ mod tests {
impl MyUpdate {
fn new(chat_id: i64, unique_number: u32) -> Self {
Self {
chat_id,
unique_number,
}
Self { chat_id, unique_number }
}
}
@ -229,26 +222,17 @@ mod tests {
}
let dispatcher = DialogueDispatcher::new(
|ctx: DialogueDispatcherHandlerCtx<MyUpdate, ()>| async move {
|cx: DialogueDispatcherHandlerCx<MyUpdate, ()>| async move {
delay_for(Duration::from_millis(300)).await;
match ctx.update {
MyUpdate {
chat_id: 1,
unique_number,
} => {
match cx.update {
MyUpdate { chat_id: 1, unique_number } => {
SEQ1.lock().await.push(unique_number);
}
MyUpdate {
chat_id: 2,
unique_number,
} => {
MyUpdate { chat_id: 2, unique_number } => {
SEQ2.lock().await.push(unique_number);
}
MyUpdate {
chat_id: 3,
unique_number,
} => {
MyUpdate { chat_id: 3, unique_number } => {
SEQ3.lock().await.push(unique_number);
}
_ => unreachable!(),
@ -283,11 +267,11 @@ mod tests {
MyUpdate::new(3, 1611),
]
.into_iter()
.map(|update| DispatcherHandlerCtx {
.map(|update| DispatcherHandlerCx {
update,
bot: Bot::new("Doesn't matter here"),
})
.collect::<Vec<DispatcherHandlerCtx<MyUpdate>>>(),
.collect::<Vec<DispatcherHandlerCx<MyUpdate>>>(),
);
let (tx, rx) = mpsc::unbounded_channel();

View file

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

View file

@ -18,20 +18,16 @@ use std::sync::Arc;
///
/// [`DialogueDispatcher`]: crate::dispatching::dialogue::DialogueDispatcher
#[derive(Debug)]
pub struct DialogueDispatcherHandlerCtx<Upd, D> {
pub struct DialogueDispatcherHandlerCx<Upd, D> {
pub bot: Arc<Bot>,
pub update: Upd,
pub dialogue: D,
}
impl<Upd, D> DialogueDispatcherHandlerCtx<Upd, D> {
impl<Upd, D> DialogueDispatcherHandlerCx<Upd, D> {
/// Creates a new instance with the provided fields.
pub fn new(bot: Arc<Bot>, update: Upd, dialogue: D) -> Self {
Self {
bot,
update,
dialogue,
}
Self { bot, update, dialogue }
}
/// Creates a new instance by substituting a dialogue and preserving
@ -39,8 +35,8 @@ impl<Upd, D> DialogueDispatcherHandlerCtx<Upd, D> {
pub fn with_new_dialogue<Nd>(
self,
new_dialogue: Nd,
) -> DialogueDispatcherHandlerCtx<Upd, Nd> {
DialogueDispatcherHandlerCtx {
) -> DialogueDispatcherHandlerCx<Upd, Nd> {
DialogueDispatcherHandlerCx {
bot: self.bot,
update: self.update,
dialogue: new_dialogue,
@ -48,7 +44,7 @@ impl<Upd, D> DialogueDispatcherHandlerCtx<Upd, D> {
}
}
impl<Upd, D> GetChatId for DialogueDispatcherHandlerCtx<Upd, D>
impl<Upd, D> GetChatId for DialogueDispatcherHandlerCx<Upd, D>
where
Upd: GetChatId,
{
@ -57,7 +53,7 @@ where
}
}
impl<D> DialogueDispatcherHandlerCtx<Message, D> {
impl<D> DialogueDispatcherHandlerCx<Message, D> {
pub fn answer<T>(&self, text: T) -> SendMessage
where
T: Into<String>,
@ -110,8 +106,7 @@ impl<D> DialogueDispatcherHandlerCtx<Message, D> {
latitude: f32,
longitude: f32,
) -> SendLocation {
self.bot
.send_location(self.update.chat.id, latitude, longitude)
self.bot.send_location(self.update.chat.id, latitude, longitude)
}
pub fn answer_venue<T, U>(
@ -147,8 +142,7 @@ impl<D> DialogueDispatcherHandlerCtx<Message, D> {
T: Into<String>,
U: Into<String>,
{
self.bot
.send_contact(self.chat_id(), phone_number, first_name)
self.bot.send_contact(self.chat_id(), phone_number, first_name)
}
pub fn answer_sticker<T>(&self, sticker: InputFile) -> SendSticker {
@ -159,8 +153,7 @@ impl<D> DialogueDispatcherHandlerCtx<Message, D> {
where
T: Into<ChatId>,
{
self.bot
.forward_message(chat_id, self.update.chat.id, self.update.id)
self.bot.forward_message(chat_id, self.update.chat.id, self.update.id)
}
pub fn edit_message_text<T>(&self, text: T) -> EditMessageText
@ -188,7 +181,6 @@ impl<D> DialogueDispatcherHandlerCtx<Message, D> {
}
pub fn pin_message(&self) -> PinChatMessage {
self.bot
.pin_chat_message(self.update.chat.id, self.update.id)
self.bot.pin_chat_message(self.update.chat.id, self.update.id)
}
}

View file

@ -6,7 +6,7 @@
//! moment.
//! 2. [`Storage<D>`], which encapsulates all the dialogues.
//! 3. Your handler, which receives an update and turns your dialogue into the
//! next state ([`DialogueDispatcherHandlerCtx<YourUpdate, D>`] ->
//! next state ([`DialogueDispatcherHandlerCx<YourUpdate, D>`] ->
//! [`DialogueStage<D>`]).
//! 4. [`DialogueDispatcher`], which encapsulates your handler, [`Storage<D>`],
//! and implements [`DispatcherHandler`].
@ -36,22 +36,22 @@
//! [`Dispatcher::messages_handler`]:
//! crate::dispatching::Dispatcher::messages_handler
//! [`UpdateKind::Message(message)`]: crate::types::UpdateKind::Message
//! [`DialogueDispatcherHandlerCtx<YourUpdate, D>`]:
//! crate::dispatching::dialogue::DialogueDispatcherHandlerCtx
//! [`DialogueDispatcherHandlerCx<YourUpdate, D>`]:
//! crate::dispatching::dialogue::DialogueDispatcherHandlerCx
//! [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_ctx;
mod dialogue_dispatcher_handler_cx;
mod dialogue_stage;
mod get_chat_id;
mod storage;
pub use dialogue_dispatcher::DialogueDispatcher;
pub use dialogue_dispatcher_handler::DialogueDispatcherHandler;
pub use dialogue_dispatcher_handler_ctx::DialogueDispatcherHandlerCtx;
pub use dialogue_dispatcher_handler_cx::DialogueDispatcherHandlerCx;
pub use dialogue_stage::{exit, next, DialogueStage};
pub use get_chat_id::GetChatId;
pub use storage::{InMemStorage, Storage};

View file

@ -1,5 +1,6 @@
use super::Storage;
use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};
use futures::future::BoxFuture;
use std::{collections::HashMap, sync::Arc};
use tokio::sync::Mutex;
/// A memory storage based on a hash map. Stores all the dialogues directly in
@ -17,9 +18,7 @@ pub struct InMemStorage<D> {
impl<S> InMemStorage<S> {
#[must_use]
pub fn new() -> Arc<Self> {
Arc::new(Self {
map: Mutex::new(HashMap::new()),
})
Arc::new(Self { map: Mutex::new(HashMap::new()) })
}
}
@ -27,7 +26,7 @@ impl<D> Storage<D> for InMemStorage<D> {
fn remove_dialogue(
self: Arc<Self>,
chat_id: i64,
) -> Pin<Box<dyn Future<Output = Option<D>> + Send + 'static>>
) -> BoxFuture<'static, Option<D>>
where
D: Send + 'static,
{
@ -38,7 +37,7 @@ impl<D> Storage<D> for InMemStorage<D> {
self: Arc<Self>,
chat_id: i64,
dialogue: D,
) -> Pin<Box<dyn Future<Output = Option<D>> + Send + 'static>>
) -> BoxFuture<'static, Option<D>>
where
D: Send + 'static,
{

View file

@ -1,7 +1,8 @@
mod in_mem_storage;
use futures::future::BoxFuture;
pub use in_mem_storage::InMemStorage;
use std::{future::Future, pin::Pin, sync::Arc};
use std::sync::Arc;
/// A storage of dialogues.
///
@ -19,7 +20,7 @@ pub trait Storage<D> {
fn remove_dialogue(
self: Arc<Self>,
chat_id: i64,
) -> Pin<Box<dyn Future<Output = Option<D>> + Send + 'static>>
) -> BoxFuture<'static, Option<D>>
where
D: Send + 'static;
@ -31,7 +32,7 @@ pub trait Storage<D> {
self: Arc<Self>,
chat_id: i64,
dialogue: D,
) -> Pin<Box<dyn Future<Output = Option<D>> + Send + 'static>>
) -> BoxFuture<'static, Option<D>>
where
D: Send + 'static;
}

View file

@ -1,9 +1,9 @@
use crate::{
dispatching::{
error_handlers::ErrorHandler, update_listeners,
update_listeners::UpdateListener, DispatcherHandler,
DispatcherHandlerCtx, LoggingErrorHandler,
update_listeners, update_listeners::UpdateListener, DispatcherHandler,
DispatcherHandlerCx,
},
error_handlers::{ErrorHandler, LoggingErrorHandler},
types::{
CallbackQuery, ChosenInlineResult, InlineQuery, Message, Poll,
PollAnswer, PreCheckoutQuery, ShippingQuery, UpdateKind,
@ -16,7 +16,7 @@ use tokio::sync::mpsc;
use tokio::sync::Mutex;
type Tx<Upd> = Option<Mutex<mpsc::UnboundedSender<DispatcherHandlerCtx<Upd>>>>;
type Tx<Upd> = Option<Mutex<mpsc::UnboundedSender<DispatcherHandlerCx<Upd>>>>;
#[macro_use]
mod macros {
@ -37,10 +37,11 @@ async fn send<'a, Upd>(
Upd: Debug,
{
if let Some(tx) = tx {
if let Err(error) = tx.lock().await.send(DispatcherHandlerCtx {
bot: Arc::clone(&bot),
update,
}) {
if let Err(error) = tx
.lock()
.await
.send(DispatcherHandlerCx { bot: Arc::clone(&bot), update })
{
log::error!(
"The RX part of the {} channel is closed, but an update is \
received.\nError:{}\n",
@ -211,7 +212,9 @@ impl Dispatcher {
pub async fn dispatch(&self) {
self.dispatch_with_listener(
update_listeners::polling_default(Arc::clone(&self.bot)),
LoggingErrorHandler::new("An error from the update listener"),
LoggingErrorHandler::with_custom_text(
"An error from the update listener",
),
)
.await;
}

View file

@ -1,6 +1,6 @@
use std::future::Future;
use crate::dispatching::{DispatcherHandlerCtx, DispatcherHandlerRx};
use crate::dispatching::{DispatcherHandlerCx, DispatcherHandlerRx};
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
DispatcherHandlerCtx<Upd>: Send + 'static;
DispatcherHandlerCx<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
DispatcherHandlerCtx<Upd>: Send + 'static,
DispatcherHandlerCx<Upd>: Send + 'static,
{
Box::pin(async move { self(updates).await })
}

View file

@ -18,12 +18,12 @@ use std::sync::Arc;
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
#[derive(Debug)]
pub struct DispatcherHandlerCtx<Upd> {
pub struct DispatcherHandlerCx<Upd> {
pub bot: Arc<Bot>,
pub update: Upd,
}
impl<Upd> GetChatId for DispatcherHandlerCtx<Upd>
impl<Upd> GetChatId for DispatcherHandlerCx<Upd>
where
Upd: GetChatId,
{
@ -32,7 +32,7 @@ where
}
}
impl DispatcherHandlerCtx<Message> {
impl DispatcherHandlerCx<Message> {
pub fn answer<T>(&self, text: T) -> SendMessage
where
T: Into<String>,
@ -85,8 +85,7 @@ impl DispatcherHandlerCtx<Message> {
latitude: f32,
longitude: f32,
) -> SendLocation {
self.bot
.send_location(self.update.chat.id, latitude, longitude)
self.bot.send_location(self.update.chat.id, latitude, longitude)
}
pub fn answer_venue<T, U>(
@ -122,8 +121,7 @@ impl DispatcherHandlerCtx<Message> {
T: Into<String>,
U: Into<String>,
{
self.bot
.send_contact(self.chat_id(), phone_number, first_name)
self.bot.send_contact(self.chat_id(), phone_number, first_name)
}
pub fn answer_sticker<T>(&self, sticker: InputFile) -> SendSticker {
@ -134,8 +132,7 @@ impl DispatcherHandlerCtx<Message> {
where
T: Into<ChatId>,
{
self.bot
.forward_message(chat_id, self.update.chat.id, self.update.id)
self.bot.forward_message(chat_id, self.update.chat.id, self.update.id)
}
pub fn edit_message_text<T>(&self, text: T) -> EditMessageText
@ -163,7 +160,6 @@ impl DispatcherHandlerCtx<Message> {
}
pub fn pin_message(&self) -> PinChatMessage {
self.bot
.pin_chat_message(self.update.chat.id, self.update.id)
self.bot.pin_chat_message(self.update.chat.id, self.update.id)
}
}

View file

@ -52,21 +52,15 @@
pub mod dialogue;
mod dispatcher;
mod dispatcher_handler;
mod dispatcher_handler_ctx;
mod error_handlers;
mod dispatcher_handler_cx;
pub mod update_listeners;
pub use dispatcher::Dispatcher;
pub use dispatcher_handler::DispatcherHandler;
pub use dispatcher_handler_ctx::DispatcherHandlerCtx;
pub use error_handlers::{
ErrorHandler, IgnoringErrorHandler, IgnoringErrorHandlerSafe,
LoggingErrorHandler,
};
pub use dispatcher_handler_cx::DispatcherHandlerCx;
use tokio::sync::mpsc::UnboundedReceiver;
/// A type of a stream, consumed by [`Dispatcher`]'s handlers.
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
pub type DispatcherHandlerRx<Upd> =
UnboundedReceiver<DispatcherHandlerCtx<Upd>>;
pub type DispatcherHandlerRx<Upd> = UnboundedReceiver<DispatcherHandlerCx<Upd>>;

View file

@ -1,3 +1,5 @@
//! Convenient error handling.
use futures::future::BoxFuture;
use std::{convert::Infallible, fmt::Debug, future::Future, sync::Arc};
@ -21,19 +23,71 @@ where
}
}
/// Something that can be handled by an error handler.
///
/// ## Examples
/// Use an arbitrary error handler:
/// ```
/// use teloxide::error_handlers::{IgnoringErrorHandler, OnError};
///
/// # #[tokio::main]
/// # async fn main() {
/// let err: Result<i32, i32> = Err(404);
/// err.on_error(IgnoringErrorHandler::new()).await;
/// # }
/// ```
pub trait OnError<E> {
#[must_use]
fn on_error<'a, Eh>(self, eh: Arc<Eh>) -> BoxFuture<'a, ()>
where
Self: 'a,
Eh: ErrorHandler<E> + Send + Sync,
Arc<Eh>: 'a;
/// A shortcut for `.on_error(LoggingErrorHandler::new())`.
#[must_use]
fn log_on_error<'a>(self) -> BoxFuture<'a, ()>
where
Self: Sized + 'a,
E: Debug,
{
self.on_error(LoggingErrorHandler::new())
}
}
impl<T, E> OnError<E> for Result<T, E>
where
T: Send,
E: Send,
{
fn on_error<'a, Eh>(self, eh: Arc<Eh>) -> BoxFuture<'a, ()>
where
Self: 'a,
Eh: ErrorHandler<E> + Send + Sync,
Arc<Eh>: 'a,
{
Box::pin(async move {
if let Err(error) = self {
eh.handle_error(error).await;
}
})
}
}
/// A handler that silently ignores all errors.
///
/// ## Example
/// ```
/// # #[tokio::main]
/// # async fn main_() {
/// use teloxide::dispatching::{ErrorHandler, IgnoringErrorHandler};
/// use teloxide::error_handlers::{ErrorHandler, IgnoringErrorHandler};
///
/// IgnoringErrorHandler::new().handle_error(()).await;
/// IgnoringErrorHandler::new().handle_error(404).await;
/// IgnoringErrorHandler::new().handle_error("error").await;
/// # }
/// ```
#[derive(Clone, Copy)]
pub struct IgnoringErrorHandler;
impl IgnoringErrorHandler {
@ -58,7 +112,7 @@ impl<E> ErrorHandler<E> for IgnoringErrorHandler {
/// # async fn main_() {
/// use std::convert::{Infallible, TryInto};
///
/// use teloxide::dispatching::{ErrorHandler, IgnoringErrorHandlerSafe};
/// use teloxide::error_handlers::{ErrorHandler, IgnoringErrorHandlerSafe};
///
/// let result: Result<String, Infallible> = "str".try_into();
/// match result {
@ -78,6 +132,7 @@ impl<E> ErrorHandler<E> for IgnoringErrorHandler {
///
/// [`!`]: https://doc.rust-lang.org/std/primitive.never.html
/// [`Infallible`]: std::convert::Infallible
#[derive(Clone, Copy)]
pub struct IgnoringErrorHandlerSafe;
impl IgnoringErrorHandlerSafe {
@ -100,11 +155,11 @@ impl ErrorHandler<Infallible> for IgnoringErrorHandlerSafe {
/// ```
/// # #[tokio::main]
/// # async fn main_() {
/// use teloxide::dispatching::{ErrorHandler, LoggingErrorHandler};
/// use teloxide::error_handlers::{ErrorHandler, LoggingErrorHandler};
///
/// LoggingErrorHandler::empty().handle_error(()).await;
/// LoggingErrorHandler::new("error").handle_error(404).await;
/// LoggingErrorHandler::new("error")
/// LoggingErrorHandler::new().handle_error(()).await;
/// LoggingErrorHandler::with_custom_text("Omg1").handle_error(404).await;
/// LoggingErrorHandler::with_custom_text("Omg2")
/// .handle_error("Invalid data type!")
/// .await;
/// # }
@ -118,17 +173,18 @@ impl LoggingErrorHandler {
///
/// The logs will be printed in this format: `{text}: {:?}`.
#[must_use]
pub fn new<T>(text: T) -> Arc<Self>
pub fn with_custom_text<T>(text: T) -> Arc<Self>
where
T: Into<String>,
{
Arc::new(Self { text: text.into() })
}
/// A shortcut for `LoggingErrorHandler::new("Error".to_owned())`.
/// A shortcut for
/// `LoggingErrorHandler::with_custom_text("Error".to_owned())`.
#[must_use]
pub fn empty() -> Arc<Self> {
Self::new("Error".to_owned())
pub fn new() -> Arc<Self> {
Self::with_custom_text("Error".to_owned())
}
}

View file

@ -21,10 +21,7 @@ pub enum DownloadError {
#[derive(Debug, Error)]
pub enum RequestError {
#[error("A Telegram's error #{status_code}: {kind:?}")]
ApiError {
status_code: StatusCode,
kind: ApiErrorKind,
},
ApiError { status_code: StatusCode, kind: ApiErrorKind },
/// The group has been migrated to a supergroup with the specified
/// identifier.

View file

@ -2,34 +2,35 @@
//! using the [`async`/`.await`] syntax in [Rust]. It handles all the difficult
//! stuff so you can focus only on your business logic.
//!
//! ## Features
//! - **Type-safe.** teloxide leverages the Rust's type system with two serious
//! implications: resistance to human mistakes and tight integration with
//! IDEs. Write fast, avoid debugging as possible.
//! # Features
//! - **Type-safe.** teloxide leverages the Rust's type system with two serious
//! implications: resistance to human mistakes and tight integration with
//! IDEs. Write fast, avoid debugging as much as possible.
//!
//! - **Persistency.** By default, teloxide stores all user dialogues in RAM,
//! but you can store them somewhere else (for example, in DB) just by
//! implementing 2 functions.
//! - **Flexible API.** teloxide gives you the power of [streams]: you can
//! combine [all 30+ patterns] when working with updates from Telegram.
//!
//! - **Convenient dialogues system.** Define a type-safe [finite automaton]
//! and transition functions to drive a user dialogue with ease (see the
//! examples below).
//! - **Persistency.** By default, teloxide stores all user dialogues in RAM,
//! but you can store them somewhere else (for example, in DB) just by
//! implementing 2 functions.
//!
//! - **Convenient API.** Automatic conversions are used to avoid boilerplate.
//! For example, functions accept `Into<String>`, rather than `&str` or
//! `String`, so you can call them without `.to_string()`/`.as_str()`/etc.
//! - **Convenient dialogues system.** Define a type-safe [finite automaton]
//! and transition functions to drive a user dialogue with ease (see [the
//! guess-a-number example](#guess-a-number) below).
//!
//! ## Getting started
//! # Getting started
//! 1. Create a new bot using [@Botfather] to get a token in the format
//! `123456789:blablabla`. 2. Initialise the `TELOXIDE_TOKEN` environmental
//! `123456789:blablabla`.
//! 2. Initialise the `TELOXIDE_TOKEN` environmental
//! variable to your token:
//! ```bash
//! ```text
//! # Unix
//! $ export TELOXIDE_TOKEN=MyAwesomeToken
//!
//! # Windows
//! $ set TELOXITE_TOKEN=MyAwesomeToken
//! ```
//!
//! 3. Be sure that you are up to date:
//! ```bash
//! $ rustup update stable
@ -37,25 +38,286 @@
//!
//! 4. Execute `cargo new my_bot`, enter the directory and put these lines into
//! your `Cargo.toml`:
//! ```toml
//! ```text
//! [dependencies]
//! teloxide = "0.1.0"
//! log = "0.4.8"
//! futures = "0.3.4"
//! tokio = "0.2.11"
//! pretty_env_logger = "0.4.0"
//! ```
//!
//! # 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))
//! ```no_run
//! use teloxide::prelude::*;
//!
//! #[tokio::main]
//! async fn main() {
//! teloxide::enable_logging!();
//! log::info!("Starting ping_pong_bot!");
//!
//! let bot = Bot::from_env();
//!
//! Dispatcher::new(bot)
//! .messages_handler(|rx: DispatcherHandlerRx<Message>| {
//! rx.for_each(|message| async move {
//! message.answer("pong").send().await.log_on_error().await;
//! })
//! })
//! .dispatch()
//! .await;
//! }
//! ```
//!
//! <details>
//! <summary>Click here to run it!</summary>
//!
//! ```text
//! git clone https://github.com/teloxide/teloxide.git
//! cd teloxide/examples/ping_pong_bot
//! TELOXIDE_TOKEN=MyAwesomeToken cargo run
//! ```
//!
//! </details>
//!
//! <div align="center">
//! <img src=https://github.com/teloxide/teloxide/raw/master/media/GUESS_A_NUMBER_BOT.png width="400" />
//! <img src=https://github.com/teloxide/teloxide/raw/master/media/PING_PONG_BOT.png width="400" />
//! </div>
//!
//! # Commands
//! Commands are defined similar to how we define CLI using [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))
//! ```no_run
//! // Imports are omitted...
//! # use teloxide::{prelude::*, utils::command::BotCommand};
//! # use futures::future;
//! # use rand::{thread_rng, Rng};
//!
//! #[derive(BotCommand)]
//! #[command(
//! rename = "lowercase",
//! description = "These commands are supported:"
//! )]
//! enum Command {
//! #[command(description = "display this text.")]
//! Help,
//! #[command(description = "be a cat.")]
//! Meow,
//! #[command(description = "generate a random number within [0; 1).")]
//! Generate,
//! }
//!
//! fn generate() -> String {
//! thread_rng().gen_range(0.0, 1.0).to_string()
//! }
//!
//! async fn answer(
//! cx: DispatcherHandlerCx<Message>,
//! command: Command,
//! ) -> ResponseResult<()> {
//! match command {
//! Command::Help => cx.answer(Command::descriptions()).send().await?,
//! Command::Generate => cx.answer(generate()).send().await?,
//! Command::Meow => cx.answer("I am a cat! Meow!").send().await?,
//! };
//!
//! Ok(())
//! }
//!
//! async fn handle_command(rx: DispatcherHandlerRx<Message>) {
//! rx.filter_map(|cx| {
//! future::ready(cx.update.text_owned().map(|text| (cx, text)))
//! })
//! .filter_map(|(cx, text)| {
//! future::ready(
//! Command::parse(&text).map(|(command, _)| (cx, command)),
//! )
//! })
//! .for_each_concurrent(None, |(cx, command)| async move {
//! answer(cx, command).await.log_on_error().await;
//! })
//! .await;
//! }
//!
//! #[tokio::main]
//! async fn main() {
//! // Setup is omitted...
//! # teloxide::enable_logging!();
//! # log::info!("Starting simple_commands_bot!");
//! # let bot = Bot::from_env();
//! # Dispatcher::new(bot).messages_handler(handle_command).dispatch().await;
//! }
//! ```
//!
//! <details>
//! <summary>Click here to run it!</summary>
//!
//! ```text
//! git clone https://github.com/teloxide/teloxide.git
//! cd teloxide/examples/simple_commands_bot
//! TELOXIDE_TOKEN=MyAwesomeToken cargo run
//! ```
//!
//! </details>
//!
//! <div align="center">
//! <img src=https://github.com/teloxide/teloxide/raw/master/media/SIMPLE_COMMANDS_BOT.png width="400" />
//! </div>
//!
//!
//! See? The dispatcher gives us a stream of messages, so we can handle it as we
//! want! Here we use [`.filter_map()`] and [`.for_each_concurrent()`], but
//! others are also available:
//! - [`.flatten()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.flatten)
//! - [`.left_stream()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.left_stream)
//! - [`.scan()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.scan)
//! - [`.skip_while()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.skip_while)
//! - [`.zip()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.zip)
//! - [`.select_next_some()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.select_next_some)
//! - [`.fold()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.fold)
//! - [`.inspect()`](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.inspect)
//! - ... And lots of [others](https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.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):
//!
//! ([Full](https://github.com/teloxide/teloxide/blob/master/examples/guess_a_number_bot/src/main.rs))
//! ```no_run
//! // Setup is omitted...
//! # #[macro_use]
//! # extern crate smart_default;
//! # use teloxide::prelude::*;
//! # use rand::{thread_rng, Rng};
//!
//! #[derive(SmartDefault)]
//! enum Dialogue {
//! #[default]
//! Start,
//! ReceiveAttempt(u8),
//! }
//!
//! async fn handle_message(
//! cx: DialogueDispatcherHandlerCx<Message, Dialogue>,
//! ) -> ResponseResult<DialogueStage<Dialogue>> {
//! match cx.dialogue {
//! Dialogue::Start => {
//! 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)))
//! }
//! Dialogue::ReceiveAttempt(secret) => match cx.update.text() {
//! None => {
//! cx.answer("Oh, please, send me a text message!")
//! .send()
//! .await?;
//! next(cx.dialogue)
//! }
//! Some(text) => match text.parse::<u8>() {
//! Ok(attempt) => match attempt {
//! x if !(1..=10).contains(&x) => {
//! cx.answer(
//! "Oh, please, send me a number in the range \
//! [1; 10]!",
//! )
//! .send()
//! .await?;
//! next(cx.dialogue)
//! }
//! x if x == secret => {
//! cx.answer("Congratulations! You won!")
//! .send()
//! .await?;
//! exit()
//! }
//! _ => {
//! cx.answer("No.").send().await?;
//! next(cx.dialogue)
//! }
//! },
//! Err(_) => {
//! cx.answer(
//! "Oh, please, send me a number in the range [1; \
//! 10]!",
//! )
//! .send()
//! .await?;
//! next(cx.dialogue)
//! }
//! },
//! },
//! }
//! }
//!
//! #[tokio::main]
//! async fn main() {
//! // Setup is omitted...
//! # 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;
//! }
//! ```
//!
//! <details>
//! <summary>Click here to run it!</summary>
//!
//! ```text
//! git clone https://github.com/teloxide/teloxide.git
//! cd teloxide/examples/guess_a_number_bot
//! TELOXIDE_TOKEN=MyAwesomeToken cargo run
//! ```
//!
//! </details>
//!
//! <div align="center">
//! <img src=https://github.com/teloxide/teloxide/raw/master/media/GUESS_A_NUMBER_BOT.png width="400" />
//! </div>
//!
//! Our [finite automaton], designating a user dialogue, cannot be in an invalid
//! state. See [examples/dialogue_bot] to see a bit more complicated bot with
//! dialogues.
//! state, and this is why it is called "type-safe". We could use `enum` +
//! `Option`s instead, but it will lead is to lots of unpleasure `.unwrap()`s.
//!
//! [See more examples](https://github.com/teloxide/teloxide/tree/master/examples).
//! Remember that a classical [finite automaton] 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`).
//!
//! ## Recommendations
//! If you're familiar with [category theory], `Dialogue` is almost a
//! [coproduct], such that:
//! - `X1` is `()`
//! - `X2` is `u8`
//! - `i1` is `Dialogue::Start`
//! - `i2` is `Dialogue::ReceiveAttempt`
//!
//! <div align="center">
//! <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/2b/Coproduct-03.svg/280px-Coproduct-03.svg.png" heigh="500" />
//! </div>
//!
//! But without the `f`, `f1`, `f2` morphisms and the `Y` object (which we can
//! freely define if we wanted).
//!
//! See [examples/dialogue_bot] to see a bit more complicated bot with
//! dialogues. [See more examples] to get into teloxide!
//!
//!
//! # Recommendations
//!
//! - Use this pattern:
//!
@ -90,6 +352,13 @@
//! [examples/dialogue_bot]: https://github.com/teloxide/teloxide/blob/master/examples/dialogue_bot/src/main.rs
//! [structopt]: https://docs.rs/structopt/0.3.9/structopt/
//! [@Botfather]: https://t.me/botfather
//! [streams]: https://docs.rs/futures/0.3.4/futures/stream/index.html
//! [all 30+ patterns]: https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html
//! [`.filter_map()`]: https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.filter_map
//! [`.for_each_concurrent()`]: https://docs.rs/futures/0.3.4/futures/stream/trait.StreamExt.html#method.for_each_concurrent
//! [See more examples]: https://github.com/teloxide/teloxide/tree/master/examples
//! [category theory]: https://en.wikipedia.org/wiki/Category_theory
//! [coproduct]: https://en.wikipedia.org/wiki/Coproduct
#![doc(
html_logo_url = "https://github.com/teloxide/teloxide/raw/master/logo.svg",
@ -105,6 +374,7 @@ mod net;
mod bot;
pub mod dispatching;
pub mod error_handlers;
mod logging;
pub mod prelude;
pub mod requests;

View file

@ -3,12 +3,12 @@
pub use crate::{
dispatching::{
dialogue::{
exit, next, DialogueDispatcher, DialogueDispatcherHandlerCtx,
exit, next, DialogueDispatcher, DialogueDispatcherHandlerCx,
DialogueStage, GetChatId,
},
Dispatcher, DispatcherHandlerCtx, DispatcherHandlerRx, ErrorHandler,
LoggingErrorHandler,
Dispatcher, DispatcherHandlerCx, DispatcherHandlerRx,
},
error_handlers::{LoggingErrorHandler, OnError},
requests::{Request, ResponseResult},
types::{Message, Update},
Bot, RequestError,

View file

@ -52,12 +52,7 @@ impl AnswerPreCheckoutQuery {
P: Into<String>,
{
let pre_checkout_query_id = pre_checkout_query_id.into();
Self {
bot,
pre_checkout_query_id,
ok,
error_message: None,
}
Self { bot, pre_checkout_query_id, ok, error_message: None }
}
/// Unique identifier for the query to be answered.

View file

@ -53,11 +53,7 @@ impl DeleteMessage {
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
message_id,
}
Self { bot, chat_id, message_id }
}
/// Unique identifier for the target chat or username of the target channel

View file

@ -36,10 +36,7 @@ impl Request for EditMessageMedia {
let mut params = FormBuilder::new();
match &self.chat_or_inline_message {
ChatOrInlineMessage::Chat {
chat_id,
message_id,
} => {
ChatOrInlineMessage::Chat { chat_id, message_id } => {
params = params
.add("chat_id", chat_id)
.await
@ -73,12 +70,7 @@ impl EditMessageMedia {
chat_or_inline_message: ChatOrInlineMessage,
media: InputMedia,
) -> Self {
Self {
bot,
chat_or_inline_message,
media,
reply_markup: None,
}
Self { bot, chat_or_inline_message, media, reply_markup: None }
}
pub fn chat_or_inline_message(mut self, val: ChatOrInlineMessage) -> Self {

View file

@ -47,11 +47,7 @@ impl EditMessageReplyMarkup {
bot: Arc<Bot>,
chat_or_inline_message: ChatOrInlineMessage,
) -> Self {
Self {
bot,
chat_or_inline_message,
reply_markup: None,
}
Self { bot, chat_or_inline_message, reply_markup: None }
}
pub fn chat_or_inline_message(mut self, val: ChatOrInlineMessage) -> Self {

View file

@ -41,11 +41,7 @@ impl GetChatMember {
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
user_id,
}
Self { bot, chat_id, user_id }
}
/// Unique identifier for the target chat or username of the target

View file

@ -50,10 +50,7 @@ impl GetFile {
where
F: Into<String>,
{
Self {
bot,
file_id: file_id.into(),
}
Self { bot, file_id: file_id.into() }
}
/// File identifier to get info about.

View file

@ -51,11 +51,7 @@ impl GetGameHighScores {
chat_or_inline_message: ChatOrInlineMessage,
user_id: i32,
) -> Self {
Self {
bot,
chat_or_inline_message,
user_id,
}
Self { bot, chat_or_inline_message, user_id }
}
pub fn chat_or_inline_message(mut self, val: ChatOrInlineMessage) -> Self {

View file

@ -38,12 +38,7 @@ impl Request for GetUserProfilePhotos {
impl GetUserProfilePhotos {
pub(crate) fn new(bot: Arc<Bot>, user_id: i32) -> Self {
Self {
bot,
user_id,
offset: None,
limit: None,
}
Self { bot, user_id, offset: None, limit: None }
}
/// Unique identifier of the target user.

View file

@ -49,12 +49,7 @@ impl KickChatMember {
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
user_id,
until_date: None,
}
Self { bot, chat_id, user_id, until_date: None }
}
/// Unique identifier for the target group or username of the target

View file

@ -46,12 +46,7 @@ impl PinChatMessage {
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
message_id,
disable_notification: None,
}
Self { bot, chat_id, message_id, disable_notification: None }
}
/// Unique identifier for the target chat or username of the target channel

View file

@ -52,13 +52,7 @@ impl RestrictChatMember {
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
user_id,
permissions,
until_date: None,
}
Self { bot, chat_id, user_id, permissions, until_date: None }
}
/// Unique identifier for the target chat or username of the target

View file

@ -95,11 +95,7 @@ impl SendChatAction {
where
C: Into<ChatId>,
{
Self {
bot,
chat_id: chat_id.into(),
action,
}
Self { bot, chat_id: chat_id.into(), action }
}
/// Unique identifier for the target chat or username of the target channel

View file

@ -50,12 +50,7 @@ impl SetChatAdministratorCustomTitle {
{
let chat_id = chat_id.into();
let custom_title = custom_title.into();
Self {
bot,
chat_id,
user_id,
custom_title,
}
Self { bot, chat_id, user_id, custom_title }
}
/// Unique identifier for the target chat or username of the target channel

View file

@ -45,11 +45,7 @@ impl SetChatDescription {
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
description: None,
}
Self { bot, chat_id, description: None }
}
/// Unique identifier for the target chat or username of the target channel

View file

@ -48,11 +48,7 @@ impl SetChatPermissions {
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
permissions,
}
Self { bot, chat_id, permissions }
}
/// Unique identifier for the target chat or username of the target

View file

@ -44,11 +44,7 @@ impl SetChatPhoto {
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
photo,
}
Self { bot, chat_id, photo }
}
/// Unique identifier for the target chat or username of the target channel

View file

@ -51,11 +51,7 @@ impl SetChatStickerSet {
{
let chat_id = chat_id.into();
let sticker_set_name = sticker_set_name.into();
Self {
bot,
chat_id,
sticker_set_name,
}
Self { bot, chat_id, sticker_set_name }
}
/// Unique identifier for the target chat or username of the target

View file

@ -46,11 +46,7 @@ impl SetChatTitle {
{
let chat_id = chat_id.into();
let title = title.into();
Self {
bot,
chat_id,
title,
}
Self { bot, chat_id, title }
}
/// Unique identifier for the target chat or username of the target channel

View file

@ -42,11 +42,7 @@ impl SetStickerPositionInSet {
S: Into<String>,
{
let sticker = sticker.into();
Self {
bot,
sticker,
position,
}
Self { bot, sticker, position }
}
/// File identifier of the sticker.

View file

@ -48,11 +48,7 @@ impl StopMessageLiveLocation {
bot: Arc<Bot>,
chat_or_inline_message: ChatOrInlineMessage,
) -> Self {
Self {
bot,
chat_or_inline_message,
reply_markup: None,
}
Self { bot, chat_or_inline_message, reply_markup: None }
}
pub fn chat_or_inline_message(mut self, val: ChatOrInlineMessage) -> Self {

View file

@ -44,12 +44,7 @@ impl StopPoll {
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
message_id,
reply_markup: None,
}
Self { bot, chat_id, message_id, reply_markup: None }
}
/// Unique identifier for the target chat or username of the target channel

View file

@ -44,11 +44,7 @@ impl UnbanChatMember {
C: Into<ChatId>,
{
let chat_id = chat_id.into();
Self {
bot,
chat_id,
user_id,
}
Self { bot, chat_id, user_id }
}
/// Unique identifier for the target group or username of the target

View file

@ -45,11 +45,7 @@ impl UploadStickerFile {
user_id: i32,
png_sticker: InputFile,
) -> Self {
Self {
bot,
user_id,
png_sticker,
}
Self { bot, user_id, png_sticker }
}
/// User identifier of sticker file owner.

View file

@ -29,9 +29,9 @@ impl FormBuilder {
{
let name = name.into().into_owned();
match value.into_form_value() {
Some(FormValue::Str(string)) => Self {
form: self.form.text(name, string),
},
Some(FormValue::Str(string)) => {
Self { form: self.form.text(name, string) }
}
Some(FormValue::File(path)) => self.add_file(name, path).await,
None => self,
}

View file

@ -22,11 +22,8 @@ impl Decoder for FileDecoder {
}
pub async fn file_to_part(path_to_file: PathBuf) -> Part {
let file_name = path_to_file
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
let file_name =
path_to_file.file_name().unwrap().to_string_lossy().into_owned();
let file = FramedRead::new(
tokio::fs::File::open(path_to_file).await.unwrap(), /* TODO: this

View file

@ -64,10 +64,7 @@ pub enum InlineKeyboardButtonKind {
/// ```
impl InlineKeyboardButton {
pub fn url(text: String, url: String) -> InlineKeyboardButton {
InlineKeyboardButton {
text,
kind: InlineKeyboardButtonKind::Url(url),
}
InlineKeyboardButton { text, kind: InlineKeyboardButtonKind::Url(url) }
}
pub fn callback(

View file

@ -31,10 +31,7 @@ impl KeyboardButton {
where
T: Into<String>,
{
Self {
text: text.into(),
request: None,
}
Self { text: text.into(), request: None }
}
pub fn request<T>(mut self, val: T) -> Self
@ -90,16 +87,11 @@ impl<'de> Deserialize<'de> for ButtonRequest {
"`request_contact` and `request_location` fields are mutually \
exclusive, but both were provided",
)),
RawRequest {
contact: Some(_), ..
} => Ok(Self::Contact),
RawRequest {
location: Some(_), ..
} => Ok(Self::Location),
RawRequest {
poll: Some(poll_type),
..
} => Ok(Self::KeyboardButtonPollType(poll_type)),
RawRequest { contact: Some(_), .. } => Ok(Self::Contact),
RawRequest { location: Some(_), .. } => Ok(Self::Location),
RawRequest { poll: Some(poll_type), .. } => {
Ok(Self::KeyboardButtonPollType(poll_type))
}
_ => Err(D::Error::custom(
"Either one of `request_contact` and `request_location` \
fields is required",
@ -114,18 +106,14 @@ impl Serialize for ButtonRequest {
S: Serializer,
{
match self {
Self::Contact => RawRequest {
contact: Some(True),
location: None,
poll: None,
Self::Contact => {
RawRequest { contact: Some(True), location: None, poll: None }
.serialize(serializer)
}
.serialize(serializer),
Self::Location => RawRequest {
contact: None,
location: Some(True),
poll: None,
Self::Location => {
RawRequest { contact: None, location: Some(True), poll: None }
.serialize(serializer)
}
.serialize(serializer),
Self::KeyboardButtonPollType(poll_type) => RawRequest {
contact: None,
location: None,
@ -142,10 +130,7 @@ mod tests {
#[test]
fn serialize_no_request() {
let button = KeyboardButton {
text: String::from(""),
request: None,
};
let button = KeyboardButton { text: String::from(""), request: None };
let expected = r#"{"text":""}"#;
let actual = serde_json::to_string(&button).unwrap();
assert_eq!(expected, actual);
@ -165,10 +150,7 @@ mod tests {
#[test]
fn deserialize_no_request() {
let json = r#"{"text":""}"#;
let expected = KeyboardButton {
text: String::from(""),
request: None,
};
let expected = KeyboardButton { text: String::from(""), request: None };
let actual = serde_json::from_str(json).unwrap();
assert_eq!(expected, actual);
}

View file

@ -25,10 +25,8 @@ mod tests {
#[test]
fn serialize() {
let labeled_price = LabeledPrice {
label: "Label".to_string(),
amount: 60,
};
let labeled_price =
LabeledPrice { label: "Label".to_string(), amount: 60 };
let expected = r#"{"label":"Label","amount":60}"#;
let actual = serde_json::to_string(&labeled_price).unwrap();
assert_eq!(actual, expected);

View file

@ -353,8 +353,7 @@ mod getters {
pub fn forward_from(&self) -> Option<&ForwardedFrom> {
match &self.kind {
Common {
forward_kind: NonChannelForward { from, .. },
..
forward_kind: NonChannelForward { from, .. }, ..
} => Some(from),
_ => None,
}
@ -363,8 +362,7 @@ mod getters {
pub fn forward_from_chat(&self) -> Option<&Chat> {
match &self.kind {
Common {
forward_kind: ChannelForward { chat, .. },
..
forward_kind: ChannelForward { chat, .. }, ..
} => Some(chat),
_ => None,
}
@ -393,12 +391,10 @@ mod getters {
pub fn forward_date(&self) -> Option<&i32> {
match &self.kind {
Common {
forward_kind: ChannelForward { date, .. },
..
forward_kind: ChannelForward { date, .. }, ..
}
| Common {
forward_kind: NonChannelForward { date, .. },
..
forward_kind: NonChannelForward { date, .. }, ..
} => Some(date),
_ => None,
}
@ -407,10 +403,7 @@ mod getters {
pub fn reply_to_message(&self) -> Option<&Message> {
match &self.kind {
Common {
forward_kind:
Origin {
reply_to_message, ..
},
forward_kind: Origin { reply_to_message, .. },
..
} => reply_to_message.as_ref().map(Deref::deref),
_ => None,
@ -426,34 +419,30 @@ mod getters {
pub fn media_group_id(&self) -> Option<&str> {
match &self.kind {
Common {
media_kind: Video { media_group_id, .. },
..
Common { media_kind: Video { media_group_id, .. }, .. }
| Common { media_kind: Photo { media_group_id, .. }, .. } => {
media_group_id.as_ref().map(Deref::deref)
}
| Common {
media_kind: Photo { media_group_id, .. },
..
} => media_group_id.as_ref().map(Deref::deref),
_ => None,
}
}
pub fn text(&self) -> Option<&str> {
match &self.kind {
Common {
media_kind: Text { text, .. },
..
} => Some(text),
Common { media_kind: Text { text, .. }, .. } => Some(text),
_ => None,
}
}
pub fn text_owned(&self) -> Option<String> {
self.text().map(ToOwned::to_owned)
}
pub fn entities(&self) -> Option<&[MessageEntity]> {
match &self.kind {
Common {
media_kind: Text { entities, .. },
..
} => Some(entities),
Common { media_kind: Text { entities, .. }, .. } => {
Some(entities)
}
_ => None,
}
}
@ -461,46 +450,24 @@ mod getters {
pub fn caption_entities(&self) -> Option<&[MessageEntity]> {
match &self.kind {
Common {
media_kind:
Animation {
caption_entities, ..
},
media_kind: Animation { caption_entities, .. },
..
}
| Common {
media_kind:
Audio {
caption_entities, ..
},
media_kind: Audio { caption_entities, .. }, ..
}
| Common {
media_kind: Document { caption_entities, .. },
..
}
| Common {
media_kind:
Document {
caption_entities, ..
},
..
media_kind: Photo { caption_entities, .. }, ..
}
| Common {
media_kind:
Photo {
caption_entities, ..
},
..
media_kind: Video { caption_entities, .. }, ..
}
| Common {
media_kind:
Video {
caption_entities, ..
},
..
}
| Common {
media_kind:
Voice {
caption_entities, ..
},
..
media_kind: Voice { caption_entities, .. }, ..
} => Some(caption_entities),
_ => None,
}
@ -508,90 +475,71 @@ mod getters {
pub fn audio(&self) -> Option<&types::Audio> {
match &self.kind {
Common {
media_kind: Audio { audio, .. },
..
} => Some(audio),
Common { media_kind: Audio { audio, .. }, .. } => Some(audio),
_ => None,
}
}
pub fn document(&self) -> Option<&types::Document> {
match &self.kind {
Common {
media_kind: Document { document, .. },
..
} => Some(document),
Common { media_kind: Document { document, .. }, .. } => {
Some(document)
}
_ => None,
}
}
pub fn animation(&self) -> Option<&types::Animation> {
match &self.kind {
Common {
media_kind: Animation { animation, .. },
..
} => Some(animation),
Common { media_kind: Animation { animation, .. }, .. } => {
Some(animation)
}
_ => None,
}
}
pub fn game(&self) -> Option<&types::Game> {
match &self.kind {
Common {
media_kind: Game { game, .. },
..
} => Some(game),
Common { media_kind: Game { game, .. }, .. } => Some(game),
_ => None,
}
}
pub fn photo(&self) -> Option<&[PhotoSize]> {
match &self.kind {
Common {
media_kind: Photo { photo, .. },
..
} => Some(photo),
Common { media_kind: Photo { photo, .. }, .. } => Some(photo),
_ => None,
}
}
pub fn sticker(&self) -> Option<&types::Sticker> {
match &self.kind {
Common {
media_kind: Sticker { sticker, .. },
..
} => Some(sticker),
Common { media_kind: Sticker { sticker, .. }, .. } => {
Some(sticker)
}
_ => None,
}
}
pub fn video(&self) -> Option<&types::Video> {
match &self.kind {
Common {
media_kind: Video { video, .. },
..
} => Some(video),
Common { media_kind: Video { video, .. }, .. } => Some(video),
_ => None,
}
}
pub fn voice(&self) -> Option<&types::Voice> {
match &self.kind {
Common {
media_kind: Voice { voice, .. },
..
} => Some(voice),
Common { media_kind: Voice { voice, .. }, .. } => Some(voice),
_ => None,
}
}
pub fn video_note(&self) -> Option<&types::VideoNote> {
match &self.kind {
Common {
media_kind: VideoNote { video_note, .. },
..
} => Some(video_note),
Common { media_kind: VideoNote { video_note, .. }, .. } => {
Some(video_note)
}
_ => None,
}
}
@ -615,40 +563,30 @@ mod getters {
pub fn contact(&self) -> Option<&types::Contact> {
match &self.kind {
Common {
media_kind: Contact { contact },
..
} => Some(contact),
Common { media_kind: Contact { contact }, .. } => Some(contact),
_ => None,
}
}
pub fn location(&self) -> Option<&types::Location> {
match &self.kind {
Common {
media_kind: Location { location, .. },
..
} => Some(location),
Common { media_kind: Location { location, .. }, .. } => {
Some(location)
}
_ => None,
}
}
pub fn venue(&self) -> Option<&types::Venue> {
match &self.kind {
Common {
media_kind: Venue { venue, .. },
..
} => Some(venue),
Common { media_kind: Venue { venue, .. }, .. } => Some(venue),
_ => None,
}
}
pub fn poll(&self) -> Option<&types::Poll> {
match &self.kind {
Common {
media_kind: Poll { poll, .. },
..
} => Some(poll),
Common { media_kind: Poll { poll, .. }, .. } => Some(poll),
_ => None,
}
}
@ -703,37 +641,34 @@ mod getters {
pub fn super_group_chat_created(&self) -> Option<True> {
match &self.kind {
SupergroupChatCreated {
supergroup_chat_created,
} => Some(*supergroup_chat_created),
SupergroupChatCreated { supergroup_chat_created } => {
Some(*supergroup_chat_created)
}
_ => None,
}
}
pub fn channel_chat_created(&self) -> Option<True> {
match &self.kind {
ChannelChatCreated {
channel_chat_created,
} => Some(*channel_chat_created),
ChannelChatCreated { channel_chat_created } => {
Some(*channel_chat_created)
}
_ => None,
}
}
pub fn migrate_to_chat_id(&self) -> Option<i64> {
match &self.kind {
Migrate {
migrate_to_chat_id, ..
} => Some(*migrate_to_chat_id),
Migrate { migrate_to_chat_id, .. } => Some(*migrate_to_chat_id),
_ => None,
}
}
pub fn migrate_from_chat_id(&self) -> Option<i64> {
match &self.kind {
Migrate {
migrate_from_chat_id,
..
} => Some(*migrate_from_chat_id),
Migrate { migrate_from_chat_id, .. } => {
Some(*migrate_from_chat_id)
}
_ => None,
}
}
@ -790,17 +725,13 @@ impl Message {
pub fn url(&self) -> Option<reqwest::Url> {
match &self.chat.kind {
ChatKind::NonPrivate {
kind:
NonPrivateChatKind::Channel {
username: Some(username),
},
kind: NonPrivateChatKind::Channel { username: Some(username) },
..
}
| ChatKind::NonPrivate {
kind:
NonPrivateChatKind::Supergroup {
username: Some(username),
..
username: Some(username), ..
},
..
} => Some(

View file

@ -58,9 +58,7 @@ mod tests {
assert_eq!(
MessageEntity {
kind: MessageEntityKind::TextLink {
url: "ya.ru".into()
},
kind: MessageEntityKind::TextLink { url: "ya.ru".into() },
offset: 1,
length: 2,
},
@ -122,9 +120,7 @@ mod tests {
username: None,
language_code: None,
}),
forward_kind: ForwardKind::Origin {
reply_to_message: None,
},
forward_kind: ForwardKind::Origin { reply_to_message: None },
edit_date: None,
media_kind: MediaKind::Text {
text: "no yes no".to_string(),

View file

@ -81,9 +81,7 @@ pub fn code_inline(s: &str) -> String {
///
/// [spec]: https://core.telegram.org/bots/api#html-style
pub fn escape(s: &str) -> String {
s.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
}
pub fn user_mention_or_link(user: &User) -> String {