mirror of
https://github.com/teloxide/teloxide.git
synced 2025-03-24 23:57:38 +01:00
Merge branch 'teloxide:master' into tryable_bot
This commit is contained in:
commit
122ddbbd42
24 changed files with 415 additions and 100 deletions
|
@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## unreleased
|
## unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add `MessageToCopyNotFound` error to `teloxide::errors::ApiError` ([PR 917](https://github.com/teloxide/teloxide/pull/917))
|
||||||
### Fixed
|
### Fixed
|
||||||
- Use `UserId` instead of `i64` for `user_id` in `html::user_mention` and `markdown::user_mention` ([PR 896](https://github.com/teloxide/teloxide/pull/896))
|
- Use `UserId` instead of `i64` for `user_id` in `html::user_mention` and `markdown::user_mention` ([PR 896](https://github.com/teloxide/teloxide/pull/896))
|
||||||
|
|
||||||
|
|
|
@ -16,16 +16,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- `Seconds` type, which represents a duration is seconds ([#859][pr859])
|
- `Seconds` type, which represents a duration is seconds ([#859][pr859])
|
||||||
- `VideoChatEnded::duration` field that was previously missed ([#859][pr859])
|
- `VideoChatEnded::duration` field that was previously missed ([#859][pr859])
|
||||||
- `ThreadId` newtype over `MessageId`, used for identifying reply threads ([#887][pr887])
|
- `ThreadId` newtype over `MessageId`, used for identifying reply threads ([#887][pr887])
|
||||||
|
- `ChatId::as_user` ([#905][pr905])
|
||||||
|
- Implement `PartialEq<ChatId> for UserId` and `PartialEq<UserId> for ChatId` ([#905][pr905])
|
||||||
|
- `ChatId::{MIN, MAX}` ([#905][pr905])
|
||||||
|
|
||||||
[pr851]: https://github.com/teloxide/teloxide/pull/851
|
[pr851]: https://github.com/teloxide/teloxide/pull/851
|
||||||
[pr887]: https://github.com/teloxide/teloxide/pull/887
|
[pr887]: https://github.com/teloxide/teloxide/pull/887
|
||||||
|
[pr905]: https://github.com/teloxide/teloxide/pull/905
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Return types of `edit_message_live_location_inline`, `stop_message_live_location_inline`, and `set_game_score_inline`: `Message` => `True` ([#854][pr854])
|
- Return types of `edit_message_live_location_inline`, `stop_message_live_location_inline`, and `set_game_score_inline`: `Message` => `True` ([#854][pr854])
|
||||||
- Remove `latitude` and `longitude` parameters from `stop_message_live_location` and `stop_message_live_location_inline` ([#854][pr854])
|
- Remove `latitude` and `longitude` parameters from `stop_message_live_location` and `stop_message_live_location_inline` ([#854][pr854])
|
||||||
|
- Fix the type of `photo_size`,`photo_width` and `photo_height` in the `send_invoice` method ([#936][pr936])
|
||||||
|
|
||||||
[pr854]: https://github.com/teloxide/teloxide/pull/854
|
[pr854]: https://github.com/teloxide/teloxide/pull/854
|
||||||
|
[pr936]: https://github.com/teloxide/teloxide/pull/936
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ takecell = "0.1"
|
||||||
take_mut = "0.2"
|
take_mut = "0.2"
|
||||||
rc-box = "1.1.1"
|
rc-box = "1.1.1"
|
||||||
never = "0.1.0"
|
never = "0.1.0"
|
||||||
chrono = { version = "0.4.19", default-features = false }
|
chrono = { version = "0.4.30", default-features = false }
|
||||||
either = "1.6.1"
|
either = "1.6.1"
|
||||||
bitflags = { version = "1.2" }
|
bitflags = { version = "1.2" }
|
||||||
|
|
||||||
|
|
|
@ -3735,17 +3735,17 @@ Schema(
|
||||||
),
|
),
|
||||||
Param(
|
Param(
|
||||||
name: "photo_size",
|
name: "photo_size",
|
||||||
ty: Option(String),
|
ty: Option(u32),
|
||||||
descr: Doc(md: "Photo size in bytes")
|
descr: Doc(md: "Photo size in bytes")
|
||||||
),
|
),
|
||||||
Param(
|
Param(
|
||||||
name: "photo_width",
|
name: "photo_width",
|
||||||
ty: Option(String),
|
ty: Option(u32),
|
||||||
descr: Doc(md: "Photo width")
|
descr: Doc(md: "Photo width")
|
||||||
),
|
),
|
||||||
Param(
|
Param(
|
||||||
name: "photo_height",
|
name: "photo_height",
|
||||||
ty: Option(String),
|
ty: Option(u32),
|
||||||
descr: Doc(md: "Photo height")
|
descr: Doc(md: "Photo height")
|
||||||
),
|
),
|
||||||
Param(
|
Param(
|
||||||
|
|
|
@ -213,6 +213,13 @@ impl_api_error! {
|
||||||
/// [`DeleteMessage`]: crate::payloads::DeleteMessage
|
/// [`DeleteMessage`]: crate::payloads::DeleteMessage
|
||||||
MessageToDeleteNotFound = "Bad Request: message to delete not found",
|
MessageToDeleteNotFound = "Bad Request: message to delete not found",
|
||||||
|
|
||||||
|
/// Occurs when bot tries to copy a message which does not exists.
|
||||||
|
/// May happen in methods:
|
||||||
|
/// 1. [`CopyMessage`]
|
||||||
|
///
|
||||||
|
/// [`CopyMessage`]: crate::payloads::CopyMessage
|
||||||
|
MessageToCopyNotFound = "Bad Request: message to copy not found",
|
||||||
|
|
||||||
/// Occurs when bot tries to send a text message without text.
|
/// Occurs when bot tries to send a text message without text.
|
||||||
///
|
///
|
||||||
/// May happen in methods:
|
/// May happen in methods:
|
||||||
|
@ -822,6 +829,10 @@ mod tests {
|
||||||
"{\"data\": \"Bad Request: message to delete not found\"}",
|
"{\"data\": \"Bad Request: message to delete not found\"}",
|
||||||
ApiError::MessageToDeleteNotFound,
|
ApiError::MessageToDeleteNotFound,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"{\"data\": \"Bad Request: message to copy not found\"}",
|
||||||
|
ApiError::MessageToCopyNotFound,
|
||||||
|
),
|
||||||
("{\"data\": \"Bad Request: message text is empty\"}", ApiError::MessageTextIsEmpty),
|
("{\"data\": \"Bad Request: message text is empty\"}", ApiError::MessageTextIsEmpty),
|
||||||
("{\"data\": \"Bad Request: message can't be edited\"}", ApiError::MessageCantBeEdited),
|
("{\"data\": \"Bad Request: message can't be edited\"}", ApiError::MessageCantBeEdited),
|
||||||
(
|
(
|
||||||
|
|
|
@ -45,11 +45,11 @@ impl_payload! {
|
||||||
/// URL of the product photo for the invoice. Can be a photo of the goods or a marketing image for a service. People like it better when they see what they are paying for.
|
/// URL of the product photo for the invoice. Can be a photo of the goods or a marketing image for a service. People like it better when they see what they are paying for.
|
||||||
pub photo_url: Url,
|
pub photo_url: Url,
|
||||||
/// Photo size in bytes
|
/// Photo size in bytes
|
||||||
pub photo_size: String [into],
|
pub photo_size: u32,
|
||||||
/// Photo width
|
/// Photo width
|
||||||
pub photo_width: String [into],
|
pub photo_width: u32,
|
||||||
/// Photo height
|
/// Photo height
|
||||||
pub photo_height: String [into],
|
pub photo_height: u32,
|
||||||
/// Pass _True_, if you require the user's full name to complete the order
|
/// Pass _True_, if you require the user's full name to complete the order
|
||||||
pub need_name: bool,
|
pub need_name: bool,
|
||||||
/// Pass _True_, if you require the user's phone number to complete the order
|
/// Pass _True_, if you require the user's phone number to complete the order
|
||||||
|
|
|
@ -269,7 +269,7 @@ pub(crate) fn serde_timestamp<E: serde::de::Error>(
|
||||||
|
|
||||||
NaiveDateTime::from_timestamp_opt(timestamp, 0)
|
NaiveDateTime::from_timestamp_opt(timestamp, 0)
|
||||||
.ok_or_else(|| E::custom("invalid timestump"))
|
.ok_or_else(|| E::custom("invalid timestump"))
|
||||||
.map(|naive| DateTime::from_utc(naive, Utc))
|
.map(|naive| DateTime::from_naive_utc_and_offset(naive, Utc))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) mod serde_opt_date_from_unix_timestamp {
|
pub(crate) mod serde_opt_date_from_unix_timestamp {
|
||||||
|
@ -305,8 +305,10 @@ pub(crate) mod serde_opt_date_from_unix_timestamp {
|
||||||
|
|
||||||
{
|
{
|
||||||
let json = r#"{"date":1}"#;
|
let json = r#"{"date":1}"#;
|
||||||
let expected =
|
let expected = DateTime::from_naive_utc_and_offset(
|
||||||
DateTime::from_utc(chrono::NaiveDateTime::from_timestamp_opt(1, 0).unwrap(), Utc);
|
chrono::NaiveDateTime::from_timestamp_opt(1, 0).unwrap(),
|
||||||
|
Utc,
|
||||||
|
);
|
||||||
|
|
||||||
let Struct { date } = serde_json::from_str(json).unwrap();
|
let Struct { date } = serde_json::from_str(json).unwrap();
|
||||||
assert_eq!(date, Some(expected));
|
assert_eq!(date, Some(expected));
|
||||||
|
|
|
@ -50,6 +50,15 @@ impl ChatId {
|
||||||
matches!(self.to_bare(), BareChatId::Channel(_))
|
matches!(self.to_bare(), BareChatId::Channel(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns user id, if this is an id of a user.
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_user(self) -> Option<UserId> {
|
||||||
|
match self.to_bare() {
|
||||||
|
BareChatId::User(u) => Some(u),
|
||||||
|
BareChatId::Group(_) | BareChatId::Channel(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts this id to "bare" MTProto peer id.
|
/// Converts this id to "bare" MTProto peer id.
|
||||||
///
|
///
|
||||||
/// See [`BareChatId`] for more.
|
/// See [`BareChatId`] for more.
|
||||||
|
@ -73,6 +82,12 @@ impl From<UserId> for ChatId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq<UserId> for ChatId {
|
||||||
|
fn eq(&self, other: &UserId) -> bool {
|
||||||
|
self.is_user() && *self == ChatId::from(*other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BareChatId {
|
impl BareChatId {
|
||||||
/// Converts bare chat id back to normal bot API [`ChatId`].
|
/// Converts bare chat id back to normal bot API [`ChatId`].
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
@ -92,8 +107,8 @@ const MIN_MARKED_CHANNEL_ID: i64 = -1997852516352;
|
||||||
const MAX_MARKED_CHANNEL_ID: i64 = -1000000000000;
|
const MAX_MARKED_CHANNEL_ID: i64 = -1000000000000;
|
||||||
const MIN_MARKED_CHAT_ID: i64 = MAX_MARKED_CHANNEL_ID + 1;
|
const MIN_MARKED_CHAT_ID: i64 = MAX_MARKED_CHANNEL_ID + 1;
|
||||||
const MAX_MARKED_CHAT_ID: i64 = MIN_USER_ID - 1;
|
const MAX_MARKED_CHAT_ID: i64 = MIN_USER_ID - 1;
|
||||||
const MIN_USER_ID: i64 = 0;
|
pub(crate) const MIN_USER_ID: i64 = 0;
|
||||||
const MAX_USER_ID: i64 = (1 << 40) - 1;
|
pub(crate) const MAX_USER_ID: i64 = (1 << 40) - 1;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
@ -143,4 +158,16 @@ mod tests {
|
||||||
fn display() {
|
fn display() {
|
||||||
assert_eq!(ChatId(1).to_string(), "1");
|
assert_eq!(ChatId(1).to_string(), "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_id_eq() {
|
||||||
|
assert_eq!(ChatId(12), UserId(12));
|
||||||
|
assert_eq!(ChatId(4652762), UserId(4652762));
|
||||||
|
assert_ne!(ChatId(17), UserId(42));
|
||||||
|
|
||||||
|
// The user id is not well formed, so even though `-1 == max` is true,
|
||||||
|
// we don't want user id to match
|
||||||
|
assert_eq!(-1i64, u64::MAX as i64);
|
||||||
|
assert_ne!(ChatId(-1), UserId(u64::MAX));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,16 +155,35 @@ impl Update {
|
||||||
/// replies, pinned messages, message entities, "via bot" fields and more.
|
/// replies, pinned messages, message entities, "via bot" fields and more.
|
||||||
/// Also note that this function can return duplicate users.
|
/// Also note that this function can return duplicate users.
|
||||||
pub fn mentioned_users(&self) -> impl Iterator<Item = &User> {
|
pub fn mentioned_users(&self) -> impl Iterator<Item = &User> {
|
||||||
use either::Either::{Left, Right};
|
use either::Either::{Left as L, Right as R};
|
||||||
use std::iter::{empty, once};
|
use std::iter::{empty, once};
|
||||||
|
|
||||||
let i0 = Left;
|
// [root]
|
||||||
let i1 = |x| Right(Left(x));
|
// / \
|
||||||
let i2 = |x| Right(Right(Left(x)));
|
// left - / \ - right
|
||||||
let i3 = |x| Right(Right(Right(Left(x))));
|
// / \
|
||||||
let i4 = |x| Right(Right(Right(Right(Left(x)))));
|
// /\ /\
|
||||||
let i5 = |x| Right(Right(Right(Right(Right(Left(x))))));
|
// / \ / \
|
||||||
let i6 = |x| Right(Right(Right(Right(Right(Right(x))))));
|
// / \ / \
|
||||||
|
// 0 /\ /\ /\
|
||||||
|
// / \ / \ / \
|
||||||
|
// 1 2 3 4 5 6
|
||||||
|
//
|
||||||
|
// 0 = LL
|
||||||
|
// 1 = LRL
|
||||||
|
// 2 = LRR
|
||||||
|
// 3 = RLL
|
||||||
|
// 4 = RLR
|
||||||
|
// 5 = RRL
|
||||||
|
// 6 = RRR
|
||||||
|
|
||||||
|
let i0 = |x| L(L(x));
|
||||||
|
let i1 = |x| L(R(L(x)));
|
||||||
|
let i2 = |x| L(R(R(x)));
|
||||||
|
let i3 = |x| R(L(L(x)));
|
||||||
|
let i4 = |x| R(L(R(x)));
|
||||||
|
let i5 = |x| R(R(L(x)));
|
||||||
|
let i6 = |x| R(R(R(x)));
|
||||||
|
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
UpdateKind::Message(message)
|
UpdateKind::Message(message)
|
||||||
|
@ -376,8 +395,10 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn message() {
|
fn message() {
|
||||||
let timestamp = 1_569_518_342;
|
let timestamp = 1_569_518_342;
|
||||||
let date =
|
let date = DateTime::from_naive_utc_and_offset(
|
||||||
DateTime::from_utc(NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap(), Utc);
|
NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap(),
|
||||||
|
Utc,
|
||||||
|
);
|
||||||
|
|
||||||
let json = r#"{
|
let json = r#"{
|
||||||
"update_id":892252934,
|
"update_id":892252934,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::types::{ChatId, MAX_USER_ID, MIN_USER_ID};
|
||||||
|
|
||||||
/// Identifier of a user.
|
/// Identifier of a user.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
#[derive(Debug, derive_more::Display)]
|
#[derive(Debug, derive_more::Display)]
|
||||||
|
@ -50,6 +52,19 @@ impl UserId {
|
||||||
|
|
||||||
self == TELEGRAM_USER_ID
|
self == TELEGRAM_USER_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The smallest user id that could possibly be returned by Telegram.
|
||||||
|
pub const MIN: Self = Self(MIN_USER_ID as u64);
|
||||||
|
|
||||||
|
/// The largest user id that could possibly be returned by Telegram.
|
||||||
|
pub const MAX: Self = Self(MAX_USER_ID as u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<ChatId> for UserId {
|
||||||
|
fn eq(&self, other: &ChatId) -> bool {
|
||||||
|
// Reuse `PartialEq<UserId> for ChatId` impl
|
||||||
|
other == self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Fix `split` parser for tuple variants with len < 2 ([issue #834](https://github.com/teloxide/teloxide/issues/834))
|
- Fix `split` parser for tuple variants with len < 2 ([issue #834](https://github.com/teloxide/teloxide/issues/834))
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Now you can use `/// doc comment` for the command help message ([PR #861](https://github.com/teloxide/teloxide/pull/861)).
|
||||||
- Now you can use `#[command(hide)]` to hide a command from the help message ([PR #862](https://github.com/teloxide/teloxide/pull/862))
|
- Now you can use `#[command(hide)]` to hide a command from the help message ([PR #862](https://github.com/teloxide/teloxide/pull/862))
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::{error::compile_error_at, Result};
|
use crate::{error::compile_error_at, Result};
|
||||||
|
|
||||||
use proc_macro2::Span;
|
use proc_macro2::{Delimiter, Span};
|
||||||
use syn::{
|
use syn::{
|
||||||
parse::{Parse, ParseBuffer, ParseStream},
|
parse::{Parse, ParseStream, Parser},
|
||||||
spanned::Spanned,
|
spanned::Spanned,
|
||||||
Attribute, Ident, Lit, Path, Token,
|
Attribute, Ident, Lit, Path, Token,
|
||||||
};
|
};
|
||||||
|
@ -18,19 +18,42 @@ pub(crate) fn fold_attrs<A, R>(
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|&a| filter(a))
|
.filter(|&a| filter(a))
|
||||||
.flat_map(|attribute| {
|
.flat_map(|attribute| {
|
||||||
// FIXME: don't allocate here
|
let Some(key) = attribute.path.get_ident().cloned() else {
|
||||||
let attrs = match attribute.parse_args_with(|input: &ParseBuffer| {
|
return vec![Err(compile_error_at(
|
||||||
input.parse_terminated::<_, Token![,]>(Attr::parse)
|
"expected an ident",
|
||||||
}) {
|
attribute.path.span(),
|
||||||
Ok(ok) => ok,
|
))];
|
||||||
Err(err) => return vec![Err(err.into())],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
attrs.into_iter().map(&parse).collect()
|
match (|input: ParseStream<'_>| Attrs::parse_with_key(input, key))
|
||||||
|
.parse(attribute.tokens.clone().into())
|
||||||
|
{
|
||||||
|
Ok(ok) => ok.0.into_iter().map(&parse).collect(),
|
||||||
|
Err(err) => vec![Err(err.into())],
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.try_fold(init, |acc, r| r.and_then(|r| f(acc, r)))
|
.try_fold(init, |acc, r| f(acc, r?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A helper to parse a set of attributes.
|
||||||
|
///
|
||||||
|
/// For example:
|
||||||
|
/// ```text
|
||||||
|
/// #[blahblah(key = "puff", value = 12, nope, inner(what = some::path))]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The code above will produce
|
||||||
|
/// ```test
|
||||||
|
/// [
|
||||||
|
/// Attr { key: [key, blahblah], value: "puff" },
|
||||||
|
/// Attr { key: [value, blahblah], value: 12 },
|
||||||
|
/// Attr { key: [nope, blahblah], value: none },
|
||||||
|
/// Attr { key: [what, inner, blahblah], value: some::path },
|
||||||
|
/// ]
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct Attrs(Vec<Attr>);
|
||||||
|
|
||||||
/// An attribute key-value pair.
|
/// An attribute key-value pair.
|
||||||
///
|
///
|
||||||
/// For example:
|
/// For example:
|
||||||
|
@ -38,8 +61,17 @@ pub(crate) fn fold_attrs<A, R>(
|
||||||
/// #[blahblah(key = "puff", value = 12, nope)]
|
/// #[blahblah(key = "puff", value = 12, nope)]
|
||||||
/// ^^^^^^^^^^^^ ^^^^^^^^^^ ^^^^
|
/// ^^^^^^^^^^^^ ^^^^^^^^^^ ^^^^
|
||||||
/// ```
|
/// ```
|
||||||
|
#[derive(Debug)]
|
||||||
pub(crate) struct Attr {
|
pub(crate) struct Attr {
|
||||||
pub key: Ident,
|
/// The key captures the full "path" in the reverse order, for example here:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// #[blahblah(key = "puff")]
|
||||||
|
/// ^^^^^^^^^^^^
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The `key` will be `[key, blahblah]`. See [Attrs] for more examples.
|
||||||
|
pub key: Vec<Ident>,
|
||||||
pub value: AttrValue,
|
pub value: AttrValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,16 +82,53 @@ pub(crate) struct Attr {
|
||||||
/// #[blahblah(key = "puff", value = 12, nope)]
|
/// #[blahblah(key = "puff", value = 12, nope)]
|
||||||
/// ^^^^^^ ^^ ^-- (None pseudo-value)
|
/// ^^^^^^ ^^ ^-- (None pseudo-value)
|
||||||
/// ```
|
/// ```
|
||||||
|
#[derive(Debug)]
|
||||||
pub(crate) enum AttrValue {
|
pub(crate) enum AttrValue {
|
||||||
Path(Path),
|
Path(Path),
|
||||||
Lit(Lit),
|
Lit(Lit),
|
||||||
None(Span),
|
None(Span),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for Attr {
|
impl Parse for Attrs {
|
||||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
fn parse(input: ParseStream) -> syn::Result<Attrs> {
|
||||||
let key = input.parse::<Ident>()?;
|
let key = input.parse::<Ident>()?;
|
||||||
|
|
||||||
|
Attrs::parse_with_key(input, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Attrs {
|
||||||
|
fn parse_with_key(input: ParseStream, key: Ident) -> syn::Result<Attrs> {
|
||||||
|
// Parse an attribute group
|
||||||
|
let attrs = input.step(|cursor| {
|
||||||
|
if let Some((group, _sp, next_cursor)) = cursor.group(Delimiter::Parenthesis) {
|
||||||
|
if !next_cursor.eof() {
|
||||||
|
return Err(syn::Error::new(next_cursor.span(), "unexpected tokens"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut attrs =
|
||||||
|
(|input: ParseStream<'_>| input.parse_terminated::<_, Token![,]>(Attrs::parse))
|
||||||
|
.parse(group.token_stream().into())?
|
||||||
|
.into_iter()
|
||||||
|
.reduce(|mut l, r| {
|
||||||
|
l.0.extend(r.0);
|
||||||
|
l
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
attrs.0.iter_mut().for_each(|attr| attr.key.push(key.clone()));
|
||||||
|
|
||||||
|
Ok((Some(attrs), next_cursor))
|
||||||
|
} else {
|
||||||
|
Ok((None, *cursor))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if let Some(attrs) = attrs {
|
||||||
|
return Ok(attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a single attribute
|
||||||
let value = match input.peek(Token![=]) {
|
let value = match input.peek(Token![=]) {
|
||||||
true => {
|
true => {
|
||||||
input.parse::<Token![=]>()?;
|
input.parse::<Token![=]>()?;
|
||||||
|
@ -68,13 +137,18 @@ impl Parse for Attr {
|
||||||
false => AttrValue::None(input.span()),
|
false => AttrValue::None(input.span()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self { key, value })
|
Ok(Attrs(vec![Attr { key: vec![key], value }]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Attr {
|
impl Attr {
|
||||||
pub(crate) fn span(&self) -> Span {
|
pub(crate) fn span(&self) -> Span {
|
||||||
self.key.span().join(self.value.span()).unwrap_or_else(|| self.key.span())
|
self.key().span().join(self.value.span()).unwrap_or_else(|| self.key().span())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key(&self) -> &Ident {
|
||||||
|
// It's an invariant of the type that `self.key` is non-empty
|
||||||
|
self.key.first().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +161,17 @@ impl AttrValue {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unwraps this value if it's a nothing.
|
||||||
|
pub fn expect_none(self, option_name: &str) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
AttrValue::None(_) => Ok(()),
|
||||||
|
_ => Err(compile_error_at(
|
||||||
|
&format!("The {option_name} option should not have a value, remove it"),
|
||||||
|
self.span(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// /// Unwraps this value if it's a path.
|
// /// Unwraps this value if it's a path.
|
||||||
// pub fn expect_path(self) -> Result<Path> {
|
// pub fn expect_path(self) -> Result<Path> {
|
||||||
// self.expect("a path", |this| match this {
|
// self.expect("a path", |this| match this {
|
||||||
|
@ -124,7 +209,7 @@ impl AttrValue {
|
||||||
/// #[blahblah(key = "puff", value = 12, nope )]
|
/// #[blahblah(key = "puff", value = 12, nope )]
|
||||||
/// ^^^^^^ ^^ ^
|
/// ^^^^^^ ^^ ^
|
||||||
/// ```
|
/// ```
|
||||||
fn span(&self) -> Span {
|
pub fn span(&self) -> Span {
|
||||||
match self {
|
match self {
|
||||||
Self::Path(p) => p.span(),
|
Self::Path(p) => p.span(),
|
||||||
Self::Lit(l) => l.span(),
|
Self::Lit(l) => l.span(),
|
||||||
|
|
|
@ -76,7 +76,7 @@ fn impl_descriptions(infos: &[Command], global: &CommandEnum) -> proc_macro2::To
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let global_description = match global.description.as_deref() {
|
let global_description = match global.description.as_ref().map(|(d, _)| d) {
|
||||||
Some(gd) => quote! { .global_description(#gd) },
|
Some(gd) => quote! { .global_description(#gd) },
|
||||||
None => quote! {},
|
None => quote! {},
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,8 @@ pub(crate) struct Command {
|
||||||
/// Prefix of this command, for example "/".
|
/// Prefix of this command, for example "/".
|
||||||
pub prefix: String,
|
pub prefix: String,
|
||||||
/// Description for the command.
|
/// Description for the command.
|
||||||
pub description: Option<(String, Span)>,
|
/// The bool is true if the description contains a doc comment.
|
||||||
|
pub description: Option<(String, bool, Span)>,
|
||||||
/// Name of the command, with all renames already applied.
|
/// Name of the command, with all renames already applied.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// Parser for arguments of this command.
|
/// Parser for arguments of this command.
|
||||||
|
@ -61,15 +62,22 @@ impl Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn description(&self) -> Option<&str> {
|
pub fn description(&self) -> Option<&str> {
|
||||||
self.description.as_ref().map(|(d, _span)| &**d)
|
self.description.as_ref().map(|(d, ..)| &**d)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains_doc_comment(&self) -> bool {
|
||||||
|
self.description.as_ref().map(|(_, is_doc, ..)| *is_doc).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn description_is_enabled(&self) -> bool {
|
pub(crate) fn description_is_enabled(&self) -> bool {
|
||||||
// FIXME: remove the first, `== "off"`, check eventually
|
// FIXME: remove the first, `== "off"`, check eventually
|
||||||
self.description() != Some("off") && !self.hidden
|
!((self.description() == Some("off") && !self.contains_doc_comment()) || self.hidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn deprecated_description_off_span(&self) -> Option<Span> {
|
pub(crate) fn deprecated_description_off_span(&self) -> Option<Span> {
|
||||||
self.description.as_ref().filter(|(d, _)| d == "off").map(|&(_, span)| span)
|
self.description
|
||||||
|
.as_ref()
|
||||||
|
.filter(|(d, ..)| d == "off" && !self.contains_doc_comment())
|
||||||
|
.map(|&(.., span)| span)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,18 @@ use crate::{
|
||||||
Result,
|
Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
use proc_macro2::Span;
|
use proc_macro2::Span;
|
||||||
use syn::Attribute;
|
use syn::{
|
||||||
|
parse::{ParseStream, Peek},
|
||||||
|
Attribute, Token,
|
||||||
|
};
|
||||||
|
|
||||||
/// All attributes that can be used for `derive(BotCommands)`
|
/// All attributes that can be used for `derive(BotCommands)`
|
||||||
pub(crate) struct CommandAttrs {
|
pub(crate) struct CommandAttrs {
|
||||||
pub prefix: Option<(String, Span)>,
|
pub prefix: Option<(String, Span)>,
|
||||||
pub description: Option<(String, Span)>,
|
/// The bool is true if the description contains a doc comment
|
||||||
|
pub description: Option<(String, bool, Span)>,
|
||||||
pub rename_rule: Option<(RenameRule, Span)>,
|
pub rename_rule: Option<(RenameRule, Span)>,
|
||||||
pub rename: Option<(String, Span)>,
|
pub rename: Option<(String, Span)>,
|
||||||
pub parser: Option<(ParserType, Span)>,
|
pub parser: Option<(ParserType, Span)>,
|
||||||
|
@ -37,7 +42,8 @@ struct CommandAttr {
|
||||||
/// Kind of [`CommandAttr`].
|
/// Kind of [`CommandAttr`].
|
||||||
enum CommandAttrKind {
|
enum CommandAttrKind {
|
||||||
Prefix(String),
|
Prefix(String),
|
||||||
Description(String),
|
/// Description of the command. and if its doc comment or not
|
||||||
|
Description(String, bool),
|
||||||
RenameRule(RenameRule),
|
RenameRule(RenameRule),
|
||||||
Rename(String),
|
Rename(String),
|
||||||
ParseWith(ParserType),
|
ParseWith(ParserType),
|
||||||
|
@ -51,7 +57,7 @@ impl CommandAttrs {
|
||||||
|
|
||||||
fold_attrs(
|
fold_attrs(
|
||||||
attributes,
|
attributes,
|
||||||
is_command_attribute,
|
|attr| is_command_attribute(attr) || is_doc_comment(attr),
|
||||||
CommandAttr::parse,
|
CommandAttr::parse,
|
||||||
Self {
|
Self {
|
||||||
prefix: None,
|
prefix: None,
|
||||||
|
@ -73,9 +79,33 @@ impl CommandAttrs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn join_string(opt: &mut Option<(String, bool, Span)>, new_str: &str, sp: Span) {
|
||||||
|
match opt {
|
||||||
|
slot @ None => {
|
||||||
|
*slot = Some((new_str.to_owned(), false, sp));
|
||||||
|
}
|
||||||
|
Some((old_str, ..)) => {
|
||||||
|
*old_str = format!("{old_str}\n{new_str}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match attr.kind {
|
match attr.kind {
|
||||||
Prefix(p) => insert(&mut this.prefix, p, attr.sp),
|
Prefix(p) => insert(&mut this.prefix, p, attr.sp),
|
||||||
Description(d) => insert(&mut this.description, d, attr.sp),
|
Description(d, is_doc) => {
|
||||||
|
join_string(
|
||||||
|
&mut this.description,
|
||||||
|
// Sometimes doc comments include a space before them, this removes it
|
||||||
|
d.strip_prefix(' ').unwrap_or(&d),
|
||||||
|
attr.sp,
|
||||||
|
);
|
||||||
|
if is_doc {
|
||||||
|
if let Some((_, is_doc, _)) = &mut this.description {
|
||||||
|
*is_doc = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
RenameRule(r) => insert(&mut this.rename_rule, r, attr.sp),
|
RenameRule(r) => insert(&mut this.rename_rule, r, attr.sp),
|
||||||
Rename(r) => insert(&mut this.rename, r, attr.sp),
|
Rename(r) => insert(&mut this.rename, r, attr.sp),
|
||||||
ParseWith(p) => insert(&mut this.parser, p, attr.sp),
|
ParseWith(p) => insert(&mut this.parser, p, attr.sp),
|
||||||
|
@ -94,22 +124,62 @@ impl CommandAttr {
|
||||||
use CommandAttrKind::*;
|
use CommandAttrKind::*;
|
||||||
|
|
||||||
let sp = attr.span();
|
let sp = attr.span();
|
||||||
let Attr { key, value } = attr;
|
let Attr { mut key, value } = attr;
|
||||||
let kind = match &*key.to_string() {
|
|
||||||
"prefix" => Prefix(value.expect_string()?),
|
let outermost_key = key.pop().unwrap(); // `Attr`'s invariants ensure `key.len() > 0`
|
||||||
"description" => Description(value.expect_string()?),
|
|
||||||
"rename_rule" => {
|
let kind = match &*outermost_key.to_string() {
|
||||||
RenameRule(value.expect_string().and_then(|r| self::RenameRule::parse(&r))?)
|
"doc" => {
|
||||||
|
if let Some(unexpected_key) = key.last() {
|
||||||
|
return Err(compile_error_at(
|
||||||
|
"`doc` can't have nested attributes",
|
||||||
|
unexpected_key.span(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Description(value.expect_string()?, true)
|
||||||
}
|
}
|
||||||
"rename" => Rename(value.expect_string()?),
|
|
||||||
"parse_with" => ParseWith(ParserType::parse(value)?),
|
"command" => {
|
||||||
"separator" => Separator(value.expect_string()?),
|
let Some(attr) = key.pop()
|
||||||
"hide" => Hide,
|
else {
|
||||||
|
return Err(compile_error_at(
|
||||||
|
"expected an attribute name",
|
||||||
|
outermost_key.span(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(unexpected_key) = key.last() {
|
||||||
|
return Err(compile_error_at(
|
||||||
|
&format!("{attr} can't have nested attributes"),
|
||||||
|
unexpected_key.span(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
match &*attr.to_string() {
|
||||||
|
"prefix" => Prefix(value.expect_string()?),
|
||||||
|
"description" => Description(value.expect_string()?, false),
|
||||||
|
"rename_rule" => {
|
||||||
|
RenameRule(value.expect_string().and_then(|r| self::RenameRule::parse(&r))?)
|
||||||
|
}
|
||||||
|
"rename" => Rename(value.expect_string()?),
|
||||||
|
"parse_with" => ParseWith(ParserType::parse(value)?),
|
||||||
|
"separator" => Separator(value.expect_string()?),
|
||||||
|
"hide" => value.expect_none("hide").map(|_| Hide)?,
|
||||||
|
_ => {
|
||||||
|
return Err(compile_error_at(
|
||||||
|
"unexpected attribute name (expected one of `prefix`, `description`, \
|
||||||
|
`rename`, `parse_with`, `separator` and `hide`",
|
||||||
|
attr.span(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
return Err(compile_error_at(
|
return Err(compile_error_at(
|
||||||
"unexpected attribute name (expected one of `prefix`, `description`, \
|
"unexpected attribute (expected `command` or `doc`)",
|
||||||
`rename`, `parse_with`, `separator` and `hide`",
|
outermost_key.span(),
|
||||||
key.span(),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -119,8 +189,21 @@ impl CommandAttr {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_command_attribute(a: &Attribute) -> bool {
|
fn is_command_attribute(a: &Attribute) -> bool {
|
||||||
match a.path.get_ident() {
|
matches!(a.path.get_ident(), Some(ident) if ident == "command")
|
||||||
Some(ident) => ident == "command",
|
}
|
||||||
_ => false,
|
|
||||||
}
|
fn is_doc_comment(a: &Attribute) -> bool {
|
||||||
|
matches!(a.path.get_ident(), Some(ident) if ident == "doc" && peek_at_token_stream(a.tokens.clone().into(), Token![=]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek_at_token_stream(s: TokenStream, p: impl Peek) -> bool {
|
||||||
|
// syn be fr challenge 2023 (impossible)
|
||||||
|
use syn::parse::Parser;
|
||||||
|
(|input: ParseStream<'_>| {
|
||||||
|
let r = input.peek(p);
|
||||||
|
_ = input.step(|_| Ok(((), syn::buffer::Cursor::empty())));
|
||||||
|
Ok(r)
|
||||||
|
})
|
||||||
|
.parse(s)
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,8 @@ use crate::{
|
||||||
|
|
||||||
pub(crate) struct CommandEnum {
|
pub(crate) struct CommandEnum {
|
||||||
pub prefix: String,
|
pub prefix: String,
|
||||||
pub description: Option<String>,
|
/// The bool is true if the description contains a doc comment
|
||||||
|
pub description: Option<(String, bool)>,
|
||||||
pub rename_rule: RenameRule,
|
pub rename_rule: RenameRule,
|
||||||
pub parser_type: ParserType,
|
pub parser_type: ParserType,
|
||||||
}
|
}
|
||||||
|
@ -37,7 +38,7 @@ impl CommandEnum {
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
prefix: prefix.map(|(p, _)| p).unwrap_or_else(|| "/".to_owned()),
|
prefix: prefix.map(|(p, _)| p).unwrap_or_else(|| "/".to_owned()),
|
||||||
description: description.map(|(d, _)| d),
|
description: description.map(|(d, is_doc, _)| (d, is_doc)),
|
||||||
rename_rule: rename_rule.map(|(rr, _)| rr).unwrap_or(RenameRule::Identity),
|
rename_rule: rename_rule.map(|(rr, _)| rr).unwrap_or(RenameRule::Identity),
|
||||||
parser_type: parser,
|
parser_type: parser,
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,21 +12,19 @@ use teloxide::{prelude::*, types::ChatPermissions, utils::command::BotCommands};
|
||||||
// your commands in this format:
|
// your commands in this format:
|
||||||
// %GENERAL-DESCRIPTION%
|
// %GENERAL-DESCRIPTION%
|
||||||
// %PREFIX%%COMMAND% - %DESCRIPTION%
|
// %PREFIX%%COMMAND% - %DESCRIPTION%
|
||||||
|
|
||||||
|
/// Use commands in format /%command% %num% %unit%
|
||||||
#[derive(BotCommands, Clone)]
|
#[derive(BotCommands, Clone)]
|
||||||
#[command(
|
#[command(rename_rule = "lowercase", parse_with = "split")]
|
||||||
rename_rule = "lowercase",
|
|
||||||
description = "Use commands in format /%command% %num% %unit%",
|
|
||||||
parse_with = "split"
|
|
||||||
)]
|
|
||||||
enum Command {
|
enum Command {
|
||||||
#[command(description = "kick user from chat.")]
|
/// Kick user from chat.
|
||||||
Kick,
|
Kick,
|
||||||
#[command(description = "ban user in chat.")]
|
/// Ban user in chat.
|
||||||
Ban {
|
Ban {
|
||||||
time: u64,
|
time: u64,
|
||||||
unit: UnitOfTime,
|
unit: UnitOfTime,
|
||||||
},
|
},
|
||||||
#[command(description = "mute user in chat.")]
|
/// Mute user in chat.
|
||||||
Mute {
|
Mute {
|
||||||
time: u64,
|
time: u64,
|
||||||
unit: UnitOfTime,
|
unit: UnitOfTime,
|
||||||
|
|
|
@ -9,12 +9,13 @@ use teloxide::{
|
||||||
utils::command::BotCommands,
|
utils::command::BotCommands,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// These commands are supported:
|
||||||
#[derive(BotCommands)]
|
#[derive(BotCommands)]
|
||||||
#[command(rename_rule = "lowercase", description = "These commands are supported:")]
|
#[command(rename_rule = "lowercase")]
|
||||||
enum Command {
|
enum Command {
|
||||||
#[command(description = "Display this text")]
|
/// Display this text
|
||||||
Help,
|
Help,
|
||||||
#[command(description = "Start")]
|
/// Start
|
||||||
Start,
|
Start,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,14 +10,16 @@ async fn main() {
|
||||||
Command::repl(bot, answer).await;
|
Command::repl(bot, answer).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// These commands are supported:
|
||||||
#[derive(BotCommands, Clone)]
|
#[derive(BotCommands, Clone)]
|
||||||
#[command(rename_rule = "lowercase", description = "These commands are supported:")]
|
#[command(rename_rule = "lowercase")]
|
||||||
enum Command {
|
enum Command {
|
||||||
#[command(description = "display this text.")]
|
/// Display this text.
|
||||||
Help,
|
Help,
|
||||||
#[command(description = "handle a username.")]
|
/// Handle a username.
|
||||||
Username(String),
|
Username(String),
|
||||||
#[command(description = "handle a username and an age.", parse_with = "split")]
|
/// Handle a username and an age.
|
||||||
|
#[command(parse_with = "split")]
|
||||||
UsernameAndAge { username: String, age: u8 },
|
UsernameAndAge { username: String, age: u8 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,12 +21,13 @@ pub enum State {
|
||||||
GotNumber(i32),
|
GotNumber(i32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// These commands are supported:
|
||||||
#[derive(Clone, BotCommands)]
|
#[derive(Clone, BotCommands)]
|
||||||
#[command(rename_rule = "lowercase", description = "These commands are supported:")]
|
#[command(rename_rule = "lowercase")]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
#[command(description = "get your number.")]
|
/// Get your number.
|
||||||
Get,
|
Get,
|
||||||
#[command(description = "reset your number.")]
|
/// Reset your number.
|
||||||
Reset,
|
Reset,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,21 +95,24 @@ struct ConfigParameters {
|
||||||
maintainer_username: Option<String>,
|
maintainer_username: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simple commands
|
||||||
#[derive(BotCommands, Clone)]
|
#[derive(BotCommands, Clone)]
|
||||||
#[command(rename_rule = "lowercase", description = "Simple commands")]
|
#[command(rename_rule = "lowercase")]
|
||||||
enum SimpleCommand {
|
enum SimpleCommand {
|
||||||
#[command(description = "shows this message.")]
|
/// Shows this message.
|
||||||
Help,
|
Help,
|
||||||
#[command(description = "shows maintainer info.")]
|
/// Shows maintainer info.
|
||||||
Maintainer,
|
Maintainer,
|
||||||
#[command(description = "shows your ID.")]
|
/// Shows your ID.
|
||||||
MyId,
|
MyId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maintainer commands
|
||||||
#[derive(BotCommands, Clone)]
|
#[derive(BotCommands, Clone)]
|
||||||
#[command(rename_rule = "lowercase", description = "Maintainer commands")]
|
#[command(rename_rule = "lowercase")]
|
||||||
enum MaintainerCommands {
|
enum MaintainerCommands {
|
||||||
#[command(parse_with = "split", description = "generate a number within range")]
|
/// Generate a number within range
|
||||||
|
#[command(parse_with = "split")]
|
||||||
Rand { from: u64, to: u64 },
|
Rand { from: u64, to: u64 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,14 +32,15 @@ pub enum State {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// These commands are supported:
|
||||||
#[derive(BotCommands, Clone)]
|
#[derive(BotCommands, Clone)]
|
||||||
#[command(rename_rule = "lowercase", description = "These commands are supported:")]
|
#[command(rename_rule = "lowercase")]
|
||||||
enum Command {
|
enum Command {
|
||||||
#[command(description = "display this text.")]
|
/// Display this text.
|
||||||
Help,
|
Help,
|
||||||
#[command(description = "start the purchase procedure.")]
|
/// Start the purchase procedure.
|
||||||
Start,
|
Start,
|
||||||
#[command(description = "cancel the purchase procedure.")]
|
/// Cancel the purchase procedure.
|
||||||
Cancel,
|
Cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ pub use teloxide_macros::BotCommands;
|
||||||
/// 2. `#[command(prefix = "prefix")]`
|
/// 2. `#[command(prefix = "prefix")]`
|
||||||
/// Change a prefix for all commands (the default is `/`).
|
/// Change a prefix for all commands (the default is `/`).
|
||||||
///
|
///
|
||||||
/// 3. `#[command(description = "description")]`
|
/// 3. `#[command(description = "description")]` and `/// description`
|
||||||
/// Add a summary description of commands before all commands.
|
/// Add a summary description of commands before all commands.
|
||||||
///
|
///
|
||||||
/// 4. `#[command(parse_with = "parser")]`
|
/// 4. `#[command(parse_with = "parser")]`
|
||||||
|
@ -167,7 +167,7 @@ pub use teloxide_macros::BotCommands;
|
||||||
/// Rename one command to `name` (literal renaming; do not confuse with
|
/// Rename one command to `name` (literal renaming; do not confuse with
|
||||||
/// `rename_rule`).
|
/// `rename_rule`).
|
||||||
///
|
///
|
||||||
/// 3. `#[command(description = "description")]`
|
/// 3. `#[command(description = "description")]` and `/// description`
|
||||||
/// Give your command a description. It will be shown in the help message.
|
/// Give your command a description. It will be shown in the help message.
|
||||||
///
|
///
|
||||||
/// 4. `#[command(parse_with = "parser")]`
|
/// 4. `#[command(parse_with = "parser")]`
|
||||||
|
|
|
@ -228,18 +228,66 @@ fn parse_named_fields() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "macros")]
|
#[cfg(feature = "macros")]
|
||||||
|
#[allow(deprecated)]
|
||||||
fn descriptions_off() {
|
fn descriptions_off() {
|
||||||
#[derive(BotCommands, Debug, PartialEq)]
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
#[command(rename_rule = "lowercase")]
|
#[command(rename_rule = "lowercase")]
|
||||||
enum DefaultCommands {
|
enum DefaultCommands {
|
||||||
#[command(hide)]
|
#[command(hide)]
|
||||||
Start,
|
Start,
|
||||||
#[command(hide)]
|
#[command(description = "off")]
|
||||||
Username,
|
Username,
|
||||||
|
/// off
|
||||||
Help,
|
Help,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(DefaultCommands::descriptions().to_string(), "/help".to_owned());
|
assert_eq!(DefaultCommands::descriptions().to_string(), "/help — off".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "macros")]
|
||||||
|
fn description_with_doc_attr() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename_rule = "lowercase")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
/// Start command
|
||||||
|
Start,
|
||||||
|
/// Help command\nwithout replace the `\n`
|
||||||
|
Help,
|
||||||
|
/// Foo command
|
||||||
|
/// with new line
|
||||||
|
Foo,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::descriptions().to_string(),
|
||||||
|
"/start — Start command\n/help — Help command\\nwithout replace the `\\n`\n/foo — Foo \
|
||||||
|
command\nwith new line"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "macros")]
|
||||||
|
fn description_with_doc_attr_and_command() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename_rule = "lowercase")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
/// Start command
|
||||||
|
#[command(description = "Start command")]
|
||||||
|
Start,
|
||||||
|
#[command(description = "Help command\nwith new line")]
|
||||||
|
Help,
|
||||||
|
/// Foo command
|
||||||
|
/// with new line
|
||||||
|
#[command(description = "Foo command\nwith new line")]
|
||||||
|
Foo,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::descriptions().to_string(),
|
||||||
|
"/start — Start command\nStart command\n/help — Help command\nwith new line\n/foo — Foo \
|
||||||
|
command\nwith new line\nFoo command\nwith new line"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Add table
Reference in a new issue