Skip to main content

botkit_core/
bot.rs

1use std::future::Future;
2
3use crate::BotError;
4use crate::handler::{BoxedHandler, IntoHandler};
5
6/// Unified Bot trait that hides connection mode differences
7///
8/// Both Discord (WebSocket) and Telegram (HTTP webhook) bots implement
9/// this trait, providing a consistent API regardless of the underlying
10/// transport mechanism.
11pub trait Bot: Sized + Send {
12    /// Run the bot and start processing events
13    ///
14    /// Returns a handle that can be used to control the bot (shutdown, etc.)
15    fn run(self) -> impl Future<Output = Result<BotHandle, BotError>> + Send;
16}
17
18/// Handle to control a running bot
19pub struct BotHandle {
20    shutdown_tx: async_channel::Sender<()>,
21}
22
23impl BotHandle {
24    /// Create a new bot handle with a shutdown channel
25    pub fn new(shutdown_tx: async_channel::Sender<()>) -> Self {
26        Self { shutdown_tx }
27    }
28
29    /// Signal the bot to shut down gracefully
30    pub async fn shutdown(self) {
31        let _ = self.shutdown_tx.send(()).await;
32    }
33
34    /// Create a shutdown channel pair
35    pub fn channel() -> (Self, async_channel::Receiver<()>) {
36        let (tx, rx) = async_channel::bounded(1);
37        (Self::new(tx), rx)
38    }
39}
40
41/// Builder for constructing bots with handlers
42pub struct BotBuilder {
43    handlers: Vec<HandlerEntry>,
44}
45
46struct HandlerEntry {
47    pattern: HandlerPattern,
48    handler: BoxedHandler,
49    description: Option<String>,
50}
51
52#[derive(Clone)]
53pub enum HandlerPattern {
54    Command(String),
55    Button(String),
56    Message,
57}
58
59impl HandlerPattern {
60    pub fn matches(&self, event_type: &str, value: &str) -> bool {
61        match self {
62            Self::Command(name) => event_type == "command" && name == value,
63            Self::Button(pattern) => {
64                if event_type != "button" {
65                    return false;
66                }
67                if pattern.ends_with('*') {
68                    value.starts_with(&pattern[..pattern.len() - 1])
69                } else {
70                    pattern == value
71                }
72            }
73            Self::Message => event_type == "message",
74        }
75    }
76}
77
78impl BotBuilder {
79    /// Create a new bot builder
80    pub fn new() -> Self {
81        Self {
82            handlers: Vec::new(),
83        }
84    }
85
86    /// Register a command handler
87    ///
88    /// Handlers use the extractor/responder pattern:
89    /// ```ignore
90    /// // Simple handler
91    /// async fn ping() -> &'static str {
92    ///     "Pong!"
93    /// }
94    ///
95    /// // With extractors
96    /// async fn greet(user: User) -> String {
97    ///     format!("Hello, {}!", user.name)
98    /// }
99    ///
100    /// bot.command("ping", ping)
101    ///    .command("greet", greet)
102    /// ```
103    pub fn command<H, Args>(mut self, name: impl Into<String>, handler: H) -> Self
104    where
105        H: IntoHandler<Args>,
106    {
107        self.handlers.push(HandlerEntry {
108            pattern: HandlerPattern::Command(name.into()),
109            handler: handler.into_handler(),
110            description: None,
111        });
112        self
113    }
114
115    /// Register a command handler with a description
116    ///
117    /// The description is used for slash command menus (e.g., Telegram's /command list).
118    pub fn command_with_description<H, Args>(
119        mut self,
120        name: impl Into<String>,
121        description: impl Into<String>,
122        handler: H,
123    ) -> Self
124    where
125        H: IntoHandler<Args>,
126    {
127        self.handlers.push(HandlerEntry {
128            pattern: HandlerPattern::Command(name.into()),
129            handler: handler.into_handler(),
130            description: Some(description.into()),
131        });
132        self
133    }
134
135    /// Register a button handler with pattern matching
136    ///
137    /// Pattern can end with `*` for prefix matching (e.g., "confirm_*")
138    pub fn button<H, Args>(mut self, pattern: impl Into<String>, handler: H) -> Self
139    where
140        H: IntoHandler<Args>,
141    {
142        self.handlers.push(HandlerEntry {
143            pattern: HandlerPattern::Button(pattern.into()),
144            handler: handler.into_handler(),
145            description: None,
146        });
147        self
148    }
149
150    /// Register a catch-all message handler
151    pub fn message<H, Args>(mut self, handler: H) -> Self
152    where
153        H: IntoHandler<Args>,
154    {
155        self.handlers.push(HandlerEntry {
156            pattern: HandlerPattern::Message,
157            handler: handler.into_handler(),
158            description: None,
159        });
160        self
161    }
162
163    /// Get all registered commands with their descriptions
164    ///
165    /// Returns an iterator of (name, description) pairs.
166    /// Commands without descriptions are included with an empty string.
167    pub fn commands(&self) -> impl Iterator<Item = (&str, &str)> {
168        self.handlers.iter().filter_map(|entry| {
169            if let HandlerPattern::Command(name) = &entry.pattern {
170                let desc = entry.description.as_deref().unwrap_or("");
171                Some((name.as_str(), desc))
172            } else {
173                None
174            }
175        })
176    }
177
178    /// Find a handler matching the event type and value
179    pub fn find_handler(&self, event_type: &str, value: &str) -> Option<BoxedHandler> {
180        self.handlers
181            .iter()
182            .find(|entry| entry.pattern.matches(event_type, value))
183            .map(|entry| entry.handler.clone())
184    }
185}
186
187impl Default for BotBuilder {
188    fn default() -> Self {
189        Self::new()
190    }
191}