Skip to main content

rust_tg_bot_ext/handlers/
base.rs

1//! Base handler trait and core types for the handler system.
2//!
3//! Every concrete handler implements [`Handler`], which provides a two-phase
4//! dispatch: [`Handler::check_update`] tests whether an update is relevant,
5//! and [`Handler::handle_update`] processes it.
6
7use std::any::Any;
8use std::collections::HashMap;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12
13use rust_tg_bot_raw::types::update::Update;
14
15use crate::context::CallbackContext;
16
17// ---------------------------------------------------------------------------
18// Match result
19// ---------------------------------------------------------------------------
20
21/// The result of [`Handler::check_update`] when the update *is* relevant.
22///
23/// Different handlers produce different kinds of match data -- for example
24/// a command handler yields the argument list, while a regex handler yields
25/// captured groups.
26#[derive(Debug)]
27#[non_exhaustive]
28pub enum MatchResult {
29    /// The handler matched but produced no additional data.
30    Empty,
31    /// Positional arguments (e.g. text after a `/command`).
32    Args(Vec<String>),
33    /// Positional-only regex capture groups (no named groups in pattern).
34    RegexMatch(Vec<String>),
35    /// Regex capture groups for patterns that contain at least one named group.
36    ///
37    /// `positional` holds every capture (index 0 = full match, 1... = groups),
38    /// exactly like `RegexMatch`. `named` maps each named group's name to its
39    /// matched value; only groups that actually matched are included.
40    RegexMatchWithNames {
41        /// All captures in index order (mirrors Python's `match.groups()`).
42        positional: Vec<String>,
43        /// Named captures (mirrors Python's `match.groupdict()`).
44        named: HashMap<String, String>,
45    },
46    /// Arbitrary handler-specific payload (type-erased).
47    Custom(Box<dyn Any + Send>),
48}
49
50// ---------------------------------------------------------------------------
51// Handler result
52// ---------------------------------------------------------------------------
53
54/// The outcome of handling an update.
55#[derive(Debug)]
56#[non_exhaustive]
57pub enum HandlerResult {
58    /// Processing succeeded; continue to next handler group.
59    Continue,
60    /// Processing succeeded; stop processing further handler groups.
61    Stop,
62    /// Processing failed with an error.
63    Error(Box<dyn std::error::Error + Send + Sync>),
64}
65
66// ---------------------------------------------------------------------------
67// Callback type alias
68// ---------------------------------------------------------------------------
69
70/// A type-erased, `Arc`-wrapped async handler callback.
71///
72/// The callback receives the [`Update`] and the [`MatchResult`] produced by
73/// `check_update`, and returns a future resolving to [`HandlerResult`].
74pub type HandlerCallback = Arc<
75    dyn Fn(Arc<Update>, MatchResult) -> Pin<Box<dyn Future<Output = HandlerResult> + Send>>
76        + Send
77        + Sync,
78>;
79
80/// A type-erased, `Arc`-wrapped async callback that receives a [`CallbackContext`].
81///
82/// Used by ergonomic constructors (`CommandHandler::new`, `MessageHandler::new`)
83/// where the user function has signature `async fn(Update, Context) -> HandlerResult`.
84pub type ContextCallback = Arc<
85    dyn Fn(
86            Arc<Update>,
87            CallbackContext,
88        )
89            -> Pin<Box<dyn Future<Output = Result<(), crate::application::HandlerError>> + Send>>
90        + Send
91        + Sync,
92>;
93
94// ---------------------------------------------------------------------------
95// Handler trait
96// ---------------------------------------------------------------------------
97
98/// Core trait that every update handler must implement.
99///
100/// # Design notes
101///
102/// * `check_update` is synchronous because it should be a cheap predicate
103///   (regex match, field presence check, etc.).
104/// * `handle_update` returns a boxed future so that concrete handlers can
105///   be stored as trait objects in a heterogeneous handler list.
106/// * The default `block()` implementation returns `true`, meaning the
107///   application will `await` the future before moving on. Handlers that
108///   wish to run concurrently can override this to return `false`.
109pub trait Handler: Send + Sync {
110    /// Determine whether this handler is interested in `update`.
111    ///
112    /// Returns `Some(match_result)` if the update should be handled, or
113    /// `None` to pass.
114    fn check_update(&self, update: &Update) -> Option<MatchResult>;
115
116    /// Process the update. Called only when [`check_update`](Handler::check_update)
117    /// returned `Some`.
118    fn handle_update(
119        &self,
120        update: Arc<Update>,
121        match_result: MatchResult,
122    ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send>>;
123
124    /// Whether the application should block on this handler's future before
125    /// dispatching to the next handler group.
126    fn block(&self) -> bool {
127        true
128    }
129
130    /// Populate additional context fields (e.g. `context.args`, `context.matches`)
131    /// from the match result before the handler callback is invoked.
132    ///
133    /// The default implementation is a no-op. Handlers should override this
134    /// to inject their match-specific data into the context.
135    fn collect_additional_context(
136        &self,
137        _context: &mut CallbackContext,
138        _match_result: &MatchResult,
139    ) {
140        // Default: no-op
141    }
142
143    /// Process the update with an Application-provided [`CallbackContext`].
144    ///
145    /// The default implementation ignores the context and delegates to
146    /// [`handle_update`](Handler::handle_update). Handlers created with
147    /// ergonomic constructors (e.g. `CommandHandler::new("start", my_fn)`)
148    /// override this to pass the context to the user's callback function.
149    fn handle_update_with_context(
150        &self,
151        update: Arc<Update>,
152        match_result: MatchResult,
153        _context: CallbackContext,
154    ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send>> {
155        self.handle_update(update, match_result)
156    }
157}
158
159// ---------------------------------------------------------------------------
160// FnHandler -- generic function-based handler
161// ---------------------------------------------------------------------------
162
163/// A lightweight handler that pairs a predicate with an async callback.
164///
165/// `FnHandler` bridges the gap between raw `application::Handler` structs and
166/// the typed handler trait system. It implements [`Handler`] so it can be
167/// registered via [`Application::add_handler`].
168///
169/// # Examples
170///
171/// ```rust,ignore
172/// use rust_tg_bot::ext::prelude::*;
173/// use rust_tg_bot::ext::handlers::base::FnHandler;
174///
175/// async fn button_handler(update: Update, context: Context) -> HandlerResult {
176///     // handle callback query ...
177///     Ok(())
178/// }
179///
180/// // Register with a predicate:
181/// app.add_handler(
182///     FnHandler::new(|u| u.callback_query().is_some(), button_handler),
183///     0,
184/// ).await;
185///
186/// // Or use a convenience constructor:
187/// app.add_handler(FnHandler::on_callback_query(button_handler), 0).await;
188/// ```
189pub struct FnHandler {
190    check: Arc<dyn Fn(&Update) -> bool + Send + Sync>,
191    context_callback: ContextCallback,
192}
193
194impl FnHandler {
195    /// Create a new `FnHandler` with a custom predicate and an async callback.
196    ///
197    /// The callback receives `(Update, CallbackContext)` and returns
198    /// `Result<(), HandlerError>`.
199    pub fn new<C, Cb, Fut>(check: C, callback: Cb) -> Self
200    where
201        C: Fn(&Update) -> bool + Send + Sync + 'static,
202        Cb: Fn(Arc<Update>, CallbackContext) -> Fut + Send + Sync + 'static,
203        Fut: Future<Output = Result<(), crate::application::HandlerError>> + Send + 'static,
204    {
205        let cb = Arc::new(callback);
206        let context_cb: ContextCallback = Arc::new(move |update, ctx| {
207            let fut = cb(update, ctx);
208            Box::pin(fut)
209                as Pin<
210                    Box<dyn Future<Output = Result<(), crate::application::HandlerError>> + Send>,
211                >
212        });
213        Self {
214            check: Arc::new(check),
215            context_callback: context_cb,
216        }
217    }
218
219    /// Match updates that have a `callback_query`.
220    pub fn on_callback_query<Cb, Fut>(callback: Cb) -> Self
221    where
222        Cb: Fn(Arc<Update>, CallbackContext) -> Fut + Send + Sync + 'static,
223        Fut: Future<Output = Result<(), crate::application::HandlerError>> + Send + 'static,
224    {
225        Self::new(|u| u.callback_query().is_some(), callback)
226    }
227
228    /// Match updates that have an `inline_query`.
229    pub fn on_inline_query<Cb, Fut>(callback: Cb) -> Self
230    where
231        Cb: Fn(Arc<Update>, CallbackContext) -> Fut + Send + Sync + 'static,
232        Fut: Future<Output = Result<(), crate::application::HandlerError>> + Send + 'static,
233    {
234        Self::new(|u| u.inline_query().is_some(), callback)
235    }
236
237    /// Match updates that have a `poll`.
238    pub fn on_poll<Cb, Fut>(callback: Cb) -> Self
239    where
240        Cb: Fn(Arc<Update>, CallbackContext) -> Fut + Send + Sync + 'static,
241        Fut: Future<Output = Result<(), crate::application::HandlerError>> + Send + 'static,
242    {
243        Self::new(|u| u.poll().is_some(), callback)
244    }
245
246    /// Match updates that have a `poll_answer`.
247    pub fn on_poll_answer<Cb, Fut>(callback: Cb) -> Self
248    where
249        Cb: Fn(Arc<Update>, CallbackContext) -> Fut + Send + Sync + 'static,
250        Fut: Future<Output = Result<(), crate::application::HandlerError>> + Send + 'static,
251    {
252        Self::new(|u| u.poll_answer().is_some(), callback)
253    }
254
255    /// Match updates that have a `shipping_query`.
256    pub fn on_shipping_query<Cb, Fut>(callback: Cb) -> Self
257    where
258        Cb: Fn(Arc<Update>, CallbackContext) -> Fut + Send + Sync + 'static,
259        Fut: Future<Output = Result<(), crate::application::HandlerError>> + Send + 'static,
260    {
261        Self::new(|u| u.shipping_query().is_some(), callback)
262    }
263
264    /// Match updates that have a `pre_checkout_query`.
265    pub fn on_pre_checkout_query<Cb, Fut>(callback: Cb) -> Self
266    where
267        Cb: Fn(Arc<Update>, CallbackContext) -> Fut + Send + Sync + 'static,
268        Fut: Future<Output = Result<(), crate::application::HandlerError>> + Send + 'static,
269    {
270        Self::new(|u| u.pre_checkout_query().is_some(), callback)
271    }
272
273    /// Match updates that have a `chat_member`.
274    pub fn on_chat_member<Cb, Fut>(callback: Cb) -> Self
275    where
276        Cb: Fn(Arc<Update>, CallbackContext) -> Fut + Send + Sync + 'static,
277        Fut: Future<Output = Result<(), crate::application::HandlerError>> + Send + 'static,
278    {
279        Self::new(|u| u.chat_member().is_some(), callback)
280    }
281
282    /// Match updates that have a `my_chat_member`.
283    pub fn on_my_chat_member<Cb, Fut>(callback: Cb) -> Self
284    where
285        Cb: Fn(Arc<Update>, CallbackContext) -> Fut + Send + Sync + 'static,
286        Fut: Future<Output = Result<(), crate::application::HandlerError>> + Send + 'static,
287    {
288        Self::new(|u| u.my_chat_member().is_some(), callback)
289    }
290
291    /// Match updates that have a `message`.
292    pub fn on_message<Cb, Fut>(callback: Cb) -> Self
293    where
294        Cb: Fn(Arc<Update>, CallbackContext) -> Fut + Send + Sync + 'static,
295        Fut: Future<Output = Result<(), crate::application::HandlerError>> + Send + 'static,
296    {
297        Self::new(|u| u.message().is_some(), callback)
298    }
299
300    /// Match every update (catch-all).
301    pub fn on_any<Cb, Fut>(callback: Cb) -> Self
302    where
303        Cb: Fn(Arc<Update>, CallbackContext) -> Fut + Send + Sync + 'static,
304        Fut: Future<Output = Result<(), crate::application::HandlerError>> + Send + 'static,
305    {
306        Self::new(|_| true, callback)
307    }
308}
309
310impl Handler for FnHandler {
311    fn check_update(&self, update: &Update) -> Option<MatchResult> {
312        if (self.check)(update) {
313            Some(MatchResult::Empty)
314        } else {
315            None
316        }
317    }
318
319    fn handle_update(
320        &self,
321        _update: Arc<Update>,
322        _match_result: MatchResult,
323    ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send>> {
324        // FnHandler always uses handle_update_with_context; this is a no-op fallback.
325        Box::pin(async { HandlerResult::Continue })
326    }
327
328    fn handle_update_with_context(
329        &self,
330        update: Arc<Update>,
331        _match_result: MatchResult,
332        context: CallbackContext,
333    ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send>> {
334        let fut = (self.context_callback)(update, context);
335        Box::pin(async move {
336            match fut.await {
337                Ok(()) => HandlerResult::Continue,
338                Err(crate::application::HandlerError::HandlerStop { .. }) => HandlerResult::Stop,
339                Err(crate::application::HandlerError::Other(e)) => HandlerResult::Error(e),
340            }
341        })
342    }
343}