mirror of
https://github.com/teloxide/teloxide.git
synced 2024-12-22 14:35:36 +01:00
Fixes
This commit is contained in:
parent
f20932a730
commit
fa554a8252
58 changed files with 774 additions and 757 deletions
129
README.md
129
README.md
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 = "../../" }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = "../../" }
|
|
@ -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.
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -60,10 +60,7 @@ impl Bot {
|
|||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
Arc::new(Self {
|
||||
token: token.into(),
|
||||
client,
|
||||
})
|
||||
Arc::new(Self { token: token.into(), client })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>>;
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
314
src/lib.rs
314
src/lib.rs
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
}
|
||||
|
||||
pub fn user_mention_or_link(user: &User) -> String {
|
||||
|
|
Loading…
Reference in a new issue