Skip to main content

rustigram_api/methods/
sending.rs

1use std::future::{Future, IntoFuture};
2use std::pin::Pin;
3
4use reqwest::multipart::{Form, Part};
5use serde::Serialize;
6
7use rustigram_types::file::InputFile;
8use rustigram_types::keyboard::ReplyMarkup;
9use rustigram_types::message::{LinkPreviewOptions, Message, ParseMode, ReplyParameters};
10use rustigram_types::poll::InputPollOption;
11use rustigram_types::user::ChatId;
12
13use crate::client::BotClient;
14use crate::error::Result;
15
16// ─── Helper macro ────────────────────────────────────────────────────────────
17
18/// Generates an `IntoFuture` impl that calls `BotClient::post_json`.
19macro_rules! impl_into_future {
20    ($builder:ident, $return_ty:ty, $method:literal) => {
21        impl IntoFuture for $builder {
22            type Output = Result<$return_ty>;
23            type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
24
25            fn into_future(self) -> Self::IntoFuture {
26                Box::pin(async move { self.client.post_json($method, &self.params).await })
27            }
28        }
29    };
30}
31
32// ─── sendMessage ─────────────────────────────────────────────────────────────
33
34#[derive(Serialize)]
35struct SendMessageParams {
36    chat_id: ChatId,
37    text: String,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    business_connection_id: Option<String>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    message_thread_id: Option<i64>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    parse_mode: Option<ParseMode>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    entities: Option<Vec<rustigram_types::message::MessageEntity>>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    link_preview_options: Option<LinkPreviewOptions>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    disable_notification: Option<bool>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    protect_content: Option<bool>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    allow_paid_broadcast: Option<bool>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    message_effect_id: Option<String>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    reply_parameters: Option<ReplyParameters>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    reply_markup: Option<ReplyMarkup>,
60}
61
62/// Builder for the [`sendMessage`](https://core.telegram.org/bots/api#sendmessage) method.
63pub struct SendMessage {
64    client: BotClient,
65    params: SendMessageParams,
66}
67
68impl SendMessage {
69    pub(crate) fn new(
70        client: BotClient,
71        chat_id: impl Into<ChatId>,
72        text: impl Into<String>,
73    ) -> Self {
74        Self {
75            client,
76            params: SendMessageParams {
77                chat_id: chat_id.into(),
78                text: text.into(),
79                business_connection_id: None,
80                message_thread_id: None,
81                parse_mode: None,
82                entities: None,
83                link_preview_options: None,
84                disable_notification: None,
85                protect_content: None,
86                allow_paid_broadcast: None,
87                message_effect_id: None,
88                reply_parameters: None,
89                reply_markup: None,
90            },
91        }
92    }
93    /// Business connection ID for sending on behalf of a business account.
94    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
95        self.params.business_connection_id = Some(id.into());
96        self
97    }
98    /// Forum topic thread ID.
99    pub fn message_thread_id(mut self, id: i64) -> Self {
100        self.params.message_thread_id = Some(id);
101        self
102    }
103    /// Sets the text parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
104    pub fn parse_mode(mut self, mode: ParseMode) -> Self {
105        self.params.parse_mode = Some(mode);
106        self
107    }
108    /// Sets custom message entities instead of using a parse mode.
109    pub fn entities(mut self, entities: Vec<rustigram_types::message::MessageEntity>) -> Self {
110        self.params.entities = Some(entities);
111        self
112    }
113    /// Configures link preview generation options.
114    pub fn link_preview_options(mut self, opts: LinkPreviewOptions) -> Self {
115        self.params.link_preview_options = Some(opts);
116        self
117    }
118    /// Sends the message silently — the recipient receives no notification sound.
119    pub fn disable_notification(mut self, v: bool) -> Self {
120        self.params.disable_notification = Some(v);
121        self
122    }
123    /// Protects the message from being forwarded or saved.
124    pub fn protect_content(mut self, v: bool) -> Self {
125        self.params.protect_content = Some(v);
126        self
127    }
128    /// Allows sending to large audiences at the cost of Telegram Stars.
129    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
130        self.params.allow_paid_broadcast = Some(v);
131        self
132    }
133    /// Attaches a message effect (animated emoji reaction) to the message.
134    pub fn message_effect_id(mut self, id: impl Into<String>) -> Self {
135        self.params.message_effect_id = Some(id.into());
136        self
137    }
138    /// Reply parameters for this message.
139    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
140        self.params.reply_parameters = Some(rp);
141        self
142    }
143    /// Convenience shortcut for `reply_parameters` — sets the reply-to message ID.
144    pub fn reply_to(mut self, message_id: i64) -> Self {
145        self.params.reply_parameters = Some(ReplyParameters {
146            message_id,
147            chat_id: None,
148            allow_sending_without_reply: None,
149            quote: None,
150            quote_parse_mode: None,
151            quote_entities: None,
152            quote_position: None,
153            poll_option_id: None,
154            checklist_task_id: None,
155        });
156        self
157    }
158    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
159    pub fn reply_markup(mut self, markup: impl Into<ReplyMarkup>) -> Self {
160        self.params.reply_markup = Some(markup.into());
161        self
162    }
163}
164
165impl_into_future!(SendMessage, Message, "sendMessage");
166
167// ─── forwardMessage ───────────────────────────────────────────────────────────
168
169#[derive(Serialize)]
170struct ForwardMessageParams {
171    chat_id: ChatId,
172    from_chat_id: ChatId,
173    message_id: i64,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    message_thread_id: Option<i64>,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    video_start_timestamp: Option<i64>,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    disable_notification: Option<bool>,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    protect_content: Option<bool>,
182}
183
184/// Builder for the [`forwardMessage`](https://core.telegram.org/bots/api#forwardmessage) method.
185pub struct ForwardMessage {
186    client: BotClient,
187    params: ForwardMessageParams,
188}
189
190impl ForwardMessage {
191    pub(crate) fn new(
192        client: BotClient,
193        chat_id: impl Into<ChatId>,
194        from_chat_id: impl Into<ChatId>,
195        message_id: i64,
196    ) -> Self {
197        Self {
198            client,
199            params: ForwardMessageParams {
200                chat_id: chat_id.into(),
201                from_chat_id: from_chat_id.into(),
202                message_id,
203                message_thread_id: None,
204                video_start_timestamp: None,
205                disable_notification: None,
206                protect_content: None,
207            },
208        }
209    }
210    /// Forum topic thread ID.
211    pub fn message_thread_id(mut self, id: i64) -> Self {
212        self.params.message_thread_id = Some(id);
213        self
214    }
215    /// Sends the message silently — the recipient receives no notification sound.
216    pub fn disable_notification(mut self, v: bool) -> Self {
217        self.params.disable_notification = Some(v);
218        self
219    }
220    /// Protects the message from being forwarded or saved.
221    pub fn protect_content(mut self, v: bool) -> Self {
222        self.params.protect_content = Some(v);
223        self
224    }
225}
226
227impl_into_future!(ForwardMessage, Message, "forwardMessage");
228
229// ─── copyMessage ──────────────────────────────────────────────────────────────
230
231#[derive(Serialize)]
232struct CopyMessageParams {
233    chat_id: ChatId,
234    from_chat_id: ChatId,
235    message_id: i64,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    message_thread_id: Option<i64>,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    video_start_timestamp: Option<i64>,
240    #[serde(skip_serializing_if = "Option::is_none")]
241    caption: Option<String>,
242    #[serde(skip_serializing_if = "Option::is_none")]
243    parse_mode: Option<ParseMode>,
244    #[serde(skip_serializing_if = "Option::is_none")]
245    caption_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
246    #[serde(skip_serializing_if = "Option::is_none")]
247    show_caption_above_media: Option<bool>,
248    #[serde(skip_serializing_if = "Option::is_none")]
249    disable_notification: Option<bool>,
250    #[serde(skip_serializing_if = "Option::is_none")]
251    protect_content: Option<bool>,
252    #[serde(skip_serializing_if = "Option::is_none")]
253    reply_parameters: Option<ReplyParameters>,
254    #[serde(skip_serializing_if = "Option::is_none")]
255    reply_markup: Option<ReplyMarkup>,
256}
257
258/// Builder for the [`copyMessage`](https://core.telegram.org/bots/api#copymessage) method.
259pub struct CopyMessage {
260    client: BotClient,
261    params: CopyMessageParams,
262}
263
264impl CopyMessage {
265    pub(crate) fn new(
266        client: BotClient,
267        chat_id: impl Into<ChatId>,
268        from_chat_id: impl Into<ChatId>,
269        message_id: i64,
270    ) -> Self {
271        Self {
272            client,
273            params: CopyMessageParams {
274                chat_id: chat_id.into(),
275                from_chat_id: from_chat_id.into(),
276                message_id,
277                message_thread_id: None,
278                video_start_timestamp: None,
279                caption: None,
280                parse_mode: None,
281                caption_entities: None,
282                show_caption_above_media: None,
283                disable_notification: None,
284                protect_content: None,
285                reply_parameters: None,
286                reply_markup: None,
287            },
288        }
289    }
290    /// Sets the caption (0–1024 characters) for media messages.
291    pub fn caption(mut self, c: impl Into<String>) -> Self {
292        self.params.caption = Some(c.into());
293        self
294    }
295    /// Sets the text parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
296    pub fn parse_mode(mut self, m: ParseMode) -> Self {
297        self.params.parse_mode = Some(m);
298        self
299    }
300    /// Sends the message silently — the recipient receives no notification sound.
301    pub fn disable_notification(mut self, v: bool) -> Self {
302        self.params.disable_notification = Some(v);
303        self
304    }
305    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
306    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
307        self.params.reply_markup = Some(m.into());
308        self
309    }
310}
311
312impl_into_future!(
313    CopyMessage,
314    rustigram_types::message::MessageId,
315    "copyMessage"
316);
317
318// ─── sendChatAction ───────────────────────────────────────────────────────────
319
320#[derive(Serialize)]
321struct SendChatActionParams {
322    chat_id: ChatId,
323    action: ChatAction,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    business_connection_id: Option<String>,
326    #[serde(skip_serializing_if = "Option::is_none")]
327    message_thread_id: Option<i64>,
328}
329
330#[derive(Serialize, Clone, Copy)]
331/// The chat action to display while the bot is preparing a response.
332#[serde(rename_all = "snake_case")]
333pub enum ChatAction {
334    /// Indicates the bot is composing a message.
335    Typing,
336    /// Indicates the bot is uploading a photo.
337    UploadPhoto,
338    /// Indicates the bot is recording a video.
339    RecordVideo,
340    /// Indicates the bot is uploading a video.
341    UploadVideo,
342    /// Indicates the bot is recording a voice note.
343    RecordVoice,
344    /// Indicates the bot is uploading a voice note.
345    UploadVoice,
346    /// Indicates the bot is uploading a document.
347    UploadDocument,
348    /// Indicates the bot is choosing a sticker.
349    ChooseSticker,
350    /// Indicates the bot is finding a location.
351    FindLocation,
352    /// Indicates the bot is recording a video note.
353    RecordVideoNote,
354    /// Indicates the bot is uploading a video note.
355    UploadVideoNote,
356}
357
358/// Builder for the [`sendChatAction`](https://core.telegram.org/bots/api#sendchataction) method.
359pub struct SendChatAction {
360    client: BotClient,
361    params: SendChatActionParams,
362}
363
364impl SendChatAction {
365    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, action: ChatAction) -> Self {
366        Self {
367            client,
368            params: SendChatActionParams {
369                chat_id: chat_id.into(),
370                action,
371                business_connection_id: None,
372                message_thread_id: None,
373            },
374        }
375    }
376    /// Business connection ID for sending on behalf of a business account.
377    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
378        self.params.business_connection_id = Some(id.into());
379        self
380    }
381    /// Forum topic thread ID.
382    pub fn message_thread_id(mut self, id: i64) -> Self {
383        self.params.message_thread_id = Some(id);
384        self
385    }
386}
387
388impl_into_future!(SendChatAction, bool, "sendChatAction");
389
390// ─── sendDice ─────────────────────────────────────────────────────────────────
391
392#[derive(Serialize)]
393struct SendDiceParams {
394    chat_id: ChatId,
395    #[serde(skip_serializing_if = "Option::is_none")]
396    emoji: Option<String>,
397    #[serde(skip_serializing_if = "Option::is_none")]
398    message_thread_id: Option<i64>,
399    #[serde(skip_serializing_if = "Option::is_none")]
400    disable_notification: Option<bool>,
401    #[serde(skip_serializing_if = "Option::is_none")]
402    protect_content: Option<bool>,
403    #[serde(skip_serializing_if = "Option::is_none")]
404    reply_parameters: Option<ReplyParameters>,
405    #[serde(skip_serializing_if = "Option::is_none")]
406    reply_markup: Option<ReplyMarkup>,
407}
408
409/// Builder for the [`sendDice`](https://core.telegram.org/bots/api#senddice) method.
410pub struct SendDice {
411    client: BotClient,
412    params: SendDiceParams,
413}
414
415impl SendDice {
416    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>) -> Self {
417        Self {
418            client,
419            params: SendDiceParams {
420                chat_id: chat_id.into(),
421                emoji: None,
422                message_thread_id: None,
423                disable_notification: None,
424                protect_content: None,
425                reply_parameters: None,
426                reply_markup: None,
427            },
428        }
429    }
430    /// The dice/emoji to animate. One of 🎲 🎯 🏀 ⚽ 🎳 🎰.
431    pub fn emoji(mut self, e: impl Into<String>) -> Self {
432        self.params.emoji = Some(e.into());
433        self
434    }
435    /// Sends the message silently — the recipient receives no notification sound.
436    pub fn disable_notification(mut self, v: bool) -> Self {
437        self.params.disable_notification = Some(v);
438        self
439    }
440    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
441    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
442        self.params.reply_markup = Some(m.into());
443        self
444    }
445}
446
447impl_into_future!(SendDice, Message, "sendDice");
448
449// ─── sendLocation ─────────────────────────────────────────────────────────────
450
451#[derive(Serialize)]
452struct SendLocationParams {
453    chat_id: ChatId,
454    latitude: f64,
455    longitude: f64,
456    #[serde(skip_serializing_if = "Option::is_none")]
457    message_thread_id: Option<i64>,
458    #[serde(skip_serializing_if = "Option::is_none")]
459    horizontal_accuracy: Option<f64>,
460    #[serde(skip_serializing_if = "Option::is_none")]
461    live_period: Option<u32>,
462    #[serde(skip_serializing_if = "Option::is_none")]
463    heading: Option<u16>,
464    #[serde(skip_serializing_if = "Option::is_none")]
465    proximity_alert_radius: Option<u32>,
466    #[serde(skip_serializing_if = "Option::is_none")]
467    disable_notification: Option<bool>,
468    #[serde(skip_serializing_if = "Option::is_none")]
469    protect_content: Option<bool>,
470    #[serde(skip_serializing_if = "Option::is_none")]
471    reply_parameters: Option<ReplyParameters>,
472    #[serde(skip_serializing_if = "Option::is_none")]
473    reply_markup: Option<ReplyMarkup>,
474}
475
476/// Builder for the [`sendLocation`](https://core.telegram.org/bots/api#sendlocation) method.
477pub struct SendLocation {
478    client: BotClient,
479    params: SendLocationParams,
480}
481
482impl SendLocation {
483    pub(crate) fn new(
484        client: BotClient,
485        chat_id: impl Into<ChatId>,
486        latitude: f64,
487        longitude: f64,
488    ) -> Self {
489        Self {
490            client,
491            params: SendLocationParams {
492                chat_id: chat_id.into(),
493                latitude,
494                longitude,
495                message_thread_id: None,
496                horizontal_accuracy: None,
497                live_period: None,
498                heading: None,
499                proximity_alert_radius: None,
500                disable_notification: None,
501                protect_content: None,
502                reply_parameters: None,
503                reply_markup: None,
504            },
505        }
506    }
507    /// Sets the radius of uncertainty for the location, in metres (0–1500).
508    pub fn horizontal_accuracy(mut self, v: f64) -> Self {
509        self.params.horizontal_accuracy = Some(v);
510        self
511    }
512    /// Sets how long the location stays live, in seconds (60–86400).
513    pub fn live_period(mut self, v: u32) -> Self {
514        self.params.live_period = Some(v);
515        self
516    }
517    /// Sets the direction of movement in degrees (1–360).
518    pub fn heading(mut self, v: u16) -> Self {
519        self.params.heading = Some(v);
520        self
521    }
522    /// Sets the maximum distance in metres for proximity alerts.
523    pub fn proximity_alert_radius(mut self, v: u32) -> Self {
524        self.params.proximity_alert_radius = Some(v);
525        self
526    }
527    /// Sends the message silently — the recipient receives no notification sound.
528    pub fn disable_notification(mut self, v: bool) -> Self {
529        self.params.disable_notification = Some(v);
530        self
531    }
532    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
533    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
534        self.params.reply_markup = Some(m.into());
535        self
536    }
537}
538
539impl_into_future!(SendLocation, Message, "sendLocation");
540
541// ─── sendContact ──────────────────────────────────────────────────────────────
542
543#[derive(Serialize)]
544struct SendContactParams {
545    chat_id: ChatId,
546    phone_number: String,
547    first_name: String,
548    #[serde(skip_serializing_if = "Option::is_none")]
549    last_name: Option<String>,
550    #[serde(skip_serializing_if = "Option::is_none")]
551    vcard: Option<String>,
552    #[serde(skip_serializing_if = "Option::is_none")]
553    message_thread_id: Option<i64>,
554    #[serde(skip_serializing_if = "Option::is_none")]
555    disable_notification: Option<bool>,
556    #[serde(skip_serializing_if = "Option::is_none")]
557    protect_content: Option<bool>,
558    #[serde(skip_serializing_if = "Option::is_none")]
559    reply_parameters: Option<ReplyParameters>,
560    #[serde(skip_serializing_if = "Option::is_none")]
561    reply_markup: Option<ReplyMarkup>,
562}
563
564/// Builder for the [`sendContact`](https://core.telegram.org/bots/api#sendcontact) method.
565pub struct SendContact {
566    client: BotClient,
567    params: SendContactParams,
568}
569
570impl SendContact {
571    pub(crate) fn new(
572        client: BotClient,
573        chat_id: impl Into<ChatId>,
574        phone_number: impl Into<String>,
575        first_name: impl Into<String>,
576    ) -> Self {
577        Self {
578            client,
579            params: SendContactParams {
580                chat_id: chat_id.into(),
581                phone_number: phone_number.into(),
582                first_name: first_name.into(),
583                last_name: None,
584                vcard: None,
585                message_thread_id: None,
586                disable_notification: None,
587                protect_content: None,
588                reply_parameters: None,
589                reply_markup: None,
590            },
591        }
592    }
593    /// Sets the last name of the contact.
594    pub fn last_name(mut self, v: impl Into<String>) -> Self {
595        self.params.last_name = Some(v.into());
596        self
597    }
598    /// Sets the vCard data of the contact.
599    pub fn vcard(mut self, v: impl Into<String>) -> Self {
600        self.params.vcard = Some(v.into());
601        self
602    }
603    /// Sends the message silently — the recipient receives no notification sound.
604    pub fn disable_notification(mut self, v: bool) -> Self {
605        self.params.disable_notification = Some(v);
606        self
607    }
608    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
609    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
610        self.params.reply_markup = Some(m.into());
611        self
612    }
613}
614
615impl_into_future!(SendContact, Message, "sendContact");
616
617// ─── sendPoll ─────────────────────────────────────────────────────────────────
618
619#[derive(Serialize)]
620struct SendPollParams {
621    chat_id: ChatId,
622    question: String,
623    options: Vec<InputPollOption>,
624    #[serde(skip_serializing_if = "Option::is_none")]
625    question_parse_mode: Option<ParseMode>,
626    #[serde(skip_serializing_if = "Option::is_none")]
627    question_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
628    #[serde(skip_serializing_if = "Option::is_none")]
629    message_thread_id: Option<i64>,
630    #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
631    poll_type: Option<rustigram_types::poll::PollType>,
632    #[serde(skip_serializing_if = "Option::is_none")]
633    is_anonymous: Option<bool>,
634    #[serde(skip_serializing_if = "Option::is_none")]
635    allows_multiple_answers: Option<bool>,
636    #[serde(skip_serializing_if = "Option::is_none")]
637    allows_revoting: Option<bool>,
638    #[serde(skip_serializing_if = "Option::is_none")]
639    correct_option_ids: Option<Vec<u8>>,
640    #[serde(skip_serializing_if = "Option::is_none")]
641    explanation: Option<String>,
642    #[serde(skip_serializing_if = "Option::is_none")]
643    explanation_parse_mode: Option<ParseMode>,
644    #[serde(skip_serializing_if = "Option::is_none")]
645    open_period: Option<u32>,
646    #[serde(skip_serializing_if = "Option::is_none")]
647    close_date: Option<i64>,
648    #[serde(skip_serializing_if = "Option::is_none")]
649    is_closed: Option<bool>,
650    #[serde(skip_serializing_if = "Option::is_none")]
651    disable_notification: Option<bool>,
652    #[serde(skip_serializing_if = "Option::is_none")]
653    protect_content: Option<bool>,
654    #[serde(skip_serializing_if = "Option::is_none")]
655    reply_parameters: Option<ReplyParameters>,
656    #[serde(skip_serializing_if = "Option::is_none")]
657    reply_markup: Option<ReplyMarkup>,
658}
659
660/// Builder for the [`sendPoll`](https://core.telegram.org/bots/api#sendpoll) method.
661pub struct SendPoll {
662    client: BotClient,
663    params: SendPollParams,
664}
665
666impl SendPoll {
667    pub(crate) fn new(
668        client: BotClient,
669        chat_id: impl Into<ChatId>,
670        question: impl Into<String>,
671        options: Vec<InputPollOption>,
672    ) -> Self {
673        Self {
674            client,
675            params: SendPollParams {
676                chat_id: chat_id.into(),
677                question: question.into(),
678                options,
679                question_parse_mode: None,
680                question_entities: None,
681                message_thread_id: None,
682                poll_type: None,
683                is_anonymous: None,
684                allows_multiple_answers: None,
685                allows_revoting: None,
686                correct_option_ids: None,
687                explanation: None,
688                explanation_parse_mode: None,
689                open_period: None,
690                close_date: None,
691                is_closed: None,
692                disable_notification: None,
693                protect_content: None,
694                reply_parameters: None,
695                reply_markup: None,
696            },
697        }
698    }
699    /// Sets whether the poll is anonymous.
700    pub fn is_anonymous(mut self, v: bool) -> Self {
701        self.params.is_anonymous = Some(v);
702        self
703    }
704    /// Allows voters to select multiple answers.
705    pub fn allows_multiple_answers(mut self, v: bool) -> Self {
706        self.params.allows_multiple_answers = Some(v);
707        self
708    }
709    /// Allows voters to change their vote.
710    pub fn allows_revoting(mut self, v: bool) -> Self {
711        self.params.allows_revoting = Some(v);
712        self
713    }
714    /// Converts the poll to a quiz with the given correct option index.
715    pub fn quiz(mut self, correct_option_id: u8) -> Self {
716        self.params.poll_type = Some(rustigram_types::poll::PollType::Quiz);
717        self.params.correct_option_ids = Some(vec![correct_option_id]);
718        self
719    }
720    /// Sets the explanation text shown after a quiz answer.
721    pub fn explanation(mut self, text: impl Into<String>) -> Self {
722        self.params.explanation = Some(text.into());
723        self
724    }
725    /// Sets how long the poll stays open in seconds (5–2628000).
726    pub fn open_period(mut self, secs: u32) -> Self {
727        self.params.open_period = Some(secs);
728        self
729    }
730    /// Sets the Unix timestamp when the poll closes automatically.
731    pub fn close_date(mut self, ts: i64) -> Self {
732        self.params.close_date = Some(ts);
733        self
734    }
735    /// Sends the message silently — the recipient receives no notification sound.
736    pub fn disable_notification(mut self, v: bool) -> Self {
737        self.params.disable_notification = Some(v);
738        self
739    }
740    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
741    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
742        self.params.reply_markup = Some(m.into());
743        self
744    }
745}
746
747impl_into_future!(SendPoll, Message, "sendPoll");
748
749// ─── sendMessageDraft ─────────────────────────────────────────────────────────
750
751#[derive(Serialize)]
752struct SendMessageDraftParams {
753    chat_id: ChatId,
754    draft_id: i64,
755    text: String,
756    #[serde(skip_serializing_if = "Option::is_none")]
757    message_thread_id: Option<i64>,
758    #[serde(skip_serializing_if = "Option::is_none")]
759    parse_mode: Option<ParseMode>,
760    #[serde(skip_serializing_if = "Option::is_none")]
761    entities: Option<Vec<rustigram_types::message::MessageEntity>>,
762}
763
764/// Builder for the [`sendMessageDraft`](https://core.telegram.org/bots/api#sendmessagedraft) method.
765/// Streams a partial message to the user while it is being generated (Bot API 9.5+).
766pub struct SendMessageDraft {
767    client: BotClient,
768    params: SendMessageDraftParams,
769}
770
771impl SendMessageDraft {
772    pub(crate) fn new(
773        client: BotClient,
774        chat_id: impl Into<ChatId>,
775        draft_id: i64,
776        text: impl Into<String>,
777    ) -> Self {
778        Self {
779            client,
780            params: SendMessageDraftParams {
781                chat_id: chat_id.into(),
782                draft_id,
783                text: text.into(),
784                message_thread_id: None,
785                parse_mode: None,
786                entities: None,
787            },
788        }
789    }
790    /// Sets the text parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
791    pub fn parse_mode(mut self, m: ParseMode) -> Self {
792        self.params.parse_mode = Some(m);
793        self
794    }
795    /// Sets custom message entities instead of using a parse mode.
796    pub fn entities(mut self, e: Vec<rustigram_types::message::MessageEntity>) -> Self {
797        self.params.entities = Some(e);
798        self
799    }
800}
801
802impl_into_future!(SendMessageDraft, bool, "sendMessageDraft");
803
804// ─── File-sending builders ────────────────────────────────────────────────────
805//
806// Photo, Audio, Document, Video, Animation, Voice, VideoNote each share a
807// similar shape but differ in field names and constraints. We use a common
808// pattern: store the InputFile and an optional Form for multipart, and build
809// the form lazily in `IntoFuture`.
810
811/// Common optional parameters shared by most media-send methods.
812#[derive(Default)]
813pub struct MediaSendOptions {
814    /// Business connection ID for sending on behalf of a business account.
815    pub business_connection_id: Option<String>,
816    /// Forum topic thread ID.
817    pub message_thread_id: Option<i64>,
818    /// Sets the caption (0–1024 characters) for media messages.
819    pub caption: Option<String>,
820    /// Parse mode for the caption.
821    pub parse_mode: Option<ParseMode>,
822    /// Special entities in the caption.
823    pub caption_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
824    /// Shows the caption above the media instead of below it.
825    pub show_caption_above_media: Option<bool>,
826    /// Marks the media as a spoiler — blurs it until the user taps to reveal.
827    pub has_spoiler: Option<bool>,
828    /// Sends the message silently — the recipient receives no notification sound.
829    pub disable_notification: Option<bool>,
830    /// Protects the message from being forwarded or saved.
831    pub protect_content: Option<bool>,
832    /// Allows sending to large audiences at the cost of Telegram Stars.
833    pub allow_paid_broadcast: Option<bool>,
834    /// Reply parameters for this message.
835    pub reply_parameters: Option<ReplyParameters>,
836    /// Reply markup attached to the message.
837    pub reply_markup: Option<ReplyMarkup>,
838}
839
840/// Builds the JSON body for a simple (non-file-upload) part of a media send.
841fn media_json_body(
842    chat_id: &ChatId,
843    media_field: &str,
844    media_value: &str,
845    opts: &MediaSendOptions,
846    extra: serde_json::Value,
847) -> serde_json::Value {
848    let mut map = serde_json::json!({
849        "chat_id": chat_id,
850        media_field: media_value,
851    });
852    let obj = map.as_object_mut().unwrap();
853    if let Some(v) = &opts.business_connection_id {
854        obj.insert("business_connection_id".to_owned(), serde_json::json!(v));
855    }
856    if let Some(v) = &opts.message_thread_id {
857        obj.insert("message_thread_id".to_owned(), serde_json::json!(v));
858    }
859    if let Some(v) = &opts.caption {
860        obj.insert("caption".to_owned(), serde_json::json!(v));
861    }
862    if let Some(v) = &opts.parse_mode {
863        obj.insert("parse_mode".to_owned(), serde_json::json!(v));
864    }
865    if let Some(v) = &opts.caption_entities {
866        obj.insert("caption_entities".to_owned(), serde_json::json!(v));
867    }
868    if let Some(v) = opts.show_caption_above_media {
869        obj.insert("show_caption_above_media".to_owned(), serde_json::json!(v));
870    }
871    if let Some(v) = opts.has_spoiler {
872        obj.insert("has_spoiler".to_owned(), serde_json::json!(v));
873    }
874    if let Some(v) = opts.disable_notification {
875        obj.insert("disable_notification".to_owned(), serde_json::json!(v));
876    }
877    if let Some(v) = opts.protect_content {
878        obj.insert("protect_content".to_owned(), serde_json::json!(v));
879    }
880    if let Some(v) = opts.allow_paid_broadcast {
881        obj.insert("allow_paid_broadcast".to_owned(), serde_json::json!(v));
882    }
883    if let Some(v) = &opts.reply_parameters {
884        obj.insert("reply_parameters".to_owned(), serde_json::json!(v));
885    }
886    if let Some(v) = &opts.reply_markup {
887        obj.insert("reply_markup".to_owned(), serde_json::json!(v));
888    }
889    if let serde_json::Value::Object(extra_obj) = extra {
890        for (k, v) in extra_obj {
891            obj.insert(k, v);
892        }
893    }
894    map
895}
896
897// ─── sendPhoto ────────────────────────────────────────────────────────────────
898
899/// Builder for the [`sendPhoto`](https://core.telegram.org/bots/api#sendphoto) method.
900pub struct SendPhoto {
901    client: BotClient,
902    chat_id: ChatId,
903    photo: InputFile,
904    opts: MediaSendOptions,
905}
906
907impl SendPhoto {
908    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, photo: InputFile) -> Self {
909        Self {
910            client,
911            chat_id: chat_id.into(),
912            photo,
913            opts: MediaSendOptions::default(),
914        }
915    }
916    /// Sets the caption (0–1024 characters) for media messages.
917    pub fn caption(mut self, c: impl Into<String>) -> Self {
918        self.opts.caption = Some(c.into());
919        self
920    }
921    /// Sets the caption parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
922    pub fn parse_mode(mut self, m: ParseMode) -> Self {
923        self.opts.parse_mode = Some(m);
924        self
925    }
926    /// Marks the media as a spoiler — blurs it until the user taps to reveal.
927    pub fn has_spoiler(mut self, v: bool) -> Self {
928        self.opts.has_spoiler = Some(v);
929        self
930    }
931    /// Shows the caption above the media instead of below it.
932    pub fn show_caption_above_media(mut self, v: bool) -> Self {
933        self.opts.show_caption_above_media = Some(v);
934        self
935    }
936    /// Sends the message silently — the recipient receives no notification sound.
937    pub fn disable_notification(mut self, v: bool) -> Self {
938        self.opts.disable_notification = Some(v);
939        self
940    }
941    /// Protects the message from being forwarded or saved.
942    pub fn protect_content(mut self, v: bool) -> Self {
943        self.opts.protect_content = Some(v);
944        self
945    }
946    /// Allows sending to large audiences at the cost of Telegram Stars.
947    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
948        self.opts.allow_paid_broadcast = Some(v);
949        self
950    }
951    /// Reply parameters for this message.
952    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
953        self.opts.reply_parameters = Some(rp);
954        self
955    }
956    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
957    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
958        self.opts.reply_markup = Some(m.into());
959        self
960    }
961}
962
963impl IntoFuture for SendPhoto {
964    type Output = Result<Message>;
965    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
966
967    fn into_future(self) -> Self::IntoFuture {
968        Box::pin(async move {
969            match &self.photo {
970                InputFile::Bytes {
971                    filename,
972                    data,
973                    mime_type,
974                } => {
975                    let part = Part::bytes(data.clone())
976                        .file_name(filename.clone())
977                        .mime_str(mime_type)
978                        .map_err(|e| crate::error::Error::Decode(e.to_string()))?;
979                    let mut form = Form::new().part("photo", part);
980                    form = form.text("chat_id", self.chat_id.to_string());
981                    if let Some(c) = &self.opts.caption {
982                        form = form.text("caption", c.clone());
983                    }
984                    if let Some(m) = &self.opts.parse_mode {
985                        form = form.text("parse_mode", format!("{m:?}"));
986                    }
987                    if let Some(v) = self.opts.disable_notification {
988                        form = form.text("disable_notification", v.to_string());
989                    }
990                    if let Some(v) = self.opts.has_spoiler {
991                        form = form.text("has_spoiler", v.to_string());
992                    }
993                    if let Some(v) = &self.opts.reply_markup {
994                        form = form.text("reply_markup", serde_json::to_string(v).unwrap());
995                    }
996                    self.client.post_multipart("sendPhoto", form).await
997                }
998                _ => {
999                    let body = media_json_body(
1000                        &self.chat_id,
1001                        "photo",
1002                        self.photo.as_str(),
1003                        &self.opts,
1004                        serde_json::Value::Null,
1005                    );
1006                    self.client.post_json("sendPhoto", &body).await
1007                }
1008            }
1009        })
1010    }
1011}
1012
1013// ─── Macro for simpler media senders (Audio, Document, Video, Animation, Voice, VideoNote, Sticker)
1014
1015macro_rules! media_sender {
1016    ($(#[$doc:meta])* $name:ident, $field:literal, $method:literal, $return_ty:ty, [$($extra_field:ident: $extra_ty:ty),*]) => {
1017        $(#[$doc])*
1018        pub struct $name {
1019            /// The API client to use for sending the request.
1020            client: BotClient,
1021            /// Unique identifier for the target chat or username of the target channel.
1022            chat_id: ChatId,
1023            /// The file to send. Can be a file ID, URL, or new upload.
1024            file: InputFile,
1025            /// Common optional parameters for media sending.
1026            opts: MediaSendOptions,
1027            /// Extra optional parameters specific to this media type.
1028            $($extra_field: Option<$extra_ty>,)*
1029        }
1030
1031        impl $name {
1032            pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, file: InputFile) -> Self {
1033                Self {
1034                    client,
1035                    chat_id: chat_id.into(),
1036                    file,
1037                    opts: MediaSendOptions::default(),
1038                    $($extra_field: None,)*
1039                }
1040            }
1041            /// Sets the caption (0–1024 characters) for media messages.
1042            pub fn caption(mut self, c: impl Into<String>) -> Self { self.opts.caption = Some(c.into()); self }
1043            /// Sets the caption parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
1044            pub fn parse_mode(mut self, m: ParseMode) -> Self { self.opts.parse_mode = Some(m); self }
1045            /// Sends the message silently — the recipient receives no notification sound.
1046            pub fn disable_notification(mut self, v: bool) -> Self { self.opts.disable_notification = Some(v); self }
1047            /// Protects the message from being forwarded or saved.
1048            pub fn protect_content(mut self, v: bool) -> Self { self.opts.protect_content = Some(v); self }
1049            /// Allows sending to large audiences at the cost of Telegram Stars.
1050            pub fn allow_paid_broadcast(mut self, v: bool) -> Self { self.opts.allow_paid_broadcast = Some(v); self }
1051            /// Reply parameters for this message.
1052            pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self { self.opts.reply_parameters = Some(rp); self }
1053            /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1054            pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self { self.opts.reply_markup = Some(m.into()); self }
1055        }
1056
1057        impl IntoFuture for $name {
1058            type Output = Result<$return_ty>;
1059            type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1060
1061            fn into_future(self) -> Self::IntoFuture {
1062                Box::pin(async move {
1063                    match &self.file {
1064                        InputFile::Bytes { filename, data, mime_type } => {
1065                            let part = Part::bytes(data.clone())
1066                                .file_name(filename.clone())
1067                                .mime_str(mime_type)
1068                                .map_err(|e| crate::error::Error::Decode(e.to_string()))?;
1069                            let mut form = Form::new().part($field, part);
1070                            form = form.text("chat_id", self.chat_id.to_string());
1071                            if let Some(c) = &self.opts.caption { form = form.text("caption", c.clone()); }
1072                            if let Some(v) = self.opts.disable_notification { form = form.text("disable_notification", v.to_string()); }
1073                            if let Some(v) = &self.opts.reply_markup { form = form.text("reply_markup", serde_json::to_string(v).unwrap()); }
1074                            self.client.post_multipart($method, form).await
1075                        }
1076                        _ => {
1077                            let mut extra = serde_json::json!({});
1078                            $(
1079                                if let Some(ref v) = self.$extra_field {
1080                                    extra[stringify!($extra_field)] = serde_json::json!(v);
1081                                }
1082                            )*
1083                            let body = media_json_body(&self.chat_id, $field, self.file.as_str(), &self.opts, extra);
1084                            self.client.post_json($method, &body).await
1085                        }
1086                    }
1087                })
1088            }
1089        }
1090    };
1091}
1092
1093media_sender!(
1094    /// Builder for the [`sendAudio`](https://core.telegram.org/bots/api#sendaudio) method.
1095    SendAudio,     "audio",      "sendAudio",     Message, [duration: u32, performer: String, title: String]);
1096media_sender!(
1097    /// Builder for the [`sendDocument`](https://core.telegram.org/bots/api#senddocument) method.
1098    SendDocument,  "document",   "sendDocument",  Message, [disable_content_type_detection: bool]);
1099media_sender!(
1100    /// Builder for the [`sendVideo`](https://core.telegram.org/bots/api#sendvideo) method.
1101    SendVideo,     "video",      "sendVideo",     Message, [duration: u32, width: u32, height: u32, supports_streaming: bool]);
1102media_sender!(
1103    /// Builder for the [`sendAnimation`](https://core.telegram.org/bots/api#sendanimation) method.
1104    SendAnimation, "animation",  "sendAnimation", Message, [duration: u32, width: u32, height: u32]);
1105media_sender!(
1106    /// Builder for the [`sendVoice`](https://core.telegram.org/bots/api#sendvoice) method.
1107    SendVoice,     "voice",      "sendVoice",     Message, [duration: u32]);
1108media_sender!(
1109    /// Builder for the [`sendVideoNote`](https://core.telegram.org/bots/api#sendvideonote) method.
1110    SendVideoNote, "video_note", "sendVideoNote", Message, [duration: u32, length: u32]);
1111media_sender!(
1112    /// Builder for the [`sendSticker`](https://core.telegram.org/bots/api#sendsticker) method.
1113    SendSticker,   "sticker",    "sendSticker",   Message, [emoji: String]);
1114
1115// ─── deleteMessage / deleteMessages ──────────────────────────────────────────
1116
1117#[derive(Serialize)]
1118struct DeleteMessageParams {
1119    chat_id: ChatId,
1120    message_id: i64,
1121}
1122
1123/// Builder for the [`deleteMessage`](https://core.telegram.org/bots/api#deletemessage) method.
1124pub struct DeleteMessage {
1125    client: BotClient,
1126    params: DeleteMessageParams,
1127}
1128impl DeleteMessage {
1129    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, message_id: i64) -> Self {
1130        Self {
1131            client,
1132            params: DeleteMessageParams {
1133                chat_id: chat_id.into(),
1134                message_id,
1135            },
1136        }
1137    }
1138}
1139impl_into_future!(DeleteMessage, bool, "deleteMessage");
1140
1141#[derive(Serialize)]
1142struct DeleteMessagesParams {
1143    chat_id: ChatId,
1144    message_ids: Vec<i64>,
1145}
1146
1147/// Builder for the [`deleteMessages`](https://core.telegram.org/bots/api#deletemessages) method.
1148pub struct DeleteMessages {
1149    client: BotClient,
1150    params: DeleteMessagesParams,
1151}
1152impl DeleteMessages {
1153    pub(crate) fn new(
1154        client: BotClient,
1155        chat_id: impl Into<ChatId>,
1156        message_ids: Vec<i64>,
1157    ) -> Self {
1158        Self {
1159            client,
1160            params: DeleteMessagesParams {
1161                chat_id: chat_id.into(),
1162                message_ids,
1163            },
1164        }
1165    }
1166}
1167impl_into_future!(DeleteMessages, bool, "deleteMessages");
1168
1169// ─── stopPoll ─────────────────────────────────────────────────────────────────
1170
1171#[derive(Serialize)]
1172struct StopPollParams {
1173    chat_id: ChatId,
1174    message_id: i64,
1175    #[serde(skip_serializing_if = "Option::is_none")]
1176    reply_markup: Option<rustigram_types::keyboard::InlineKeyboardMarkup>,
1177}
1178
1179/// Builder for the [`stopPoll`](https://core.telegram.org/bots/api#stoppoll) method.
1180pub struct StopPoll {
1181    client: BotClient,
1182    params: StopPollParams,
1183}
1184impl StopPoll {
1185    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, message_id: i64) -> Self {
1186        Self {
1187            client,
1188            params: StopPollParams {
1189                chat_id: chat_id.into(),
1190                message_id,
1191                reply_markup: None,
1192            },
1193        }
1194    }
1195    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1196    pub fn reply_markup(mut self, m: rustigram_types::keyboard::InlineKeyboardMarkup) -> Self {
1197        self.params.reply_markup = Some(m);
1198        self
1199    }
1200}
1201impl_into_future!(StopPoll, rustigram_types::poll::Poll, "stopPoll");
1202
1203// ─── answerCallbackQuery ──────────────────────────────────────────────────────
1204
1205#[derive(Serialize)]
1206struct AnswerCallbackQueryParams {
1207    callback_query_id: String,
1208    #[serde(skip_serializing_if = "Option::is_none")]
1209    text: Option<String>,
1210    #[serde(skip_serializing_if = "Option::is_none")]
1211    show_alert: Option<bool>,
1212    #[serde(skip_serializing_if = "Option::is_none")]
1213    url: Option<String>,
1214    #[serde(skip_serializing_if = "Option::is_none")]
1215    cache_time: Option<u32>,
1216}
1217
1218/// Builder for the [`answerCallbackQuery`](https://core.telegram.org/bots/api#answercallbackquery) method.
1219pub struct AnswerCallbackQuery {
1220    client: BotClient,
1221    params: AnswerCallbackQueryParams,
1222}
1223impl AnswerCallbackQuery {
1224    pub(crate) fn new(client: BotClient, callback_query_id: impl Into<String>) -> Self {
1225        Self {
1226            client,
1227            params: AnswerCallbackQueryParams {
1228                callback_query_id: callback_query_id.into(),
1229                text: None,
1230                show_alert: None,
1231                url: None,
1232                cache_time: None,
1233            },
1234        }
1235    }
1236    /// The text of the notification shown to the user. 0–200 characters.
1237    pub fn text(mut self, t: impl Into<String>) -> Self {
1238        self.params.text = Some(t.into());
1239        self
1240    }
1241    /// Shows an alert dialog instead of a toast notification for the callback answer.
1242    pub fn show_alert(mut self, v: bool) -> Self {
1243        self.params.show_alert = Some(v);
1244        self
1245    }
1246    /// Sets the URL to open when the callback button answer is tapped.
1247    pub fn url(mut self, u: impl Into<String>) -> Self {
1248        self.params.url = Some(u.into());
1249        self
1250    }
1251    /// Sets how long the callback answer may be cached on the client in seconds.
1252    pub fn cache_time(mut self, secs: u32) -> Self {
1253        self.params.cache_time = Some(secs);
1254        self
1255    }
1256    /// Shorthand for `.text(t).show_alert(true)` — shows a popup alert to the user.
1257    pub fn alert(self, text: impl Into<String>) -> Self {
1258        self.text(text).show_alert(true)
1259    }
1260}
1261impl_into_future!(AnswerCallbackQuery, bool, "answerCallbackQuery");