diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ea743ff8..3e84df636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 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 ### Changed @@ -75,7 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 @@ -188,7 +199,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `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 @@ -265,7 +276,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - 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)). - 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 -- Now methods which can send file to Telegram returns `tokio::io::Result`. 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`. 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)). ## 0.2.0 - 2020-02-25 diff --git a/Cargo.toml b/Cargo.toml index 65e5135ae..f4056c492 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "teloxide" -version = "0.11.0" +version = "0.11.1" edition = "2021" description = "An elegant Telegram bots framework for Rust" repository = "https://github.com/teloxide/teloxide" @@ -19,6 +19,7 @@ webhooks-axum = ["webhooks", "axum", "tower", "tower-http"] sqlite-storage = ["sqlx"] redis-storage = ["redis"] +rocksdb-storage = ["rocksdb"] cbor-serializer = ["serde_cbor"] bincode-serializer = ["bincode"] @@ -42,6 +43,7 @@ full = [ "webhooks-axum", "sqlite-storage", "redis-storage", + "rocksdb-storage", "cbor-serializer", "bincode-serializer", "macros", @@ -92,6 +94,9 @@ sqlx = { version = "0.6", optional = true, default-features = false, features = "sqlite", ] } 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 } bincode = { version = "1.3", optional = true } axum = { version = "0.5.13", optional = true } diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 83ea0829e..c5da2a04b 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -1,6 +1,22 @@ 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. +## 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 ### 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`. 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(); ``` -```diff,rust +```diff -async fn start(bot: AutoSend, 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: MessageId = message.id; ``` -```diff,rust +```diff let (cid, mid): (ChatId, i32) = get_message_to_delete_from_db(); -bot.delete_message(cid, 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. 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?; 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: -```diff,rust +```diff teloxide::repl(bot, |bot: Bot, msg: Message| async move { bot.send_dice(msg.chat.id).await?; - 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. +"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 `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> { todo!() } @@ -94,7 +120,7 @@ enum Command { `rename` now only renames a command literally; use `rename_rule` to change the case of a command: -```diff,rust +```diff #[derive(BotCommands)] - #[command(rename = "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`. -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. @@ -237,7 +263,7 @@ List of changed 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. -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: - `get_chat_administrators`: `ChatMember` -> `Vec` @@ -324,7 +350,7 @@ List of renamed items: #### Added `impl Clone` for {`CacheMe`, `DefaultParseMode`, `Throttle`} 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`: ```diff diff --git a/README.md b/README.md index 2fb0010a5..f211a5473 100644 --- a/README.md +++ b/README.md @@ -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)
@@ -29,12 +29,13 @@ [`dptree`]: https://github.com/teloxide/dptree [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) [Redis]: https://redis.io/ +[RocksDB]: https://rocksdb.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`]. @@ -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`: ```toml [dependencies] -teloxide = { version = "0.11", features = ["macros", "auto-send"] } +teloxide = { version = "0.11", features = ["macros"] } log = "0.4" pretty_env_logger = "0.4" 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 -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)] @@ -131,7 +132,7 @@ async fn main() { let bot = Bot::from_env(); - teloxide::commands_repl(bot, answer, Command::ty()).await; + Command::repl(bot, answer).await; } #[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. - [`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. - - [`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.
diff --git a/examples/admin.rs b/examples/admin.rs index 4be9a5430..113d6f075 100644 --- a/examples/admin.rs +++ b/examples/admin.rs @@ -5,8 +5,8 @@ use teloxide::{prelude::*, types::ChatPermissions, utils::command::BotCommands}; // Derive BotCommands to parse text with a command into this enumeration. // -// 1. rename = "lowercase" turns all the commands into lowercase letters. -// 2. `description = "..."` specifies a text before all the commands. +// 1. `rename_rule = "lowercase"` turns all the commands into lowercase letters. +// 2. `description = "..."` specifies a text before all the commands. // // That is, you can just call Command::descriptions() to get a description of // your commands in this format: @@ -60,7 +60,7 @@ async fn main() { 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<()> { diff --git a/examples/command.rs b/examples/command.rs index 00f443154..26848015b 100644 --- a/examples/command.rs +++ b/examples/command.rs @@ -7,7 +7,7 @@ async fn main() { let bot = Bot::from_env(); - teloxide::commands_repl(bot, answer, Command::ty()).await; + Command::repl(bot, answer).await; } #[derive(BotCommands, Clone)] diff --git a/examples/db_remember.rs b/examples/db_remember.rs index de09db0d6..980d13571 100644 --- a/examples/db_remember.rs +++ b/examples/db_remember.rs @@ -91,7 +91,7 @@ async fn got_number( } Command::Reset => { dialogue.reset().await?; - bot.send_message(msg.chat.id, "Number resetted.").await?; + bot.send_message(msg.chat.id, "Number reset.").await?; } } Ok(()) diff --git a/examples/dispatching_features.rs b/examples/dispatching_features.rs index 983f00027..6ce059d98 100644 --- a/examples/dispatching_features.rs +++ b/examples/dispatching_features.rs @@ -32,7 +32,7 @@ async fn main() { .endpoint(simple_commands_handler), ) .branch( - // Filter a maintainer by a used ID. + // Filter a maintainer by a user ID. dptree::filter(|cfg: ConfigParameters, msg: Message| { msg.from().map(|user| user.id == cfg.bot_maintainer).unwrap_or_default() }) diff --git a/examples/heroku_ping_pong.rs b/examples/heroku_ping_pong.rs index 6fe508473..597670474 100644 --- a/examples/heroku_ping_pong.rs +++ b/examples/heroku_ping_pong.rs @@ -10,7 +10,7 @@ // heroku create --buildpack emk/rust // ``` // -// To set buildpack for existing applicaton: +// To set buildpack for existing application: // // ``` // heroku buildpacks:set emk/rust diff --git a/src/dispatching/dialogue/storage.rs b/src/dispatching/dialogue/storage.rs index b78cd72e0..e8d2c1587 100644 --- a/src/dispatching/dialogue/storage.rs +++ b/src/dispatching/dialogue/storage.rs @@ -9,6 +9,9 @@ mod redis_storage; #[cfg(feature = "sqlite-storage")] mod sqlite_storage; +#[cfg(feature = "rocksdb-storage")] +mod rocksdb_storage; + use futures::future::BoxFuture; use teloxide_core::types::ChatId; @@ -25,6 +28,9 @@ use std::sync::Arc; #[cfg(feature = "sqlite-storage")] pub use sqlite_storage::{SqliteStorage, SqliteStorageError}; +#[cfg(feature = "rocksdb-storage")] +pub use rocksdb_storage::{RocksDbStorage, RocksDbStorageError}; + /// A storage with an erased error type. pub type ErasedStorage = dyn Storage> + Send + Sync; @@ -41,10 +47,12 @@ pub type ErasedStorage = /// /// - [`InMemStorage`] -- a storage based on [`std::collections::HashMap`]. /// - [`RedisStorage`] -- a Redis-based storage. +/// - [`RocksDbStorage`] -- a RocksDB-based persistent storage. /// - [`SqliteStorage`] -- an SQLite-based persistent storage. /// /// [`InMemStorage`]: crate::dispatching::dialogue::InMemStorage /// [`RedisStorage`]: crate::dispatching::dialogue::RedisStorage +/// [`RocksDbStorage`]: crate::dispatching::dialogue::RocksDbStorage /// [`SqliteStorage`]: crate::dispatching::dialogue::SqliteStorage pub trait Storage { type Error; diff --git a/src/dispatching/dialogue/storage/rocksdb_storage.rs b/src/dispatching/dialogue/storage/rocksdb_storage.rs new file mode 100644 index 000000000..d01d8fd9b --- /dev/null +++ b/src/dispatching/dialogue/storage/rocksdb_storage.rs @@ -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 { + db: DBWithThreadMode, + serializer: S, +} + +/// An error returned from [`RocksDbStorage`]. +#[derive(Debug, Error)] +pub enum RocksDbStorageError +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 RocksDbStorage { + pub async fn open( + path: &str, + serializer: S, + options: Option, + ) -> Result, RocksDbStorageError> { + 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::::open(&options, path)?; + Ok(Arc::new(Self { db, serializer })) + } +} + +impl Storage for RocksDbStorage +where + S: Send + Sync + Serializer + 'static, + D: Send + Serialize + DeserializeOwned + 'static, + >::Error: Debug + Display, +{ + type Error = RocksDbStorageError<>::Error>; + + /// Returns [`RocksDbStorageError::DialogueNotFound`] if a dialogue does not + /// exist. + fn remove_dialogue( + self: Arc, + 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, + 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, + ChatId(chat_id): ChatId, + ) -> BoxFuture<'static, Result, 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() + }) + } +} diff --git a/src/dispatching/repls.rs b/src/dispatching/repls.rs index 71d703430..aea4806eb 100644 --- a/src/dispatching/repls.rs +++ b/src/dispatching/repls.rs @@ -11,5 +11,7 @@ mod commands_repl; mod repl; +pub use commands_repl::CommandReplExt; +#[allow(deprecated)] pub use commands_repl::{commands_repl, commands_repl_with_listener}; pub use repl::{repl, repl_with_listener}; diff --git a/src/dispatching/repls/commands_repl.rs b/src/dispatching/repls/commands_repl.rs index fe9d9fb7a..15f2396d5 100644 --- a/src/dispatching/repls/commands_repl.rs +++ b/src/dispatching/repls/commands_repl.rs @@ -8,8 +8,147 @@ use crate::{ utils::command::BotCommands, }; use dptree::di::{DependencyMap, Injectable}; +use futures::future::BoxFuture; 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, + ::GetUpdates: Send, + ::GetWebhookInfo: Send, + ::GetMe: Send, + ::DeleteWebhook: Send, + H: Injectable, 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, Args> + Send + Sync + 'static, + L: UpdateListener + Send + 'a, + L::Err: Debug + Send + 'a, + R: Requester + Clone + Send + Sync + 'static, + ::GetMe: Send; +} + +#[cfg(feature = "ctrlc_handler")] +impl 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, + ::GetUpdates: Send, + ::GetWebhookInfo: Send, + ::GetMe: Send, + ::DeleteWebhook: Send, + H: Injectable, 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, Args> + Send + Sync + 'static, + L: UpdateListener + Send + 'a, + L::Err: Debug + Send + 'a, + R: Requester + Clone + Send + Sync + 'static, + ::GetMe: Send, + { + use crate::dispatching::Dispatcher; + + // Other update types are of no interest to use since this REPL is only for + // commands. See . + let ignore_update = |_upd| Box::pin(async {}); + + Box::pin(async move { + Dispatcher::builder( + bot, + Update::filter_message().filter_command::().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. // /// @@ -59,6 +198,7 @@ use std::{fmt::Debug, marker::PhantomData}; #[doc = include_str!("caution.md")] /// #[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) where R: Requester + Clone + Send + Sync + 'static, @@ -68,6 +208,7 @@ where { let cloned_bot = bot.clone(); + #[allow(deprecated)] commands_repl_with_listener( bot, handler, @@ -127,6 +268,7 @@ where #[doc = include_str!("caution.md")] /// #[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>( bot: R, handler: H, diff --git a/src/dispatching/update_listeners/polling.rs b/src/dispatching/update_listeners/polling.rs index 5fb863f07..ffe8aa5a3 100644 --- a/src/dispatching/update_listeners/polling.rs +++ b/src/dispatching/update_listeners/polling.rs @@ -225,7 +225,7 @@ where /// /// C->>P: next /// -/// P->>T: *Acknolegment of update(5)* +/// P->>T: *Acknowledgement of update(5)* /// T->>P: ok /// /// P->>C: None diff --git a/src/features.md b/src/features.md index 1545af787..a04563779 100644 --- a/src/features.md +++ b/src/features.md @@ -1,27 +1,28 @@ ## Cargo features -| Feature | Description | -|----------------------|--------------------------------------------------------------------------------------------| -| `webhooks` | Enables general webhook utilities (almost useless on its own) | -| `webhooks-axum` | Enables webhook implementation based on axum framework | -| `macros` | Re-exports macros from [`teloxide-macros`]. | +| Feature | Description | +|----------------------|-------------| +| `webhooks` | Enables general webhook utilities (almost useless on its own). | +| `webhooks-axum` | Enables webhook implementation based on axum framework. | +| `macros` | Re-exports macros from [`teloxide-macros`]. | | `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**). | -| `throttle` | Enables the [`Throttle`](adaptors::Throttle) bot adaptor. | -| `cache-me` | Enables the [`CacheMe`](adaptors::CacheMe) bot adaptor. | -| `trace-adaptor` | Enables the [`Trace`](adaptors::Trace) bot adaptor. | -| `erased` | Enables the [`ErasedRequester`](adaptors::ErasedRequester) bot adaptor. | -| `full` | Enables all the features except `nightly`. | -| `nightly` | Enables nightly-only features (see the [`teloxide-core` features]). | -| `native-tls` | Enables the [`native-tls`] TLS implementation (**enabled by default**). | -| `rustls` | Enables the [`rustls`] TLS implementation. | -| `redis-storage` | Enables the [Redis] storage support for dialogues. | -| `sqlite-storage` | Enables the [Sqlite] storage support for dialogues. | -| `cbor-serializer` | Enables the [CBOR] serializer for dialogues. | -| `bincode-serializer` | Enables the [Bincode] serializer for dialogues. | - +| `auto-send` | Enables the [`AutoSend`](adaptors::AutoSend) bot adaptor (**enabled by default; DEPRECATED**). | +| `throttle` | Enables the [`Throttle`](adaptors::Throttle) bot adaptor. | +| `cache-me` | Enables the [`CacheMe`](adaptors::CacheMe) bot adaptor. | +| `trace-adaptor` | Enables the [`Trace`](adaptors::Trace) bot adaptor. | +| `erased` | Enables the [`ErasedRequester`](adaptors::ErasedRequester) bot adaptor. | +| `full` | Enables all the features except `nightly`. | +| `nightly` | Enables nightly-only features (see the [`teloxide-core` features]). | +| `native-tls` | Enables the [`native-tls`] TLS implementation (**enabled by default**). | +| `rustls` | Enables the [`rustls`] TLS implementation. | +| `redis-storage` | Enables the [Redis] storage support for dialogues. | +| `rocksdb-storage` | Enables the [RocksDB] storage support for dialogues. | +| `sqlite-storage` | Enables the [Sqlite] storage support for dialogues. | +| `cbor-serializer` | Enables the [CBOR] serializer for dialogues. | +| `bincode-serializer` | Enables the [Bincode] serializer for dialogues. | [Redis]: https://redis.io/ +[RocksDB]: https://rocksdb.org/ [Sqlite]: https://www.sqlite.org/ [CBOR]: https://en.wikipedia.org/wiki/CBOR [Bincode]: https://github.com/servo/bincode @@ -31,4 +32,4 @@ [`teloxide::utils::UpState`]: utils::UpState [`teloxide-core` features]: https://docs.rs/teloxide-core/latest/teloxide_core/#cargo-features -[`DispatcherBuilder::enable_ctrlc_handler`]: dispatching::DispatcherBuilder::enable_ctrlc_handler \ No newline at end of file +[`DispatcherBuilder::enable_ctrlc_handler`]: dispatching::DispatcherBuilder::enable_ctrlc_handler diff --git a/src/lib.rs b/src/lib.rs index 80237a9dc..04012a60c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,9 +58,10 @@ #![allow(clippy::nonstandard_macro_braces)] #[cfg(feature = "ctrlc_handler")] -pub use dispatching::repls::{ - commands_repl, commands_repl_with_listener, repl, repl_with_listener, -}; +pub use dispatching::repls::{repl, repl_with_listener}; + +#[allow(deprecated)] +pub use dispatching::repls::{commands_repl, commands_repl_with_listener}; pub mod dispatching; pub mod error_handlers; diff --git a/src/prelude.rs b/src/prelude.rs index 6dbe94df2..ca6afa901 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -6,7 +6,8 @@ pub use crate::error_handlers::{LoggingErrorHandler, OnError}; pub use crate::respond; 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::{ diff --git a/src/stop.rs b/src/stop.rs index f61caf3d1..71fcebe95 100644 --- a/src/stop.rs +++ b/src/stop.rs @@ -1,8 +1,5 @@ -//! This module contains stop [token] and stop [flag] that are used to stop -//! async tasks, for example [listeners]. +//! Stopping asynchronous tasks, e.g., [listeners]. //! -//! [token]: StopToken -//! [flag]: StopFlag //! [listeners]: crate::dispatching::update_listeners use std::{convert::Infallible, future::Future, pin::Pin, task}; diff --git a/src/utils/command.rs b/src/utils/command.rs index 24e001cfa..8f96998e0 100644 --- a/src/utils/command.rs +++ b/src/utils/command.rs @@ -91,7 +91,7 @@ pub use teloxide_macros::BotCommands; /// Change a prefix for all commands (the default is `/`). /// /// 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")]` /// 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, /// which must implement [`FromStr`]. /// @@ -233,8 +233,9 @@ pub trait BotCommands: Sized { /// Returns `PhantomData` that is used as a param of [`commands_repl`] /// - /// [`commands_repl`]: (crate::repls2::commands_repl) + /// [`commands_repl`]: (crate::repls::commands_repl) #[must_use] + #[deprecated(note = "Use `CommandReplExt` instead")] fn ty() -> PhantomData { PhantomData } @@ -412,9 +413,9 @@ where return None; } let mut words = text.split_whitespace(); - let mut splited = words.next()?[prefix.len()..].split('@'); - let command = splited.next()?; - let bot = splited.next(); + let mut split = words.next()?[prefix.len()..].split('@'); + let command = split.next()?; + let bot = split.next(); match bot { Some(name) if name.eq_ignore_ascii_case(bot_name.as_ref()) => {} 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. #[cfg(test)] mod tests { diff --git a/src/utils/html.rs b/src/utils/html.rs index 407f07929..d2243d5d2 100644 --- a/src/utils/html.rs +++ b/src/utils/html.rs @@ -95,7 +95,7 @@ pub fn code_inline(s: &str) -> String { /// style. /// /// 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 #[must_use = "This function returns a new string, rather than mutating the argument, so calling it \ @@ -176,7 +176,7 @@ mod tests { assert_eq!( code_block_with_lang( "

pre-'formatted'\n & fixed-width \\code `block`

", - "\"" + "\"", ), concat!( "
",
diff --git a/src/utils/markdown.rs b/src/utils/markdown.rs
index 9c3554445..976e0ab44 100644
--- a/src/utils/markdown.rs
+++ b/src/utils/markdown.rs
@@ -38,7 +38,7 @@ pub fn italic(s: &str) -> String {
               without using its output does nothing useful"]
 pub fn underline(s: &str) -> String {
     // 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
     // ___italic underline_\r__, where \r is a character with code 13, which
     // will be ignored.
diff --git a/tests/rocksdb.rs b/tests/rocksdb.rs
new file mode 100644
index 000000000..7366a2629
--- /dev/null
+++ b/tests/rocksdb.rs
@@ -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(storage: Arc>)
+where
+    S: Send + Sync + Serializer + 'static,
+    >::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
+    ));
+}