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}