mirror of
https://github.com/teloxide/teloxide.git
synced 2025-01-10 20:12:25 +01:00
Merge branch 'dev' of github.com:teloxide/teloxide into from-env-proxy
Потому что сразу надо было пр мерджить
This commit is contained in:
commit
2f4dfd8f0c
30 changed files with 312 additions and 624 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -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
24
CHANGELOG.md
Normal 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.
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -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
|
||||
|
|
24
README.md
24
README.md
|
@ -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).
|
||||
|
|
|
@ -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. |
|
|
@ -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;
|
||||
|
|
14
examples/shared_state_bot/Cargo.toml
Normal file
14
examples/shared_state_bot/Cargo.toml
Normal 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 = "../../" }
|
41
examples/shared_state_bot/src/main.rs
Normal file
41
examples/shared_state_bot/src/main.rs
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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>>(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
13
src/lib.rs
13
src/lib.rs
|
@ -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;
|
||||
|
|
|
@ -71,7 +71,7 @@ mod tests {
|
|||
assert_eq!(expected, kind);
|
||||
}
|
||||
else {
|
||||
panic!("Этой херни здесь не должно быть");
|
||||
panic!("Expected ApiErrorKind::TerminatedByOtherGetUpdates");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"\_ \* \[ \] \( \) \~ \ \` \> \# \+ \- \= \| \{ \} \. \!",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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 })
|
||||
}
|
||||
}
|
|
@ -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 })
|
||||
}
|
|
@ -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 })
|
||||
}
|
|
@ -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) })
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue