mirror of
https://github.com/teloxide/teloxide.git
synced 2024-12-22 22:46:39 +01:00
Merge branch 'teloxide:master' into tryable_bot
This commit is contained in:
commit
d2d572cce3
39 changed files with 473 additions and 177 deletions
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
|
@ -19,12 +19,14 @@ env:
|
||||||
# When updating this, also update:
|
# When updating this, also update:
|
||||||
# - crates/teloxide-core/src/codegen.rs
|
# - crates/teloxide-core/src/codegen.rs
|
||||||
# - rust-toolchain.toml
|
# - rust-toolchain.toml
|
||||||
rust_nightly: nightly-2023-05-28
|
rust_nightly: nightly-2023-09-27
|
||||||
# When updating this, also update:
|
# When updating this, also update:
|
||||||
# - **/README.md
|
# - **/README.md
|
||||||
# - **/src/lib.rs
|
# - **/src/lib.rs
|
||||||
# - down below in a matrix
|
# - down below in a matrix
|
||||||
rust_msrv: 1.65.0
|
# - `Cargo.toml`
|
||||||
|
# - **/CHANGELOG.md
|
||||||
|
rust_msrv: 1.68.0
|
||||||
|
|
||||||
CI: 1
|
CI: 1
|
||||||
|
|
||||||
|
@ -33,6 +35,7 @@ jobs:
|
||||||
ci-pass:
|
ci-pass:
|
||||||
name: CI succeeded
|
name: CI succeeded
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: always()
|
||||||
|
|
||||||
needs:
|
needs:
|
||||||
- fmt
|
- fmt
|
||||||
|
@ -42,7 +45,10 @@ jobs:
|
||||||
- doc
|
- doc
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- run: exit 0
|
- name: Check whether the needed jobs succeeded or failed
|
||||||
|
uses: re-actors/alls-green@release/v1
|
||||||
|
with:
|
||||||
|
jobs: ${{ toJSON(needs) }}
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
name: fmt
|
name: fmt
|
||||||
|
@ -84,10 +90,10 @@ jobs:
|
||||||
toolchain: beta
|
toolchain: beta
|
||||||
features: "--features full"
|
features: "--features full"
|
||||||
- rust: nightly
|
- rust: nightly
|
||||||
toolchain: nightly-2023-05-28
|
toolchain: nightly-2023-09-27
|
||||||
features: "--features full nightly"
|
features: "--features full nightly"
|
||||||
- rust: msrv
|
- rust: msrv
|
||||||
toolchain: 1.65.0
|
toolchain: 1.68.0
|
||||||
features: "--features full"
|
features: "--features full"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -7,9 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
## unreleased
|
## unreleased
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Add `MessageToCopyNotFound` error to `teloxide::errors::ApiError` ([PR 917](https://github.com/teloxide/teloxide/pull/917))
|
- 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))
|
||||||
|
- Greatly improved the speed of graceful shutdown (`^C`) ([PR 938](https://github.com/teloxide/teloxide/pull/938))
|
||||||
|
- Fix typos in docstrings ([PR 953](https://github.com/teloxide/teloxide/pull/953))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- MSRV (Minimal Supported Rust Version) was bumped from `1.64.0` to `1.68.0` ([PR 950][https://github.com/teloxide/teloxide/pull/950])
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- `UpdateListener::timeout_hint` and related APIs ([PR 938](https://github.com/teloxide/teloxide/pull/938))
|
||||||
|
|
||||||
## 0.12.2 - 2023-02-15
|
## 0.12.2 - 2023-02-15
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
# The settings below will be applied to all crates in the workspace
|
# The settings below will be applied to all crates in the workspace
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
# MSRV (minimal supported Rust version).
|
# MSRV (minimal supported Rust version).
|
||||||
rust-version = "1.65"
|
rust-version = "1.68"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
homepage = "https://github.com/teloxide/teloxide"
|
homepage = "https://github.com/teloxide/teloxide"
|
||||||
repository = "https://github.com/teloxide/teloxide"
|
repository = "https://github.com/teloxide/teloxide"
|
||||||
|
|
||||||
|
[workspace.metadata.release]
|
||||||
|
tag-message = "Release {{crate_name}} version {{version}}"
|
||||||
|
tag-name = "{{prefix}}v{{version}}"
|
||||||
|
|
|
@ -58,7 +58,7 @@ $ set TELOXIDE_TOKEN=<Your token here>
|
||||||
$ $env:TELOXIDE_TOKEN=<Your token here>
|
$ $env:TELOXIDE_TOKEN=<Your token here>
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Make sure that your Rust compiler is up to date (`teloxide` currently requires rustc at least version 1.65):
|
4. Make sure that your Rust compiler is up to date (`teloxide` currently requires rustc at least version 1.68):
|
||||||
```bash
|
```bash
|
||||||
# If you're using stable
|
# If you're using stable
|
||||||
$ rustup update stable
|
$ rustup update stable
|
||||||
|
|
|
@ -54,7 +54,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- `CallbackGame`, `ForumTopicClosed`, `ForumTopicReopened`, `GeneralForumTopicHidden`, `GeneralForumTopicUnhidden` and `WriteAccessAllowed` structures
|
- `CallbackGame`, `ForumTopicClosed`, `ForumTopicReopened`, `GeneralForumTopicHidden`, `GeneralForumTopicUnhidden` and `WriteAccessAllowed` structures
|
||||||
are now defined as named (`struct S {}`) instead of unit (`struct S;`) in order to fix their deserialization ([#876][pr876])
|
are now defined as named (`struct S {}`) instead of unit (`struct S;`) in order to fix their deserialization ([#876][pr876])
|
||||||
- `Download` now uses GAT feature on the `Fut` and `Err` associated types, instead of a lifetime on the whole trait ([#885][pr885])
|
- `Download` now uses GAT feature on the `Fut` and `Err` associated types, instead of a lifetime on the whole trait ([#885][pr885])
|
||||||
- MSRV (Minimal Supported Rust Version) was bumped from `1.64.0` to `1.65.0`
|
|
||||||
- Renamed `ForumTopic::message_thread_id` into `thread_id` ([#887][pr887])
|
- Renamed `ForumTopic::message_thread_id` into `thread_id` ([#887][pr887])
|
||||||
- `ForumTopic::thread_id` and `Message::thread_id` now use `ThreadId` instead of `i32` ([#887][pr887])
|
- `ForumTopic::thread_id` and `Message::thread_id` now use `ThreadId` instead of `i32` ([#887][pr887])
|
||||||
- `message_thread_id` method parameters now use `ThreadId` instead of `i32` ([#887][pr887])
|
- `message_thread_id` method parameters now use `ThreadId` instead of `i32` ([#887][pr887])
|
||||||
|
@ -67,12 +66,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Use `u32` for sizes and `Seconds` for timespans in `InlineQueryResult*` ([#887][pr887])
|
- Use `u32` for sizes and `Seconds` for timespans in `InlineQueryResult*` ([#887][pr887])
|
||||||
- `SendGame::reply_to_message_id`, `SendSticker::reply_to_message_id` and `SendInvoice::reply_to_message_id` now use `MessageId` instead of `i32` ([#887][pr887])
|
- `SendGame::reply_to_message_id`, `SendSticker::reply_to_message_id` and `SendInvoice::reply_to_message_id` now use `MessageId` instead of `i32` ([#887][pr887])
|
||||||
- Use `UpdateId` for `Update::id` ([#892][pr892])
|
- Use `UpdateId` for `Update::id` ([#892][pr892])
|
||||||
|
- MSRV (Minimal Supported Rust Version) was bumped from `1.64.0` to `1.68.0` ([#950][pr950])
|
||||||
|
|
||||||
[pr852]: https://github.com/teloxide/teloxide/pull/853
|
[pr852]: https://github.com/teloxide/teloxide/pull/853
|
||||||
[pr859]: https://github.com/teloxide/teloxide/pull/859
|
[pr859]: https://github.com/teloxide/teloxide/pull/859
|
||||||
[pr876]: https://github.com/teloxide/teloxide/pull/876
|
[pr876]: https://github.com/teloxide/teloxide/pull/876
|
||||||
[pr885]: https://github.com/teloxide/teloxide/pull/885
|
[pr885]: https://github.com/teloxide/teloxide/pull/885
|
||||||
[pr892]: https://github.com/teloxide/teloxide/pull/892
|
[pr892]: https://github.com/teloxide/teloxide/pull/892
|
||||||
|
[pr950]: https://github.com/teloxide/teloxide/pull/950
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ native-tls = ["reqwest/native-tls"]
|
||||||
nightly = []
|
nightly = []
|
||||||
|
|
||||||
# Throttling bot adaptor
|
# Throttling bot adaptor
|
||||||
throttle = ["vecrem", "tokio/macros"]
|
throttle = ["vecrem"]
|
||||||
|
|
||||||
# Trace bot adaptor
|
# Trace bot adaptor
|
||||||
trace_adaptor = []
|
trace_adaptor = []
|
||||||
|
@ -55,7 +55,11 @@ tokio = { version = "1.12.0", features = ["fs"] }
|
||||||
tokio-util = { version = "0.7.0", features = ["codec"] }
|
tokio-util = { version = "0.7.0", features = ["codec"] }
|
||||||
pin-project = "1.0.12"
|
pin-project = "1.0.12"
|
||||||
bytes = "1.0.0"
|
bytes = "1.0.0"
|
||||||
reqwest = { version = "0.11.10", features = ["json", "stream", "multipart"], default-features = false }
|
reqwest = { version = "0.11.10", features = [
|
||||||
|
"json",
|
||||||
|
"stream",
|
||||||
|
"multipart",
|
||||||
|
], default-features = false }
|
||||||
url = { version = "2", features = ["serde"] }
|
url = { version = "2", features = ["serde"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
||||||
|
@ -81,7 +85,12 @@ vecrem = { version = "0.1", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
tokio = { version = "1.8.0", features = ["fs", "macros", "macros", "rt-multi-thread"] }
|
tokio = { version = "1.8.0", features = [
|
||||||
|
"fs",
|
||||||
|
"macros",
|
||||||
|
"macros",
|
||||||
|
"rt-multi-thread",
|
||||||
|
] }
|
||||||
cool_asserts = "2.0.3"
|
cool_asserts = "2.0.3"
|
||||||
|
|
||||||
xshell = "0.2"
|
xshell = "0.2"
|
||||||
|
@ -98,6 +107,14 @@ rustdoc-args = ["--cfg", "docsrs", "-Znormalize-docs"]
|
||||||
# https://github.com/rust-lang/rust/issues/88791
|
# https://github.com/rust-lang/rust/issues/88791
|
||||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||||
|
|
||||||
|
[package.metadata.release]
|
||||||
|
tag-prefix = "core-"
|
||||||
|
enable-features = ["full"]
|
||||||
|
pre-release-replacements = [
|
||||||
|
{ file = "README.md", search = "teloxide-core = \"[^\"]+\"", replace = "teloxide-core = \"{{version}}\"" },
|
||||||
|
{ file = "src/lib.rs", search = "teloxide-core = \"[^\"]+\"", replace = "teloxide-core = \"{{version}}\"" },
|
||||||
|
{ file = "CHANGELOG.md", search = "## unreleased", replace = "## unreleased\n\n## {{version}} - {{date}}", exactly = 1 },
|
||||||
|
]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "self_info"
|
name = "self_info"
|
||||||
|
@ -110,4 +127,9 @@ doc-scrape-examples = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "erased"
|
name = "erased"
|
||||||
required-features = ["tokio/macros", "tokio/rt-multi-thread", "erased", "trace_adaptor"]
|
required-features = [
|
||||||
|
"tokio/macros",
|
||||||
|
"tokio/rt-multi-thread",
|
||||||
|
"erased",
|
||||||
|
"trace_adaptor",
|
||||||
|
]
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
```toml
|
```toml
|
||||||
teloxide-core = "0.9"
|
teloxide-core = "0.9"
|
||||||
```
|
```
|
||||||
_Compiler support: requires rustc 1.65+_.
|
_Compiler support: requires rustc 1.68+_.
|
||||||
|
|
||||||
[`teloxide`]: https://docs.rs/teloxide
|
[`teloxide`]: https://docs.rs/teloxide
|
||||||
[Telegram Bot API]: https://core.telegram.org/bots/api
|
[Telegram Bot API]: https://core.telegram.org/bots/api
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::{hash_map::Entry, HashMap, VecDeque},
|
collections::{hash_map::Entry, HashMap, VecDeque},
|
||||||
|
pin::pin,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use either::Either;
|
||||||
|
use futures::{future, FutureExt as _};
|
||||||
use tokio::sync::{mpsc, mpsc::error::TryRecvError, oneshot::Sender};
|
use tokio::sync::{mpsc, mpsc::error::TryRecvError, oneshot::Sender};
|
||||||
use vecrem::VecExt;
|
use vecrem::VecExt;
|
||||||
|
|
||||||
|
@ -129,17 +132,19 @@ pub(super) async fn worker<B>(
|
||||||
answer_info(&mut info_rx, &mut limits);
|
answer_info(&mut info_rx, &mut limits);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
let res = future::select(
|
||||||
freeze_until = freeze_rx.recv() => {
|
pin!(freeze_rx.recv()),
|
||||||
freeze(
|
pin!(read_from_rx(&mut rx, &mut queue, &mut rx_is_closed)),
|
||||||
&mut freeze_rx,
|
|
||||||
slow_mode.as_mut(),
|
|
||||||
&bot,
|
|
||||||
freeze_until
|
|
||||||
)
|
)
|
||||||
.await;
|
.map(either)
|
||||||
},
|
.await
|
||||||
() = read_from_rx(&mut rx, &mut queue, &mut rx_is_closed) => break,
|
.map_either(|l| l.0, |r| r.0);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Either::Left(freeze_until) => {
|
||||||
|
freeze(&mut freeze_rx, slow_mode.as_mut(), &bot, freeze_until).await;
|
||||||
|
}
|
||||||
|
Either::Right(()) => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//debug_assert_eq!(queue.capacity(), limits.messages_per_sec_overall as usize);
|
//debug_assert_eq!(queue.capacity(), limits.messages_per_sec_overall as usize);
|
||||||
|
@ -155,18 +160,18 @@ pub(super) async fn worker<B>(
|
||||||
//
|
//
|
||||||
// Reasons (not to use `spawn_blocking`):
|
// Reasons (not to use `spawn_blocking`):
|
||||||
//
|
//
|
||||||
// 1. The work seems not very CPU-bound, it's not heavy computations,
|
// 1. The work seems not very CPU-bound, it's not heavy computations, it's more
|
||||||
// it's more like light computations.
|
// like light computations.
|
||||||
//
|
//
|
||||||
// 2. `spawn_blocking` is not zero-cost — it spawns a new system thread
|
// 2. `spawn_blocking` is not zero-cost — it spawns a new system thread
|
||||||
// + do so other work. This may actually be *worse* then current
|
// + do so other work. This may actually be *worse* then current
|
||||||
// "just do everything in this async fn" approach.
|
// "just do everything in this async fn" approach.
|
||||||
//
|
//
|
||||||
// 3. With `rt-threaded` feature, tokio uses [`num_cpus()`] threads
|
// 3. With `rt-threaded` feature, tokio uses [`num_cpus()`] threads which should
|
||||||
// which should be enough to work fine with one a-bit-blocking task.
|
// be enough to work fine with one a-bit-blocking task. Crucially current
|
||||||
// Crucially current behaviour will be problem mostly with
|
// behaviour will be problem mostly with single-threaded runtimes (and in
|
||||||
// single-threaded runtimes (and in case you're using one, you
|
// case you're using one, you probably don't want to spawn unnecessary
|
||||||
// probably don't want to spawn unnecessary threads anyway).
|
// threads anyway).
|
||||||
//
|
//
|
||||||
// I think if we'll ever change this behaviour, we need to make it
|
// I think if we'll ever change this behaviour, we need to make it
|
||||||
// _configurable_.
|
// _configurable_.
|
||||||
|
@ -292,6 +297,8 @@ fn answer_info(rx: &mut mpsc::Receiver<InfoMessage>, limits: &mut Limits) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: https://github.com/rust-lang/rust-clippy/issues/11610
|
||||||
|
#[allow(clippy::needless_pass_by_ref_mut)]
|
||||||
async fn freeze(
|
async fn freeze(
|
||||||
rx: &mut mpsc::Receiver<FreezeUntil>,
|
rx: &mut mpsc::Receiver<FreezeUntil>,
|
||||||
mut slow_mode: Option<&mut HashMap<ChatIdHash, (Duration, Instant)>>,
|
mut slow_mode: Option<&mut HashMap<ChatIdHash, (Duration, Instant)>>,
|
||||||
|
@ -372,6 +379,13 @@ async fn read_from_rx<T>(rx: &mut mpsc::Receiver<T>, queue: &mut Vec<T>, rx_is_c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn either<L, R>(x: future::Either<L, R>) -> Either<L, R> {
|
||||||
|
match x {
|
||||||
|
future::Either::Left(l) => Either::Left(l),
|
||||||
|
future::Either::Right(r) => Either::Right(r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
|
@ -23,7 +23,7 @@ use xshell::{cmd, Shell};
|
||||||
|
|
||||||
fn ensure_rustfmt(sh: &Shell) {
|
fn ensure_rustfmt(sh: &Shell) {
|
||||||
// FIXME(waffle): find a better way to set toolchain
|
// FIXME(waffle): find a better way to set toolchain
|
||||||
let toolchain = "nightly-2023-05-28";
|
let toolchain = "nightly-2023-09-27";
|
||||||
|
|
||||||
let version = cmd!(sh, "rustup run {toolchain} rustfmt --version").read().unwrap_or_default();
|
let version = cmd!(sh, "rustup run {toolchain} rustfmt --version").read().unwrap_or_default();
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ fn ensure_rustfmt(sh: &Shell) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reformat(text: String) -> String {
|
pub fn reformat(text: String) -> String {
|
||||||
let toolchain = "nightly-2023-05-28";
|
let toolchain = "nightly-2023-09-27";
|
||||||
|
|
||||||
let sh = Shell::new().unwrap();
|
let sh = Shell::new().unwrap();
|
||||||
ensure_rustfmt(&sh);
|
ensure_rustfmt(&sh);
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
//! asynchronous and built using [`tokio`].
|
//! asynchronous and built using [`tokio`].
|
||||||
//!
|
//!
|
||||||
//!```toml
|
//!```toml
|
||||||
//! teloxide_core = "0.9"
|
//! teloxide-core = "0.9"
|
||||||
//! ```
|
//! ```
|
||||||
//! _Compiler support: requires rustc 1.65+_.
|
//! _Compiler support: requires rustc 1.68+_.
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! # async {
|
//! # async {
|
||||||
|
|
|
@ -1333,6 +1333,8 @@ macro_rules! requester_forward {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
// waffle: efficiency is not important here, and I don't want to rewrite this
|
||||||
|
#[allow(clippy::format_collect)]
|
||||||
fn codegen_requester_forward() {
|
fn codegen_requester_forward() {
|
||||||
use crate::codegen::{
|
use crate::codegen::{
|
||||||
add_hidden_preamble,
|
add_hidden_preamble,
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// waffle: efficiency is not important here, and I don't want to rewrite this
|
||||||
|
#![allow(clippy::format_collect)]
|
||||||
|
|
||||||
use std::{borrow::Borrow, collections::HashSet, ops::Deref};
|
use std::{borrow::Borrow, collections::HashSet, ops::Deref};
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
@ -51,7 +54,7 @@ fn codegen_payloads() {
|
||||||
|
|
||||||
let multipart = multipart_input_file_fields(&method)
|
let multipart = multipart_input_file_fields(&method)
|
||||||
.map(|field| format!(" @[multipart = {}]\n", field.join(", ")))
|
.map(|field| format!(" @[multipart = {}]\n", field.join(", ")))
|
||||||
.unwrap_or_else(String::new);
|
.unwrap_or_default();
|
||||||
|
|
||||||
let derive = if !multipart.is_empty()
|
let derive = if !multipart.is_empty()
|
||||||
|| matches!(
|
|| matches!(
|
||||||
|
|
|
@ -1337,6 +1337,8 @@ where
|
||||||
// }
|
// }
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
// waffle: efficiency is not important here, and I don't want to rewrite this
|
||||||
|
#[allow(clippy::format_collect)]
|
||||||
fn codegen_requester_methods() {
|
fn codegen_requester_methods() {
|
||||||
use crate::codegen::{
|
use crate::codegen::{
|
||||||
add_hidden_preamble,
|
add_hidden_preamble,
|
||||||
|
|
|
@ -315,7 +315,7 @@ pub(crate) mod serde_opt_date_from_unix_timestamp {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let json = r#"{}"#;
|
let json = "{}";
|
||||||
|
|
||||||
let Struct { date } = serde_json::from_str(json).unwrap();
|
let Struct { date } = serde_json::from_str(json).unwrap();
|
||||||
assert_eq!(date, None);
|
assert_eq!(date, None);
|
||||||
|
|
|
@ -114,7 +114,7 @@ impl InputFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shorthand for `Self { file_name: None, inner, id: default() }`
|
/// Shorthand for `Self { file_name: None, inner, id: default() }`
|
||||||
/// (private because `InnerFile` iы private implementation detail)
|
/// (private because `InnerFile` is private implementation detail)
|
||||||
fn new(inner: InnerFile) -> Self {
|
fn new(inner: InnerFile) -> Self {
|
||||||
Self { file_name: None, inner, id: OnceCell::new() }
|
Self { file_name: None, inner, id: OnceCell::new() }
|
||||||
}
|
}
|
||||||
|
@ -258,7 +258,7 @@ impl Read {
|
||||||
let res = ArcBox::<TakeCell<dyn AsyncRead + Send + Unpin>>::try_from(self.inner);
|
let res = ArcBox::<TakeCell<dyn AsyncRead + Send + Unpin>>::try_from(self.inner);
|
||||||
match res {
|
match res {
|
||||||
// Fast/easy path: this is the only file copy, so we can just forward the underlying
|
// Fast/easy path: this is the only file copy, so we can just forward the underlying
|
||||||
// `dyn AsynсRead` via some adaptors to reqwest.
|
// `dyn AsyncRead` via some adaptors to reqwest.
|
||||||
Ok(arc_box) => {
|
Ok(arc_box) => {
|
||||||
let fr = FramedRead::new(ExclusiveArcAsyncRead(arc_box), BytesDecoder);
|
let fr = FramedRead::new(ExclusiveArcAsyncRead(arc_box), BytesDecoder);
|
||||||
|
|
||||||
|
@ -270,7 +270,7 @@ impl Read {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slow path: either wait until someone will read the whole `dyn AsynсRead` into
|
// Slow path: either wait until someone will read the whole `dyn AsyncRead` into
|
||||||
// a buffer, or be the one who reads
|
// a buffer, or be the one who reads
|
||||||
let body = self.into_shared_body().await;
|
let body = self.into_shared_body().await;
|
||||||
|
|
||||||
|
@ -321,7 +321,7 @@ impl Read {
|
||||||
let _ = self.notify.send(());
|
let _ = self.notify.send(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait until `dyn AsynсRead` is read into a buffer, if it hasn't been read yet
|
// Wait until `dyn AsyncRead` is read into a buffer, if it hasn't been read yet
|
||||||
None if self.buf.get().is_none() => {
|
None if self.buf.get().is_none() => {
|
||||||
// Error indicates that the sender was dropped, by we hold `Arc<Sender>`, so
|
// Error indicates that the sender was dropped, by we hold `Arc<Sender>`, so
|
||||||
// this can't happen
|
// this can't happen
|
||||||
|
|
|
@ -452,7 +452,7 @@ pub struct InputMediaDocument {
|
||||||
/// using multipart/form-data.
|
/// using multipart/form-data.
|
||||||
pub thumb: Option<InputFile>,
|
pub thumb: Option<InputFile>,
|
||||||
|
|
||||||
/// Caption of the document to be sent, 0-1024 charactersю
|
/// Caption of the document to be sent, 0-1024 characters.
|
||||||
pub caption: Option<String>,
|
pub caption: Option<String>,
|
||||||
|
|
||||||
/// Send [Markdown] or [HTML], if you want Telegram apps to show [bold,
|
/// Send [Markdown] or [HTML], if you want Telegram apps to show [bold,
|
||||||
|
|
|
@ -203,8 +203,8 @@ impl PassportElementErrorReverseSide {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//// Represents an issue with the selfie with a document.
|
/// Represents an issue with the selfie with a document.
|
||||||
//
|
///
|
||||||
/// The error is considered resolved when the file with the selfie changes.
|
/// The error is considered resolved when the file with the selfie changes.
|
||||||
///
|
///
|
||||||
/// [The official docs](https://core.telegram.org/bots/api#passportelementerrorselfie).
|
/// [The official docs](https://core.telegram.org/bots/api#passportelementerrorselfie).
|
||||||
|
|
|
@ -31,7 +31,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chat_id_id_serialization() {
|
fn chat_id_id_serialization() {
|
||||||
let expected_json = String::from(r#"123456"#);
|
let expected_json = String::from("123456");
|
||||||
let actual_json = serde_json::to_string(&Recipient::Id(ChatId(123_456))).unwrap();
|
let actual_json = serde_json::to_string(&Recipient::Id(ChatId(123_456))).unwrap();
|
||||||
|
|
||||||
assert_eq!(expected_json, actual_json)
|
assert_eq!(expected_json, actual_json)
|
||||||
|
|
|
@ -66,7 +66,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn smoke_deser() {
|
fn smoke_deser() {
|
||||||
let json = r#"123"#;
|
let json = "123";
|
||||||
let mid: ThreadId = serde_json::from_str(json).unwrap();
|
let mid: ThreadId = serde_json::from_str(json).unwrap();
|
||||||
assert_eq!(mid, ThreadId(MessageId(123)));
|
assert_eq!(mid, ThreadId(MessageId(123)));
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,6 @@ mod tests {
|
||||||
fn smoke_ser() {
|
fn smoke_ser() {
|
||||||
let mid: ThreadId = ThreadId(MessageId(123));
|
let mid: ThreadId = ThreadId(MessageId(123));
|
||||||
let json = serde_json::to_string(&mid).unwrap();
|
let json = serde_json::to_string(&mid).unwrap();
|
||||||
assert_eq!(json, r#"123"#);
|
assert_eq!(json, "123");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## unreleased
|
## unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Now you can use `#[command(command_separator="sep")]` (default is a whitespace character) to set the separator between command and its arguments ([issue #897](https://github.com/teloxide/teloxide/issues/897))
|
||||||
|
- 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))
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- 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
|
### Changed
|
||||||
- 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))
|
- MSRV (Minimal Supported Rust Version) was bumped from `1.64.0` to `1.68.0` ([PR 950][https://github.com/teloxide/teloxide/pull/950])
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,17 @@ documentation = "https://docs.rs/teloxide-core/"
|
||||||
# FIXME: add a simple readme for teloxide-macros
|
# FIXME: add a simple readme for teloxide-macros
|
||||||
#readme = "README.md"
|
#readme = "README.md"
|
||||||
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
quote = "1.0.7"
|
quote = "1.0.7"
|
||||||
proc-macro2 = "1.0.19"
|
proc-macro2 = "1.0.67"
|
||||||
syn = { version = "1.0.13", features = ["full"] }
|
syn = { version = "1.0.13", features = ["full"] }
|
||||||
heck = "0.4.0"
|
heck = "0.4.0"
|
||||||
|
|
||||||
|
[package.metadata.release]
|
||||||
|
tag-prefix = "macros-"
|
||||||
|
pre-release-replacements = [
|
||||||
|
{ file = "CHANGELOG.md", search = "## unreleased", replace = "## unreleased\n\n## {{version}} - {{date}}", exactly = 1 },
|
||||||
|
]
|
||||||
|
|
|
@ -19,10 +19,7 @@ pub(crate) fn fold_attrs<A, R>(
|
||||||
.filter(|&a| filter(a))
|
.filter(|&a| filter(a))
|
||||||
.flat_map(|attribute| {
|
.flat_map(|attribute| {
|
||||||
let Some(key) = attribute.path.get_ident().cloned() else {
|
let Some(key) = attribute.path.get_ident().cloned() else {
|
||||||
return vec![Err(compile_error_at(
|
return vec![Err(compile_error_at("expected an ident", attribute.path.span()))];
|
||||||
"expected an ident",
|
|
||||||
attribute.path.span(),
|
|
||||||
))];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match (|input: ParseStream<'_>| Attrs::parse_with_key(input, key))
|
match (|input: ParseStream<'_>| Attrs::parse_with_key(input, key))
|
||||||
|
|
|
@ -28,7 +28,7 @@ pub(crate) fn bot_commands_impl(input: DeriveInput) -> Result<TokenStream> {
|
||||||
|
|
||||||
let type_name = &input.ident;
|
let type_name = &input.ident;
|
||||||
let fn_descriptions = impl_descriptions(&var_info, &command_enum);
|
let fn_descriptions = impl_descriptions(&var_info, &command_enum);
|
||||||
let fn_parse = impl_parse(&var_info, &var_init);
|
let fn_parse = impl_parse(&var_info, &var_init, &command_enum.command_separator);
|
||||||
let fn_commands = impl_commands(&var_info);
|
let fn_commands = impl_commands(&var_info);
|
||||||
|
|
||||||
let trait_impl = quote! {
|
let trait_impl = quote! {
|
||||||
|
@ -99,6 +99,7 @@ fn impl_descriptions(infos: &[Command], global: &CommandEnum) -> proc_macro2::To
|
||||||
fn impl_parse(
|
fn impl_parse(
|
||||||
infos: &[Command],
|
infos: &[Command],
|
||||||
variants_initialization: &[proc_macro2::TokenStream],
|
variants_initialization: &[proc_macro2::TokenStream],
|
||||||
|
command_separator: &str,
|
||||||
) -> proc_macro2::TokenStream {
|
) -> proc_macro2::TokenStream {
|
||||||
let matching_values = infos.iter().map(|c| c.get_prefixed_command());
|
let matching_values = infos.iter().map(|c| c.get_prefixed_command());
|
||||||
|
|
||||||
|
@ -110,7 +111,7 @@ fn impl_parse(
|
||||||
|
|
||||||
// 2 is used to only split once (=> in two parts),
|
// 2 is used to only split once (=> in two parts),
|
||||||
// we only need to split the command and the rest of arguments.
|
// we only need to split the command and the rest of arguments.
|
||||||
let mut words = s.splitn(2, ' ');
|
let mut words = s.splitn(2, #command_separator);
|
||||||
|
|
||||||
// Unwrap: split iterators always have at least one item
|
// Unwrap: split iterators always have at least one item
|
||||||
let mut full_command = words.next().unwrap().split('@');
|
let mut full_command = words.next().unwrap().split('@');
|
||||||
|
|
|
@ -34,6 +34,8 @@ impl Command {
|
||||||
parser,
|
parser,
|
||||||
// FIXME: error on/do not ignore separator
|
// FIXME: error on/do not ignore separator
|
||||||
separator: _,
|
separator: _,
|
||||||
|
// FIXME: error on/do not ignore command separator
|
||||||
|
command_separator: _,
|
||||||
hide,
|
hide,
|
||||||
} = attrs;
|
} = attrs;
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ pub(crate) struct CommandAttrs {
|
||||||
pub rename: Option<(String, Span)>,
|
pub rename: Option<(String, Span)>,
|
||||||
pub parser: Option<(ParserType, Span)>,
|
pub parser: Option<(ParserType, Span)>,
|
||||||
pub separator: Option<(String, Span)>,
|
pub separator: Option<(String, Span)>,
|
||||||
|
pub command_separator: Option<(String, Span)>,
|
||||||
pub hide: Option<((), Span)>,
|
pub hide: Option<((), Span)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +49,7 @@ enum CommandAttrKind {
|
||||||
Rename(String),
|
Rename(String),
|
||||||
ParseWith(ParserType),
|
ParseWith(ParserType),
|
||||||
Separator(String),
|
Separator(String),
|
||||||
|
CommandSeparator(String),
|
||||||
Hide,
|
Hide,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +68,7 @@ impl CommandAttrs {
|
||||||
rename: None,
|
rename: None,
|
||||||
parser: None,
|
parser: None,
|
||||||
separator: None,
|
separator: None,
|
||||||
|
command_separator: None,
|
||||||
hide: None,
|
hide: None,
|
||||||
},
|
},
|
||||||
|mut this, attr| {
|
|mut this, attr| {
|
||||||
|
@ -110,6 +113,7 @@ impl CommandAttrs {
|
||||||
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),
|
||||||
Separator(s) => insert(&mut this.separator, s, attr.sp),
|
Separator(s) => insert(&mut this.separator, s, attr.sp),
|
||||||
|
CommandSeparator(s) => insert(&mut this.command_separator, s, attr.sp),
|
||||||
Hide => insert(&mut this.hide, (), attr.sp),
|
Hide => insert(&mut this.hide, (), attr.sp),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
|
@ -141,12 +145,11 @@ impl CommandAttr {
|
||||||
}
|
}
|
||||||
|
|
||||||
"command" => {
|
"command" => {
|
||||||
let Some(attr) = key.pop()
|
let Some(attr) = key.pop() else {
|
||||||
else {
|
|
||||||
return Err(compile_error_at(
|
return Err(compile_error_at(
|
||||||
"expected an attribute name",
|
"expected an attribute name",
|
||||||
outermost_key.span(),
|
outermost_key.span(),
|
||||||
))
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(unexpected_key) = key.last() {
|
if let Some(unexpected_key) = key.last() {
|
||||||
|
@ -165,6 +168,7 @@ impl CommandAttr {
|
||||||
"rename" => Rename(value.expect_string()?),
|
"rename" => Rename(value.expect_string()?),
|
||||||
"parse_with" => ParseWith(ParserType::parse(value)?),
|
"parse_with" => ParseWith(ParserType::parse(value)?),
|
||||||
"separator" => Separator(value.expect_string()?),
|
"separator" => Separator(value.expect_string()?),
|
||||||
|
"command_separator" => CommandSeparator(value.expect_string()?),
|
||||||
"hide" => value.expect_none("hide").map(|_| Hide)?,
|
"hide" => value.expect_none("hide").map(|_| Hide)?,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(compile_error_at(
|
return Err(compile_error_at(
|
||||||
|
|
|
@ -7,6 +7,7 @@ pub(crate) struct CommandEnum {
|
||||||
pub prefix: String,
|
pub prefix: String,
|
||||||
/// The bool is true if the description contains a doc comment
|
/// The bool is true if the description contains a doc comment
|
||||||
pub description: Option<(String, bool)>,
|
pub description: Option<(String, bool)>,
|
||||||
|
pub command_separator: String,
|
||||||
pub rename_rule: RenameRule,
|
pub rename_rule: RenameRule,
|
||||||
pub parser_type: ParserType,
|
pub parser_type: ParserType,
|
||||||
}
|
}
|
||||||
|
@ -14,8 +15,16 @@ pub(crate) struct CommandEnum {
|
||||||
impl CommandEnum {
|
impl CommandEnum {
|
||||||
pub fn from_attributes(attributes: &[syn::Attribute]) -> Result<Self> {
|
pub fn from_attributes(attributes: &[syn::Attribute]) -> Result<Self> {
|
||||||
let attrs = CommandAttrs::from_attributes(attributes)?;
|
let attrs = CommandAttrs::from_attributes(attributes)?;
|
||||||
let CommandAttrs { prefix, description, rename_rule, rename, parser, separator, hide } =
|
let CommandAttrs {
|
||||||
attrs;
|
prefix,
|
||||||
|
description,
|
||||||
|
rename_rule,
|
||||||
|
rename,
|
||||||
|
parser,
|
||||||
|
separator,
|
||||||
|
command_separator,
|
||||||
|
hide,
|
||||||
|
} = attrs;
|
||||||
|
|
||||||
if let Some((_rename, sp)) = rename {
|
if let Some((_rename, sp)) = rename {
|
||||||
return Err(compile_error_at(
|
return Err(compile_error_at(
|
||||||
|
@ -39,6 +48,9 @@ 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, is_doc, _)| (d, is_doc)),
|
description: description.map(|(d, is_doc, _)| (d, is_doc)),
|
||||||
|
command_separator: command_separator
|
||||||
|
.map(|(s, _)| s)
|
||||||
|
.unwrap_or_else(|| String::from(" ")),
|
||||||
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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,6 +4,7 @@ version = "0.12.2"
|
||||||
description = "An elegant Telegram bots framework for Rust"
|
description = "An elegant Telegram bots framework for Rust"
|
||||||
|
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
|
@ -37,7 +38,9 @@ native-tls = ["teloxide-core/native-tls"]
|
||||||
rustls = ["teloxide-core/rustls"]
|
rustls = ["teloxide-core/rustls"]
|
||||||
auto-send = ["teloxide-core/auto_send"]
|
auto-send = ["teloxide-core/auto_send"]
|
||||||
throttle = ["teloxide-core/throttle"]
|
throttle = ["teloxide-core/throttle"]
|
||||||
cache-me = ["teloxide-core/cache_me"] # FIXME: why teloxide and core use - _ differently?
|
cache-me = [
|
||||||
|
"teloxide-core/cache_me",
|
||||||
|
] # FIXME: why teloxide and core use - _ differently?
|
||||||
trace-adaptor = ["teloxide-core/trace_adaptor"]
|
trace-adaptor = ["teloxide-core/trace_adaptor"]
|
||||||
erased = ["teloxide-core/erased"]
|
erased = ["teloxide-core/erased"]
|
||||||
|
|
||||||
|
@ -94,6 +97,7 @@ futures = "0.3.15"
|
||||||
pin-project = "1.0"
|
pin-project = "1.0"
|
||||||
serde_with_macros = "1.4"
|
serde_with_macros = "1.4"
|
||||||
aquamarine = "0.1.11"
|
aquamarine = "0.1.11"
|
||||||
|
either = "1.9.0"
|
||||||
|
|
||||||
sqlx = { version = "0.6", optional = true, default-features = false, features = [
|
sqlx = { version = "0.6", optional = true, default-features = false, features = [
|
||||||
"macros",
|
"macros",
|
||||||
|
@ -127,6 +131,13 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||||
rustc-args = ["--cfg", "dep_docsrs"]
|
rustc-args = ["--cfg", "dep_docsrs"]
|
||||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||||
|
|
||||||
|
[package.metadata.release]
|
||||||
|
tag-prefix = ""
|
||||||
|
enable-features = ["full"]
|
||||||
|
pre-release-replacements = [
|
||||||
|
{ file = "../../README.md", search = "teloxide = \\{ version = \"[^\"]+\"", replace = "teloxide = { version = \"{{version}}\"" },
|
||||||
|
{ file = "../../CHANGELOG.md", search = "## unreleased", replace = "## unreleased\n\n## {{version}} - {{date}}", exactly = 1 },
|
||||||
|
]
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
|
@ -158,7 +169,12 @@ required-features = ["macros", "ctrlc_handler"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "db_remember"
|
name = "db_remember"
|
||||||
required-features = ["sqlite-storage", "redis-storage", "bincode-serializer", "macros"]
|
required-features = [
|
||||||
|
"sqlite-storage",
|
||||||
|
"redis-storage",
|
||||||
|
"bincode-serializer",
|
||||||
|
"macros",
|
||||||
|
]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "dialogue"
|
name = "dialogue"
|
||||||
|
|
|
@ -42,12 +42,12 @@ impl<S> SqliteStorage<S> {
|
||||||
let pool = SqlitePool::connect(format!("sqlite:{path}?mode=rwc").as_str()).await?;
|
let pool = SqlitePool::connect(format!("sqlite:{path}?mode=rwc").as_str()).await?;
|
||||||
let mut conn = pool.acquire().await?;
|
let mut conn = pool.acquire().await?;
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
"
|
||||||
CREATE TABLE IF NOT EXISTS teloxide_dialogues (
|
CREATE TABLE IF NOT EXISTS teloxide_dialogues (
|
||||||
chat_id BIGINT PRIMARY KEY,
|
chat_id BIGINT PRIMARY KEY,
|
||||||
dialogue BLOB NOT NULL
|
dialogue BLOB NOT NULL
|
||||||
);
|
);
|
||||||
"#,
|
",
|
||||||
)
|
)
|
||||||
.execute(&mut conn)
|
.execute(&mut conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -98,10 +98,10 @@ where
|
||||||
.await?
|
.await?
|
||||||
.execute(
|
.execute(
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
"
|
||||||
INSERT INTO teloxide_dialogues VALUES (?, ?)
|
INSERT INTO teloxide_dialogues VALUES (?, ?)
|
||||||
ON CONFLICT(chat_id) DO UPDATE SET dialogue=excluded.dialogue
|
ON CONFLICT(chat_id) DO UPDATE SET dialogue=excluded.dialogue
|
||||||
"#,
|
",
|
||||||
)
|
)
|
||||||
.bind(chat_id)
|
.bind(chat_id)
|
||||||
.bind(d),
|
.bind(d),
|
||||||
|
|
|
@ -7,12 +7,15 @@ use crate::{
|
||||||
requests::{Request, Requester},
|
requests::{Request, Requester},
|
||||||
types::{Update, UpdateKind},
|
types::{Update, UpdateKind},
|
||||||
update_listeners::{self, UpdateListener},
|
update_listeners::{self, UpdateListener},
|
||||||
utils::shutdown_token::shutdown_check_timeout_for,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use dptree::di::{DependencyMap, DependencySupplier};
|
use dptree::di::{DependencyMap, DependencySupplier};
|
||||||
use futures::{future::BoxFuture, stream::FuturesUnordered, StreamExt};
|
use either::Either;
|
||||||
use tokio::time::timeout;
|
use futures::{
|
||||||
|
future::{self, BoxFuture},
|
||||||
|
stream::FuturesUnordered,
|
||||||
|
FutureExt as _, StreamExt as _,
|
||||||
|
};
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -21,6 +24,7 @@ use std::{
|
||||||
future::Future,
|
future::Future,
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
ops::{ControlFlow, Deref},
|
ops::{ControlFlow, Deref},
|
||||||
|
pin::pin,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, AtomicU32, Ordering},
|
atomic::{AtomicBool, AtomicU32, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
|
@ -336,7 +340,6 @@ where
|
||||||
log::debug!("hinting allowed updates: {:?}", allowed_updates);
|
log::debug!("hinting allowed updates: {:?}", allowed_updates);
|
||||||
update_listener.hint_allowed_updates(&mut allowed_updates.into_iter());
|
update_listener.hint_allowed_updates(&mut allowed_updates.into_iter());
|
||||||
|
|
||||||
let shutdown_check_timeout = shutdown_check_timeout_for(&update_listener);
|
|
||||||
let mut stop_token = Some(update_listener.stop_token());
|
let mut stop_token = Some(update_listener.stop_token());
|
||||||
|
|
||||||
self.state.start_dispatching();
|
self.state.start_dispatching();
|
||||||
|
@ -348,15 +351,17 @@ where
|
||||||
loop {
|
loop {
|
||||||
self.remove_inactive_workers_if_needed().await;
|
self.remove_inactive_workers_if_needed().await;
|
||||||
|
|
||||||
// False positive
|
let res = future::select(stream.next(), pin!(self.state.wait_for_changes()))
|
||||||
#[allow(clippy::collapsible_match)]
|
.map(either)
|
||||||
if let Ok(upd) = timeout(shutdown_check_timeout, stream.next()).await {
|
.await
|
||||||
match upd {
|
.map_either(|l| l.0, |r| r.0);
|
||||||
None => break,
|
|
||||||
Some(upd) => self.process_update(upd, &update_listener_error_handler).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Either::Left(upd) => match upd {
|
||||||
|
Some(upd) => self.process_update(upd, &update_listener_error_handler).await,
|
||||||
|
None => break,
|
||||||
|
},
|
||||||
|
Either::Right(()) => {
|
||||||
if self.state.is_shutting_down() {
|
if self.state.is_shutting_down() {
|
||||||
if let Some(token) = stop_token.take() {
|
if let Some(token) = stop_token.take() {
|
||||||
log::debug!("Start shutting down dispatching...");
|
log::debug!("Start shutting down dispatching...");
|
||||||
|
@ -365,6 +370,8 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.workers
|
self.workers
|
||||||
.drain()
|
.drain()
|
||||||
|
@ -609,6 +616,12 @@ async fn handle_update<Err>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn either<L, R>(x: future::Either<L, R>) -> Either<L, R> {
|
||||||
|
match x {
|
||||||
|
future::Either::Left(l) => Either::Left(l),
|
||||||
|
future::Either::Right(r) => Either::Right(r),
|
||||||
|
}
|
||||||
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
|
|
|
@ -165,9 +165,9 @@ where
|
||||||
/// the [`Requester`] trait.
|
/// the [`Requester`] trait.
|
||||||
/// 2. `handler` is an `async` function that takes arguments from
|
/// 2. `handler` is an `async` function that takes arguments from
|
||||||
/// [`DependencyMap`] (see below) and returns [`ResponseResult`].
|
/// [`DependencyMap`] (see below) and returns [`ResponseResult`].
|
||||||
/// 3. `cmd` is a type hint for your command enumeration
|
/// 3. `cmd` is a type hint for your command enumeration `MyCommand`: just write
|
||||||
/// `MyCommand`: just write `MyCommand::ty()`. Note that `MyCommand` must
|
/// `MyCommand::ty()`. Note that `MyCommand` must implement the
|
||||||
/// implement the [`BotCommands`] trait, typically via
|
/// [`BotCommands`] trait, typically via
|
||||||
/// `#[derive(BotCommands)]`.
|
/// `#[derive(BotCommands)]`.
|
||||||
///
|
///
|
||||||
/// All the other requirements are about thread safety and data validity and can
|
/// All the other requirements are about thread safety and data validity and can
|
||||||
|
@ -236,8 +236,8 @@ where
|
||||||
/// [`DependencyMap`] (see below) and returns [`ResponseResult`].
|
/// [`DependencyMap`] (see below) and returns [`ResponseResult`].
|
||||||
/// 3. `listener` is something that takes updates from a Telegram server and
|
/// 3. `listener` is something that takes updates from a Telegram server and
|
||||||
/// implements [`UpdateListener`].
|
/// implements [`UpdateListener`].
|
||||||
/// 4. `cmd` is a type hint for your command enumeration `MyCommand`: just
|
/// 4. `cmd` is a type hint for your command enumeration `MyCommand`: just write
|
||||||
/// write `MyCommand::ty()`. Note that `MyCommand` must implement the
|
/// `MyCommand::ty()`. Note that `MyCommand` must implement the
|
||||||
/// [`BotCommands`] trait, typically via `#[derive(BotCommands)]`.
|
/// [`BotCommands`] trait, typically via `#[derive(BotCommands)]`.
|
||||||
///
|
///
|
||||||
/// All the other requirements are about thread safety and data validity and can
|
/// All the other requirements are about thread safety and data validity and can
|
||||||
|
|
|
@ -32,8 +32,6 @@ pub mod webhooks;
|
||||||
|
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
stop::StopToken,
|
stop::StopToken,
|
||||||
types::{AllowedUpdate, Update},
|
types::{AllowedUpdate, Update},
|
||||||
|
@ -94,19 +92,6 @@ pub trait UpdateListener:
|
||||||
fn hint_allowed_updates(&mut self, hint: &mut dyn Iterator<Item = AllowedUpdate>) {
|
fn hint_allowed_updates(&mut self, hint: &mut dyn Iterator<Item = AllowedUpdate>) {
|
||||||
let _ = hint;
|
let _ = hint;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The timeout duration hint.
|
|
||||||
///
|
|
||||||
/// This hints how often dispatcher should check for a shutdown. E.g., for
|
|
||||||
/// [`polling()`] this returns the [`timeout`].
|
|
||||||
///
|
|
||||||
/// [`timeout`]: crate::payloads::GetUpdates::timeout
|
|
||||||
///
|
|
||||||
/// If you are implementing this trait and not sure what to return from this
|
|
||||||
/// function, just leave it with the default implementation.
|
|
||||||
fn timeout_hint(&self) -> Option<Duration> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`UpdateListener`]'s supertrait/extension.
|
/// [`UpdateListener`]'s supertrait/extension.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
convert::TryInto,
|
convert::TryInto,
|
||||||
future::Future,
|
future::Future,
|
||||||
|
mem,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{
|
task::{
|
||||||
self,
|
self,
|
||||||
|
@ -97,8 +98,16 @@ where
|
||||||
pub fn build(self) -> Polling<R> {
|
pub fn build(self) -> Polling<R> {
|
||||||
let Self { bot, timeout, limit, allowed_updates, drop_pending_updates } = self;
|
let Self { bot, timeout, limit, allowed_updates, drop_pending_updates } = self;
|
||||||
let (token, flag) = mk_stop_token();
|
let (token, flag) = mk_stop_token();
|
||||||
let polling =
|
let polling = Polling {
|
||||||
Polling { bot, timeout, limit, allowed_updates, drop_pending_updates, flag, token };
|
bot,
|
||||||
|
timeout,
|
||||||
|
limit,
|
||||||
|
allowed_updates,
|
||||||
|
drop_pending_updates,
|
||||||
|
flag: Some(flag),
|
||||||
|
token,
|
||||||
|
stop_token_cloned: false,
|
||||||
|
};
|
||||||
|
|
||||||
assert_update_listener(polling)
|
assert_update_listener(polling)
|
||||||
}
|
}
|
||||||
|
@ -240,17 +249,21 @@ pub struct Polling<B: Requester> {
|
||||||
limit: Option<u8>,
|
limit: Option<u8>,
|
||||||
allowed_updates: Option<Vec<AllowedUpdate>>,
|
allowed_updates: Option<Vec<AllowedUpdate>>,
|
||||||
drop_pending_updates: bool,
|
drop_pending_updates: bool,
|
||||||
flag: StopFlag,
|
flag: Option<StopFlag>,
|
||||||
token: StopToken,
|
token: StopToken,
|
||||||
|
stop_token_cloned: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R> Polling<R>
|
impl<R> Polling<R>
|
||||||
where
|
where
|
||||||
R: Requester + Send + 'static,
|
R: Requester,
|
||||||
<R as Requester>::GetUpdates: Send,
|
|
||||||
{
|
{
|
||||||
/// Returns a builder for polling update listener.
|
/// Returns a builder for polling update listener.
|
||||||
pub fn builder(bot: R) -> PollingBuilder<R> {
|
pub fn builder(bot: R) -> PollingBuilder<R>
|
||||||
|
where
|
||||||
|
R: Send + 'static,
|
||||||
|
<R as Requester>::GetUpdates: Send,
|
||||||
|
{
|
||||||
PollingBuilder {
|
PollingBuilder {
|
||||||
bot,
|
bot,
|
||||||
timeout: None,
|
timeout: None,
|
||||||
|
@ -259,6 +272,19 @@ where
|
||||||
drop_pending_updates: false,
|
drop_pending_updates: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if re-initialization happened *and*
|
||||||
|
/// the previous token was cloned.
|
||||||
|
fn reinit_stop_flag_if_needed(&mut self) -> bool {
|
||||||
|
if self.flag.is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (token, flag) = mk_stop_token();
|
||||||
|
self.token = token;
|
||||||
|
self.flag = Some(flag);
|
||||||
|
mem::replace(&mut self.stop_token_cloned, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pin_project::pin_project]
|
#[pin_project::pin_project]
|
||||||
|
@ -287,12 +313,18 @@ pub struct PollingStream<'a, B: Requester> {
|
||||||
/// In-flight `get_updates()` call.
|
/// In-flight `get_updates()` call.
|
||||||
#[pin]
|
#[pin]
|
||||||
in_flight: Option<<B::GetUpdates as Request>::Send>,
|
in_flight: Option<<B::GetUpdates as Request>::Send>,
|
||||||
|
|
||||||
|
/// The flag that notifies polling to stop polling.
|
||||||
|
#[pin]
|
||||||
|
flag: StopFlag,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: Requester + Send + 'static> UpdateListener for Polling<B> {
|
impl<B: Requester + Send + 'static> UpdateListener for Polling<B> {
|
||||||
type Err = B::Err;
|
type Err = B::Err;
|
||||||
|
|
||||||
fn stop_token(&mut self) -> StopToken {
|
fn stop_token(&mut self) -> StopToken {
|
||||||
|
self.reinit_stop_flag_if_needed();
|
||||||
|
self.stop_token_cloned = true;
|
||||||
self.token.clone()
|
self.token.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,10 +333,6 @@ impl<B: Requester + Send + 'static> UpdateListener for Polling<B> {
|
||||||
// before
|
// before
|
||||||
self.allowed_updates = Some(hint.collect());
|
self.allowed_updates = Some(hint.collect());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn timeout_hint(&self) -> Option<Duration> {
|
|
||||||
self.timeout
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, B: Requester + Send + 'a> AsUpdateStream<'a> for Polling<B> {
|
impl<'a, B: Requester + Send + 'a> AsUpdateStream<'a> for Polling<B> {
|
||||||
|
@ -315,6 +343,21 @@ impl<'a, B: Requester + Send + 'a> AsUpdateStream<'a> for Polling<B> {
|
||||||
let timeout = self.timeout.map(|t| t.as_secs().try_into().expect("timeout is too big"));
|
let timeout = self.timeout.map(|t| t.as_secs().try_into().expect("timeout is too big"));
|
||||||
let allowed_updates = self.allowed_updates.clone();
|
let allowed_updates = self.allowed_updates.clone();
|
||||||
let drop_pending_updates = self.drop_pending_updates;
|
let drop_pending_updates = self.drop_pending_updates;
|
||||||
|
|
||||||
|
let token_used_and_updated = self.reinit_stop_flag_if_needed();
|
||||||
|
|
||||||
|
// FIXME: document that `as_stream` is a destructive operation, actually,
|
||||||
|
// and you need to call `stop_token` *again* after it
|
||||||
|
if token_used_and_updated {
|
||||||
|
panic!(
|
||||||
|
"detected calling `as_stream` a second time after calling `stop_token`. \
|
||||||
|
`as_stream` updates the stop token, thus you need to call it again after calling \
|
||||||
|
`as_stream`"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap: just called reinit
|
||||||
|
let flag = self.flag.take().unwrap();
|
||||||
PollingStream {
|
PollingStream {
|
||||||
polling: self,
|
polling: self,
|
||||||
drop_pending_updates,
|
drop_pending_updates,
|
||||||
|
@ -325,6 +368,7 @@ impl<'a, B: Requester + Send + 'a> AsUpdateStream<'a> for Polling<B> {
|
||||||
stopping: false,
|
stopping: false,
|
||||||
buffer: Vec::new().into_iter(),
|
buffer: Vec::new().into_iter(),
|
||||||
in_flight: None,
|
in_flight: None,
|
||||||
|
flag,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -333,15 +377,33 @@ impl<B: Requester> Stream for PollingStream<'_, B> {
|
||||||
type Item = Result<Update, B::Err>;
|
type Item = Result<Update, B::Err>;
|
||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
log::trace!("polling polling stream");
|
||||||
let mut this = self.as_mut().project();
|
let mut this = self.as_mut().project();
|
||||||
|
|
||||||
if *this.force_stop {
|
if *this.force_stop {
|
||||||
return Ready(None);
|
return Ready(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there are any buffered updates, return one
|
||||||
|
if let Some(upd) = this.buffer.next() {
|
||||||
|
return Ready(Some(Ok(upd)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should stop and if so — drop in flight request,
|
||||||
|
// we don't care about updates that happened *after* we started stopping
|
||||||
|
//
|
||||||
|
// N.B.: it's important to use `poll` and not `is_stopped` here,
|
||||||
|
// so that *this stream* is polled when the flag is set to stop
|
||||||
|
if !*this.stopping && matches!(this.flag.poll(cx), Poll::Ready(())) {
|
||||||
|
*this.stopping = true;
|
||||||
|
|
||||||
|
log::trace!("dropping in-flight request");
|
||||||
|
this.in_flight.set(None);
|
||||||
|
}
|
||||||
// Poll in-flight future until completion
|
// Poll in-flight future until completion
|
||||||
if let Some(in_flight) = this.in_flight.as_mut().as_pin_mut() {
|
else if let Some(in_flight) = this.in_flight.as_mut().as_pin_mut() {
|
||||||
let res = ready!(in_flight.poll(cx));
|
let res = ready!(in_flight.poll(cx));
|
||||||
|
log::trace!("in-flight request completed");
|
||||||
this.in_flight.set(None);
|
this.in_flight.set(None);
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
|
@ -366,12 +428,6 @@ impl<B: Requester> Stream for PollingStream<'_, B> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are any buffered updates, return one
|
|
||||||
if let Some(upd) = this.buffer.next() {
|
|
||||||
return Ready(Some(Ok(upd)));
|
|
||||||
}
|
|
||||||
|
|
||||||
*this.stopping = this.polling.flag.is_stopped();
|
|
||||||
let (offset, limit, timeout) = match (this.stopping, this.drop_pending_updates) {
|
let (offset, limit, timeout) = match (this.stopping, this.drop_pending_updates) {
|
||||||
// Normal `get_updates()` call
|
// Normal `get_updates()` call
|
||||||
(false, false) => (*this.offset, this.polling.limit, *this.timeout),
|
(false, false) => (*this.offset, this.polling.limit, *this.timeout),
|
||||||
|
@ -380,7 +436,10 @@ impl<B: Requester> Stream for PollingStream<'_, B> {
|
||||||
//
|
//
|
||||||
// When stopping we set `timeout = 0` and `limit = 1` so that `get_updates()`
|
// When stopping we set `timeout = 0` and `limit = 1` so that `get_updates()`
|
||||||
// set last seen update (offset) and return immediately
|
// set last seen update (offset) and return immediately
|
||||||
(true, _) => (*this.offset, Some(1), Some(0)),
|
(true, _) => {
|
||||||
|
log::trace!("graceful shutdown `get_updates` call");
|
||||||
|
(*this.offset, Some(1), Some(0))
|
||||||
|
}
|
||||||
// Drop pending updates
|
// Drop pending updates
|
||||||
(_, true) => (-1, Some(1), Some(0)),
|
(_, true) => (-1, Some(1), Some(0)),
|
||||||
};
|
};
|
||||||
|
@ -398,8 +457,10 @@ impl<B: Requester> Stream for PollingStream<'_, B> {
|
||||||
.send();
|
.send();
|
||||||
this.in_flight.set(Some(req));
|
this.in_flight.set(Some(req));
|
||||||
|
|
||||||
// Recurse to poll `self.in_flight`
|
// Immediately wake up to poll `self.in_flight`
|
||||||
self.poll_next(cx)
|
// (without this this stream becomes a zombie)
|
||||||
|
cx.waker().wake_by_ref();
|
||||||
|
Poll::Pending
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -17,7 +15,7 @@ use crate::{
|
||||||
///
|
///
|
||||||
/// [`polling`]: crate::update_listeners::polling()
|
/// [`polling`]: crate::update_listeners::polling()
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct StatefulListener<St, Assf, Sf, Hauf, Thf> {
|
pub struct StatefulListener<St, Assf, Sf, Hauf> {
|
||||||
/// The state of the listener.
|
/// The state of the listener.
|
||||||
pub state: St,
|
pub state: St,
|
||||||
|
|
||||||
|
@ -36,38 +34,30 @@ pub struct StatefulListener<St, Assf, Sf, Hauf, Thf> {
|
||||||
/// Must implement `FnMut(&mut St, &mut dyn Iterator<Item =
|
/// Must implement `FnMut(&mut St, &mut dyn Iterator<Item =
|
||||||
/// AllowedUpdate>)`.
|
/// AllowedUpdate>)`.
|
||||||
pub hint_allowed_updates: Option<Hauf>,
|
pub hint_allowed_updates: Option<Hauf>,
|
||||||
|
|
||||||
/// The function used as [`UpdateListener::timeout_hint`].
|
|
||||||
///
|
|
||||||
/// Must implement `Fn(&St) -> Option<Duration>`.
|
|
||||||
pub timeout_hint: Option<Thf>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Haufn<State> = for<'a, 'b> fn(&'a mut State, &'b mut dyn Iterator<Item = AllowedUpdate>);
|
type Haufn<State> = for<'a, 'b> fn(&'a mut State, &'b mut dyn Iterator<Item = AllowedUpdate>);
|
||||||
type Thfn<State> = for<'a> fn(&'a State) -> Option<Duration>;
|
|
||||||
|
|
||||||
impl<St, Assf, Sf> StatefulListener<St, Assf, Sf, Haufn<St>, Thfn<St>> {
|
impl<St, Assf, Sf> StatefulListener<St, Assf, Sf, Haufn<St>> {
|
||||||
/// Creates a new stateful listener from its components.
|
/// Creates a new stateful listener from its components.
|
||||||
pub fn new(state: St, stream: Assf, stop_token: Sf) -> Self {
|
pub fn new(state: St, stream: Assf, stop_token: Sf) -> Self {
|
||||||
Self::new_with_hints(state, stream, stop_token, None, None)
|
Self::new_with_hints(state, stream, stop_token, None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<St, Assf, Sf, Hauf, Thf> StatefulListener<St, Assf, Sf, Hauf, Thf> {
|
impl<St, Assf, Sf, Hauf> StatefulListener<St, Assf, Sf, Hauf> {
|
||||||
/// Creates a new stateful listener from its components.
|
/// Creates a new stateful listener from its components.
|
||||||
pub fn new_with_hints(
|
pub fn new_with_hints(
|
||||||
state: St,
|
state: St,
|
||||||
stream: Assf,
|
stream: Assf,
|
||||||
stop_token: Sf,
|
stop_token: Sf,
|
||||||
hint_allowed_updates: Option<Hauf>,
|
hint_allowed_updates: Option<Hauf>,
|
||||||
timeout_hint: Option<Thf>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self { state, stream, stop_token, hint_allowed_updates, timeout_hint }
|
Self { state, stream, stop_token, hint_allowed_updates }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, St, Assf, Sf, Hauf, Thf, Strm, E> AsUpdateStream<'a>
|
impl<'a, St, Assf, Sf, Hauf, Strm, E> AsUpdateStream<'a> for StatefulListener<St, Assf, Hauf, Sf>
|
||||||
for StatefulListener<St, Assf, Hauf, Sf, Thf>
|
|
||||||
where
|
where
|
||||||
(St, Strm): 'a,
|
(St, Strm): 'a,
|
||||||
Strm: Send,
|
Strm: Send,
|
||||||
|
@ -82,12 +72,11 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<St, Assf, Sf, Hauf, Thf, E> UpdateListener for StatefulListener<St, Assf, Sf, Hauf, Thf>
|
impl<St, Assf, Sf, Hauf, E> UpdateListener for StatefulListener<St, Assf, Sf, Hauf>
|
||||||
where
|
where
|
||||||
Self: for<'a> AsUpdateStream<'a, StreamErr = E>,
|
Self: for<'a> AsUpdateStream<'a, StreamErr = E>,
|
||||||
Sf: FnMut(&mut St) -> StopToken,
|
Sf: FnMut(&mut St) -> StopToken,
|
||||||
Hauf: FnMut(&mut St, &mut dyn Iterator<Item = AllowedUpdate>),
|
Hauf: FnMut(&mut St, &mut dyn Iterator<Item = AllowedUpdate>),
|
||||||
Thf: Fn(&St) -> Option<Duration>,
|
|
||||||
{
|
{
|
||||||
type Err = E;
|
type Err = E;
|
||||||
|
|
||||||
|
@ -100,8 +89,4 @@ where
|
||||||
f(&mut self.state, hint);
|
f(&mut self.state, hint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn timeout_hint(&self) -> Option<Duration> {
|
|
||||||
self.timeout_hint.as_ref().and_then(|f| f(&self.state))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,6 +155,30 @@ pub use teloxide_macros::BotCommands;
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
/// 6. `#[command(command_separator = "sep")]`
|
||||||
|
/// Specify separator between command and args. Default is a space character.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```
|
||||||
|
/// # #[cfg(feature = "macros")] {
|
||||||
|
/// use teloxide::utils::command::BotCommands;
|
||||||
|
///
|
||||||
|
/// #[derive(BotCommands, PartialEq, Debug)]
|
||||||
|
/// #[command(
|
||||||
|
/// rename_rule = "lowercase",
|
||||||
|
/// parse_with = "split",
|
||||||
|
/// separator = "_",
|
||||||
|
/// command_separator = "_"
|
||||||
|
/// )]
|
||||||
|
/// enum Command {
|
||||||
|
/// Nums(u8, u16, i32),
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let command = Command::parse("/nums_1_32_5", "").unwrap();
|
||||||
|
/// assert_eq!(command, Command::Nums(1, 32, 5));
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
/// # Variant attributes
|
/// # Variant attributes
|
||||||
/// All variant attributes override the corresponding `enum` attributes.
|
/// All variant attributes override the corresponding `enum` attributes.
|
||||||
///
|
///
|
||||||
|
|
|
@ -283,9 +283,6 @@ mod tests {
|
||||||
is_premium: false,
|
is_premium: false,
|
||||||
added_to_attachment_menu: false,
|
added_to_attachment_menu: false,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(user_mention_or_link(&user_without_username), "[Name](tg://user/?id=123456789)")
|
||||||
user_mention_or_link(&user_without_username),
|
|
||||||
r#"[Name](tg://user/?id=123456789)"#
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,18 +5,16 @@ use std::{
|
||||||
atomic::{AtomicU8, Ordering},
|
atomic::{AtomicU8, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
},
|
},
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
|
|
||||||
use crate::update_listeners::UpdateListener;
|
|
||||||
|
|
||||||
/// A token which used to shutdown [`Dispatcher`].
|
/// A token which used to shutdown [`Dispatcher`].
|
||||||
///
|
///
|
||||||
/// [`Dispatcher`]: crate::dispatching::Dispatcher
|
/// [`Dispatcher`]: crate::dispatching::Dispatcher
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ShutdownToken {
|
pub struct ShutdownToken {
|
||||||
|
// FIXME: use a single arc
|
||||||
dispatcher_state: Arc<DispatcherState>,
|
dispatcher_state: Arc<DispatcherState>,
|
||||||
shutdown_notify_back: Arc<Notify>,
|
shutdown_notify_back: Arc<Notify>,
|
||||||
}
|
}
|
||||||
|
@ -49,11 +47,16 @@ impl ShutdownToken {
|
||||||
Self {
|
Self {
|
||||||
dispatcher_state: Arc::new(DispatcherState {
|
dispatcher_state: Arc::new(DispatcherState {
|
||||||
inner: AtomicU8::new(ShutdownState::Idle as _),
|
inner: AtomicU8::new(ShutdownState::Idle as _),
|
||||||
|
notify: <_>::default(),
|
||||||
}),
|
}),
|
||||||
shutdown_notify_back: <_>::default(),
|
shutdown_notify_back: <_>::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn wait_for_changes(&self) {
|
||||||
|
self.dispatcher_state.notify.notified().await;
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn start_dispatching(&self) {
|
pub(crate) fn start_dispatching(&self) {
|
||||||
if let Err(actual) =
|
if let Err(actual) =
|
||||||
self.dispatcher_state.compare_exchange(ShutdownState::Idle, ShutdownState::Running)
|
self.dispatcher_state.compare_exchange(ShutdownState::Idle, ShutdownState::Running)
|
||||||
|
@ -93,27 +96,20 @@ impl fmt::Display for IdleShutdownError {
|
||||||
|
|
||||||
impl std::error::Error for IdleShutdownError {}
|
impl std::error::Error for IdleShutdownError {}
|
||||||
|
|
||||||
pub(crate) fn shutdown_check_timeout_for(update_listener: &impl UpdateListener) -> Duration {
|
|
||||||
const MIN_SHUTDOWN_CHECK_TIMEOUT: Duration = Duration::from_secs(1);
|
|
||||||
const DZERO: Duration = Duration::ZERO;
|
|
||||||
|
|
||||||
let shutdown_check_timeout = update_listener.timeout_hint().unwrap_or(DZERO);
|
|
||||||
shutdown_check_timeout.saturating_add(MIN_SHUTDOWN_CHECK_TIMEOUT)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DispatcherState {
|
struct DispatcherState {
|
||||||
inner: AtomicU8,
|
inner: AtomicU8,
|
||||||
|
notify: Notify,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DispatcherState {
|
impl DispatcherState {
|
||||||
// Ordering::Relaxed: only one atomic variable, nothing to synchronize.
|
// Ordering::Relaxed: only one atomic variable, nothing to synchronize.
|
||||||
|
|
||||||
fn load(&self) -> ShutdownState {
|
fn load(&self) -> ShutdownState {
|
||||||
ShutdownState::from_u8(self.inner.load(Ordering::Relaxed))
|
ShutdownState::from_u8(self.inner.load(Ordering::Relaxed))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store(&self, new: ShutdownState) {
|
fn store(&self, new: ShutdownState) {
|
||||||
self.inner.store(new as _, Ordering::Relaxed)
|
self.inner.store(new as _, Ordering::Relaxed);
|
||||||
|
self.notify.notify_waiters();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compare_exchange(
|
fn compare_exchange(
|
||||||
|
@ -125,6 +121,11 @@ impl DispatcherState {
|
||||||
.compare_exchange(current as _, new as _, Ordering::Relaxed, Ordering::Relaxed)
|
.compare_exchange(current as _, new as _, Ordering::Relaxed, Ordering::Relaxed)
|
||||||
.map(ShutdownState::from_u8)
|
.map(ShutdownState::from_u8)
|
||||||
.map_err(ShutdownState::from_u8)
|
.map_err(ShutdownState::from_u8)
|
||||||
|
// FIXME: `Result::inspect` when :(
|
||||||
|
.map(|st| {
|
||||||
|
self.notify.notify_waiters();
|
||||||
|
st
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -166,6 +166,67 @@ fn parse_with_split4() {
|
||||||
assert_eq!(DefaultCommands::Start(), DefaultCommands::parse("/start", "").unwrap(),);
|
assert_eq!(DefaultCommands::Start(), DefaultCommands::parse("/start", "").unwrap(),);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "macros")]
|
||||||
|
fn parse_with_command_separator1() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename_rule = "lowercase")]
|
||||||
|
#[command(parse_with = "split", separator = "|", command_separator = "_")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
Start(u8, String),
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::Start(10, "hello".to_string()),
|
||||||
|
DefaultCommands::parse("/start_10|hello", "").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "macros")]
|
||||||
|
fn parse_with_command_separator2() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename_rule = "lowercase")]
|
||||||
|
#[command(parse_with = "split", separator = "_", command_separator = "_")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
Start(u8, String),
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::Start(10, "hello".to_string()),
|
||||||
|
DefaultCommands::parse("/start_10_hello", "").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "macros")]
|
||||||
|
fn parse_with_command_separator3() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename_rule = "lowercase")]
|
||||||
|
#[command(parse_with = "split", command_separator = ":")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(DefaultCommands::Help, DefaultCommands::parse("/help", "").unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "macros")]
|
||||||
|
fn parse_with_command_separator4() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename_rule = "lowercase")]
|
||||||
|
#[command(parse_with = "split", command_separator = ":")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
Start(u8),
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(DefaultCommands::Start(10), DefaultCommands::parse("/start:10", "").unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "macros")]
|
#[cfg(feature = "macros")]
|
||||||
fn parse_custom_parser() {
|
fn parse_custom_parser() {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "nightly-2023-05-28"
|
channel = "nightly-2023-09-27"
|
||||||
components = ["rustfmt", "clippy"]
|
components = ["rustfmt", "clippy"]
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
|
|
60
triagebot.toml
Normal file
60
triagebot.toml
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
[assign]
|
||||||
|
warn_non_default_branch = true
|
||||||
|
# contributing_url = "https://rustc-dev-guide.rust-lang.org/contributing.html" # FIXME: configure
|
||||||
|
|
||||||
|
[assign.adhoc_groups]
|
||||||
|
# This is a special group that will be used if none of the `owners` entries matches.
|
||||||
|
fallback = ["@WaffleLapkin", "@Hirrolot"]
|
||||||
|
|
||||||
|
[assign.owners]
|
||||||
|
"crates/teloxide-core" = ["@WaffleLapkin"]
|
||||||
|
"crates/teloxide-macros" = ["@WaffleLapkin"]
|
||||||
|
"crates/teloxide" = ["@Hirrolot"]
|
||||||
|
".github" = ["@WaffleLapkin"]
|
||||||
|
|
||||||
|
|
||||||
|
[autolabel."S-waiting-on-review"]
|
||||||
|
new_pr = true
|
||||||
|
|
||||||
|
#[autolabel."new-issue"]
|
||||||
|
#new_issue = true
|
||||||
|
|
||||||
|
[autolabel."C-core"]
|
||||||
|
trigger_files = ["crates/teloxide-core"]
|
||||||
|
|
||||||
|
[autolabel."C-main"]
|
||||||
|
trigger_files = ["crates/teloxide"]
|
||||||
|
|
||||||
|
[autolabel."C-macros"]
|
||||||
|
trigger_files = ["crates/teloxide-macros"]
|
||||||
|
|
||||||
|
|
||||||
|
[relabel]
|
||||||
|
allow-unauthenticated = [
|
||||||
|
"S-*", # Status
|
||||||
|
"C-*", # Crate
|
||||||
|
"breaking change",
|
||||||
|
"bug",
|
||||||
|
"documentation",
|
||||||
|
"duplicate",
|
||||||
|
"feature-request",
|
||||||
|
"FIXME",
|
||||||
|
"frozen",
|
||||||
|
"proposal",
|
||||||
|
"question",
|
||||||
|
"tba-update",
|
||||||
|
"Unknown API error",
|
||||||
|
"WIP",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# https://forge.rust-lang.org/triagebot/github-releases.html?
|
||||||
|
|
||||||
|
|
||||||
|
[review-submitted]
|
||||||
|
# This label is added when a review is submitted.
|
||||||
|
reviewed_label = "S-waiting-on-author"
|
||||||
|
# These labels are removed when a review is submitted.
|
||||||
|
review_labels = ["S-waiting-on-review"]
|
||||||
|
|
||||||
|
[shortcut]
|
Loading…
Reference in a new issue