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}