Skip to main content

rust_tg_bot_ext/handlers/
message.rs

1//! [`MessageHandler`] -- handles Telegram messages filtered by a predicate.
2//!
3//! Ported from `python-telegram-bot`'s `MessageHandler`. Accepts any
4//! `Update` that passes an optional filter. When no filter is provided the
5//! handler matches every `Update`.
6//!
7//! ## Filter integration
8//!
9//! The handler accepts `Option<crate::filters::base::F>`, the composable
10//! `Filter` wrapper from the filter system. Filter data extracted via
11//! `FilterResult::MatchWithData` is forwarded to the handler callback
12//! through `MatchResult::Custom`.
13
14use std::future::Future;
15use std::pin::Pin;
16use std::sync::Arc;
17
18use rust_tg_bot_raw::types::update::Update;
19
20use super::base::{ContextCallback, Handler, HandlerCallback, HandlerResult, MatchResult};
21use crate::context::CallbackContext;
22use crate::filters::base::{self, Filter, FilterResult};
23
24/// Legacy type alias kept for backward compatibility. New code should use
25/// `crate::filters::base::F` directly.
26pub type FilterFn = Arc<dyn Fn(&Update) -> bool + Send + Sync>;
27
28/// Handler that matches updates based on the composable [`Filter`](base::Filter)
29/// trait system.
30///
31/// This is the most general-purpose handler. It mirrors the Python
32/// `MessageHandler` which delegates to a `BaseFilter` tree. The Rust port
33/// uses `crate::filters::base::F` -- the operator-overloaded filter wrapper
34/// -- so filters compose naturally with `&`, `|`, `^`, `!`.
35///
36/// When a filter returns `FilterResult::MatchWithData`, the extracted data
37/// is stored in `MatchResult::Custom` and flows through to the handler
38/// callback.
39///
40/// # Ergonomic constructor
41///
42/// ```rust,ignore
43/// use rust_tg_bot_ext::prelude::*;
44///
45/// async fn echo(update: Update, context: Context) -> HandlerResult {
46///     let text = update.effective_message().and_then(|m| m.text.as_deref()).unwrap_or("");
47///     context.reply_text(&update, text).await?;
48///     Ok(())
49/// }
50///
51/// MessageHandler::new(TEXT & !COMMAND, echo);
52/// ```
53///
54/// # Full-control constructor
55///
56/// ```rust,ignore
57/// use rust_tg_bot_ext::handlers::message::MessageHandler;
58/// use rust_tg_bot_ext::handlers::base::*;
59/// use rust_tg_bot_ext::filters::base::F;
60/// use rust_tg_bot_ext::filters::base::All;
61/// use std::sync::Arc;
62///
63/// let handler = MessageHandler::with_options(
64///     Some(F::new(All)),
65///     Arc::new(|update, _mr| Box::pin(async move { HandlerResult::Continue })),
66///     true,
67/// );
68/// ```
69pub struct MessageHandler {
70    filter: Option<base::F>,
71    callback: HandlerCallback,
72    block: bool,
73    /// Optional context-aware callback for the ergonomic API.
74    context_callback: Option<ContextCallback>,
75}
76
77impl MessageHandler {
78    /// Ergonomic constructor matching python-telegram-bot's
79    /// `MessageHandler(filters, callback)`.
80    ///
81    /// Accepts a composable filter and an async handler function with
82    /// signature `async fn(Update, Context) -> HandlerResult`.
83    ///
84    /// # Example
85    ///
86    /// ```rust,ignore
87    /// use rust_tg_bot_ext::prelude::*;
88    ///
89    /// async fn echo(update: Update, context: Context) -> HandlerResult {
90    ///     let text = update.effective_message().and_then(|m| m.text.as_deref()).unwrap_or("");
91    ///     context.reply_text(&update, text).await?;
92    ///     Ok(())
93    /// }
94    ///
95    /// MessageHandler::new(TEXT & !COMMAND, echo);
96    /// ```
97    pub fn new<Cb, Fut>(filter: base::F, callback: Cb) -> Self
98    where
99        Cb: Fn(Arc<Update>, CallbackContext) -> Fut + Send + Sync + 'static,
100        Fut: Future<Output = Result<(), crate::application::HandlerError>> + Send + 'static,
101    {
102        let cb = Arc::new(callback);
103        let context_cb: ContextCallback = Arc::new(move |update, ctx| {
104            let fut = cb(update, ctx);
105            Box::pin(fut)
106                as Pin<
107                    Box<dyn Future<Output = Result<(), crate::application::HandlerError>> + Send>,
108                >
109        });
110
111        // The raw callback is a no-op; handle_update_with_context is used instead.
112        let noop_callback: HandlerCallback =
113            Arc::new(|_update, _mr| Box::pin(async { HandlerResult::Continue }));
114
115        Self {
116            filter: Some(filter),
117            callback: noop_callback,
118            block: true,
119            context_callback: Some(context_cb),
120        }
121    }
122
123    /// Full-control constructor for advanced use cases.
124    ///
125    /// If `filter` is `None`, every `Update` matches (equivalent to the
126    /// Python `filters.ALL`).
127    pub fn with_options(filter: Option<base::F>, callback: HandlerCallback, block: bool) -> Self {
128        Self {
129            filter,
130            callback,
131            block,
132            context_callback: None,
133        }
134    }
135
136    /// Create a `MessageHandler` from a legacy closure filter.
137    ///
138    /// This is a convenience constructor for backward compatibility with code
139    /// that uses `Fn(&Update) -> bool` closures instead of the `Filter` trait.
140    pub fn from_fn(filter: Option<FilterFn>, callback: HandlerCallback, block: bool) -> Self {
141        let f = filter.map(|closure| base::F::new(ClosureFilter(closure)));
142        Self {
143            filter: f,
144            callback,
145            block,
146            context_callback: None,
147        }
148    }
149}
150
151/// Internal adapter: wraps a `Fn(&Update) -> bool` closure as a `Filter`.
152///
153/// Now that the `Filter` trait and the handler both use the same typed
154/// `Update`, no conversion is needed.
155struct ClosureFilter(Arc<dyn Fn(&Update) -> bool + Send + Sync>);
156
157impl base::Filter for ClosureFilter {
158    fn check_update(&self, update: &base::Update) -> FilterResult {
159        if (self.0)(update) {
160            FilterResult::Match
161        } else {
162            FilterResult::NoMatch
163        }
164    }
165
166    fn name(&self) -> &str {
167        "ClosureFilter"
168    }
169}
170
171impl Handler for MessageHandler {
172    fn check_update(&self, update: &Update) -> Option<MatchResult> {
173        match &self.filter {
174            Some(f) => {
175                let result = f.check_update(update);
176                match result {
177                    FilterResult::NoMatch => None,
178                    FilterResult::Match => Some(MatchResult::Empty),
179                    FilterResult::MatchWithData(data) => Some(MatchResult::Custom(Box::new(data))),
180                }
181            }
182            None => Some(MatchResult::Empty),
183        }
184    }
185
186    fn handle_update(
187        &self,
188        update: Arc<Update>,
189        match_result: MatchResult,
190    ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send>> {
191        (self.callback)(update, match_result)
192    }
193
194    fn block(&self) -> bool {
195        self.block
196    }
197
198    fn handle_update_with_context(
199        &self,
200        update: Arc<Update>,
201        match_result: MatchResult,
202        context: CallbackContext,
203    ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send>> {
204        if let Some(ref cb) = self.context_callback {
205            let fut = cb(update, context);
206            Box::pin(async move {
207                match fut.await {
208                    Ok(()) => HandlerResult::Continue,
209                    Err(crate::application::HandlerError::HandlerStop { .. }) => {
210                        HandlerResult::Stop
211                    }
212                    Err(crate::application::HandlerError::Other(e)) => HandlerResult::Error(e),
213                }
214            })
215        } else {
216            (self.callback)(update, match_result)
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::filters::base::{All, FnFilter, F};
225
226    fn noop_callback() -> HandlerCallback {
227        Arc::new(|_update, _mr| Box::pin(async { HandlerResult::Continue }))
228    }
229
230    fn empty_update() -> Update {
231        serde_json::from_str(r#"{"update_id": 1}"#).unwrap()
232    }
233
234    fn text_update() -> Update {
235        serde_json::from_value(serde_json::json!({
236            "update_id": 1,
237            "message": {
238                "message_id": 1,
239                "date": 0,
240                "chat": {"id": 1, "type": "private"},
241                "text": "hello"
242            }
243        }))
244        .unwrap()
245    }
246
247    #[test]
248    fn no_filter_matches_everything() {
249        let h = MessageHandler::with_options(None, noop_callback(), true);
250        assert!(h.check_update(&empty_update()).is_some());
251    }
252
253    #[test]
254    fn filter_trait_all_matches_message() {
255        let h = MessageHandler::with_options(Some(F::new(All)), noop_callback(), true);
256        assert!(h.check_update(&text_update()).is_some());
257    }
258
259    #[test]
260    fn filter_trait_all_rejects_empty() {
261        let h = MessageHandler::with_options(Some(F::new(All)), noop_callback(), true);
262        assert!(h.check_update(&empty_update()).is_none());
263    }
264
265    #[test]
266    fn filter_not_combinator() {
267        // !ALL should reject messages.
268        let h = MessageHandler::with_options(Some(!F::new(All)), noop_callback(), true);
269        assert!(h.check_update(&text_update()).is_none());
270        // ...but accept empty updates.
271        assert!(h.check_update(&empty_update()).is_some());
272    }
273
274    #[test]
275    fn from_fn_filter_rejects() {
276        let h = MessageHandler::from_fn(Some(Arc::new(|_u| false)), noop_callback(), true);
277        assert!(h.check_update(&empty_update()).is_none());
278    }
279
280    #[test]
281    fn from_fn_filter_accepts() {
282        let h = MessageHandler::from_fn(Some(Arc::new(|_u| true)), noop_callback(), true);
283        assert!(h.check_update(&empty_update()).is_some());
284    }
285
286    #[test]
287    fn filter_data_flows_through() {
288        let f = FnFilter::new("always", |_| true);
289        let h = MessageHandler::with_options(Some(F::new(f)), noop_callback(), true);
290        let result = h.check_update(&empty_update());
291        assert!(result.is_some());
292        assert!(matches!(result.unwrap(), MatchResult::Empty));
293    }
294
295    #[test]
296    fn composed_filters_work() {
297        let always = FnFilter::new("always", |_| true);
298        let never = FnFilter::new("never", |_| false);
299        let h = MessageHandler::with_options(
300            Some(F::new(always) & F::new(never)),
301            noop_callback(),
302            true,
303        );
304        assert!(h.check_update(&empty_update()).is_none());
305    }
306
307    #[test]
308    fn or_composed_filters_work() {
309        let always = FnFilter::new("always", |_| true);
310        let never = FnFilter::new("never", |_| false);
311        let h = MessageHandler::with_options(
312            Some(F::new(always) | F::new(never)),
313            noop_callback(),
314            true,
315        );
316        assert!(h.check_update(&empty_update()).is_some());
317    }
318}