├── .gitignore ├── src ├── ratelimit │ ├── predicate │ │ ├── mod.rs │ │ ├── direct │ │ │ ├── tests.rs │ │ │ └── mod.rs │ │ └── keyed │ │ │ ├── mod.rs │ │ │ └── tests.rs │ ├── jitter.rs │ ├── mod.rs │ ├── method.rs │ └── key.rs ├── core │ ├── predicate │ │ ├── mod.rs │ │ ├── command │ │ │ ├── mod.rs │ │ │ └── tests.rs │ │ ├── result │ │ │ ├── tests.rs │ │ │ └── mod.rs │ │ ├── ext.rs │ │ └── base │ │ │ ├── tests.rs │ │ │ └── mod.rs │ ├── mod.rs │ ├── context │ │ ├── tests.rs │ │ └── mod.rs │ ├── handler │ │ ├── tests.rs │ │ └── mod.rs │ ├── app │ │ ├── tests.rs │ │ └── mod.rs │ ├── error │ │ ├── tests.rs │ │ └── mod.rs │ ├── chain │ │ ├── tests.rs │ │ └── mod.rs │ └── convert │ │ ├── mod.rs │ │ └── tests.rs ├── access │ ├── mod.rs │ ├── ext.rs │ ├── policy │ │ ├── tests.rs │ │ └── mod.rs │ ├── predicate │ │ ├── mod.rs │ │ └── tests.rs │ ├── principal │ │ ├── tests.rs │ │ └── mod.rs │ └── rule │ │ ├── mod.rs │ │ └── tests.rs ├── dialogue │ ├── mod.rs │ ├── state.rs │ ├── result.rs │ ├── ext.rs │ ├── error.rs │ ├── input.rs │ ├── predicate.rs │ ├── decorator.rs │ └── tests.rs ├── lib.rs └── session │ └── mod.rs ├── .rustfmt.toml ├── CODE_OF_CONDUCT.md ├── examples ├── app │ ├── command.rs │ ├── access.rs │ ├── error.rs │ ├── predicate.rs │ ├── ratelimit.rs │ ├── dialogue.rs │ ├── main.rs │ └── session.rs └── echo.rs ├── .github └── workflows │ ├── coverage.yml │ ├── gh-pages.yml │ └── ci.yml ├── sample.env ├── LICENSE ├── README.md ├── tests └── versions.rs ├── Cargo.toml ├── flake.nix ├── flake.lock └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | Cargo.lock 3 | /target/ 4 | -------------------------------------------------------------------------------- /src/ratelimit/predicate/mod.rs: -------------------------------------------------------------------------------- 1 | mod direct; 2 | mod keyed; 3 | 4 | pub use self::{direct::*, keyed::*}; 5 | -------------------------------------------------------------------------------- /src/ratelimit/jitter.rs: -------------------------------------------------------------------------------- 1 | /// Wait without jitter. 2 | #[derive(Clone, Copy, Debug)] 3 | pub struct NoJitter; 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | max_width = 120 3 | use_try_shorthand = true 4 | imports_granularity = "Crate" 5 | -------------------------------------------------------------------------------- /src/core/predicate/mod.rs: -------------------------------------------------------------------------------- 1 | mod base; 2 | mod command; 3 | mod ext; 4 | mod result; 5 | 6 | pub use self::{base::*, command::*, ext::*, result::*}; 7 | -------------------------------------------------------------------------------- /src/ratelimit/mod.rs: -------------------------------------------------------------------------------- 1 | mod jitter; 2 | mod key; 3 | mod method; 4 | mod predicate; 5 | 6 | pub use self::{jitter::*, key::*, method::*, predicate::*}; 7 | -------------------------------------------------------------------------------- /src/access/mod.rs: -------------------------------------------------------------------------------- 1 | mod ext; 2 | mod policy; 3 | mod predicate; 4 | mod principal; 5 | mod rule; 6 | 7 | pub use self::{ext::*, policy::*, predicate::*, principal::*, rule::*}; 8 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod chain; 3 | mod context; 4 | mod convert; 5 | mod error; 6 | mod handler; 7 | mod predicate; 8 | 9 | pub use self::{app::*, chain::*, context::*, convert::*, error::*, handler::*, predicate::*}; 10 | -------------------------------------------------------------------------------- /src/ratelimit/method.rs: -------------------------------------------------------------------------------- 1 | /// Discards updates when the rate limit is reached. 2 | #[derive(Clone, Copy, Debug)] 3 | pub struct MethodDiscard; 4 | 5 | /// Allows update to pass as soon as the rate limiter allows it. 6 | #[derive(Clone, Copy, Debug)] 7 | pub struct MethodWait; 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | This project adheres to No Code of Conduct. We are all adults. We accept anyone's contributions. Nothing else matters. 4 | 5 | For more information please visit the [No Code of Conduct](https://github.com/domgetter/NCoC) homepage. 6 | -------------------------------------------------------------------------------- /src/dialogue/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | decorator::DialogueDecorator, error::DialogueError, ext::DialogueExt, input::DialogueInput, 3 | predicate::DialoguePredicate, result::DialogueResult, state::DialogueState, 4 | }; 5 | 6 | mod decorator; 7 | mod error; 8 | mod ext; 9 | mod input; 10 | mod predicate; 11 | mod result; 12 | mod state; 13 | 14 | #[cfg(test)] 15 | mod tests; 16 | -------------------------------------------------------------------------------- /src/core/context/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq)] 4 | struct X; 5 | 6 | #[test] 7 | fn context() { 8 | let mut context = Context::default(); 9 | let x = X; 10 | assert!(context.insert(x).is_none()); 11 | assert!(context.get::().is_some()); 12 | assert!(context.insert(x).is_some()); 13 | } 14 | 15 | #[test] 16 | fn reference() { 17 | let x = X; 18 | let ref_x = Ref::new(x); 19 | assert_eq!(x, *ref_x); 20 | } 21 | -------------------------------------------------------------------------------- /src/dialogue/state.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, de::DeserializeOwned}; 2 | 3 | const SESSION_KEY_PREFIX: &str = "__carapax_dialogue"; 4 | 5 | /// Represents a state of dialogue. 6 | pub trait DialogueState: Default + DeserializeOwned + Serialize { 7 | /// Returns a unique name for the dialogue. 8 | fn dialogue_name() -> &'static str; 9 | 10 | /// Returns a key for the dialogue state in a session. 11 | fn session_key() -> String { 12 | format!("{}:{}", SESSION_KEY_PREFIX, Self::dialogue_name()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/dialogue/result.rs: -------------------------------------------------------------------------------- 1 | use crate::dialogue::state::DialogueState; 2 | 3 | /// Represents a result of a dialogue handler. 4 | #[derive(Debug)] 5 | pub enum DialogueResult { 6 | /// Indicates the next step of the dialogue containing the current value of the state. 7 | Next(S), 8 | /// Indicates an exit from the dialogue. 9 | Exit, 10 | } 11 | 12 | impl From for DialogueResult 13 | where 14 | S: DialogueState, 15 | { 16 | fn from(state: S) -> Self { 17 | DialogueResult::Next(state) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A Telegram Bot framework 2 | #![cfg_attr(nightly, feature(doc_cfg))] 3 | 4 | pub use tgbot::*; 5 | 6 | pub use self::core::*; 7 | 8 | mod core; 9 | 10 | /// Access control 11 | #[cfg(feature = "access")] 12 | #[cfg_attr(nightly, doc(cfg(feature = "access")))] 13 | pub mod access; 14 | 15 | /// Dialogue support 16 | #[cfg(feature = "dialogue")] 17 | #[cfg_attr(nightly, doc(cfg(feature = "dialogue")))] 18 | pub mod dialogue; 19 | 20 | /// Ratelimit support 21 | #[cfg(feature = "ratelimit")] 22 | #[cfg_attr(nightly, doc(cfg(feature = "ratelimit")))] 23 | pub mod ratelimit; 24 | 25 | /// Session support 26 | #[cfg(feature = "session")] 27 | #[cfg_attr(nightly, doc(cfg(feature = "session")))] 28 | pub mod session; 29 | -------------------------------------------------------------------------------- /examples/app/command.rs: -------------------------------------------------------------------------------- 1 | //! # Commands 2 | //! 3 | //! By wrapping the [`greet`] handler with the [`carapax::CommandPredicate`], 4 | //! it ensures that the handler is executed only when an incoming update 5 | //! contains a message with the `/hello` command. 6 | use carapax::{ 7 | Chain, CommandExt, Ref, 8 | api::Client, 9 | types::{ChatPeerId, SendMessage, User}, 10 | }; 11 | 12 | use crate::error::AppError; 13 | 14 | pub fn setup(chain: Chain) -> Chain { 15 | chain.with(greet.with_command("/hello")) 16 | } 17 | 18 | async fn greet(client: Ref, chat_id: ChatPeerId, user: User) -> Result<(), AppError> { 19 | let method = SendMessage::new(chat_id, format!("Hello, {}", user.first_name)); 20 | client.execute(method).await?; 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /src/core/predicate/command/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{core::handler::Handler, types::Command}; 2 | 3 | #[cfg(test)] 4 | mod tests; 5 | 6 | /// Allows to run a handler only for a specific command. 7 | #[derive(Clone)] 8 | pub struct CommandPredicate { 9 | name: String, 10 | } 11 | 12 | impl CommandPredicate { 13 | /// Creates a new `CommandPredicate`. 14 | /// 15 | /// # Arguments 16 | /// 17 | /// * `name` - A name of a command with leading `/`. 18 | pub fn new>(name: S) -> Self { 19 | Self { name: name.into() } 20 | } 21 | } 22 | 23 | impl Handler for CommandPredicate { 24 | type Output = bool; 25 | 26 | async fn handle(&self, input: Command) -> Self::Output { 27 | input.get_name() == self.name 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/access/ext.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | access::predicate::AccessPredicate, 3 | core::{Handler, HandlerInput, Predicate, TryFromInput}, 4 | }; 5 | 6 | /// Provides a shortcut for wrapping a [`Handler`] by an [`AccessPredicate`]. 7 | pub trait AccessExt: Sized { 8 | /// Shortcut to wrap a [`Handler`] with an access predicate. 9 | /// 10 | /// Example: `let handler = handler.access(policy)`. 11 | /// 12 | /// # Arguments 13 | /// 14 | /// * `policy` - A [`crate::access::AccessPolicy`]. 15 | fn with_access_policy(self, policy: P) -> Predicate, HandlerInput, Self, HI> { 16 | Predicate::new(AccessPredicate::new(policy), self) 17 | } 18 | } 19 | 20 | impl AccessExt for H 21 | where 22 | H: Handler, 23 | HI: TryFromInput, 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /src/core/predicate/result/tests.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use super::*; 4 | 5 | #[derive(Debug)] 6 | struct ExampleError; 7 | 8 | impl Error for ExampleError {} 9 | 10 | impl fmt::Display for ExampleError { 11 | fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result { 12 | write!(out, "Example error") 13 | } 14 | } 15 | 16 | #[test] 17 | fn convert_result() { 18 | assert!(matches!(true.into(), PredicateResult::True)); 19 | assert!(matches!(false.into(), PredicateResult::False)); 20 | assert!(matches!(Ok::(true).into(), PredicateResult::True)); 21 | assert!(matches!(Ok::(false).into(), PredicateResult::False)); 22 | assert!(matches!( 23 | Err::(ExampleError).into(), 24 | PredicateResult::Err(_) 25 | )); 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | on: 3 | push: 4 | branches-ignore: 5 | - gh-pages 6 | pull_request: 7 | branches-ignore: 8 | - gh-pages 9 | 10 | jobs: 11 | coverage: 12 | name: Coverage 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Install stable toolchain 18 | uses: dtolnay/rust-toolchain@stable 19 | with: 20 | toolchain: stable 21 | - name: Install cargo-llvm-cov 22 | uses: taiki-e/install-action@cargo-llvm-cov 23 | - name: Generate 24 | run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info 25 | - name: Publish 26 | uses: codecov/codecov-action@v4 27 | with: 28 | token: ${{ secrets.CODECOV_TOKEN }} 29 | file: lcov.info 30 | fail_ci_if_error: true 31 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | docs: 9 | name: Generate master API documentation 10 | runs-on: ubuntu-latest 11 | env: 12 | RUSTDOCFLAGS: --cfg=nightly 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Install nightly toolchain 17 | uses: dtolnay/rust-toolchain@stable 18 | with: 19 | toolchain: nightly 20 | components: rustfmt 21 | - name: Test 22 | run: cargo test --doc --all-features 23 | - name: Generate 24 | run: cargo doc --all --all-features 25 | - name: Publish 26 | uses: peaceiris/actions-gh-pages@v4 27 | with: 28 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 29 | force_orphan: true 30 | publish_branch: gh-pages 31 | publish_dir: ./target/doc 32 | -------------------------------------------------------------------------------- /src/core/predicate/command/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::types::Message; 2 | 3 | use super::*; 4 | 5 | fn create_command(command: &str) -> Command { 6 | let len = command.len(); 7 | let message: Message = serde_json::from_value(serde_json::json!( 8 | { 9 | "message_id": 1111, 10 | "date": 0, 11 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 12 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 13 | "text": command, 14 | "entities": [ 15 | {"type": "bot_command", "offset": 0, "length": len} 16 | ] 17 | } 18 | )) 19 | .unwrap(); 20 | Command::try_from(message).unwrap() 21 | } 22 | 23 | #[tokio::test] 24 | async fn command_predicate() { 25 | let handler = CommandPredicate::new("/start"); 26 | assert!(handler.handle(create_command("/start")).await,); 27 | assert!(!handler.handle(create_command("/unexpected")).await); 28 | } 29 | -------------------------------------------------------------------------------- /examples/app/access.rs: -------------------------------------------------------------------------------- 1 | //! # Access control 2 | //! 3 | //! Here we create an access policy that determines 4 | //! whether access should be granted or denied based on the [`carapax::HandlerInput`]. 5 | //! 6 | //! You can implement your own policy using [`carapax::access::AccessPolicy`] trait. 7 | //! 8 | //! After we wrap the [`log_protected`] handler with the [`carapax::access::AccessPredicate`]. 9 | //! This ensures that the handler executes only when the access policy grants permission. 10 | //! 11 | //! Note that you need to enable the `access` feature in your `Cargo.toml`. 12 | use carapax::{ 13 | Chain, 14 | access::{AccessExt, AccessRule, InMemoryAccessPolicy}, 15 | types::Update, 16 | }; 17 | 18 | pub fn setup(chain: Chain, username: &str) -> Chain { 19 | let policy = InMemoryAccessPolicy::from(vec![AccessRule::allow_user(username)]); 20 | chain.with(log_protected.with_access_policy(policy)) 21 | } 22 | 23 | async fn log_protected(update: Update) { 24 | log::info!("Got a new update in the protected handler: {update:?}"); 25 | } 26 | -------------------------------------------------------------------------------- /src/core/predicate/result/mod.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use crate::core::handler::HandlerError; 4 | 5 | #[cfg(test)] 6 | mod tests; 7 | 8 | /// Represents a result of a predicate. 9 | #[derive(Debug)] 10 | pub enum PredicateResult { 11 | /// A decorated handler will be executed. 12 | True, 13 | /// A decorated handler was not executed. 14 | False, 15 | /// An error occurred during the predicate execution. 16 | Err(HandlerError), 17 | } 18 | 19 | impl From for PredicateResult { 20 | fn from(value: bool) -> Self { 21 | if value { 22 | PredicateResult::True 23 | } else { 24 | PredicateResult::False 25 | } 26 | } 27 | } 28 | 29 | impl From> for PredicateResult 30 | where 31 | T: Into, 32 | E: Error + Send + 'static, 33 | { 34 | fn from(value: Result) -> Self { 35 | match value { 36 | Ok(value) => value.into(), 37 | Err(err) => PredicateResult::Err(HandlerError::new(err)), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | # Logging settings 2 | # See https://docs.rs/env_logger/ for more information 3 | RUST_LOG=info 4 | 5 | # A telegram bot token 6 | CARAPAX_TOKEN=YOUR-BOT-TOKEN-HERE 7 | 8 | # Updates will be denied for all except given username 9 | # Specify a username without @ 10 | #CARAPAX_ACCESS_USERNAME=username 11 | 12 | # A rate-limit strategy: 13 | # 14 | # * direct_discard - Discard all updates when ratelimit is reached 15 | # * direct_wait - Wait for next available cell when ratelimit is reached 16 | # * direct_wait_with_jitter - Wait for next available cell when ratelimit is reached (with jitter) 17 | # * keyed_discard - Discard update for a specific key when ratelimit is reached 18 | # * keyed_wait - Wait for next available cell for a specific key when ratelimit is reached 19 | # * keyed_wait_with_jitter - Wait for next available cell for a specific key when ratelimit is reached (with jitter) 20 | #CARAPAX_RATE_LIMIT_STRATEGY=list 21 | 22 | # Period between session GC calls (seconds) 23 | CARAPAX_SESSION_GC_PERIOD=60 24 | 25 | # How long session lives (seconds) 26 | CARAPAX_SESSION_LIFETIME=86400 27 | -------------------------------------------------------------------------------- /examples/app/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | use carapax::{api::ExecuteError, session::SessionError}; 4 | 5 | #[derive(Debug)] 6 | pub enum AppError { 7 | Execute(ExecuteError), 8 | Session(SessionError), 9 | } 10 | 11 | impl fmt::Display for AppError { 12 | fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result { 13 | match self { 14 | AppError::Execute(err) => write!(out, "Execute error: {err}"), 15 | AppError::Session(err) => write!(out, "Session error: {err}"), 16 | } 17 | } 18 | } 19 | 20 | impl Error for AppError { 21 | fn source(&self) -> Option<&(dyn Error + 'static)> { 22 | match self { 23 | AppError::Execute(err) => Some(err), 24 | AppError::Session(err) => Some(err), 25 | } 26 | } 27 | } 28 | 29 | impl From for AppError { 30 | fn from(err: ExecuteError) -> Self { 31 | AppError::Execute(err) 32 | } 33 | } 34 | 35 | impl From for AppError { 36 | fn from(err: SessionError) -> Self { 37 | AppError::Session(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/core/handler/tests.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use super::*; 4 | 5 | #[test] 6 | fn convert_input() { 7 | let update: Update = serde_json::from_value(serde_json::json!( 8 | { 9 | "update_id": 1, 10 | "message": { 11 | "message_id": 1111, 12 | "date": 0, 13 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 14 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 15 | "text": "test", 16 | } 17 | } 18 | )) 19 | .unwrap(); 20 | assert_eq!(HandlerInput::from(update).update.id, 1); 21 | } 22 | 23 | #[derive(Debug)] 24 | struct ExampleError; 25 | 26 | impl Error for ExampleError {} 27 | 28 | impl fmt::Display for ExampleError { 29 | fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result { 30 | write!(out, "Example error") 31 | } 32 | } 33 | 34 | #[test] 35 | fn convert() { 36 | assert!(matches!(().into_result(), Ok(()))); 37 | assert!(matches!(Ok::<(), ExampleError>(()).into_result(), Ok(()))); 38 | assert!(Err::<(), ExampleError>(ExampleError).into_result().is_err()); 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present Ross Nomann and contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/dialogue/ext.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | core::{Handler, HandlerInput, Predicate, TryFromInput}, 3 | dialogue::{decorator::DialogueDecorator, predicate::DialoguePredicate}, 4 | }; 5 | 6 | /// Provides a shortcut for wrapping a [`Handler`] by a [`DialogueDecorator`]. 7 | pub trait DialogueExt: Sized { 8 | /// Shortcut to wrap a [`Handler`] with a [`DialogueDecorator`]. 9 | /// 10 | /// Example: `handler.dialogue(predicate)`. 11 | /// 12 | /// # Arguments 13 | /// 14 | /// * `predicate` - A predicate to be execute before starting the dialogue. 15 | /// 16 | /// If you don't need to start the dialogue conditionally, 17 | /// you can use [`DialogueDecorator::new`] directly. 18 | #[allow(clippy::type_complexity)] 19 | fn with_dialogue( 20 | self, 21 | predicate: P, 22 | ) -> Predicate, HandlerInput, DialogueDecorator, HandlerInput> 23 | { 24 | Predicate::new(DialoguePredicate::new(predicate), DialogueDecorator::new(self)) 25 | } 26 | } 27 | 28 | impl DialogueExt for H 29 | where 30 | P: Handler, 31 | PI: TryFromInput, 32 | H: Handler, 33 | HI: TryFromInput, 34 | { 35 | } 36 | -------------------------------------------------------------------------------- /src/dialogue/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | use seance::SessionError; 4 | 5 | use crate::session::CreateSessionError; 6 | 7 | /// An error when processing dialogue. 8 | #[derive(Debug)] 9 | pub enum DialogueError { 10 | /// Failed to obtain input for the dialogue handler. 11 | ConvertHandlerInput, 12 | /// Failed to create a session. 13 | CreateSession(CreateSessionError), 14 | /// Failed to load the dialogue state. 15 | LoadState(SessionError), 16 | } 17 | 18 | impl From for DialogueError { 19 | fn from(err: CreateSessionError) -> Self { 20 | DialogueError::CreateSession(err) 21 | } 22 | } 23 | 24 | impl fmt::Display for DialogueError { 25 | fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result { 26 | use self::DialogueError::*; 27 | match self { 28 | ConvertHandlerInput => write!(out, "Could not obtain input for dialogue handler"), 29 | CreateSession(err) => write!(out, "{err}"), 30 | LoadState(err) => write!(out, "Failed to load dialogue state: {err}"), 31 | } 32 | } 33 | } 34 | 35 | impl Error for DialogueError { 36 | fn source(&self) -> Option<&(dyn Error + 'static)> { 37 | use self::DialogueError::*; 38 | match self { 39 | ConvertHandlerInput => None, 40 | CreateSession(err) => Some(err), 41 | LoadState(err) => Some(err), 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/access/policy/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::types::Update; 2 | 3 | use super::*; 4 | 5 | #[tokio::test] 6 | async fn in_memory_access_policy() { 7 | let policy = InMemoryAccessPolicy::from(vec![AccessRule::allow_user(1)]); 8 | 9 | let update_granted: Update = serde_json::from_value(serde_json::json!( 10 | { 11 | "update_id": 1, 12 | "message": { 13 | "message_id": 1111, 14 | "date": 0, 15 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 16 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 17 | "text": "test", 18 | } 19 | } 20 | )) 21 | .unwrap(); 22 | let input_granted = HandlerInput::from(update_granted); 23 | assert!(policy.is_granted(input_granted).await.unwrap()); 24 | 25 | let update_forbidden: Update = serde_json::from_value(serde_json::json!( 26 | { 27 | "update_id": 1, 28 | "message": { 29 | "message_id": 1111, 30 | "date": 0, 31 | "from": {"id": 2, "is_bot": false, "first_name": "test"}, 32 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 33 | "text": "test", 34 | } 35 | } 36 | )) 37 | .unwrap(); 38 | let input_forbidden = HandlerInput::from(update_forbidden); 39 | assert!(!policy.is_granted(input_forbidden).await.unwrap()); 40 | } 41 | -------------------------------------------------------------------------------- /examples/app/predicate.rs: -------------------------------------------------------------------------------- 1 | //! # Predicates 2 | //! 3 | //! [`carapax::Predicate`] is a decorator that helps determine whether a handler should run or not. 4 | //! This is particularly useful for implementing functionalities like rate-limiting 5 | //! or restricting certain users from triggering a handler. 6 | //! 7 | //! A predicate handler must implement the [`carapax::Handler`] trait, 8 | //! and return a [`carapax::PredicateResult`] 9 | //! or a type that can be converted into it: 10 | //! 11 | //! | From | To | 12 | //! |---------------------|----------------------------------------| 13 | //! | `true` | [`carapax::PredicateResult::True`] | 14 | //! | `false` | [`carapax::PredicateResult::False`] | 15 | //! | `Result::Ok` | `PredicateResult::from::()` | 16 | //! | `Result::Err` | [`carapax::PredicateResult::Err`] | 17 | use carapax::{ 18 | Chain, PredicateExt, Ref, 19 | api::Client, 20 | types::{ChatPeerId, SendMessage, Text}, 21 | }; 22 | 23 | use crate::error::AppError; 24 | 25 | pub fn setup(chain: Chain) -> Chain { 26 | chain.with(pong.with_predicate(is_ping)) 27 | } 28 | 29 | async fn is_ping(text: Text) -> bool { 30 | text.data == "ping" 31 | } 32 | 33 | async fn pong(client: Ref, chat_id: ChatPeerId) -> Result<(), AppError> { 34 | let method = SendMessage::new(chat_id, "pong"); 35 | client.execute(method).await?; 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /src/core/predicate/ext.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | core::{ 3 | convert::TryFromInput, 4 | handler::Handler, 5 | predicate::{base::Predicate, command::CommandPredicate}, 6 | }, 7 | types::Command, 8 | }; 9 | 10 | /// Provides a shortcut for wrapping a [`Handler`] by a [`Predicate`]. 11 | pub trait PredicateExt: Sized { 12 | /// Shortcut to create a wrap a [`Handler`] with a [`Predicate`]. 13 | /// 14 | /// Example: `handler.predicate(predicate)`. 15 | /// 16 | /// # Arguments 17 | /// 18 | /// * `predicate` - A predicate handler. 19 | fn with_predicate(self, predicate: P) -> Predicate { 20 | Predicate::new(predicate, self) 21 | } 22 | } 23 | 24 | impl PredicateExt for H 25 | where 26 | H: Handler, 27 | HI: TryFromInput, 28 | { 29 | } 30 | 31 | /// Provides a shortcut for wrapping a [`Handler`] by a [`CommandPredicate`]. 32 | pub trait CommandExt: Sized { 33 | /// Shortcut to create a command handler. 34 | /// 35 | /// Example: `handler.command("/name")`. 36 | /// 37 | /// # Arguments 38 | /// 39 | /// * `name` - A name of a command with leading `/`. 40 | fn with_command>(self, name: S) -> Predicate { 41 | Predicate::new(CommandPredicate::new(name), self) 42 | } 43 | } 44 | 45 | impl CommandExt for H 46 | where 47 | H: Handler, 48 | I: TryFromInput, 49 | { 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches-ignore: 5 | - gh-pages 6 | pull_request: 7 | branches-ignore: 8 | - gh-pages 9 | schedule: 10 | - cron: '00 01 * * *' 11 | jobs: 12 | rustfmt: 13 | name: Rustfmt 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Install nightly toolchain 19 | uses: dtolnay/rust-toolchain@stable 20 | with: 21 | toolchain: nightly 22 | components: rustfmt 23 | - name: Check formatting 24 | run: cargo fmt --all -- --check 25 | clippy: 26 | name: Clippy 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | - name: Install stable toolchain 32 | uses: dtolnay/rust-toolchain@stable 33 | with: 34 | toolchain: stable 35 | components: clippy 36 | - name: Clippy 37 | run: cargo clippy --all-targets --all-features -- -D warnings 38 | test: 39 | name: Test 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v4 44 | - name: Install stable toolchain 45 | uses: dtolnay/rust-toolchain@stable 46 | with: 47 | toolchain: stable 48 | components: clippy 49 | - name: Generate Cargo.lock 50 | run: cargo generate-lockfile 51 | - name: Test 52 | run: cargo test --features=full 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CARAPAX 2 | 3 | [![CI](https://img.shields.io/github/actions/workflow/status/tg-rs/carapax/ci.yml?branch=master&style=flat-square)](https://github.com/tg-rs/carapax/actions/) 4 | [![Coverage](https://img.shields.io/codecov/c/github/tg-rs/carapax.svg?style=flat-square)](https://codecov.io/gh/tg-rs/carapax) 5 | [![Version](https://img.shields.io/crates/v/carapax.svg?style=flat-square)](https://crates.io/crates/carapax) 6 | 7 | A Telegram Bot framework based on [tgbot](https://github.com/tg-rs/tgbot). 8 | 9 | The name comes from [Carapace](https://en.wikipedia.org/wiki/Carapace) (carapax in latin). 10 | 11 | ## Installation 12 | 13 | ```toml 14 | [dependencies] 15 | carapax = "0.32.0" 16 | ``` 17 | 18 | ## Examples 19 | 20 | See examples in the [examples](https://github.com/tg-rs/carapax/tree/0.32.0/examples) directory. 21 | 22 | To run examples you need to create a `.env` file: 23 | 24 | ```sh 25 | cp sample.env .env 26 | ``` 27 | 28 | Don't forget to change the value of `CARAPAX_TOKEN` and other variables if required. 29 | 30 | ## Versioning 31 | 32 | This project adheres to [ZeroVer](https://0ver.org/). 33 | 34 | ## Links 35 | 36 | - [Latest Documentation](https://docs.rs/carapax) 37 | - [Master Documentation](https://tg-rs.github.io/carapax/carapax/) 38 | - [Telegram Chat](https://t.me/tgrsusers) 39 | - [Changelog](https://github.com/tg-rs/carapax/tree/0.32.0/CHANGELOG.md) 40 | - [Code of Conduct](https://github.com/tg-rs/carapax/tree/0.32.0/CODE_OF_CONDUCT.md). 41 | 42 | ## LICENSE 43 | 44 | The MIT License (MIT) 45 | -------------------------------------------------------------------------------- /tests/versions.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | use std::fs::read_to_string; 3 | 4 | use regex::Regex; 5 | use toml::Value; 6 | 7 | fn get_crate_version() -> String { 8 | let manifest = read_to_string("./Cargo.toml").expect("Failed to get Cargo.toml data"); 9 | let value: Value = toml::from_str(&manifest).expect("Failed to parse Cargo.toml"); 10 | let version = value["package"]["version"] 11 | .as_str() 12 | .expect("Can not get version from Cargo.toml"); 13 | String::from(version) 14 | } 15 | 16 | #[test] 17 | fn versions() { 18 | let version = get_crate_version(); 19 | for filename in &["./README.md"] { 20 | let readme = read_to_string(filename).unwrap(); 21 | for pattern in &[ 22 | r#"https://github\.com/tg-rs/carapax/tree/([\d\.]+)"#, 23 | r#"carapax\s?=\s?"([\d\.]+)""#, 24 | r#"carapax\s?=\s?\{\s?version\s?=\s?"([\d\.]+)""#, 25 | ] { 26 | let regex = Regex::new(pattern).expect("Can not create regex"); 27 | for (line_idx, line_data) in readme.lines().enumerate() { 28 | let line_number = line_idx + 1; 29 | if let Some(captures) = regex.captures(line_data) { 30 | let line_version = &captures[1]; 31 | assert_eq!( 32 | line_version, version, 33 | "Expects version {version} at {filename}:{line_number} '{line_data}', found {line_version}" 34 | ); 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ratelimit/predicate/direct/tests.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use super::*; 4 | 5 | #[tokio::test] 6 | async fn direct() { 7 | let handler = DirectRateLimitPredicate::discard(Quota::per_minute(nonzero!(1u32))); 8 | assert!( 9 | matches!(handler.handle(()).await, PredicateResult::True), 10 | "[direct/discard/1] true" 11 | ); 12 | assert!( 13 | matches!(handler.handle(()).await, PredicateResult::False), 14 | "[direct/discard/2] false" 15 | ); 16 | 17 | let handler = DirectRateLimitPredicate::wait( 18 | Quota::with_period(Duration::from_millis(100)) 19 | .unwrap() 20 | .allow_burst(nonzero!(1u32)), 21 | ); 22 | assert!( 23 | matches!(handler.handle(()).await, PredicateResult::True), 24 | "[direct/wait/1] continue" 25 | ); 26 | assert!( 27 | matches!(handler.handle(()).await, PredicateResult::True), 28 | "[direct/wait/2] continue" 29 | ); 30 | 31 | let handler = DirectRateLimitPredicate::wait_with_jitter( 32 | Quota::with_period(Duration::from_millis(100)) 33 | .unwrap() 34 | .allow_burst(nonzero!(1u32)), 35 | Jitter::new(Duration::from_secs(0), Duration::from_millis(100)), 36 | ); 37 | assert!( 38 | matches!(handler.handle(()).await, PredicateResult::True), 39 | "[direct/wait_with_jitter/1] continue" 40 | ); 41 | assert!( 42 | matches!(handler.handle(()).await, PredicateResult::True), 43 | "[direct/wait_with_jitter/2] continue" 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "carapax" 3 | description = "A Telegram Bot Framework" 4 | version = "0.32.0" 5 | authors = ["Ross Nomann "] 6 | edition = "2024" 7 | readme = "./README.md" 8 | license = "MIT" 9 | documentation = "https://docs.rs/carapax" 10 | repository = "https://github.com/tg-rs/carapax" 11 | 12 | [features] 13 | # include nothing by default 14 | default = [] 15 | 16 | # enable everything 17 | full = ["access", "dialogue", "ratelimit", "session-redis", "session-fs", "webhook"] 18 | 19 | access = ["dep:serde"] 20 | dialogue = ["session", "dep:serde"] 21 | ratelimit = ["dep:governor", "dep:nonzero_ext"] 22 | session = ["dep:seance"] 23 | session-fs = ["session", "seance?/fs-backend"] 24 | session-redis = ["session", "seance?/redis-backend"] 25 | webhook = ["tgbot/webhook"] 26 | 27 | [dependencies] 28 | futures-util = "0.3" 29 | governor = { version = "0.10", optional = true } 30 | log = "0.4" 31 | nonzero_ext = { version = "0.3", optional = true } 32 | seance = { version = "0.19", optional = true } 33 | serde = { version = "1", optional = true } 34 | tgbot = "0.40" 35 | tokio = "1" 36 | 37 | [dev-dependencies] 38 | dotenvy = "0.15" 39 | env_logger = "0.11" 40 | regex = "1" 41 | serde_json = "1" 42 | tempfile = "3" 43 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 44 | toml = "0.9" 45 | 46 | [lints.rust] 47 | missing_docs = "warn" 48 | unexpected_cfgs = { level = "allow", check-cfg = ['cfg(nightly)'] } 49 | 50 | [[example]] 51 | name = "app" 52 | required-features = ["full"] 53 | 54 | [package.metadata.docs.rs] 55 | all-features = true 56 | rustdoc-args = ["--cfg", "nightly"] 57 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | rust-overlay = { 6 | url = "github:oxalica/rust-overlay"; 7 | inputs.nixpkgs.follows = "nixpkgs"; 8 | }; 9 | }; 10 | outputs = 11 | inputs: 12 | inputs.flake-utils.lib.eachDefaultSystem ( 13 | system: 14 | let 15 | overlays = [ inputs.rust-overlay.overlays.default ]; 16 | pkgs = import inputs.nixpkgs { inherit system overlays; }; 17 | rust-dev = ( 18 | pkgs.rust-bin.selectLatestNightlyWith ( 19 | toolchain: 20 | toolchain.minimal.override { 21 | extensions = [ 22 | "rust-analyzer" 23 | "rust-src" 24 | "rustfmt" 25 | ]; 26 | } 27 | ) 28 | ); 29 | in 30 | { 31 | devShells.default = pkgs.mkShell { 32 | RUST_SRC_PATH = "${rust-dev}/lib/rustlib/src/rust/library"; 33 | buildInputs = [ 34 | (pkgs.lib.hiPrio ( 35 | pkgs.rust-bin.stable.latest.minimal.override { 36 | extensions = [ 37 | "rust-docs" 38 | "clippy" 39 | ]; 40 | } 41 | )) 42 | rust-dev 43 | ]; 44 | shellHook = '' 45 | export CARGO_HOME="$PWD/.cargo" 46 | export PATH="$CARGO_HOME/bin:$PATH" 47 | mkdir -p .cargo 48 | echo '*' > .cargo/.gitignore 49 | ''; 50 | }; 51 | } 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/dialogue/input.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use seance::{Session, backend::SessionBackend}; 4 | 5 | use crate::{ 6 | core::{HandlerInput, TryFromInput}, 7 | dialogue::{error::DialogueError, state::DialogueState}, 8 | }; 9 | 10 | /// Represents an input for a dialogue handler. 11 | /// 12 | /// The input provides access to the dialogue state. 13 | /// When included in a list of handler arguments, 14 | /// [`TryFromInput`] will automatically handle the extraction of the input. 15 | #[derive(Clone)] 16 | pub struct DialogueInput 17 | where 18 | S: DialogueState, 19 | B: SessionBackend, 20 | { 21 | /// Dialogue state 22 | pub state: S, 23 | session_backend: PhantomData, 24 | } 25 | 26 | impl TryFromInput for DialogueInput 27 | where 28 | S: DialogueState + Send, 29 | B: SessionBackend + Send + 'static, 30 | { 31 | type Error = DialogueError; 32 | 33 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 34 | match >::try_from_input(input.clone()).await? { 35 | Some(ref mut session) => { 36 | let session_key = S::session_key(); 37 | let state = session 38 | .get(session_key) 39 | .await 40 | .map_err(DialogueError::LoadState)? 41 | .unwrap_or_default(); 42 | Ok(Some(Self { 43 | state, 44 | session_backend: PhantomData, 45 | })) 46 | } 47 | None => unreachable!("TryFromInput implementation for Session never returns None"), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/core/app/tests.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | use tokio::sync::Mutex; 4 | 5 | use crate::core::{chain::Chain, context::Ref}; 6 | 7 | use super::*; 8 | 9 | fn create_update() -> Update { 10 | serde_json::from_value(serde_json::json!({ 11 | "update_id": 1, 12 | "message": { 13 | "message_id": 1111, 14 | "date": 0, 15 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 16 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 17 | "text": "test message from private chat" 18 | } 19 | })) 20 | .unwrap() 21 | } 22 | 23 | #[derive(Clone)] 24 | struct Counter { 25 | value: Arc>, 26 | } 27 | 28 | #[derive(Debug)] 29 | struct ExampleError; 30 | 31 | impl fmt::Display for ExampleError { 32 | fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result { 33 | write!(out, "Example error") 34 | } 35 | } 36 | 37 | impl Error for ExampleError {} 38 | 39 | async fn success_handler(counter: Ref) { 40 | *counter.value.lock().await += 1; 41 | } 42 | 43 | async fn error_handler(counter: Ref) -> Result<(), ExampleError> { 44 | *counter.value.lock().await += 1; 45 | Err(ExampleError) 46 | } 47 | 48 | #[tokio::test] 49 | async fn handle() { 50 | let counter = Counter { 51 | value: Arc::new(Mutex::new(0)), 52 | }; 53 | 54 | let mut context = Context::default(); 55 | context.insert(counter.clone()); 56 | 57 | let chain = Chain::all().with(success_handler).with(error_handler); 58 | 59 | let app = App::new(context, chain); 60 | 61 | let update = create_update(); 62 | app.handle(update).await; 63 | 64 | assert_eq!(*counter.value.lock().await, 2); 65 | } 66 | -------------------------------------------------------------------------------- /src/core/error/tests.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt, sync::Arc}; 2 | 3 | use tokio::sync::Mutex; 4 | 5 | use crate::{core::handler::HandlerInput, types::Update}; 6 | 7 | use super::*; 8 | 9 | #[derive(Clone)] 10 | struct Condition { 11 | value: Arc>, 12 | } 13 | 14 | #[derive(Debug)] 15 | struct ExampleError; 16 | 17 | impl fmt::Display for ExampleError { 18 | fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result { 19 | write!(out, "Example error") 20 | } 21 | } 22 | 23 | impl Error for ExampleError {} 24 | 25 | impl ErrorHandler for Condition { 26 | async fn handle(&self, err: HandlerError) -> HandlerError { 27 | let value = self.value.clone(); 28 | *value.lock().await = true; 29 | err 30 | } 31 | } 32 | 33 | async fn handler(_: ()) -> Result<(), ExampleError> { 34 | Err(ExampleError) 35 | } 36 | 37 | fn create_update() -> Update { 38 | serde_json::from_value(serde_json::json!({ 39 | "update_id": 1, 40 | "message": { 41 | "message_id": 1111, 42 | "date": 0, 43 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 44 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 45 | "text": "test message from private chat" 46 | } 47 | })) 48 | .unwrap() 49 | } 50 | 51 | #[tokio::test] 52 | async fn error_decorator() { 53 | let condition = Condition { 54 | value: Arc::new(Mutex::new(false)), 55 | }; 56 | let handler = ErrorDecorator::new(condition.clone(), handler); 57 | let update = create_update(); 58 | let input = HandlerInput::from(update); 59 | let result = handler.handle(input).await; 60 | assert!(result.is_err()); 61 | assert!(*condition.value.lock().await) 62 | } 63 | -------------------------------------------------------------------------------- /src/core/context/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | collections::HashMap, 4 | ops::Deref, 5 | }; 6 | 7 | #[cfg(test)] 8 | mod tests; 9 | 10 | /// A shared state storage for use in [`crate::Handler`] trait implementations. 11 | #[derive(Debug, Default)] 12 | pub struct Context { 13 | items: HashMap>, 14 | } 15 | 16 | impl Context { 17 | /// Returns an immutable reference to the value of type `T`. 18 | pub fn get(&self) -> Option<&T> { 19 | self.items 20 | .get(&TypeId::of::()) 21 | .and_then(|boxed| boxed.downcast_ref()) 22 | } 23 | 24 | /// Inserts a value of type `T` into the context. 25 | /// 26 | /// # Arguments 27 | /// 28 | /// * `value` - The value to insert. 29 | /// 30 | /// Returns a previously inserted value if it exists. 31 | pub fn insert(&mut self, value: T) -> Option { 32 | self.items 33 | .insert(TypeId::of::(), Box::new(value)) 34 | .and_then(|boxed| >::downcast(boxed).ok().map(|boxed| *boxed)) 35 | } 36 | } 37 | 38 | /// A link to a value of type `T` stored in the [`Context`]. 39 | /// 40 | /// The link implements [`crate::TryFromInput`] trait, 41 | /// enabling you to use `Ref` as the type of an argument in your 42 | /// [`crate::Handler`] trait implementations. 43 | /// 44 | /// Keep in mind that each time a handler is called with `Ref` as an argument, 45 | /// the underlying value is cloned. 46 | #[derive(Clone)] 47 | pub struct Ref(pub T); 48 | 49 | impl Ref { 50 | pub(super) fn new(object: T) -> Self { 51 | Self(object) 52 | } 53 | } 54 | 55 | impl Deref for Ref { 56 | type Target = T; 57 | 58 | fn deref(&self) -> &Self::Target { 59 | &self.0 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/access/policy/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, error::Error, future::Future, sync::Arc}; 2 | 3 | use futures_util::future::{Ready, ok}; 4 | 5 | use crate::{access::rule::AccessRule, core::HandlerInput}; 6 | 7 | #[cfg(test)] 8 | mod tests; 9 | 10 | /// Decides whether [`HandlerInput`] should be processed or not. 11 | pub trait AccessPolicy: Send { 12 | /// An error that may be returned by the [`Self::is_granted`] method. 13 | type Error: Error + Send; 14 | /// A future representing the result of the [`Self::is_granted`] method. 15 | type Future: Future> + Send; 16 | 17 | /// Determines if access is granted for the given input. 18 | /// 19 | /// # Arguments 20 | /// 21 | /// * `input` - The input to be processed by the access policy. 22 | /// 23 | /// The [`Self::Future`] resolves to `true` if access is allowed, and `false` otherwise. 24 | fn is_granted(&self, input: HandlerInput) -> Self::Future; 25 | } 26 | 27 | /// In-memory access policy implementation. 28 | /// 29 | /// If there are no rules found, [`AccessPolicy::is_granted`] will return `false`. 30 | /// You can use [`AccessRule::allow_all`] as the last rule to modify this behavior. 31 | #[derive(Default, Clone)] 32 | pub struct InMemoryAccessPolicy { 33 | rules: Arc>, 34 | } 35 | 36 | impl From for InMemoryAccessPolicy 37 | where 38 | T: IntoIterator, 39 | { 40 | fn from(rules: T) -> Self { 41 | Self { 42 | rules: Arc::new(rules.into_iter().collect()), 43 | } 44 | } 45 | } 46 | 47 | impl AccessPolicy for InMemoryAccessPolicy { 48 | type Error = Infallible; 49 | type Future = Ready>; 50 | 51 | fn is_granted(&self, input: HandlerInput) -> Self::Future { 52 | let mut result = false; 53 | let rules = Arc::clone(&self.rules); 54 | for rule in rules.iter() { 55 | if rule.accepts(&input.update) { 56 | result = rule.is_granted(); 57 | log::info!("Found rule: {rule:?} (is_granted={result:?})"); 58 | break; 59 | } 60 | } 61 | ok(result) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/app/ratelimit.rs: -------------------------------------------------------------------------------- 1 | //! # Ratelimit 2 | //! 3 | //! Carapax provides a ratelimit support using [governor](https://crates.io/crates/governor) crate. 4 | //! 5 | //! There are two type of predicates: [`DirectRateLimitPredicate`] and [`KeyedRateLimitPredicate`]. 6 | //! 7 | //! Direct is used when you need to apply ratelimit for all incoming updates. 8 | //! Keyed - when you need to limit updates per chat and/or user. 9 | //! 10 | //! Once the limit is reached you can choose to either [discard](carapax::ratelimit::MethodDiscard) the updates, 11 | //! or [wait](carapax::ratelimit::MethodWait) for the next available time slot. 12 | //! 13 | //! Both types of predicates can be used [with](Jitter) 14 | //! or [without](carapax::ratelimit::NoJitter) jitter. 15 | //! 16 | //! Note that you need to enable the `ratelimit` feature in `Cargo.toml`. 17 | use std::time::Duration; 18 | 19 | use carapax::{ 20 | Chain, PredicateExt, 21 | ratelimit::{ 22 | DirectRateLimitPredicate, Jitter, KeyChat, KeyChatUser, KeyUser, KeyedRateLimitPredicate, Quota, nonzero, 23 | }, 24 | }; 25 | 26 | pub fn setup(chain: Chain, strategy: &str) -> Chain { 27 | let quota = Quota::with_period(Duration::from_secs(5)) 28 | .expect("Failed to create quota") 29 | .allow_burst(nonzero!(1u32)); 30 | let jitter = Jitter::up_to(Duration::from_secs(5)); 31 | let result = Chain::once(); 32 | match strategy { 33 | "direct_discard" => result.with(chain.with_predicate(DirectRateLimitPredicate::discard(quota))), 34 | "direct_wait" => result.with(chain.with_predicate(DirectRateLimitPredicate::wait(quota))), 35 | "direct_wait_with_jitter" => { 36 | result.with(chain.with_predicate(DirectRateLimitPredicate::wait_with_jitter(quota, jitter))) 37 | } 38 | "keyed_discard" => result.with(chain.with_predicate(>::discard(quota))), 39 | "keyed_wait" => result.with(chain.with_predicate(>::wait(quota))), 40 | "keyed_wait_with_jitter" => result.with(chain.with_predicate( 41 | >::wait_with_jitter(quota, jitter), 42 | )), 43 | key => panic!("Unknown ratelimit strategy: {key}"), 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ratelimit/key.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, hash::Hash}; 2 | 3 | use crate::{ 4 | core::{HandlerInput, TryFromInput}, 5 | types::{ChatPeerId, UserPeerId}, 6 | }; 7 | 8 | /// Represents a key for a keyed rate limiter. 9 | pub trait Key: Clone + Eq + Hash + TryFromInput {} 10 | 11 | /// Represents a rate limit key for a chat. 12 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 13 | pub struct KeyChat(ChatPeerId); 14 | 15 | impl From for KeyChat 16 | where 17 | T: Into, 18 | { 19 | fn from(value: T) -> Self { 20 | Self(value.into()) 21 | } 22 | } 23 | 24 | impl TryFromInput for KeyChat { 25 | type Error = Infallible; 26 | 27 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 28 | Ok(input.update.get_chat_id().map(Self)) 29 | } 30 | } 31 | 32 | impl Key for KeyChat {} 33 | 34 | /// Represents a rate limit key for a user. 35 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 36 | pub struct KeyUser(UserPeerId); 37 | 38 | impl From for KeyUser 39 | where 40 | T: Into, 41 | { 42 | fn from(value: T) -> Self { 43 | Self(value.into()) 44 | } 45 | } 46 | 47 | impl TryFromInput for KeyUser { 48 | type Error = Infallible; 49 | 50 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 51 | Ok(input.update.get_user().map(|user| Self(user.id))) 52 | } 53 | } 54 | 55 | impl Key for KeyUser {} 56 | 57 | /// Represents a rate limit key for a user in a chat. 58 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 59 | pub struct KeyChatUser(ChatPeerId, UserPeerId); 60 | 61 | impl From<(A, B)> for KeyChatUser 62 | where 63 | A: Into, 64 | B: Into, 65 | { 66 | fn from((chat_id, user_id): (A, B)) -> Self { 67 | Self(chat_id.into(), user_id.into()) 68 | } 69 | } 70 | 71 | impl TryFromInput for KeyChatUser { 72 | type Error = Infallible; 73 | 74 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 75 | Ok(match input.update.get_chat_id() { 76 | Some(chat_id) => input.update.get_user().map(|user| Self(chat_id, user.id)), 77 | _ => None, 78 | }) 79 | } 80 | } 81 | 82 | impl Key for KeyChatUser {} 83 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1762111121, 24 | "narHash": "sha256-4vhDuZ7OZaZmKKrnDpxLZZpGIJvAeMtK6FKLJYUtAdw=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "b3d51a0365f6695e7dd5cdf3e180604530ed33b4", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs", 41 | "rust-overlay": "rust-overlay" 42 | } 43 | }, 44 | "rust-overlay": { 45 | "inputs": { 46 | "nixpkgs": [ 47 | "nixpkgs" 48 | ] 49 | }, 50 | "locked": { 51 | "lastModified": 1762396738, 52 | "narHash": "sha256-BarSecuxtzp1boERdABLkkoxQTi6s/V33lJwUbWLrLY=", 53 | "owner": "oxalica", 54 | "repo": "rust-overlay", 55 | "rev": "c63598992afd54d215d54f2b764adc0484c2b159", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "oxalica", 60 | "repo": "rust-overlay", 61 | "type": "github" 62 | } 63 | }, 64 | "systems": { 65 | "locked": { 66 | "lastModified": 1681028828, 67 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 68 | "owner": "nix-systems", 69 | "repo": "default", 70 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "nix-systems", 75 | "repo": "default", 76 | "type": "github" 77 | } 78 | } 79 | }, 80 | "root": "root", 81 | "version": 7 82 | } 83 | -------------------------------------------------------------------------------- /src/core/predicate/base/tests.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt, sync::Arc}; 2 | 3 | use tokio::sync::Mutex; 4 | 5 | use crate::{ 6 | core::context::Ref, 7 | types::{Integer, User}, 8 | }; 9 | 10 | use super::*; 11 | 12 | #[tokio::test] 13 | async fn decorator() { 14 | let condition = Ref::new(Condition::new()); 15 | let handler = Predicate::new(has_access, process_user); 16 | let user_1 = create_user(1); 17 | let user_2 = create_user(2); 18 | let user_3 = create_user(3); 19 | 20 | assert!(matches!( 21 | handler.handle(((user_1.clone(),), (user_1, condition.clone()))).await, 22 | PredicateOutput::True(Ok(())) 23 | )); 24 | assert!(*condition.value.lock().await); 25 | condition.set(false).await; 26 | 27 | assert!(matches!( 28 | handler.handle(((user_2.clone(),), (user_2, condition.clone()))).await, 29 | PredicateOutput::False 30 | )); 31 | assert!(!*condition.value.lock().await); 32 | condition.set(false).await; 33 | 34 | assert!(matches!( 35 | handler.handle(((user_3.clone(),), (user_3, condition.clone()))).await, 36 | PredicateOutput::True(Err(_)) 37 | )); 38 | assert!(*condition.value.lock().await); 39 | condition.set(false).await; 40 | } 41 | 42 | fn create_user(id: Integer) -> User { 43 | User::new(id, format!("test #{id}"), false) 44 | } 45 | 46 | async fn has_access(user: User) -> PredicateResult { 47 | if user.id != 2 { 48 | PredicateResult::True 49 | } else { 50 | PredicateResult::False 51 | } 52 | } 53 | 54 | async fn process_user(user: User, condition: Ref) -> Result<(), ProcessError> { 55 | condition.set(true).await; 56 | log::info!("Processing user: {user:?}"); 57 | if user.id == 3 { Err(ProcessError) } else { Ok(()) } 58 | } 59 | 60 | #[derive(Clone)] 61 | struct Condition { 62 | value: Arc>, 63 | } 64 | 65 | impl Condition { 66 | fn new() -> Self { 67 | Self { 68 | value: Arc::new(Mutex::new(false)), 69 | } 70 | } 71 | 72 | async fn set(&self, value: bool) { 73 | *self.value.lock().await = value; 74 | } 75 | } 76 | 77 | #[derive(Debug)] 78 | struct ProcessError; 79 | 80 | impl Error for ProcessError {} 81 | 82 | impl fmt::Display for ProcessError { 83 | fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result { 84 | write!(out, "Process error") 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/core/app/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{marker::PhantomData, sync::Arc}; 2 | 3 | use crate::{ 4 | core::{ 5 | context::Context, 6 | convert::TryFromInput, 7 | handler::{Handler, HandlerInput, IntoHandlerResult}, 8 | }, 9 | handler::UpdateHandler, 10 | types::Update, 11 | }; 12 | 13 | #[cfg(test)] 14 | mod tests; 15 | 16 | /// The main entry point. 17 | /// 18 | /// Implements the [`UpdateHandler`] trait, so you can use it 19 | /// in [`crate::handler::LongPoll`] or [crate::handler::WebhookServer]. 20 | /// 21 | /// Wraps an update into the [`HandlerInput`] struct and passes it to the inner handler. 22 | /// 23 | /// Use [`crate::Chain`] struct to configure multiple handlers. 24 | #[derive(Clone)] 25 | pub struct App { 26 | context: Arc, 27 | handler: H, 28 | handler_input: PhantomData, 29 | } 30 | 31 | impl App 32 | where 33 | H: Handler, 34 | HI: TryFromInput, 35 | HI::Error: 'static, 36 | HO: IntoHandlerResult, 37 | { 38 | /// Creates a new `App`. 39 | /// 40 | /// # Arguments 41 | /// 42 | /// * `context` - A context responsible for storing shared state. 43 | /// * `handler` - A handler responsible for processing updates. 44 | pub fn new(context: Context, handler: H) -> Self { 45 | Self { 46 | context: Arc::new(context), 47 | handler, 48 | handler_input: PhantomData, 49 | } 50 | } 51 | 52 | async fn handle_update(&self, update: Update) { 53 | let input = HandlerInput { 54 | update, 55 | context: self.context.clone(), 56 | }; 57 | let handler = self.handler.clone(); 58 | let input = match HI::try_from_input(input).await { 59 | Ok(Some(input)) => input, 60 | Ok(None) => return, 61 | Err(err) => { 62 | log::error!("Failed to convert input: {err}"); 63 | return; 64 | } 65 | }; 66 | let future = handler.handle(input); 67 | if let Err(err) = future.await.into_result() { 68 | log::error!("An error has occurred: {err}"); 69 | } 70 | } 71 | } 72 | 73 | impl UpdateHandler for App 74 | where 75 | H: Handler + Sync + 'static, 76 | HI: TryFromInput + Sync + 'static, 77 | HI::Error: 'static, 78 | HO: IntoHandlerResult + Send + 'static, 79 | { 80 | async fn handle(&self, update: Update) { 81 | self.handle_update(update).await 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/access/predicate/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::{ 4 | access::policy::AccessPolicy, 5 | core::{Handler, HandlerInput}, 6 | types::{ChatPeerId, ChatUsername, Update, UserPeerId, UserUsername}, 7 | }; 8 | 9 | #[cfg(test)] 10 | mod tests; 11 | 12 | /// An access predicate for protecting a [`Handler`] with a specified [`AccessPolicy`]. 13 | #[derive(Clone)] 14 | pub struct AccessPredicate

{ 15 | policy: P, 16 | } 17 | 18 | impl

AccessPredicate

{ 19 | /// Creates a new `AccessPredicate`. 20 | /// 21 | /// # Arguments 22 | /// 23 | /// * `policy` - An access policy. 24 | pub fn new(policy: P) -> Self { 25 | Self { policy } 26 | } 27 | } 28 | 29 | impl

Handler for AccessPredicate

30 | where 31 | P: AccessPolicy + Clone + Sync + 'static, 32 | { 33 | type Output = Result; 34 | 35 | async fn handle(&self, input: HandlerInput) -> Self::Output { 36 | let debug_principal = DebugPrincipal::from(&input.update); 37 | let value = self.policy.is_granted(input).await; 38 | match value { 39 | Ok(value) => { 40 | log::info!( 41 | "Access for {:?} is {}", 42 | debug_principal, 43 | if value { "granted" } else { "forbidden" } 44 | ); 45 | Ok(value) 46 | } 47 | Err(err) => Err(err), 48 | } 49 | } 50 | } 51 | 52 | struct DebugPrincipal { 53 | user_id: Option, 54 | user_username: Option, 55 | chat_id: Option, 56 | chat_username: Option, 57 | } 58 | 59 | impl From<&Update> for DebugPrincipal { 60 | fn from(update: &Update) -> Self { 61 | DebugPrincipal { 62 | user_id: update.get_user_id(), 63 | user_username: update.get_user_username().cloned(), 64 | chat_id: update.get_chat_id(), 65 | chat_username: update.get_chat_username().cloned(), 66 | } 67 | } 68 | } 69 | 70 | impl fmt::Debug for DebugPrincipal { 71 | fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result { 72 | let mut debug_struct = out.debug_struct("Principal"); 73 | macro_rules! debug_field { 74 | ($field_name:ident) => { 75 | if let Some(ref $field_name) = self.$field_name { 76 | debug_struct.field(stringify!($field_name), &$field_name); 77 | } 78 | }; 79 | } 80 | debug_field!(user_id); 81 | debug_field!(user_username); 82 | debug_field!(chat_id); 83 | debug_field!(chat_username); 84 | debug_struct.finish() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/session/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, error::Error, fmt}; 2 | 3 | use seance::backend::SessionBackend; 4 | pub use seance::{Session, SessionCollector, SessionCollectorHandle, SessionError, SessionManager, backend}; 5 | 6 | use crate::{ 7 | core::{HandlerInput, TryFromInput}, 8 | types::{ChatPeerId, UserPeerId}, 9 | }; 10 | 11 | impl TryFromInput for Session 12 | where 13 | B: SessionBackend + Send + 'static, 14 | { 15 | type Error = CreateSessionError; 16 | 17 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 18 | match input.context.get::>() { 19 | Some(manager) => match SessionId::try_from_input(input.clone()).await { 20 | Ok(Some(session_id)) => { 21 | let session = manager.get_session(session_id.0); 22 | Ok(Some(session)) 23 | } 24 | Ok(None) => Err(CreateSessionError::SessionIdNotFound), 25 | Err(_) => unreachable!(), 26 | }, 27 | None => Err(CreateSessionError::ManagerNotFound), 28 | } 29 | } 30 | } 31 | 32 | /// Represents an ID of a session. 33 | pub struct SessionId(String); 34 | 35 | impl SessionId { 36 | /// Creates a new `SessionID`. 37 | /// 38 | /// # Arguments 39 | /// 40 | /// * `chat_id` - ID of a chat. 41 | /// * `user_id` - ID of a user. 42 | pub fn new(chat_id: ChatPeerId, user_id: UserPeerId) -> Self { 43 | Self(format!("{chat_id}-{user_id}")) 44 | } 45 | } 46 | 47 | impl From for String { 48 | fn from(value: SessionId) -> Self { 49 | value.0 50 | } 51 | } 52 | 53 | impl TryFromInput for SessionId { 54 | type Error = Infallible; 55 | 56 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 57 | let chat_id = input.update.get_chat_id(); 58 | let user_id = input.update.get_user_id(); 59 | Ok(if let (Some(chat_id), Some(user_id)) = (chat_id, user_id) { 60 | Some(SessionId::new(chat_id, user_id)) 61 | } else { 62 | None 63 | }) 64 | } 65 | } 66 | 67 | /// An error when creating a session. 68 | #[derive(Debug)] 69 | pub enum CreateSessionError { 70 | /// Session manager not found in the [`crate::Context`]. 71 | ManagerNotFound, 72 | /// Could not create a session ID. 73 | /// 74 | /// Chat ID or User ID is missing in the [`crate::types::Update`]. 75 | SessionIdNotFound, 76 | } 77 | 78 | impl fmt::Display for CreateSessionError { 79 | fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result { 80 | use self::CreateSessionError::*; 81 | write!( 82 | out, 83 | "{}", 84 | match self { 85 | ManagerNotFound => "Session manager not found in context", 86 | SessionIdNotFound => "Could not create session ID: chat or user ID is missing", 87 | } 88 | ) 89 | } 90 | } 91 | 92 | impl Error for CreateSessionError {} 93 | -------------------------------------------------------------------------------- /src/access/predicate/tests.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | use futures_util::future::{Ready, err, ok}; 4 | 5 | use crate::{ 6 | core::HandlerInput, 7 | types::{Integer, Update}, 8 | }; 9 | 10 | use super::*; 11 | 12 | #[derive(Debug)] 13 | struct ErrorMock; 14 | 15 | impl Error for ErrorMock {} 16 | 17 | impl fmt::Display for ErrorMock { 18 | fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | write!(out, "error") 20 | } 21 | } 22 | 23 | #[derive(Clone)] 24 | struct PolicyMock; 25 | 26 | impl AccessPolicy for PolicyMock { 27 | type Error = ErrorMock; 28 | type Future = Ready>; 29 | 30 | fn is_granted(&self, input: HandlerInput) -> Self::Future { 31 | match input.update.get_user().map(|user| Integer::from(user.id)) { 32 | Some(1) => ok(true), 33 | Some(2) => ok(false), 34 | Some(_) => err(ErrorMock), 35 | None => err(ErrorMock), 36 | } 37 | } 38 | } 39 | 40 | #[tokio::test] 41 | async fn access_predicate() { 42 | let policy = PolicyMock; 43 | let predicate = AccessPredicate::new(policy); 44 | 45 | let update_granted: Update = serde_json::from_value(serde_json::json!( 46 | { 47 | "update_id": 1, 48 | "message": { 49 | "message_id": 1111, 50 | "date": 0, 51 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 52 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 53 | "text": "test", 54 | } 55 | } 56 | )) 57 | .unwrap(); 58 | let input_granted = HandlerInput::from(update_granted); 59 | let result = predicate.handle(input_granted).await; 60 | assert!(result.unwrap()); 61 | 62 | let update_forbidden: Update = serde_json::from_value(serde_json::json!( 63 | { 64 | "update_id": 1, 65 | "message": { 66 | "message_id": 1111, 67 | "date": 0, 68 | "from": {"id": 2, "is_bot": false, "first_name": "test"}, 69 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 70 | "text": "test", 71 | } 72 | } 73 | )) 74 | .unwrap(); 75 | let input_forbidden = HandlerInput::from(update_forbidden); 76 | let result = predicate.handle(input_forbidden).await; 77 | assert!(!result.unwrap()); 78 | 79 | let update_error: Update = serde_json::from_value(serde_json::json!( 80 | { 81 | "update_id": 1, 82 | "message": { 83 | "message_id": 1111, 84 | "date": 0, 85 | "from": {"id": 3, "is_bot": false, "first_name": "test"}, 86 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 87 | "text": "test", 88 | } 89 | } 90 | )) 91 | .unwrap(); 92 | let input_error = HandlerInput::from(update_error); 93 | let result = predicate.handle(input_error).await; 94 | assert!(result.is_err()); 95 | } 96 | -------------------------------------------------------------------------------- /src/core/chain/tests.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | use tokio::sync::Mutex; 4 | 5 | use crate::{ 6 | core::context::{Context, Ref}, 7 | types::Update, 8 | }; 9 | 10 | use super::*; 11 | 12 | #[derive(Clone)] 13 | struct UpdateStore(Arc>>); 14 | 15 | impl UpdateStore { 16 | fn new() -> Self { 17 | Self(Arc::new(Mutex::new(Vec::new()))) 18 | } 19 | 20 | async fn push(&self, update: Update) { 21 | self.0.lock().await.push(update) 22 | } 23 | 24 | async fn count(&self) -> usize { 25 | self.0.lock().await.len() 26 | } 27 | } 28 | 29 | async fn handler_ok(store: Ref, update: Update) { 30 | store.push(update).await; 31 | } 32 | 33 | async fn handler_error(store: Ref, update: Update) -> HandlerResult { 34 | store.push(update).await; 35 | Err(HandlerError::new(ErrorMock)) 36 | } 37 | 38 | #[derive(Debug)] 39 | struct ErrorMock; 40 | 41 | impl Error for ErrorMock {} 42 | 43 | impl fmt::Display for ErrorMock { 44 | fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result { 45 | write!(out, "Test error") 46 | } 47 | } 48 | 49 | fn create_update() -> Update { 50 | serde_json::from_value(serde_json::json!({ 51 | "update_id": 1, 52 | "message": { 53 | "message_id": 1111, 54 | "date": 0, 55 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 56 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 57 | "text": "test message from private chat" 58 | } 59 | })) 60 | .unwrap() 61 | } 62 | 63 | #[tokio::test] 64 | async fn chain() { 65 | macro_rules! assert_handle { 66 | ($strategy:ident, $count:expr, $($handler:expr),*) => {{ 67 | let mut context = Context::default(); 68 | context.insert(UpdateStore::new()); 69 | let context = Arc::new(context); 70 | let mut chain = Chain::$strategy(); 71 | $(chain = chain.with($handler);)* 72 | let update = create_update(); 73 | let input = HandlerInput { 74 | context: context.clone(), 75 | update 76 | }; 77 | let result = chain.handle(input).await; 78 | let count = context.get::().unwrap().count().await; 79 | assert_eq!(count, $count); 80 | result 81 | }}; 82 | } 83 | 84 | let result = assert_handle!(all, 2, handler_ok, handler_error, handler_ok); 85 | assert!(result.is_err()); 86 | let result = assert_handle!(once, 1, handler_ok, handler_error, handler_ok); 87 | assert!(matches!(result, Ok(()))); 88 | 89 | let result = assert_handle!(all, 1, handler_error, handler_ok); 90 | assert!(result.is_err()); 91 | let result = assert_handle!(once, 1, handler_error, handler_ok); 92 | assert!(result.is_err()); 93 | 94 | let result = assert_handle!(all, 2, handler_ok, handler_ok); 95 | assert!(matches!(result, Ok(()))); 96 | let result = assert_handle!(once, 1, handler_ok, handler_ok); 97 | assert!(matches!(result, Ok(()))); 98 | } 99 | -------------------------------------------------------------------------------- /src/dialogue/predicate.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use seance::{Session, backend::SessionBackend}; 4 | 5 | use crate::{ 6 | core::{Handler, HandlerError, HandlerInput, PredicateResult, TryFromInput}, 7 | dialogue::state::DialogueState, 8 | }; 9 | 10 | /// A predicate for dialogue 11 | /// 12 | /// Allows to decide whether a dialogue should start or not. 13 | /// The dialogue handler runs only when his state exists in a session 14 | /// or when the inner predicate returns `true`. 15 | pub struct DialoguePredicate { 16 | session_backend: PhantomData, 17 | predicate: P, 18 | predicate_input: PhantomData, 19 | handler_state: PhantomData, 20 | } 21 | 22 | impl DialoguePredicate { 23 | /// Creates a new `DialoguePredicate`. 24 | /// 25 | /// # Arguments 26 | /// 27 | /// * `predicate` - The inner predicate (e.g. command). 28 | pub fn new(predicate: P) -> Self { 29 | Self { 30 | session_backend: PhantomData, 31 | predicate, 32 | predicate_input: PhantomData, 33 | handler_state: PhantomData, 34 | } 35 | } 36 | } 37 | 38 | impl Clone for DialoguePredicate 39 | where 40 | P: Clone, 41 | { 42 | fn clone(&self) -> Self { 43 | Self { 44 | session_backend: self.session_backend, 45 | predicate: self.predicate.clone(), 46 | predicate_input: self.predicate_input, 47 | handler_state: self.handler_state, 48 | } 49 | } 50 | } 51 | 52 | impl Handler for DialoguePredicate 53 | where 54 | B: SessionBackend + Send + Sync + 'static, 55 | P: Handler + Sync + 'static, 56 | PI: TryFromInput + Sync, 57 | PI::Error: 'static, 58 | PO: Into, 59 | HS: DialogueState + Send + Sync, 60 | { 61 | type Output = PredicateResult; 62 | 63 | async fn handle(&self, input: HandlerInput) -> Self::Output { 64 | let mut session = match >::try_from_input(input.clone()).await { 65 | Ok(Some(session)) => session, 66 | Ok(None) => unreachable!("TryFromInput implementation for Session never returns None"), 67 | Err(err) => return PredicateResult::Err(HandlerError::new(err)), 68 | }; 69 | let session_key = HS::session_key(); 70 | match session.get::<&str, HS>(&session_key).await { 71 | Ok(Some(_)) => { 72 | // We have dialogue state in session, so we must run dialog handler 73 | PredicateResult::True 74 | } 75 | Ok(None) => { 76 | // Dialogue state not found in session, let's check predicate 77 | match PI::try_from_input(input.clone()).await { 78 | Ok(Some(predicate_input)) => self.predicate.handle(predicate_input).await.into(), 79 | Ok(None) => PredicateResult::False, 80 | Err(err) => PredicateResult::Err(HandlerError::new(err)), 81 | } 82 | } 83 | Err(err) => PredicateResult::Err(HandlerError::new(err)), 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/access/principal/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::types::Integer; 2 | 3 | use super::*; 4 | 5 | #[test] 6 | fn convert_principal() { 7 | assert_eq!(Principal::from(UserId::from(1)), Principal::user(1)); 8 | assert_eq!(Principal::from(ChatId::from(1)), Principal::chat(1)); 9 | assert_eq!( 10 | Principal::from((ChatId::from(1), UserId::from(1))), 11 | Principal::chat_user(ChatId::from(1), UserId::from(1)) 12 | ); 13 | } 14 | 15 | #[test] 16 | fn user_id_resolver() { 17 | let user_id = UserId::from(1); 18 | assert!(user_id.accepts(&UpdateBuilder::default().user_id(1).build())); 19 | assert!(!user_id.accepts(&UpdateBuilder::default().user_id(2).build())); 20 | 21 | let user_id = UserId::from("test"); 22 | assert!(user_id.accepts(&UpdateBuilder::default().user_username("test").build())); 23 | assert!(!user_id.accepts(&UpdateBuilder::default().user_username("username_user").build())); 24 | assert!(!user_id.accepts(&UpdateBuilder::default().build())); 25 | } 26 | 27 | #[test] 28 | fn chat_id_resolver() { 29 | let chat_id = ChatId::from(1); 30 | assert!(chat_id.accepts(&UpdateBuilder::default().chat_id(1).build())); 31 | assert!(!chat_id.accepts(&UpdateBuilder::default().chat_id(2).build())); 32 | 33 | let chat_id = ChatId::from("test"); 34 | assert!(chat_id.accepts(&UpdateBuilder::default().chat_username("test").build())); 35 | assert!(!chat_id.accepts(&UpdateBuilder::default().chat_username("username_chat").build())); 36 | assert!(!chat_id.accepts(&UpdateBuilder::default().build())); 37 | } 38 | 39 | #[test] 40 | fn chat_id_and_user_id_resolvers() { 41 | let principal = Principal::from((ChatId::from(1), UserId::from(1))); 42 | assert_eq!(principal, Principal::chat_user(ChatId::from(1), UserId::from(1))); 43 | assert!(principal.accepts(&UpdateBuilder::default().user_id(1).chat_id(1).build())); 44 | assert!(!principal.accepts(&UpdateBuilder::default().build())); 45 | } 46 | 47 | #[derive(Default)] 48 | struct UpdateBuilder { 49 | user_id: Integer, 50 | user_username: Option, 51 | chat_id: Integer, 52 | chat_username: Option, 53 | } 54 | 55 | impl UpdateBuilder { 56 | fn user_id(mut self, user_id: Integer) -> Self { 57 | self.user_id = user_id; 58 | self 59 | } 60 | 61 | fn user_username(mut self, user_username: T) -> Self 62 | where 63 | T: Into, 64 | { 65 | self.user_username = Some(user_username.into()); 66 | self 67 | } 68 | 69 | fn chat_id(mut self, chat_id: Integer) -> Self { 70 | self.chat_id = chat_id; 71 | self 72 | } 73 | 74 | fn chat_username(mut self, chat_username: T) -> Self 75 | where 76 | T: Into, 77 | { 78 | self.chat_username = Some(chat_username.into()); 79 | self 80 | } 81 | 82 | fn build(self) -> Update { 83 | serde_json::from_value::(serde_json::json!({ 84 | "update_id": 1, 85 | "message": { 86 | "message_id": 1, 87 | "date": 1, 88 | "from": {"id": self.user_id, "is_bot": false, "first_name": "test", "username": self.user_username}, 89 | "chat": {"id": self.chat_id, "type": "supergroup", "title": "test", "username": self.chat_username}, 90 | "text": "test" 91 | } 92 | })) 93 | .unwrap() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/core/error/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{future::Future, marker::PhantomData}; 2 | 3 | use crate::core::{ 4 | convert::TryFromInput, 5 | handler::{Handler, HandlerError, HandlerInput, HandlerResult, IntoHandlerResult}, 6 | }; 7 | 8 | #[cfg(test)] 9 | mod tests; 10 | 11 | /// Allows to process an error returned by a handler. 12 | pub struct ErrorDecorator { 13 | error_handler: E, 14 | handler: H, 15 | handler_input: PhantomData, 16 | } 17 | 18 | impl ErrorDecorator { 19 | /// Creates a new `ErrorDecorator`. 20 | /// 21 | /// # Arguments 22 | /// 23 | /// * `error_handler` - A handler for errors returned by a decorated handler. 24 | /// * `handler` - The handler to be decorated. 25 | pub fn new(error_handler: E, handler: H) -> Self { 26 | Self { 27 | error_handler, 28 | handler, 29 | handler_input: PhantomData, 30 | } 31 | } 32 | } 33 | 34 | impl Clone for ErrorDecorator 35 | where 36 | E: Clone, 37 | H: Clone, 38 | { 39 | fn clone(&self) -> Self { 40 | Self { 41 | error_handler: self.error_handler.clone(), 42 | handler: self.handler.clone(), 43 | handler_input: PhantomData, 44 | } 45 | } 46 | } 47 | 48 | impl Handler for ErrorDecorator 49 | where 50 | E: ErrorHandler + Clone + Sync + 'static, 51 | H: Handler + Sync + 'static, 52 | HI: TryFromInput + Sync, 53 | HI::Error: 'static, 54 | H::Output: IntoHandlerResult, 55 | { 56 | type Output = HandlerResult; 57 | 58 | async fn handle(&self, input: HandlerInput) -> Self::Output { 59 | let future = HI::try_from_input(input); 60 | match future.await { 61 | Ok(Some(input)) => match self.handler.handle(input).await.into_result() { 62 | Err(err) => Err(self.error_handler.handle(err).await), 63 | result => result, 64 | }, 65 | Ok(None) => Ok(()), 66 | Err(err) => Err(self.error_handler.handle(HandlerError::new(err)).await), 67 | } 68 | } 69 | } 70 | 71 | /// Allows to process errors returned by handlers. 72 | pub trait ErrorHandler: Send { 73 | /// Handles a errors. 74 | /// 75 | /// # Arguments 76 | /// 77 | /// * `err` - An error to handle. 78 | fn handle(&self, err: HandlerError) -> impl Future + Send; 79 | } 80 | 81 | impl ErrorHandler for H 82 | where 83 | H: Fn(HandlerError) -> F + Send + Sync, 84 | F: Future + Send, 85 | { 86 | async fn handle(&self, err: HandlerError) -> HandlerError { 87 | (self)(err).await 88 | } 89 | } 90 | 91 | /// Provides a shortcut for creating error decorator. 92 | pub trait ErrorExt: Sized { 93 | /// A shortcut to create a new error decorator. 94 | /// 95 | /// Example: `handler.on_error(error_handler)` 96 | /// 97 | /// # Arguments 98 | /// 99 | /// * `error_handler` - An error handler. 100 | fn on_error(self, error_handler: E) -> ErrorDecorator { 101 | ErrorDecorator::new(error_handler, self) 102 | } 103 | } 104 | 105 | impl ErrorExt for H 106 | where 107 | H: Handler, 108 | HI: TryFromInput, 109 | { 110 | } 111 | -------------------------------------------------------------------------------- /examples/app/dialogue.rs: -------------------------------------------------------------------------------- 1 | //! # Dialogues 2 | //! 3 | //! A dialogue is a stateful handler that receives the current state and returns a new state. 4 | //! 5 | //! The dialogue handler operates similarly to a regular handler but returns a [`DialogueResult`]. 6 | //! To obtain the state from the session, you can use the [`DialogueInput`] struct, 7 | //! which implements the [`carapax::TryFromInput`] trait 8 | //! and can be used as an argument for your handler. 9 | //! 10 | //! The state must implement the [`DialogueState`] trait, 11 | //! and each dialogue must have a unique name, 12 | //! defining a value for the session key to store the state. 13 | //! 14 | //! The state can be converted into the [`DialogueResult`]. 15 | //! Thus, you can return `state.into()` instead of `DialogueResult::Next(state)`. 16 | //! 17 | //! You need to wrap the dialogue handler with the [`carapax::dialogue::DialogueDecorator`] 18 | //! and the [`carapax::dialogue::DialoguePredicate`]. 19 | //! 20 | //! Predicate decides should the dialogue handler run or not, 21 | //! decorator saves the state and converts a result of the dialogue 22 | //! into the [`carapax::HandlerResult`]. 23 | //! 24 | //! Note that you need to enable the `session` and `dialogue` features in `Cargo.toml`. 25 | use serde::{Deserialize, Serialize}; 26 | 27 | use carapax::{ 28 | Chain, CommandPredicate, Ref, 29 | api::Client, 30 | dialogue::{DialogueExt, DialogueInput, DialogueResult, DialogueState}, 31 | session::backend::fs::FilesystemBackend, 32 | types::{ChatPeerId, SendMessage, Text}, 33 | }; 34 | 35 | use crate::error::AppError; 36 | 37 | pub fn setup(chain: Chain) -> Chain { 38 | chain.with(example_dialogue.with_dialogue::(CommandPredicate::new("/dialogue"))) 39 | } 40 | 41 | type ExampleDialogueInput = DialogueInput; 42 | 43 | #[derive(Clone, Default, Deserialize, Serialize)] 44 | enum ExampleDialogueState { 45 | #[default] 46 | Start, 47 | FirstName, 48 | LastName { 49 | first_name: String, 50 | }, 51 | } 52 | 53 | impl DialogueState for ExampleDialogueState { 54 | fn dialogue_name() -> &'static str { 55 | "example" 56 | } 57 | } 58 | 59 | async fn example_dialogue( 60 | client: Ref, 61 | chat_id: ChatPeerId, 62 | input: ExampleDialogueInput, 63 | text: Text, 64 | ) -> Result, AppError> { 65 | let state = match input.state { 66 | ExampleDialogueState::Start => { 67 | client 68 | .execute(SendMessage::new(chat_id, "What is your first name?")) 69 | .await?; 70 | ExampleDialogueState::FirstName 71 | } 72 | ExampleDialogueState::FirstName => { 73 | let first_name = text.data.clone(); 74 | client 75 | .execute(SendMessage::new(chat_id, "What is your last name?")) 76 | .await?; 77 | ExampleDialogueState::LastName { first_name } 78 | } 79 | ExampleDialogueState::LastName { first_name } => { 80 | let last_name = &text.data; 81 | client 82 | .execute(SendMessage::new( 83 | chat_id, 84 | format!("Your name is: {first_name} {last_name}"), 85 | )) 86 | .await?; 87 | return Ok(DialogueResult::Exit); 88 | } 89 | }; 90 | Ok(state.into()) 91 | } 92 | -------------------------------------------------------------------------------- /examples/app/main.rs: -------------------------------------------------------------------------------- 1 | //! The example demonstrates the advanced usage of the framework covering all available features. 2 | //! 3 | //! For detailed information about each feature, refer to the documentation for each respective module. 4 | use std::{env, time::Duration}; 5 | 6 | use dotenvy::dotenv; 7 | use seance::{SessionCollector, SessionManager, backend::fs::FilesystemBackend}; 8 | use tempfile::tempdir; 9 | 10 | use carapax::{App, Chain, Context, ErrorExt, HandlerError, api::Client, handler::LongPoll}; 11 | 12 | mod access; 13 | mod command; 14 | mod dialogue; 15 | mod error; 16 | mod predicate; 17 | mod ratelimit; 18 | mod session; 19 | 20 | #[tokio::main] 21 | async fn main() { 22 | dotenv().ok(); 23 | env_logger::init(); 24 | 25 | let client = Client::new(get_env("CARAPAX_TOKEN")).expect("Failed to create API"); 26 | 27 | let mut context = Context::default(); 28 | context.insert(client.clone()); 29 | 30 | let session_backend = create_session_backend(); 31 | spawn_session_collector(session_backend.clone()); 32 | let session_manager = SessionManager::new(session_backend); 33 | context.insert(session_manager); 34 | 35 | // Chain is a handler which allows to execute several handlers, one after another. 36 | // Handlers will run in same order as added. 37 | // If a handler returns `Err(_)`, all the subsequent handlers will not run. 38 | let mut chain = Chain::all(); 39 | if let Ok(username) = env::var("CARAPAX_ACCESS_USERNAME") { 40 | chain = access::setup(chain, &username); 41 | } 42 | chain = command::setup(chain); 43 | chain = dialogue::setup(chain); 44 | chain = predicate::setup(chain); 45 | chain = session::setup(chain); 46 | 47 | if let Ok(ratelimit_strategy) = env::var("CARAPAX_RATE_LIMIT_STRATEGY") { 48 | chain = ratelimit::setup(chain, &ratelimit_strategy); 49 | } 50 | 51 | // By default, `App` logs an error produced by a handler. 52 | // Here we use a `ErrorDecorator` which allows to configure a custom handler 53 | // for errors returned by the handler. 54 | let handler = chain.on_error(error_handler); 55 | 56 | let app = App::new(context, handler); 57 | LongPoll::new(client, app).run().await 58 | } 59 | 60 | fn get_env(s: &str) -> String { 61 | env::var(s).unwrap_or_else(|_| panic!("{s} is not set")) 62 | } 63 | 64 | fn create_session_backend() -> FilesystemBackend { 65 | let tmpdir = tempdir().expect("Failed to create temp directory"); 66 | log::info!("Session directory: {}", tmpdir.path().display()); 67 | FilesystemBackend::new(tmpdir.path()) 68 | } 69 | 70 | /// Spawns a garbage collector for expired sessions 71 | fn spawn_session_collector(backend: FilesystemBackend) { 72 | let gc_period = get_env("CARAPAX_SESSION_GC_PERIOD"); 73 | let gc_period = Duration::from_secs( 74 | gc_period 75 | .parse::() 76 | .expect("CARAPAX_SESSION_GC_PERIOD must be integer"), 77 | ); // period between GC calls 78 | 79 | let session_lifetime = get_env("CARAPAX_SESSION_LIFETIME"); 80 | let session_lifetime = Duration::from_secs( 81 | session_lifetime 82 | .parse::() 83 | .expect("CARAPAX_SESSION_LIFETIME must be integer"), 84 | ); // how long session lives 85 | 86 | // spawn GC to remove old sessions 87 | let mut collector = SessionCollector::new(backend, gc_period, session_lifetime); 88 | tokio::spawn(async move { collector.run().await }); 89 | } 90 | 91 | async fn error_handler(err: HandlerError) -> HandlerError { 92 | log::error!("Got an error in custom error handler: {err}"); 93 | err 94 | } 95 | -------------------------------------------------------------------------------- /src/access/principal/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::types::{ChatId, Update, UserId}; 4 | 5 | #[cfg(test)] 6 | mod tests; 7 | 8 | /// Represents a principal entity that decides whether 9 | /// an [`crate::access::AccessRule`] should accept an update. 10 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 11 | pub enum Principal { 12 | /// Accepts all updates without any specific conditions. 13 | All, 14 | /// Accepts updates only from a specified user. 15 | User(UserId), 16 | /// Accepts updates only from a specified chat. 17 | Chat(ChatId), 18 | /// Accepts updates only from a user within a specific chat. 19 | ChatUser(ChatId, UserId), 20 | } 21 | 22 | impl From for Principal { 23 | fn from(user_id: UserId) -> Principal { 24 | Principal::User(user_id) 25 | } 26 | } 27 | 28 | impl From for Principal { 29 | fn from(chat_id: ChatId) -> Principal { 30 | Principal::Chat(chat_id) 31 | } 32 | } 33 | 34 | impl From<(ChatId, UserId)> for Principal { 35 | fn from((chat_id, user_id): (ChatId, UserId)) -> Principal { 36 | Principal::ChatUser(chat_id, user_id) 37 | } 38 | } 39 | 40 | impl Principal { 41 | /// Creates a principal for a specific user. 42 | /// 43 | /// # Arguments 44 | /// 45 | /// * `user_id` - ID of the user. 46 | pub fn user(user_id: T) -> Self 47 | where 48 | T: Into, 49 | { 50 | Principal::User(user_id.into()) 51 | } 52 | 53 | /// Creates a principal for a specific chat. 54 | /// 55 | /// # Arguments 56 | /// 57 | /// * `chat_id` - ID of the chat. 58 | pub fn chat(chat_id: T) -> Self 59 | where 60 | T: Into, 61 | { 62 | Principal::Chat(chat_id.into()) 63 | } 64 | 65 | /// Creates a principal for a user within a specific chat. 66 | /// 67 | /// # Arguments 68 | /// 69 | /// * `chat_id` - ID of the chat. 70 | /// * `user_id` - ID of the user. 71 | pub fn chat_user(chat_id: A, user_id: B) -> Self 72 | where 73 | A: Into, 74 | B: Into, 75 | { 76 | Principal::ChatUser(chat_id.into(), user_id.into()) 77 | } 78 | 79 | pub(super) fn accepts(&self, update: &Update) -> bool { 80 | match self { 81 | Principal::User(user_id) => user_id.accepts(update), 82 | Principal::Chat(chat_id) => chat_id.accepts(update), 83 | Principal::ChatUser(chat_id, user_id) => chat_id.accepts(update) && user_id.accepts(update), 84 | Principal::All => true, 85 | } 86 | } 87 | } 88 | 89 | trait Resolver { 90 | fn accepts(&self, update: &Update) -> bool; 91 | } 92 | 93 | impl Resolver for UserId { 94 | fn accepts(&self, update: &Update) -> bool { 95 | match self { 96 | UserId::Id(user_id) => update.get_user().map(|u| u.id == *user_id), 97 | UserId::Username(username) => update 98 | .get_user() 99 | .and_then(|u| u.username.as_ref().map(|x| x == username)), 100 | } 101 | .unwrap_or(false) 102 | } 103 | } 104 | 105 | impl Resolver for ChatId { 106 | fn accepts(&self, update: &Update) -> bool { 107 | match self { 108 | ChatId::Id(chat_id) => update.get_chat_id().map(|x| x == *chat_id), 109 | ChatId::Username(chat_username) => update.get_chat_username().map(|x| x == chat_username), 110 | } 111 | .unwrap_or(false) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/core/predicate/base/mod.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::core::{ 4 | convert::TryFromInput, 5 | handler::{Handler, HandlerError, HandlerResult, IntoHandlerResult}, 6 | predicate::result::PredicateResult, 7 | }; 8 | 9 | #[cfg(test)] 10 | mod tests; 11 | 12 | /// Decorates a handler with a predicate, allowing control over whether the handler should run. 13 | /// 14 | /// The predicate must return a [`PredicateResult`]. 15 | pub struct Predicate { 16 | predicate: P, 17 | predicate_input: PhantomData, 18 | handler: H, 19 | handler_input: PhantomData, 20 | } 21 | 22 | impl Predicate { 23 | /// Creates a new `Predicate`. 24 | /// 25 | /// # Arguments 26 | /// 27 | /// * `predicate` - A predicate handler. 28 | /// * `handler` - A handler to be decorated. 29 | pub fn new(predicate: P, handler: H) -> Self { 30 | Self { 31 | predicate, 32 | predicate_input: PhantomData, 33 | handler, 34 | handler_input: PhantomData, 35 | } 36 | } 37 | } 38 | 39 | impl Handler<(PI, HI)> for Predicate 40 | where 41 | P: Handler + Sync + 'static, 42 | P::Output: Into, 43 | PI: TryFromInput + Sync + 'static, 44 | PI::Error: 'static, 45 | H: Handler + Sync + 'static, 46 | H::Output: IntoHandlerResult, 47 | HI: TryFromInput + Sync + 'static, 48 | HI::Error: 'static, 49 | { 50 | type Output = PredicateOutput; 51 | 52 | async fn handle(&self, (predicate_input, handler_input): (PI, HI)) -> Self::Output { 53 | let predicate_result = self.predicate.handle(predicate_input).await.into(); 54 | match predicate_result { 55 | PredicateResult::True => self.handler.handle(handler_input).await.into_result().into(), 56 | _ => predicate_result.into(), 57 | } 58 | } 59 | } 60 | 61 | impl Clone for Predicate 62 | where 63 | P: Clone, 64 | H: Clone, 65 | { 66 | fn clone(&self) -> Self { 67 | Predicate { 68 | predicate: self.predicate.clone(), 69 | predicate_input: self.predicate_input, 70 | handler: self.handler.clone(), 71 | handler_input: self.handler_input, 72 | } 73 | } 74 | } 75 | 76 | /// Output of the predicate decorator 77 | pub enum PredicateOutput { 78 | /// A decorated handler has been executed. 79 | True(HandlerResult), 80 | /// A decorated handler has not been executed. 81 | False, 82 | /// An error occurred during a predicate execution. 83 | Err(HandlerError), 84 | } 85 | 86 | impl From for PredicateOutput { 87 | fn from(result: PredicateResult) -> Self { 88 | match result { 89 | PredicateResult::True => PredicateOutput::True(Ok(())), 90 | PredicateResult::False => PredicateOutput::False, 91 | PredicateResult::Err(err) => PredicateOutput::Err(err), 92 | } 93 | } 94 | } 95 | 96 | impl From for PredicateOutput { 97 | fn from(result: HandlerResult) -> Self { 98 | PredicateOutput::True(result) 99 | } 100 | } 101 | 102 | impl IntoHandlerResult for PredicateOutput { 103 | fn into_result(self) -> HandlerResult { 104 | match self { 105 | PredicateOutput::True(result) => result, 106 | PredicateOutput::False => Ok(()), 107 | PredicateOutput::Err(err) => Err(err), 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/ratelimit/predicate/direct/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | pub use governor::{Jitter, Quota}; 4 | use governor::{ 5 | RateLimiter, 6 | clock::DefaultClock, 7 | middleware::NoOpMiddleware, 8 | state::{InMemoryState, NotKeyed}, 9 | }; 10 | pub use nonzero_ext::nonzero; 11 | 12 | use crate::{ 13 | core::{Handler, PredicateResult}, 14 | ratelimit::{ 15 | jitter::NoJitter, 16 | method::{MethodDiscard, MethodWait}, 17 | }, 18 | }; 19 | 20 | #[cfg(test)] 21 | mod tests; 22 | 23 | /// A predicate with a direct rate limiter. 24 | /// 25 | /// Use this predicate when you need to limit all updates. 26 | #[derive(Clone)] 27 | pub struct DirectRateLimitPredicate { 28 | limiter: Arc>, 29 | jitter: J, 30 | _method: M, 31 | } 32 | 33 | impl DirectRateLimitPredicate { 34 | /// Creates a new `DirectRateLimitPredicate` with the discard method. 35 | /// 36 | /// The predicate will stop update propagation when the rate limit is reached. 37 | /// 38 | /// # Arguments 39 | /// 40 | /// * `quota` - A rate limiting quota. 41 | pub fn discard(quota: Quota) -> Self { 42 | Self { 43 | limiter: Arc::new(RateLimiter::direct(quota)), 44 | jitter: NoJitter, 45 | _method: MethodDiscard, 46 | } 47 | } 48 | } 49 | 50 | impl DirectRateLimitPredicate { 51 | /// Creates a new `DirectRateLimitPredicate` with the wait method. 52 | /// 53 | /// The predicate will pause update propagation when the rate limit is reached. 54 | /// 55 | /// # Arguments 56 | /// 57 | /// * `quota` - A rate limiting quota. 58 | pub fn wait(quota: Quota) -> Self { 59 | Self { 60 | limiter: Arc::new(RateLimiter::direct(quota)), 61 | jitter: NoJitter, 62 | _method: MethodWait, 63 | } 64 | } 65 | } 66 | 67 | impl DirectRateLimitPredicate { 68 | /// Creates a new `DirectRateLimitPredicate` with the wait method and jitter. 69 | /// 70 | /// Predicate will pause update propagation when the rate limit is reached. 71 | /// 72 | /// # Arguments 73 | /// 74 | /// * `quota` - A rate limiting quota. 75 | /// * `jitter` - An interval specification for deviating from the nominal wait time. 76 | pub fn wait_with_jitter(quota: Quota, jitter: Jitter) -> Self { 77 | Self { 78 | limiter: Arc::new(RateLimiter::direct(quota)), 79 | jitter, 80 | _method: MethodWait, 81 | } 82 | } 83 | } 84 | 85 | impl Handler<()> for DirectRateLimitPredicate { 86 | type Output = PredicateResult; 87 | 88 | async fn handle(&self, (): ()) -> Self::Output { 89 | match self.limiter.check() { 90 | Ok(_) => PredicateResult::True, 91 | Err(_) => { 92 | log::info!("DirectRateLimitPredicate: update discarded"); 93 | PredicateResult::False 94 | } 95 | } 96 | } 97 | } 98 | 99 | impl Handler<()> for DirectRateLimitPredicate { 100 | type Output = PredicateResult; 101 | 102 | async fn handle(&self, (): ()) -> Self::Output { 103 | self.limiter.until_ready().await; 104 | PredicateResult::True 105 | } 106 | } 107 | 108 | impl Handler<()> for DirectRateLimitPredicate { 109 | type Output = PredicateResult; 110 | 111 | async fn handle(&self, (): ()) -> Self::Output { 112 | self.limiter.until_ready_with_jitter(self.jitter).await; 113 | PredicateResult::True 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/dialogue/decorator.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, marker::PhantomData}; 2 | 3 | use seance::{Session, backend::SessionBackend}; 4 | 5 | use crate::{ 6 | core::{Handler, HandlerError, HandlerInput, HandlerResult, TryFromInput}, 7 | dialogue::{error::DialogueError, result::DialogueResult, state::DialogueState}, 8 | }; 9 | 10 | /// A decorator for dialogue handlers. 11 | /// 12 | /// An inner handler must return a [`DialogueResult`]. 13 | /// The decorator will automatically load and save the state of the dialogue returned in the result. 14 | /// 15 | /// Note that you need to register 16 | /// a [`crate::session::SessionManager`] instance in the [`crate::Context`]. 17 | /// Otherwise the decorator will return an error indicating that 18 | /// the session manager is not registered. 19 | pub struct DialogueDecorator { 20 | session_backend: PhantomData, 21 | handler: H, 22 | handler_input: PhantomData, 23 | handler_state: PhantomData, 24 | } 25 | 26 | impl DialogueDecorator { 27 | /// Creates a new `DialogueDecorator`. 28 | /// 29 | /// # Arguments 30 | /// 31 | /// * `handler` - A dialogue handler. 32 | pub fn new(handler: H) -> Self { 33 | Self { 34 | session_backend: PhantomData, 35 | handler, 36 | handler_input: PhantomData, 37 | handler_state: PhantomData, 38 | } 39 | } 40 | } 41 | 42 | impl Clone for DialogueDecorator 43 | where 44 | H: Clone, 45 | { 46 | fn clone(&self) -> Self { 47 | DialogueDecorator { 48 | session_backend: PhantomData, 49 | handler: self.handler.clone(), 50 | handler_input: self.handler_input, 51 | handler_state: self.handler_state, 52 | } 53 | } 54 | } 55 | 56 | impl Handler for DialogueDecorator 57 | where 58 | B: SessionBackend + Send + Sync + 'static, 59 | H: Handler> + Sync + 'static, 60 | HI: TryFromInput + Sync, 61 | HI::Error: 'static, 62 | HR: Into>, 63 | HS: DialogueState + Send + Sync, 64 | HE: Error + Send + 'static, 65 | { 66 | type Output = HandlerResult; 67 | 68 | async fn handle(&self, input: HandlerInput) -> Self::Output { 69 | let handler_input = match HI::try_from_input(input.clone()).await { 70 | Ok(Some(input)) => input, 71 | Ok(None) => return Err(HandlerError::new(DialogueError::ConvertHandlerInput)), 72 | Err(err) => return Err(HandlerError::new(err)), 73 | }; 74 | let result = match self.handler.handle(handler_input).await { 75 | Ok(result) => result.into(), 76 | Err(err) => return Err(HandlerError::new(err)), 77 | }; 78 | 79 | let mut session = match >::try_from_input(input).await { 80 | Ok(Some(session)) => session, 81 | Ok(None) => unreachable!("TryFromInput implementation for Session never returns None"), 82 | Err(err) => return Err(HandlerError::new(err)), 83 | }; 84 | let session_key = HS::session_key(); 85 | 86 | match result { 87 | DialogueResult::Next(state) => { 88 | if let Err(err) = session.set(session_key, &state).await { 89 | return Err(HandlerError::new(err)); 90 | } 91 | } 92 | DialogueResult::Exit => { 93 | // Explicitly remove the state from the session to make sure that the dialog will not run again. 94 | if let Err(err) = session.remove(session_key).await { 95 | return Err(HandlerError::new(err)); 96 | } 97 | } 98 | } 99 | 100 | Ok(()) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/core/handler/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt, future::Future, sync::Arc}; 2 | 3 | use crate::{ 4 | core::{context::Context, convert::TryFromInput}, 5 | types::Update, 6 | }; 7 | 8 | #[cfg(test)] 9 | mod tests; 10 | 11 | /// Allows to handle a specific [`HandlerInput`]. 12 | pub trait Handler: Clone + Send 13 | where 14 | I: TryFromInput, 15 | { 16 | /// A future output returned by [`Self::handle`] method. 17 | /// 18 | /// Use [`HandlerResult`] or any type that can be converted into it 19 | /// if you want to use the handler in [`crate::App`]. 20 | /// 21 | /// It is possible to use any other type, e.g. if you want to use it in a decorator. 22 | /// But finally you need to convert it into [`HandlerResult`]. 23 | type Output: Send; 24 | 25 | /// Handles a specific input. 26 | /// 27 | /// # Arguments 28 | /// 29 | /// * `input` - The input to handle. 30 | /// 31 | /// See [`TryFromInput`] trait implementations for a list of supported types. 32 | fn handle(&self, input: I) -> impl Future + Send; 33 | } 34 | 35 | macro_rules! impl_fn { 36 | ($($I:ident),+) => { 37 | #[allow(non_snake_case)] 38 | impl Handler<($($I,)+)> for X 39 | where 40 | X: Fn($($I,)+) -> R + Clone + Send + Sync, 41 | ($($I,)+): TryFromInput, 42 | R: Future + Send, 43 | R::Output: Send 44 | { 45 | type Output = R::Output; 46 | 47 | async fn handle(&self, ($($I,)+): ($($I,)+)) -> Self::Output { 48 | (self)($($I,)+).await 49 | } 50 | } 51 | }; 52 | } 53 | 54 | impl_fn!(A); 55 | impl_fn!(A, B); 56 | impl_fn!(A, B, C); 57 | impl_fn!(A, B, C, D); 58 | impl_fn!(A, B, C, D, E); 59 | impl_fn!(A, B, C, D, E, F); 60 | impl_fn!(A, B, C, D, E, F, G); 61 | impl_fn!(A, B, C, D, E, F, G, H); 62 | impl_fn!(A, B, C, D, E, F, G, H, I); 63 | impl_fn!(A, B, C, D, E, F, G, H, I, J); 64 | 65 | /// An input for a [`Handler`] trait implementations. 66 | #[derive(Clone, Debug)] 67 | pub struct HandlerInput { 68 | /// An Update received from Telegram API. 69 | pub update: Update, 70 | /// A context with shared state. 71 | pub context: Arc, 72 | } 73 | 74 | impl From for HandlerInput { 75 | fn from(update: Update) -> Self { 76 | HandlerInput { 77 | update, 78 | context: Arc::new(Default::default()), 79 | } 80 | } 81 | } 82 | 83 | /// An error returned by a [`Handler`] trait implementation. 84 | pub struct HandlerError(Box); 85 | 86 | impl HandlerError { 87 | /// Creates a new `HandlerError`. 88 | /// 89 | /// # Arguments 90 | /// 91 | /// * `err` - The actual error. 92 | pub fn new(err: E) -> Self 93 | where 94 | E: Error + Send + 'static, 95 | { 96 | Self(Box::new(err)) 97 | } 98 | } 99 | 100 | impl fmt::Debug for HandlerError { 101 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 102 | fmt::Debug::fmt(&self.0, f) 103 | } 104 | } 105 | 106 | impl fmt::Display for HandlerError { 107 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 108 | fmt::Display::fmt(&self.0, f) 109 | } 110 | } 111 | 112 | impl Error for HandlerError { 113 | fn source(&self) -> Option<&(dyn Error + 'static)> { 114 | self.0.source() 115 | } 116 | } 117 | 118 | /// A result returned by a [`Handler`] trait implementation. 119 | pub type HandlerResult = Result<(), HandlerError>; 120 | 121 | /// Converts objects into the [`HandlerResult`]. 122 | pub trait IntoHandlerResult { 123 | /// Returns the converted object. 124 | fn into_result(self) -> HandlerResult; 125 | } 126 | 127 | impl IntoHandlerResult for () { 128 | fn into_result(self) -> HandlerResult { 129 | Ok(self) 130 | } 131 | } 132 | 133 | impl IntoHandlerResult for Result<(), E> 134 | where 135 | E: Error + Send + 'static, 136 | { 137 | fn into_result(self) -> HandlerResult { 138 | self.map_err(HandlerError::new) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/dialogue/tests.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, sync::Arc}; 2 | 3 | use seance::Session; 4 | use serde::{Deserialize, Serialize}; 5 | use tempfile::tempdir; 6 | 7 | use crate::{ 8 | core::{Chain, Context, Handler, HandlerInput, PredicateOutput, TryFromInput}, 9 | session::{SessionManager, backend::fs::FilesystemBackend}, 10 | types::Text, 11 | }; 12 | 13 | use super::*; 14 | 15 | #[derive(Clone, Copy, Default, Deserialize, Serialize)] 16 | enum State { 17 | #[default] 18 | Start, 19 | Step, 20 | } 21 | 22 | impl DialogueState for State { 23 | fn dialogue_name() -> &'static str { 24 | "test" 25 | } 26 | } 27 | 28 | type InputMock = DialogueInput; 29 | 30 | async fn dialogue_predicate(text: Text) -> bool { 31 | text.data == "start" 32 | } 33 | 34 | async fn dialogue_handler(input: InputMock) -> Result, Infallible> { 35 | Ok(match input.state { 36 | State::Start => State::Step.into(), 37 | State::Step => DialogueResult::Exit, 38 | }) 39 | } 40 | 41 | fn create_input(context: Arc, text: &str) -> HandlerInput { 42 | let update = serde_json::from_value(serde_json::json!({ 43 | "update_id": 1, 44 | "message": { 45 | "message_id": 1111, 46 | "date": 0, 47 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 48 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 49 | "text": text 50 | } 51 | })) 52 | .unwrap(); 53 | HandlerInput { context, update } 54 | } 55 | 56 | fn create_context() -> Arc { 57 | let tmpdir = tempdir().expect("Failed to create temp directory"); 58 | let backend = FilesystemBackend::new(tmpdir.path()); 59 | let mut context = Context::default(); 60 | let session_manager = SessionManager::new(backend); 61 | context.insert(session_manager); 62 | Arc::new(context) 63 | } 64 | 65 | async fn get_session(input: HandlerInput) -> Session { 66 | >::try_from_input(input.clone()) 67 | .await 68 | .expect("Failed to get session") 69 | .expect("Session is None") 70 | } 71 | 72 | #[tokio::test] 73 | async fn dialogue() { 74 | let context = create_context(); 75 | let handler = dialogue_handler.with_dialogue::(dialogue_predicate); 76 | 77 | let input = create_input(context.clone(), "start"); 78 | 79 | let session = &mut get_session(input.clone()).await; 80 | let session_key = State::session_key(); 81 | 82 | assert!(matches!( 83 | handler.handle((input.clone(), input)).await, 84 | PredicateOutput::True(Ok(())) 85 | )); 86 | let state: Option = session.get(&session_key).await.expect("Failed to get state"); 87 | assert!(matches!(state, Some(State::Step))); 88 | 89 | let input = create_input(context.clone(), "step"); 90 | assert!(matches!( 91 | handler.handle((input.clone(), input)).await, 92 | PredicateOutput::True(Ok(())) 93 | )); 94 | let state: Option = session.get(&session_key).await.expect("Failed to get state"); 95 | assert!(state.is_none()); 96 | } 97 | 98 | async fn skip_handler(mut session: Session) { 99 | session 100 | .set("is_skipped", &true) 101 | .await 102 | .expect("Failed to set is_skipped key") 103 | } 104 | 105 | #[tokio::test] 106 | async fn dialogue_in_chain_skipped() { 107 | let context = create_context(); 108 | let handler = dialogue_handler.with_dialogue::(dialogue_predicate); 109 | let chain = Chain::once().with(handler).with(skip_handler); 110 | let input = create_input(context.clone(), "skipped"); 111 | let mut session = get_session(input.clone()).await; 112 | chain.handle(input).await.expect("Failed to run chain handler"); 113 | let is_skipped: bool = session 114 | .get("is_skipped") 115 | .await 116 | .expect("Failed to get is_skipped key") 117 | .expect("is_skipped key is not set"); 118 | assert!(is_skipped); 119 | } 120 | -------------------------------------------------------------------------------- /examples/app/session.rs: -------------------------------------------------------------------------------- 1 | //! # Session 2 | //! 3 | //! Sessions provide a mechanism for storing a user-specific state in a storage such as filesystem or redis. 4 | //! 5 | //! Carapax utilizes the [seance](http://crates.io/crates/seance) crate for session support. 6 | //! The required types are reexported from Carapax, 7 | //! eliminating the need to add `seance` to your `Cargo.toml`. 8 | //! 9 | //! Every session is identified by a [SessionId](carapax::session::SessionId) struct, 10 | //! which includes both a chat ID and a user ID. 11 | //! 12 | //! You can either get [`Session`] directly from the manager, 13 | //! or use [`carapax::TryFromInput`] and specify `session: Session` in handler arguments. 14 | //! Where `B` is a [session backend](carapax::session::backend). 15 | //! In both cases make sure that session manager is added to the context. 16 | //! 17 | //! 18 | //! If an update lacks `chat_id` and/or `user_id`, 19 | //! and the handler contains [`Session`] or [`carapax::session::SessionId`] in its arguments, 20 | //! the handler will not be execute. 21 | //! In such cases, you must obtain the session from the manager manually. 22 | //! 23 | //! Note that you need to enable either the `session-fs` or `session-redis` feature in `Cargo.toml`. 24 | //! Alternatively, use the `session` feature if you have your own backend. 25 | use carapax::{ 26 | Chain, CommandExt, Ref, 27 | api::Client, 28 | session::{Session, backend::fs::FilesystemBackend}, 29 | types::{ChatPeerId, Command, SendMessage}, 30 | }; 31 | 32 | use crate::error::AppError; 33 | 34 | const KEY: &str = "example-session-key"; 35 | 36 | pub fn setup(chain: Chain) -> Chain { 37 | chain 38 | .with(get.with_command("/s_get")) 39 | .with(set.with_command("/s_set")) 40 | .with(expire.with_command("/s_expire")) 41 | .with(reset.with_command("/s_del")) 42 | } 43 | 44 | async fn get( 45 | client: Ref, 46 | mut session: Session, 47 | chat_id: ChatPeerId, 48 | ) -> Result<(), AppError> { 49 | log::info!("/s_get"); 50 | let val: Option = session.get(KEY).await?; 51 | client 52 | .execute(SendMessage::new( 53 | chat_id, 54 | format!("Value: {}", val.as_deref().unwrap_or("None")), 55 | )) 56 | .await?; 57 | Ok(()) 58 | } 59 | 60 | async fn set( 61 | client: Ref, 62 | mut session: Session, 63 | chat_id: ChatPeerId, 64 | command: Command, 65 | ) -> Result<(), AppError> { 66 | let args = command.get_args(); 67 | if args.is_empty() { 68 | client 69 | .execute(SendMessage::new(chat_id, "You need to provide a value")) 70 | .await?; 71 | return Ok(()); 72 | } 73 | let val = &args[0]; 74 | log::info!("/s_set {val}"); 75 | session.set(KEY, &val).await?; 76 | client.execute(SendMessage::new(chat_id, "OK")).await?; 77 | Ok(()) 78 | } 79 | 80 | async fn expire( 81 | client: Ref, 82 | mut session: Session, 83 | chat_id: ChatPeerId, 84 | command: Command, 85 | ) -> Result<(), AppError> { 86 | let args = command.get_args(); 87 | let seconds = if args.is_empty() { 88 | 0 89 | } else { 90 | match args[0].parse::() { 91 | Ok(x) => x, 92 | Err(err) => { 93 | client 94 | .execute(SendMessage::new( 95 | chat_id, 96 | format!("Number of seconds is invalid: {err}"), 97 | )) 98 | .await?; 99 | return Ok(()); 100 | } 101 | } 102 | }; 103 | log::info!("/s_expire {seconds}"); 104 | session.expire(KEY, seconds).await?; 105 | client.execute(SendMessage::new(chat_id, "OK")).await?; 106 | Ok(()) 107 | } 108 | 109 | async fn reset( 110 | client: Ref, 111 | mut session: Session, 112 | chat_id: ChatPeerId, 113 | ) -> Result<(), AppError> { 114 | log::info!("/s_del"); 115 | session.remove(KEY).await.unwrap(); 116 | client.execute(SendMessage::new(chat_id, "OK")).await?; 117 | Ok(()) 118 | } 119 | -------------------------------------------------------------------------------- /examples/echo.rs: -------------------------------------------------------------------------------- 1 | //! The example demonstrates the basic usage of the framework. 2 | //! 3 | //! The following steps are required to run a simple bot: 4 | //! 5 | //! 1. Declare an [`Update`] handler. 6 | //! 2. Obtain a Bot API token. 7 | //! 3. Create an API [`Client`] instance to interact with the Telegram Bot API. 8 | //! 4. Create a [`Context`] instance to share objects with the handler. 9 | //! 5. Create an [`App`] instance which serves as the main entry point for the bot. 10 | //! 6. Start the bot using the [`LongPoll`] for development environment 11 | //! or the [`WebhookServer`] for production environment. 12 | //! 13 | //! The [`App`] creates a [`HandlerInput`] containing the [`Context`] instance 14 | //! and an [`Update`] received from the Telegram Bot API, 15 | //! then converts it into an input for the handler using [`TryFromInput`] trait. 16 | //! 17 | //! The handler must implement the [`Handler`] trait. 18 | //! Since this trait is implemented for [`Fn`] (with up to 10 arguments), 19 | //! you can use regular functions as handlers. 20 | //! 21 | //! It's recommended to use a function when you need a simple handler. 22 | //! Implement the [`Handler`] trait for your own type only in complex cases, 23 | //! e.g. you need to write your own decorator or a predicate. 24 | //! 25 | //! The handler is executed only when the [`TryFromInput`] returns `Some(T)`. 26 | //! When your handler is a regular function, [`TryFromInput`] must return `Some(T)` 27 | //! for all arguments of the function. Otherwise the handler will not be executed. 28 | //! 29 | //! The handler must return a future with a [`HandlerResult`] output. 30 | //! You can use any type that converts into it 31 | //! ([`IntoHandlerResult`] is implemented for the type): 32 | //! 33 | //! | From | To | 34 | //! |------------------|---------------------| 35 | //! | `()` | `Ok(())` | 36 | //! | `Ok(())` | `Ok(())` | 37 | //! | `Err(YourError)` | `Err(HandlerError)` | 38 | //! 39 | //! See the `app` example for advanced usage information. 40 | //! 41 | //! [`IntoHandlerResult`]: carapax::IntoHandlerResult 42 | //! [`Handler`]: carapax::Handler 43 | //! [`HandlerInput`]: carapax::HandlerInput 44 | //! [`HandlerResult`]: carapax::HandlerResult 45 | //! [`TryFromInput`]: carapax::TryFromInput 46 | //! [`Update`]: carapax::types::Update 47 | 48 | use std::env; 49 | 50 | use dotenvy::dotenv; 51 | 52 | use carapax::{ 53 | App, Context, Ref, 54 | api::{Client, ExecuteError}, 55 | handler::{LongPoll, WebhookServer}, 56 | types::{ChatPeerId, SendMessage, Text}, 57 | }; 58 | 59 | const DEBUG: bool = true; 60 | 61 | /// The update handler 62 | /// 63 | /// It will be executed only when the [`carapax::types::Update`] contains 64 | /// a [`ChatPeerId`] and a [`Text`] (message text, media captions). 65 | /// 66 | /// Use [`Ref`] as the argument type when you need to retrieve an object from the context. 67 | /// If the object is not found, [`carapax::TryFromInput`] returns an error and 68 | /// the handler will not be executed. 69 | async fn echo(client: Ref, chat_id: ChatPeerId, message: Text) -> Result<(), ExecuteError> { 70 | let method = SendMessage::new(chat_id, message.data); 71 | client.execute(method).await?; 72 | Ok(()) 73 | } 74 | 75 | #[tokio::main] 76 | async fn main() { 77 | dotenv().ok(); 78 | env_logger::init(); 79 | 80 | // Create an api client with a token provided by Bot Father. 81 | let token = env::var("CARAPAX_TOKEN").expect("CARAPAX_TOKEN is not set"); 82 | let client = Client::new(token).expect("Failed to create API"); 83 | 84 | // Context is a type map that enables the sharing of objects with handlers. 85 | // Each object inserted into the context must implement the `Clone` trait. 86 | let mut context = Context::default(); 87 | context.insert(client.clone()); 88 | 89 | let app = App::new(context, echo); 90 | 91 | if DEBUG { 92 | // Start receiving updates using long polling method 93 | LongPoll::new(client, app).run().await 94 | } else { 95 | // or webhook 96 | WebhookServer::new("/", app) 97 | .run(([127, 0, 0, 1], 8080)) 98 | .await 99 | .expect("Failed to run webhook"); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/access/rule/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | access::principal::Principal, 3 | types::{ChatId, Update, UserId}, 4 | }; 5 | 6 | #[cfg(test)] 7 | mod tests; 8 | 9 | /// Represents an access rule containing information about a principal and access grant status. 10 | #[derive(Debug)] 11 | pub struct AccessRule { 12 | principal: Principal, 13 | is_granted: bool, 14 | } 15 | 16 | impl AccessRule { 17 | /// Creates a new `AccessRule`. 18 | /// 19 | /// # Arguments 20 | /// 21 | /// * `principal` - A principal. 22 | /// * `is_granted` - A flag indicating whether access is granted (`true`) or denied (`false`). 23 | pub fn new(principal: T, is_granted: bool) -> Self 24 | where 25 | T: Into, 26 | { 27 | AccessRule { 28 | principal: principal.into(), 29 | is_granted, 30 | } 31 | } 32 | 33 | /// Creates a new `AccessRule` with granted access for a principal. 34 | /// 35 | /// # Arguments 36 | /// 37 | /// * `value` - The principal. 38 | pub fn allow(value: T) -> Self 39 | where 40 | T: Into, 41 | { 42 | Self::new(value, true) 43 | } 44 | 45 | /// Creates a new `AccessRule` with forbidden access for a principal. 46 | /// 47 | /// # Arguments 48 | /// 49 | /// * `value` - The principal. 50 | pub fn deny(value: T) -> Self 51 | where 52 | T: Into, 53 | { 54 | Self::new(value, false) 55 | } 56 | 57 | /// Creates a new `AccessRule` with granted access for all principals. 58 | pub fn allow_all() -> Self { 59 | Self::allow(Principal::All) 60 | } 61 | 62 | /// Creates a new `AccessRule` with denied access for all principals. 63 | pub fn deny_all() -> Self { 64 | Self::deny(Principal::All) 65 | } 66 | 67 | /// Creates a new `AccessRule` with granted access for a specific user. 68 | /// 69 | /// # Arguments 70 | /// 71 | /// * `value` - Identifier of the user. 72 | pub fn allow_user(value: T) -> Self 73 | where 74 | T: Into, 75 | { 76 | Self::allow(value.into()) 77 | } 78 | 79 | /// Creates a new `AccessRule` with denied access for a specific user. 80 | /// 81 | /// # Arguments 82 | /// 83 | /// * `value` - Identifier of the user. 84 | pub fn deny_user(value: T) -> Self 85 | where 86 | T: Into, 87 | { 88 | Self::deny(value.into()) 89 | } 90 | 91 | /// Creates a new `AccessRule` with granted access for a specific chat. 92 | /// 93 | /// # Arguments 94 | /// 95 | /// * `value` - Identifier of the chat. 96 | pub fn allow_chat(value: T) -> Self 97 | where 98 | T: Into, 99 | { 100 | Self::allow(value.into()) 101 | } 102 | 103 | /// Creates a new `AccessRule` with denied access for a specific chat. 104 | /// 105 | /// # Arguments 106 | /// 107 | /// * `value` - Identifier of the chat. 108 | pub fn deny_chat(value: T) -> Self 109 | where 110 | T: Into, 111 | { 112 | Self::deny(value.into()) 113 | } 114 | 115 | /// Creates a new `AccessRule` with granted access for a user within a specific chat. 116 | /// 117 | /// # Arguments 118 | /// 119 | /// * `chat_id` - Identifier of the chat. 120 | /// * `user_id` - Identifier of the user. 121 | pub fn allow_chat_user(chat_id: A, user_id: B) -> Self 122 | where 123 | A: Into, 124 | B: Into, 125 | { 126 | Self::allow((chat_id.into(), user_id.into())) 127 | } 128 | 129 | /// Creates a new `AccessRule` with denied access for a user within a specific chat. 130 | /// 131 | /// # Arguments 132 | /// 133 | /// * `chat_id` - Identifier of the chat. 134 | /// * `user_id` - Identifier of the user. 135 | pub fn deny_chat_user(chat_id: A, user_id: B) -> Self 136 | where 137 | A: Into, 138 | B: Into, 139 | { 140 | Self::deny((chat_id.into(), user_id.into())) 141 | } 142 | 143 | /// Indicates whether the `AccessRule` accepts an [`Update`]. 144 | /// 145 | /// # Arguments 146 | /// 147 | /// * `update` - The update to be evaluated by the access rule. 148 | /// 149 | /// Returns `true` if `AccessRule` accepts an update and `false` otherwise. 150 | pub fn accepts(&self, update: &Update) -> bool { 151 | self.principal.accepts(update) 152 | } 153 | 154 | /// Indicates whether access is granted by the rule. 155 | /// 156 | /// Returns `true` if access is granted and `false` otherwise. 157 | pub fn is_granted(&self) -> bool { 158 | self.is_granted 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/ratelimit/predicate/keyed/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, sync::Arc}; 2 | 3 | pub use governor::{Jitter, Quota}; 4 | use governor::{RateLimiter, clock::DefaultClock, state::keyed::DefaultKeyedStateStore}; 5 | #[allow(unused_imports)] 6 | pub use nonzero_ext::nonzero; 7 | 8 | use crate::{ 9 | core::{Handler, PredicateResult}, 10 | ratelimit::{ 11 | jitter::NoJitter, 12 | key::Key, 13 | method::{MethodDiscard, MethodWait}, 14 | }, 15 | }; 16 | 17 | #[cfg(test)] 18 | mod tests; 19 | 20 | /// A predicate with keyed rate limiter. 21 | /// 22 | /// Each update will have it's own rate limit under key `K`. 23 | #[derive(Clone)] 24 | pub struct KeyedRateLimitPredicate 25 | where 26 | K: Key, 27 | { 28 | limiter: Arc, DefaultClock>>, 29 | jitter: J, 30 | _method: M, 31 | keys: HashSet, 32 | } 33 | 34 | impl KeyedRateLimitPredicate 35 | where 36 | K: Key, 37 | { 38 | fn new(quota: Quota, jitter: J, method: M) -> Self { 39 | Self { 40 | limiter: Arc::new(RateLimiter::dashmap(quota)), 41 | jitter, 42 | _method: method, 43 | keys: Default::default(), 44 | } 45 | } 46 | 47 | /// Use this method when you need to run a predicate only for a specific key. 48 | /// 49 | /// If this method is not called, predicate will run for all updates. 50 | /// 51 | /// # Arguments 52 | /// 53 | /// * `key` - A key to filter by. 54 | pub fn with_key>(mut self, key: T) -> Self { 55 | self.keys.insert(key.into()); 56 | self 57 | } 58 | 59 | fn has_key(&self, key: &K) -> bool { 60 | if self.keys.is_empty() { 61 | true 62 | } else { 63 | self.keys.contains(key) 64 | } 65 | } 66 | } 67 | 68 | impl KeyedRateLimitPredicate 69 | where 70 | K: Key, 71 | { 72 | /// Creates a new `KeyedRateLimitPredicate` with the discard method. 73 | /// 74 | /// Predicate will stop update propagation when the rate limit is reached. 75 | /// 76 | /// # Arguments 77 | /// 78 | /// * `quota` - A rate limiting quota. 79 | pub fn discard(quota: Quota) -> Self { 80 | Self::new(quota, NoJitter, MethodDiscard) 81 | } 82 | } 83 | 84 | impl KeyedRateLimitPredicate 85 | where 86 | K: Key, 87 | { 88 | /// Creates a new `KeyedRateLimitPredicate` with wait method. 89 | /// 90 | /// Predicate will pause update propagation when the rate limit is reached. 91 | /// 92 | /// # Arguments 93 | /// 94 | /// * `quota` - A rate limiting quota. 95 | pub fn wait(quota: Quota) -> Self { 96 | Self::new(quota, NoJitter, MethodWait) 97 | } 98 | } 99 | 100 | impl KeyedRateLimitPredicate 101 | where 102 | K: Key, 103 | { 104 | /// Creates a new `KeyedRateLimitPredicate` with wait method and jitter. 105 | /// 106 | /// Predicate will pause update propagation when the rate limit is reached. 107 | /// 108 | /// # Arguments 109 | /// 110 | /// * `quota` - A rate limiting quota. 111 | /// * `jitter` - An interval specification for deviating from the nominal wait time. 112 | pub fn wait_with_jitter(quota: Quota, jitter: Jitter) -> Self { 113 | Self::new(quota, jitter, MethodWait) 114 | } 115 | } 116 | 117 | impl Handler for KeyedRateLimitPredicate 118 | where 119 | K: Key + Sync, 120 | { 121 | type Output = PredicateResult; 122 | 123 | async fn handle(&self, input: K) -> Self::Output { 124 | if self.has_key(&input) { 125 | match self.limiter.check_key(&input) { 126 | Ok(_) => PredicateResult::True, 127 | Err(_) => { 128 | log::info!("KeyedRateLimitPredicate: update discarded"); 129 | PredicateResult::False 130 | } 131 | } 132 | } else { 133 | PredicateResult::True 134 | } 135 | } 136 | } 137 | 138 | impl Handler for KeyedRateLimitPredicate 139 | where 140 | K: Key + Sync + 'static, 141 | { 142 | type Output = PredicateResult; 143 | 144 | async fn handle(&self, input: K) -> Self::Output { 145 | if self.has_key(&input) { 146 | self.limiter.until_key_ready(&input).await; 147 | PredicateResult::True 148 | } else { 149 | PredicateResult::True 150 | } 151 | } 152 | } 153 | 154 | impl Handler for KeyedRateLimitPredicate 155 | where 156 | K: Key + Sync + 'static, 157 | { 158 | type Output = PredicateResult; 159 | 160 | async fn handle(&self, input: K) -> Self::Output { 161 | if self.has_key(&input) { 162 | self.limiter.until_key_ready_with_jitter(&input, self.jitter).await; 163 | PredicateResult::True 164 | } else { 165 | PredicateResult::True 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/ratelimit/predicate/keyed/tests.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::ratelimit::key::{KeyChat, KeyChatUser, KeyUser}; 4 | 5 | use super::*; 6 | 7 | #[tokio::test] 8 | async fn keyed() { 9 | macro_rules! test_key { 10 | ($key_type:ty, $first_key:expr, $second_key:expr) => { 11 | // discard 12 | let handler: KeyedRateLimitPredicate<$key_type, _, _> = 13 | KeyedRateLimitPredicate::discard(Quota::per_minute(nonzero!(1u32))); 14 | assert!( 15 | matches!(handler.handle($first_key).await, PredicateResult::True), 16 | "[keyed/discard] handle({:?}) -> continue", 17 | $first_key 18 | ); 19 | assert!( 20 | matches!(handler.handle($first_key).await, PredicateResult::False), 21 | "[keyed/discard] handle({:?}) -> stop", 22 | $first_key 23 | ); 24 | assert!( 25 | matches!(handler.handle($second_key).await, PredicateResult::True), 26 | "[keyed/discard] handle({:?}) -> continue", 27 | $second_key 28 | ); 29 | assert!( 30 | matches!(handler.handle($second_key).await, PredicateResult::False), 31 | "[keyed/discard] handle({:?}) -> stop", 32 | $second_key 33 | ); 34 | 35 | // discard a specific key only 36 | let handler: KeyedRateLimitPredicate<$key_type, _, _> = 37 | KeyedRateLimitPredicate::discard(Quota::per_minute(nonzero!(1u32))).with_key($second_key); 38 | assert!( 39 | matches!(handler.handle($first_key).await, PredicateResult::True), 40 | "[keyed/discard/with_key] handle({:?}) -> continue", 41 | $first_key, 42 | ); 43 | assert!( 44 | matches!(handler.handle($first_key).await, PredicateResult::True), 45 | "[keyed/discard/with_key] handle({:?}) -> continue", 46 | $first_key, 47 | ); 48 | assert!( 49 | matches!(handler.handle($second_key).await, PredicateResult::True), 50 | "[keyed/discard/with_key] handle({:?}) -> continue", 51 | $first_key, 52 | ); 53 | assert!( 54 | matches!(handler.handle($second_key).await, PredicateResult::False), 55 | "[keyed/discard/with_key] handle({:?}) -> stop", 56 | $first_key, 57 | ); 58 | 59 | // wait 60 | let handler: KeyedRateLimitPredicate<$key_type, _, _> = KeyedRateLimitPredicate::wait( 61 | Quota::with_period(Duration::from_millis(100)) 62 | .unwrap() 63 | .allow_burst(nonzero!(1u32)), 64 | ); 65 | for key in [$first_key, $first_key, $second_key, $second_key] { 66 | assert!( 67 | matches!(handler.handle(key).await, PredicateResult::True), 68 | "[keyed/wait] handle({:?}) -> continue", 69 | key 70 | ); 71 | } 72 | 73 | // wait a specific key only 74 | let handler: KeyedRateLimitPredicate<$key_type, _, _> = KeyedRateLimitPredicate::wait( 75 | Quota::with_period(Duration::from_millis(100)) 76 | .unwrap() 77 | .allow_burst(nonzero!(1u32)), 78 | ) 79 | .with_key($second_key); 80 | for key in [$first_key, $first_key, $second_key, $second_key] { 81 | assert!( 82 | matches!(handler.handle(key).await, PredicateResult::True), 83 | "[keyed/wait/with_key] handle({:?}) -> continue", 84 | key 85 | ); 86 | } 87 | 88 | // wait with jitter 89 | let handler: KeyedRateLimitPredicate<$key_type, _, _> = KeyedRateLimitPredicate::wait_with_jitter( 90 | Quota::with_period(Duration::from_millis(100)) 91 | .unwrap() 92 | .allow_burst(nonzero!(1u32)), 93 | Jitter::new(Duration::from_millis(0), Duration::from_millis(100)), 94 | ); 95 | for key in [$first_key, $first_key, $second_key, $second_key] { 96 | assert!( 97 | matches!(handler.handle(key).await, PredicateResult::True), 98 | "[keyed/wait_with_jitter] handle({:?}) -> continue", 99 | key 100 | ); 101 | } 102 | 103 | // wait with jitter a specific key only 104 | let handler: KeyedRateLimitPredicate<$key_type, _, _> = KeyedRateLimitPredicate::wait_with_jitter( 105 | Quota::with_period(Duration::from_millis(100)) 106 | .unwrap() 107 | .allow_burst(nonzero!(1u32)), 108 | Jitter::new(Duration::from_millis(0), Duration::from_millis(100)), 109 | ) 110 | .with_key($second_key); 111 | for key in [$first_key, $first_key, $second_key, $second_key] { 112 | assert!( 113 | matches!(handler.handle(key).await, PredicateResult::True), 114 | "[keyed/wait_with_jitter/with_key] handle({:?}) -> continue", 115 | key 116 | ); 117 | } 118 | }; 119 | } 120 | let chat_1 = KeyChat::from(1); 121 | let chat_2 = KeyChat::from(2); 122 | test_key!(KeyChat, chat_1, chat_2); 123 | let user_1 = KeyUser::from(1); 124 | let user_2 = KeyUser::from(2); 125 | test_key!(KeyUser, user_1, user_2); 126 | let chat_user_1 = KeyChatUser::from((1, 1)); 127 | let chat_user_2 = KeyChatUser::from((1, 2)); 128 | test_key!(KeyChatUser, chat_user_1, chat_user_2); 129 | } 130 | -------------------------------------------------------------------------------- /src/access/rule/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::types::Update; 2 | 3 | use super::*; 4 | 5 | #[test] 6 | fn access_rule_new() { 7 | let update: Update = serde_json::from_value(serde_json::json!({ 8 | "update_id": 1, 9 | "message": { 10 | "message_id": 1, 11 | "date": 1, 12 | "from": {"id": 1, "is_bot": false, "first_name": "test", "username": "username_user"}, 13 | "chat": {"id": 1, "type": "supergroup", "title": "test", "username": "username_chat"}, 14 | "text": "test" 15 | } 16 | })) 17 | .unwrap(); 18 | 19 | let principal_chat = Principal::from(ChatId::from(1)); 20 | let principal_user = Principal::from(UserId::from(1)); 21 | 22 | let rule = AccessRule::new(principal_user.clone(), true); 23 | assert_eq!(rule.principal, principal_user); 24 | assert!(rule.is_granted()); 25 | assert!(rule.accepts(&update)); 26 | 27 | let rule = AccessRule::new(principal_chat.clone(), false); 28 | assert_eq!(rule.principal, principal_chat); 29 | assert!(!rule.is_granted()); 30 | assert!(rule.accepts(&update)); 31 | } 32 | 33 | #[test] 34 | fn access_rule_allow_deny() { 35 | let update: Update = serde_json::from_value(serde_json::json!({ 36 | "update_id": 1, 37 | "message": { 38 | "message_id": 1, 39 | "date": 1, 40 | "from": {"id": 1, "is_bot": false, "first_name": "test", "username": "username_user"}, 41 | "chat": {"id": 1, "type": "supergroup", "title": "test", "username": "username_chat"}, 42 | "text": "test" 43 | } 44 | })) 45 | .unwrap(); 46 | 47 | let principal_chat = Principal::from(ChatId::from(1)); 48 | let principal_user = Principal::from(UserId::from(1)); 49 | 50 | let rule = AccessRule::allow(principal_user.clone()); 51 | assert_eq!(rule.principal, principal_user); 52 | assert!(rule.is_granted()); 53 | assert!(rule.accepts(&update)); 54 | 55 | let rule = AccessRule::deny(principal_chat.clone()); 56 | assert_eq!(rule.principal, principal_chat); 57 | assert!(!rule.is_granted()); 58 | assert!(rule.accepts(&update)); 59 | } 60 | 61 | #[test] 62 | fn access_rule_principal_all() { 63 | let update: Update = serde_json::from_value(serde_json::json!({ 64 | "update_id": 1, 65 | "message": { 66 | "message_id": 1, 67 | "date": 1, 68 | "from": {"id": 1, "is_bot": false, "first_name": "test", "username": "username_user"}, 69 | "chat": {"id": 1, "type": "supergroup", "title": "test", "username": "username_chat"}, 70 | "text": "test" 71 | } 72 | })) 73 | .unwrap(); 74 | 75 | let rule = AccessRule::allow_all(); 76 | assert_eq!(rule.principal, Principal::All); 77 | assert!(rule.is_granted()); 78 | assert!(rule.accepts(&update)); 79 | 80 | let rule = AccessRule::deny_all(); 81 | assert_eq!(rule.principal, Principal::All); 82 | assert!(!rule.is_granted()); 83 | assert!(rule.accepts(&update)); 84 | } 85 | 86 | #[test] 87 | fn access_rule_principal_user() { 88 | let update: Update = serde_json::from_value(serde_json::json!({ 89 | "update_id": 1, 90 | "message": { 91 | "message_id": 1, 92 | "date": 1, 93 | "from": {"id": 1, "is_bot": false, "first_name": "test", "username": "username_user"}, 94 | "chat": {"id": 1, "type": "supergroup", "title": "test", "username": "username_chat"}, 95 | "text": "test" 96 | } 97 | })) 98 | .unwrap(); 99 | 100 | let principal_user = Principal::from(UserId::from(1)); 101 | 102 | let rule = AccessRule::allow_user(1); 103 | assert_eq!(rule.principal, principal_user); 104 | assert!(rule.is_granted()); 105 | assert!(rule.accepts(&update)); 106 | 107 | let rule = AccessRule::deny_user(1); 108 | assert_eq!(rule.principal, principal_user); 109 | assert!(!rule.is_granted()); 110 | assert!(rule.accepts(&update)); 111 | } 112 | 113 | #[test] 114 | fn access_rule_principal_chat() { 115 | let update: Update = serde_json::from_value(serde_json::json!({ 116 | "update_id": 1, 117 | "message": { 118 | "message_id": 1, 119 | "date": 1, 120 | "from": {"id": 1, "is_bot": false, "first_name": "test", "username": "username_user"}, 121 | "chat": {"id": 1, "type": "supergroup", "title": "test", "username": "username_chat"}, 122 | "text": "test" 123 | } 124 | })) 125 | .unwrap(); 126 | 127 | let principal_chat = Principal::from(ChatId::from(1)); 128 | 129 | let rule = AccessRule::allow_chat(1); 130 | assert_eq!(rule.principal, principal_chat); 131 | assert!(rule.is_granted()); 132 | assert!(rule.accepts(&update)); 133 | 134 | let rule = AccessRule::deny_chat(1); 135 | assert_eq!(rule.principal, principal_chat); 136 | assert!(!rule.is_granted()); 137 | assert!(rule.accepts(&update)); 138 | } 139 | 140 | #[test] 141 | fn access_rule_principal_chat_user() { 142 | let update: Update = serde_json::from_value(serde_json::json!({ 143 | "update_id": 1, 144 | "message": { 145 | "message_id": 1, 146 | "date": 1, 147 | "from": {"id": 1, "is_bot": false, "first_name": "test", "username": "username_user"}, 148 | "chat": {"id": 1, "type": "supergroup", "title": "test", "username": "username_chat"}, 149 | "text": "test" 150 | } 151 | })) 152 | .unwrap(); 153 | 154 | let rule = AccessRule::allow_chat_user(1, 1); 155 | assert_eq!(rule.principal, Principal::from((ChatId::from(1), UserId::from(1)))); 156 | assert!(rule.is_granted()); 157 | assert!(rule.accepts(&update)); 158 | 159 | let rule = AccessRule::deny_chat_user(1, 1); 160 | assert_eq!(rule.principal, Principal::from((ChatId::from(1), UserId::from(1)))); 161 | assert!(!rule.is_granted()); 162 | assert!(rule.accepts(&update)); 163 | } 164 | -------------------------------------------------------------------------------- /src/core/chain/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{any::type_name, error::Error, marker::PhantomData, sync::Arc}; 2 | 3 | use futures_util::future::BoxFuture; 4 | 5 | use crate::core::{ 6 | convert::TryFromInput, 7 | handler::{Handler, HandlerError, HandlerInput, HandlerResult, IntoHandlerResult}, 8 | predicate::PredicateOutput, 9 | }; 10 | 11 | #[cfg(test)] 12 | mod tests; 13 | 14 | /// Represents a chain of handlers. 15 | /// 16 | /// The chain allows you to configure multiple handlers for the [`crate::App`]. 17 | /// 18 | /// There are two strategies to run handlers: 19 | /// - [`Self::once`] - the chain runs only a first found handler. 20 | /// - [`Self::all`] - the chain runs all found handlers. 21 | /// 22 | /// A [`Handler`] considered found when [`TryFromInput::try_from_input`] for the handler returns [`Some`]. 23 | /// 24 | /// Handlers are dispatched in the order they are added. 25 | /// 26 | /// If a handler returns an error, all subsequent handlers will not run. 27 | #[derive(Clone)] 28 | pub struct Chain { 29 | handlers: Arc>>, 30 | strategy: ChainStrategy, 31 | } 32 | 33 | impl Chain { 34 | fn new(strategy: ChainStrategy) -> Self { 35 | Self { 36 | handlers: Arc::new(Vec::new()), 37 | strategy, 38 | } 39 | } 40 | 41 | /// Creates a new `Chain` that runs only the first found handler. 42 | pub fn once() -> Self { 43 | Self::new(ChainStrategy::FirstFound) 44 | } 45 | 46 | /// Creates a new `Chain` that runs all given handlers. 47 | pub fn all() -> Self { 48 | Self::new(ChainStrategy::All) 49 | } 50 | 51 | /// Adds a handler to the chain. 52 | /// 53 | /// # Arguments 54 | /// 55 | /// * `handler` - The handler to add. 56 | /// 57 | /// # Panics 58 | /// 59 | /// Panics when trying to add a handler to a shared chain. 60 | pub fn with(mut self, handler: H) -> Self 61 | where 62 | H: Handler + Sync + Clone + 'static, 63 | I: TryFromInput + Sync + 'static, 64 | O: Into, 65 | { 66 | let handlers = Arc::get_mut(&mut self.handlers).expect("Can not add handler, chain is shared"); 67 | handlers.push(ConvertHandler::boxed(handler)); 68 | self 69 | } 70 | 71 | async fn handle_input(&self, input: HandlerInput) -> HandlerResult { 72 | let handlers = self.handlers.clone(); 73 | let strategy = self.strategy; 74 | 75 | for handler in handlers.iter() { 76 | let type_name = handler.get_type_name(); 77 | log::debug!("Running '{type_name}' handler..."); 78 | let result = handler.handle(input.clone()).await; 79 | match result { 80 | ChainResult::Done(result) => match strategy { 81 | ChainStrategy::All => match result { 82 | Ok(()) => { 83 | log::debug!("[CONTINUE] Handler '{type_name}' succeeded"); 84 | } 85 | Err(err) => { 86 | log::debug!("[STOP] Handler '{type_name}' returned an error: {err}"); 87 | return Err(err); 88 | } 89 | }, 90 | ChainStrategy::FirstFound => { 91 | log::debug!("[STOP] First found handler: '{type_name}'"); 92 | return result; 93 | } 94 | }, 95 | ChainResult::Err(err) => { 96 | log::debug!("[STOP] Could not convert input for '{type_name}' handler: {err}"); 97 | return Err(err); 98 | } 99 | ChainResult::Skipped => { 100 | log::debug!("[CONTINUE] Input not found for '{type_name}' handler"); 101 | } 102 | } 103 | } 104 | Ok(()) 105 | } 106 | } 107 | 108 | impl Handler for Chain { 109 | type Output = HandlerResult; 110 | 111 | async fn handle(&self, input: HandlerInput) -> Self::Output { 112 | self.handle_input(input).await 113 | } 114 | } 115 | 116 | #[derive(Clone, Copy)] 117 | enum ChainStrategy { 118 | All, 119 | FirstFound, 120 | } 121 | 122 | trait ChainHandler: Send { 123 | fn handle(&self, input: HandlerInput) -> BoxFuture<'static, ChainResult>; 124 | 125 | fn get_type_name(&self) -> &'static str { 126 | type_name::() 127 | } 128 | } 129 | 130 | /// A specialized result for the [`Chain`] handler. 131 | pub enum ChainResult { 132 | /// A handler has been successfully executed. 133 | Done(HandlerResult), 134 | /// An error has occurred before handler execution. 135 | Err(HandlerError), 136 | /// A handler has not been execute. 137 | Skipped, 138 | } 139 | 140 | impl From<()> for ChainResult { 141 | fn from(_: ()) -> Self { 142 | ChainResult::Done(Ok(())) 143 | } 144 | } 145 | 146 | impl From> for ChainResult 147 | where 148 | E: Error + Send + 'static, 149 | { 150 | fn from(result: Result<(), E>) -> Self { 151 | ChainResult::Done(result.map_err(HandlerError::new)) 152 | } 153 | } 154 | 155 | impl From for ChainResult { 156 | fn from(output: PredicateOutput) -> Self { 157 | match output { 158 | PredicateOutput::True(result) => ChainResult::Done(result), 159 | PredicateOutput::False => ChainResult::Skipped, 160 | PredicateOutput::Err(err) => ChainResult::Err(err), 161 | } 162 | } 163 | } 164 | 165 | impl IntoHandlerResult for ChainResult { 166 | fn into_result(self) -> HandlerResult { 167 | match self { 168 | ChainResult::Done(result) => result, 169 | ChainResult::Err(err) => Err(err), 170 | ChainResult::Skipped => Ok(()), 171 | } 172 | } 173 | } 174 | 175 | #[derive(Clone)] 176 | struct ConvertHandler { 177 | handler: H, 178 | input: PhantomData, 179 | } 180 | 181 | impl ConvertHandler { 182 | pub(in crate::core) fn boxed(handler: H) -> Box { 183 | Box::new(Self { 184 | handler, 185 | input: PhantomData, 186 | }) 187 | } 188 | } 189 | 190 | impl ChainHandler for ConvertHandler 191 | where 192 | H: Handler + 'static, 193 | I: TryFromInput, 194 | I::Error: 'static, 195 | R: Into, 196 | { 197 | fn handle(&self, input: HandlerInput) -> BoxFuture<'static, ChainResult> { 198 | let handler = self.handler.clone(); 199 | Box::pin(async move { 200 | match I::try_from_input(input).await { 201 | Ok(Some(input)) => { 202 | let future = handler.handle(input); 203 | future.await.into() 204 | } 205 | Ok(None) => ChainResult::Skipped, 206 | Err(err) => ChainResult::Err(HandlerError::new(err)), 207 | } 208 | }) 209 | } 210 | 211 | fn get_type_name(&self) -> &'static str { 212 | type_name::() 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.32.0 (16.08.2025) 4 | 5 | - Updated dependencies: 6 | - tgbot 0.40 7 | 8 | ## 0.31.0 (05.07.2025) 9 | 10 | - Updated dependencies: 11 | - seance 0.19 12 | - tgbot 0.39 13 | - Set serde version to 1 14 | - Set tokio version to 1 15 | 16 | ## 0.30.0 (12.04.2025) 17 | 18 | - Updated dependencies: 19 | - governor 0.10 20 | - seance 0.18 21 | - tgbot 0.36 22 | - tokio 1.44 23 | 24 | ## 0.29.0 (12.02.2025) 25 | 26 | - Updated dependencies: 27 | - seance 0.17 28 | - tgbot 0.35 29 | - tokio 1.43 30 | 31 | ## 0.28.0 (02.01.2024) 32 | 33 | - Updated dependencies: 34 | - governor 0.8 35 | - tgbot 0.34 36 | 37 | ## 0.27.0 (04.12.2024) 38 | 39 | - Updated dependencies: 40 | - seance 0.16 41 | - tgbot 0.33 42 | - tokio 1.42 43 | 44 | ## 0.26.0 (17.11.2024) 45 | 46 | - Updated dependencies: 47 | - tgbot 0.32 48 | 49 | ## 0.25.0 (01.11.2024) 50 | 51 | - Updated dependencies: 52 | - governor 0.7 53 | - seance 0.15 54 | - tgbot 0.31 55 | - tokio 1.41 56 | 57 | ## 0.24.0 (07.09.2024) 58 | 59 | - Updated dependencies: 60 | - seance 0.14 61 | - tgbot 0.30 62 | - tokio 1.40 63 | 64 | ## 0.23.0 (18.08.2024) 65 | 66 | - Updated dependencies: 67 | - tgbot 0.29 68 | 69 | ## 0.22.0 (31.07.2024) 70 | 71 | - Updated dependencies: 72 | - seance 0.13 73 | - tgbot 0.28 74 | - tokio 1.39 75 | 76 | ## 0.21.0 (07.07.2024) 77 | 78 | - Updated dependencies: 79 | - tgbot 0.27 80 | 81 | ## 0.20.0 (02.07.2024) 82 | 83 | - Updated dependencies: 84 | - tgbot 0.26 85 | 86 | ## 0.19.0 (18.06.2024) 87 | 88 | - Updated dependencies: 89 | - seance 0.12 90 | - tgbot 0.25 91 | - tokio 1.38 92 | - Inner value of `Ref` is public now. 93 | This allows to use pattern matching syntax in handler arguments when you need direct access to the underlying data. 94 | 95 | ## 0.18.0 (29.05.2024) 96 | 97 | - Updated dependencies: 98 | - tgbot 0.24 99 | 100 | ## 0.17.0 (07.05.2024) 101 | 102 | - Updated dependencies: 103 | - tgbot 0.23 104 | 105 | ## 0.16.0 (01.04.2024) 106 | 107 | - Updated dependencies: 108 | - seance 0.11 109 | - tgbot 0.22 110 | - tokio 1.37 111 | 112 | ## 0.15.0 (18.02.2024) 113 | 114 | - Updated dependencies: 115 | - seance 0.10 116 | - tgbot 0.21 117 | - tokio 1.36 118 | 119 | ## 0.14.0 (01.01.2024) 120 | 121 | - Updated dependencies: 122 | - seance 0.9 123 | - tgbot 0.20 124 | - tokio 1.35 125 | - `async fn` in traits: 126 | - Removed `TryFromInput::Future` associated type. 127 | - Removed `Handler::Future` associated type. 128 | 129 | ## 0.13.0 (05.12.2023) 130 | 131 | - Updated dependencies: 132 | - governor 0.6 133 | - seance 0.8 134 | - tgbot 0.19 135 | - tokio 1.34 136 | - Renamed `add` method of `Chain` struct to `with`. 137 | - Updated `TryFromInput` implementations according to changes in tgbot. 138 | - Renamed shortcuts: 139 | - `AccessExt`: `access` to `with_access_policy`. 140 | - `PredicateExt`: `predicate` to `with_predicate`. 141 | - `CommandExt`: `command` to `with_command`. 142 | - `DialogueExt`: `dialogue` to `with_dialogue`. 143 | - Extracted a predicate from `DialogueDecorator`. 144 | This allows to skip a dialogue handler in `Chain` using a first-found strategy. 145 | As a result, you can now use multiple dialogue handlers. 146 | Previously, only one handler could be used, and it had to be the last handler in a chain. 147 | 148 | ## 0.12.0 (10.02.2022) 149 | 150 | - Updated tgbot version to 0.18. 151 | - Added `Chain::once` method which allows to run first found handler only. 152 | - Removed `PrincipalUser` and `PrincipalChat` in favor of `UserId` and `ChatId`. 153 | 154 | ## 0.11.0 (02.02.2022) 155 | 156 | - Tokio 1.16 and tgbot 0.17 support. 157 | - New handlers API. 158 | - Removed `async_trait` and `carapax-codegen` dependencies. 159 | - Removed `Dispatcher` in favor of `App` and `Chain`. 160 | - `HandlerResult` is the alias to `Result<(), HandlerError>` now. 161 | - `HandlerError` now wraps `Box`. 162 | - Changed signature of `Handler` trait. 163 | - Added `HandlerInput` struct containing `Context` and `Update`. 164 | - Renamed `TryFromUpdate` trait to `TryFromInput`. 165 | - Removed `ErrorPolicy`. 166 | - Added `Ref` to allow to pass objects from context to handlers directly. 167 | - Added `Predicate` handler to allow to wrap handlers with predicates. 168 | - Added `CommandPredicate` handler to allow to run a handler only for a specific command. 169 | - Replaced `ratelimit_meter` by `governor`. 170 | - Removed i18n support. 171 | - And other breaking changes, see examples for more information. 172 | 173 | ## 0.10.0 (09.01.2020) 174 | 175 | - Added tokio 1.0 and tgbot 0.12 support. 176 | 177 | ## 0.9.0 (15.11.2020) 178 | 179 | - Added tgbot 0.11.0 support. 180 | 181 | ## 0.8.0 (20.06.2020) 182 | 183 | - Added tgbot 0.10.0 support. 184 | 185 | ## 0.7.0 (26.04.2020) 186 | 187 | - Added tgbot 0.9 support. 188 | 189 | ## 0.6.0 (01.04.2020) 190 | 191 | - Added tgbot 0.8 support. 192 | 193 | ## 0.5.1 (28.03.2020) 194 | 195 | - Fixed docs.rs build. 196 | 197 | ## 0.5.0 (08.03.2020) 198 | 199 | - All `carapax-*` crates was merged into one `carapax` crate. 200 | Now you need to enable a corresponding feature in order 201 | to get access to features provided by those crates. 202 | - Added `Dispatcher::set_error_handler` method. 203 | Introduced `LoggingErrorHandler` as default error handler. 204 | Now `ErrorPolicy` is available in public API. 205 | So that you can easily override error handler and/or change update propagation behavior. 206 | - `seance` dependency was upgraded to 0.3 version. 207 | - Added dialogues support. 208 | - `HandlerError` now is a type alias for `Box`. 209 | - `CommandDispatcher` was removed, use `#[handler(command = "/name")]` instead. 210 | - `#[handler]` proc macro emits a clear error message when function is not async. 211 | - Value of `command` argument in `#[handler]` proc macro now always requires a leading slash. 212 | - Use `TryFrom/TryInto` when converting an `Update` to `SessionId`. 213 | We must be sure that `SessionId` always contains `chat_id` and `user_id` in order to prevent bugs. 214 | - `Command` type was moved to `types` module. 215 | - Added tgbot 0.7.0 support. 216 | 217 | ## 0.4.0 (27.01.2020) 218 | 219 | - Added tgbot 0.6 support. 220 | 221 | ## 0.3.1 (10.01.2020) 222 | 223 | - Added `CommandDispatcher::new()` method in order to support context without Default impl. 224 | - Fixed handler visibility when using proc macro. 225 | 226 | ## 0.3.0 (07.01.2020) 227 | 228 | - Added async/await support. 229 | - Removed App struct, use Dispatcher instead. 230 | - Function handlers can be implemented using proc macro only. 231 | - Now context is global and generic. 232 | - Added Error variant to HandlerResult. 233 | - Removed CommandHandler trait in favor of Command struct. 234 | - Removed TextRule-based handlers. 235 | 236 | ## 0.2.0 (07.05.2019) 237 | 238 | - `App::new()` now takes no arguments. 239 | - Added `api` argument to `App::run()` method. 240 | - `App::run()` now returns a future. 241 | - Changed API for handlers. 242 | - Removed middlewares support, use handlers instead. 243 | - Removed `Dispatcher` and `DispatcherFuture` from public API. 244 | - Access middleware moved to carapax-access crate. 245 | - Rate limit middleware moved to carapax-ratelimit crate. 246 | 247 | ## 0.1.0 (12.03.2019) 248 | 249 | - First release. 250 | -------------------------------------------------------------------------------- /src/core/convert/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{any::TypeId, convert::Infallible, error::Error, fmt, future::Future}; 2 | 3 | use crate::{ 4 | core::{context::Ref, handler::HandlerInput}, 5 | types::{ 6 | CallbackQuery, Chat, ChatJoinRequest, ChatMemberUpdated, ChatPeerId, ChatUsername, ChosenInlineResult, Command, 7 | CommandError, InlineQuery, Message, Poll, PollAnswer, PreCheckoutQuery, ShippingQuery, Text, Update, User, 8 | UserPeerId, UserUsername, 9 | }, 10 | }; 11 | 12 | #[cfg(test)] 13 | mod tests; 14 | 15 | /// Allows to create a specific handler input. 16 | pub trait TryFromInput: Send + Sized { 17 | /// An error when conversion failed. 18 | type Error: Error + Send; 19 | 20 | /// Performs conversion. 21 | /// 22 | /// # Arguments 23 | /// 24 | /// * `input` - An input to convert from. 25 | fn try_from_input(input: HandlerInput) -> impl Future, Self::Error>> + Send; 26 | } 27 | 28 | impl TryFromInput for HandlerInput { 29 | type Error = Infallible; 30 | 31 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 32 | Ok(Some(input)) 33 | } 34 | } 35 | 36 | impl TryFromInput for () { 37 | type Error = Infallible; 38 | 39 | async fn try_from_input(_input: HandlerInput) -> Result, Self::Error> { 40 | Ok(Some(())) 41 | } 42 | } 43 | 44 | impl TryFromInput for Ref 45 | where 46 | T: Clone + Send + 'static, 47 | { 48 | type Error = ConvertInputError; 49 | 50 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 51 | input 52 | .context 53 | .get::() 54 | .cloned() 55 | .map(Ref::new) 56 | .ok_or_else(ConvertInputError::context::) 57 | .map(Some) 58 | } 59 | } 60 | 61 | impl TryFromInput for Update { 62 | type Error = Infallible; 63 | 64 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 65 | Ok(Some(input.update)) 66 | } 67 | } 68 | 69 | impl TryFromInput for ChatPeerId { 70 | type Error = Infallible; 71 | 72 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 73 | Ok(input.update.get_chat_id()) 74 | } 75 | } 76 | 77 | impl TryFromInput for ChatUsername { 78 | type Error = Infallible; 79 | 80 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 81 | Ok(input.update.get_chat_username().cloned()) 82 | } 83 | } 84 | 85 | impl TryFromInput for Chat { 86 | type Error = Infallible; 87 | 88 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 89 | Ok(input.update.get_chat().cloned()) 90 | } 91 | } 92 | 93 | impl TryFromInput for UserPeerId { 94 | type Error = Infallible; 95 | 96 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 97 | Ok(input.update.get_user_id()) 98 | } 99 | } 100 | 101 | impl TryFromInput for UserUsername { 102 | type Error = Infallible; 103 | 104 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 105 | Ok(input.update.get_user_username().cloned()) 106 | } 107 | } 108 | 109 | impl TryFromInput for User { 110 | type Error = Infallible; 111 | 112 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 113 | Ok(input.update.get_user().cloned()) 114 | } 115 | } 116 | 117 | impl TryFromInput for Text { 118 | type Error = Infallible; 119 | 120 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 121 | Ok(Message::try_from(input.update).ok().and_then(|x| x.get_text().cloned())) 122 | } 123 | } 124 | 125 | impl TryFromInput for Message { 126 | type Error = Infallible; 127 | 128 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 129 | Ok(input.update.try_into().ok()) 130 | } 131 | } 132 | 133 | impl TryFromInput for Command { 134 | type Error = CommandError; 135 | 136 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 137 | Message::try_from(input.update) 138 | .ok() 139 | .map(Command::try_from) 140 | .transpose() 141 | .or_else(|err| match err { 142 | CommandError::NotFound => Ok(None), 143 | err => Err(err), 144 | }) 145 | } 146 | } 147 | 148 | impl TryFromInput for InlineQuery { 149 | type Error = Infallible; 150 | 151 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 152 | Ok(input.update.try_into().ok()) 153 | } 154 | } 155 | 156 | impl TryFromInput for ChosenInlineResult { 157 | type Error = Infallible; 158 | 159 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 160 | Ok(input.update.try_into().ok()) 161 | } 162 | } 163 | 164 | impl TryFromInput for CallbackQuery { 165 | type Error = Infallible; 166 | 167 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 168 | Ok(input.update.try_into().ok()) 169 | } 170 | } 171 | 172 | impl TryFromInput for ShippingQuery { 173 | type Error = Infallible; 174 | 175 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 176 | Ok(input.update.try_into().ok()) 177 | } 178 | } 179 | 180 | impl TryFromInput for PreCheckoutQuery { 181 | type Error = Infallible; 182 | 183 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 184 | Ok(input.update.try_into().ok()) 185 | } 186 | } 187 | 188 | impl TryFromInput for Poll { 189 | type Error = Infallible; 190 | 191 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 192 | Ok(input.update.try_into().ok()) 193 | } 194 | } 195 | 196 | impl TryFromInput for PollAnswer { 197 | type Error = Infallible; 198 | 199 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 200 | Ok(input.update.try_into().ok()) 201 | } 202 | } 203 | 204 | impl TryFromInput for ChatMemberUpdated { 205 | type Error = Infallible; 206 | 207 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 208 | Ok(input.update.try_into().ok()) 209 | } 210 | } 211 | 212 | impl TryFromInput for ChatJoinRequest { 213 | type Error = Infallible; 214 | 215 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 216 | Ok(input.update.try_into().ok()) 217 | } 218 | } 219 | 220 | macro_rules! convert_tuple { 221 | ($($T:ident),+) => { 222 | #[allow(non_snake_case)] 223 | impl<$($T),+> TryFromInput for ($($T,)+) 224 | where 225 | $( 226 | $T: TryFromInput, 227 | $T::Error: 'static, 228 | )+ 229 | { 230 | type Error = ConvertInputError; 231 | 232 | async fn try_from_input(input: HandlerInput) -> Result, Self::Error> { 233 | $( 234 | let $T = match <$T>::try_from_input( 235 | input.clone() 236 | ).await.map_err(ConvertInputError::tuple)? { 237 | Some(v) => v, 238 | None => return Ok(None) 239 | }; 240 | )+ 241 | Ok(Some(($($T,)+))) 242 | } 243 | } 244 | }; 245 | } 246 | 247 | convert_tuple!(A); 248 | convert_tuple!(A, B); 249 | convert_tuple!(A, B, C); 250 | convert_tuple!(A, B, C, D); 251 | convert_tuple!(A, B, C, D, E); 252 | convert_tuple!(A, B, C, D, E, F); 253 | convert_tuple!(A, B, C, D, E, F, G); 254 | convert_tuple!(A, B, C, D, E, F, G, H); 255 | convert_tuple!(A, B, C, D, E, F, G, H, I); 256 | convert_tuple!(A, B, C, D, E, F, G, H, I, J); 257 | 258 | /// An error when converting a [`HandlerInput`]. 259 | #[derive(Debug)] 260 | pub enum ConvertInputError { 261 | /// Object is not found in the [`crate::Context`]. 262 | Context(TypeId), 263 | /// Unable to convert [`HandlerInput`] into a tuple of specific inputs. 264 | /// 265 | /// Contains a first occurred error. 266 | Tuple(Box), 267 | } 268 | 269 | impl ConvertInputError { 270 | fn context() -> Self { 271 | Self::Context(TypeId::of::()) 272 | } 273 | 274 | fn tuple(err: E) -> Self { 275 | Self::Tuple(Box::new(err)) 276 | } 277 | } 278 | 279 | impl Error for ConvertInputError { 280 | fn source(&self) -> Option<&(dyn Error + 'static)> { 281 | use self::ConvertInputError::*; 282 | match self { 283 | Context(_) => None, 284 | Tuple(err) => err.source(), 285 | } 286 | } 287 | } 288 | 289 | impl fmt::Display for ConvertInputError { 290 | fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result { 291 | use self::ConvertInputError::*; 292 | match self { 293 | Context(type_id) => write!(out, "Object of type {type_id:?} not found in context"), 294 | Tuple(err) => write!(out, "Unable to convert HandlerInput into tuple: {err}"), 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/core/convert/tests.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::core::context::Context; 4 | 5 | use super::*; 6 | 7 | #[tokio::test] 8 | async fn empty_tuple() { 9 | let update: Update = serde_json::from_value(serde_json::json!( 10 | { 11 | "update_id": 1, 12 | "message": { 13 | "message_id": 1111, 14 | "date": 0, 15 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 16 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 17 | "text": "test", 18 | } 19 | } 20 | )) 21 | .unwrap(); 22 | let input = HandlerInput::from(update); 23 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 24 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 25 | assert!(matches!(<()>::try_from_input(input).await, Ok(Some(())))); 26 | } 27 | 28 | #[tokio::test] 29 | async fn context_ref() { 30 | let mut context = Context::default(); 31 | context.insert(3usize); 32 | let update: Update = serde_json::from_value(serde_json::json!( 33 | { 34 | "update_id": 1, 35 | "message": { 36 | "message_id": 1111, 37 | "date": 0, 38 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 39 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 40 | "text": "test", 41 | } 42 | } 43 | )) 44 | .unwrap(); 45 | let input = HandlerInput { 46 | update, 47 | context: Arc::new(context), 48 | }; 49 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 50 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 51 | assert_eq!( 52 | >::try_from_input(input.clone()).await.unwrap().as_deref(), 53 | Some(&3) 54 | ); 55 | assert!(matches!( 56 | >::try_from_input(input.clone()).await, 57 | Err(ConvertInputError::Context(_)) 58 | )); 59 | } 60 | 61 | #[tokio::test] 62 | async fn chat_id() { 63 | let update: Update = serde_json::from_value(serde_json::json!( 64 | { 65 | "update_id": 1, 66 | "message": { 67 | "message_id": 1111, 68 | "date": 0, 69 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 70 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 71 | "text": "test", 72 | } 73 | } 74 | )) 75 | .unwrap(); 76 | let input = HandlerInput::from(update); 77 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 78 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 79 | assert_eq!(ChatPeerId::try_from_input(input).await, Ok(Some(1.into()))); 80 | } 81 | 82 | #[tokio::test] 83 | async fn user() { 84 | let update: Update = serde_json::from_value(serde_json::json!( 85 | { 86 | "update_id": 1, 87 | "message": { 88 | "message_id": 1111, 89 | "date": 0, 90 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 91 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 92 | "text": "test", 93 | } 94 | } 95 | )) 96 | .unwrap(); 97 | let input = HandlerInput::from(update); 98 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 99 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 100 | assert!(User::try_from_input(input).await.unwrap().is_some()); 101 | } 102 | 103 | #[tokio::test] 104 | async fn message() { 105 | for data in [ 106 | serde_json::json!({ 107 | "update_id": 1, 108 | "message": { 109 | "message_id": 1111, 110 | "date": 0, 111 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 112 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 113 | "text": "test message from private chat" 114 | } 115 | }), 116 | serde_json::json!({ 117 | "update_id": 1, 118 | "edited_message": { 119 | "message_id": 1111, 120 | "date": 0, 121 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 122 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 123 | "text": "test edited message from private chat", 124 | "edit_date": 1213 125 | } 126 | }), 127 | serde_json::json!({ 128 | "update_id": 1, 129 | "channel_post": { 130 | "message_id": 1111, 131 | "date": 0, 132 | "author_signature": "test", 133 | "chat": {"id": 1, "type": "channel", "title": "channel title", "username": "channel_username"}, 134 | "text": "test message from channel" 135 | } 136 | }), 137 | serde_json::json!({ 138 | "update_id": 1, 139 | "edited_channel_post": { 140 | "message_id": 1111, 141 | "date": 0, 142 | "chat": {"id": 1, "type": "channel", "title": "channel title", "username": "channel_username"}, 143 | "text": "test edited message from channel", 144 | "edit_date": 1213 145 | } 146 | }), 147 | ] { 148 | let update: Update = serde_json::from_value(data).unwrap(); 149 | let input = HandlerInput::from(update); 150 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 151 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 152 | assert!(Message::try_from_input(input).await.unwrap().is_some()); 153 | } 154 | } 155 | 156 | #[tokio::test] 157 | async fn command() { 158 | let update: Update = serde_json::from_value(serde_json::json!( 159 | { 160 | "update_id": 1, 161 | "message": { 162 | "message_id": 1111, 163 | "date": 0, 164 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 165 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 166 | "text": "/test", 167 | "entities": [ 168 | {"type": "bot_command", "offset": 0, "length": 5} 169 | ] 170 | } 171 | } 172 | )) 173 | .unwrap(); 174 | let input = HandlerInput::from(update); 175 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 176 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 177 | assert!(Command::try_from_input(input).await.unwrap().is_some()); 178 | } 179 | 180 | #[tokio::test] 181 | async fn inline_query() { 182 | let update: Update = serde_json::from_value(serde_json::json!( 183 | { 184 | "update_id": 1, 185 | "inline_query": { 186 | "id": "id", 187 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 188 | "query": "query", 189 | "offset": "offset" 190 | } 191 | } 192 | )) 193 | .unwrap(); 194 | let input = HandlerInput::from(update); 195 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 196 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 197 | assert!(InlineQuery::try_from_input(input).await.unwrap().is_some()); 198 | } 199 | 200 | #[tokio::test] 201 | async fn chosen_inline_result() { 202 | let update: Update = serde_json::from_value(serde_json::json!( 203 | { 204 | "update_id": 1, 205 | "chosen_inline_result": { 206 | "result_id": "id", 207 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 208 | "query": "query" 209 | } 210 | } 211 | )) 212 | .unwrap(); 213 | let input = HandlerInput::from(update); 214 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 215 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 216 | assert!(ChosenInlineResult::try_from_input(input).await.unwrap().is_some()); 217 | } 218 | 219 | #[tokio::test] 220 | async fn callback_query() { 221 | let update: Update = serde_json::from_value(serde_json::json!( 222 | { 223 | "update_id": 1, 224 | "callback_query": { 225 | "id": "id", 226 | "from": {"id": 1, "is_bot": false, "first_name": "test"} 227 | } 228 | } 229 | )) 230 | .unwrap(); 231 | let input = HandlerInput::from(update); 232 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 233 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 234 | assert!(CallbackQuery::try_from_input(input).await.unwrap().is_some()); 235 | } 236 | 237 | #[tokio::test] 238 | async fn shipping_query() { 239 | let update: Update = serde_json::from_value(serde_json::json!( 240 | { 241 | "update_id": 1, 242 | "shipping_query": { 243 | "id": "id", 244 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 245 | "invoice_payload": "payload", 246 | "shipping_address": { 247 | "country_code": "RU", 248 | "state": "State", 249 | "city": "City", 250 | "street_line1": "Line 1", 251 | "street_line2": "Line 2", 252 | "post_code": "Post Code" 253 | } 254 | } 255 | } 256 | )) 257 | .unwrap(); 258 | let input = HandlerInput::from(update); 259 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 260 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 261 | assert!(ShippingQuery::try_from_input(input).await.unwrap().is_some()); 262 | } 263 | 264 | #[tokio::test] 265 | async fn pre_checkout_query() { 266 | let update: Update = serde_json::from_value(serde_json::json!( 267 | { 268 | "update_id": 1, 269 | "pre_checkout_query": { 270 | "id": "id", 271 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 272 | "currency": "RUB", 273 | "total_amount": 145, 274 | "invoice_payload": "payload" 275 | } 276 | } 277 | )) 278 | .unwrap(); 279 | let input = HandlerInput::from(update); 280 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 281 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 282 | assert!(PreCheckoutQuery::try_from_input(input).await.unwrap().is_some()); 283 | } 284 | 285 | #[tokio::test] 286 | async fn poll() { 287 | let update: Update = serde_json::from_value(serde_json::json!( 288 | { 289 | "update_id": 1, 290 | "poll": { 291 | "id": "id", 292 | "question": "test poll", 293 | "options": [ 294 | {"text": "opt 1", "voter_count": 1}, 295 | {"text": "opt 2", "voter_count": 2} 296 | ], 297 | "is_closed": false, 298 | "total_voter_count": 3, 299 | "is_anonymous": true, 300 | "type": "regular", 301 | "allows_multiple_answers": false 302 | } 303 | } 304 | )) 305 | .unwrap(); 306 | let input = HandlerInput::from(update); 307 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 308 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 309 | assert!(Poll::try_from_input(input).await.unwrap().is_some()); 310 | } 311 | 312 | #[tokio::test] 313 | async fn poll_answer() { 314 | let update: Update = serde_json::from_value(serde_json::json!( 315 | { 316 | "update_id": 1, 317 | "poll_answer": { 318 | "poll_id": "poll-id", 319 | "user": { 320 | "id": 1, 321 | "first_name": "Jamie", 322 | "is_bot": false 323 | }, 324 | "option_ids": [0], 325 | } 326 | } 327 | )) 328 | .unwrap(); 329 | let input = HandlerInput::from(update); 330 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 331 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 332 | assert!(PollAnswer::try_from_input(input).await.unwrap().is_some()); 333 | } 334 | 335 | #[tokio::test] 336 | async fn bot_status() { 337 | let update: Update = serde_json::from_value(serde_json::json!( 338 | { 339 | "update_id": 1, 340 | "my_chat_member": { 341 | "chat": { 342 | "id": 1, 343 | "type": "group", 344 | "title": "group title" 345 | }, 346 | "from": { 347 | "id": 1, 348 | "is_bot": true, 349 | "first_name": "firstname" 350 | }, 351 | "date": 0, 352 | "old_chat_member": { 353 | "status": "member", 354 | "user": { 355 | "id": 2, 356 | "is_bot": true, 357 | "first_name": "firstname" 358 | } 359 | }, 360 | "new_chat_member": { 361 | "status": "kicked", 362 | "user": { 363 | "id": 2, 364 | "is_bot": true, 365 | "first_name": "firstname", 366 | }, 367 | "until_date": 0 368 | } 369 | } 370 | } 371 | )) 372 | .unwrap(); 373 | let input = HandlerInput::from(update); 374 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 375 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 376 | assert!(ChatMemberUpdated::try_from_input(input).await.unwrap().is_some()); 377 | } 378 | 379 | #[tokio::test] 380 | async fn user_status() { 381 | let update: Update = serde_json::from_value(serde_json::json!( 382 | { 383 | "update_id": 1, 384 | "chat_member": { 385 | "chat": { 386 | "id": 1, 387 | "type": "group", 388 | "title": "group title" 389 | }, 390 | "from": { 391 | "id": 1, 392 | "is_bot": true, 393 | "first_name": "firstname" 394 | }, 395 | "date": 0, 396 | "old_chat_member": { 397 | "status": "member", 398 | "user": { 399 | "id": 2, 400 | "is_bot": false, 401 | "first_name": "firstname" 402 | } 403 | }, 404 | "new_chat_member": { 405 | "status": "kicked", 406 | "user": { 407 | "id": 2, 408 | "is_bot": false, 409 | "first_name": "firstname", 410 | }, 411 | "until_date": 0 412 | } 413 | } 414 | } 415 | )) 416 | .unwrap(); 417 | let input = HandlerInput::from(update); 418 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 419 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 420 | assert!(ChatMemberUpdated::try_from_input(input).await.unwrap().is_some()); 421 | } 422 | 423 | #[tokio::test] 424 | async fn chat_join_request() { 425 | let update: Update = serde_json::from_value(serde_json::json!( 426 | { 427 | "update_id": 1, 428 | "chat_join_request": { 429 | "chat": { 430 | "id": 1, 431 | "type": "group", 432 | "title": "group title" 433 | }, 434 | "from": { 435 | "id": 1, 436 | "is_bot": false, 437 | "first_name": "firstname" 438 | }, 439 | "date": 0 440 | } 441 | } 442 | )) 443 | .unwrap(); 444 | let input = HandlerInput::from(update); 445 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 446 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 447 | assert!(ChatJoinRequest::try_from_input(input).await.unwrap().is_some()); 448 | } 449 | 450 | #[tokio::test] 451 | async fn tuple() { 452 | let mut context = Context::default(); 453 | context.insert(3usize); 454 | let update: Update = serde_json::from_value(serde_json::json!( 455 | { 456 | "update_id": 1, 457 | "message": { 458 | "message_id": 1111, 459 | "date": 0, 460 | "from": {"id": 1, "is_bot": false, "first_name": "test"}, 461 | "chat": {"id": 1, "type": "private", "first_name": "test"}, 462 | "text": "test", 463 | } 464 | } 465 | )) 466 | .unwrap(); 467 | let input = HandlerInput { 468 | update, 469 | context: Arc::new(context), 470 | }; 471 | assert!(HandlerInput::try_from_input(input.clone()).await.unwrap().is_some()); 472 | assert!(Update::try_from_input(input.clone()).await.unwrap().is_some()); 473 | assert!( 474 | <(Ref, Update, User, Message)>::try_from_input(input.clone()) 475 | .await 476 | .unwrap() 477 | .is_some() 478 | ); 479 | } 480 | --------------------------------------------------------------------------------