mirror of
https://github.com/teloxide/teloxide.git
synced 2025-01-03 17:52:12 +01:00
Merge branch 'master' into redis
This commit is contained in:
commit
33910864c7
9 changed files with 543 additions and 277 deletions
86
.github/workflows/ci.yml
vendored
86
.github/workflows/ci.yml
vendored
|
@ -1,48 +1,64 @@
|
|||
on: [push, pull_request]
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
name: Continuous integration
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
code-checks:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- stable
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
toolchain: nightly
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Cargo fmt
|
||||
run: cargo +nightly fmt --all -- --check
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Cargo clippy
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
stable-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Setup redis
|
||||
run: sudo apt install redis-server && redis-server --port 7777 > /dev/null &
|
||||
|
||||
- name: stable/beta test
|
||||
uses: actions-rs/cargo@v1
|
||||
if: matrix.rust == 'stable' || matrix.rust == 'beta'
|
||||
- name: Cargo test
|
||||
run: cargo test --all-features
|
||||
build-example:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
example: [
|
||||
admin_bot,
|
||||
dialogue_bot,
|
||||
heroku_ping_pong_bot,
|
||||
ngrok_ping_pong_bot,
|
||||
ping_pong_bot,
|
||||
shared_state_bot,
|
||||
simple_commands_bot,
|
||||
]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
command: test
|
||||
args: --verbose --all-features
|
||||
|
||||
- name: stable/beta clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
if: matrix.rust == 'stable' || matrix.rust == 'beta'
|
||||
with:
|
||||
command: clippy
|
||||
args: --all-targets --all-features -- -D warnings
|
||||
|
||||
- name: Test the examples
|
||||
run: cd examples && bash test_examples.sh
|
||||
|
||||
- name: fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
if: matrix.rust == 'nightly'
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Test the example
|
||||
run: cd examples && cd ${{ matrix.example }} && cargo check
|
||||
|
|
|
@ -52,7 +52,7 @@ redis = { version = "0.15.1", optional = true }
|
|||
serde_cbor = { version = "0.11.1", optional = true }
|
||||
bincode = { version = "1.2.1", optional = true }
|
||||
|
||||
teloxide-macros = "0.2.1"
|
||||
teloxide-macros = "0.3.1"
|
||||
|
||||
[dev-dependencies]
|
||||
smart-default = "0.6.0"
|
||||
|
|
20
README.md
20
README.md
|
@ -35,9 +35,16 @@
|
|||
|
||||
## Features
|
||||
|
||||
<h3 align="center">Type safety</h3>
|
||||
<h3 align="center">Functional reactive design</h3>
|
||||
<p align="center">
|
||||
All the API <a href="https://docs.rs/teloxide/latest/teloxide/types/index.html">types</a> and <a href="https://docs.rs/teloxide/0.2.0/teloxide/requests/index.html">methods</a> are implemented with heavy use of <a href="https://en.wikipedia.org/wiki/Algebraic_data_type"><strong>ADT</strong>s</a> to enforce type safety and tight integration with IDEs. Bot's commands <a href="https://github.com/teloxide/teloxide#commands">have precise types too</a>, thereby serving as a self-documenting code and respecting the <a href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/">parse, don't validate</a> programming idiom.
|
||||
teloxide has <a href="https://en.wikipedia.org/wiki/Functional_reactive_programming">functional reactive design</a>, allowing you to declaratively manipulate streams of updates from Telegram using filters, maps, folds, zips, and a lot of <a href="https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html">other adaptors</a>.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3 align="center">API types as ADTs</h3>
|
||||
<p align="center">
|
||||
All the API <a href="https://docs.rs/teloxide/latest/teloxide/types/index.html">types</a> and <a href="https://docs.rs/teloxide/latest/teloxide/requests/index.html">methods</a> are hand-written, with heavy use of <a href="https://en.wikipedia.org/wiki/Algebraic_data_type"><strong>ADT</strong>s</a> (algebraic data types) to enforce type safety and tight integration with IDEs. As few <code>Option</code>s as possible.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
@ -47,6 +54,15 @@ All the API <a href="https://docs.rs/teloxide/latest/teloxide/types/index.html">
|
|||
Dialogues management is independent of how/where they are stored: just replace one line and make them <a href="https://en.wikipedia.org/wiki/Persistence_(computer_science)">persistent</a> (for example, store on a disk, transmit through a network), without affecting the actual <a href="https://en.wikipedia.org/wiki/Finite-state_machine">FSM</a> algorithm. By default, teloxide stores all user dialogues in RAM. Default database implementations <a href="https://github.com/teloxide/teloxide/issues/183">are coming</a>!
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3 align="center">Strongly typed bot commands</h3>
|
||||
<p align="center">
|
||||
You can describe bot commands as enumerations, and then they'll be automatically constructed from strings. Just like you describe JSON structures in <a href="https://github.com/serde-rs/json">serde-json</a> and command-line arguments in <a href="https://github.com/TeXitoi/structopt">structopt</a>.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
## Setting up your environment
|
||||
1. [Download Rust](http://rustup.rs/).
|
||||
2. Create a new bot using [@Botfather](https://t.me/botfather) to get a token in the format `123456789:blablabla`.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// TODO: simplify this and use typed command variants (see https://github.com/teloxide/teloxide/issues/152).
|
||||
use std::str::FromStr;
|
||||
|
||||
use teloxide::{
|
||||
prelude::*, types::ChatPermissions, utils::command::BotCommand,
|
||||
prelude::*, types::ChatPermissions, utils::command::BotCommand
|
||||
};
|
||||
|
||||
use futures::future;
|
||||
|
@ -18,82 +18,68 @@ use futures::future;
|
|||
#[derive(BotCommand)]
|
||||
#[command(
|
||||
rename = "lowercase",
|
||||
description = "Use commands in format /%command% %num% %unit%"
|
||||
description = "Use commands in format /%command% %num% %unit%",
|
||||
parse_with = "split"
|
||||
)]
|
||||
enum Command {
|
||||
#[command(description = "kick user from chat.")]
|
||||
Kick,
|
||||
#[command(description = "ban user in chat.")]
|
||||
Ban,
|
||||
Ban {
|
||||
time: u32,
|
||||
unit: UnitOfTime,
|
||||
},
|
||||
#[command(description = "mute user in chat.")]
|
||||
Mute,
|
||||
|
||||
Mute {
|
||||
time: u32,
|
||||
unit: UnitOfTime,
|
||||
},
|
||||
Help,
|
||||
}
|
||||
|
||||
enum UnitOfTime {
|
||||
Seconds,
|
||||
Minutes,
|
||||
Hours,
|
||||
}
|
||||
|
||||
impl FromStr for UnitOfTime {
|
||||
type Err = &'static str;
|
||||
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
|
||||
match s {
|
||||
"h" | "hours" => Ok(UnitOfTime::Hours),
|
||||
"m" | "minutes" => Ok(UnitOfTime::Minutes),
|
||||
"s" | "seconds" => Ok(UnitOfTime::Seconds),
|
||||
_ => Err("Allowed units: h, m, s"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculates time of user restriction.
|
||||
fn calc_restrict_time(num: i32, unit: &str) -> Result<i32, &str> {
|
||||
fn calc_restrict_time(time: u32, unit: UnitOfTime) -> u32 {
|
||||
match unit {
|
||||
"h" | "hours" => Ok(num * 3600),
|
||||
"m" | "minutes" => Ok(num * 60),
|
||||
"s" | "seconds" => Ok(num),
|
||||
_ => Err("Allowed units: h, m, s"),
|
||||
UnitOfTime::Hours => time * 3600,
|
||||
UnitOfTime::Minutes => time * 60,
|
||||
UnitOfTime::Seconds => time,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse arguments after a command.
|
||||
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%"),
|
||||
};
|
||||
let unit = match args.get(1) {
|
||||
Some(s) => s,
|
||||
None => return Err("Use command in format /%command% %num% %unit%"),
|
||||
};
|
||||
|
||||
match num.parse::<i32>() {
|
||||
Ok(n) => Ok((n, unit)),
|
||||
Err(_) => Err("input positive number!"),
|
||||
}
|
||||
}
|
||||
|
||||
// Parse arguments into a user restriction duration.
|
||||
fn parse_time_restrict(args: &[String]) -> Result<i32, &str> {
|
||||
let (num, unit) = parse_args(args)?;
|
||||
calc_restrict_time(num, unit)
|
||||
}
|
||||
|
||||
type Cx = DispatcherHandlerCx<Message>;
|
||||
type Cx = UpdateWithCx<Message>;
|
||||
|
||||
// Mute a user with a replied message.
|
||||
async fn mute_user(cx: &Cx, args: &[String]) -> ResponseResult<()> {
|
||||
async fn mute_user(cx: &Cx, time: u32) -> ResponseResult<()> {
|
||||
match cx.update.reply_to_message() {
|
||||
Some(msg1) => match parse_time_restrict(args) {
|
||||
// Mute user temporarily...
|
||||
Ok(time) => {
|
||||
cx.bot
|
||||
.restrict_chat_member(
|
||||
cx.update.chat_id(),
|
||||
msg1.from().expect("Must be MessageKind::Common").id,
|
||||
ChatPermissions::default(),
|
||||
)
|
||||
.until_date(cx.update.date + time)
|
||||
.send()
|
||||
.await?;
|
||||
}
|
||||
// ...or permanently
|
||||
Err(_) => {
|
||||
cx.bot
|
||||
.restrict_chat_member(
|
||||
cx.update.chat_id(),
|
||||
msg1.from().unwrap().id,
|
||||
ChatPermissions::default(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
}
|
||||
},
|
||||
Some(msg1) => {
|
||||
cx.bot
|
||||
.restrict_chat_member(
|
||||
cx.update.chat_id(),
|
||||
msg1.from().expect("Must be MessageKind::Common").id,
|
||||
ChatPermissions::default(),
|
||||
)
|
||||
.until_date(cx.update.date + time as i32)
|
||||
.send()
|
||||
.await?;
|
||||
}
|
||||
None => {
|
||||
cx.reply_to("Use this command in reply to another message")
|
||||
.send()
|
||||
|
@ -123,31 +109,18 @@ async fn kick_user(cx: &Cx) -> ResponseResult<()> {
|
|||
}
|
||||
|
||||
// Ban a user with replied message.
|
||||
async fn ban_user(cx: &Cx, args: &[String]) -> ResponseResult<()> {
|
||||
async fn ban_user(cx: &Cx, time: u32) -> ResponseResult<()> {
|
||||
match cx.update.reply_to_message() {
|
||||
Some(message) => match parse_time_restrict(args) {
|
||||
// Mute user temporarily...
|
||||
Ok(time) => {
|
||||
cx.bot
|
||||
.kick_chat_member(
|
||||
cx.update.chat_id(),
|
||||
message.from().expect("Must be MessageKind::Common").id,
|
||||
)
|
||||
.until_date(cx.update.date + time)
|
||||
.send()
|
||||
.await?;
|
||||
}
|
||||
// ...or permanently
|
||||
Err(_) => {
|
||||
cx.bot
|
||||
.kick_chat_member(
|
||||
cx.update.chat_id(),
|
||||
message.from().unwrap().id,
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
}
|
||||
},
|
||||
Some(message) => {
|
||||
cx.bot
|
||||
.kick_chat_member(
|
||||
cx.update.chat_id(),
|
||||
message.from().expect("Must be MessageKind::Common").id,
|
||||
)
|
||||
.until_date(cx.update.date + time as i32)
|
||||
.send()
|
||||
.await?;
|
||||
}
|
||||
None => {
|
||||
cx.reply_to("Use this command in a reply to another message!")
|
||||
.send()
|
||||
|
@ -158,17 +131,20 @@ async fn ban_user(cx: &Cx, args: &[String]) -> ResponseResult<()> {
|
|||
}
|
||||
|
||||
async fn action(
|
||||
cx: DispatcherHandlerCx<Message>,
|
||||
cx: UpdateWithCx<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?,
|
||||
Command::Ban { time, unit } => {
|
||||
ban_user(&cx, calc_restrict_time(time, unit)).await?
|
||||
}
|
||||
Command::Mute { time, unit } => {
|
||||
mute_user(&cx, calc_restrict_time(time, unit)).await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
|
@ -177,8 +153,9 @@ async fn action(
|
|||
async fn handle_commands(rx: DispatcherHandlerRx<Message>) {
|
||||
rx.filter(|cx| future::ready(cx.update.chat.is_group()))
|
||||
.commands::<Command, &str>(panic!("Insert here your bot's name"))
|
||||
.for_each_concurrent(None, |(cx, command, args)| async move {
|
||||
action(cx, command, &args).await.log_on_error().await;
|
||||
// Execute all incoming commands concurrently:
|
||||
.for_each_concurrent(None, |(cx, command)| async move {
|
||||
action(cx, command).await.log_on_error().await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ async fn answer(
|
|||
|
||||
async fn handle_commands(rx: DispatcherHandlerRx<Message>) {
|
||||
rx.commands::<Command, &str>(panic!("Insert here your bot's name"))
|
||||
.for_each_concurrent(None, |(cx, command, _)| async move {
|
||||
.for_each_concurrent(None, |(cx, command)| async move {
|
||||
answer(cx, command).await.log_on_error().await;
|
||||
})
|
||||
.await;
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
##!/bin/sh
|
||||
##!/bin/sh
|
||||
|
||||
for example in */; do
|
||||
echo Testing $example...
|
||||
cd $example; cargo check; cd ..;
|
||||
cd $example
|
||||
cargo check &
|
||||
cd ..
|
||||
done
|
||||
|
||||
wait
|
||||
|
|
|
@ -19,7 +19,7 @@ pub trait DispatcherHandlerRxExt {
|
|||
fn commands<C, N>(
|
||||
self,
|
||||
bot_name: N,
|
||||
) -> BoxStream<'static, (UpdateWithCx<Message>, C, Vec<String>)>
|
||||
) -> BoxStream<'static, (UpdateWithCx<Message>, C)>
|
||||
where
|
||||
Self: Stream<Item = UpdateWithCx<Message>>,
|
||||
C: BotCommand,
|
||||
|
@ -44,7 +44,7 @@ where
|
|||
fn commands<C, N>(
|
||||
self,
|
||||
bot_name: N,
|
||||
) -> BoxStream<'static, (UpdateWithCx<Message>, C, Vec<String>)>
|
||||
) -> BoxStream<'static, (UpdateWithCx<Message>, C)>
|
||||
where
|
||||
Self: Stream<Item = UpdateWithCx<Message>>,
|
||||
C: BotCommand,
|
||||
|
@ -56,15 +56,7 @@ where
|
|||
let bot_name = bot_name.clone();
|
||||
|
||||
async move {
|
||||
C::parse(&text, &bot_name).map(|(command, args)| {
|
||||
(
|
||||
cx,
|
||||
command,
|
||||
args.into_iter()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
})
|
||||
C::parse(&text, &bot_name).map(|command| (cx, command)).ok()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
//! Command parsers.
|
||||
//!
|
||||
//! You can either create an `enum`, containing commands of your bot, or use
|
||||
//! functions, which split input text into a string command with its arguments.
|
||||
//! You can either create an `enum` with derived [`BotCommand`], containing
|
||||
//! commands of your bot, or use functions, which split input text into a string
|
||||
//! command with its arguments.
|
||||
//!
|
||||
//! ## Examples
|
||||
//! Using `enum`:
|
||||
//! # Using BotCommand
|
||||
//! ```
|
||||
//! use teloxide::utils::command::BotCommand;
|
||||
//!
|
||||
//! type UnitOfTime = u8;
|
||||
//!
|
||||
//! #[derive(BotCommand, PartialEq, Debug)]
|
||||
//! #[command(rename = "lowercase")]
|
||||
//! #[command(rename = "lowercase", parse_with = "split")]
|
||||
//! enum AdminCommand {
|
||||
//! Kick,
|
||||
//! Ban,
|
||||
//! Mute(UnitOfTime, char),
|
||||
//! Ban(UnitOfTime, char),
|
||||
//! }
|
||||
//!
|
||||
//! let (command, args) =
|
||||
//! AdminCommand::parse("/ban 3 hours", "MyBotName").unwrap();
|
||||
//! assert_eq!(command, AdminCommand::Ban);
|
||||
//! assert_eq!(args, vec!["3", "hours"]);
|
||||
//! let command = AdminCommand::parse("/ban 5 h", "bot_name").unwrap();
|
||||
//! assert_eq!(command, AdminCommand::Ban(5, 'h'));
|
||||
//! ```
|
||||
//!
|
||||
//! Using [`parse_command`]:
|
||||
//! # Using parse_command
|
||||
//! ```
|
||||
//! use teloxide::utils::command::parse_command;
|
||||
//!
|
||||
|
@ -31,7 +31,7 @@
|
|||
//! assert_eq!(args, vec!["3", "hours"]);
|
||||
//! ```
|
||||
//!
|
||||
//! Using [`parse_command_with_prefix`]:
|
||||
//! # Using parse_command_with_prefix
|
||||
//! ```
|
||||
//! use teloxide::utils::command::parse_command_with_prefix;
|
||||
//!
|
||||
|
@ -41,45 +41,37 @@
|
|||
//! assert_eq!(args, vec!["3", "hours"]);
|
||||
//! ```
|
||||
//!
|
||||
//! If the name of a bot does not match, it will return `None`:
|
||||
//! ```
|
||||
//! use teloxide::utils::command::parse_command;
|
||||
//!
|
||||
//! let result = parse_command("/ban@MyNameBot1 3 hours", "MyNameBot2");
|
||||
//! assert!(result.is_none());
|
||||
//! ```
|
||||
//!
|
||||
//! See [examples/admin_bot] as a more complicated examples.
|
||||
//!
|
||||
//! [`parse_command`]: crate::utils::command::parse_command
|
||||
//! [`parse_command_with_prefix`]:
|
||||
//! crate::utils::command::parse_command_with_prefix
|
||||
//! [examples/admin_bot]: https://github.com/teloxide/teloxide/blob/master/examples/miltiple_handlers_bot/
|
||||
//! [examples/admin_bot]: https://github.com/teloxide/teloxide/blob/master/examples/admin_bot/
|
||||
|
||||
use serde::export::Formatter;
|
||||
use std::{error::Error, fmt::Display};
|
||||
pub use teloxide_macros::BotCommand;
|
||||
|
||||
/// An enumeration of bot's commands.
|
||||
///
|
||||
/// ## Example
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use teloxide::utils::command::BotCommand;
|
||||
///
|
||||
/// type UnitOfTime = u8;
|
||||
///
|
||||
/// #[derive(BotCommand, PartialEq, Debug)]
|
||||
/// #[command(rename = "lowercase")]
|
||||
/// #[command(rename = "lowercase", parse_with = "split")]
|
||||
/// enum AdminCommand {
|
||||
/// Mute,
|
||||
/// Ban,
|
||||
/// Mute(UnitOfTime, char),
|
||||
/// Ban(UnitOfTime, char),
|
||||
/// }
|
||||
///
|
||||
/// let (command, args) = AdminCommand::parse("/ban 5 h", "bot_name").unwrap();
|
||||
/// assert_eq!(command, AdminCommand::Ban);
|
||||
/// assert_eq!(args, vec!["5", "h"]);
|
||||
/// let command = AdminCommand::parse("/ban 5 h", "bot_name").unwrap();
|
||||
/// assert_eq!(command, AdminCommand::Ban(5, 'h'));
|
||||
/// ```
|
||||
///
|
||||
/// ## Enum attributes
|
||||
/// 1. `#[command(rename = "rule")]`
|
||||
/// Rename all commands by rule. Allowed rules are `lowercase`. If you will not
|
||||
/// use this attribute, commands will be parsed by their original names.
|
||||
/// Rename all commands by `rule`. Allowed rules are `lowercase`. If you will
|
||||
/// not use this attribute, commands will be parsed by their original names.
|
||||
///
|
||||
/// 2. `#[command(prefix = "prefix")]`
|
||||
/// Change a prefix for all commands (the default is `/`).
|
||||
|
@ -87,29 +79,186 @@ pub use teloxide_macros::BotCommand;
|
|||
/// 3. `#[command(description = "description")]`
|
||||
/// Add a sumary description of commands before all commands.
|
||||
///
|
||||
/// 4. `#[command(parse_with = "parser")]`
|
||||
/// Change the parser of arguments. Possible values:
|
||||
/// - `default` - the same as the unspecified parser. It only puts all text
|
||||
/// after the first space into the first argument, which must implement
|
||||
/// [`FromStr`].
|
||||
///
|
||||
/// ### Example
|
||||
/// ```
|
||||
/// use teloxide::utils::command::BotCommand;
|
||||
///
|
||||
/// #[derive(BotCommand, PartialEq, Debug)]
|
||||
/// #[command(rename = "lowercase")]
|
||||
/// enum Command {
|
||||
/// Text(String),
|
||||
/// }
|
||||
///
|
||||
/// let command = Command::parse("/text hello my dear friend!", "").unwrap();
|
||||
/// assert_eq!(command, Command::Text("hello my dear friend!".to_string()));
|
||||
/// ```
|
||||
///
|
||||
/// - `split` - separates a messsage by a given separator (the default is the
|
||||
/// space character) and parses each part into the corresponding arguments,
|
||||
/// which must implement [`FromStr`].
|
||||
///
|
||||
/// ### Example
|
||||
/// ```
|
||||
/// use teloxide::utils::command::BotCommand;
|
||||
///
|
||||
/// #[derive(BotCommand, PartialEq, Debug)]
|
||||
/// #[command(rename = "lowercase", parse_with = "split")]
|
||||
/// enum Command {
|
||||
/// Nums(u8, u16, i32),
|
||||
/// }
|
||||
///
|
||||
/// let command = Command::parse("/nums 1 32 -5", "").unwrap();
|
||||
/// assert_eq!(command, Command::Nums(1, 32, -5));
|
||||
/// ```
|
||||
///
|
||||
/// 5. `#[command(separator = "sep")]`
|
||||
/// Specify separator used by the `split` parser. It will be ignored when
|
||||
/// accompanied by another type of parsers.
|
||||
///
|
||||
/// ### Example
|
||||
/// ```
|
||||
/// use teloxide::utils::command::BotCommand;
|
||||
///
|
||||
/// #[derive(BotCommand, PartialEq, Debug)]
|
||||
/// #[command(rename = "lowercase", parse_with = "split", separator = "|")]
|
||||
/// enum Command {
|
||||
/// Nums(u8, u16, i32),
|
||||
/// }
|
||||
///
|
||||
/// let command = Command::parse("/nums 1|32|5", "").unwrap();
|
||||
/// assert_eq!(command, Command::Nums(1, 32, 5));
|
||||
/// ```
|
||||
///
|
||||
/// ## Variant attributes
|
||||
/// All variant attributes override the corresponding `enum` attributes.
|
||||
///
|
||||
/// 1. `#[command(rename = "rule")]`
|
||||
/// Rename one command by a rule. Allowed rules are `lowercase`, `%some_name%`,
|
||||
/// where `%some_name%` is any string, a new name.
|
||||
///
|
||||
/// 2. `#[command(prefix = "prefix")]`
|
||||
/// Change a prefix for one command (the default is `/`).
|
||||
/// 2. `#[command(parse_with = "parser")]`
|
||||
/// One more option is available for variants.
|
||||
/// - `custom_parser` - your own parser of the signature `fn(String) ->
|
||||
/// Result<Tuple, ParseError>`, where `Tuple` corresponds to the variant's
|
||||
/// arguments.
|
||||
///
|
||||
/// 3. `#[command(description = "description")]`
|
||||
/// Add a description of one command.
|
||||
/// ### Example
|
||||
/// ```
|
||||
/// use teloxide::utils::command::{BotCommand, ParseError};
|
||||
///
|
||||
/// All variant attributes overlap the `enum` attributes.
|
||||
/// fn accept_two_digits(input: String) -> Result<(u8,), ParseError> {
|
||||
/// match input.len() {
|
||||
/// 2 => {
|
||||
/// let num = input
|
||||
/// .parse::<u8>()
|
||||
/// .map_err(|e| ParseError::IncorrectFormat(e.into()))?;
|
||||
/// Ok((num,))
|
||||
/// }
|
||||
/// len => Err(ParseError::Custom(
|
||||
/// format!("Only 2 digits allowed, not {}", len).into(),
|
||||
/// )),
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[derive(BotCommand, PartialEq, Debug)]
|
||||
/// #[command(rename = "lowercase")]
|
||||
/// enum Command {
|
||||
/// #[command(parse_with = "accept_two_digits")]
|
||||
/// Num(u8),
|
||||
/// }
|
||||
///
|
||||
/// let command = Command::parse("/num 12", "").unwrap();
|
||||
/// assert_eq!(command, Command::Num(12));
|
||||
/// let command = Command::parse("/num 333", "");
|
||||
/// assert!(command.is_err());
|
||||
/// ```
|
||||
///
|
||||
/// 3. `#[command(prefix = "prefix")]`
|
||||
/// 4. `#[command(description = "description")]`
|
||||
/// 5. `#[command(separator = "sep")]`
|
||||
///
|
||||
/// Analogous to the descriptions above.
|
||||
///
|
||||
/// [`FromStr`]: https://doc.rust-lang.org/std/str/trait.FromStr.html
|
||||
/// [`BotCommand`]: crate::utils::command::BotCommand
|
||||
pub trait BotCommand: Sized {
|
||||
fn try_from(s: &str) -> Option<Self>;
|
||||
fn descriptions() -> String;
|
||||
fn parse<N>(s: &str, bot_name: N) -> Option<(Self, Vec<&str>)>
|
||||
fn parse<N>(s: &str, bot_name: N) -> Result<Self, ParseError>
|
||||
where
|
||||
N: Into<String>;
|
||||
}
|
||||
|
||||
pub type PrefixedBotCommand = String;
|
||||
pub type BotName = String;
|
||||
|
||||
/// Errors returned from [`BotCommand::parse`].
|
||||
///
|
||||
/// [`BotCommand::parse`]: crate::utils::command::BotCommand::parse
|
||||
#[derive(Debug)]
|
||||
pub enum ParseError {
|
||||
TooFewArguments {
|
||||
expected: usize,
|
||||
found: usize,
|
||||
message: String,
|
||||
},
|
||||
TooManyArguments {
|
||||
expected: usize,
|
||||
found: usize,
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// Redirected from [`FromStr::from_str`].
|
||||
///
|
||||
/// [`FromStr::from_str`]: https://doc.rust-lang.org/std/str/trait.FromStr.html#tymethod.from_str
|
||||
IncorrectFormat(Box<dyn Error>),
|
||||
|
||||
UnknownCommand(PrefixedBotCommand),
|
||||
WrongBotName(BotName),
|
||||
|
||||
/// A custom error which you can return from your custom parser.
|
||||
Custom(Box<dyn Error>),
|
||||
}
|
||||
|
||||
impl Display for ParseError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
ParseError::TooFewArguments { expected, found, message } => write!(
|
||||
f,
|
||||
"Too few arguments (expected {}, found {}, message = '{}')",
|
||||
expected, found, message
|
||||
),
|
||||
ParseError::TooManyArguments { expected, found, message } => {
|
||||
write!(
|
||||
f,
|
||||
"Too many arguments (expected {}, found {}, message = \
|
||||
'{}')",
|
||||
expected, found, message
|
||||
)
|
||||
}
|
||||
ParseError::IncorrectFormat(e) => {
|
||||
write!(f, "Incorrect format of command args: {}", e)
|
||||
}
|
||||
ParseError::UnknownCommand(e) => {
|
||||
write!(f, "Unknown command: {}", e)
|
||||
}
|
||||
ParseError::WrongBotName(n) => write!(f, "Wrong bot name: {}", n),
|
||||
ParseError::Custom(e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParseError {}
|
||||
|
||||
/// Parses a string into a command with args.
|
||||
///
|
||||
/// It calls [`parse_command_with_prefix`] with the default prefix `/`.
|
||||
/// This function is just a shortcut for calling [`parse_command_with_prefix`]
|
||||
/// with the default prefix `/`.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```
|
||||
|
@ -121,6 +270,14 @@ pub trait BotCommand: Sized {
|
|||
/// assert_eq!(args, vec!["5", "hours"]);
|
||||
/// ```
|
||||
///
|
||||
/// If the name of a bot does not match, it will return `None`:
|
||||
/// ```
|
||||
/// use teloxide::utils::command::parse_command;
|
||||
///
|
||||
/// let result = parse_command("/ban@MyNameBot1 3 hours", "MyNameBot2");
|
||||
/// assert!(result.is_none());
|
||||
/// ```
|
||||
///
|
||||
/// [`parse_command_with_prefix`]:
|
||||
/// crate::utils::command::parse_command_with_prefix
|
||||
pub fn parse_command<N>(text: &str, bot_name: N) -> Option<(&str, Vec<&str>)>
|
||||
|
@ -143,6 +300,15 @@ where
|
|||
/// assert_eq!(command, "mute");
|
||||
/// assert_eq!(args, vec!["5", "hours"]);
|
||||
/// ```
|
||||
///
|
||||
/// If the name of a bot does not match, it will return `None`:
|
||||
/// ```
|
||||
/// use teloxide::utils::command::parse_command_with_prefix;
|
||||
///
|
||||
/// let result =
|
||||
/// parse_command_with_prefix("!", "!ban@MyNameBot1 3 hours", "MyNameBot2");
|
||||
/// assert!(result.is_none());
|
||||
/// ```
|
||||
pub fn parse_command_with_prefix<'a, N>(
|
||||
prefix: &str,
|
||||
text: &'a str,
|
||||
|
@ -166,6 +332,8 @@ where
|
|||
Some((command, words.collect()))
|
||||
}
|
||||
|
||||
// The rest of tests are integrational due to problems with macro expansion in
|
||||
// unit tests.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -185,96 +353,4 @@ mod tests {
|
|||
let actual = parse_command(data, "");
|
||||
assert_eq!(actual, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_command_with_args() {
|
||||
#[command(rename = "lowercase")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
Start,
|
||||
Help,
|
||||
}
|
||||
|
||||
let data = "/start arg1 arg2";
|
||||
let expected = Some((DefaultCommands::Start, vec!["arg1", "arg2"]));
|
||||
let actual = DefaultCommands::parse(data, "");
|
||||
assert_eq!(actual, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attribute_prefix() {
|
||||
#[command(rename = "lowercase")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
#[command(prefix = "!")]
|
||||
Start,
|
||||
Help,
|
||||
}
|
||||
|
||||
let data = "!start arg1 arg2";
|
||||
let expected = Some((DefaultCommands::Start, vec!["arg1", "arg2"]));
|
||||
let actual = DefaultCommands::parse(data, "");
|
||||
assert_eq!(actual, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_attributes() {
|
||||
#[command(rename = "lowercase")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
#[command(prefix = "!", description = "desc")]
|
||||
Start,
|
||||
Help,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
DefaultCommands::Start,
|
||||
DefaultCommands::parse("!start", "").unwrap().0
|
||||
);
|
||||
assert_eq!(DefaultCommands::descriptions(), "!start - desc\n/help\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_attributes() {
|
||||
#[command(
|
||||
prefix = "!",
|
||||
rename = "lowercase",
|
||||
description = "Bot commands"
|
||||
)]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
#[command(prefix = "/")]
|
||||
Start,
|
||||
Help,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
DefaultCommands::Start,
|
||||
DefaultCommands::parse("/start", "MyNameBot").unwrap().0
|
||||
);
|
||||
assert_eq!(
|
||||
DefaultCommands::Help,
|
||||
DefaultCommands::parse("!help", "MyNameBot").unwrap().0
|
||||
);
|
||||
assert_eq!(
|
||||
DefaultCommands::descriptions(),
|
||||
"Bot commands\n/start\n!help\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_command_with_bot_name() {
|
||||
#[command(rename = "lowercase")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
#[command(prefix = "/")]
|
||||
Start,
|
||||
Help,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
DefaultCommands::Start,
|
||||
DefaultCommands::parse("/start@MyNameBot", "MyNameBot").unwrap().0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
186
tests/command.rs
Normal file
186
tests/command.rs
Normal file
|
@ -0,0 +1,186 @@
|
|||
use teloxide::utils::command::{BotCommand, ParseError};
|
||||
|
||||
// We put tests here because macro expand in unit tests in module
|
||||
// teloxide::utils::command was a failure
|
||||
|
||||
#[test]
|
||||
fn parse_command_with_args() {
|
||||
#[command(rename = "lowercase")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
Start(String),
|
||||
Help,
|
||||
}
|
||||
|
||||
let data = "/start arg1 arg2";
|
||||
let expected = DefaultCommands::Start("arg1 arg2".to_string());
|
||||
let actual = DefaultCommands::parse(data, "").unwrap();
|
||||
assert_eq!(actual, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attribute_prefix() {
|
||||
#[command(rename = "lowercase")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
#[command(prefix = "!")]
|
||||
Start(String),
|
||||
Help,
|
||||
}
|
||||
|
||||
let data = "!start arg1 arg2";
|
||||
let expected = DefaultCommands::Start("arg1 arg2".to_string());
|
||||
let actual = DefaultCommands::parse(data, "").unwrap();
|
||||
assert_eq!(actual, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_attributes() {
|
||||
#[command(rename = "lowercase")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
#[command(prefix = "!", description = "desc")]
|
||||
Start,
|
||||
Help,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
DefaultCommands::Start,
|
||||
DefaultCommands::parse("!start", "").unwrap()
|
||||
);
|
||||
assert_eq!(DefaultCommands::descriptions(), "!start - desc\n/help\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_attributes() {
|
||||
#[command(prefix = "!", rename = "lowercase", description = "Bot commands")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
#[command(prefix = "/")]
|
||||
Start,
|
||||
Help,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
DefaultCommands::Start,
|
||||
DefaultCommands::parse("/start", "MyNameBot").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
DefaultCommands::Help,
|
||||
DefaultCommands::parse("!help", "MyNameBot").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
DefaultCommands::descriptions(),
|
||||
"Bot commands\n/start\n!help\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_command_with_bot_name() {
|
||||
#[command(rename = "lowercase")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
#[command(prefix = "/")]
|
||||
Start,
|
||||
Help,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
DefaultCommands::Start,
|
||||
DefaultCommands::parse("/start@MyNameBot", "MyNameBot").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_split() {
|
||||
#[command(rename = "lowercase")]
|
||||
#[command(parse_with = "split")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
Start(u8, String),
|
||||
Help,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
DefaultCommands::Start(10, "hello".to_string()),
|
||||
DefaultCommands::parse("/start 10 hello", "").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_split2() {
|
||||
#[command(rename = "lowercase")]
|
||||
#[command(parse_with = "split", separator = "|")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
Start(u8, String),
|
||||
Help,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
DefaultCommands::Start(10, "hello".to_string()),
|
||||
DefaultCommands::parse("/start 10|hello", "").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_custom_parser() {
|
||||
fn custom_parse_function(s: String) -> Result<(u8, String), ParseError> {
|
||||
let vec = s.split_whitespace().collect::<Vec<_>>();
|
||||
let (left, right) = match vec.as_slice() {
|
||||
[l, r] => (l, r),
|
||||
_ => {
|
||||
return Err(ParseError::IncorrectFormat(
|
||||
"might be 2 arguments!".into(),
|
||||
))
|
||||
}
|
||||
};
|
||||
left.parse::<u8>().map(|res| (res, right.to_string())).map_err(|_| {
|
||||
ParseError::Custom(
|
||||
"First argument must be a integer!".to_owned().into(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[command(rename = "lowercase")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
#[command(parse_with = "custom_parse_function")]
|
||||
Start(u8, String),
|
||||
Help,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
DefaultCommands::Start(10, "hello".to_string()),
|
||||
DefaultCommands::parse("/start 10 hello", "").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_named_fields() {
|
||||
#[command(rename = "lowercase")]
|
||||
#[command(parse_with = "split")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
Start { num: u8, data: String },
|
||||
Help,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
DefaultCommands::Start { num: 10, data: "hello".to_string() },
|
||||
DefaultCommands::parse("/start 10 hello", "").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descriptions_off() {
|
||||
#[command(rename = "lowercase")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
#[command(description = "off")]
|
||||
Start,
|
||||
Help,
|
||||
}
|
||||
|
||||
assert_eq!(DefaultCommands::descriptions(), "/help\n".to_owned());
|
||||
}
|
Loading…
Reference in a new issue