Skip to main content

rustigram_bot/
dispatcher.rs

1use std::sync::Arc;
2
3use rustigram_api::BotClient;
4use rustigram_types::update::Update;
5use tracing::{debug, error, info, warn};
6
7use crate::context::Context;
8use crate::error::{BotError, BotResult};
9use crate::filter::Filter;
10use crate::handler::{BoxHandler, Handler};
11
12/// A single routing rule: a filter + handler pair.
13struct Route {
14    filter: Box<dyn Filter>,
15    handler: BoxHandler,
16}
17
18/// Builder for constructing a [`Dispatcher`].
19///
20/// Obtain one via [`Dispatcher::builder`] or [`crate::bot::Bot::dispatcher`].
21/// Register filter-handler pairs with [`on`](DispatcherBuilder::on),
22/// then call [`build`](DispatcherBuilder::build) to finalise.
23pub struct DispatcherBuilder {
24    client: BotClient,
25    routes: Vec<Route>,
26    fallback: Option<BoxHandler>,
27}
28
29impl DispatcherBuilder {
30    fn new(client: BotClient) -> Self {
31        Self {
32            client,
33            routes: Vec::new(),
34            fallback: None,
35        }
36    }
37
38    /// Registers a filter-handler pair.
39    ///
40    /// Routes are evaluated in registration order. The first route whose filter
41    /// returns `true` for the incoming update wins — subsequent routes are not
42    /// checked.
43    ///
44    /// # Example
45    ///
46    /// ```rust,ignore
47    /// dispatcher_builder
48    ///     .on(filters::command("start"), handler_fn(start_handler))
49    ///     .on(filters::message(),        handler_fn(echo_handler));
50    /// ```
51    #[must_use]
52    pub fn on<F, H>(mut self, filter: F, handler: H) -> Self
53    where
54        F: Filter,
55        H: Handler,
56    {
57        self.routes.push(Route {
58            filter: Box::new(filter),
59            handler: Arc::new(handler),
60        });
61        self
62    }
63
64    /// Registers a handler called when no other route matches.
65    ///
66    /// If no fallback is set, unmatched updates are silently ignored.
67    #[must_use]
68    pub fn fallback<H: Handler>(mut self, handler: H) -> Self {
69        self.fallback = Some(Arc::new(handler));
70        self
71    }
72
73    /// Finalises the builder into a [`Dispatcher`].
74    #[must_use]
75    pub fn build(self) -> Dispatcher {
76        Dispatcher {
77            client: self.client,
78            routes: Arc::new(self.routes),
79            fallback: self.fallback,
80        }
81    }
82}
83
84/// Routes incoming updates to the first matching handler.
85///
86/// # Concurrency
87///
88/// Each update is dispatched in its own `tokio::spawn` task so handlers run
89/// concurrently. The dispatcher itself is cheaply cloneable (all state is
90/// `Arc`-backed).
91#[derive(Clone)]
92/// Routes incoming updates to the first matching handler.
93///
94/// Build a dispatcher with [`Dispatcher::builder`] or the convenience
95/// method [`crate::bot::Bot::dispatcher`]. Once built, start receiving updates with
96/// [`polling`](Dispatcher::polling) or [`webhook`](Dispatcher::webhook).
97///
98/// # Concurrency
99///
100/// `Dispatcher` is cheaply cloneable — all state is `Arc`-backed.
101/// Each update is dispatched in its own [`tokio::spawn`] task so handlers
102/// run concurrently.
103pub struct Dispatcher {
104    client: BotClient,
105    routes: Arc<Vec<Route>>,
106    fallback: Option<BoxHandler>,
107}
108
109impl Dispatcher {
110    /// Creates a new [`DispatcherBuilder`] for the given client.
111    #[must_use]
112    pub fn builder(client: BotClient) -> DispatcherBuilder {
113        DispatcherBuilder::new(client)
114    }
115
116    /// Dispatches a single update.
117    ///
118    /// Finds the first matching route and spawns its handler. If no route
119    /// matches and a fallback is registered, the fallback is called.
120    pub async fn dispatch(&self, update: Update) {
121        let ctx = Context::new(update, self.client.clone());
122        debug!("Dispatching update {}", ctx.update_id());
123
124        for route in self.routes.as_ref() {
125            if route.filter.check(&ctx) {
126                let handler = route.handler.clone();
127                let ctx = ctx.clone();
128                tokio::spawn(async move {
129                    if let Err(e) = handler.handle(ctx).await {
130                        error!("Handler error: {}", e);
131                    }
132                });
133                return;
134            }
135        }
136
137        // No route matched.
138        if let Some(fallback) = &self.fallback {
139            let fallback = fallback.clone();
140            let ctx = ctx.clone();
141            tokio::spawn(async move {
142                if let Err(e) = fallback.handle(ctx).await {
143                    error!("Fallback handler error: {}", e);
144                }
145            });
146        } else {
147            debug!("No handler matched update {}", ctx.update_id());
148        }
149    }
150
151    /// Starts a long-polling loop, blocking until a fatal error occurs.
152    ///
153    /// Transient network errors (timeouts, connection resets) are retried
154    /// automatically after a short delay. Rate-limit responses (HTTP 429)
155    /// honour the `retry_after` value from Telegram. Only non-recoverable
156    /// errors (invalid token, unexpected API error) propagate to the caller.
157    ///
158    /// # Errors
159    ///
160    /// Returns a [`BotError`] on unrecoverable failure.
161    pub async fn polling(self) -> BotResult<()> {
162        use crate::update_listener::polling::LongPoller;
163        info!("Starting long-polling dispatcher");
164
165        let mut poller = LongPoller::new(self.client.clone());
166
167        loop {
168            match poller.next_batch().await {
169                Ok(updates) => {
170                    for update in updates {
171                        self.dispatch(update).await;
172                    }
173                }
174                Err(BotError::Api(rustigram_api::Error::RateLimit { retry_after })) => {
175                    warn!("Rate-limited during polling, waiting {}s", retry_after);
176                    tokio::time::sleep(std::time::Duration::from_secs(u64::from(retry_after)))
177                        .await;
178                }
179                Err(BotError::Api(rustigram_api::Error::Http(ref e)))
180                    if e.is_timeout() || e.is_connect() =>
181                {
182                    warn!(
183                        "Transient network error during polling, retrying in 3s: {}",
184                        e
185                    );
186                    tokio::time::sleep(std::time::Duration::from_secs(3)).await;
187                }
188                Err(e) => {
189                    error!("Fatal polling error: {}", e);
190                    return Err(e);
191                }
192            }
193        }
194    }
195
196    /// Starts an axum-based webhook server, blocking until shutdown.
197    ///
198    /// Accepts a bare [`SocketAddr`](std::net::SocketAddr) or a
199    /// [`WebhookConfig`](crate::update_listener::webhook::WebhookConfig).
200    /// Pass the config form to have Telegram's
201    /// `X-Telegram-Bot-Api-Secret-Token` header validated on every request —
202    /// with a bare address the header is not checked, and anything that can
203    /// reach the port can post updates.
204    ///
205    /// Telegram must already be configured to deliver updates to your server:
206    /// call [`rustigram_api::BotClient::set_webhook`] once beforehand with the
207    /// same secret.
208    ///
209    /// ```rust,ignore
210    /// dispatcher
211    ///     .webhook(WebhookConfig::new(addr).secret_token(&secret))
212    ///     .await?;
213    /// ```
214    ///
215    /// # Errors
216    ///
217    /// Returns a [`BotError`] if the TCP listener cannot be bound.
218    pub async fn webhook(
219        self,
220        config: impl Into<crate::update_listener::webhook::WebhookConfig>,
221    ) -> BotResult<()> {
222        use crate::update_listener::webhook::WebhookServer;
223        let config = config.into();
224        info!("Starting webhook dispatcher on {}", config.addr);
225        let mut server = WebhookServer::new(config.addr, self);
226        if let Some(secret) = config.secret_token {
227            server = server.secret_token(secret);
228        }
229        server.serve().await
230    }
231}