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::suggested_post::SuggestedPostParameters;
12use rustigram_types::user::ChatId;
13
14use crate::client::BotClient;
15use crate::error::Result;
16
17// ─── Helper macro ────────────────────────────────────────────────────────────
18
19/// Generates an `IntoFuture` impl that calls `BotClient::post_json`.
20macro_rules! impl_into_future {
21    ($builder:ident, $return_ty:ty, $method:literal) => {
22        impl IntoFuture for $builder {
23            type Output = Result<$return_ty>;
24            type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
25
26            fn into_future(self) -> Self::IntoFuture {
27                Box::pin(async move { self.client.post_json($method, &self.params).await })
28            }
29        }
30    };
31}
32
33// ─── sendMessage ─────────────────────────────────────────────────────────────
34
35#[derive(Serialize)]
36struct SendMessageParams {
37    chat_id: ChatId,
38    text: String,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    business_connection_id: Option<String>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    message_thread_id: Option<i64>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    direct_messages_topic_id: Option<i64>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    parse_mode: Option<ParseMode>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    entities: Option<Vec<rustigram_types::message::MessageEntity>>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    link_preview_options: Option<LinkPreviewOptions>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    disable_notification: Option<bool>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    protect_content: Option<bool>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    allow_paid_broadcast: Option<bool>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    message_effect_id: Option<String>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    reply_parameters: Option<ReplyParameters>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    reply_markup: Option<ReplyMarkup>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    suggested_post_parameters: Option<SuggestedPostParameters>,
65}
66
67/// Builder for the [`sendMessage`](https://core.telegram.org/bots/api#sendmessage) method.
68pub struct SendMessage {
69    client: BotClient,
70    params: SendMessageParams,
71}
72
73impl SendMessage {
74    pub(crate) fn new(
75        client: BotClient,
76        chat_id: impl Into<ChatId>,
77        text: impl Into<String>,
78    ) -> Self {
79        Self {
80            client,
81            params: SendMessageParams {
82                chat_id: chat_id.into(),
83                text: text.into(),
84                business_connection_id: None,
85                message_thread_id: None,
86                direct_messages_topic_id: None,
87                parse_mode: None,
88                entities: None,
89                link_preview_options: None,
90                disable_notification: None,
91                protect_content: None,
92                allow_paid_broadcast: None,
93                message_effect_id: None,
94                reply_parameters: None,
95                reply_markup: None,
96                suggested_post_parameters: None,
97            },
98        }
99    }
100    /// Business connection ID for sending on behalf of a business account.
101    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
102        self.params.business_connection_id = Some(id.into());
103        self
104    }
105    /// Forum topic thread ID.
106    pub fn message_thread_id(mut self, id: i64) -> Self {
107        self.params.message_thread_id = Some(id);
108        self
109    }
110    /// Identifier of a direct messages chat topic.
111    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
112        self.params.direct_messages_topic_id = Some(id);
113        self
114    }
115    /// Sets the text parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
116    pub fn parse_mode(mut self, mode: ParseMode) -> Self {
117        self.params.parse_mode = Some(mode);
118        self
119    }
120    /// Sets custom message entities instead of using a parse mode.
121    pub fn entities(mut self, entities: Vec<rustigram_types::message::MessageEntity>) -> Self {
122        self.params.entities = Some(entities);
123        self
124    }
125    /// Configures link preview generation options.
126    pub fn link_preview_options(mut self, opts: LinkPreviewOptions) -> Self {
127        self.params.link_preview_options = Some(opts);
128        self
129    }
130    /// Sends the message silently — the recipient receives no notification sound.
131    pub fn disable_notification(mut self, v: bool) -> Self {
132        self.params.disable_notification = Some(v);
133        self
134    }
135    /// Protects the message from being forwarded or saved.
136    pub fn protect_content(mut self, v: bool) -> Self {
137        self.params.protect_content = Some(v);
138        self
139    }
140    /// Allows sending to large audiences at the cost of Telegram Stars.
141    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
142        self.params.allow_paid_broadcast = Some(v);
143        self
144    }
145    /// Attaches a message effect (animated emoji reaction) to the message.
146    pub fn message_effect_id(mut self, id: impl Into<String>) -> Self {
147        self.params.message_effect_id = Some(id.into());
148        self
149    }
150    /// Reply parameters for this message.
151    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
152        self.params.reply_parameters = Some(rp);
153        self
154    }
155    /// Convenience shortcut for `reply_parameters` — sets the reply-to message ID.
156    pub fn reply_to(mut self, message_id: i64) -> Self {
157        self.params.reply_parameters = Some(ReplyParameters {
158            message_id,
159            chat_id: None,
160            allow_sending_without_reply: None,
161            quote: None,
162            quote_parse_mode: None,
163            quote_entities: None,
164            quote_position: None,
165            poll_option_id: None,
166            checklist_task_id: None,
167        });
168        self
169    }
170    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
171    pub fn reply_markup(mut self, markup: impl Into<ReplyMarkup>) -> Self {
172        self.params.reply_markup = Some(markup.into());
173        self
174    }
175    /// Suggested post parameters for channel direct messages chats.
176    pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self {
177        self.params.suggested_post_parameters = Some(params);
178        self
179    }
180}
181
182impl_into_future!(SendMessage, Message, "sendMessage");
183
184// ─── forwardMessage ───────────────────────────────────────────────────────────
185
186#[derive(Serialize)]
187struct ForwardMessageParams {
188    chat_id: ChatId,
189    from_chat_id: ChatId,
190    message_id: i64,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    message_thread_id: Option<i64>,
193    #[serde(skip_serializing_if = "Option::is_none")]
194    direct_messages_topic_id: Option<i64>,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    video_start_timestamp: Option<i64>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    disable_notification: Option<bool>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    protect_content: Option<bool>,
201}
202
203/// Builder for the [`forwardMessage`](https://core.telegram.org/bots/api#forwardmessage) method.
204pub struct ForwardMessage {
205    client: BotClient,
206    params: ForwardMessageParams,
207}
208
209impl ForwardMessage {
210    pub(crate) fn new(
211        client: BotClient,
212        chat_id: impl Into<ChatId>,
213        from_chat_id: impl Into<ChatId>,
214        message_id: i64,
215    ) -> Self {
216        Self {
217            client,
218            params: ForwardMessageParams {
219                chat_id: chat_id.into(),
220                from_chat_id: from_chat_id.into(),
221                message_id,
222                message_thread_id: None,
223                direct_messages_topic_id: None,
224                video_start_timestamp: None,
225                disable_notification: None,
226                protect_content: None,
227            },
228        }
229    }
230    /// Forum topic thread ID.
231    pub fn message_thread_id(mut self, id: i64) -> Self {
232        self.params.message_thread_id = Some(id);
233        self
234    }
235    /// Identifier of a direct messages chat topic.
236    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
237        self.params.direct_messages_topic_id = Some(id);
238        self
239    }
240    /// New start timestamp for a forwarded video.
241    pub fn video_start_timestamp(mut self, ts: i64) -> Self {
242        self.params.video_start_timestamp = Some(ts);
243        self
244    }
245    /// Sends the message silently — the recipient receives no notification sound.
246    pub fn disable_notification(mut self, v: bool) -> Self {
247        self.params.disable_notification = Some(v);
248        self
249    }
250    /// Protects the message from being forwarded or saved.
251    pub fn protect_content(mut self, v: bool) -> Self {
252        self.params.protect_content = Some(v);
253        self
254    }
255}
256
257impl_into_future!(ForwardMessage, Message, "forwardMessage");
258
259// ─── copyMessage ──────────────────────────────────────────────────────────────
260
261#[derive(Serialize)]
262struct CopyMessageParams {
263    chat_id: ChatId,
264    from_chat_id: ChatId,
265    message_id: i64,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    message_thread_id: Option<i64>,
268    #[serde(skip_serializing_if = "Option::is_none")]
269    direct_messages_topic_id: Option<i64>,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    video_start_timestamp: Option<i64>,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    caption: Option<String>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    parse_mode: Option<ParseMode>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    caption_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    show_caption_above_media: Option<bool>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    disable_notification: Option<bool>,
282    #[serde(skip_serializing_if = "Option::is_none")]
283    protect_content: Option<bool>,
284    #[serde(skip_serializing_if = "Option::is_none")]
285    reply_parameters: Option<ReplyParameters>,
286    #[serde(skip_serializing_if = "Option::is_none")]
287    reply_markup: Option<ReplyMarkup>,
288}
289
290/// Builder for the [`copyMessage`](https://core.telegram.org/bots/api#copymessage) method.
291pub struct CopyMessage {
292    client: BotClient,
293    params: CopyMessageParams,
294}
295
296impl CopyMessage {
297    pub(crate) fn new(
298        client: BotClient,
299        chat_id: impl Into<ChatId>,
300        from_chat_id: impl Into<ChatId>,
301        message_id: i64,
302    ) -> Self {
303        Self {
304            client,
305            params: CopyMessageParams {
306                chat_id: chat_id.into(),
307                from_chat_id: from_chat_id.into(),
308                message_id,
309                message_thread_id: None,
310                direct_messages_topic_id: None,
311                video_start_timestamp: None,
312                caption: None,
313                parse_mode: None,
314                caption_entities: None,
315                show_caption_above_media: None,
316                disable_notification: None,
317                protect_content: None,
318                reply_parameters: None,
319                reply_markup: None,
320            },
321        }
322    }
323    /// Forum topic thread ID.
324    pub fn message_thread_id(mut self, id: i64) -> Self {
325        self.params.message_thread_id = Some(id);
326        self
327    }
328    /// Identifier of a direct messages chat topic.
329    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
330        self.params.direct_messages_topic_id = Some(id);
331        self
332    }
333    /// New start timestamp for a copied video.
334    pub fn video_start_timestamp(mut self, ts: i64) -> Self {
335        self.params.video_start_timestamp = Some(ts);
336        self
337    }
338    /// Sets the caption (0–1024 characters) for media messages.
339    pub fn caption(mut self, c: impl Into<String>) -> Self {
340        self.params.caption = Some(c.into());
341        self
342    }
343    /// Sets the text parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
344    pub fn parse_mode(mut self, m: ParseMode) -> Self {
345        self.params.parse_mode = Some(m);
346        self
347    }
348    /// Sends the message silently — the recipient receives no notification sound.
349    pub fn disable_notification(mut self, v: bool) -> Self {
350        self.params.disable_notification = Some(v);
351        self
352    }
353    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
354    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
355        self.params.reply_markup = Some(m.into());
356        self
357    }
358}
359
360impl_into_future!(
361    CopyMessage,
362    rustigram_types::message::MessageId,
363    "copyMessage"
364);
365
366// ─── sendChatAction ───────────────────────────────────────────────────────────
367
368#[derive(Serialize)]
369struct SendChatActionParams {
370    chat_id: ChatId,
371    action: ChatAction,
372    #[serde(skip_serializing_if = "Option::is_none")]
373    business_connection_id: Option<String>,
374    #[serde(skip_serializing_if = "Option::is_none")]
375    message_thread_id: Option<i64>,
376}
377
378#[derive(Serialize, Clone, Copy)]
379/// The chat action to display while the bot is preparing a response.
380#[serde(rename_all = "snake_case")]
381pub enum ChatAction {
382    /// Indicates the bot is composing a message.
383    Typing,
384    /// Indicates the bot is uploading a photo.
385    UploadPhoto,
386    /// Indicates the bot is recording a video.
387    RecordVideo,
388    /// Indicates the bot is uploading a video.
389    UploadVideo,
390    /// Indicates the bot is recording a voice note.
391    RecordVoice,
392    /// Indicates the bot is uploading a voice note.
393    UploadVoice,
394    /// Indicates the bot is uploading a document.
395    UploadDocument,
396    /// Indicates the bot is choosing a sticker.
397    ChooseSticker,
398    /// Indicates the bot is finding a location.
399    FindLocation,
400    /// Indicates the bot is recording a video note.
401    RecordVideoNote,
402    /// Indicates the bot is uploading a video note.
403    UploadVideoNote,
404}
405
406/// Builder for the [`sendChatAction`](https://core.telegram.org/bots/api#sendchataction) method.
407pub struct SendChatAction {
408    client: BotClient,
409    params: SendChatActionParams,
410}
411
412impl SendChatAction {
413    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, action: ChatAction) -> Self {
414        Self {
415            client,
416            params: SendChatActionParams {
417                chat_id: chat_id.into(),
418                action,
419                business_connection_id: None,
420                message_thread_id: None,
421            },
422        }
423    }
424    /// Business connection ID for sending on behalf of a business account.
425    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
426        self.params.business_connection_id = Some(id.into());
427        self
428    }
429    /// Forum topic thread ID.
430    pub fn message_thread_id(mut self, id: i64) -> Self {
431        self.params.message_thread_id = Some(id);
432        self
433    }
434}
435
436impl_into_future!(SendChatAction, bool, "sendChatAction");
437
438// ─── sendDice ─────────────────────────────────────────────────────────────────
439
440#[derive(Serialize)]
441struct SendDiceParams {
442    chat_id: ChatId,
443    #[serde(skip_serializing_if = "Option::is_none")]
444    emoji: Option<String>,
445    #[serde(skip_serializing_if = "Option::is_none")]
446    message_thread_id: Option<i64>,
447    #[serde(skip_serializing_if = "Option::is_none")]
448    direct_messages_topic_id: Option<i64>,
449    #[serde(skip_serializing_if = "Option::is_none")]
450    disable_notification: Option<bool>,
451    #[serde(skip_serializing_if = "Option::is_none")]
452    protect_content: Option<bool>,
453    #[serde(skip_serializing_if = "Option::is_none")]
454    reply_parameters: Option<ReplyParameters>,
455    #[serde(skip_serializing_if = "Option::is_none")]
456    reply_markup: Option<ReplyMarkup>,
457}
458
459/// Builder for the [`sendDice`](https://core.telegram.org/bots/api#senddice) method.
460pub struct SendDice {
461    client: BotClient,
462    params: SendDiceParams,
463}
464
465impl SendDice {
466    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>) -> Self {
467        Self {
468            client,
469            params: SendDiceParams {
470                chat_id: chat_id.into(),
471                emoji: None,
472                message_thread_id: None,
473                direct_messages_topic_id: None,
474                disable_notification: None,
475                protect_content: None,
476                reply_parameters: None,
477                reply_markup: None,
478            },
479        }
480    }
481    /// The dice/emoji to animate. One of 🎲 🎯 🏀 ⚽ 🎳 🎰.
482    pub fn emoji(mut self, e: impl Into<String>) -> Self {
483        self.params.emoji = Some(e.into());
484        self
485    }
486    /// Forum topic thread ID.
487    pub fn message_thread_id(mut self, id: i64) -> Self {
488        self.params.message_thread_id = Some(id);
489        self
490    }
491    /// Identifier of a direct messages chat topic.
492    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
493        self.params.direct_messages_topic_id = Some(id);
494        self
495    }
496    /// Sends the message silently — the recipient receives no notification sound.
497    pub fn disable_notification(mut self, v: bool) -> Self {
498        self.params.disable_notification = Some(v);
499        self
500    }
501    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
502    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
503        self.params.reply_markup = Some(m.into());
504        self
505    }
506}
507
508impl_into_future!(SendDice, Message, "sendDice");
509
510// ─── sendLocation ─────────────────────────────────────────────────────────────
511
512#[derive(Serialize)]
513struct SendLocationParams {
514    chat_id: ChatId,
515    latitude: f64,
516    longitude: f64,
517    #[serde(skip_serializing_if = "Option::is_none")]
518    message_thread_id: Option<i64>,
519    #[serde(skip_serializing_if = "Option::is_none")]
520    direct_messages_topic_id: Option<i64>,
521    #[serde(skip_serializing_if = "Option::is_none")]
522    horizontal_accuracy: Option<f64>,
523    #[serde(skip_serializing_if = "Option::is_none")]
524    live_period: Option<u32>,
525    #[serde(skip_serializing_if = "Option::is_none")]
526    heading: Option<u16>,
527    #[serde(skip_serializing_if = "Option::is_none")]
528    proximity_alert_radius: Option<u32>,
529    #[serde(skip_serializing_if = "Option::is_none")]
530    disable_notification: Option<bool>,
531    #[serde(skip_serializing_if = "Option::is_none")]
532    protect_content: Option<bool>,
533    #[serde(skip_serializing_if = "Option::is_none")]
534    reply_parameters: Option<ReplyParameters>,
535    #[serde(skip_serializing_if = "Option::is_none")]
536    reply_markup: Option<ReplyMarkup>,
537}
538
539/// Builder for the [`sendLocation`](https://core.telegram.org/bots/api#sendlocation) method.
540pub struct SendLocation {
541    client: BotClient,
542    params: SendLocationParams,
543}
544
545impl SendLocation {
546    pub(crate) fn new(
547        client: BotClient,
548        chat_id: impl Into<ChatId>,
549        latitude: f64,
550        longitude: f64,
551    ) -> Self {
552        Self {
553            client,
554            params: SendLocationParams {
555                chat_id: chat_id.into(),
556                latitude,
557                longitude,
558                message_thread_id: None,
559                direct_messages_topic_id: None,
560                horizontal_accuracy: None,
561                live_period: None,
562                heading: None,
563                proximity_alert_radius: None,
564                disable_notification: None,
565                protect_content: None,
566                reply_parameters: None,
567                reply_markup: None,
568            },
569        }
570    }
571    /// Forum topic thread ID.
572    pub fn message_thread_id(mut self, id: i64) -> Self {
573        self.params.message_thread_id = Some(id);
574        self
575    }
576    /// Identifier of a direct messages chat topic.
577    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
578        self.params.direct_messages_topic_id = Some(id);
579        self
580    }
581    /// Sets the radius of uncertainty for the location, in metres (0–1500).
582    pub fn horizontal_accuracy(mut self, v: f64) -> Self {
583        self.params.horizontal_accuracy = Some(v);
584        self
585    }
586    /// Sets how long the location stays live, in seconds (60–86400).
587    pub fn live_period(mut self, v: u32) -> Self {
588        self.params.live_period = Some(v);
589        self
590    }
591    /// Sets the direction of movement in degrees (1–360).
592    pub fn heading(mut self, v: u16) -> Self {
593        self.params.heading = Some(v);
594        self
595    }
596    /// Sets the maximum distance in metres for proximity alerts.
597    pub fn proximity_alert_radius(mut self, v: u32) -> Self {
598        self.params.proximity_alert_radius = Some(v);
599        self
600    }
601    /// Sends the message silently — the recipient receives no notification sound.
602    pub fn disable_notification(mut self, v: bool) -> Self {
603        self.params.disable_notification = Some(v);
604        self
605    }
606    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
607    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
608        self.params.reply_markup = Some(m.into());
609        self
610    }
611}
612
613impl_into_future!(SendLocation, Message, "sendLocation");
614
615// ─── sendContact ──────────────────────────────────────────────────────────────
616
617#[derive(Serialize)]
618struct SendContactParams {
619    chat_id: ChatId,
620    phone_number: String,
621    first_name: String,
622    #[serde(skip_serializing_if = "Option::is_none")]
623    last_name: Option<String>,
624    #[serde(skip_serializing_if = "Option::is_none")]
625    vcard: Option<String>,
626    #[serde(skip_serializing_if = "Option::is_none")]
627    message_thread_id: Option<i64>,
628    #[serde(skip_serializing_if = "Option::is_none")]
629    direct_messages_topic_id: Option<i64>,
630    #[serde(skip_serializing_if = "Option::is_none")]
631    disable_notification: Option<bool>,
632    #[serde(skip_serializing_if = "Option::is_none")]
633    protect_content: Option<bool>,
634    #[serde(skip_serializing_if = "Option::is_none")]
635    reply_parameters: Option<ReplyParameters>,
636    #[serde(skip_serializing_if = "Option::is_none")]
637    reply_markup: Option<ReplyMarkup>,
638}
639
640/// Builder for the [`sendContact`](https://core.telegram.org/bots/api#sendcontact) method.
641pub struct SendContact {
642    client: BotClient,
643    params: SendContactParams,
644}
645
646impl SendContact {
647    pub(crate) fn new(
648        client: BotClient,
649        chat_id: impl Into<ChatId>,
650        phone_number: impl Into<String>,
651        first_name: impl Into<String>,
652    ) -> Self {
653        Self {
654            client,
655            params: SendContactParams {
656                chat_id: chat_id.into(),
657                phone_number: phone_number.into(),
658                first_name: first_name.into(),
659                last_name: None,
660                vcard: None,
661                message_thread_id: None,
662                direct_messages_topic_id: None,
663                disable_notification: None,
664                protect_content: None,
665                reply_parameters: None,
666                reply_markup: None,
667            },
668        }
669    }
670    /// Forum topic thread ID.
671    pub fn message_thread_id(mut self, id: i64) -> Self {
672        self.params.message_thread_id = Some(id);
673        self
674    }
675    /// Identifier of a direct messages chat topic.
676    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
677        self.params.direct_messages_topic_id = Some(id);
678        self
679    }
680    /// Sets the last name of the contact.
681    pub fn last_name(mut self, v: impl Into<String>) -> Self {
682        self.params.last_name = Some(v.into());
683        self
684    }
685    /// Sets the vCard data of the contact.
686    pub fn vcard(mut self, v: impl Into<String>) -> Self {
687        self.params.vcard = Some(v.into());
688        self
689    }
690    /// Sends the message silently — the recipient receives no notification sound.
691    pub fn disable_notification(mut self, v: bool) -> Self {
692        self.params.disable_notification = Some(v);
693        self
694    }
695    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
696    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
697        self.params.reply_markup = Some(m.into());
698        self
699    }
700}
701
702impl_into_future!(SendContact, Message, "sendContact");
703
704// ─── sendPoll ─────────────────────────────────────────────────────────────────
705
706#[derive(Serialize)]
707struct SendPollParams {
708    chat_id: ChatId,
709    question: String,
710    options: Vec<InputPollOption>,
711    #[serde(skip_serializing_if = "Option::is_none")]
712    question_parse_mode: Option<ParseMode>,
713    #[serde(skip_serializing_if = "Option::is_none")]
714    question_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
715    #[serde(skip_serializing_if = "Option::is_none")]
716    message_thread_id: Option<i64>,
717    #[serde(skip_serializing_if = "Option::is_none")]
718    direct_messages_topic_id: Option<i64>,
719    #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
720    poll_type: Option<rustigram_types::poll::PollType>,
721    #[serde(skip_serializing_if = "Option::is_none")]
722    is_anonymous: Option<bool>,
723    #[serde(skip_serializing_if = "Option::is_none")]
724    allows_multiple_answers: Option<bool>,
725    #[serde(skip_serializing_if = "Option::is_none")]
726    allows_revoting: Option<bool>,
727    #[serde(skip_serializing_if = "Option::is_none")]
728    correct_option_ids: Option<Vec<u8>>,
729    #[serde(skip_serializing_if = "Option::is_none")]
730    explanation: Option<String>,
731    #[serde(skip_serializing_if = "Option::is_none")]
732    explanation_parse_mode: Option<ParseMode>,
733    #[serde(skip_serializing_if = "Option::is_none")]
734    explanation_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
735    #[serde(skip_serializing_if = "Option::is_none")]
736    open_period: Option<u32>,
737    #[serde(skip_serializing_if = "Option::is_none")]
738    close_date: Option<i64>,
739    #[serde(skip_serializing_if = "Option::is_none")]
740    is_closed: Option<bool>,
741    #[serde(skip_serializing_if = "Option::is_none")]
742    shuffle_options: Option<bool>,
743    #[serde(skip_serializing_if = "Option::is_none")]
744    allow_adding_options: Option<bool>,
745    #[serde(skip_serializing_if = "Option::is_none")]
746    hide_results_until_closes: Option<bool>,
747    #[serde(skip_serializing_if = "Option::is_none")]
748    description: Option<String>,
749    #[serde(skip_serializing_if = "Option::is_none")]
750    description_parse_mode: Option<ParseMode>,
751    #[serde(skip_serializing_if = "Option::is_none")]
752    description_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
753    #[serde(skip_serializing_if = "Option::is_none")]
754    disable_notification: Option<bool>,
755    #[serde(skip_serializing_if = "Option::is_none")]
756    protect_content: Option<bool>,
757    #[serde(skip_serializing_if = "Option::is_none")]
758    reply_parameters: Option<ReplyParameters>,
759    #[serde(skip_serializing_if = "Option::is_none")]
760    reply_markup: Option<ReplyMarkup>,
761    #[serde(skip_serializing_if = "Option::is_none")]
762    suggested_post_parameters: Option<SuggestedPostParameters>,
763    #[serde(skip_serializing_if = "Option::is_none")]
764    members_only: Option<bool>,
765    #[serde(skip_serializing_if = "Option::is_none")]
766    country_codes: Option<Vec<String>>,
767}
768
769/// Builder for the [`sendPoll`](https://core.telegram.org/bots/api#sendpoll) method.
770pub struct SendPoll {
771    client: BotClient,
772    params: SendPollParams,
773}
774
775impl SendPoll {
776    pub(crate) fn new(
777        client: BotClient,
778        chat_id: impl Into<ChatId>,
779        question: impl Into<String>,
780        options: Vec<InputPollOption>,
781    ) -> Self {
782        Self {
783            client,
784            params: SendPollParams {
785                chat_id: chat_id.into(),
786                question: question.into(),
787                options,
788                question_parse_mode: None,
789                question_entities: None,
790                message_thread_id: None,
791                direct_messages_topic_id: None,
792                poll_type: None,
793                is_anonymous: None,
794                allows_multiple_answers: None,
795                allows_revoting: None,
796                correct_option_ids: None,
797                explanation: None,
798                explanation_parse_mode: None,
799                explanation_entities: None,
800                open_period: None,
801                close_date: None,
802                is_closed: None,
803                shuffle_options: None,
804                allow_adding_options: None,
805                hide_results_until_closes: None,
806                description: None,
807                description_parse_mode: None,
808                description_entities: None,
809                disable_notification: None,
810                protect_content: None,
811                reply_parameters: None,
812                reply_markup: None,
813                suggested_post_parameters: None,
814                members_only: None,
815                country_codes: None,
816            },
817        }
818    }
819    /// Forum topic thread ID.
820    pub fn message_thread_id(mut self, id: i64) -> Self {
821        self.params.message_thread_id = Some(id);
822        self
823    }
824    /// Identifier of a direct messages chat topic.
825    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
826        self.params.direct_messages_topic_id = Some(id);
827        self
828    }
829    /// Sets whether the poll is anonymous.
830    pub fn is_anonymous(mut self, v: bool) -> Self {
831        self.params.is_anonymous = Some(v);
832        self
833    }
834    /// Allows voters to select multiple answers.
835    pub fn allows_multiple_answers(mut self, v: bool) -> Self {
836        self.params.allows_multiple_answers = Some(v);
837        self
838    }
839    /// Allows voters to change their vote.
840    pub fn allows_revoting(mut self, v: bool) -> Self {
841        self.params.allows_revoting = Some(v);
842        self
843    }
844    /// Converts the poll to a quiz with the given correct option indices.
845    pub fn quiz(mut self, ids: Vec<u8>) -> Self {
846        self.params.poll_type = Some(rustigram_types::poll::PollType::Quiz);
847        self.params.correct_option_ids = Some(ids);
848        self
849    }
850    /// Convenience method for a quiz with a single correct option.
851    pub fn quiz_single(self, id: u8) -> Self {
852        self.quiz(vec![id])
853    }
854    /// Sets the explanation text shown after a quiz answer.
855    pub fn explanation(mut self, text: impl Into<String>) -> Self {
856        self.params.explanation = Some(text.into());
857        self
858    }
859    /// Sets the parse mode for the explanation.
860    pub fn explanation_parse_mode(mut self, mode: ParseMode) -> Self {
861        self.params.explanation_parse_mode = Some(mode);
862        self
863    }
864    /// Sets entities for the explanation.
865    pub fn explanation_entities(mut self, e: Vec<rustigram_types::message::MessageEntity>) -> Self {
866        self.params.explanation_entities = Some(e);
867        self
868    }
869    /// Sets how long the poll stays open in seconds (5–2628000).
870    pub fn open_period(mut self, secs: u32) -> Self {
871        self.params.open_period = Some(secs);
872        self
873    }
874    /// Sets the Unix timestamp when the poll closes automatically.
875    pub fn close_date(mut self, ts: i64) -> Self {
876        self.params.close_date = Some(ts);
877        self
878    }
879    /// Sets whether the options should be shuffled.
880    pub fn shuffle_options(mut self, v: bool) -> Self {
881        self.params.shuffle_options = Some(v);
882        self
883    }
884    /// Allows users to add their own options to the poll.
885    pub fn allow_adding_options(mut self, v: bool) -> Self {
886        self.params.allow_adding_options = Some(v);
887        self
888    }
889    /// Hides the poll results until it's closed.
890    pub fn hide_results_until_closes(mut self, v: bool) -> Self {
891        self.params.hide_results_until_closes = Some(v);
892        self
893    }
894    /// Sets the poll description (0-1024 chars).
895    pub fn description(mut self, d: impl Into<String>) -> Self {
896        self.params.description = Some(d.into());
897        self
898    }
899    /// Sets description parse mode.
900    pub fn description_parse_mode(mut self, mode: ParseMode) -> Self {
901        self.params.description_parse_mode = Some(mode);
902        self
903    }
904    /// Sets description entities.
905    pub fn description_entities(mut self, e: Vec<rustigram_types::message::MessageEntity>) -> Self {
906        self.params.description_entities = Some(e);
907        self
908    }
909    /// Sets the question parse mode.
910    pub fn question_parse_mode(mut self, mode: ParseMode) -> Self {
911        self.params.question_parse_mode = Some(mode);
912        self
913    }
914    /// Sets question entities.
915    pub fn question_entities(mut self, e: Vec<rustigram_types::message::MessageEntity>) -> Self {
916        self.params.question_entities = Some(e);
917        self
918    }
919    /// Sends the message silently — the recipient receives no notification sound.
920    pub fn disable_notification(mut self, v: bool) -> Self {
921        self.params.disable_notification = Some(v);
922        self
923    }
924    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
925    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
926        self.params.reply_markup = Some(m.into());
927        self
928    }
929    /// Suggested post parameters for channel direct messages chats.
930    pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self {
931        self.params.suggested_post_parameters = Some(params);
932        self
933    }
934    /// Pass `true` to limit voting to users who have been members of the chat for more than
935    /// 24 hours; for channel chats only.
936    pub fn members_only(mut self, v: bool) -> Self {
937        self.params.members_only = Some(v);
938        self
939    }
940    /// Two-letter ISO 3166-1 alpha-2 country codes indicating the countries from which users
941    /// can vote; for channel chats only. Pass an empty list to allow any country.
942    pub fn country_codes(mut self, codes: Vec<impl Into<String>>) -> Self {
943        self.params.country_codes = Some(codes.into_iter().map(Into::into).collect());
944        self
945    }
946}
947
948impl_into_future!(SendPoll, Message, "sendPoll");
949
950// ─── sendMessageDraft ─────────────────────────────────────────────────────────
951
952#[derive(Serialize)]
953struct SendMessageDraftParams {
954    chat_id: ChatId,
955    draft_id: i64,
956    text: String,
957    #[serde(skip_serializing_if = "Option::is_none")]
958    message_thread_id: Option<i64>,
959    #[serde(skip_serializing_if = "Option::is_none")]
960    parse_mode: Option<ParseMode>,
961    #[serde(skip_serializing_if = "Option::is_none")]
962    entities: Option<Vec<rustigram_types::message::MessageEntity>>,
963}
964
965/// Builder for the [`sendMessageDraft`](https://core.telegram.org/bots/api#sendmessagedraft) method.
966/// Streams a partial message to the user while it is being generated (Bot API 9.5+).
967pub struct SendMessageDraft {
968    client: BotClient,
969    params: SendMessageDraftParams,
970}
971
972impl SendMessageDraft {
973    pub(crate) fn new(
974        client: BotClient,
975        chat_id: impl Into<ChatId>,
976        draft_id: i64,
977        text: impl Into<String>,
978    ) -> Self {
979        Self {
980            client,
981            params: SendMessageDraftParams {
982                chat_id: chat_id.into(),
983                draft_id,
984                text: text.into(),
985                message_thread_id: None,
986                parse_mode: None,
987                entities: None,
988            },
989        }
990    }
991    /// Sets the text parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
992    pub fn parse_mode(mut self, m: ParseMode) -> Self {
993        self.params.parse_mode = Some(m);
994        self
995    }
996    /// Sets custom message entities instead of using a parse mode.
997    pub fn entities(mut self, e: Vec<rustigram_types::message::MessageEntity>) -> Self {
998        self.params.entities = Some(e);
999        self
1000    }
1001}
1002
1003impl_into_future!(SendMessageDraft, bool, "sendMessageDraft");
1004
1005// ─── File-sending builders ────────────────────────────────────────────────────
1006//
1007// Photo, Audio, Document, Video, Animation, Voice, VideoNote each share a
1008// similar shape but differ in field names and constraints. We use a common
1009// pattern: store the InputFile and an optional Form for multipart, and build
1010// the form lazily in `IntoFuture`.
1011
1012/// Common optional parameters shared by most media-send methods.
1013#[derive(Default)]
1014pub struct MediaSendOptions {
1015    /// Business connection ID for sending on behalf of a business account.
1016    pub business_connection_id: Option<String>,
1017    /// Forum topic thread ID.
1018    pub message_thread_id: Option<i64>,
1019    /// Identifier of a direct messages chat topic.
1020    pub direct_messages_topic_id: Option<i64>,
1021    /// Sets the caption (0–1024 characters) for media messages.
1022    pub caption: Option<String>,
1023    /// Parse mode for the caption.
1024    pub parse_mode: Option<ParseMode>,
1025    /// Special entities in the caption.
1026    pub caption_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
1027    /// Shows the caption above the media instead of below it.
1028    pub show_caption_above_media: Option<bool>,
1029    /// Marks the media as a spoiler — blurs it until the user taps to reveal.
1030    pub has_spoiler: Option<bool>,
1031    /// Sends the message silently — the recipient receives no notification sound.
1032    pub disable_notification: Option<bool>,
1033    /// Protects the message from being forwarded or saved.
1034    pub protect_content: Option<bool>,
1035    /// Allows sending to large audiences at the cost of Telegram Stars.
1036    pub allow_paid_broadcast: Option<bool>,
1037    /// Reply parameters for this message.
1038    pub reply_parameters: Option<ReplyParameters>,
1039    /// Reply markup attached to the message.
1040    pub reply_markup: Option<ReplyMarkup>,
1041    /// Suggested post parameters for channel direct messages chats.
1042    pub suggested_post_parameters: Option<SuggestedPostParameters>,
1043}
1044
1045/// Builds the JSON body for a simple (non-file-upload) part of a media send.
1046fn media_json_body(
1047    chat_id: &ChatId,
1048    media_field: &str,
1049    media_value: &str,
1050    opts: &MediaSendOptions,
1051    extra: serde_json::Value,
1052) -> serde_json::Value {
1053    let mut map = serde_json::json!({
1054        "chat_id": chat_id,
1055        media_field: media_value,
1056    });
1057    let obj = map.as_object_mut().unwrap();
1058    if let Some(v) = &opts.business_connection_id {
1059        obj.insert("business_connection_id".to_owned(), serde_json::json!(v));
1060    }
1061    if let Some(v) = &opts.message_thread_id {
1062        obj.insert("message_thread_id".to_owned(), serde_json::json!(v));
1063    }
1064    if let Some(v) = &opts.direct_messages_topic_id {
1065        obj.insert("direct_messages_topic_id".to_owned(), serde_json::json!(v));
1066    }
1067    if let Some(v) = &opts.caption {
1068        obj.insert("caption".to_owned(), serde_json::json!(v));
1069    }
1070    if let Some(v) = &opts.parse_mode {
1071        obj.insert("parse_mode".to_owned(), serde_json::json!(v));
1072    }
1073    if let Some(v) = &opts.caption_entities {
1074        obj.insert("caption_entities".to_owned(), serde_json::json!(v));
1075    }
1076    if let Some(v) = opts.show_caption_above_media {
1077        obj.insert("show_caption_above_media".to_owned(), serde_json::json!(v));
1078    }
1079    if let Some(v) = opts.has_spoiler {
1080        obj.insert("has_spoiler".to_owned(), serde_json::json!(v));
1081    }
1082    if let Some(v) = opts.disable_notification {
1083        obj.insert("disable_notification".to_owned(), serde_json::json!(v));
1084    }
1085    if let Some(v) = opts.protect_content {
1086        obj.insert("protect_content".to_owned(), serde_json::json!(v));
1087    }
1088    if let Some(v) = opts.allow_paid_broadcast {
1089        obj.insert("allow_paid_broadcast".to_owned(), serde_json::json!(v));
1090    }
1091    if let Some(v) = &opts.reply_parameters {
1092        obj.insert("reply_parameters".to_owned(), serde_json::json!(v));
1093    }
1094    if let Some(v) = &opts.reply_markup {
1095        obj.insert("reply_markup".to_owned(), serde_json::json!(v));
1096    }
1097    if let Some(v) = &opts.suggested_post_parameters {
1098        obj.insert("suggested_post_parameters".to_owned(), serde_json::json!(v));
1099    }
1100    if let serde_json::Value::Object(extra_obj) = extra {
1101        for (k, v) in extra_obj {
1102            obj.insert(k, v);
1103        }
1104    }
1105    map
1106}
1107
1108// ─── sendPhoto ────────────────────────────────────────────────────────────────
1109
1110/// Builder for the [`sendPhoto`](https://core.telegram.org/bots/api#sendphoto) method.
1111pub struct SendPhoto {
1112    client: BotClient,
1113    chat_id: ChatId,
1114    photo: InputFile,
1115    opts: MediaSendOptions,
1116}
1117
1118impl SendPhoto {
1119    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, photo: InputFile) -> Self {
1120        Self {
1121            client,
1122            chat_id: chat_id.into(),
1123            photo,
1124            opts: MediaSendOptions::default(),
1125        }
1126    }
1127    /// Business connection ID for sending on behalf of a business account.
1128    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
1129        self.opts.business_connection_id = Some(id.into());
1130        self
1131    }
1132    /// Forum topic thread ID.
1133    pub fn message_thread_id(mut self, id: i64) -> Self {
1134        self.opts.message_thread_id = Some(id);
1135        self
1136    }
1137    /// Identifier of a direct messages chat topic.
1138    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1139        self.opts.direct_messages_topic_id = Some(id);
1140        self
1141    }
1142    /// Sets the caption (0–1024 characters) for media messages.
1143    pub fn caption(mut self, c: impl Into<String>) -> Self {
1144        self.opts.caption = Some(c.into());
1145        self
1146    }
1147    /// Sets the caption parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
1148    pub fn parse_mode(mut self, m: ParseMode) -> Self {
1149        self.opts.parse_mode = Some(m);
1150        self
1151    }
1152    /// Marks the media as a spoiler — blurs it until the user taps to reveal.
1153    pub fn has_spoiler(mut self, v: bool) -> Self {
1154        self.opts.has_spoiler = Some(v);
1155        self
1156    }
1157    /// Shows the caption above the media instead of below it.
1158    pub fn show_caption_above_media(mut self, v: bool) -> Self {
1159        self.opts.show_caption_above_media = Some(v);
1160        self
1161    }
1162    /// Sends the message silently — the recipient receives no notification sound.
1163    pub fn disable_notification(mut self, v: bool) -> Self {
1164        self.opts.disable_notification = Some(v);
1165        self
1166    }
1167    /// Protects the message from being forwarded or saved.
1168    pub fn protect_content(mut self, v: bool) -> Self {
1169        self.opts.protect_content = Some(v);
1170        self
1171    }
1172    /// Allows sending to large audiences at the cost of Telegram Stars.
1173    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
1174        self.opts.allow_paid_broadcast = Some(v);
1175        self
1176    }
1177    /// Reply parameters for this message.
1178    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
1179        self.opts.reply_parameters = Some(rp);
1180        self
1181    }
1182    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1183    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
1184        self.opts.reply_markup = Some(m.into());
1185        self
1186    }
1187    /// Suggested post parameters for channel direct messages chats.
1188    pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self {
1189        self.opts.suggested_post_parameters = Some(params);
1190        self
1191    }
1192}
1193
1194impl IntoFuture for SendPhoto {
1195    type Output = Result<Message>;
1196    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1197
1198    fn into_future(self) -> Self::IntoFuture {
1199        Box::pin(async move {
1200            match &self.photo {
1201                InputFile::Bytes {
1202                    filename,
1203                    data,
1204                    mime_type,
1205                } => {
1206                    let part = Part::bytes(data.clone())
1207                        .file_name(filename.clone())
1208                        .mime_str(mime_type)
1209                        .map_err(|e| crate::error::Error::Decode(e.to_string()))?;
1210                    let mut form = Form::new().part("photo", part);
1211                    form = form.text("chat_id", self.chat_id.to_string());
1212                    if let Some(id) = &self.opts.business_connection_id {
1213                        form = form.text("business_connection_id", id.clone());
1214                    }
1215                    if let Some(id) = self.opts.message_thread_id {
1216                        form = form.text("message_thread_id", id.to_string());
1217                    }
1218                    if let Some(id) = self.opts.direct_messages_topic_id {
1219                        form = form.text("direct_messages_topic_id", id.to_string());
1220                    }
1221                    if let Some(c) = &self.opts.caption {
1222                        form = form.text("caption", c.clone());
1223                    }
1224                    if let Some(m) = &self.opts.parse_mode {
1225                        form = form.text("parse_mode", format!("{m:?}"));
1226                    }
1227                    if let Some(v) = self.opts.disable_notification {
1228                        form = form.text("disable_notification", v.to_string());
1229                    }
1230                    if let Some(v) = self.opts.has_spoiler {
1231                        form = form.text("has_spoiler", v.to_string());
1232                    }
1233                    if let Some(v) = &self.opts.reply_markup {
1234                        form = form.text("reply_markup", serde_json::to_string(v).unwrap());
1235                    }
1236                    if let Some(p) = &self.opts.suggested_post_parameters {
1237                        form = form.text(
1238                            "suggested_post_parameters",
1239                            serde_json::to_string(p).unwrap(),
1240                        );
1241                    }
1242                    self.client.post_multipart("sendPhoto", form).await
1243                }
1244                _ => {
1245                    let body = media_json_body(
1246                        &self.chat_id,
1247                        "photo",
1248                        self.photo.as_str(),
1249                        &self.opts,
1250                        serde_json::Value::Null,
1251                    );
1252                    self.client.post_json("sendPhoto", &body).await
1253                }
1254            }
1255        })
1256    }
1257}
1258
1259// ─── Macro for simpler media senders (Audio, Document, Video, Animation, Voice, VideoNote, Sticker)
1260
1261macro_rules! media_sender {
1262    ($(#[$doc:meta])* $name:ident, $field:literal, $method:literal, $return_ty:ty, [$($extra_field:ident: $extra_ty:ty),*]) => {
1263        $(#[$doc])*
1264        pub struct $name {
1265            /// The API client to use for sending the request.
1266            client: BotClient,
1267            /// Unique identifier for the target chat or username of the target channel.
1268            chat_id: ChatId,
1269            /// The file to send. Can be a file ID, URL, or new upload.
1270            file: InputFile,
1271            /// Common optional parameters for media sending.
1272            opts: MediaSendOptions,
1273            /// Extra optional parameters specific to this media type.
1274            $($extra_field: Option<$extra_ty>,)*
1275        }
1276
1277        impl $name {
1278            pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, file: InputFile) -> Self {
1279                Self {
1280                    client,
1281                    chat_id: chat_id.into(),
1282                    file,
1283                    opts: MediaSendOptions::default(),
1284                    $($extra_field: None,)*
1285                }
1286            }
1287            /// Business connection ID for sending on behalf of a business account.
1288            pub fn business_connection_id(mut self, id: impl Into<String>) -> Self { self.opts.business_connection_id = Some(id.into()); self }
1289            /// Forum topic thread ID.
1290            pub fn message_thread_id(mut self, id: i64) -> Self { self.opts.message_thread_id = Some(id); self }
1291            /// Identifier of a direct messages chat topic.
1292            pub fn direct_messages_topic_id(mut self, id: i64) -> Self { self.opts.direct_messages_topic_id = Some(id); self }
1293            /// Sets the caption (0–1024 characters) for media messages.
1294            pub fn caption(mut self, c: impl Into<String>) -> Self { self.opts.caption = Some(c.into()); self }
1295            /// Sets the caption parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
1296            pub fn parse_mode(mut self, m: ParseMode) -> Self { self.opts.parse_mode = Some(m); self }
1297            /// Sends the message silently — the recipient receives no notification sound.
1298            pub fn disable_notification(mut self, v: bool) -> Self { self.opts.disable_notification = Some(v); self }
1299            /// Protects the message from being forwarded or saved.
1300            pub fn protect_content(mut self, v: bool) -> Self { self.opts.protect_content = Some(v); self }
1301            /// Allows sending to large audiences at the cost of Telegram Stars.
1302            pub fn allow_paid_broadcast(mut self, v: bool) -> Self { self.opts.allow_paid_broadcast = Some(v); self }
1303            /// Reply parameters for this message.
1304            pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self { self.opts.reply_parameters = Some(rp); self }
1305            /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1306            pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self { self.opts.reply_markup = Some(m.into()); self }
1307            /// Suggested post parameters for channel direct messages chats.
1308            pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self { self.opts.suggested_post_parameters = Some(params); self }
1309
1310            $(
1311                #[doc = concat!("Sets the ", stringify!($extra_field), " for the media.")]
1312                pub fn $extra_field(mut self, v: $extra_ty) -> Self {
1313                    self.$extra_field = Some(v);
1314                    self
1315                }
1316            )*
1317        }
1318
1319        impl IntoFuture for $name {
1320            type Output = Result<$return_ty>;
1321            type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1322
1323            fn into_future(self) -> Self::IntoFuture {
1324                Box::pin(async move {
1325                    match &self.file {
1326                        InputFile::Bytes { filename, data, mime_type } => {
1327                            let part = Part::bytes(data.clone())
1328                                .file_name(filename.clone())
1329                                .mime_str(mime_type)
1330                                .map_err(|e| crate::error::Error::Decode(e.to_string()))?;
1331                            let mut form = Form::new().part($field, part);
1332                            form = form.text("chat_id", self.chat_id.to_string());
1333                            if let Some(id) = &self.opts.business_connection_id { form = form.text("business_connection_id", id.clone()); }
1334                            if let Some(id) = self.opts.message_thread_id { form = form.text("message_thread_id", id.to_string()); }
1335                            if let Some(id) = self.opts.direct_messages_topic_id { form = form.text("direct_messages_topic_id", id.to_string()); }
1336                            if let Some(c) = &self.opts.caption { form = form.text("caption", c.clone()); }
1337                            if let Some(v) = self.opts.disable_notification { form = form.text("disable_notification", v.to_string()); }
1338                            if let Some(v) = &self.opts.reply_markup { form = form.text("reply_markup", serde_json::to_string(v).unwrap()); }
1339                            if let Some(p) = &self.opts.suggested_post_parameters { form = form.text("suggested_post_parameters", serde_json::to_string(p).unwrap()); }
1340
1341                            $(
1342                                if let Some(ref v) = self.$extra_field {
1343                                    form = form.text(stringify!($extra_field), v.to_string());
1344                                }
1345                            )*
1346
1347                            self.client.post_multipart($method, form).await
1348                        }
1349                        _ => {
1350                            let mut extra = serde_json::json!({});
1351                            $(
1352                                if let Some(ref v) = self.$extra_field {
1353                                    extra[stringify!($extra_field)] = serde_json::json!(v);
1354                                }
1355                            )*
1356                            let body = media_json_body(&self.chat_id, $field, self.file.as_str(), &self.opts, extra);
1357                            self.client.post_json($method, &body).await
1358                        }
1359                    }
1360                })
1361            }
1362        }
1363    };
1364}
1365
1366media_sender!(
1367    /// Builder for the [`sendAudio`](https://core.telegram.org/bots/api#sendaudio) method.
1368    SendAudio,      "audio",      "sendAudio",      Message, [duration: u32, performer: String, title: String]);
1369media_sender!(
1370    /// Builder for the [`sendDocument`](https://core.telegram.org/bots/api#senddocument) method.
1371    SendDocument,  "document",   "sendDocument",  Message, [disable_content_type_detection: bool]);
1372media_sender!(
1373    /// Builder for the [`sendVideo`](https://core.telegram.org/bots/api#sendvideo) method.
1374    SendVideo,      "video",      "sendVideo",      Message, [duration: u32, width: u32, height: u32, supports_streaming: bool, cover: String, start_timestamp: i64]);
1375media_sender!(
1376    /// Builder for the [`sendAnimation`](https://core.telegram.org/bots/api#sendanimation) method.
1377    SendAnimation, "animation",  "sendAnimation", Message, [duration: u32, width: u32, height: u32]);
1378media_sender!(
1379    /// Builder for the [`sendVoice`](https://core.telegram.org/bots/api#sendvoice) method.
1380    SendVoice,      "voice",      "sendVoice",      Message, [duration: u32]);
1381media_sender!(
1382    /// Builder for the [`sendVideoNote`](https://core.telegram.org/bots/api#sendvideonote) method.
1383    SendVideoNote, "video_note", "sendVideoNote", Message, [duration: u32, length: u32]);
1384media_sender!(
1385    /// Builder for the [`sendSticker`](https://core.telegram.org/bots/api#sendsticker) method.
1386    SendSticker,   "sticker",    "sendSticker",    Message, [emoji: String]);
1387
1388// ─── deleteMessage / deleteMessages ──────────────────────────────────────────
1389
1390#[derive(Serialize)]
1391struct DeleteMessageParams {
1392    chat_id: ChatId,
1393    message_id: i64,
1394}
1395
1396/// Builder for the [`deleteMessage`](https://core.telegram.org/bots/api#deletemessage) method.
1397pub struct DeleteMessage {
1398    client: BotClient,
1399    params: DeleteMessageParams,
1400}
1401impl DeleteMessage {
1402    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, message_id: i64) -> Self {
1403        Self {
1404            client,
1405            params: DeleteMessageParams {
1406                chat_id: chat_id.into(),
1407                message_id,
1408            },
1409        }
1410    }
1411}
1412impl_into_future!(DeleteMessage, bool, "deleteMessage");
1413
1414#[derive(Serialize)]
1415struct DeleteMessagesParams {
1416    chat_id: ChatId,
1417    message_ids: Vec<i64>,
1418}
1419
1420/// Builder for the [`deleteMessages`](https://core.telegram.org/bots/api#deletemessages) method.
1421pub struct DeleteMessages {
1422    client: BotClient,
1423    params: DeleteMessagesParams,
1424}
1425impl DeleteMessages {
1426    pub(crate) fn new(
1427        client: BotClient,
1428        chat_id: impl Into<ChatId>,
1429        message_ids: Vec<i64>,
1430    ) -> Self {
1431        Self {
1432            client,
1433            params: DeleteMessagesParams {
1434                chat_id: chat_id.into(),
1435                message_ids,
1436            },
1437        }
1438    }
1439}
1440impl_into_future!(DeleteMessages, bool, "deleteMessages");
1441
1442// ─── stopPoll ─────────────────────────────────────────────────────────────────
1443
1444#[derive(Serialize)]
1445struct StopPollParams {
1446    chat_id: ChatId,
1447    message_id: i64,
1448    #[serde(skip_serializing_if = "Option::is_none")]
1449    reply_markup: Option<rustigram_types::keyboard::InlineKeyboardMarkup>,
1450}
1451
1452/// Builder for the [`stopPoll`](https://core.telegram.org/bots/api#stoppoll) method.
1453pub struct StopPoll {
1454    client: BotClient,
1455    params: StopPollParams,
1456}
1457impl StopPoll {
1458    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, message_id: i64) -> Self {
1459        Self {
1460            client,
1461            params: StopPollParams {
1462                chat_id: chat_id.into(),
1463                message_id,
1464                reply_markup: None,
1465            },
1466        }
1467    }
1468    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1469    pub fn reply_markup(mut self, m: rustigram_types::keyboard::InlineKeyboardMarkup) -> Self {
1470        self.params.reply_markup = Some(m);
1471        self
1472    }
1473}
1474impl_into_future!(StopPoll, rustigram_types::poll::Poll, "stopPoll");
1475
1476// ─── answerCallbackQuery ──────────────────────────────────────────────────────
1477
1478#[derive(Serialize)]
1479struct AnswerCallbackQueryParams {
1480    callback_query_id: String,
1481    #[serde(skip_serializing_if = "Option::is_none")]
1482    text: Option<String>,
1483    #[serde(skip_serializing_if = "Option::is_none")]
1484    show_alert: Option<bool>,
1485    #[serde(skip_serializing_if = "Option::is_none")]
1486    url: Option<String>,
1487    #[serde(skip_serializing_if = "Option::is_none")]
1488    cache_time: Option<u32>,
1489}
1490
1491/// Builder for the [`answerCallbackQuery`](https://core.telegram.org/bots/api#answercallbackquery) method.
1492pub struct AnswerCallbackQuery {
1493    client: BotClient,
1494    params: AnswerCallbackQueryParams,
1495}
1496impl AnswerCallbackQuery {
1497    pub(crate) fn new(client: BotClient, callback_query_id: impl Into<String>) -> Self {
1498        Self {
1499            client,
1500            params: AnswerCallbackQueryParams {
1501                callback_query_id: callback_query_id.into(),
1502                text: None,
1503                show_alert: None,
1504                url: None,
1505                cache_time: None,
1506            },
1507        }
1508    }
1509    /// The text of the notification shown to the user. 0–200 characters.
1510    pub fn text(mut self, t: impl Into<String>) -> Self {
1511        self.params.text = Some(t.into());
1512        self
1513    }
1514    /// Shows an alert dialog instead of a toast notification for the callback answer.
1515    pub fn show_alert(mut self, v: bool) -> Self {
1516        self.params.show_alert = Some(v);
1517        self
1518    }
1519    /// Sets the URL to open when the callback button answer is tapped.
1520    pub fn url(mut self, u: impl Into<String>) -> Self {
1521        self.params.url = Some(u.into());
1522        self
1523    }
1524    /// Sets how long the callback answer may be cached on the client in seconds.
1525    pub fn cache_time(mut self, secs: u32) -> Self {
1526        self.params.cache_time = Some(secs);
1527        self
1528    }
1529    /// Shorthand for `.text(t).show_alert(true)` — shows a popup alert to the user.
1530    pub fn alert(self, text: impl Into<String>) -> Self {
1531        self.text(text).show_alert(true)
1532    }
1533}
1534impl_into_future!(AnswerCallbackQuery, bool, "answerCallbackQuery");
1535// ─── forwardMessages ──────────────────────────────────────────────────────────
1536
1537#[derive(Serialize)]
1538struct ForwardMessagesParams {
1539    chat_id: ChatId,
1540    from_chat_id: ChatId,
1541    message_ids: Vec<i64>,
1542    #[serde(skip_serializing_if = "Option::is_none")]
1543    message_thread_id: Option<i64>,
1544    #[serde(skip_serializing_if = "Option::is_none")]
1545    direct_messages_topic_id: Option<i64>,
1546    #[serde(skip_serializing_if = "Option::is_none")]
1547    disable_notification: Option<bool>,
1548    #[serde(skip_serializing_if = "Option::is_none")]
1549    protect_content: Option<bool>,
1550}
1551
1552/// Builder for the [`forwardMessages`](https://core.telegram.org/bots/api#forwardmessages) method.
1553///
1554/// Forwards 1–100 messages at once, preserving album grouping.
1555/// Returns a `Vec<MessageId>` of the sent messages.
1556pub struct ForwardMessages {
1557    client: BotClient,
1558    params: ForwardMessagesParams,
1559}
1560
1561impl ForwardMessages {
1562    pub(crate) fn new(
1563        client: BotClient,
1564        chat_id: impl Into<ChatId>,
1565        from_chat_id: impl Into<ChatId>,
1566        message_ids: Vec<i64>,
1567    ) -> Self {
1568        Self {
1569            client,
1570            params: ForwardMessagesParams {
1571                chat_id: chat_id.into(),
1572                from_chat_id: from_chat_id.into(),
1573                message_ids,
1574                message_thread_id: None,
1575                direct_messages_topic_id: None,
1576                disable_notification: None,
1577                protect_content: None,
1578            },
1579        }
1580    }
1581    /// Forum topic thread ID.
1582    pub fn message_thread_id(mut self, id: i64) -> Self {
1583        self.params.message_thread_id = Some(id);
1584        self
1585    }
1586    /// Identifier of a direct messages chat topic.
1587    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1588        self.params.direct_messages_topic_id = Some(id);
1589        self
1590    }
1591    /// Sends the messages silently — recipients receive no notification sound.
1592    pub fn disable_notification(mut self, v: bool) -> Self {
1593        self.params.disable_notification = Some(v);
1594        self
1595    }
1596    /// Protects the messages from being forwarded or saved.
1597    pub fn protect_content(mut self, v: bool) -> Self {
1598        self.params.protect_content = Some(v);
1599        self
1600    }
1601}
1602
1603impl_into_future!(
1604    ForwardMessages,
1605    Vec<rustigram_types::message::MessageId>,
1606    "forwardMessages"
1607);
1608
1609// ─── copyMessages ─────────────────────────────────────────────────────────────
1610
1611#[derive(Serialize)]
1612struct CopyMessagesParams {
1613    chat_id: ChatId,
1614    from_chat_id: ChatId,
1615    message_ids: Vec<i64>,
1616    #[serde(skip_serializing_if = "Option::is_none")]
1617    message_thread_id: Option<i64>,
1618    #[serde(skip_serializing_if = "Option::is_none")]
1619    direct_messages_topic_id: Option<i64>,
1620    #[serde(skip_serializing_if = "Option::is_none")]
1621    disable_notification: Option<bool>,
1622    #[serde(skip_serializing_if = "Option::is_none")]
1623    protect_content: Option<bool>,
1624    #[serde(skip_serializing_if = "Option::is_none")]
1625    remove_caption: Option<bool>,
1626}
1627
1628/// Builder for the [`copyMessages`](https://core.telegram.org/bots/api#copymessages) method.
1629///
1630/// Copies 1–100 messages without a forward link, preserving album grouping.
1631/// Returns a `Vec<MessageId>` of the sent messages.
1632pub struct CopyMessages {
1633    client: BotClient,
1634    params: CopyMessagesParams,
1635}
1636
1637impl CopyMessages {
1638    pub(crate) fn new(
1639        client: BotClient,
1640        chat_id: impl Into<ChatId>,
1641        from_chat_id: impl Into<ChatId>,
1642        message_ids: Vec<i64>,
1643    ) -> Self {
1644        Self {
1645            client,
1646            params: CopyMessagesParams {
1647                chat_id: chat_id.into(),
1648                from_chat_id: from_chat_id.into(),
1649                message_ids,
1650                message_thread_id: None,
1651                direct_messages_topic_id: None,
1652                disable_notification: None,
1653                protect_content: None,
1654                remove_caption: None,
1655            },
1656        }
1657    }
1658    /// Forum topic thread ID.
1659    pub fn message_thread_id(mut self, id: i64) -> Self {
1660        self.params.message_thread_id = Some(id);
1661        self
1662    }
1663    /// Identifier of a direct messages chat topic.
1664    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1665        self.params.direct_messages_topic_id = Some(id);
1666        self
1667    }
1668    /// Sends the messages silently — recipients receive no notification sound.
1669    pub fn disable_notification(mut self, v: bool) -> Self {
1670        self.params.disable_notification = Some(v);
1671        self
1672    }
1673    /// Protects the messages from being forwarded or saved.
1674    pub fn protect_content(mut self, v: bool) -> Self {
1675        self.params.protect_content = Some(v);
1676        self
1677    }
1678    /// Copies the messages without their captions.
1679    pub fn remove_caption(mut self, v: bool) -> Self {
1680        self.params.remove_caption = Some(v);
1681        self
1682    }
1683}
1684
1685impl_into_future!(
1686    CopyMessages,
1687    Vec<rustigram_types::message::MessageId>,
1688    "copyMessages"
1689);
1690
1691// ─── sendVenue ────────────────────────────────────────────────────────────────
1692
1693#[derive(Serialize)]
1694struct SendVenueParams {
1695    chat_id: ChatId,
1696    latitude: f64,
1697    longitude: f64,
1698    title: String,
1699    address: String,
1700    #[serde(skip_serializing_if = "Option::is_none")]
1701    message_thread_id: Option<i64>,
1702    #[serde(skip_serializing_if = "Option::is_none")]
1703    direct_messages_topic_id: Option<i64>,
1704    #[serde(skip_serializing_if = "Option::is_none")]
1705    foursquare_id: Option<String>,
1706    #[serde(skip_serializing_if = "Option::is_none")]
1707    foursquare_type: Option<String>,
1708    #[serde(skip_serializing_if = "Option::is_none")]
1709    google_place_id: Option<String>,
1710    #[serde(skip_serializing_if = "Option::is_none")]
1711    google_place_type: Option<String>,
1712    #[serde(skip_serializing_if = "Option::is_none")]
1713    disable_notification: Option<bool>,
1714    #[serde(skip_serializing_if = "Option::is_none")]
1715    protect_content: Option<bool>,
1716    #[serde(skip_serializing_if = "Option::is_none")]
1717    reply_parameters: Option<ReplyParameters>,
1718    #[serde(skip_serializing_if = "Option::is_none")]
1719    reply_markup: Option<ReplyMarkup>,
1720}
1721
1722/// Builder for the [`sendVenue`](https://core.telegram.org/bots/api#sendvenue) method.
1723pub struct SendVenue {
1724    client: BotClient,
1725    params: SendVenueParams,
1726}
1727
1728impl SendVenue {
1729    pub(crate) fn new(
1730        client: BotClient,
1731        chat_id: impl Into<ChatId>,
1732        latitude: f64,
1733        longitude: f64,
1734        title: impl Into<String>,
1735        address: impl Into<String>,
1736    ) -> Self {
1737        Self {
1738            client,
1739            params: SendVenueParams {
1740                chat_id: chat_id.into(),
1741                latitude,
1742                longitude,
1743                title: title.into(),
1744                address: address.into(),
1745                message_thread_id: None,
1746                direct_messages_topic_id: None,
1747                foursquare_id: None,
1748                foursquare_type: None,
1749                google_place_id: None,
1750                google_place_type: None,
1751                disable_notification: None,
1752                protect_content: None,
1753                reply_parameters: None,
1754                reply_markup: None,
1755            },
1756        }
1757    }
1758    /// Forum topic thread ID.
1759    pub fn message_thread_id(mut self, id: i64) -> Self {
1760        self.params.message_thread_id = Some(id);
1761        self
1762    }
1763    /// Identifier of a direct messages chat topic.
1764    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1765        self.params.direct_messages_topic_id = Some(id);
1766        self
1767    }
1768    /// Sets the Foursquare identifier of the venue.
1769    pub fn foursquare_id(mut self, id: impl Into<String>) -> Self {
1770        self.params.foursquare_id = Some(id.into());
1771        self
1772    }
1773    /// Sets the Foursquare type of the venue (e.g. `"arts_entertainment/aquarium"`).
1774    pub fn foursquare_type(mut self, t: impl Into<String>) -> Self {
1775        self.params.foursquare_type = Some(t.into());
1776        self
1777    }
1778    /// Sets the Google Places identifier of the venue.
1779    pub fn google_place_id(mut self, id: impl Into<String>) -> Self {
1780        self.params.google_place_id = Some(id.into());
1781        self
1782    }
1783    /// Sets the Google Places type of the venue.
1784    pub fn google_place_type(mut self, t: impl Into<String>) -> Self {
1785        self.params.google_place_type = Some(t.into());
1786        self
1787    }
1788    /// Sends the message silently — the recipient receives no notification sound.
1789    pub fn disable_notification(mut self, v: bool) -> Self {
1790        self.params.disable_notification = Some(v);
1791        self
1792    }
1793    /// Protects the message from being forwarded or saved.
1794    pub fn protect_content(mut self, v: bool) -> Self {
1795        self.params.protect_content = Some(v);
1796        self
1797    }
1798    /// Reply parameters for this message.
1799    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
1800        self.params.reply_parameters = Some(rp);
1801        self
1802    }
1803    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1804    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
1805        self.params.reply_markup = Some(m.into());
1806        self
1807    }
1808}
1809
1810impl_into_future!(SendVenue, Message, "sendVenue");
1811
1812// ─── sendMediaGroup ───────────────────────────────────────────────────────────
1813
1814#[derive(Serialize)]
1815struct SendMediaGroupParams {
1816    chat_id: ChatId,
1817    /// Array of `InputMedia` objects (photo, video, audio, or document).
1818    ///
1819    /// Uses `serde_json::Value` until the `InputMedia` enum is defined in
1820    /// Priority 4. Pass the result of `serde_json::to_value(&your_input_media_vec)`.
1821    media: Vec<serde_json::Value>,
1822    #[serde(skip_serializing_if = "Option::is_none")]
1823    message_thread_id: Option<i64>,
1824    #[serde(skip_serializing_if = "Option::is_none")]
1825    direct_messages_topic_id: Option<i64>,
1826    #[serde(skip_serializing_if = "Option::is_none")]
1827    business_connection_id: Option<String>,
1828    #[serde(skip_serializing_if = "Option::is_none")]
1829    disable_notification: Option<bool>,
1830    #[serde(skip_serializing_if = "Option::is_none")]
1831    protect_content: Option<bool>,
1832    #[serde(skip_serializing_if = "Option::is_none")]
1833    reply_parameters: Option<ReplyParameters>,
1834}
1835
1836/// Builder for the [`sendMediaGroup`](https://core.telegram.org/bots/api#sendmediagroup) method.
1837///
1838/// Sends a group of photos, videos, documents, or audios as an album (2–10 items).
1839///
1840/// The `media` parameter accepts `Vec<serde_json::Value>` until the `InputMedia`
1841/// enum is defined in Priority 4. Construct items with `serde_json::json!({...})`
1842/// or `serde_json::to_value(&input_media)`.
1843pub struct SendMediaGroup {
1844    client: BotClient,
1845    params: SendMediaGroupParams,
1846}
1847
1848impl SendMediaGroup {
1849    pub(crate) fn new(
1850        client: BotClient,
1851        chat_id: impl Into<ChatId>,
1852        media: Vec<serde_json::Value>,
1853    ) -> Self {
1854        Self {
1855            client,
1856            params: SendMediaGroupParams {
1857                chat_id: chat_id.into(),
1858                media,
1859                message_thread_id: None,
1860                direct_messages_topic_id: None,
1861                business_connection_id: None,
1862                disable_notification: None,
1863                protect_content: None,
1864                reply_parameters: None,
1865            },
1866        }
1867    }
1868    /// Forum topic thread ID.
1869    pub fn message_thread_id(mut self, id: i64) -> Self {
1870        self.params.message_thread_id = Some(id);
1871        self
1872    }
1873    /// Identifier of a direct messages chat topic.
1874    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1875        self.params.direct_messages_topic_id = Some(id);
1876        self
1877    }
1878    /// Business connection ID for sending on behalf of a business account.
1879    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
1880        self.params.business_connection_id = Some(id.into());
1881        self
1882    }
1883    /// Sends the messages silently — recipients receive no notification sound.
1884    pub fn disable_notification(mut self, v: bool) -> Self {
1885        self.params.disable_notification = Some(v);
1886        self
1887    }
1888    /// Protects the messages from being forwarded or saved.
1889    pub fn protect_content(mut self, v: bool) -> Self {
1890        self.params.protect_content = Some(v);
1891        self
1892    }
1893    /// Reply parameters for this message.
1894    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
1895        self.params.reply_parameters = Some(rp);
1896        self
1897    }
1898}
1899
1900impl_into_future!(SendMediaGroup, Vec<Message>, "sendMediaGroup");
1901
1902// ─── sendPaidMedia ────────────────────────────────────────────────────────────
1903
1904#[derive(Serialize)]
1905struct SendPaidMediaParams {
1906    chat_id: ChatId,
1907    star_count: u32,
1908    /// Array of `InputPaidMedia` objects (photo or video).
1909    ///
1910    /// Uses `serde_json::Value` until the `InputPaidMedia` enum is defined in
1911    /// Priority 4. Pass the result of `serde_json::to_value(&your_paid_media_vec)`.
1912    media: Vec<serde_json::Value>,
1913    #[serde(skip_serializing_if = "Option::is_none")]
1914    business_connection_id: Option<String>,
1915    #[serde(skip_serializing_if = "Option::is_none")]
1916    payload: Option<String>,
1917    #[serde(skip_serializing_if = "Option::is_none")]
1918    caption: Option<String>,
1919    #[serde(skip_serializing_if = "Option::is_none")]
1920    parse_mode: Option<ParseMode>,
1921    #[serde(skip_serializing_if = "Option::is_none")]
1922    show_caption_above_media: Option<bool>,
1923    #[serde(skip_serializing_if = "Option::is_none")]
1924    disable_notification: Option<bool>,
1925    #[serde(skip_serializing_if = "Option::is_none")]
1926    protect_content: Option<bool>,
1927    #[serde(skip_serializing_if = "Option::is_none")]
1928    reply_parameters: Option<ReplyParameters>,
1929    #[serde(skip_serializing_if = "Option::is_none")]
1930    reply_markup: Option<ReplyMarkup>,
1931}
1932
1933/// Builder for the [`sendPaidMedia`](https://core.telegram.org/bots/api#sendpaidmedia) method.
1934///
1935/// Sends paid media that users must pay Telegram Stars to view (up to 10 items).
1936///
1937/// The `media` parameter accepts `Vec<serde_json::Value>` until the `InputPaidMedia`
1938/// enum is defined in Priority 4.
1939pub struct SendPaidMedia {
1940    client: BotClient,
1941    params: SendPaidMediaParams,
1942}
1943
1944impl SendPaidMedia {
1945    pub(crate) fn new(
1946        client: BotClient,
1947        chat_id: impl Into<ChatId>,
1948        star_count: u32,
1949        media: Vec<serde_json::Value>,
1950    ) -> Self {
1951        Self {
1952            client,
1953            params: SendPaidMediaParams {
1954                chat_id: chat_id.into(),
1955                star_count,
1956                media,
1957                business_connection_id: None,
1958                payload: None,
1959                caption: None,
1960                parse_mode: None,
1961                show_caption_above_media: None,
1962                disable_notification: None,
1963                protect_content: None,
1964                reply_parameters: None,
1965                reply_markup: None,
1966            },
1967        }
1968    }
1969    /// Business connection ID for sending on behalf of a business account.
1970    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
1971        self.params.business_connection_id = Some(id.into());
1972        self
1973    }
1974    /// Bot-defined paid media payload (0–128 bytes); not shown to the user.
1975    pub fn payload(mut self, p: impl Into<String>) -> Self {
1976        self.params.payload = Some(p.into());
1977        self
1978    }
1979    /// Sets the caption (0–1024 characters).
1980    pub fn caption(mut self, c: impl Into<String>) -> Self {
1981        self.params.caption = Some(c.into());
1982        self
1983    }
1984    /// Sets the caption parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
1985    pub fn parse_mode(mut self, m: ParseMode) -> Self {
1986        self.params.parse_mode = Some(m);
1987        self
1988    }
1989    /// Shows the caption above the media instead of below it.
1990    pub fn show_caption_above_media(mut self, v: bool) -> Self {
1991        self.params.show_caption_above_media = Some(v);
1992        self
1993    }
1994    /// Sends the message silently — the recipient receives no notification sound.
1995    pub fn disable_notification(mut self, v: bool) -> Self {
1996        self.params.disable_notification = Some(v);
1997        self
1998    }
1999    /// Protects the message from being forwarded or saved.
2000    pub fn protect_content(mut self, v: bool) -> Self {
2001        self.params.protect_content = Some(v);
2002        self
2003    }
2004    /// Reply parameters for this message.
2005    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2006        self.params.reply_parameters = Some(rp);
2007        self
2008    }
2009    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
2010    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
2011        self.params.reply_markup = Some(m.into());
2012        self
2013    }
2014}
2015
2016impl_into_future!(SendPaidMedia, Message, "sendPaidMedia");
2017
2018// ─── sendGame ─────────────────────────────────────────────────────────────────
2019
2020#[derive(Serialize)]
2021struct SendGameParams {
2022    chat_id: i64,
2023    game_short_name: String,
2024    #[serde(skip_serializing_if = "Option::is_none")]
2025    business_connection_id: Option<String>,
2026    #[serde(skip_serializing_if = "Option::is_none")]
2027    message_thread_id: Option<i64>,
2028    #[serde(skip_serializing_if = "Option::is_none")]
2029    direct_messages_topic_id: Option<i64>,
2030    #[serde(skip_serializing_if = "Option::is_none")]
2031    disable_notification: Option<bool>,
2032    #[serde(skip_serializing_if = "Option::is_none")]
2033    protect_content: Option<bool>,
2034    #[serde(skip_serializing_if = "Option::is_none")]
2035    reply_parameters: Option<ReplyParameters>,
2036    #[serde(skip_serializing_if = "Option::is_none")]
2037    reply_markup: Option<rustigram_types::keyboard::InlineKeyboardMarkup>,
2038}
2039
2040/// Builder for the [`sendGame`](https://core.telegram.org/bots/api#sendgame) method.
2041///
2042/// Note: `chat_id` is an integer — games can't be sent to channel direct messages
2043/// chats or channel chats.
2044pub struct SendGame {
2045    client: BotClient,
2046    params: SendGameParams,
2047}
2048
2049impl SendGame {
2050    pub(crate) fn new(client: BotClient, chat_id: i64, game_short_name: impl Into<String>) -> Self {
2051        Self {
2052            client,
2053            params: SendGameParams {
2054                chat_id,
2055                game_short_name: game_short_name.into(),
2056                business_connection_id: None,
2057                message_thread_id: None,
2058                direct_messages_topic_id: None,
2059                disable_notification: None,
2060                protect_content: None,
2061                reply_parameters: None,
2062                reply_markup: None,
2063            },
2064        }
2065    }
2066    /// Business connection ID for sending on behalf of a business account.
2067    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
2068        self.params.business_connection_id = Some(id.into());
2069        self
2070    }
2071    /// Forum topic thread ID.
2072    pub fn message_thread_id(mut self, id: i64) -> Self {
2073        self.params.message_thread_id = Some(id);
2074        self
2075    }
2076    /// Identifier of a direct messages chat topic.
2077    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2078        self.params.direct_messages_topic_id = Some(id);
2079        self
2080    }
2081    /// Sends the message silently — the recipient receives no notification sound.
2082    pub fn disable_notification(mut self, v: bool) -> Self {
2083        self.params.disable_notification = Some(v);
2084        self
2085    }
2086    /// Protects the message from being forwarded or saved.
2087    pub fn protect_content(mut self, v: bool) -> Self {
2088        self.params.protect_content = Some(v);
2089        self
2090    }
2091    /// Reply parameters for this message.
2092    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2093        self.params.reply_parameters = Some(rp);
2094        self
2095    }
2096    /// Attaches an inline keyboard. The first button must launch the game.
2097    pub fn reply_markup(mut self, m: rustigram_types::keyboard::InlineKeyboardMarkup) -> Self {
2098        self.params.reply_markup = Some(m);
2099        self
2100    }
2101}
2102
2103impl_into_future!(SendGame, Message, "sendGame");
2104
2105// ─── sendChecklist ────────────────────────────────────────────────────────────
2106
2107#[derive(Serialize)]
2108struct SendChecklistParams {
2109    business_connection_id: String,
2110    chat_id: i64,
2111    checklist: rustigram_types::checklist::InputChecklist,
2112    #[serde(skip_serializing_if = "Option::is_none")]
2113    direct_messages_topic_id: Option<i64>,
2114    #[serde(skip_serializing_if = "Option::is_none")]
2115    disable_notification: Option<bool>,
2116    #[serde(skip_serializing_if = "Option::is_none")]
2117    protect_content: Option<bool>,
2118    #[serde(skip_serializing_if = "Option::is_none")]
2119    message_effect_id: Option<String>,
2120    #[serde(skip_serializing_if = "Option::is_none")]
2121    reply_parameters: Option<ReplyParameters>,
2122    #[serde(skip_serializing_if = "Option::is_none")]
2123    reply_markup: Option<rustigram_types::keyboard::InlineKeyboardMarkup>,
2124    #[serde(skip_serializing_if = "Option::is_none")]
2125    suggested_post_parameters: Option<SuggestedPostParameters>,
2126}
2127
2128/// Builder for the [`sendChecklist`](https://core.telegram.org/bots/api#sendchecklist) method.
2129///
2130/// Business bots only — sends a checklist on behalf of a connected business account.
2131/// Requires the `can_reply` business bot right.
2132pub struct SendChecklist {
2133    client: BotClient,
2134    params: SendChecklistParams,
2135}
2136
2137impl SendChecklist {
2138    pub(crate) fn new(
2139        client: BotClient,
2140        business_connection_id: impl Into<String>,
2141        chat_id: i64,
2142        checklist: rustigram_types::checklist::InputChecklist,
2143    ) -> Self {
2144        Self {
2145            client,
2146            params: SendChecklistParams {
2147                business_connection_id: business_connection_id.into(),
2148                chat_id,
2149                checklist,
2150                direct_messages_topic_id: None,
2151                disable_notification: None,
2152                protect_content: None,
2153                message_effect_id: None,
2154                reply_parameters: None,
2155                reply_markup: None,
2156                suggested_post_parameters: None,
2157            },
2158        }
2159    }
2160    /// Identifier of a direct messages chat topic.
2161    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2162        self.params.direct_messages_topic_id = Some(id);
2163        self
2164    }
2165    /// Sends the message silently — the recipient receives no notification sound.
2166    pub fn disable_notification(mut self, v: bool) -> Self {
2167        self.params.disable_notification = Some(v);
2168        self
2169    }
2170    /// Protects the message from being forwarded or saved.
2171    pub fn protect_content(mut self, v: bool) -> Self {
2172        self.params.protect_content = Some(v);
2173        self
2174    }
2175    /// Unique identifier of the message effect to add to the message.
2176    pub fn message_effect_id(mut self, id: impl Into<String>) -> Self {
2177        self.params.message_effect_id = Some(id.into());
2178        self
2179    }
2180    /// Reply parameters for this message.
2181    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2182        self.params.reply_parameters = Some(rp);
2183        self
2184    }
2185    /// Attaches an inline keyboard to the message.
2186    pub fn reply_markup(mut self, m: rustigram_types::keyboard::InlineKeyboardMarkup) -> Self {
2187        self.params.reply_markup = Some(m);
2188        self
2189    }
2190    /// Suggested post parameters for channel direct messages chats.
2191    pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self {
2192        self.params.suggested_post_parameters = Some(params);
2193        self
2194    }
2195}
2196
2197impl_into_future!(SendChecklist, Message, "sendChecklist");