Skip to main content

rust_tg_bot_raw/bot/
mod.rs

1#![allow(clippy::too_many_arguments)]
2
3use crate::error::Result;
4use crate::request::base::{BaseRequest, TimeoutOverride};
5use crate::request::request_data::RequestData;
6use crate::request::request_parameter::{InputFileRef, RequestParameter};
7use crate::types::files;
8use crate::types::link_preview_options;
9use crate::types::message;
10use crate::types::update;
11use crate::types::user;
12use crate::types::webhook_info;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::sync::Arc;
16use std::time::Duration;
17use tokio::sync::OnceCell;
18
19// ---------------------------------------------------------------------------
20// Shared enums
21// ---------------------------------------------------------------------------
22
23/// Represents a chat identifier, which can be either a numeric ID or a `@username` string.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(untagged)]
26#[non_exhaustive]
27pub enum ChatId {
28    /// Numeric chat identifier.
29    Id(i64),
30    /// Username of the target channel (in the format `@channelusername`).
31    Username(String),
32}
33
34impl std::fmt::Display for ChatId {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            ChatId::Id(id) => write!(f, "{id}"),
38            ChatId::Username(u) => write!(f, "{u}"),
39        }
40    }
41}
42
43impl From<i64> for ChatId {
44    fn from(id: i64) -> Self {
45        ChatId::Id(id)
46    }
47}
48
49impl From<String> for ChatId {
50    fn from(username: String) -> Self {
51        ChatId::Username(username)
52    }
53}
54
55impl From<&str> for ChatId {
56    fn from(username: &str) -> Self {
57        ChatId::Username(username.to_owned())
58    }
59}
60
61/// Result type for edit methods that return either a [`Message`](message::Message) or a `bool`.
62///
63/// Some Telegram API edit methods return the edited `Message` when called with `chat_id`
64/// and `message_id`, but return `true` when called with `inline_message_id`.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(untagged)]
67#[non_exhaustive]
68pub enum MessageOrBool {
69    /// The edited message object.
70    Message(Box<message::Message>),
71    /// `true` on success (for inline messages).
72    Bool(bool),
73}
74
75// ---------------------------------------------------------------------------
76// Defaults -- user-configurable default parameters (C10)
77// ---------------------------------------------------------------------------
78
79/// Default parameter values merged into every API call when the
80/// caller has not provided an explicit value.
81#[derive(Debug, Clone, Default)]
82#[non_exhaustive]
83pub struct Defaults {
84    /// Default parse mode for text formatting (e.g. `"HTML"`, `"MarkdownV2"`).
85    pub parse_mode: Option<String>,
86    /// Whether to send messages silently by default.
87    pub disable_notification: Option<bool>,
88    /// Whether to protect forwarded messages from being saved by default.
89    pub protect_content: Option<bool>,
90    /// Whether to allow sending without a reply by default.
91    pub allow_sending_without_reply: Option<bool>,
92    /// Default link preview options.
93    pub link_preview_options: Option<link_preview_options::LinkPreviewOptions>,
94    /// Whether to quote the original message by default when replying.
95    pub quote: Option<bool>,
96}
97
98// ---------------------------------------------------------------------------
99// Bot struct
100// ---------------------------------------------------------------------------
101/// The core Telegram Bot API client.
102///
103/// `Bot` holds the API token, HTTP request backend, and optional defaults.
104/// It provides async methods for every Telegram Bot API endpoint (sending
105/// messages, managing chats, uploading files, etc.).
106///
107/// # Construction
108///
109/// Use [`Bot::new`] for the simplest case or [`Bot::with_options`] for full
110/// control over request backends and defaults.
111///
112/// # Thread safety
113///
114/// `Bot` is `Send + Sync` and can be shared across tasks via `Arc<Bot>`.
115pub struct Bot {
116    token: Arc<str>,
117    base_url: Arc<str>,
118    base_file_url: Arc<str>,
119    request: Arc<dyn BaseRequest>,
120    /// User-configured defaults merged into outgoing API calls (C10).
121    defaults: Option<Defaults>,
122    /// Cached result of `get_me()` after `initialize()` (M5).
123    cached_bot_data: Arc<OnceCell<user::User>>,
124    /// When `true`, [`files::input_file::InputFile::Path`] is sent as a
125    /// `file://` URI instead of uploading the file bytes.  Required when
126    /// connecting to a locally-hosted Bot API server.
127    local_mode: bool,
128}
129
130impl std::fmt::Debug for Bot {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        f.debug_struct("Bot")
133            .field("token", &"[REDACTED]")
134            .field("base_url", &self.base_url)
135            .field("base_file_url", &self.base_file_url)
136            .field("defaults", &self.defaults)
137            .field("local_mode", &self.local_mode)
138            .finish()
139    }
140}
141
142// ---------------------------------------------------------------------------
143// Private helpers
144// ---------------------------------------------------------------------------
145
146/// Convert an `InputFile` into a `RequestParameter`, handling file uploads.
147fn input_file_param(name: &'static str, file: files::input_file::InputFile) -> RequestParameter {
148    match file {
149        files::input_file::InputFile::FileId(id) => {
150            RequestParameter::new(name, serde_json::Value::String(id))
151        }
152        files::input_file::InputFile::Url(url) => {
153            RequestParameter::new(name, serde_json::Value::String(url))
154        }
155        files::input_file::InputFile::Bytes { filename, data } => {
156            let file_ref = InputFileRef {
157                attach_name: None,
158                bytes: data,
159                mime_type: None,
160                file_name: Some(filename),
161            };
162            RequestParameter::file_only(name, file_ref)
163        }
164        files::input_file::InputFile::Path(path) => {
165            let filename = path
166                .file_name()
167                .unwrap_or_default()
168                .to_string_lossy()
169                .to_string();
170            let path_str = path.to_string_lossy().to_string();
171            let file_ref = InputFileRef {
172                attach_name: None,
173                bytes: Vec::new(),
174                mime_type: None,
175                file_name: Some(filename),
176            };
177            RequestParameter {
178                name: std::borrow::Cow::Borrowed(name),
179                value: Some(serde_json::Value::String(format!(
180                    "__filepath__:{path_str}"
181                ))),
182                input_files: Some(vec![file_ref]),
183            }
184        }
185    }
186}
187/// Push an optional value as a `RequestParameter` if present.
188fn push_opt<T: Serialize>(
189    params: &mut Vec<RequestParameter>,
190    name: &'static str,
191    val: &Option<T>,
192) -> std::result::Result<(), serde_json::Error> {
193    if let Some(v) = val {
194        params.push(RequestParameter::new(name, serde_json::to_value(v)?));
195    }
196    Ok(())
197}
198
199/// Push an optional `&str` parameter.
200fn push_opt_str(params: &mut Vec<RequestParameter>, name: &'static str, val: Option<&str>) {
201    if let Some(v) = val {
202        params.push(RequestParameter::new(
203            name,
204            serde_json::Value::String(v.to_owned()),
205        ));
206    }
207}
208
209/// Push an optional `InputFile` parameter.
210fn push_opt_file(
211    params: &mut Vec<RequestParameter>,
212    name: &'static str,
213    val: Option<files::input_file::InputFile>,
214) {
215    if let Some(f) = val {
216        params.push(input_file_param(name, f));
217    }
218}
219
220// ---------------------------------------------------------------------------
221// Core impl: constructors, infrastructure, get_updates, basic methods
222// ---------------------------------------------------------------------------
223
224#[allow(dead_code)]
225impl Bot {
226    /// Creates a new `Bot` with the given token and HTTP request backend.
227    ///
228    /// Uses the Telegram production API endpoint (`https://api.telegram.org`).
229    /// For custom endpoints (e.g. a local Bot API server), use [`Bot::with_options`].
230    pub fn new(token: impl Into<String>, request: Arc<dyn BaseRequest>) -> Self {
231        let token = token.into();
232        let base_url: Arc<str> = format!("https://api.telegram.org/bot{token}").into();
233        let base_file_url: Arc<str> = format!("https://api.telegram.org/file/bot{token}").into();
234        let token: Arc<str> = token.into();
235        Self {
236            token,
237            base_url,
238            base_file_url,
239            request,
240            defaults: None,
241            cached_bot_data: Arc::new(OnceCell::new()),
242            local_mode: false,
243        }
244    }
245
246    /// Creates a `Bot` with full configuration options.
247    ///
248    /// Allows optional [`Defaults`] to merge into every API call.
249    pub fn with_options(
250        token: impl Into<String>,
251        request: Arc<dyn BaseRequest>,
252        defaults: Option<Defaults>,
253    ) -> Self {
254        let token = token.into();
255        let base_url: Arc<str> = format!("https://api.telegram.org/bot{token}").into();
256        let base_file_url: Arc<str> = format!("https://api.telegram.org/file/bot{token}").into();
257        let token: Arc<str> = token.into();
258        Self {
259            token,
260            base_url,
261            base_file_url,
262            request,
263            defaults,
264            cached_bot_data: Arc::new(OnceCell::new()),
265            local_mode: false,
266        }
267    }
268
269    /// Returns the bot token.
270    pub fn token(&self) -> &str {
271        &self.token
272    }
273    /// Returns the base API URL.
274    pub fn base_url(&self) -> &str {
275        &self.base_url
276    }
277    /// Returns the base file-download URL.
278    pub fn base_file_url(&self) -> &str {
279        &self.base_file_url
280    }
281    /// Returns the user-configured defaults, if any.
282    pub fn defaults(&self) -> Option<&Defaults> {
283        self.defaults.as_ref()
284    }
285    /// Returns the cached bot user data from `get_me()`, if initialized.
286    pub fn bot_data(&self) -> Option<&user::User> {
287        self.cached_bot_data.get()
288    }
289
290    /// Returns `true` if the bot is operating in local mode.
291    pub fn local_mode(&self) -> bool {
292        self.local_mode
293    }
294
295    /// Enable local mode.  When enabled, [`files::input_file::InputFile::Path`]
296    /// values are sent as `file://` URIs instead of being uploaded as bytes.
297    ///
298    /// Use this when connecting to a locally-hosted Bot API server instance.
299    #[must_use]
300    pub fn with_local_mode(mut self) -> Self {
301        self.local_mode = true;
302        self
303    }
304
305    fn api_url(&self, method: &str) -> String {
306        format!("{}/{method}", self.base_url)
307    }
308
309    async fn resolve_file_paths(&self, params: &mut [RequestParameter]) -> Result<()> {
310        for param in params.iter_mut() {
311            let path_str = param
312                .value
313                .as_ref()
314                .and_then(|v| v.as_str())
315                .and_then(|s| s.strip_prefix("__filepath__:"))
316                .map(str::to_owned);
317            if let Some(path_str) = path_str {
318                if self.local_mode {
319                    // In local mode, send the path as a file:// URI rather than uploading bytes.
320                    param.value = Some(serde_json::Value::String(format!("file://{path_str}")));
321                    param.input_files = None;
322                } else {
323                    let data = tokio::fs::read(&path_str).await?;
324                    param.value = None;
325                    if let Some(ref mut files) = param.input_files {
326                        for f in files.iter_mut() {
327                            if f.bytes.is_empty() {
328                                f.bytes = data.clone();
329                            }
330                        }
331                    }
332                }
333            }
334        }
335        Ok(())
336    }
337
338    fn apply_defaults(&self, params: &mut Vec<RequestParameter>) {
339        let defaults = match &self.defaults {
340            Some(d) => d,
341            None => return,
342        };
343        let existing: std::collections::HashSet<String> =
344            params.iter().map(|p| p.name.as_ref().to_owned()).collect();
345        if let Some(ref pm) = defaults.parse_mode {
346            if !existing.contains("parse_mode") {
347                params.push(RequestParameter::new(
348                    "parse_mode",
349                    serde_json::Value::String(pm.clone()),
350                ));
351            }
352        }
353        if let Some(dn) = defaults.disable_notification {
354            if !existing.contains("disable_notification") {
355                params.push(RequestParameter::new(
356                    "disable_notification",
357                    serde_json::Value::Bool(dn),
358                ));
359            }
360        }
361        if let Some(pc) = defaults.protect_content {
362            if !existing.contains("protect_content") {
363                params.push(RequestParameter::new(
364                    "protect_content",
365                    serde_json::Value::Bool(pc),
366                ));
367            }
368        }
369        if let Some(aswr) = defaults.allow_sending_without_reply {
370            if !existing.contains("allow_sending_without_reply") {
371                params.push(RequestParameter::new(
372                    "allow_sending_without_reply",
373                    serde_json::Value::Bool(aswr),
374                ));
375            }
376        }
377        if let Some(ref lpo) = defaults.link_preview_options {
378            if !existing.contains("link_preview_options") {
379                if let Ok(v) = serde_json::to_value(lpo) {
380                    params.push(RequestParameter::new("link_preview_options", v));
381                }
382            }
383        }
384    }
385
386    async fn do_post<T: serde::de::DeserializeOwned>(
387        &self,
388        method: &str,
389        params: Vec<RequestParameter>,
390    ) -> Result<T> {
391        self.do_post_inner(method, params, TimeoutOverride::default_none(), None)
392            .await
393    }
394
395    #[allow(dead_code)]
396    async fn do_post_with_timeouts<T: serde::de::DeserializeOwned>(
397        &self,
398        method: &str,
399        params: Vec<RequestParameter>,
400        timeouts: TimeoutOverride,
401    ) -> Result<T> {
402        self.do_post_inner(method, params, timeouts, None).await
403    }
404
405    /// Central dispatch -- heap-allocates the future via Box::pin to prevent
406    /// stack overflow from deeply nested async state machines.
407    fn do_post_inner<'a, T: serde::de::DeserializeOwned + 'a>(
408        &'a self,
409        method: &'a str,
410        mut params: Vec<RequestParameter>,
411        timeouts: TimeoutOverride,
412        api_kwargs: Option<HashMap<String, serde_json::Value>>,
413    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<T>> + Send + 'a>> {
414        Box::pin(async move {
415            self.apply_defaults(&mut params);
416            if let Some(kwargs) = api_kwargs {
417                let existing: std::collections::HashSet<String> =
418                    params.iter().map(|p| p.name.as_ref().to_owned()).collect();
419                for (key, value) in kwargs {
420                    if !existing.contains(key.as_str()) {
421                        params.push(RequestParameter::new(key, value));
422                    }
423                }
424            }
425            self.resolve_file_paths(&mut params).await?;
426            let url = self.api_url(method);
427            let data = RequestData::from_parameters(params);
428            let result = self.request.post(&url, Some(&data), timeouts).await?;
429            serde_json::from_value(result).map_err(Into::into)
430        })
431    }
432
433    /// Send pre-serialized JSON bytes directly to the API, bypassing the
434    /// `RequestParameter` -> `RequestData` -> double-serialize path.
435    ///
436    /// This is the fast path for text-only API methods whose builders
437    /// derive `Serialize` and produce a `Vec<u8>` via `serde_json::to_vec`.
438    pub(crate) async fn do_post_json<T: serde::de::DeserializeOwned>(
439        &self,
440        method: &str,
441        payload: &[u8],
442    ) -> Result<T> {
443        let url = self.api_url(method);
444        let result = self
445            .request
446            .post_json(&url, payload, TimeoutOverride::default_none())
447            .await?;
448        serde_json::from_value(result).map_err(Into::into)
449    }
450
451    /// Downloads a file from the Telegram servers given its `file_path`.
452    pub async fn download_file_raw(&self, file_path: &str) -> Result<Vec<u8>> {
453        let url = format!("{}/{file_path}", self.base_file_url);
454        let bytes = self
455            .request
456            .retrieve(&url, TimeoutOverride::default_none())
457            .await?;
458        Ok(bytes.to_vec())
459    }
460
461    /// Initializes the bot by calling `get_me()` and caching the result.
462    pub async fn initialize(&mut self) -> Result<()> {
463        self.request.initialize().await?;
464        let me = self.get_me_raw().await?;
465        let _ = self.cached_bot_data.set(me);
466        Ok(())
467    }
468
469    /// Shuts down the bot and releases the HTTP request backend.
470    pub async fn shutdown(&self) -> Result<()> {
471        self.request.shutdown().await?;
472        Ok(())
473    }
474
475    /// Sends a raw API request with the given method name and parameters.
476    pub async fn do_api_request<T: serde::de::DeserializeOwned>(
477        &self,
478        method: &str,
479        params: Vec<RequestParameter>,
480    ) -> Result<T> {
481        self.do_post(method, params).await
482    }
483
484    /// Sends a raw API request with additional keyword arguments merged into the parameters.
485    pub async fn do_api_request_with_kwargs<T: serde::de::DeserializeOwned>(
486        &self,
487        method: &str,
488        params: Vec<RequestParameter>,
489        api_kwargs: Option<HashMap<String, serde_json::Value>>,
490    ) -> Result<T> {
491        self.do_post_inner(method, params, TimeoutOverride::default_none(), api_kwargs)
492            .await
493    }
494
495    // ======================================================================
496    // Getting updates
497    // ======================================================================
498
499    /// Calls the Telegram `getUpdates` long-polling endpoint.
500    pub async fn get_updates_raw(
501        &self,
502        offset: Option<i64>,
503        limit: Option<i32>,
504        timeout: Option<i32>,
505        allowed_updates: Option<Vec<String>>,
506    ) -> Result<Vec<update::Update>> {
507        let mut params = Vec::new();
508        push_opt(&mut params, "offset", &offset)?;
509        push_opt(&mut params, "limit", &limit)?;
510        push_opt(&mut params, "timeout", &timeout)?;
511        push_opt(&mut params, "allowed_updates", &allowed_updates)?;
512        self.apply_defaults(&mut params);
513        let timeouts = if let Some(t) = timeout {
514            let effective = Duration::from_secs(t as u64 + 2);
515            TimeoutOverride {
516                read: Some(Some(effective)),
517                ..TimeoutOverride::default_none()
518            }
519        } else {
520            TimeoutOverride::default_none()
521        };
522        let url = self.api_url("getUpdates");
523        let data = RequestData::from_parameters(params);
524        let result = self.request.post(&url, Some(&data), timeouts).await?;
525        serde_json::from_value(result).map_err(Into::into)
526    }
527
528    /// Sets a webhook for receiving updates. Internal raw method.
529    ///
530    /// Calls the Telegram `setWebhook` API method.
531    pub async fn set_webhook_raw(
532        &self,
533        url: &str,
534        certificate: Option<files::input_file::InputFile>,
535        ip_address: Option<&str>,
536        max_connections: Option<i32>,
537        allowed_updates: Option<Vec<String>>,
538        drop_pending_updates: Option<bool>,
539        secret_token: Option<&str>,
540    ) -> Result<bool> {
541        let mut params = vec![RequestParameter::new(
542            "url",
543            serde_json::Value::String(url.to_owned()),
544        )];
545        push_opt_file(&mut params, "certificate", certificate);
546        push_opt_str(&mut params, "ip_address", ip_address);
547        push_opt(&mut params, "max_connections", &max_connections)?;
548        push_opt(&mut params, "allowed_updates", &allowed_updates)?;
549        push_opt(&mut params, "drop_pending_updates", &drop_pending_updates)?;
550        push_opt_str(&mut params, "secret_token", secret_token);
551        self.do_post("setWebhook", params).await
552    }
553
554    /// Removes the webhook integration. Internal raw method.
555    ///
556    /// Calls the Telegram `deleteWebhook` API method.
557    pub async fn delete_webhook_raw(&self, drop_pending_updates: Option<bool>) -> Result<bool> {
558        let mut params = Vec::new();
559        push_opt(&mut params, "drop_pending_updates", &drop_pending_updates)?;
560        self.do_post("deleteWebhook", params).await
561    }
562
563    /// Use this method to get current webhook status.
564    ///
565    /// Calls the Telegram `getWebhookInfo` API method.
566    pub async fn get_webhook_info_raw(&self) -> Result<webhook_info::WebhookInfo> {
567        self.do_post("getWebhookInfo", Vec::new()).await
568    }
569
570    // ======================================================================
571    // Basic methods
572    // ======================================================================
573
574    /// Calls the `getMe` endpoint, returning the bot's own [`User`](user::User) object.
575    pub async fn get_me_raw(&self) -> Result<user::User> {
576        self.do_post("getMe", Vec::new()).await
577    }
578
579    /// Use this method to log out from the cloud Bot API server.
580    ///
581    /// Calls the Telegram `logOut` API method.
582    pub async fn log_out_raw(&self) -> Result<bool> {
583        self.do_post("logOut", Vec::new()).await
584    }
585
586    /// Use this method to close the bot instance before moving it from one local server to another.
587    ///
588    /// Calls the Telegram `close` API method.
589    pub async fn close_raw(&self) -> Result<bool> {
590        self.do_post("close", Vec::new()).await
591    }
592}
593
594// ---------------------------------------------------------------------------
595// Per-method-group submodules
596// ---------------------------------------------------------------------------
597
598mod admin;
599mod business_methods;
600mod chat;
601mod editing;
602mod forum;
603mod games_methods;
604mod gifts_methods;
605mod inline_methods;
606mod keyboard_methods;
607mod managed_bots;
608mod media;
609mod messages;
610mod other_content;
611mod passport;
612mod payments;
613mod reactions;
614mod stickers;
615mod stories;
616mod suggested_posts;
617mod user_profile;
618mod verification;