Skip to main content

rustigram_api/
client.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use reqwest::multipart::{Form, Part};
5use serde::de::DeserializeOwned;
6use serde::Serialize;
7use tracing::{debug, warn};
8
9use crate::error::{Error, Result};
10use crate::methods::bot_settings::*;
11use crate::methods::business::*;
12use crate::methods::chat_management::*;
13use crate::methods::editing::*;
14use crate::methods::forum::*;
15use crate::methods::getters::*;
16use crate::methods::inline::*;
17use crate::methods::payments::*;
18use crate::methods::reactions::*;
19use crate::methods::sending::*;
20use crate::methods::stickers::*;
21use crate::methods::updates::*;
22use crate::methods::verification::*;
23
24// ─── Wire-format API response ─────────────────────────────────────────────────
25
26// #[serde(bound(...))] overrides the auto-generated bounds so serde does not
27// require T: Default just because the `result` field uses #[serde(default)].
28#[derive(serde::Deserialize)]
29#[serde(bound(deserialize = "T: serde::de::DeserializeOwned"))]
30struct ApiResponse<T> {
31    ok: bool,
32    #[serde(default)]
33    result: Option<T>,
34    description: Option<String>,
35    error_code: Option<u16>,
36    parameters: Option<ResponseParameters>,
37}
38
39#[derive(serde::Deserialize)]
40struct ResponseParameters {
41    migrate_to_chat_id: Option<i64>,
42    retry_after: Option<u32>,
43}
44
45// ─── ClientConfig ─────────────────────────────────────────────────────────────
46
47#[derive(Debug, Clone)]
48/// Configuration for [`BotClient`].
49///
50/// Use the builder methods to customise behaviour, then pass the config to
51/// [`BotClient::new`].
52///
53/// # Example
54///
55/// ```rust,ignore
56/// use std::time::Duration;
57///
58/// let config = ClientConfig::new("123456:ABC...")?
59///     .api_base_url("http://localhost:8081") // local Bot API server
60///     .timeout(Duration::from_secs(60))
61///     .max_retries(5);
62/// ```
63pub struct ClientConfig {
64    /// Bot token used to authenticate with the Telegram API.
65    pub token: String,
66    /// Base URL of the Bot API server (default: `https://api.telegram.org`).
67    pub api_base_url: String,
68    /// Per-request HTTP timeout.
69    pub timeout: Duration,
70    /// Maximum number of automatic retries on flood control responses.
71    pub max_retries: u8,
72}
73
74impl ClientConfig {
75    /// Creates a new `ClientConfig` with the given bot token and default settings.
76    pub fn new(token: impl Into<String>) -> Result<Self> {
77        let token = token.into();
78        validate_token(&token)?;
79        Ok(Self {
80            token,
81            api_base_url: "https://api.telegram.org".to_owned(),
82            timeout: Duration::from_secs(30),
83            max_retries: 3,
84        })
85    }
86
87    /// Sets a custom base URL for API requests, e.g. for a local Bot API server.
88    #[must_use]
89    pub fn api_base_url(mut self, url: impl Into<String>) -> Self {
90        self.api_base_url = url.into();
91        self
92    }
93
94    /// Sets a custom timeout for API requests (default 30 seconds).
95    #[must_use]
96    pub fn timeout(mut self, timeout: Duration) -> Self {
97        self.timeout = timeout;
98        self
99    }
100
101    /// Sets the maximum number of retries on HTTP 429 (flood control) errors (default 3).
102    #[must_use]
103    pub fn max_retries(mut self, n: u8) -> Self {
104        self.max_retries = n;
105        self
106    }
107}
108
109// ─── BotClient ────────────────────────────────────────────────────────────────
110
111struct Inner {
112    http: reqwest::Client,
113    config: ClientConfig,
114}
115
116#[derive(Clone)]
117/// The Telegram Bot API HTTP client.
118///
119/// `BotClient` is cheap to clone — all internal state is reference-counted.
120/// It is safe to share across tasks and threads without additional
121/// synchronisation.
122///
123/// # Creating a client
124///
125/// ```rust,ignore
126/// // From a token string (simplest)
127/// let client = BotClient::from_token("123456:ABC...")?;
128///
129/// // From a ClientConfig for advanced options
130/// let config = ClientConfig::new("123456:ABC...")?
131///     .api_base_url("http://localhost:8081")
132///     .timeout(Duration::from_secs(60));
133/// let client = BotClient::new(config)?;
134/// ```
135///
136/// # Making API calls
137///
138/// Every Bot API method is available as a method on `BotClient`. Each method
139/// returns a builder — set optional parameters with chained calls, then
140/// `.await` to execute:
141///
142/// ```rust,ignore
143/// client
144///     .send_message(chat_id, "Hello!")
145///     .parse_mode(ParseMode::HTML)
146///     .disable_notification(true)
147///     .await?;
148/// ```
149pub struct BotClient {
150    inner: Arc<Inner>,
151}
152
153impl BotClient {
154    /// Creates a new `BotClient` from a [`ClientConfig`].
155    ///
156    /// # Errors
157    ///
158    /// Returns an error if the underlying HTTP client cannot be initialised.
159    pub fn new(config: ClientConfig) -> Result<Self> {
160        let http = reqwest::Client::builder()
161            .timeout(config.timeout)
162            .build()
163            .map_err(Error::Http)?;
164        Ok(Self {
165            inner: Arc::new(Inner { http, config }),
166        })
167    }
168
169    /// Creates a `BotClient` directly from a bot token string.
170    ///
171    /// This is equivalent to `BotClient::new(ClientConfig::new(token)?)`.
172    ///
173    /// # Errors
174    ///
175    /// Returns [`Error::InvalidToken`] if the token format is invalid.
176    pub fn from_token(token: impl Into<String>) -> Result<Self> {
177        Self::new(ClientConfig::new(token)?)
178    }
179
180    /// Returns the bot token used for authentication.
181    #[must_use]
182    pub fn token(&self) -> &str {
183        &self.inner.config.token
184    }
185
186    /// Returns the base URL used for API requests, defaulting to `https://api.telegram.org`.
187    #[must_use]
188    pub fn api_base_url(&self) -> &str {
189        &self.inner.config.api_base_url
190    }
191
192    #[must_use]
193    fn method_url(&self, method: &str) -> String {
194        format!(
195            "{}/bot{}/{}",
196            self.inner.config.api_base_url, self.inner.config.token, method
197        )
198    }
199
200    /// Sends a JSON POST request to a Bot API method and deserialises the result.
201    ///
202    /// Automatically retries on HTTP 429 (flood control) up to `max_retries`
203    /// times, waiting the `retry_after` duration between attempts.
204    ///
205    /// # Errors
206    ///
207    /// Returns an error on network failure, API error (`ok: false`), or
208    /// deserialisation failure.
209    pub async fn post_json<P, R>(&self, method: &str, params: &P) -> Result<R>
210    where
211        P: Serialize + ?Sized,
212        R: DeserializeOwned,
213    {
214        let url = self.method_url(method);
215        let body = serde_json::to_vec(params).map_err(Error::Serialization)?;
216        let max_retries = self.inner.config.max_retries;
217
218        for attempt in 0..=max_retries {
219            debug!("POST {} (attempt {})", method, attempt + 1);
220
221            let resp = self
222                .inner
223                .http
224                .post(&url)
225                .header("Content-Type", "application/json")
226                .body(body.clone())
227                .send()
228                .await
229                .map_err(Error::Http)?;
230
231            let api_resp: ApiResponse<R> = resp
232                .json()
233                .await
234                .map_err(|e| Error::Decode(e.to_string()))?;
235
236            if api_resp.ok {
237                return api_resp
238                    .result
239                    .ok_or_else(|| Error::Decode("ok=true but result is null".to_owned()));
240            }
241
242            let error_code = api_resp.error_code.unwrap_or(0);
243            let description = api_resp
244                .description
245                .unwrap_or_else(|| "Unknown error".to_owned());
246            let retry_after = api_resp.parameters.as_ref().and_then(|p| p.retry_after);
247            let migrate_to_chat_id = api_resp
248                .parameters
249                .as_ref()
250                .and_then(|p| p.migrate_to_chat_id);
251
252            if error_code == 429 {
253                let wait = retry_after.unwrap_or(1);
254                if attempt < max_retries {
255                    warn!(
256                        "Flood control on {}: waiting {}s (attempt {}/{})",
257                        method,
258                        wait,
259                        attempt + 1,
260                        max_retries
261                    );
262                    tokio::time::sleep(Duration::from_secs(u64::from(wait))).await;
263                    continue;
264                }
265                return Err(Error::RateLimit { retry_after: wait });
266            }
267
268            return Err(Error::Api {
269                error_code,
270                description,
271                migrate_to_chat_id,
272                retry_after,
273            });
274        }
275
276        unreachable!()
277    }
278
279    /// Sends a multipart/form-data POST request to a Bot API method and deserialises the result.
280    pub async fn post_multipart<R>(&self, method: &str, form: Form) -> Result<R>
281    where
282        R: DeserializeOwned,
283    {
284        let url = self.method_url(method);
285        debug!("POST multipart {}", method);
286
287        let resp = self
288            .inner
289            .http
290            .post(&url)
291            .multipart(form)
292            .send()
293            .await
294            .map_err(Error::Http)?;
295
296        let api_resp: ApiResponse<R> = resp
297            .json()
298            .await
299            .map_err(|e| Error::Decode(e.to_string()))?;
300
301        if api_resp.ok {
302            return api_resp
303                .result
304                .ok_or_else(|| Error::Decode("ok=true but result is null".to_owned()));
305        }
306
307        let error_code = api_resp.error_code.unwrap_or(0);
308        let description = api_resp
309            .description
310            .unwrap_or_else(|| "Unknown error".to_owned());
311        let retry_after = api_resp.parameters.as_ref().and_then(|p| p.retry_after);
312        let migrate_to_chat_id = api_resp
313            .parameters
314            .as_ref()
315            .and_then(|p| p.migrate_to_chat_id);
316
317        if error_code == 429 {
318            return Err(Error::RateLimit {
319                retry_after: retry_after.unwrap_or(1),
320            });
321        }
322
323        Err(Error::Api {
324            error_code,
325            description,
326            migrate_to_chat_id,
327            retry_after,
328        })
329    }
330
331    /// Downloads a file by its path as returned by [`BotClient::get_file`].
332    ///
333    /// The file path must be obtained by calling `get_file` first:
334    ///
335    /// ```rust,ignore
336    /// let file = client.get_file(&document.file_id).await?;
337    /// let bytes = client.download_file(&file.file_path.unwrap()).await?;
338    /// ```
339    ///
340    /// Maximum file size via the Telegram cloud server is 20 MB.
341    /// Use a [local Bot API server](https://github.com/tdlib/telegram-bot-api)
342    /// to lift this restriction.
343    pub async fn download_file(&self, file_path: &str) -> Result<bytes::Bytes> {
344        let url = format!(
345            "{}/file/bot{}/{}",
346            self.inner.config.api_base_url, self.inner.config.token, file_path
347        );
348        self.inner
349            .http
350            .get(&url)
351            .send()
352            .await
353            .map_err(Error::Http)?
354            .bytes()
355            .await
356            .map_err(Error::Http)
357    }
358
359    // ── Update methods ────────────────────────────────────────────────────────
360
361    /// Calls `getUpdates` — fetches a batch of incoming updates via long polling.
362    pub fn get_updates(&self) -> GetUpdates {
363        GetUpdates::new(self.clone())
364    }
365    /// Calls `setWebhook` — registers a webhook URL with Telegram.
366    pub fn set_webhook(&self, url: impl Into<String>) -> SetWebhook {
367        SetWebhook::new(self.clone(), url)
368    }
369    /// Calls `deleteWebhook` — removes the webhook integration.
370    pub fn delete_webhook(&self) -> DeleteWebhook {
371        DeleteWebhook::new(self.clone())
372    }
373    /// Calls `getWebhookInfo` — returns the current webhook status.
374    pub fn get_webhook_info(&self) -> GetWebhookInfo {
375        GetWebhookInfo::new(self.clone())
376    }
377
378    // ── Getters ───────────────────────────────────────────────────────────────
379
380    /// Calls `getMe` — returns basic information about the bot.
381    pub fn get_me(&self) -> GetMe {
382        GetMe::new(self.clone())
383    }
384    /// Calls `getChat` — returns detailed information about a chat.
385    pub fn get_chat(&self, chat_id: impl Into<rustigram_types::user::ChatId>) -> GetChat {
386        GetChat::new(self.clone(), chat_id)
387    }
388    /// Calls `getChatAdministrators` — returns a list of all chat administrators.
389    pub fn get_chat_administrators(
390        &self,
391        chat_id: impl Into<rustigram_types::user::ChatId>,
392    ) -> GetChatAdministrators {
393        GetChatAdministrators::new(self.clone(), chat_id)
394    }
395    /// Calls `getChatMemberCount` — returns the number of members in a chat.
396    pub fn get_chat_member_count(
397        &self,
398        chat_id: impl Into<rustigram_types::user::ChatId>,
399    ) -> GetChatMemberCount {
400        GetChatMemberCount::new(self.clone(), chat_id)
401    }
402    /// Calls `getChatMember` — returns information about a specific chat member.
403    pub fn get_chat_member(
404        &self,
405        chat_id: impl Into<rustigram_types::user::ChatId>,
406        user_id: i64,
407    ) -> GetChatMember {
408        GetChatMember::new(self.clone(), chat_id, user_id)
409    }
410    /// Calls `getFile` — returns file metadata and a download path.
411    pub fn get_file(&self, file_id: impl Into<String>) -> GetFile {
412        GetFile::new(self.clone(), file_id)
413    }
414    /// Calls `getUserProfilePhotos` — returns a user's profile pictures.
415    pub fn get_user_profile_photos(&self, user_id: i64) -> GetUserProfilePhotos {
416        GetUserProfilePhotos::new(self.clone(), user_id)
417    }
418
419    // ── Sending ───────────────────────────────────────────────────────────────
420
421    /// Calls `sendMessage` — sends a text message to a chat.
422    pub fn send_message(
423        &self,
424        chat_id: impl Into<rustigram_types::user::ChatId>,
425        text: impl Into<String>,
426    ) -> SendMessage {
427        SendMessage::new(self.clone(), chat_id, text)
428    }
429    /// Calls `forwardMessage` — forwards a message from one chat to another.
430    pub fn forward_message(
431        &self,
432        chat_id: impl Into<rustigram_types::user::ChatId>,
433        from_chat_id: impl Into<rustigram_types::user::ChatId>,
434        message_id: i64,
435    ) -> ForwardMessage {
436        ForwardMessage::new(self.clone(), chat_id, from_chat_id, message_id)
437    }
438    /// Calls `copyMessage` — copies a message without the forward header.
439    pub fn copy_message(
440        &self,
441        chat_id: impl Into<rustigram_types::user::ChatId>,
442        from_chat_id: impl Into<rustigram_types::user::ChatId>,
443        message_id: i64,
444    ) -> CopyMessage {
445        CopyMessage::new(self.clone(), chat_id, from_chat_id, message_id)
446    }
447    /// Calls `sendChatAction` — displays a typing or upload indicator.
448    pub fn send_chat_action(
449        &self,
450        chat_id: impl Into<rustigram_types::user::ChatId>,
451        action: ChatAction,
452    ) -> SendChatAction {
453        SendChatAction::new(self.clone(), chat_id, action)
454    }
455    /// Calls `sendPhoto` — sends a photo.
456    pub fn send_photo(
457        &self,
458        chat_id: impl Into<rustigram_types::user::ChatId>,
459        photo: rustigram_types::file::InputFile,
460    ) -> SendPhoto {
461        SendPhoto::new(self.clone(), chat_id, photo)
462    }
463    /// Calls `sendAudio` — sends an audio file treated as music.
464    pub fn send_audio(
465        &self,
466        chat_id: impl Into<rustigram_types::user::ChatId>,
467        audio: rustigram_types::file::InputFile,
468    ) -> SendAudio {
469        SendAudio::new(self.clone(), chat_id, audio)
470    }
471    /// Calls `sendDocument` — sends a general file.
472    pub fn send_document(
473        &self,
474        chat_id: impl Into<rustigram_types::user::ChatId>,
475        document: rustigram_types::file::InputFile,
476    ) -> SendDocument {
477        SendDocument::new(self.clone(), chat_id, document)
478    }
479    /// Calls `sendVideo` — sends a video file.
480    pub fn send_video(
481        &self,
482        chat_id: impl Into<rustigram_types::user::ChatId>,
483        video: rustigram_types::file::InputFile,
484    ) -> SendVideo {
485        SendVideo::new(self.clone(), chat_id, video)
486    }
487    /// Calls `sendAnimation` — sends a GIF or silent H.264 video.
488    pub fn send_animation(
489        &self,
490        chat_id: impl Into<rustigram_types::user::ChatId>,
491        animation: rustigram_types::file::InputFile,
492    ) -> SendAnimation {
493        SendAnimation::new(self.clone(), chat_id, animation)
494    }
495    /// Calls `sendVoice` — sends a voice note.
496    pub fn send_voice(
497        &self,
498        chat_id: impl Into<rustigram_types::user::ChatId>,
499        voice: rustigram_types::file::InputFile,
500    ) -> SendVoice {
501        SendVoice::new(self.clone(), chat_id, voice)
502    }
503    /// Calls `sendVideoNote` — sends a rounded-square video.
504    pub fn send_video_note(
505        &self,
506        chat_id: impl Into<rustigram_types::user::ChatId>,
507        video_note: rustigram_types::file::InputFile,
508    ) -> SendVideoNote {
509        SendVideoNote::new(self.clone(), chat_id, video_note)
510    }
511    /// Calls `sendSticker` — sends a sticker.
512    pub fn send_sticker(
513        &self,
514        chat_id: impl Into<rustigram_types::user::ChatId>,
515        sticker: rustigram_types::file::InputFile,
516    ) -> SendSticker {
517        SendSticker::new(self.clone(), chat_id, sticker)
518    }
519    /// Calls `sendLocation` — sends a geographic location, optionally live.
520    pub fn send_location(
521        &self,
522        chat_id: impl Into<rustigram_types::user::ChatId>,
523        latitude: f64,
524        longitude: f64,
525    ) -> SendLocation {
526        SendLocation::new(self.clone(), chat_id, latitude, longitude)
527    }
528    /// Calls `sendContact` — sends a phone contact.
529    pub fn send_contact(
530        &self,
531        chat_id: impl Into<rustigram_types::user::ChatId>,
532        phone_number: impl Into<String>,
533        first_name: impl Into<String>,
534    ) -> SendContact {
535        SendContact::new(self.clone(), chat_id, phone_number, first_name)
536    }
537    /// Calls `sendPoll` — sends a native poll or quiz.
538    pub fn send_poll(
539        &self,
540        chat_id: impl Into<rustigram_types::user::ChatId>,
541        question: impl Into<String>,
542        options: Vec<rustigram_types::poll::InputPollOption>,
543    ) -> SendPoll {
544        SendPoll::new(self.clone(), chat_id, question, options)
545    }
546    /// Calls `sendDice` — sends an animated random emoji.
547    pub fn send_dice(&self, chat_id: impl Into<rustigram_types::user::ChatId>) -> SendDice {
548        SendDice::new(self.clone(), chat_id)
549    }
550    /// Calls `sendMessageDraft` — streams a partial message (Bot API 9.5+).
551    pub fn send_message_draft(
552        &self,
553        chat_id: impl Into<rustigram_types::user::ChatId>,
554        draft_id: i64,
555        text: impl Into<String>,
556    ) -> SendMessageDraft {
557        SendMessageDraft::new(self.clone(), chat_id, draft_id, text)
558    }
559    /// Calls `deleteMessage` — deletes a message.
560    pub fn delete_message(
561        &self,
562        chat_id: impl Into<rustigram_types::user::ChatId>,
563        message_id: i64,
564    ) -> DeleteMessage {
565        DeleteMessage::new(self.clone(), chat_id, message_id)
566    }
567    /// Calls `deleteMessages` — deletes up to 100 messages at once.
568    pub fn delete_messages(
569        &self,
570        chat_id: impl Into<rustigram_types::user::ChatId>,
571        message_ids: Vec<i64>,
572    ) -> DeleteMessages {
573        DeleteMessages::new(self.clone(), chat_id, message_ids)
574    }
575    /// Calls `stopPoll` — stops an open poll.
576    pub fn stop_poll(
577        &self,
578        chat_id: impl Into<rustigram_types::user::ChatId>,
579        message_id: i64,
580    ) -> StopPoll {
581        StopPoll::new(self.clone(), chat_id, message_id)
582    }
583    /// Calls `answerCallbackQuery` — acknowledges a callback button press.
584    pub fn answer_callback_query(
585        &self,
586        callback_query_id: impl Into<String>,
587    ) -> AnswerCallbackQuery {
588        AnswerCallbackQuery::new(self.clone(), callback_query_id)
589    }
590
591    // ── Editing ───────────────────────────────────────────────────────────────
592
593    /// Calls `editMessageText` — edits the text of a sent message.
594    pub fn edit_message_text(
595        &self,
596        chat_id: impl Into<rustigram_types::user::ChatId>,
597        message_id: i64,
598        text: impl Into<String>,
599    ) -> EditMessageText {
600        EditMessageText::in_chat(self.clone(), chat_id, message_id, text)
601    }
602    /// Calls `editMessageText` for an inline message sent via inline mode.
603    pub fn edit_inline_message_text(
604        &self,
605        inline_message_id: impl Into<String>,
606        text: impl Into<String>,
607    ) -> EditMessageText {
608        EditMessageText::inline(self.clone(), inline_message_id, text)
609    }
610    /// Calls `editMessageCaption` — edits the caption of a media message.
611    pub fn edit_message_caption(
612        &self,
613        chat_id: impl Into<rustigram_types::user::ChatId>,
614        message_id: i64,
615    ) -> EditMessageCaption {
616        EditMessageCaption::in_chat(self.clone(), chat_id, message_id)
617    }
618    /// Calls `editMessageReplyMarkup` — replaces the inline keyboard of a message.
619    pub fn edit_message_reply_markup(
620        &self,
621        chat_id: impl Into<rustigram_types::user::ChatId>,
622        message_id: i64,
623    ) -> EditMessageReplyMarkup {
624        EditMessageReplyMarkup::in_chat(self.clone(), chat_id, message_id)
625    }
626    /// Calls `editMessageLiveLocation` — updates the position of a live location.
627    pub fn edit_message_live_location(
628        &self,
629        chat_id: impl Into<rustigram_types::user::ChatId>,
630        message_id: i64,
631        latitude: f64,
632        longitude: f64,
633    ) -> EditMessageLiveLocation {
634        EditMessageLiveLocation::in_chat(self.clone(), chat_id, message_id, latitude, longitude)
635    }
636    /// Calls `stopMessageLiveLocation` — stops a live location from updating.
637    pub fn stop_message_live_location(
638        &self,
639        chat_id: impl Into<rustigram_types::user::ChatId>,
640        message_id: i64,
641    ) -> StopMessageLiveLocation {
642        StopMessageLiveLocation::in_chat(self.clone(), chat_id, message_id)
643    }
644
645    // ── Chat management ───────────────────────────────────────────────────────
646
647    /// Calls `banChatMember` — bans a user from a chat.
648    pub fn ban_chat_member(
649        &self,
650        chat_id: impl Into<rustigram_types::user::ChatId>,
651        user_id: i64,
652    ) -> BanChatMember {
653        BanChatMember::new(self.clone(), chat_id, user_id)
654    }
655    /// Calls `unbanChatMember` — lifts a ban from a user.
656    pub fn unban_chat_member(
657        &self,
658        chat_id: impl Into<rustigram_types::user::ChatId>,
659        user_id: i64,
660    ) -> UnbanChatMember {
661        UnbanChatMember::new(self.clone(), chat_id, user_id)
662    }
663    /// Calls `restrictChatMember` — restricts what a user can do in a chat.
664    pub fn restrict_chat_member(
665        &self,
666        chat_id: impl Into<rustigram_types::user::ChatId>,
667        user_id: i64,
668        permissions: rustigram_types::chat::ChatPermissions,
669    ) -> RestrictChatMember {
670        RestrictChatMember::new(self.clone(), chat_id, user_id, permissions)
671    }
672    /// Calls `promoteChatMember` — grants or revokes admin privileges.
673    pub fn promote_chat_member(
674        &self,
675        chat_id: impl Into<rustigram_types::user::ChatId>,
676        user_id: i64,
677    ) -> PromoteChatMember {
678        PromoteChatMember::new(self.clone(), chat_id, user_id)
679    }
680    /// Calls `createChatInviteLink` — generates a new invite link.
681    pub fn create_chat_invite_link(
682        &self,
683        chat_id: impl Into<rustigram_types::user::ChatId>,
684    ) -> CreateChatInviteLink {
685        CreateChatInviteLink::new(self.clone(), chat_id)
686    }
687    /// Calls `pinChatMessage` — pins a message in a chat.
688    pub fn pin_chat_message(
689        &self,
690        chat_id: impl Into<rustigram_types::user::ChatId>,
691        message_id: i64,
692    ) -> PinChatMessage {
693        PinChatMessage::new(self.clone(), chat_id, message_id)
694    }
695    /// Calls `unpinChatMessage` — unpins a message in a chat.
696    pub fn unpin_chat_message(
697        &self,
698        chat_id: impl Into<rustigram_types::user::ChatId>,
699    ) -> UnpinChatMessage {
700        UnpinChatMessage::new(self.clone(), chat_id)
701    }
702
703    // ── Bot settings ──────────────────────────────────────────────────────────
704
705    /// Calls `setMyCommands` — sets the bot's command list.
706    pub fn set_my_commands(
707        &self,
708        commands: Vec<rustigram_types::user::BotCommand>,
709    ) -> SetMyCommands {
710        SetMyCommands::new(self.clone(), commands)
711    }
712    /// Calls `getMyCommands` — returns the bot's current command list.
713    pub fn get_my_commands(&self) -> GetMyCommands {
714        GetMyCommands::new(self.clone())
715    }
716    /// Calls `setMyName` — changes the bot's display name.
717    pub fn set_my_name(&self) -> SetMyName {
718        SetMyName::new(self.clone())
719    }
720    /// Calls `setMyDescription` — changes the bot's profile description.
721    pub fn set_my_description(&self) -> SetMyDescription {
722        SetMyDescription::new(self.clone())
723    }
724    /// Calls `getChatMenuButton` — returns the current menu button for a private chat.
725    pub fn get_chat_menu_button(&self) -> GetChatMenuButton {
726        GetChatMenuButton::new(self.clone())
727    }
728    /// Calls `getManagedBotToken` — returns the token of a managed bot (Bot API 9.6).
729    pub fn get_managed_bot_token(&self, user_id: i64) -> GetManagedBotToken {
730        GetManagedBotToken::new(self.clone(), user_id)
731    }
732
733    // ── Reactions ─────────────────────────────────────────────────────────────
734
735    /// Calls `setMessageReaction` — sets a reaction on a message.
736    pub fn set_message_reaction(
737        &self,
738        chat_id: impl Into<rustigram_types::user::ChatId>,
739        message_id: i64,
740    ) -> SetMessageReaction {
741        SetMessageReaction::new(self.clone(), chat_id, message_id)
742    }
743
744    // ── Inline mode ───────────────────────────────────────────────────────────
745
746    /// Calls `answerInlineQuery` — sends up to 50 results for an inline query.
747    pub fn answer_inline_query(
748        &self,
749        inline_query_id: impl Into<String>,
750        results: Vec<rustigram_types::inline::InlineQueryResult>,
751    ) -> AnswerInlineQuery {
752        AnswerInlineQuery::new(self.clone(), inline_query_id, results)
753    }
754
755    // ── Payments ──────────────────────────────────────────────────────────────
756
757    /// Calls `sendInvoice` — sends a payment invoice.
758    pub fn send_invoice(
759        &self,
760        chat_id: impl Into<rustigram_types::user::ChatId>,
761        title: impl Into<String>,
762        description: impl Into<String>,
763        payload: impl Into<String>,
764        currency: impl Into<String>,
765        prices: Vec<rustigram_types::payments::LabeledPrice>,
766    ) -> SendInvoice {
767        SendInvoice::new(
768            self.clone(),
769            chat_id,
770            title,
771            description,
772            payload,
773            currency,
774            prices,
775        )
776    }
777    /// Calls `getMyStarBalance` — returns the bot's Telegram Star balance.
778    pub fn get_my_star_balance(&self) -> GetMyStarBalance {
779        GetMyStarBalance::new(self.clone())
780    }
781    /// Calls `getStarTransactions` — returns the bot's Star transaction history.
782    pub fn get_star_transactions(&self) -> GetStarTransactions {
783        GetStarTransactions::new(self.clone())
784    }
785
786    // ── Stickers ──────────────────────────────────────────────────────────────
787
788    /// Calls `getStickerSet` — returns a sticker set by name.
789    pub fn get_sticker_set(&self, name: impl Into<String>) -> GetStickerSet {
790        GetStickerSet::new(self.clone(), name)
791    }
792    /// Calls `getCustomEmojiStickers` — returns stickers for the given custom emoji IDs.
793    pub fn get_custom_emoji_stickers(&self, ids: Vec<impl Into<String>>) -> GetCustomEmojiStickers {
794        GetCustomEmojiStickers::new(self.clone(), ids)
795    }
796    /// Calls `uploadStickerFile` — uploads a sticker file for later use in a set.
797    pub fn upload_sticker_file(
798        &self,
799        user_id: i64,
800        sticker: rustigram_types::file::InputFile,
801        format: rustigram_types::sticker::StickerFormat,
802    ) -> UploadStickerFile {
803        UploadStickerFile::new(self.clone(), user_id, sticker, format)
804    }
805    /// Calls `createNewStickerSet` — creates a new sticker set owned by a user.
806    pub fn create_new_sticker_set(
807        &self,
808        user_id: i64,
809        name: impl Into<String>,
810        title: impl Into<String>,
811        stickers: Vec<rustigram_types::sticker::InputSticker>,
812    ) -> CreateNewStickerSet {
813        CreateNewStickerSet::new(self.clone(), user_id, name, title, stickers)
814    }
815    /// Calls `addStickerToSet` — adds a new sticker to an existing set.
816    pub fn add_sticker_to_set(
817        &self,
818        user_id: i64,
819        name: impl Into<String>,
820        sticker: rustigram_types::sticker::InputSticker,
821    ) -> AddStickerToSet {
822        AddStickerToSet::new(self.clone(), user_id, name, sticker)
823    }
824    /// Calls `setStickerPositionInSet` — moves a sticker to a new position in its set.
825    pub fn set_sticker_position_in_set(
826        &self,
827        sticker: impl Into<String>,
828        position: u32,
829    ) -> SetStickerPositionInSet {
830        SetStickerPositionInSet::new(self.clone(), sticker, position)
831    }
832    /// Calls `deleteStickerFromSet` — removes a sticker from its set.
833    pub fn delete_sticker_from_set(&self, sticker: impl Into<String>) -> DeleteStickerFromSet {
834        DeleteStickerFromSet::new(self.clone(), sticker)
835    }
836    /// Calls `setStickerEmojiList` — updates the emoji list for a sticker.
837    pub fn set_sticker_emoji_list(
838        &self,
839        sticker: impl Into<String>,
840        emoji_list: Vec<impl Into<String>>,
841    ) -> SetStickerEmojiList {
842        SetStickerEmojiList::new(self.clone(), sticker, emoji_list)
843    }
844    /// Calls `setStickerKeywords` — updates the search keywords for a sticker.
845    pub fn set_sticker_keywords(&self, sticker: impl Into<String>) -> SetStickerKeywords {
846        SetStickerKeywords::new(self.clone(), sticker)
847    }
848    /// Calls `setStickerMaskPosition` — updates the mask position for a mask sticker.
849    pub fn set_sticker_mask_position(&self, sticker: impl Into<String>) -> SetStickerMaskPosition {
850        SetStickerMaskPosition::new(self.clone(), sticker)
851    }
852    /// Calls `setStickerSetTitle` — renames a sticker set.
853    pub fn set_sticker_set_title(
854        &self,
855        name: impl Into<String>,
856        title: impl Into<String>,
857    ) -> SetStickerSetTitle {
858        SetStickerSetTitle::new(self.clone(), name, title)
859    }
860    /// Calls `deleteStickerSet` — deletes a sticker set created by the bot.
861    pub fn delete_sticker_set(&self, name: impl Into<String>) -> DeleteStickerSet {
862        DeleteStickerSet::new(self.clone(), name)
863    }
864    /// Calls `getForumTopicIconStickers` — returns all available forum topic icon stickers.
865    pub fn get_forum_topic_icon_stickers(&self) -> GetForumTopicIconStickers {
866        GetForumTopicIconStickers::new(self.clone())
867    }
868
869    // ── Forum topics ──────────────────────────────────────────────────────────
870
871    /// Calls `createForumTopic` — creates a new topic in a forum supergroup.
872    pub fn create_forum_topic(
873        &self,
874        chat_id: impl Into<rustigram_types::user::ChatId>,
875        name: impl Into<String>,
876    ) -> CreateForumTopic {
877        CreateForumTopic::new(self.clone(), chat_id, name)
878    }
879    /// Calls `editForumTopic` — edits the name or icon of a forum topic.
880    pub fn edit_forum_topic(
881        &self,
882        chat_id: impl Into<rustigram_types::user::ChatId>,
883        thread_id: i64,
884    ) -> EditForumTopic {
885        EditForumTopic::new(self.clone(), chat_id, thread_id)
886    }
887    /// Calls `closeForumTopic` — closes an open forum topic.
888    pub fn close_forum_topic(
889        &self,
890        chat_id: impl Into<rustigram_types::user::ChatId>,
891        thread_id: i64,
892    ) -> CloseForumTopic {
893        CloseForumTopic::new(self.clone(), chat_id, thread_id)
894    }
895    /// Calls `reopenForumTopic` — reopens a closed forum topic.
896    pub fn reopen_forum_topic(
897        &self,
898        chat_id: impl Into<rustigram_types::user::ChatId>,
899        thread_id: i64,
900    ) -> ReopenForumTopic {
901        ReopenForumTopic::new(self.clone(), chat_id, thread_id)
902    }
903    /// Calls `deleteForumTopic` — deletes a forum topic and all its messages.
904    pub fn delete_forum_topic(
905        &self,
906        chat_id: impl Into<rustigram_types::user::ChatId>,
907        thread_id: i64,
908    ) -> DeleteForumTopic {
909        DeleteForumTopic::new(self.clone(), chat_id, thread_id)
910    }
911    /// Calls `editGeneralForumTopic` — renames the General topic.
912    pub fn edit_general_forum_topic(
913        &self,
914        chat_id: impl Into<rustigram_types::user::ChatId>,
915        name: impl Into<String>,
916    ) -> EditGeneralForumTopic {
917        EditGeneralForumTopic::new(self.clone(), chat_id, name)
918    }
919    /// Calls `closeGeneralForumTopic` — closes the General topic.
920    pub fn close_general_forum_topic(
921        &self,
922        chat_id: impl Into<rustigram_types::user::ChatId>,
923    ) -> CloseGeneralForumTopic {
924        CloseGeneralForumTopic::new(self.clone(), chat_id)
925    }
926    /// Calls `reopenGeneralForumTopic` — reopens the General topic.
927    pub fn reopen_general_forum_topic(
928        &self,
929        chat_id: impl Into<rustigram_types::user::ChatId>,
930    ) -> ReopenGeneralForumTopic {
931        ReopenGeneralForumTopic::new(self.clone(), chat_id)
932    }
933    /// Calls `hideGeneralForumTopic` — hides the General topic from the topic list.
934    pub fn hide_general_forum_topic(
935        &self,
936        chat_id: impl Into<rustigram_types::user::ChatId>,
937    ) -> HideGeneralForumTopic {
938        HideGeneralForumTopic::new(self.clone(), chat_id)
939    }
940    /// Calls `unhideGeneralForumTopic` — makes the General topic visible again.
941    pub fn unhide_general_forum_topic(
942        &self,
943        chat_id: impl Into<rustigram_types::user::ChatId>,
944    ) -> UnhideGeneralForumTopic {
945        UnhideGeneralForumTopic::new(self.clone(), chat_id)
946    }
947
948    // ── Verification ──────────────────────────────────────────────────────────
949
950    /// Calls `verifyUser` — verifies a user on behalf of the organisation.
951    pub fn verify_user(&self, user_id: i64) -> VerifyUser {
952        VerifyUser::new(self.clone(), user_id)
953    }
954    /// Calls `verifyChat` — verifies a chat on behalf of the organisation.
955    pub fn verify_chat(&self, chat_id: impl Into<rustigram_types::user::ChatId>) -> VerifyChat {
956        VerifyChat::new(self.clone(), chat_id)
957    }
958    /// Calls `removeUserVerification` — removes verification from a user.
959    pub fn remove_user_verification(&self, user_id: i64) -> RemoveUserVerification {
960        RemoveUserVerification::new(self.clone(), user_id)
961    }
962    /// Calls `removeChatVerification` — removes verification from a chat.
963    pub fn remove_chat_verification(
964        &self,
965        chat_id: impl Into<rustigram_types::user::ChatId>,
966    ) -> RemoveChatVerification {
967        RemoveChatVerification::new(self.clone(), chat_id)
968    }
969
970    // ── Business account ──────────────────────────────────────────────────────
971
972    /// Calls `getBusinessConnection` — returns business connection information.
973    pub fn get_business_connection(&self, id: impl Into<String>) -> GetBusinessConnection {
974        GetBusinessConnection::new(self.clone(), id)
975    }
976    /// Calls `readBusinessMessage` — marks a business account message as read.
977    pub fn read_business_message(
978        &self,
979        business_connection_id: impl Into<String>,
980        chat_id: impl Into<rustigram_types::user::ChatId>,
981        message_id: i64,
982    ) -> ReadBusinessMessage {
983        ReadBusinessMessage::new(self.clone(), business_connection_id, chat_id, message_id)
984    }
985    /// Calls `deleteBusinessMessages` — deletes messages from a business account.
986    pub fn delete_business_messages(
987        &self,
988        business_connection_id: impl Into<String>,
989        message_ids: Vec<i64>,
990    ) -> DeleteBusinessMessages {
991        DeleteBusinessMessages::new(self.clone(), business_connection_id, message_ids)
992    }
993    /// Calls `setBusinessAccountName` — sets the name of a managed business account.
994    pub fn set_business_account_name(
995        &self,
996        business_connection_id: impl Into<String>,
997        first_name: impl Into<String>,
998        last_name: Option<String>,
999    ) -> SetBusinessAccountName {
1000        SetBusinessAccountName::new(
1001            self.clone(),
1002            business_connection_id,
1003            first_name.into(),
1004            last_name,
1005        )
1006    }
1007    /// Calls `setBusinessAccountUsername` — sets the username of a managed business account.
1008    pub fn set_business_account_username(
1009        &self,
1010        business_connection_id: impl Into<String>,
1011        username: Option<String>,
1012    ) -> SetBusinessAccountUsername {
1013        SetBusinessAccountUsername::new(self.clone(), business_connection_id, username)
1014    }
1015    /// Calls `setBusinessAccountBio` — sets the bio of a managed business account.
1016    pub fn set_business_account_bio(
1017        &self,
1018        business_connection_id: impl Into<String>,
1019        bio: Option<String>,
1020    ) -> SetBusinessAccountBio {
1021        SetBusinessAccountBio::new(self.clone(), business_connection_id, bio)
1022    }
1023    /// Calls `getBusinessAccountStarBalance` — returns a business account's Star balance.
1024    pub fn get_business_account_star_balance(
1025        &self,
1026        business_connection_id: impl Into<String>,
1027    ) -> GetBusinessAccountStarBalance {
1028        GetBusinessAccountStarBalance::new(self.clone(), business_connection_id)
1029    }
1030    /// Calls `transferBusinessAccountStars` — transfers Stars from a business account to the bot.
1031    pub fn transfer_business_account_stars(
1032        &self,
1033        business_connection_id: impl Into<String>,
1034        star_count: u64,
1035    ) -> TransferBusinessAccountStars {
1036        TransferBusinessAccountStars::new(self.clone(), business_connection_id, star_count)
1037    }
1038}
1039
1040// ─── Helpers ──────────────────────────────────────────────────────────────────
1041
1042#[allow(dead_code)]
1043/// Converts an `InputFile::Bytes` into a multipart `Part` for file uploads.
1044pub(crate) fn input_file_to_part(file: rustigram_types::file::InputFile) -> Option<(String, Part)> {
1045    use rustigram_types::file::InputFile;
1046    match file {
1047        InputFile::Bytes {
1048            filename,
1049            data,
1050            mime_type,
1051        } => {
1052            let part = Part::bytes(data)
1053                .file_name(filename.clone())
1054                .mime_str(&mime_type)
1055                .ok()?;
1056            Some((filename, part))
1057        }
1058        _ => None,
1059    }
1060}
1061
1062fn validate_token(token: &str) -> Result<()> {
1063    let colon = token.find(':').ok_or(Error::InvalidToken)?;
1064    let id_part = &token[..colon];
1065    if id_part.is_empty() || !id_part.chars().all(|c| c.is_ascii_digit()) {
1066        return Err(Error::InvalidToken);
1067    }
1068    if token[colon + 1..].is_empty() {
1069        return Err(Error::InvalidToken);
1070    }
1071    Ok(())
1072}