Merge branch 'dev' of github.com:teloxide/teloxide into from-env-proxy

Потому что сразу надо было пр мерджить
This commit is contained in:
Mr-Andersen 2020-03-06 12:16:36 +03:00
commit 2f4dfd8f0c
30 changed files with 312 additions and 624 deletions

7
.gitignore vendored
View file

@ -3,9 +3,4 @@
Cargo.lock
.idea/
.vscode/
examples/ping_pong_bot/target
examples/dialogue_bot/target
examples/multiple_handlers_bot/target
examples/admin_bot/target
examples/guess_a_number_bot/target
examples/simple_commands_bot/target
examples/*/target

24
CHANGELOG.md Normal file
View file

@ -0,0 +1,24 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.0] - 2020-02-25
### Added
- The functionality to parse commands only with a correct bot's name (breaks backwards compatibility) (https://github.com/teloxide/teloxide/issues/168).
- This `CHANGELOG.md`.
### Fixed
- Fix parsing a pinned message (https://github.com/teloxide/teloxide/issues/167).
- Replace `LanguageCode` with `String`, Because [the official Telegram documentation](https://core.telegram.org/bots/api#getchat) doesn't specify a concrete version of IETF language tag.
- Problems with the `poll_type` field (https://github.com/teloxide/teloxide/issues/178).
- Make `polling_default` actually a long polling update listener (https://github.com/teloxide/teloxide/pull/182).
### Removed
- [either](https://crates.io/crates/either) from the dependencies in `Cargo.toml`.
- `teloxide-macros` migrated into [the separate repository](https://github.com/teloxide/teloxide-macros) to easier releases and testing.
## [0.1.0] - 2020-02-19
### Added
- This project.

View file

@ -1,7 +1,7 @@
# Contributing
Before contributing, please read [our code style](https://github.com/teloxide/teloxide/blob/master/CODE_STYLE.md) and [the license](https://github.com/teloxide/teloxide/blob/master/LICENSE).
To change the source code, fork this repository and work inside your own branch. Then send us a PR and wait for the CI to check everything. However, you'd better check changes first locally:
To change the source code, fork this repository and work inside your own branch. Then send us a PR into the [dev](https://github.com/teloxide/teloxide/tree/dev) branch and wait for the CI to check everything. However, you'd better check changes first locally:
```
cargo clippy --all --all-features --all-targets

View file

@ -1,6 +1,6 @@
[package]
name = "teloxide"
version = "0.1.0"
version = "0.2.0"
edition = "2018"
description = "An elegant Telegram bots framework for Rust"
repository = "https://github.com/teloxide/teloxide"
@ -44,9 +44,8 @@ async-trait = "0.1.22"
futures = "0.3.1"
pin-project = "0.4.6"
serde_with_macros = "1.0.1"
either = "1.5.3"
teloxide-macros = "0.1.0"
teloxide-macros = "0.2.1"
[dev-dependencies]
smart-default = "0.6.0"

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 teloxide
Copyright (c) 2019-2020 teloxide
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -3,13 +3,13 @@
<h1>teloxide</h1>
<a href="https://docs.rs/teloxide/">
<img src="https://img.shields.io/badge/docs.rs-v0.1.0-blue.svg">
<img src="https://img.shields.io/badge/docs.rs-v0.2.0-blue.svg">
</a>
<a href="https://github.com/teloxide/teloxide/actions">
<img src="https://github.com/teloxide/teloxide/workflows/Continuous%20integration/badge.svg">
</a>
<a href="https://crates.io/crates/teloxide">
<img src="https://img.shields.io/badge/crates.io-v0.1.0-orange.svg">
<img src="https://img.shields.io/badge/crates.io-v0.2.0-orange.svg">
</a>
<a href="https://t.me/teloxide">
<img src="https://img.shields.io/badge/official%20chat-t.me%2Fteloxide-blueviolet">
@ -34,9 +34,11 @@
- [Contributing](https://github.com/teloxide/teloxide#contributing)
## Features
- **Declarative API.** You tell teloxide what you want instead of describing what to do.
- **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.
- **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. Feel free to glue handlers both horizontally and vertically.
- **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.
@ -51,17 +53,23 @@
$ export TELOXIDE_TOKEN=<Your token here>
# Windows
$ set TELOXITE_TOKEN=<Your token here>
$ set TELOXIDE_TOKEN=<Your token here>
```
3. Be sure that you are up to date:
```bash
# If you're using stable
$ rustup update stable
$ rustup override set stable
# If you're using nightly
$ rustup update nightly
$ rustup override set nightly
```
4. Execute `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`:
```toml
[dependencies]
teloxide = "0.1.0"
teloxide = "0.2.0"
log = "0.4.8"
tokio = "0.2.11"
pretty_env_logger = "0.4.0"
@ -136,7 +144,7 @@ async fn answer(
async fn handle_commands(rx: DispatcherHandlerRx<Message>) {
// Only iterate through commands in a proper format:
rx.commands::<Command>()
rx.commands::<Command, &str>(panic!("Insert here your bot's name"))
// Execute all incoming commands concurrently:
.for_each_concurrent(None, |(cx, command, _)| async move {
answer(cx, command).await.log_on_error().await;
@ -280,13 +288,13 @@ The second one produces very strange compiler messages because of the `#[tokio::
## FAQ
### Where I can ask questions?
[Issues](https://github.com/teloxide/teloxide/issues) is a good place for well-formed questions, for example, about the library design, enhancements, bug reports. But if you can't compile your bot due to compilation errors and need a quick help, feel free to ask in our official group: https://t.me/teloxide.
[Issues](https://github.com/teloxide/teloxide/issues) is a good place for well-formed questions, for example, about the library design, enhancements, bug reports. But if you can't compile your bot due to compilation errors and need quick help, feel free to ask in our official group: https://t.me/teloxide.
### Why Rust?
Most programming languages have their own implementations of Telegram bots frameworks, so why not Rust? We think Rust provides enough good ecosystem and the language itself to be suitable for writing bots.
## Community bots
Feel free to push your own bot into our collection: https://github.com/teloxide/community-bots. Later you will be able to play with them right in our official chat: https://t.me/teloxide (coming soon...).
Feel free to push your own bot into our collection: https://github.com/teloxide/community-bots. Later you will be able to play with them right in our official chat: https://t.me/teloxide.
## Contributing
See [CONRIBUTING.md](https://github.com/teloxide/teloxide/blob/master/CONTRIBUTING.md).

View file

@ -7,4 +7,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. |
| [shared_state_bot](shared_state_bot) | A bot that shows how to deal with shared state. |

View file

@ -179,7 +179,7 @@ async fn handle_commands(rx: DispatcherHandlerRx<Message>) {
// Only iterate through messages from groups:
rx.filter(|cx| future::ready(cx.update.chat.is_group()))
// Only iterate through commands in a proper format:
.commands::<Command>()
.commands::<Command, &str>(panic!("Insert here your bot's name"))
// Execute all incoming commands concurrently:
.for_each_concurrent(None, |(cx, command, args)| async move {
action(cx, command, &args).await.log_on_error().await;

View file

@ -0,0 +1,14 @@
[package]
name = "shared_state_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"
lazy_static = "1.4.0"
teloxide = { path = "../../" }

View file

@ -0,0 +1,41 @@
// This bot answers how many messages it received in total on every message.
use std::sync::atomic::{AtomicU64, Ordering};
use lazy_static::lazy_static;
use teloxide::prelude::*;
lazy_static! {
static ref MESSAGES_TOTAL: AtomicU64 = AtomicU64::new(0);
}
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
teloxide::enable_logging!();
log::info!("Starting shared_state_bot!");
let bot = Bot::from_env();
Dispatcher::new(bot)
.messages_handler(|rx: DispatcherHandlerRx<Message>| {
rx.for_each_concurrent(None, |message| async move {
let previous = MESSAGES_TOTAL.fetch_add(1, Ordering::Relaxed);
message
.answer(format!(
"I received {} messages in total.",
previous
))
.send()
.await
.log_on_error()
.await;
})
})
.dispatch()
.await;
}

View file

@ -32,7 +32,7 @@ async fn answer(
async fn handle_commands(rx: DispatcherHandlerRx<Message>) {
// Only iterate through commands in a proper format:
rx.commands::<Command>()
rx.commands::<Command, &str>(panic!("Insert here your bot's name"))
// Execute all incoming commands concurrently:
.for_each_concurrent(None, |(cx, command, _)| async move {
answer(cx, command).await.log_on_error().await;

View file

@ -14,9 +14,7 @@ use futures::StreamExt;
use std::{fmt::Debug, sync::Arc};
use tokio::sync::mpsc;
use tokio::sync::Mutex;
type Tx<Upd> = Option<Mutex<mpsc::UnboundedSender<DispatcherHandlerCx<Upd>>>>;
type Tx<Upd> = Option<mpsc::UnboundedSender<DispatcherHandlerCx<Upd>>>;
#[macro_use]
mod macros {
@ -37,10 +35,8 @@ async fn send<'a, Upd>(
Upd: Debug,
{
if let Some(tx) = tx {
if let Err(error) = tx
.lock()
.await
.send(DispatcherHandlerCx { bot: Arc::clone(&bot), update })
if let Err(error) =
tx.send(DispatcherHandlerCx { bot: Arc::clone(&bot), update })
{
log::error!(
"The RX part of the {} channel is closed, but an update is \
@ -103,7 +99,7 @@ impl Dispatcher {
let fut = h.handle(rx);
fut.await;
});
Some(Mutex::new(tx))
Some(tx)
}
#[must_use]

View file

@ -16,12 +16,14 @@ pub trait DispatcherHandlerRxExt {
/// Extracts only commands with their arguments from this stream of
/// arbitrary messages.
fn commands<C>(
fn commands<C, N>(
self,
bot_name: N,
) -> BoxStream<'static, (DispatcherHandlerCx<Message>, C, Vec<String>)>
where
Self: Stream<Item = DispatcherHandlerCx<Message>>,
C: BotCommand;
C: BotCommand,
N: Into<String> + Send;
}
impl<T> DispatcherHandlerRxExt for T
@ -39,23 +41,31 @@ where
}))
}
fn commands<C>(
fn commands<C, N>(
self,
bot_name: N,
) -> BoxStream<'static, (DispatcherHandlerCx<Message>, C, Vec<String>)>
where
Self: Stream<Item = DispatcherHandlerCx<Message>>,
C: BotCommand,
N: Into<String> + Send,
{
Box::pin(self.text_messages().filter_map(|(cx, text)| async move {
C::parse(&text).map(|(command, args)| {
(
cx,
command,
args.into_iter()
.map(ToOwned::to_owned)
.collect::<Vec<String>>(),
)
})
let bot_name = bot_name.into();
Box::pin(self.text_messages().filter_map(move |(cx, text)| {
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>>(),
)
})
}
}))
}
}

View file

@ -116,11 +116,11 @@ pub trait UpdateListener<E>: Stream<Item = Result<Update, E>> {
}
impl<S, E> UpdateListener<E> for S where S: Stream<Item = Result<Update, E>> {}
/// Returns a long polling update listener with the default configuration.
/// Returns a long polling update listener with `timeout` of 1 minute.
///
/// See also: [`polling`](polling).
pub fn polling_default(bot: Arc<Bot>) -> impl UpdateListener<RequestError> {
polling(bot, None, None, None)
polling(bot, Some(Duration::from_secs(60)), None, None)
}
/// Returns a long/short polling update listener with some additional options.

View file

@ -14,19 +14,25 @@
//! $ export TELOXIDE_TOKEN=<Your token here>
//!
//! # Windows
//! $ set TELOXITE_TOKEN=<Your token here>
//! $ set TELOXIDE_TOKEN=<Your token here>
//! ```
//!
//! 3. Be sure that you are up to date:
//! ```bash
//! # If you're using stable
//! $ rustup update stable
//! $ rustup override set stable
//!
//! # If you're using nightly
//! $ rustup update nightly
//! $ rustup override set nightly
//! ```
//!
//! 4. Execute `cargo new my_bot`, enter the directory and put these lines into
//! your `Cargo.toml`:
//! ```text
//! [dependencies]
//! teloxide = "0.1.0"
//! teloxide = "0.2.0"
//! log = "0.4.8"
//! tokio = "0.2.11"
//! pretty_env_logger = "0.4.0"
@ -72,6 +78,7 @@
//! ([Full](https://github.com/teloxide/teloxide/blob/master/examples/simple_commands_bot/src/main.rs))
//! ```no_run
//! // Imports are omitted...
//! # #[allow(unreachable_code)]
//! # use teloxide::{prelude::*, utils::command::BotCommand};
//! # use rand::{thread_rng, Rng};
//!
@ -108,7 +115,7 @@
//!
//! async fn handle_commands(rx: DispatcherHandlerRx<Message>) {
//! // Only iterate through commands in a proper format:
//! rx.commands::<Command>()
//! rx.commands::<Command, &str>(panic!("Insert here your bot's name"))
//! // Execute all incoming commands concurrently:
//! .for_each_concurrent(None, |(cx, command, _)| async move {
//! answer(cx, command).await.log_on_error().await;

View file

@ -71,7 +71,7 @@ mod tests {
assert_eq!(expected, kind);
}
else {
panic!("Этой херни здесь не должно быть");
panic!("Expected ApiErrorKind::TerminatedByOtherGetUpdates");
}
}
}

View file

@ -113,6 +113,7 @@ pub enum MessageKind {
/// Specified message was pinned. Note that the Message object in this
/// field will not contain further `reply_to_message` fields even if it
/// is itself a reply.
#[serde(rename = "pinned_message")]
pinned: Box<Message>,
},
Invoice {

View file

@ -1,190 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LanguageCode {
AA,
AB,
AE,
AF,
AK,
AM,
AN,
AR,
AS,
AV,
AY,
AZ,
BA,
BE,
BG,
BH,
BI,
BM,
BN,
BO,
BR,
BS,
CA,
CE,
CH,
CO,
CR,
CS,
CU,
CV,
CY,
DA,
DE,
DV,
DZ,
EE,
EL,
EN,
EO,
ES,
ET,
EU,
FA,
FF,
FI,
FJ,
FO,
FR,
FY,
GA,
GD,
GL,
GN,
GU,
GV,
HA,
HE,
HI,
HO,
HR,
HT,
HU,
HY,
HZ,
IA,
ID,
IE,
IG,
II,
IK,
IO,
IS,
IT,
IU,
JA,
JV,
KA,
KG,
KI,
KJ,
KK,
KL,
KM,
KN,
KO,
KR,
KS,
KU,
KV,
KW,
KY,
LA,
LB,
LG,
LI,
LN,
LO,
LT,
LU,
LV,
MG,
MH,
MI,
MK,
ML,
MN,
MR,
MS,
MT,
MY,
NA,
NB,
ND,
NE,
NG,
NL,
NN,
NO,
NR,
NV,
NY,
OC,
OJ,
OM,
OR,
OS,
PA,
PI,
PL,
PS,
PT,
QU,
RM,
RN,
RO,
RU,
RW,
SA,
SC,
SD,
SE,
SG,
SI,
SK,
SL,
SM,
SN,
SO,
SQ,
SR,
SS,
ST,
SU,
SV,
SW,
TA,
TE,
TG,
TH,
TI,
TK,
TL,
TN,
TO,
TR,
TS,
TT,
TW,
TY,
UG,
UK,
UR,
UZ,
VE,
VI,
VO,
WA,
WO,
XH,
YI,
YO,
ZA,
ZH,
ZU,
}

View file

@ -1,9 +1,7 @@
pub use country_code::*;
pub use currency::*;
pub use language_code::*;
pub use mime_wrapper::*;
mod country_code;
mod currency;
mod language_code;
mod mime_wrapper;

View file

@ -25,6 +25,7 @@ pub struct Poll {
pub is_anonymous: bool,
/// Poll type, currently can be “regular” or “quiz”
#[serde(rename = "type")]
pub poll_type: PollType,
/// True, if the poll allows multiple answers
@ -47,3 +48,46 @@ pub struct PollOption {
/// Number of users that voted for this option.
pub voter_count: i32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize() {
let data = r#"
{
"allows_multiple_answers": false,
"id": "5377643193141559299",
"is_anonymous": true,
"is_closed": false,
"options": [
{
"text": "1",
"voter_count": 1
},
{
"text": "2",
"voter_count": 0
},
{
"text": "3",
"voter_count": 0
},
{
"text": "4",
"voter_count": 0
},
{
"text": "5",
"voter_count": 0
}
],
"question": "Rate me from 1 to 5.",
"total_voter_count": 1,
"type": "regular"
}
"#;
serde_json::from_str::<Poll>(data).unwrap();
}
}

View file

@ -107,8 +107,8 @@ impl Update {
#[cfg(test)]
mod test {
use crate::types::{
Chat, ChatKind, ForwardKind, LanguageCode, MediaKind, Message,
MessageKind, Update, UpdateKind, User,
Chat, ChatKind, ForwardKind, MediaKind, Message, MessageKind, Update,
UpdateKind, User,
};
// TODO: more tests for deserialization
@ -158,7 +158,7 @@ mod test {
first_name: String::from("Waffle"),
last_name: None,
username: Some(String::from("WaffleLapkin")),
language_code: Some(LanguageCode::EN),
language_code: Some(String::from("en")),
}),
forward_kind: ForwardKind::Origin {
reply_to_message: None,
@ -205,4 +205,46 @@ mod test {
assert!(serde_json::from_str::<Update>(text).is_ok());
}
#[test]
fn pinned_message_works() {
let json = r#"{
"message": {
"chat": {
"id": -1001276785818,
"title": "teloxide dev",
"type": "supergroup",
"username": "teloxide_dev"
},
"date": 1582134655,
"from": {
"first_name": "Hirrolot",
"id": 408258968,
"is_bot": false,
"username": "hirrolot"
},
"message_id": 20225,
"pinned_message": {
"chat": {
"id": -1001276785818,
"title": "teloxide dev",
"type": "supergroup",
"username": "teloxide_dev"
},
"date": 1582134643,
"from": {
"first_name": "Hirrolot",
"id": 408258968,
"is_bot": false,
"username": "hirrolot"
},
"message_id": 20224,
"text": "Faster than a bullet"
}
},
"update_id": 845402291
}"#;
serde_json::from_str::<Update>(json).unwrap();
}
}

View file

@ -1,4 +1,3 @@
use crate::types::LanguageCode;
use serde::{Deserialize, Serialize};
/// This object represents a Telegram user or bot.
@ -25,7 +24,7 @@ pub struct User {
/// [IETF language tag] of the user's language.
///
/// [IETF language tag]: https://en.wikipedia.org/wiki/IETF_language_tag
pub language_code: Option<LanguageCode>,
pub language_code: Option<String>,
}
impl User {
@ -86,7 +85,7 @@ mod tests {
first_name: "firstName".to_string(),
last_name: Some("lastName".to_string()),
username: Some("Username".to_string()),
language_code: Some(LanguageCode::RU),
language_code: Some(String::from("ru")),
};
let actual = serde_json::from_str::<User>(&json).unwrap();
assert_eq!(actual, expected)

View file

@ -15,7 +15,8 @@
//! Ban,
//! }
//!
//! let (command, args) = AdminCommand::parse("/ban 3 hours").unwrap();
//! let (command, args) =
//! AdminCommand::parse("/ban 3 hours", "MyBotName").unwrap();
//! assert_eq!(command, AdminCommand::Ban);
//! assert_eq!(args, vec!["3", "hours"]);
//! ```
@ -24,8 +25,9 @@
//! ```
//! use teloxide::utils::command::parse_command;
//!
//! let (command, args) = parse_command("/ban 3 hours").unwrap();
//! assert_eq!(command, "/ban");
//! let (command, args) =
//! parse_command("/ban@MyBotName 3 hours", "MyBotName").unwrap();
//! assert_eq!(command, "ban");
//! assert_eq!(args, vec!["3", "hours"]);
//! ```
//!
@ -34,11 +36,19 @@
//! use teloxide::utils::command::parse_command_with_prefix;
//!
//! let text = "!ban 3 hours";
//! let (command, args) = parse_command_with_prefix("!", text).unwrap();
//! let (command, args) = parse_command_with_prefix("!", text, "").unwrap();
//! assert_eq!(command, "ban");
//! 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
@ -61,7 +71,7 @@ pub use teloxide_macros::BotCommand;
/// Ban,
/// }
///
/// let (command, args) = AdminCommand::parse("/ban 5 h").unwrap();
/// let (command, args) = AdminCommand::parse("/ban 5 h", "bot_name").unwrap();
/// assert_eq!(command, AdminCommand::Ban);
/// assert_eq!(args, vec!["5", "h"]);
/// ```
@ -92,50 +102,67 @@ pub use teloxide_macros::BotCommand;
pub trait BotCommand: Sized {
fn try_from(s: &str) -> Option<Self>;
fn descriptions() -> String;
fn parse(s: &str) -> Option<(Self, Vec<&str>)>;
fn parse<N>(s: &str, bot_name: N) -> Option<(Self, Vec<&str>)>
where
N: Into<String>;
}
/// Parses a string into a command with args.
///
/// It calls [`parse_command_with_prefix`] with default prefix `/`.
/// It calls [`parse_command_with_prefix`] with the default prefix `/`.
///
/// ## Example
/// ```
/// use teloxide::utils::command::parse_command;
///
/// let text = "/mute 5 hours";
/// let (command, args) = parse_command(text).unwrap();
/// assert_eq!(command, "/mute");
/// let text = "/mute@my_admin_bot 5 hours";
/// let (command, args) = parse_command(text, "my_admin_bot").unwrap();
/// assert_eq!(command, "mute");
/// assert_eq!(args, vec!["5", "hours"]);
/// ```
pub fn parse_command(text: &str) -> Option<(&str, Vec<&str>)> {
let mut words = text.split_whitespace();
let command = words.next()?;
Some((command, words.collect()))
///
/// [`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>)>
where
N: AsRef<str>,
{
parse_command_with_prefix("/", text, bot_name)
}
/// Parses a string into a command with args (custom prefix).
///
/// `prefix`: start symbols which denote start of a command.
/// `prefix`: symbols, which denote start of a command.
///
/// Example:
/// ## Example
/// ```
/// use teloxide::utils::command::parse_command_with_prefix;
///
/// let text = "!mute 5 hours";
/// let (command, args) = parse_command_with_prefix("!", text).unwrap();
/// let (command, args) = parse_command_with_prefix("!", text, "").unwrap();
/// assert_eq!(command, "mute");
/// assert_eq!(args, vec!["5", "hours"]);
/// ```
pub fn parse_command_with_prefix<'a>(
pub fn parse_command_with_prefix<'a, N>(
prefix: &str,
text: &'a str,
) -> Option<(&'a str, Vec<&'a str>)> {
bot_name: N,
) -> Option<(&'a str, Vec<&'a str>)>
where
N: AsRef<str>,
{
if !text.starts_with(prefix) {
return None;
}
let mut words = text.split_whitespace();
let command = &words.next()?[prefix.len()..];
let mut splited = words.next()?[prefix.len()..].split('@');
let command = splited.next()?;
let bot = splited.next();
match bot {
Some(name) if name == bot_name.as_ref() => {}
None => {}
_ => return None,
}
Some((command, words.collect()))
}
@ -146,16 +173,16 @@ mod tests {
#[test]
fn parse_command_with_args_() {
let data = "/command arg1 arg2";
let expected = Some(("/command", vec!["arg1", "arg2"]));
let actual = parse_command(data);
let expected = Some(("command", vec!["arg1", "arg2"]));
let actual = parse_command(data, "");
assert_eq!(actual, expected)
}
#[test]
fn parse_command_with_args_without_args() {
let data = "/command";
let expected = Some(("/command", vec![]));
let actual = parse_command(data);
let expected = Some(("command", vec![]));
let actual = parse_command(data, "");
assert_eq!(actual, expected)
}
@ -170,7 +197,7 @@ mod tests {
let data = "/start arg1 arg2";
let expected = Some((DefaultCommands::Start, vec!["arg1", "arg2"]));
let actual = DefaultCommands::parse(data);
let actual = DefaultCommands::parse(data, "");
assert_eq!(actual, expected)
}
@ -186,7 +213,7 @@ mod tests {
let data = "!start arg1 arg2";
let expected = Some((DefaultCommands::Start, vec!["arg1", "arg2"]));
let actual = DefaultCommands::parse(data);
let actual = DefaultCommands::parse(data, "");
assert_eq!(actual, expected)
}
@ -202,12 +229,9 @@ mod tests {
assert_eq!(
DefaultCommands::Start,
DefaultCommands::parse("!start").unwrap().0
);
assert_eq!(
DefaultCommands::descriptions(),
"!start - desc\n/help - \n"
DefaultCommands::parse("!start", "").unwrap().0
);
assert_eq!(DefaultCommands::descriptions(), "!start - desc\n/help\n");
}
#[test]
@ -226,15 +250,31 @@ mod tests {
assert_eq!(
DefaultCommands::Start,
DefaultCommands::parse("/start").unwrap().0
DefaultCommands::parse("/start", "MyNameBot").unwrap().0
);
assert_eq!(
DefaultCommands::Help,
DefaultCommands::parse("!help").unwrap().0
DefaultCommands::parse("!help", "MyNameBot").unwrap().0
);
assert_eq!(
DefaultCommands::descriptions(),
"Bot commands\n/start - \n!help - \n"
"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
);
}
}

View file

@ -97,6 +97,7 @@ pub fn escape(s: &str) -> String {
.replace(")", r"\)")
.replace("~", r"\~")
.replace("`", r"\`")
.replace(">", r"\>")
.replace("#", r"\#")
.replace("+", r"\+")
.replace("-", r"\-")
@ -218,8 +219,8 @@ mod tests {
fn test_escape() {
assert_eq!(escape("* foobar *"), r"\* foobar \*");
assert_eq!(
escape(r"_ * [ ] ( ) ~ \ ` # + - = | { } . !"),
r"\_ \* \[ \] \( \) \~ \ \` \# \+ \- \= \| \{ \} \. \!",
escape(r"_ * [ ] ( ) ~ \ ` > # + - = | { } . !"),
r"\_ \* \[ \] \( \) \~ \ \` \> \# \+ \- \= \| \{ \} \. \!",
);
}

View file

@ -1,16 +0,0 @@
[package]
name = "teloxide-macros"
version = "0.1.0"
description = "The teloxide's macros for internal usage"
authors = ["p0lunin <dmytro.polunin@gmail.com>"]
license = "MIT"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
quote = "1.0.2"
syn = "1.0.13"
[lib]
proc-macro = true

View file

@ -1,64 +0,0 @@
use syn::{
parse::{Parse, ParseStream},
LitStr, Token,
};
pub enum BotCommandAttribute {
Prefix,
Description,
RenameRule,
}
impl Parse for BotCommandAttribute {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let name_arg: syn::Ident = input.parse()?;
match name_arg.to_string().as_str() {
"prefix" => Ok(BotCommandAttribute::Prefix),
"description" => Ok(BotCommandAttribute::Description),
"rename" => Ok(BotCommandAttribute::RenameRule),
_ => Err(syn::Error::new(name_arg.span(), "unexpected argument")),
}
}
}
pub struct Attr {
name: BotCommandAttribute,
value: String,
}
impl Parse for Attr {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let name = input.parse::<BotCommandAttribute>()?;
input.parse::<Token![=]>()?;
let value = input.parse::<LitStr>()?.value();
Ok(Self { name, value })
}
}
impl Attr {
pub fn name(&self) -> &BotCommandAttribute {
&self.name
}
pub fn value(&self) -> String {
self.value.clone()
}
}
pub struct VecAttrs {
pub data: Vec<Attr>,
}
impl Parse for VecAttrs {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let mut data = vec![];
while !input.is_empty() {
data.push(input.parse()?);
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self { data })
}
}

View file

@ -1,54 +0,0 @@
use crate::{
attr::{Attr, BotCommandAttribute},
rename_rules::rename_by_rule,
};
pub struct Command {
pub prefix: Option<String>,
pub description: Option<String>,
pub name: String,
pub renamed: bool,
}
impl Command {
pub fn try_from(attrs: &[Attr], name: &str) -> Result<Self, String> {
let attrs = parse_attrs(attrs)?;
let mut new_name = name.to_string();
let mut renamed = false;
let prefix = attrs.prefix;
let description = attrs.description;
let rename = attrs.rename;
if let Some(rename_rule) = rename {
new_name = rename_by_rule(name, &rename_rule);
renamed = true;
}
Ok(Self { prefix, description, name: new_name, renamed })
}
}
struct CommandAttrs {
prefix: Option<String>,
description: Option<String>,
rename: Option<String>,
}
fn parse_attrs(attrs: &[Attr]) -> Result<CommandAttrs, String> {
let mut prefix = None;
let mut description = None;
let mut rename_rule = None;
for attr in attrs {
match attr.name() {
BotCommandAttribute::Prefix => prefix = Some(attr.value()),
BotCommandAttribute::Description => {
description = Some(attr.value())
}
BotCommandAttribute::RenameRule => rename_rule = Some(attr.value()),
#[allow(unreachable_patterns)]
_ => return Err("unexpected attribute".to_owned()),
}
}
Ok(CommandAttrs { prefix, description, rename: rename_rule })
}

View file

@ -1,50 +0,0 @@
use crate::attr::{Attr, BotCommandAttribute};
pub struct CommandEnum {
pub prefix: Option<String>,
pub description: Option<String>,
pub rename_rule: Option<String>,
}
impl CommandEnum {
pub fn try_from(attrs: &[Attr]) -> Result<Self, String> {
let attrs = parse_attrs(attrs)?;
let prefix = attrs.prefix;
let description = attrs.description;
let rename = attrs.rename;
if let Some(rename_rule) = &rename {
match rename_rule.as_str() {
"lowercase" => {}
_ => return Err("disallowed value".to_owned()),
}
}
Ok(Self { prefix, description, rename_rule: rename })
}
}
struct CommandAttrs {
prefix: Option<String>,
description: Option<String>,
rename: Option<String>,
}
fn parse_attrs(attrs: &[Attr]) -> Result<CommandAttrs, String> {
let mut prefix = None;
let mut description = None;
let mut rename_rule = None;
for attr in attrs {
match attr.name() {
BotCommandAttribute::Prefix => prefix = Some(attr.value()),
BotCommandAttribute::Description => {
description = Some(attr.value())
}
BotCommandAttribute::RenameRule => rename_rule = Some(attr.value()),
#[allow(unreachable_patterns)]
_ => return Err("unexpected attribute".to_owned()),
}
}
Ok(CommandAttrs { prefix, description, rename: rename_rule })
}

View file

@ -1,151 +0,0 @@
mod attr;
mod command;
mod enum_attributes;
mod rename_rules;
extern crate proc_macro;
extern crate syn;
use crate::{
attr::{Attr, VecAttrs},
command::Command,
enum_attributes::CommandEnum,
rename_rules::rename_by_rule,
};
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, DeriveInput};
macro_rules! get_or_return {
($($some:tt)*) => {
match $($some)* {
Ok(elem) => elem,
Err(e) => return e
};
}
}
#[proc_macro_derive(BotCommand, attributes(command))]
pub fn derive_telegram_command_enum(tokens: TokenStream) -> TokenStream {
let input = parse_macro_input!(tokens as DeriveInput);
let data_enum: &syn::DataEnum = get_or_return!(get_enum_data(&input));
let enum_attrs: Vec<Attr> = get_or_return!(parse_attributes(&input.attrs));
let command_enum = match CommandEnum::try_from(enum_attrs.as_slice()) {
Ok(command_enum) => command_enum,
Err(e) => return compile_error(e),
};
let variants: Vec<&syn::Variant> =
data_enum.variants.iter().map(|attr| attr).collect();
let mut variant_infos = vec![];
for variant in variants.iter() {
let mut attrs = Vec::new();
for attr in &variant.attrs {
match attr.parse_args::<VecAttrs>() {
Ok(mut attrs_) => {
attrs.append(attrs_.data.as_mut());
}
Err(e) => {
return compile_error(e.to_compile_error());
}
}
}
match Command::try_from(attrs.as_slice(), &variant.ident.to_string()) {
Ok(command) => variant_infos.push(command),
Err(e) => return compile_error(e),
}
}
let variant_ident = variants.iter().map(|variant| &variant.ident);
let variant_name = variant_infos.iter().map(|info| {
if info.renamed {
info.name.clone()
} else if let Some(rename_rule) = &command_enum.rename_rule {
rename_by_rule(&info.name, rename_rule)
} else {
info.name.clone()
}
});
let variant_prefixes = variant_infos.iter().map(|info| {
if let Some(prefix) = &info.prefix {
prefix
} else if let Some(prefix) = &command_enum.prefix {
prefix
} else {
"/"
}
});
let variant_str1 = variant_prefixes
.zip(variant_name)
.map(|(prefix, command)| prefix.to_string() + command.as_str());
let variant_str2 = variant_str1.clone();
let variant_description = variant_infos
.iter()
.map(|info| info.description.as_deref().unwrap_or(""));
let ident = &input.ident;
let global_description = if let Some(s) = &command_enum.description {
quote! { #s, "\n", }
} else {
quote! {}
};
let expanded = quote! {
impl BotCommand for #ident {
fn try_from(value: &str) -> Option<Self> {
match value {
#(
#variant_str1 => Some(Self::#variant_ident),
)*
_ => None
}
}
fn descriptions() -> String {
std::concat!(#global_description #(#variant_str2, " - ", #variant_description, '\n'),*).to_string()
}
fn parse(s: &str) -> Option<(Self, Vec<&str>)> {
let mut words = s.split_whitespace();
let command = Self::try_from(words.next()?)?;
Some((command, words.collect()))
}
}
};
//for debug
//println!("{}", &expanded.to_string());
TokenStream::from(expanded)
}
fn get_enum_data(input: &DeriveInput) -> Result<&syn::DataEnum, TokenStream> {
match &input.data {
syn::Data::Enum(data) => Ok(data),
_ => Err(compile_error("TelegramBotCommand allowed only for enums")),
}
}
fn parse_attributes(
input: &[syn::Attribute],
) -> Result<Vec<Attr>, TokenStream> {
let mut enum_attrs = Vec::new();
for attr in input.iter() {
match attr.parse_args::<VecAttrs>() {
Ok(mut attrs_) => {
enum_attrs.append(attrs_.data.as_mut());
}
Err(e) => {
return Err(compile_error(e.to_compile_error()));
}
}
}
Ok(enum_attrs)
}
fn compile_error<T>(data: T) -> TokenStream
where
T: ToTokens,
{
TokenStream::from(quote! { compile_error!(#data) })
}

View file

@ -1,6 +0,0 @@
pub fn rename_by_rule(input: &str, rule: &str) -> String {
match rule {
"lowercase" => input.to_string().to_lowercase(),
_ => rule.to_string(),
}
}