Merge pull request #758 from teloxide/dev

Merge v0.11.1

Former-commit-id: 637dcb0d59090dbcf13b2e44a3cc7a27f03ac298
This commit is contained in:
Sima Kinsart 2022-10-31 22:04:01 +06:00 committed by GitHub
commit 2c52e2a1d4
22 changed files with 472 additions and 68 deletions

View file

@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## unreleased ## unreleased
## 0.11.1 - 2022-10-31
### Added
- The `rocksdb-storage` feature -- enables the RocksDB support ([PR #753](https://github.com/teloxide/teloxide/pull/753))
- `teloxide::dispatching::repls::CommandReplExt`, `teloxide::prelude::CommandReplExt` ([issue #740](https://github.com/teloxide/teloxide/issues/740))
### Deprecated
- `teloxide::dispatching::repls::{commands_repl, commands_repl_with_listener}`, `teloxide::utils::command::BotCommands::ty` (use `CommandReplExt` instead)
## 0.11.0 - 2022-10-07 ## 0.11.0 - 2022-10-07
### Changed ### Changed
@ -75,7 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fix Api Unknown error (Can't parse entities) on message created with `utils::markdown::user_mention_or_link` if user full name contaiins some escapable symbols eg '.' - Fix Api Unknown error (Can't parse entities) on message created with `utils::markdown::user_mention_or_link` if user full name contains some escapable symbols eg '.'
## 0.9.1 - 2022-05-27 ## 0.9.1 - 2022-05-27
@ -188,7 +199,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- `BotCommand::bot_commands` to obtain Telegram API commands ([issue 262](https://github.com/teloxide/teloxide/issues/262)). - `BotCommand::bot_commands` to obtain Telegram API commands ([issue 262](https://github.com/teloxide/teloxide/issues/262)).
- The `dispatching2` and `prelude2` modules. They presents a new dispatching model based on `dptree`. - The `dispatching2` and `prelude2` modules. They present a new dispatching model based on `dptree`.
### Changed ### Changed
@ -265,7 +276,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Remove the `reqwest` dependency. It's not needed after the [teloxide-core] integration. - Remove the `reqwest` dependency. It's not needed after the [teloxide-core] integration.
- A storage persistency bug ([issue 304](https://github.com/teloxide/teloxide/issues/304)). - A storage persistence bug ([issue 304](https://github.com/teloxide/teloxide/issues/304)).
- Log errors from `Storage::{remove_dialogue, update_dialogue}` in `DialogueDispatcher` ([issue 302](https://github.com/teloxide/teloxide/issues/302)). - Log errors from `Storage::{remove_dialogue, update_dialogue}` in `DialogueDispatcher` ([issue 302](https://github.com/teloxide/teloxide/issues/302)).
- Mark all the functions of `Storage` as `#[must_use]`. - Mark all the functions of `Storage` as `#[must_use]`.
@ -371,7 +382,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Now methods which can send file to Telegram returns `tokio::io::Result<T>`. Early its could panic ([issue 216](https://github.com/teloxide/teloxide/issues/216)). - Now methods which can send file to Telegram return `tokio::io::Result<T>`. Before that it could panic ([issue 216](https://github.com/teloxide/teloxide/issues/216)).
- If a bot wasn't triggered for several days, it stops responding ([issue 223](https://github.com/teloxide/teloxide/issues/223)). - If a bot wasn't triggered for several days, it stops responding ([issue 223](https://github.com/teloxide/teloxide/issues/223)).
## 0.2.0 - 2020-02-25 ## 0.2.0 - 2020-02-25

View file

@ -1,6 +1,6 @@
[package] [package]
name = "teloxide" name = "teloxide"
version = "0.11.0" version = "0.11.1"
edition = "2021" edition = "2021"
description = "An elegant Telegram bots framework for Rust" description = "An elegant Telegram bots framework for Rust"
repository = "https://github.com/teloxide/teloxide" repository = "https://github.com/teloxide/teloxide"
@ -19,6 +19,7 @@ webhooks-axum = ["webhooks", "axum", "tower", "tower-http"]
sqlite-storage = ["sqlx"] sqlite-storage = ["sqlx"]
redis-storage = ["redis"] redis-storage = ["redis"]
rocksdb-storage = ["rocksdb"]
cbor-serializer = ["serde_cbor"] cbor-serializer = ["serde_cbor"]
bincode-serializer = ["bincode"] bincode-serializer = ["bincode"]
@ -42,6 +43,7 @@ full = [
"webhooks-axum", "webhooks-axum",
"sqlite-storage", "sqlite-storage",
"redis-storage", "redis-storage",
"rocksdb-storage",
"cbor-serializer", "cbor-serializer",
"bincode-serializer", "bincode-serializer",
"macros", "macros",
@ -92,6 +94,9 @@ sqlx = { version = "0.6", optional = true, default-features = false, features =
"sqlite", "sqlite",
] } ] }
redis = { version = "0.21", features = ["tokio-comp"], optional = true } redis = { version = "0.21", features = ["tokio-comp"], optional = true }
rocksdb = { version = "0.19", optional = true, default-features = false, features = [
"lz4",
] }
serde_cbor = { version = "0.11", optional = true } serde_cbor = { version = "0.11", optional = true }
bincode = { version = "1.3", optional = true } bincode = { version = "1.3", optional = true }
axum = { version = "0.5.13", optional = true } axum = { version = "0.5.13", optional = true }

View file

@ -1,6 +1,22 @@
This document describes breaking changes of `teloxide` crate, as well as the ways to update code. This document describes breaking changes of `teloxide` crate, as well as the ways to update code.
Note that the list of required changes is not fully exhaustive and it may lack something in rare cases. Note that the list of required changes is not fully exhaustive and it may lack something in rare cases.
## 0.11 -> 0.11.1
### teloxide
We have introduced the new trait `CommandRepl` that replaces the old `commands_repl_(with_listener)` functions:
```diff
- teloxide::commands_repl(bot, answer, Command::ty())
+ Command::repl(bot, answer)
```
```diff
- teloxide::commands_repl_with_listener(bot, answer, listener, Command::ty())
+ Command::repl_with_listener(bot, answer, listener)
```
## 0.10 -> 0.11 ## 0.10 -> 0.11
### core ### core
@ -8,12 +24,12 @@ Note that the list of required changes is not fully exhaustive and it may lack s
Requests can now be `.await`ed directly, without need of `.send()` or `AutoSend`. Requests can now be `.await`ed directly, without need of `.send()` or `AutoSend`.
If you previously used `AutoSend` adaptor, you can safely remove it: If you previously used `AutoSend` adaptor, you can safely remove it:
```diff,rust ```diff
-let bot = Bot::from_env().auto_send(); -let bot = Bot::from_env().auto_send();
+let bot = Bot::from_env(); +let bot = Bot::from_env();
``` ```
```diff,rust ```diff
-async fn start(bot: AutoSend<Bot>, dialogue: MyDialogue, msg: Message) -> HandlerResult { -async fn start(bot: AutoSend<Bot>, dialogue: MyDialogue, msg: Message) -> HandlerResult {
+async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult { +async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
``` ```
@ -41,7 +57,7 @@ You may need to change code accordingly:
-let id: i32 = message.id; -let id: i32 = message.id;
+let id: MessageId = message.id; +let id: MessageId = message.id;
``` ```
```diff,rust ```diff
let (cid, mid): (ChatId, i32) = get_message_to_delete_from_db(); let (cid, mid): (ChatId, i32) = get_message_to_delete_from_db();
-bot.delete_message(cid, mid).await?; -bot.delete_message(cid, mid).await?;
+bot.delete_message(cid, MessageId(mid)).await?; +bot.delete_message(cid, MessageId(mid)).await?;
@ -50,7 +66,7 @@ let (cid, mid): (ChatId, i32) = get_message_to_delete_from_db();
Note that at the same time `MessageId` is now a tuple struct. Note that at the same time `MessageId` is now a tuple struct.
If you've accessed its only field you'll need to change it too: If you've accessed its only field you'll need to change it too:
```diff,rust ```diff
-let MessageId { message_id } = bot.copy_message(dst_chat, src_chat, mid).await?; -let MessageId { message_id } = bot.copy_message(dst_chat, src_chat, mid).await?;
+let MessageId(message_id) = bot.copy_message(dst_chat, src_chat, mid).await?; +let MessageId(message_id) = bot.copy_message(dst_chat, src_chat, mid).await?;
save_to_db(message_id); save_to_db(message_id);
@ -64,7 +80,7 @@ See `Sticker` documentation for more information about the new structure.
You can now write `Ok(())` instead of `respond(())` at the end of closures provided to RELPs: You can now write `Ok(())` instead of `respond(())` at the end of closures provided to RELPs:
```diff,rust ```diff
teloxide::repl(bot, |bot: Bot, msg: Message| async move { teloxide::repl(bot, |bot: Bot, msg: Message| async move {
bot.send_dice(msg.chat.id).await?; bot.send_dice(msg.chat.id).await?;
- respond(()) - respond(())
@ -75,11 +91,21 @@ teloxide::repl(bot, |bot: Bot, msg: Message| async move {
This is because REPLs now require the closure to return `RequestError` instead of a generic error type, so type inference works perfectly for a return value. If you use something other than `RequestError`, you can transfer your code to `teloxide::dispatching`, which still permits a generic error type. This is because REPLs now require the closure to return `RequestError` instead of a generic error type, so type inference works perfectly for a return value. If you use something other than `RequestError`, you can transfer your code to `teloxide::dispatching`, which still permits a generic error type.
"Stop tokens" were refactored, the trait is now removed and the types were renamed:
```diff
-use teloxide::dispatching::stop_token::{AsyncStopToken, AsyncStopFlag};
+use teloxide::stop::{StopToken, StopFlag, mk_stop_token};
-let (token, flag): (AsyncStopToken, AsyncStopFlag) = AsyncStopToken::new_pair();
+let (token, flag): (StopToken, StopFlag) = mk_stop_token();
```
### macros ### macros
`parse_with` now accepts a Rust _path_ to a custom parser function instead of a string: `parse_with` now accepts a Rust _path_ to a custom parser function instead of a string:
```diff,rust ```diff
fn custom_parser(input: String) -> Result<(u8,), ParseError> { fn custom_parser(input: String) -> Result<(u8,), ParseError> {
todo!() todo!()
} }
@ -94,7 +120,7 @@ enum Command {
`rename` now only renames a command literally; use `rename_rule` to change the case of a command: `rename` now only renames a command literally; use `rename_rule` to change the case of a command:
```diff,rust ```diff
#[derive(BotCommands)] #[derive(BotCommands)]
- #[command(rename = "lowercase", description = "These commands are supported:")] - #[command(rename = "lowercase", description = "These commands are supported:")]
+ #[command(rename_rule = "lowercase", description = "These commands are supported:")] + #[command(rename_rule = "lowercase", description = "These commands are supported:")]
@ -193,7 +219,7 @@ In order to make `Dispatcher` implement `Send`, `DispatcherBuilder::{default_han
v0.6 of teloxide introduces a new dispatching model based on the [chain of responsibility pattern]. To use it, you need to replace `prelude` with `prelude2` and `dispatching` with `dispatching2`. Instead of using old REPLs, you should now use `teloxide::repls2`. v0.6 of teloxide introduces a new dispatching model based on the [chain of responsibility pattern]. To use it, you need to replace `prelude` with `prelude2` and `dispatching` with `dispatching2`. Instead of using old REPLs, you should now use `teloxide::repls2`.
The whole design is different than the previous one based on Tokio streams. In this section, we are only to address the most common usage scenarios. The whole design is different from the previous one based on Tokio streams. In this section, we are only to address the most common usage scenarios.
First of all, now there are no streams. Instead of using streams, you use [`dptree`], which is a more suitable alternative for our purposes. Thus, if you previously used `Dispatcher::messages_handler`, now you should use `Update::filter_message()`, and so on. First of all, now there are no streams. Instead of using streams, you use [`dptree`], which is a more suitable alternative for our purposes. Thus, if you previously used `Dispatcher::messages_handler`, now you should use `Update::filter_message()`, and so on.
@ -237,7 +263,7 @@ List of changed types:
In teloxide `v0.4` (core `v0.2`) some API methods had wrong return types. In teloxide `v0.4` (core `v0.2`) some API methods had wrong return types.
This made them practically unusable as they've always returned parsing error. This made them practically unusable as they've always returned parsing error.
On the offchance you were using the methods, you may need to adjust types in your code. On the off-chance you were using the methods, you may need to adjust types in your code.
List of changed return types: List of changed return types:
- `get_chat_administrators`: `ChatMember` -> `Vec<ChatMember>` - `get_chat_administrators`: `ChatMember` -> `Vec<ChatMember>`
@ -324,7 +350,7 @@ List of renamed items:
#### Added `impl Clone` for {`CacheMe`, `DefaultParseMode`, `Throttle`} #### Added `impl Clone` for {`CacheMe`, `DefaultParseMode`, `Throttle`}
Previously said bot adaptors were lacking `Clone` implementation. Previously said bot adaptors were lacking `Clone` implementation.
To workaround this issue it was proposed to wrap bot in `Arc`. To work around this issue it was proposed to wrap bot in `Arc`.
Now it's not required, so you can remove the `Arc`: Now it's not required, so you can remove the `Arc`:
```diff ```diff

View file

@ -1,4 +1,4 @@
> [v0.10 -> v0.11 migration guide >>](MIGRATION_GUIDE.md#010---011) > [v0.11 -> v0.11.1 migration guide >>](MIGRATION_GUIDE.md#011---0111)
<div align="center"> <div align="center">
<img src="./ICON.png" width="250"/> <img src="./ICON.png" width="250"/>
@ -29,12 +29,13 @@
[`dptree`]: https://github.com/teloxide/dptree [`dptree`]: https://github.com/teloxide/dptree
[chain of responsibility]: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern [chain of responsibility]: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern
- **Feature-rich.** You can use both long polling and webhooks, configure an underlying HTTPS client, set a custom URL of a Telegram API server, and much more. - **Feature-rich.** You can use both long polling and webhooks, configure an underlying HTTPS client, set a custom URL of a Telegram API server, do graceful shutdown, and much more.
- **Simple dialogues.** Our dialogues subsystem is simple and easy-to-use, and, furthermore, is agnostic of how/where dialogues are stored. For example, you can just replace a one line to achieve [persistence]. Out-of-the-box storages include [Redis] and [Sqlite]. - **Simple dialogues.** Our dialogues subsystem is simple and easy-to-use, and, furthermore, is agnostic of how/where dialogues are stored. For example, you can just replace a one line to achieve [persistence]. Out-of-the-box storages include [Redis], [RocksDB] and [Sqlite].
[persistence]: https://en.wikipedia.org/wiki/Persistence_(computer_science) [persistence]: https://en.wikipedia.org/wiki/Persistence_(computer_science)
[Redis]: https://redis.io/ [Redis]: https://redis.io/
[RocksDB]: https://rocksdb.org/
[Sqlite]: https://www.sqlite.org [Sqlite]: https://www.sqlite.org
- **Strongly typed commands.** Define bot commands as an `enum` and teloxide will parse them automatically — just like JSON structures in [`serde-json`] and command-line arguments in [`structopt`]. - **Strongly typed commands.** Define bot commands as an `enum` and teloxide will parse them automatically — just like JSON structures in [`serde-json`] and command-line arguments in [`structopt`].
@ -72,7 +73,7 @@ $ rustup override set nightly
5. Run `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`: 5. Run `cargo new my_bot`, enter the directory and put these lines into your `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
teloxide = { version = "0.11", features = ["macros", "auto-send"] } teloxide = { version = "0.11", features = ["macros"] }
log = "0.4" log = "0.4"
pretty_env_logger = "0.4" pretty_env_logger = "0.4"
tokio = { version = "1.8", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.8", features = ["rt-multi-thread", "macros"] }
@ -82,7 +83,7 @@ tokio = { version = "1.8", features = ["rt-multi-thread", "macros"] }
### The dices bot ### The dices bot
This bot replies with a dice throw to each received message: This bot replies with a die throw to each received message:
[[`examples/throw_dice.rs`](examples/throw_dice.rs)] [[`examples/throw_dice.rs`](examples/throw_dice.rs)]
@ -131,7 +132,7 @@ async fn main() {
let bot = Bot::from_env(); let bot = Bot::from_env();
teloxide::commands_repl(bot, answer, Command::ty()).await; Command::repl(bot, answer).await;
} }
#[derive(BotCommands, Clone)] #[derive(BotCommands, Clone)]
@ -326,7 +327,7 @@ Feel free to propose your own bot to our collection!
- [`modos189/tg_blackbox_bot`](https://gitlab.com/modos189/tg_blackbox_bot) — Anonymous feedback for your Telegram project. - [`modos189/tg_blackbox_bot`](https://gitlab.com/modos189/tg_blackbox_bot) — Anonymous feedback for your Telegram project.
- [`0xNima/spacecraft`](https://github.com/0xNima/spacecraft) — Yet another telegram bot to downloading Twitter spaces. - [`0xNima/spacecraft`](https://github.com/0xNima/spacecraft) — Yet another telegram bot to downloading Twitter spaces.
- [`0xNima/Twideo`](https://github.com/0xNima/Twideo) — Simple Telegram Bot for downloading videos from Twitter via their links. - [`0xNima/Twideo`](https://github.com/0xNima/Twideo) — Simple Telegram Bot for downloading videos from Twitter via their links.
- [`mattrighetti/libgen-bot-rs`](https://github.com/mattrighetti/libgen-bot-rs) — Telgram bot to interface with libgen. - [`mattrighetti/libgen-bot-rs`](https://github.com/mattrighetti/libgen-bot-rs) — Telegram bot to interface with libgen.
- [`zamazan4ik/npaperbot-telegram`](https://github.com/zamazan4ik/npaperbot-telegram) — Telegram bot for searching via C++ proposals. - [`zamazan4ik/npaperbot-telegram`](https://github.com/zamazan4ik/npaperbot-telegram) — Telegram bot for searching via C++ proposals.
<details> <details>

View file

@ -5,8 +5,8 @@ use teloxide::{prelude::*, types::ChatPermissions, utils::command::BotCommands};
// Derive BotCommands to parse text with a command into this enumeration. // Derive BotCommands to parse text with a command into this enumeration.
// //
// 1. rename = "lowercase" turns all the commands into lowercase letters. // 1. `rename_rule = "lowercase"` turns all the commands into lowercase letters.
// 2. `description = "..."` specifies a text before all the commands. // 2. `description = "..."` specifies a text before all the commands.
// //
// That is, you can just call Command::descriptions() to get a description of // That is, you can just call Command::descriptions() to get a description of
// your commands in this format: // your commands in this format:
@ -60,7 +60,7 @@ async fn main() {
let bot = teloxide::Bot::from_env(); let bot = teloxide::Bot::from_env();
teloxide::commands_repl(bot, action, Command::ty()).await; Command::repl(bot, action).await;
} }
async fn action(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> { async fn action(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> {

View file

@ -7,7 +7,7 @@ async fn main() {
let bot = Bot::from_env(); let bot = Bot::from_env();
teloxide::commands_repl(bot, answer, Command::ty()).await; Command::repl(bot, answer).await;
} }
#[derive(BotCommands, Clone)] #[derive(BotCommands, Clone)]

View file

@ -91,7 +91,7 @@ async fn got_number(
} }
Command::Reset => { Command::Reset => {
dialogue.reset().await?; dialogue.reset().await?;
bot.send_message(msg.chat.id, "Number resetted.").await?; bot.send_message(msg.chat.id, "Number reset.").await?;
} }
} }
Ok(()) Ok(())

View file

@ -32,7 +32,7 @@ async fn main() {
.endpoint(simple_commands_handler), .endpoint(simple_commands_handler),
) )
.branch( .branch(
// Filter a maintainer by a used ID. // Filter a maintainer by a user ID.
dptree::filter(|cfg: ConfigParameters, msg: Message| { dptree::filter(|cfg: ConfigParameters, msg: Message| {
msg.from().map(|user| user.id == cfg.bot_maintainer).unwrap_or_default() msg.from().map(|user| user.id == cfg.bot_maintainer).unwrap_or_default()
}) })

View file

@ -10,7 +10,7 @@
// heroku create --buildpack emk/rust // heroku create --buildpack emk/rust
// ``` // ```
// //
// To set buildpack for existing applicaton: // To set buildpack for existing application:
// //
// ``` // ```
// heroku buildpacks:set emk/rust // heroku buildpacks:set emk/rust

View file

@ -9,6 +9,9 @@ mod redis_storage;
#[cfg(feature = "sqlite-storage")] #[cfg(feature = "sqlite-storage")]
mod sqlite_storage; mod sqlite_storage;
#[cfg(feature = "rocksdb-storage")]
mod rocksdb_storage;
use futures::future::BoxFuture; use futures::future::BoxFuture;
use teloxide_core::types::ChatId; use teloxide_core::types::ChatId;
@ -25,6 +28,9 @@ use std::sync::Arc;
#[cfg(feature = "sqlite-storage")] #[cfg(feature = "sqlite-storage")]
pub use sqlite_storage::{SqliteStorage, SqliteStorageError}; pub use sqlite_storage::{SqliteStorage, SqliteStorageError};
#[cfg(feature = "rocksdb-storage")]
pub use rocksdb_storage::{RocksDbStorage, RocksDbStorageError};
/// A storage with an erased error type. /// A storage with an erased error type.
pub type ErasedStorage<D> = pub type ErasedStorage<D> =
dyn Storage<D, Error = Box<dyn std::error::Error + Send + Sync>> + Send + Sync; dyn Storage<D, Error = Box<dyn std::error::Error + Send + Sync>> + Send + Sync;
@ -41,10 +47,12 @@ pub type ErasedStorage<D> =
/// ///
/// - [`InMemStorage`] -- a storage based on [`std::collections::HashMap`]. /// - [`InMemStorage`] -- a storage based on [`std::collections::HashMap`].
/// - [`RedisStorage`] -- a Redis-based storage. /// - [`RedisStorage`] -- a Redis-based storage.
/// - [`RocksDbStorage`] -- a RocksDB-based persistent storage.
/// - [`SqliteStorage`] -- an SQLite-based persistent storage. /// - [`SqliteStorage`] -- an SQLite-based persistent storage.
/// ///
/// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage /// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage
/// [`RedisStorage`]: crate::dispatching::dialogue::RedisStorage /// [`RedisStorage`]: crate::dispatching::dialogue::RedisStorage
/// [`RocksDbStorage`]: crate::dispatching::dialogue::RocksDbStorage
/// [`SqliteStorage`]: crate::dispatching::dialogue::SqliteStorage /// [`SqliteStorage`]: crate::dispatching::dialogue::SqliteStorage
pub trait Storage<D> { pub trait Storage<D> {
type Error; type Error;

View file

@ -0,0 +1,113 @@
use super::{serializer::Serializer, Storage};
use futures::future::BoxFuture;
use rocksdb::{DBCompressionType, DBWithThreadMode, MultiThreaded};
use serde::{de::DeserializeOwned, Serialize};
use std::{
convert::Infallible,
fmt::{Debug, Display},
str,
sync::Arc,
};
use teloxide_core::types::ChatId;
use thiserror::Error;
/// A persistent dialogue storage based on [RocksDb](http://rocksdb.org/).
pub struct RocksDbStorage<S> {
db: DBWithThreadMode<MultiThreaded>,
serializer: S,
}
/// An error returned from [`RocksDbStorage`].
#[derive(Debug, Error)]
pub enum RocksDbStorageError<SE>
where
SE: Debug + Display,
{
#[error("dialogue serialization error: {0}")]
SerdeError(SE),
#[error("RocksDb error: {0}")]
RocksDbError(#[from] rocksdb::Error),
/// Returned from [`RocksDbStorage::remove_dialogue`].
#[error("row not found")]
DialogueNotFound,
}
impl<S> RocksDbStorage<S> {
pub async fn open(
path: &str,
serializer: S,
options: Option<rocksdb::Options>,
) -> Result<Arc<Self>, RocksDbStorageError<Infallible>> {
let options = match options {
Some(opts) => opts,
None => {
let mut opts = rocksdb::Options::default();
opts.set_compression_type(DBCompressionType::Lz4);
opts.create_if_missing(true);
opts
}
};
let db = DBWithThreadMode::<MultiThreaded>::open(&options, path)?;
Ok(Arc::new(Self { db, serializer }))
}
}
impl<S, D> Storage<D> for RocksDbStorage<S>
where
S: Send + Sync + Serializer<D> + 'static,
D: Send + Serialize + DeserializeOwned + 'static,
<S as Serializer<D>>::Error: Debug + Display,
{
type Error = RocksDbStorageError<<S as Serializer<D>>::Error>;
/// Returns [`RocksDbStorageError::DialogueNotFound`] if a dialogue does not
/// exist.
fn remove_dialogue(
self: Arc<Self>,
ChatId(chat_id): ChatId,
) -> BoxFuture<'static, Result<(), Self::Error>> {
Box::pin(async move {
let key = chat_id.to_le_bytes();
if self.db.get(&key)?.is_none() {
return Err(RocksDbStorageError::DialogueNotFound);
}
self.db.delete(&key).unwrap();
Ok(())
})
}
fn update_dialogue(
self: Arc<Self>,
ChatId(chat_id): ChatId,
dialogue: D,
) -> BoxFuture<'static, Result<(), Self::Error>> {
Box::pin(async move {
let d =
self.serializer.serialize(&dialogue).map_err(RocksDbStorageError::SerdeError)?;
let key = chat_id.to_le_bytes();
self.db.put(&key, &d)?;
Ok(())
})
}
fn get_dialogue(
self: Arc<Self>,
ChatId(chat_id): ChatId,
) -> BoxFuture<'static, Result<Option<D>, Self::Error>> {
Box::pin(async move {
let key = chat_id.to_le_bytes();
self.db
.get(&key)?
.map(|d| self.serializer.deserialize(&d).map_err(RocksDbStorageError::SerdeError))
.transpose()
})
}
}

View file

@ -11,5 +11,7 @@
mod commands_repl; mod commands_repl;
mod repl; mod repl;
pub use commands_repl::CommandReplExt;
#[allow(deprecated)]
pub use commands_repl::{commands_repl, commands_repl_with_listener}; pub use commands_repl::{commands_repl, commands_repl_with_listener};
pub use repl::{repl, repl_with_listener}; pub use repl::{repl, repl_with_listener};

View file

@ -8,8 +8,147 @@ use crate::{
utils::command::BotCommands, utils::command::BotCommands,
}; };
use dptree::di::{DependencyMap, Injectable}; use dptree::di::{DependencyMap, Injectable};
use futures::future::BoxFuture;
use std::{fmt::Debug, marker::PhantomData}; use std::{fmt::Debug, marker::PhantomData};
/// A [REPL] for commands.
///
/// REPLs are meant only for simple bots and rapid prototyping. If you need to
/// supply dependencies or describe more complex dispatch logic, please use
/// [`Dispatcher`]. See also: ["Dispatching or
/// REPLs?"](../index.html#dispatching-or-repls).
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
///
/// All errors from the handler and update listener will be logged.
///
/// [REPL]: https://en.wikipedia.org/wiki/Read-eval-print_loop
///
/// This trait extends your [`BotCommands`] type with REPL facilities.
///
/// ## Signatures
///
/// Don't be scared by many trait bounds in the signatures, in essence they
/// require:
///
/// 1. `bot` is a bot, client for the Telegram bot API. It is represented via
/// the [`Requester`] trait.
/// 2. `handler` is an `async` function that takes arguments from
/// [`DependencyMap`] (see below) and returns [`ResponseResult`].
/// 3. `listener` is something that takes updates from a Telegram server and
/// implements [`UpdateListener`].
///
/// All the other requirements are about thread safety and data validity and can
/// be ignored for most of the time.
///
/// ## Handler arguments
///
/// `teloxide` provides the following types to the `handler`:
/// - [`Message`]
/// - `R` (type of the `bot`)
/// - `Cmd` (type of the parsed command)
/// - [`Me`]
///
/// Each of these types can be accepted as a handler parameter. Note that they
/// aren't all required at the same time: e.g., you can take only the bot and
/// the command without [`Me`] and [`Message`].
///
/// [`Me`]: crate::types::Me
/// [`Message`]: crate::types::Message
///
/// ## Stopping
//
#[doc = include_str!("stopping.md")]
///
/// ## Caution
//
#[doc = include_str!("caution.md")]
///
#[cfg(feature = "ctrlc_handler")]
pub trait CommandReplExt {
/// A REPL for commands.
///
/// See [`CommandReplExt`] for more details.
#[must_use]
fn repl<'a, R, H, Args>(bot: R, handler: H) -> BoxFuture<'a, ()>
where
R: Requester + Clone + Send + Sync + 'static,
<R as Requester>::GetUpdates: Send,
<R as Requester>::GetWebhookInfo: Send,
<R as Requester>::GetMe: Send,
<R as Requester>::DeleteWebhook: Send,
H: Injectable<DependencyMap, ResponseResult<()>, Args> + Send + Sync + 'static;
/// A REPL for commands with a custom [`UpdateListener`].
///
/// See [`CommandReplExt`] for more details.
#[must_use]
fn repl_with_listener<'a, R, H, L, Args>(bot: R, handler: H, listener: L) -> BoxFuture<'a, ()>
where
H: Injectable<DependencyMap, ResponseResult<()>, Args> + Send + Sync + 'static,
L: UpdateListener + Send + 'a,
L::Err: Debug + Send + 'a,
R: Requester + Clone + Send + Sync + 'static,
<R as Requester>::GetMe: Send;
}
#[cfg(feature = "ctrlc_handler")]
impl<Cmd> CommandReplExt for Cmd
where
Cmd: BotCommands + Send + Sync + 'static,
{
fn repl<'a, R, H, Args>(bot: R, handler: H) -> BoxFuture<'a, ()>
where
R: Requester + Clone + Send + Sync + 'static,
<R as Requester>::GetUpdates: Send,
<R as Requester>::GetWebhookInfo: Send,
<R as Requester>::GetMe: Send,
<R as Requester>::DeleteWebhook: Send,
H: Injectable<DependencyMap, ResponseResult<()>, Args> + Send + Sync + 'static,
{
let cloned_bot = bot.clone();
Box::pin(async move {
Self::repl_with_listener(
bot,
handler,
update_listeners::polling_default(cloned_bot).await,
)
.await
})
}
fn repl_with_listener<'a, R, H, L, Args>(bot: R, handler: H, listener: L) -> BoxFuture<'a, ()>
where
H: Injectable<DependencyMap, ResponseResult<()>, Args> + Send + Sync + 'static,
L: UpdateListener + Send + 'a,
L::Err: Debug + Send + 'a,
R: Requester + Clone + Send + Sync + 'static,
<R as Requester>::GetMe: Send,
{
use crate::dispatching::Dispatcher;
// Other update types are of no interest to use since this REPL is only for
// commands. See <https://github.com/teloxide/teloxide/issues/557>.
let ignore_update = |_upd| Box::pin(async {});
Box::pin(async move {
Dispatcher::builder(
bot,
Update::filter_message().filter_command::<Cmd>().endpoint(handler),
)
.default_handler(ignore_update)
.enable_ctrlc_handler()
.build()
.dispatch_with_listener(
listener,
LoggingErrorHandler::with_custom_text("An error from the update listener"),
)
.await
})
}
}
/// A [REPL] for commands. /// A [REPL] for commands.
// //
/// ///
@ -59,6 +198,7 @@ use std::{fmt::Debug, marker::PhantomData};
#[doc = include_str!("caution.md")] #[doc = include_str!("caution.md")]
/// ///
#[cfg(feature = "ctrlc_handler")] #[cfg(feature = "ctrlc_handler")]
#[deprecated(note = "Use `CommandsRepl::repl` instead")]
pub async fn commands_repl<'a, R, Cmd, H, Args>(bot: R, handler: H, cmd: PhantomData<Cmd>) pub async fn commands_repl<'a, R, Cmd, H, Args>(bot: R, handler: H, cmd: PhantomData<Cmd>)
where where
R: Requester + Clone + Send + Sync + 'static, R: Requester + Clone + Send + Sync + 'static,
@ -68,6 +208,7 @@ where
{ {
let cloned_bot = bot.clone(); let cloned_bot = bot.clone();
#[allow(deprecated)]
commands_repl_with_listener( commands_repl_with_listener(
bot, bot,
handler, handler,
@ -127,6 +268,7 @@ where
#[doc = include_str!("caution.md")] #[doc = include_str!("caution.md")]
/// ///
#[cfg(feature = "ctrlc_handler")] #[cfg(feature = "ctrlc_handler")]
#[deprecated(note = "Use `CommandsRepl::repl_with_listener` instead")]
pub async fn commands_repl_with_listener<'a, R, Cmd, H, L, Args>( pub async fn commands_repl_with_listener<'a, R, Cmd, H, L, Args>(
bot: R, bot: R,
handler: H, handler: H,

View file

@ -225,7 +225,7 @@ where
/// ///
/// C->>P: next /// C->>P: next
/// ///
/// P->>T: *Acknolegment of update(5)* /// P->>T: *Acknowledgement of update(5)*
/// T->>P: ok /// T->>P: ok
/// ///
/// P->>C: None /// P->>C: None

View file

@ -1,27 +1,28 @@
## Cargo features ## Cargo features
| Feature | Description | | Feature | Description |
|----------------------|--------------------------------------------------------------------------------------------| |----------------------|-------------|
| `webhooks` | Enables general webhook utilities (almost useless on its own) | | `webhooks` | Enables general webhook utilities (almost useless on its own). |
| `webhooks-axum` | Enables webhook implementation based on axum framework | | `webhooks-axum` | Enables webhook implementation based on axum framework. |
| `macros` | Re-exports macros from [`teloxide-macros`]. | | `macros` | Re-exports macros from [`teloxide-macros`]. |
| `ctrlc_handler` | Enables the [`DispatcherBuilder::enable_ctrlc_handler`] function (**enabled by default**). | | `ctrlc_handler` | Enables the [`DispatcherBuilder::enable_ctrlc_handler`] function (**enabled by default**). |
| `auto-send` | Enables the [`AutoSend`](adaptors::AutoSend) bot adaptor (**enabled by default; DEPRECATED**). | | `auto-send` | Enables the [`AutoSend`](adaptors::AutoSend) bot adaptor (**enabled by default; DEPRECATED**). |
| `throttle` | Enables the [`Throttle`](adaptors::Throttle) bot adaptor. | | `throttle` | Enables the [`Throttle`](adaptors::Throttle) bot adaptor. |
| `cache-me` | Enables the [`CacheMe`](adaptors::CacheMe) bot adaptor. | | `cache-me` | Enables the [`CacheMe`](adaptors::CacheMe) bot adaptor. |
| `trace-adaptor` | Enables the [`Trace`](adaptors::Trace) bot adaptor. | | `trace-adaptor` | Enables the [`Trace`](adaptors::Trace) bot adaptor. |
| `erased` | Enables the [`ErasedRequester`](adaptors::ErasedRequester) bot adaptor. | | `erased` | Enables the [`ErasedRequester`](adaptors::ErasedRequester) bot adaptor. |
| `full` | Enables all the features except `nightly`. | | `full` | Enables all the features except `nightly`. |
| `nightly` | Enables nightly-only features (see the [`teloxide-core` features]). | | `nightly` | Enables nightly-only features (see the [`teloxide-core` features]). |
| `native-tls` | Enables the [`native-tls`] TLS implementation (**enabled by default**). | | `native-tls` | Enables the [`native-tls`] TLS implementation (**enabled by default**). |
| `rustls` | Enables the [`rustls`] TLS implementation. | | `rustls` | Enables the [`rustls`] TLS implementation. |
| `redis-storage` | Enables the [Redis] storage support for dialogues. | | `redis-storage` | Enables the [Redis] storage support for dialogues. |
| `sqlite-storage` | Enables the [Sqlite] storage support for dialogues. | | `rocksdb-storage` | Enables the [RocksDB] storage support for dialogues. |
| `cbor-serializer` | Enables the [CBOR] serializer for dialogues. | | `sqlite-storage` | Enables the [Sqlite] storage support for dialogues. |
| `bincode-serializer` | Enables the [Bincode] serializer for dialogues. | | `cbor-serializer` | Enables the [CBOR] serializer for dialogues. |
| `bincode-serializer` | Enables the [Bincode] serializer for dialogues. |
[Redis]: https://redis.io/ [Redis]: https://redis.io/
[RocksDB]: https://rocksdb.org/
[Sqlite]: https://www.sqlite.org/ [Sqlite]: https://www.sqlite.org/
[CBOR]: https://en.wikipedia.org/wiki/CBOR [CBOR]: https://en.wikipedia.org/wiki/CBOR
[Bincode]: https://github.com/servo/bincode [Bincode]: https://github.com/servo/bincode
@ -31,4 +32,4 @@
[`teloxide::utils::UpState`]: utils::UpState [`teloxide::utils::UpState`]: utils::UpState
[`teloxide-core` features]: https://docs.rs/teloxide-core/latest/teloxide_core/#cargo-features [`teloxide-core` features]: https://docs.rs/teloxide-core/latest/teloxide_core/#cargo-features
[`DispatcherBuilder::enable_ctrlc_handler`]: dispatching::DispatcherBuilder::enable_ctrlc_handler [`DispatcherBuilder::enable_ctrlc_handler`]: dispatching::DispatcherBuilder::enable_ctrlc_handler

View file

@ -58,9 +58,10 @@
#![allow(clippy::nonstandard_macro_braces)] #![allow(clippy::nonstandard_macro_braces)]
#[cfg(feature = "ctrlc_handler")] #[cfg(feature = "ctrlc_handler")]
pub use dispatching::repls::{ pub use dispatching::repls::{repl, repl_with_listener};
commands_repl, commands_repl_with_listener, repl, repl_with_listener,
}; #[allow(deprecated)]
pub use dispatching::repls::{commands_repl, commands_repl_with_listener};
pub mod dispatching; pub mod dispatching;
pub mod error_handlers; pub mod error_handlers;

View file

@ -6,7 +6,8 @@ pub use crate::error_handlers::{LoggingErrorHandler, OnError};
pub use crate::respond; pub use crate::respond;
pub use crate::dispatching::{ pub use crate::dispatching::{
dialogue::Dialogue, Dispatcher, HandlerExt as _, MessageFilterExt as _, UpdateFilterExt as _, dialogue::Dialogue, repls::CommandReplExt as _, Dispatcher, HandlerExt as _,
MessageFilterExt as _, UpdateFilterExt as _,
}; };
pub use teloxide_core::{ pub use teloxide_core::{

View file

@ -1,8 +1,5 @@
//! This module contains stop [token] and stop [flag] that are used to stop //! Stopping asynchronous tasks, e.g., [listeners].
//! async tasks, for example [listeners].
//! //!
//! [token]: StopToken
//! [flag]: StopFlag
//! [listeners]: crate::dispatching::update_listeners //! [listeners]: crate::dispatching::update_listeners
use std::{convert::Infallible, future::Future, pin::Pin, task}; use std::{convert::Infallible, future::Future, pin::Pin, task};

View file

@ -91,7 +91,7 @@ pub use teloxide_macros::BotCommands;
/// 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")]`
/// Add a sumary 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")]`
/// Change the parser of arguments. Possible values: /// Change the parser of arguments. Possible values:
@ -115,7 +115,7 @@ pub use teloxide_macros::BotCommands;
/// # } /// # }
/// ``` /// ```
/// ///
/// - `split` - separates a messsage by a given separator (the default is the /// - `split` - separates a message by a given separator (the default is the
/// space character) and parses each part into the corresponding arguments, /// space character) and parses each part into the corresponding arguments,
/// which must implement [`FromStr`]. /// which must implement [`FromStr`].
/// ///
@ -233,8 +233,9 @@ pub trait BotCommands: Sized {
/// Returns `PhantomData<Self>` that is used as a param of [`commands_repl`] /// Returns `PhantomData<Self>` that is used as a param of [`commands_repl`]
/// ///
/// [`commands_repl`]: (crate::repls2::commands_repl) /// [`commands_repl`]: (crate::repls::commands_repl)
#[must_use] #[must_use]
#[deprecated(note = "Use `CommandReplExt` instead")]
fn ty() -> PhantomData<Self> { fn ty() -> PhantomData<Self> {
PhantomData PhantomData
} }
@ -412,9 +413,9 @@ where
return None; return None;
} }
let mut words = text.split_whitespace(); let mut words = text.split_whitespace();
let mut splited = words.next()?[prefix.len()..].split('@'); let mut split = words.next()?[prefix.len()..].split('@');
let command = splited.next()?; let command = split.next()?;
let bot = splited.next(); let bot = split.next();
match bot { match bot {
Some(name) if name.eq_ignore_ascii_case(bot_name.as_ref()) => {} Some(name) if name.eq_ignore_ascii_case(bot_name.as_ref()) => {}
None => {} None => {}
@ -485,7 +486,7 @@ impl Display for CommandDescriptions<'_> {
} }
} }
// The rest of tests are integrational due to problems with macro expansion in // The rest of tests are integration due to problems with macro expansion in
// unit tests. // unit tests.
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View file

@ -95,7 +95,7 @@ pub fn code_inline(s: &str) -> String {
/// style. /// style.
/// ///
/// Does not escape ' and " characters (as should be for usual HTML), because /// Does not escape ' and " characters (as should be for usual HTML), because
/// they shoudn't be escaped by the [spec]. /// they shouldn't be escaped by the [spec].
/// ///
/// [spec]: https://core.telegram.org/bots/api#html-style /// [spec]: https://core.telegram.org/bots/api#html-style
#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \ #[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
@ -176,7 +176,7 @@ mod tests {
assert_eq!( assert_eq!(
code_block_with_lang( code_block_with_lang(
"<p>pre-'formatted'\n & fixed-width \\code `block`</p>", "<p>pre-'formatted'\n & fixed-width \\code `block`</p>",
"<html>\"" "<html>\"",
), ),
concat!( concat!(
"<pre><code class=\"language-&lt;html&gt;&quot;\">", "<pre><code class=\"language-&lt;html&gt;&quot;\">",

View file

@ -38,7 +38,7 @@ pub fn italic(s: &str) -> String {
without using its output does nothing useful"] without using its output does nothing useful"]
pub fn underline(s: &str) -> String { pub fn underline(s: &str) -> String {
// In case of ambiguity between italic and underline entities // In case of ambiguity between italic and underline entities
// __ is always greadily treated from left to right as beginning or end of // __ is always greedily treated from left to right as beginning or end of
// underline entity, so instead of ___italic underline___ we should use // underline entity, so instead of ___italic underline___ we should use
// ___italic underline_\r__, where \r is a character with code 13, which // ___italic underline_\r__, where \r is a character with code 13, which
// will be ignored. // will be ignored.

95
tests/rocksdb.rs Normal file
View file

@ -0,0 +1,95 @@
use std::{
fmt::{Debug, Display},
fs,
sync::Arc,
};
use teloxide::{
dispatching::dialogue::{RocksDbStorage, RocksDbStorageError, Serializer, Storage},
types::ChatId,
};
#[tokio::test(flavor = "multi_thread")]
async fn test_rocksdb_json() {
fs::remove_dir_all("./test_db1").ok();
fs::create_dir("./test_db1").unwrap();
let storage = RocksDbStorage::open(
"./test_db1/test_db1.rocksdb",
teloxide::dispatching::dialogue::serializer::Json,
None,
)
.await
.unwrap();
test_rocksdb(storage).await;
fs::remove_dir_all("./test_db1").unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_rocksdb_bincode() {
fs::remove_dir_all("./test_db2").ok();
fs::create_dir("./test_db2").unwrap();
let storage = RocksDbStorage::open(
"./test_db2/test_db2.rocksdb",
teloxide::dispatching::dialogue::serializer::Bincode,
None,
)
.await
.unwrap();
test_rocksdb(storage).await;
fs::remove_dir_all("./test_db2").unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_rocksdb_cbor() {
fs::remove_dir_all("./test_db3").ok();
fs::create_dir("./test_db3").unwrap();
let storage = RocksDbStorage::open(
"./test_db3/test_db3.rocksdb",
teloxide::dispatching::dialogue::serializer::Cbor,
None,
)
.await
.unwrap();
test_rocksdb(storage).await;
fs::remove_dir_all("./test_db3").unwrap();
}
type Dialogue = String;
macro_rules! test_dialogues {
($storage:expr, $_0:expr, $_1:expr, $_2:expr) => {
assert_eq!(Arc::clone(&$storage).get_dialogue(ChatId(1)).await.unwrap(), $_0);
assert_eq!(Arc::clone(&$storage).get_dialogue(ChatId(11)).await.unwrap(), $_1);
assert_eq!(Arc::clone(&$storage).get_dialogue(ChatId(256)).await.unwrap(), $_2);
};
}
async fn test_rocksdb<S>(storage: Arc<RocksDbStorage<S>>)
where
S: Send + Sync + Serializer<Dialogue> + 'static,
<S as Serializer<Dialogue>>::Error: Debug + Display,
{
test_dialogues!(storage, None, None, None);
Arc::clone(&storage).update_dialogue(ChatId(1), "ABC".to_owned()).await.unwrap();
Arc::clone(&storage).update_dialogue(ChatId(11), "DEF".to_owned()).await.unwrap();
Arc::clone(&storage).update_dialogue(ChatId(256), "GHI".to_owned()).await.unwrap();
test_dialogues!(
storage,
Some("ABC".to_owned()),
Some("DEF".to_owned()),
Some("GHI".to_owned())
);
Arc::clone(&storage).remove_dialogue(ChatId(1)).await.unwrap();
Arc::clone(&storage).remove_dialogue(ChatId(11)).await.unwrap();
Arc::clone(&storage).remove_dialogue(ChatId(256)).await.unwrap();
test_dialogues!(storage, None, None, None);
// Check that a try to remove a non-existing dialogue results in an error.
assert!(matches!(
Arc::clone(&storage).remove_dialogue(ChatId(1)).await.unwrap_err(),
RocksDbStorageError::DialogueNotFound
));
}