Skip to main content

rust_tg_bot_ext/handlers/
string_command.rs

1//! [`StringCommandHandler`] -- handles `/command` strings extracted from
2//! message text.
3//!
4//! Adapted from `python-telegram-bot`'s `StringCommandHandler`. The Python
5//! version operates on raw strings put into the queue, not Telegram updates.
6//! Per the design decision, this Rust version operates on `Update` objects,
7//! extracting message text and checking for `/command` syntax.
8
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12
13use rust_tg_bot_raw::types::update::Update;
14
15use super::base::{Handler, HandlerCallback, HandlerResult, MatchResult};
16
17/// Handler that matches messages whose text starts with `/command`.
18///
19/// Unlike [`CommandHandler`](super::command), this handler does **not**
20/// require the message to have a `bot_command` entity. It performs a plain
21/// string prefix check on the message text.
22///
23/// # Matching rules
24///
25/// 1. The update must carry an effective message with non-empty `text`.
26/// 2. The text must start with `/<command>` (case-sensitive).
27/// 3. Arguments are the remaining words after the command.
28pub struct StringCommandHandler {
29    /// The command to listen for (without leading `/`).
30    command: String,
31    callback: HandlerCallback,
32    block: bool,
33}
34
35impl StringCommandHandler {
36    /// Create a new `StringCommandHandler`.
37    pub fn new(command: String, callback: HandlerCallback, block: bool) -> Self {
38        Self {
39            command,
40            callback,
41            block,
42        }
43    }
44}
45
46impl Handler for StringCommandHandler {
47    fn check_update(&self, update: &Update) -> Option<MatchResult> {
48        let message = update.effective_message()?;
49        let text = message.text.as_ref()?;
50
51        if !text.starts_with('/') {
52            return None;
53        }
54
55        let without_slash = &text[1..];
56        let mut parts = without_slash.split_whitespace();
57        let cmd = parts.next()?;
58
59        if cmd != self.command {
60            return None;
61        }
62
63        let args: Vec<String> = parts.map(String::from).collect();
64        Some(MatchResult::Args(args))
65    }
66
67    fn handle_update(
68        &self,
69        update: Arc<Update>,
70        match_result: MatchResult,
71    ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send>> {
72        (self.callback)(update, match_result)
73    }
74
75    fn block(&self) -> bool {
76        self.block
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use std::sync::Arc;
83
84    use super::*;
85
86    fn noop_callback() -> HandlerCallback {
87        Arc::new(|_update, _mr| Box::pin(async { HandlerResult::Continue }))
88    }
89
90    #[test]
91    fn matches_correct_command() {
92        let h = StringCommandHandler::new("start".into(), noop_callback(), true);
93        // Build a minimal update with message text "/start hello world"
94        let update: Update = serde_json::from_str(
95            r#"{"update_id":1,"message":{"message_id":1,"date":0,"chat":{"id":1,"type":"private"},"text":"/start hello world"}}"#,
96        ).unwrap();
97        let result = h.check_update(&update);
98        assert!(result.is_some());
99        if let Some(MatchResult::Args(args)) = result {
100            assert_eq!(args, vec!["hello", "world"]);
101        } else {
102            panic!("expected Args");
103        }
104    }
105
106    #[test]
107    fn rejects_wrong_command() {
108        let h = StringCommandHandler::new("start".into(), noop_callback(), true);
109        let update: Update = serde_json::from_str(
110            r#"{"update_id":1,"message":{"message_id":1,"date":0,"chat":{"id":1,"type":"private"},"text":"/help"}}"#,
111        ).unwrap();
112        assert!(h.check_update(&update).is_none());
113    }
114}