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}
764
765/// Builder for the [`sendPoll`](https://core.telegram.org/bots/api#sendpoll) method.
766pub struct SendPoll {
767    client: BotClient,
768    params: SendPollParams,
769}
770
771impl SendPoll {
772    pub(crate) fn new(
773        client: BotClient,
774        chat_id: impl Into<ChatId>,
775        question: impl Into<String>,
776        options: Vec<InputPollOption>,
777    ) -> Self {
778        Self {
779            client,
780            params: SendPollParams {
781                chat_id: chat_id.into(),
782                question: question.into(),
783                options,
784                question_parse_mode: None,
785                question_entities: None,
786                message_thread_id: None,
787                direct_messages_topic_id: None,
788                poll_type: None,
789                is_anonymous: None,
790                allows_multiple_answers: None,
791                allows_revoting: None,
792                correct_option_ids: None,
793                explanation: None,
794                explanation_parse_mode: None,
795                explanation_entities: None,
796                open_period: None,
797                close_date: None,
798                is_closed: None,
799                shuffle_options: None,
800                allow_adding_options: None,
801                hide_results_until_closes: None,
802                description: None,
803                description_parse_mode: None,
804                description_entities: None,
805                disable_notification: None,
806                protect_content: None,
807                reply_parameters: None,
808                reply_markup: None,
809                suggested_post_parameters: None,
810            },
811        }
812    }
813    /// Forum topic thread ID.
814    pub fn message_thread_id(mut self, id: i64) -> Self {
815        self.params.message_thread_id = Some(id);
816        self
817    }
818    /// Identifier of a direct messages chat topic.
819    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
820        self.params.direct_messages_topic_id = Some(id);
821        self
822    }
823    /// Sets whether the poll is anonymous.
824    pub fn is_anonymous(mut self, v: bool) -> Self {
825        self.params.is_anonymous = Some(v);
826        self
827    }
828    /// Allows voters to select multiple answers.
829    pub fn allows_multiple_answers(mut self, v: bool) -> Self {
830        self.params.allows_multiple_answers = Some(v);
831        self
832    }
833    /// Allows voters to change their vote.
834    pub fn allows_revoting(mut self, v: bool) -> Self {
835        self.params.allows_revoting = Some(v);
836        self
837    }
838    /// Converts the poll to a quiz with the given correct option indices.
839    pub fn quiz(mut self, ids: Vec<u8>) -> Self {
840        self.params.poll_type = Some(rustigram_types::poll::PollType::Quiz);
841        self.params.correct_option_ids = Some(ids);
842        self
843    }
844    /// Convenience method for a quiz with a single correct option.
845    pub fn quiz_single(self, id: u8) -> Self {
846        self.quiz(vec![id])
847    }
848    /// Sets the explanation text shown after a quiz answer.
849    pub fn explanation(mut self, text: impl Into<String>) -> Self {
850        self.params.explanation = Some(text.into());
851        self
852    }
853    /// Sets the parse mode for the explanation.
854    pub fn explanation_parse_mode(mut self, mode: ParseMode) -> Self {
855        self.params.explanation_parse_mode = Some(mode);
856        self
857    }
858    /// Sets entities for the explanation.
859    pub fn explanation_entities(mut self, e: Vec<rustigram_types::message::MessageEntity>) -> Self {
860        self.params.explanation_entities = Some(e);
861        self
862    }
863    /// Sets how long the poll stays open in seconds (5–2628000).
864    pub fn open_period(mut self, secs: u32) -> Self {
865        self.params.open_period = Some(secs);
866        self
867    }
868    /// Sets the Unix timestamp when the poll closes automatically.
869    pub fn close_date(mut self, ts: i64) -> Self {
870        self.params.close_date = Some(ts);
871        self
872    }
873    /// Sets whether the options should be shuffled.
874    pub fn shuffle_options(mut self, v: bool) -> Self {
875        self.params.shuffle_options = Some(v);
876        self
877    }
878    /// Allows users to add their own options to the poll.
879    pub fn allow_adding_options(mut self, v: bool) -> Self {
880        self.params.allow_adding_options = Some(v);
881        self
882    }
883    /// Hides the poll results until it's closed.
884    pub fn hide_results_until_closes(mut self, v: bool) -> Self {
885        self.params.hide_results_until_closes = Some(v);
886        self
887    }
888    /// Sets the poll description (0-1024 chars).
889    pub fn description(mut self, d: impl Into<String>) -> Self {
890        self.params.description = Some(d.into());
891        self
892    }
893    /// Sets description parse mode.
894    pub fn description_parse_mode(mut self, mode: ParseMode) -> Self {
895        self.params.description_parse_mode = Some(mode);
896        self
897    }
898    /// Sets description entities.
899    pub fn description_entities(mut self, e: Vec<rustigram_types::message::MessageEntity>) -> Self {
900        self.params.description_entities = Some(e);
901        self
902    }
903    /// Sets the question parse mode.
904    pub fn question_parse_mode(mut self, mode: ParseMode) -> Self {
905        self.params.question_parse_mode = Some(mode);
906        self
907    }
908    /// Sets question entities.
909    pub fn question_entities(mut self, e: Vec<rustigram_types::message::MessageEntity>) -> Self {
910        self.params.question_entities = Some(e);
911        self
912    }
913    /// Sends the message silently — the recipient receives no notification sound.
914    pub fn disable_notification(mut self, v: bool) -> Self {
915        self.params.disable_notification = Some(v);
916        self
917    }
918    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
919    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
920        self.params.reply_markup = Some(m.into());
921        self
922    }
923    /// Suggested post parameters for channel direct messages chats.
924    pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self {
925        self.params.suggested_post_parameters = Some(params);
926        self
927    }
928}
929
930impl_into_future!(SendPoll, Message, "sendPoll");
931
932// ─── sendMessageDraft ─────────────────────────────────────────────────────────
933
934#[derive(Serialize)]
935struct SendMessageDraftParams {
936    chat_id: ChatId,
937    draft_id: i64,
938    text: String,
939    #[serde(skip_serializing_if = "Option::is_none")]
940    message_thread_id: Option<i64>,
941    #[serde(skip_serializing_if = "Option::is_none")]
942    parse_mode: Option<ParseMode>,
943    #[serde(skip_serializing_if = "Option::is_none")]
944    entities: Option<Vec<rustigram_types::message::MessageEntity>>,
945}
946
947/// Builder for the [`sendMessageDraft`](https://core.telegram.org/bots/api#sendmessagedraft) method.
948/// Streams a partial message to the user while it is being generated (Bot API 9.5+).
949pub struct SendMessageDraft {
950    client: BotClient,
951    params: SendMessageDraftParams,
952}
953
954impl SendMessageDraft {
955    pub(crate) fn new(
956        client: BotClient,
957        chat_id: impl Into<ChatId>,
958        draft_id: i64,
959        text: impl Into<String>,
960    ) -> Self {
961        Self {
962            client,
963            params: SendMessageDraftParams {
964                chat_id: chat_id.into(),
965                draft_id,
966                text: text.into(),
967                message_thread_id: None,
968                parse_mode: None,
969                entities: None,
970            },
971        }
972    }
973    /// Sets the text parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
974    pub fn parse_mode(mut self, m: ParseMode) -> Self {
975        self.params.parse_mode = Some(m);
976        self
977    }
978    /// Sets custom message entities instead of using a parse mode.
979    pub fn entities(mut self, e: Vec<rustigram_types::message::MessageEntity>) -> Self {
980        self.params.entities = Some(e);
981        self
982    }
983}
984
985impl_into_future!(SendMessageDraft, bool, "sendMessageDraft");
986
987// ─── File-sending builders ────────────────────────────────────────────────────
988//
989// Photo, Audio, Document, Video, Animation, Voice, VideoNote each share a
990// similar shape but differ in field names and constraints. We use a common
991// pattern: store the InputFile and an optional Form for multipart, and build
992// the form lazily in `IntoFuture`.
993
994/// Common optional parameters shared by most media-send methods.
995#[derive(Default)]
996pub struct MediaSendOptions {
997    /// Business connection ID for sending on behalf of a business account.
998    pub business_connection_id: Option<String>,
999    /// Forum topic thread ID.
1000    pub message_thread_id: Option<i64>,
1001    /// Identifier of a direct messages chat topic.
1002    pub direct_messages_topic_id: Option<i64>,
1003    /// Sets the caption (0–1024 characters) for media messages.
1004    pub caption: Option<String>,
1005    /// Parse mode for the caption.
1006    pub parse_mode: Option<ParseMode>,
1007    /// Special entities in the caption.
1008    pub caption_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
1009    /// Shows the caption above the media instead of below it.
1010    pub show_caption_above_media: Option<bool>,
1011    /// Marks the media as a spoiler — blurs it until the user taps to reveal.
1012    pub has_spoiler: Option<bool>,
1013    /// Sends the message silently — the recipient receives no notification sound.
1014    pub disable_notification: Option<bool>,
1015    /// Protects the message from being forwarded or saved.
1016    pub protect_content: Option<bool>,
1017    /// Allows sending to large audiences at the cost of Telegram Stars.
1018    pub allow_paid_broadcast: Option<bool>,
1019    /// Reply parameters for this message.
1020    pub reply_parameters: Option<ReplyParameters>,
1021    /// Reply markup attached to the message.
1022    pub reply_markup: Option<ReplyMarkup>,
1023    /// Suggested post parameters for channel direct messages chats.
1024    pub suggested_post_parameters: Option<SuggestedPostParameters>,
1025}
1026
1027/// Builds the JSON body for a simple (non-file-upload) part of a media send.
1028fn media_json_body(
1029    chat_id: &ChatId,
1030    media_field: &str,
1031    media_value: &str,
1032    opts: &MediaSendOptions,
1033    extra: serde_json::Value,
1034) -> serde_json::Value {
1035    let mut map = serde_json::json!({
1036        "chat_id": chat_id,
1037        media_field: media_value,
1038    });
1039    let obj = map.as_object_mut().unwrap();
1040    if let Some(v) = &opts.business_connection_id {
1041        obj.insert("business_connection_id".to_owned(), serde_json::json!(v));
1042    }
1043    if let Some(v) = &opts.message_thread_id {
1044        obj.insert("message_thread_id".to_owned(), serde_json::json!(v));
1045    }
1046    if let Some(v) = &opts.direct_messages_topic_id {
1047        obj.insert("direct_messages_topic_id".to_owned(), serde_json::json!(v));
1048    }
1049    if let Some(v) = &opts.caption {
1050        obj.insert("caption".to_owned(), serde_json::json!(v));
1051    }
1052    if let Some(v) = &opts.parse_mode {
1053        obj.insert("parse_mode".to_owned(), serde_json::json!(v));
1054    }
1055    if let Some(v) = &opts.caption_entities {
1056        obj.insert("caption_entities".to_owned(), serde_json::json!(v));
1057    }
1058    if let Some(v) = opts.show_caption_above_media {
1059        obj.insert("show_caption_above_media".to_owned(), serde_json::json!(v));
1060    }
1061    if let Some(v) = opts.has_spoiler {
1062        obj.insert("has_spoiler".to_owned(), serde_json::json!(v));
1063    }
1064    if let Some(v) = opts.disable_notification {
1065        obj.insert("disable_notification".to_owned(), serde_json::json!(v));
1066    }
1067    if let Some(v) = opts.protect_content {
1068        obj.insert("protect_content".to_owned(), serde_json::json!(v));
1069    }
1070    if let Some(v) = opts.allow_paid_broadcast {
1071        obj.insert("allow_paid_broadcast".to_owned(), serde_json::json!(v));
1072    }
1073    if let Some(v) = &opts.reply_parameters {
1074        obj.insert("reply_parameters".to_owned(), serde_json::json!(v));
1075    }
1076    if let Some(v) = &opts.reply_markup {
1077        obj.insert("reply_markup".to_owned(), serde_json::json!(v));
1078    }
1079    if let Some(v) = &opts.suggested_post_parameters {
1080        obj.insert("suggested_post_parameters".to_owned(), serde_json::json!(v));
1081    }
1082    if let serde_json::Value::Object(extra_obj) = extra {
1083        for (k, v) in extra_obj {
1084            obj.insert(k, v);
1085        }
1086    }
1087    map
1088}
1089
1090// ─── sendPhoto ────────────────────────────────────────────────────────────────
1091
1092/// Builder for the [`sendPhoto`](https://core.telegram.org/bots/api#sendphoto) method.
1093pub struct SendPhoto {
1094    client: BotClient,
1095    chat_id: ChatId,
1096    photo: InputFile,
1097    opts: MediaSendOptions,
1098}
1099
1100impl SendPhoto {
1101    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, photo: InputFile) -> Self {
1102        Self {
1103            client,
1104            chat_id: chat_id.into(),
1105            photo,
1106            opts: MediaSendOptions::default(),
1107        }
1108    }
1109    /// Business connection ID for sending on behalf of a business account.
1110    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
1111        self.opts.business_connection_id = Some(id.into());
1112        self
1113    }
1114    /// Forum topic thread ID.
1115    pub fn message_thread_id(mut self, id: i64) -> Self {
1116        self.opts.message_thread_id = Some(id);
1117        self
1118    }
1119    /// Identifier of a direct messages chat topic.
1120    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1121        self.opts.direct_messages_topic_id = Some(id);
1122        self
1123    }
1124    /// Sets the caption (0–1024 characters) for media messages.
1125    pub fn caption(mut self, c: impl Into<String>) -> Self {
1126        self.opts.caption = Some(c.into());
1127        self
1128    }
1129    /// Sets the caption parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
1130    pub fn parse_mode(mut self, m: ParseMode) -> Self {
1131        self.opts.parse_mode = Some(m);
1132        self
1133    }
1134    /// Marks the media as a spoiler — blurs it until the user taps to reveal.
1135    pub fn has_spoiler(mut self, v: bool) -> Self {
1136        self.opts.has_spoiler = Some(v);
1137        self
1138    }
1139    /// Shows the caption above the media instead of below it.
1140    pub fn show_caption_above_media(mut self, v: bool) -> Self {
1141        self.opts.show_caption_above_media = Some(v);
1142        self
1143    }
1144    /// Sends the message silently — the recipient receives no notification sound.
1145    pub fn disable_notification(mut self, v: bool) -> Self {
1146        self.opts.disable_notification = Some(v);
1147        self
1148    }
1149    /// Protects the message from being forwarded or saved.
1150    pub fn protect_content(mut self, v: bool) -> Self {
1151        self.opts.protect_content = Some(v);
1152        self
1153    }
1154    /// Allows sending to large audiences at the cost of Telegram Stars.
1155    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
1156        self.opts.allow_paid_broadcast = Some(v);
1157        self
1158    }
1159    /// Reply parameters for this message.
1160    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
1161        self.opts.reply_parameters = Some(rp);
1162        self
1163    }
1164    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1165    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
1166        self.opts.reply_markup = Some(m.into());
1167        self
1168    }
1169    /// Suggested post parameters for channel direct messages chats.
1170    pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self {
1171        self.opts.suggested_post_parameters = Some(params);
1172        self
1173    }
1174}
1175
1176impl IntoFuture for SendPhoto {
1177    type Output = Result<Message>;
1178    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1179
1180    fn into_future(self) -> Self::IntoFuture {
1181        Box::pin(async move {
1182            match &self.photo {
1183                InputFile::Bytes {
1184                    filename,
1185                    data,
1186                    mime_type,
1187                } => {
1188                    let part = Part::bytes(data.clone())
1189                        .file_name(filename.clone())
1190                        .mime_str(mime_type)
1191                        .map_err(|e| crate::error::Error::Decode(e.to_string()))?;
1192                    let mut form = Form::new().part("photo", part);
1193                    form = form.text("chat_id", self.chat_id.to_string());
1194                    if let Some(id) = &self.opts.business_connection_id {
1195                        form = form.text("business_connection_id", id.clone());
1196                    }
1197                    if let Some(id) = self.opts.message_thread_id {
1198                        form = form.text("message_thread_id", id.to_string());
1199                    }
1200                    if let Some(id) = self.opts.direct_messages_topic_id {
1201                        form = form.text("direct_messages_topic_id", id.to_string());
1202                    }
1203                    if let Some(c) = &self.opts.caption {
1204                        form = form.text("caption", c.clone());
1205                    }
1206                    if let Some(m) = &self.opts.parse_mode {
1207                        form = form.text("parse_mode", format!("{m:?}"));
1208                    }
1209                    if let Some(v) = self.opts.disable_notification {
1210                        form = form.text("disable_notification", v.to_string());
1211                    }
1212                    if let Some(v) = self.opts.has_spoiler {
1213                        form = form.text("has_spoiler", v.to_string());
1214                    }
1215                    if let Some(v) = &self.opts.reply_markup {
1216                        form = form.text("reply_markup", serde_json::to_string(v).unwrap());
1217                    }
1218                    if let Some(p) = &self.opts.suggested_post_parameters {
1219                        form = form.text(
1220                            "suggested_post_parameters",
1221                            serde_json::to_string(p).unwrap(),
1222                        );
1223                    }
1224                    self.client.post_multipart("sendPhoto", form).await
1225                }
1226                _ => {
1227                    let body = media_json_body(
1228                        &self.chat_id,
1229                        "photo",
1230                        self.photo.as_str(),
1231                        &self.opts,
1232                        serde_json::Value::Null,
1233                    );
1234                    self.client.post_json("sendPhoto", &body).await
1235                }
1236            }
1237        })
1238    }
1239}
1240
1241// ─── Macro for simpler media senders (Audio, Document, Video, Animation, Voice, VideoNote, Sticker)
1242
1243macro_rules! media_sender {
1244    ($(#[$doc:meta])* $name:ident, $field:literal, $method:literal, $return_ty:ty, [$($extra_field:ident: $extra_ty:ty),*]) => {
1245        $(#[$doc])*
1246        pub struct $name {
1247            /// The API client to use for sending the request.
1248            client: BotClient,
1249            /// Unique identifier for the target chat or username of the target channel.
1250            chat_id: ChatId,
1251            /// The file to send. Can be a file ID, URL, or new upload.
1252            file: InputFile,
1253            /// Common optional parameters for media sending.
1254            opts: MediaSendOptions,
1255            /// Extra optional parameters specific to this media type.
1256            $($extra_field: Option<$extra_ty>,)*
1257        }
1258
1259        impl $name {
1260            pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, file: InputFile) -> Self {
1261                Self {
1262                    client,
1263                    chat_id: chat_id.into(),
1264                    file,
1265                    opts: MediaSendOptions::default(),
1266                    $($extra_field: None,)*
1267                }
1268            }
1269            /// Business connection ID for sending on behalf of a business account.
1270            pub fn business_connection_id(mut self, id: impl Into<String>) -> Self { self.opts.business_connection_id = Some(id.into()); self }
1271            /// Forum topic thread ID.
1272            pub fn message_thread_id(mut self, id: i64) -> Self { self.opts.message_thread_id = Some(id); self }
1273            /// Identifier of a direct messages chat topic.
1274            pub fn direct_messages_topic_id(mut self, id: i64) -> Self { self.opts.direct_messages_topic_id = Some(id); self }
1275            /// Sets the caption (0–1024 characters) for media messages.
1276            pub fn caption(mut self, c: impl Into<String>) -> Self { self.opts.caption = Some(c.into()); self }
1277            /// Sets the caption parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
1278            pub fn parse_mode(mut self, m: ParseMode) -> Self { self.opts.parse_mode = Some(m); self }
1279            /// Sends the message silently — the recipient receives no notification sound.
1280            pub fn disable_notification(mut self, v: bool) -> Self { self.opts.disable_notification = Some(v); self }
1281            /// Protects the message from being forwarded or saved.
1282            pub fn protect_content(mut self, v: bool) -> Self { self.opts.protect_content = Some(v); self }
1283            /// Allows sending to large audiences at the cost of Telegram Stars.
1284            pub fn allow_paid_broadcast(mut self, v: bool) -> Self { self.opts.allow_paid_broadcast = Some(v); self }
1285            /// Reply parameters for this message.
1286            pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self { self.opts.reply_parameters = Some(rp); self }
1287            /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1288            pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self { self.opts.reply_markup = Some(m.into()); self }
1289            /// Suggested post parameters for channel direct messages chats.
1290            pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self { self.opts.suggested_post_parameters = Some(params); self }
1291
1292            $(
1293                #[doc = concat!("Sets the ", stringify!($extra_field), " for the media.")]
1294                pub fn $extra_field(mut self, v: $extra_ty) -> Self {
1295                    self.$extra_field = Some(v);
1296                    self
1297                }
1298            )*
1299        }
1300
1301        impl IntoFuture for $name {
1302            type Output = Result<$return_ty>;
1303            type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1304
1305            fn into_future(self) -> Self::IntoFuture {
1306                Box::pin(async move {
1307                    match &self.file {
1308                        InputFile::Bytes { filename, data, mime_type } => {
1309                            let part = Part::bytes(data.clone())
1310                                .file_name(filename.clone())
1311                                .mime_str(mime_type)
1312                                .map_err(|e| crate::error::Error::Decode(e.to_string()))?;
1313                            let mut form = Form::new().part($field, part);
1314                            form = form.text("chat_id", self.chat_id.to_string());
1315                            if let Some(id) = &self.opts.business_connection_id { form = form.text("business_connection_id", id.clone()); }
1316                            if let Some(id) = self.opts.message_thread_id { form = form.text("message_thread_id", id.to_string()); }
1317                            if let Some(id) = self.opts.direct_messages_topic_id { form = form.text("direct_messages_topic_id", id.to_string()); }
1318                            if let Some(c) = &self.opts.caption { form = form.text("caption", c.clone()); }
1319                            if let Some(v) = self.opts.disable_notification { form = form.text("disable_notification", v.to_string()); }
1320                            if let Some(v) = &self.opts.reply_markup { form = form.text("reply_markup", serde_json::to_string(v).unwrap()); }
1321                            if let Some(p) = &self.opts.suggested_post_parameters { form = form.text("suggested_post_parameters", serde_json::to_string(p).unwrap()); }
1322
1323                            $(
1324                                if let Some(ref v) = self.$extra_field {
1325                                    form = form.text(stringify!($extra_field), v.to_string());
1326                                }
1327                            )*
1328
1329                            self.client.post_multipart($method, form).await
1330                        }
1331                        _ => {
1332                            let mut extra = serde_json::json!({});
1333                            $(
1334                                if let Some(ref v) = self.$extra_field {
1335                                    extra[stringify!($extra_field)] = serde_json::json!(v);
1336                                }
1337                            )*
1338                            let body = media_json_body(&self.chat_id, $field, self.file.as_str(), &self.opts, extra);
1339                            self.client.post_json($method, &body).await
1340                        }
1341                    }
1342                })
1343            }
1344        }
1345    };
1346}
1347
1348media_sender!(
1349    /// Builder for the [`sendAudio`](https://core.telegram.org/bots/api#sendaudio) method.
1350    SendAudio,      "audio",      "sendAudio",      Message, [duration: u32, performer: String, title: String]);
1351media_sender!(
1352    /// Builder for the [`sendDocument`](https://core.telegram.org/bots/api#senddocument) method.
1353    SendDocument,  "document",   "sendDocument",  Message, [disable_content_type_detection: bool]);
1354media_sender!(
1355    /// Builder for the [`sendVideo`](https://core.telegram.org/bots/api#sendvideo) method.
1356    SendVideo,      "video",      "sendVideo",      Message, [duration: u32, width: u32, height: u32, supports_streaming: bool, cover: String, start_timestamp: i64]);
1357media_sender!(
1358    /// Builder for the [`sendAnimation`](https://core.telegram.org/bots/api#sendanimation) method.
1359    SendAnimation, "animation",  "sendAnimation", Message, [duration: u32, width: u32, height: u32]);
1360media_sender!(
1361    /// Builder for the [`sendVoice`](https://core.telegram.org/bots/api#sendvoice) method.
1362    SendVoice,      "voice",      "sendVoice",      Message, [duration: u32]);
1363media_sender!(
1364    /// Builder for the [`sendVideoNote`](https://core.telegram.org/bots/api#sendvideonote) method.
1365    SendVideoNote, "video_note", "sendVideoNote", Message, [duration: u32, length: u32]);
1366media_sender!(
1367    /// Builder for the [`sendSticker`](https://core.telegram.org/bots/api#sendsticker) method.
1368    SendSticker,   "sticker",    "sendSticker",    Message, [emoji: String]);
1369
1370// ─── deleteMessage / deleteMessages ──────────────────────────────────────────
1371
1372#[derive(Serialize)]
1373struct DeleteMessageParams {
1374    chat_id: ChatId,
1375    message_id: i64,
1376}
1377
1378/// Builder for the [`deleteMessage`](https://core.telegram.org/bots/api#deletemessage) method.
1379pub struct DeleteMessage {
1380    client: BotClient,
1381    params: DeleteMessageParams,
1382}
1383impl DeleteMessage {
1384    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, message_id: i64) -> Self {
1385        Self {
1386            client,
1387            params: DeleteMessageParams {
1388                chat_id: chat_id.into(),
1389                message_id,
1390            },
1391        }
1392    }
1393}
1394impl_into_future!(DeleteMessage, bool, "deleteMessage");
1395
1396#[derive(Serialize)]
1397struct DeleteMessagesParams {
1398    chat_id: ChatId,
1399    message_ids: Vec<i64>,
1400}
1401
1402/// Builder for the [`deleteMessages`](https://core.telegram.org/bots/api#deletemessages) method.
1403pub struct DeleteMessages {
1404    client: BotClient,
1405    params: DeleteMessagesParams,
1406}
1407impl DeleteMessages {
1408    pub(crate) fn new(
1409        client: BotClient,
1410        chat_id: impl Into<ChatId>,
1411        message_ids: Vec<i64>,
1412    ) -> Self {
1413        Self {
1414            client,
1415            params: DeleteMessagesParams {
1416                chat_id: chat_id.into(),
1417                message_ids,
1418            },
1419        }
1420    }
1421}
1422impl_into_future!(DeleteMessages, bool, "deleteMessages");
1423
1424// ─── stopPoll ─────────────────────────────────────────────────────────────────
1425
1426#[derive(Serialize)]
1427struct StopPollParams {
1428    chat_id: ChatId,
1429    message_id: i64,
1430    #[serde(skip_serializing_if = "Option::is_none")]
1431    reply_markup: Option<rustigram_types::keyboard::InlineKeyboardMarkup>,
1432}
1433
1434/// Builder for the [`stopPoll`](https://core.telegram.org/bots/api#stoppoll) method.
1435pub struct StopPoll {
1436    client: BotClient,
1437    params: StopPollParams,
1438}
1439impl StopPoll {
1440    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, message_id: i64) -> Self {
1441        Self {
1442            client,
1443            params: StopPollParams {
1444                chat_id: chat_id.into(),
1445                message_id,
1446                reply_markup: None,
1447            },
1448        }
1449    }
1450    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1451    pub fn reply_markup(mut self, m: rustigram_types::keyboard::InlineKeyboardMarkup) -> Self {
1452        self.params.reply_markup = Some(m);
1453        self
1454    }
1455}
1456impl_into_future!(StopPoll, rustigram_types::poll::Poll, "stopPoll");
1457
1458// ─── answerCallbackQuery ──────────────────────────────────────────────────────
1459
1460#[derive(Serialize)]
1461struct AnswerCallbackQueryParams {
1462    callback_query_id: String,
1463    #[serde(skip_serializing_if = "Option::is_none")]
1464    text: Option<String>,
1465    #[serde(skip_serializing_if = "Option::is_none")]
1466    show_alert: Option<bool>,
1467    #[serde(skip_serializing_if = "Option::is_none")]
1468    url: Option<String>,
1469    #[serde(skip_serializing_if = "Option::is_none")]
1470    cache_time: Option<u32>,
1471}
1472
1473/// Builder for the [`answerCallbackQuery`](https://core.telegram.org/bots/api#answercallbackquery) method.
1474pub struct AnswerCallbackQuery {
1475    client: BotClient,
1476    params: AnswerCallbackQueryParams,
1477}
1478impl AnswerCallbackQuery {
1479    pub(crate) fn new(client: BotClient, callback_query_id: impl Into<String>) -> Self {
1480        Self {
1481            client,
1482            params: AnswerCallbackQueryParams {
1483                callback_query_id: callback_query_id.into(),
1484                text: None,
1485                show_alert: None,
1486                url: None,
1487                cache_time: None,
1488            },
1489        }
1490    }
1491    /// The text of the notification shown to the user. 0–200 characters.
1492    pub fn text(mut self, t: impl Into<String>) -> Self {
1493        self.params.text = Some(t.into());
1494        self
1495    }
1496    /// Shows an alert dialog instead of a toast notification for the callback answer.
1497    pub fn show_alert(mut self, v: bool) -> Self {
1498        self.params.show_alert = Some(v);
1499        self
1500    }
1501    /// Sets the URL to open when the callback button answer is tapped.
1502    pub fn url(mut self, u: impl Into<String>) -> Self {
1503        self.params.url = Some(u.into());
1504        self
1505    }
1506    /// Sets how long the callback answer may be cached on the client in seconds.
1507    pub fn cache_time(mut self, secs: u32) -> Self {
1508        self.params.cache_time = Some(secs);
1509        self
1510    }
1511    /// Shorthand for `.text(t).show_alert(true)` — shows a popup alert to the user.
1512    pub fn alert(self, text: impl Into<String>) -> Self {
1513        self.text(text).show_alert(true)
1514    }
1515}
1516impl_into_future!(AnswerCallbackQuery, bool, "answerCallbackQuery");
1517// ─── forwardMessages ──────────────────────────────────────────────────────────
1518
1519#[derive(Serialize)]
1520struct ForwardMessagesParams {
1521    chat_id: ChatId,
1522    from_chat_id: ChatId,
1523    message_ids: Vec<i64>,
1524    #[serde(skip_serializing_if = "Option::is_none")]
1525    message_thread_id: Option<i64>,
1526    #[serde(skip_serializing_if = "Option::is_none")]
1527    direct_messages_topic_id: Option<i64>,
1528    #[serde(skip_serializing_if = "Option::is_none")]
1529    disable_notification: Option<bool>,
1530    #[serde(skip_serializing_if = "Option::is_none")]
1531    protect_content: Option<bool>,
1532}
1533
1534/// Builder for the [`forwardMessages`](https://core.telegram.org/bots/api#forwardmessages) method.
1535///
1536/// Forwards 1–100 messages at once, preserving album grouping.
1537/// Returns a `Vec<MessageId>` of the sent messages.
1538pub struct ForwardMessages {
1539    client: BotClient,
1540    params: ForwardMessagesParams,
1541}
1542
1543impl ForwardMessages {
1544    pub(crate) fn new(
1545        client: BotClient,
1546        chat_id: impl Into<ChatId>,
1547        from_chat_id: impl Into<ChatId>,
1548        message_ids: Vec<i64>,
1549    ) -> Self {
1550        Self {
1551            client,
1552            params: ForwardMessagesParams {
1553                chat_id: chat_id.into(),
1554                from_chat_id: from_chat_id.into(),
1555                message_ids,
1556                message_thread_id: None,
1557                direct_messages_topic_id: None,
1558                disable_notification: None,
1559                protect_content: None,
1560            },
1561        }
1562    }
1563    /// Forum topic thread ID.
1564    pub fn message_thread_id(mut self, id: i64) -> Self {
1565        self.params.message_thread_id = Some(id);
1566        self
1567    }
1568    /// Identifier of a direct messages chat topic.
1569    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1570        self.params.direct_messages_topic_id = Some(id);
1571        self
1572    }
1573    /// Sends the messages silently — recipients receive no notification sound.
1574    pub fn disable_notification(mut self, v: bool) -> Self {
1575        self.params.disable_notification = Some(v);
1576        self
1577    }
1578    /// Protects the messages from being forwarded or saved.
1579    pub fn protect_content(mut self, v: bool) -> Self {
1580        self.params.protect_content = Some(v);
1581        self
1582    }
1583}
1584
1585impl_into_future!(
1586    ForwardMessages,
1587    Vec<rustigram_types::message::MessageId>,
1588    "forwardMessages"
1589);
1590
1591// ─── copyMessages ─────────────────────────────────────────────────────────────
1592
1593#[derive(Serialize)]
1594struct CopyMessagesParams {
1595    chat_id: ChatId,
1596    from_chat_id: ChatId,
1597    message_ids: Vec<i64>,
1598    #[serde(skip_serializing_if = "Option::is_none")]
1599    message_thread_id: Option<i64>,
1600    #[serde(skip_serializing_if = "Option::is_none")]
1601    direct_messages_topic_id: Option<i64>,
1602    #[serde(skip_serializing_if = "Option::is_none")]
1603    disable_notification: Option<bool>,
1604    #[serde(skip_serializing_if = "Option::is_none")]
1605    protect_content: Option<bool>,
1606    #[serde(skip_serializing_if = "Option::is_none")]
1607    remove_caption: Option<bool>,
1608}
1609
1610/// Builder for the [`copyMessages`](https://core.telegram.org/bots/api#copymessages) method.
1611///
1612/// Copies 1–100 messages without a forward link, preserving album grouping.
1613/// Returns a `Vec<MessageId>` of the sent messages.
1614pub struct CopyMessages {
1615    client: BotClient,
1616    params: CopyMessagesParams,
1617}
1618
1619impl CopyMessages {
1620    pub(crate) fn new(
1621        client: BotClient,
1622        chat_id: impl Into<ChatId>,
1623        from_chat_id: impl Into<ChatId>,
1624        message_ids: Vec<i64>,
1625    ) -> Self {
1626        Self {
1627            client,
1628            params: CopyMessagesParams {
1629                chat_id: chat_id.into(),
1630                from_chat_id: from_chat_id.into(),
1631                message_ids,
1632                message_thread_id: None,
1633                direct_messages_topic_id: None,
1634                disable_notification: None,
1635                protect_content: None,
1636                remove_caption: None,
1637            },
1638        }
1639    }
1640    /// Forum topic thread ID.
1641    pub fn message_thread_id(mut self, id: i64) -> Self {
1642        self.params.message_thread_id = Some(id);
1643        self
1644    }
1645    /// Identifier of a direct messages chat topic.
1646    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1647        self.params.direct_messages_topic_id = Some(id);
1648        self
1649    }
1650    /// Sends the messages silently — recipients receive no notification sound.
1651    pub fn disable_notification(mut self, v: bool) -> Self {
1652        self.params.disable_notification = Some(v);
1653        self
1654    }
1655    /// Protects the messages from being forwarded or saved.
1656    pub fn protect_content(mut self, v: bool) -> Self {
1657        self.params.protect_content = Some(v);
1658        self
1659    }
1660    /// Copies the messages without their captions.
1661    pub fn remove_caption(mut self, v: bool) -> Self {
1662        self.params.remove_caption = Some(v);
1663        self
1664    }
1665}
1666
1667impl_into_future!(
1668    CopyMessages,
1669    Vec<rustigram_types::message::MessageId>,
1670    "copyMessages"
1671);
1672
1673// ─── sendVenue ────────────────────────────────────────────────────────────────
1674
1675#[derive(Serialize)]
1676struct SendVenueParams {
1677    chat_id: ChatId,
1678    latitude: f64,
1679    longitude: f64,
1680    title: String,
1681    address: String,
1682    #[serde(skip_serializing_if = "Option::is_none")]
1683    message_thread_id: Option<i64>,
1684    #[serde(skip_serializing_if = "Option::is_none")]
1685    direct_messages_topic_id: Option<i64>,
1686    #[serde(skip_serializing_if = "Option::is_none")]
1687    foursquare_id: Option<String>,
1688    #[serde(skip_serializing_if = "Option::is_none")]
1689    foursquare_type: Option<String>,
1690    #[serde(skip_serializing_if = "Option::is_none")]
1691    google_place_id: Option<String>,
1692    #[serde(skip_serializing_if = "Option::is_none")]
1693    google_place_type: Option<String>,
1694    #[serde(skip_serializing_if = "Option::is_none")]
1695    disable_notification: Option<bool>,
1696    #[serde(skip_serializing_if = "Option::is_none")]
1697    protect_content: Option<bool>,
1698    #[serde(skip_serializing_if = "Option::is_none")]
1699    reply_parameters: Option<ReplyParameters>,
1700    #[serde(skip_serializing_if = "Option::is_none")]
1701    reply_markup: Option<ReplyMarkup>,
1702}
1703
1704/// Builder for the [`sendVenue`](https://core.telegram.org/bots/api#sendvenue) method.
1705pub struct SendVenue {
1706    client: BotClient,
1707    params: SendVenueParams,
1708}
1709
1710impl SendVenue {
1711    pub(crate) fn new(
1712        client: BotClient,
1713        chat_id: impl Into<ChatId>,
1714        latitude: f64,
1715        longitude: f64,
1716        title: impl Into<String>,
1717        address: impl Into<String>,
1718    ) -> Self {
1719        Self {
1720            client,
1721            params: SendVenueParams {
1722                chat_id: chat_id.into(),
1723                latitude,
1724                longitude,
1725                title: title.into(),
1726                address: address.into(),
1727                message_thread_id: None,
1728                direct_messages_topic_id: None,
1729                foursquare_id: None,
1730                foursquare_type: None,
1731                google_place_id: None,
1732                google_place_type: None,
1733                disable_notification: None,
1734                protect_content: None,
1735                reply_parameters: None,
1736                reply_markup: None,
1737            },
1738        }
1739    }
1740    /// Forum topic thread ID.
1741    pub fn message_thread_id(mut self, id: i64) -> Self {
1742        self.params.message_thread_id = Some(id);
1743        self
1744    }
1745    /// Identifier of a direct messages chat topic.
1746    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1747        self.params.direct_messages_topic_id = Some(id);
1748        self
1749    }
1750    /// Sets the Foursquare identifier of the venue.
1751    pub fn foursquare_id(mut self, id: impl Into<String>) -> Self {
1752        self.params.foursquare_id = Some(id.into());
1753        self
1754    }
1755    /// Sets the Foursquare type of the venue (e.g. `"arts_entertainment/aquarium"`).
1756    pub fn foursquare_type(mut self, t: impl Into<String>) -> Self {
1757        self.params.foursquare_type = Some(t.into());
1758        self
1759    }
1760    /// Sets the Google Places identifier of the venue.
1761    pub fn google_place_id(mut self, id: impl Into<String>) -> Self {
1762        self.params.google_place_id = Some(id.into());
1763        self
1764    }
1765    /// Sets the Google Places type of the venue.
1766    pub fn google_place_type(mut self, t: impl Into<String>) -> Self {
1767        self.params.google_place_type = Some(t.into());
1768        self
1769    }
1770    /// Sends the message silently — the recipient receives no notification sound.
1771    pub fn disable_notification(mut self, v: bool) -> Self {
1772        self.params.disable_notification = Some(v);
1773        self
1774    }
1775    /// Protects the message from being forwarded or saved.
1776    pub fn protect_content(mut self, v: bool) -> Self {
1777        self.params.protect_content = Some(v);
1778        self
1779    }
1780    /// Reply parameters for this message.
1781    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
1782        self.params.reply_parameters = Some(rp);
1783        self
1784    }
1785    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1786    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
1787        self.params.reply_markup = Some(m.into());
1788        self
1789    }
1790}
1791
1792impl_into_future!(SendVenue, Message, "sendVenue");
1793
1794// ─── sendMediaGroup ───────────────────────────────────────────────────────────
1795
1796#[derive(Serialize)]
1797struct SendMediaGroupParams {
1798    chat_id: ChatId,
1799    /// Array of `InputMedia` objects (photo, video, audio, or document).
1800    ///
1801    /// Uses `serde_json::Value` until the `InputMedia` enum is defined in
1802    /// Priority 4. Pass the result of `serde_json::to_value(&your_input_media_vec)`.
1803    media: Vec<serde_json::Value>,
1804    #[serde(skip_serializing_if = "Option::is_none")]
1805    message_thread_id: Option<i64>,
1806    #[serde(skip_serializing_if = "Option::is_none")]
1807    direct_messages_topic_id: Option<i64>,
1808    #[serde(skip_serializing_if = "Option::is_none")]
1809    business_connection_id: Option<String>,
1810    #[serde(skip_serializing_if = "Option::is_none")]
1811    disable_notification: Option<bool>,
1812    #[serde(skip_serializing_if = "Option::is_none")]
1813    protect_content: Option<bool>,
1814    #[serde(skip_serializing_if = "Option::is_none")]
1815    reply_parameters: Option<ReplyParameters>,
1816}
1817
1818/// Builder for the [`sendMediaGroup`](https://core.telegram.org/bots/api#sendmediagroup) method.
1819///
1820/// Sends a group of photos, videos, documents, or audios as an album (2–10 items).
1821///
1822/// The `media` parameter accepts `Vec<serde_json::Value>` until the `InputMedia`
1823/// enum is defined in Priority 4. Construct items with `serde_json::json!({...})`
1824/// or `serde_json::to_value(&input_media)`.
1825pub struct SendMediaGroup {
1826    client: BotClient,
1827    params: SendMediaGroupParams,
1828}
1829
1830impl SendMediaGroup {
1831    pub(crate) fn new(
1832        client: BotClient,
1833        chat_id: impl Into<ChatId>,
1834        media: Vec<serde_json::Value>,
1835    ) -> Self {
1836        Self {
1837            client,
1838            params: SendMediaGroupParams {
1839                chat_id: chat_id.into(),
1840                media,
1841                message_thread_id: None,
1842                direct_messages_topic_id: None,
1843                business_connection_id: None,
1844                disable_notification: None,
1845                protect_content: None,
1846                reply_parameters: None,
1847            },
1848        }
1849    }
1850    /// Forum topic thread ID.
1851    pub fn message_thread_id(mut self, id: i64) -> Self {
1852        self.params.message_thread_id = Some(id);
1853        self
1854    }
1855    /// Identifier of a direct messages chat topic.
1856    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1857        self.params.direct_messages_topic_id = Some(id);
1858        self
1859    }
1860    /// Business connection ID for sending on behalf of a business account.
1861    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
1862        self.params.business_connection_id = Some(id.into());
1863        self
1864    }
1865    /// Sends the messages silently — recipients receive no notification sound.
1866    pub fn disable_notification(mut self, v: bool) -> Self {
1867        self.params.disable_notification = Some(v);
1868        self
1869    }
1870    /// Protects the messages from being forwarded or saved.
1871    pub fn protect_content(mut self, v: bool) -> Self {
1872        self.params.protect_content = Some(v);
1873        self
1874    }
1875    /// Reply parameters for this message.
1876    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
1877        self.params.reply_parameters = Some(rp);
1878        self
1879    }
1880}
1881
1882impl_into_future!(SendMediaGroup, Vec<Message>, "sendMediaGroup");
1883
1884// ─── sendPaidMedia ────────────────────────────────────────────────────────────
1885
1886#[derive(Serialize)]
1887struct SendPaidMediaParams {
1888    chat_id: ChatId,
1889    star_count: u32,
1890    /// Array of `InputPaidMedia` objects (photo or video).
1891    ///
1892    /// Uses `serde_json::Value` until the `InputPaidMedia` enum is defined in
1893    /// Priority 4. Pass the result of `serde_json::to_value(&your_paid_media_vec)`.
1894    media: Vec<serde_json::Value>,
1895    #[serde(skip_serializing_if = "Option::is_none")]
1896    business_connection_id: Option<String>,
1897    #[serde(skip_serializing_if = "Option::is_none")]
1898    payload: Option<String>,
1899    #[serde(skip_serializing_if = "Option::is_none")]
1900    caption: Option<String>,
1901    #[serde(skip_serializing_if = "Option::is_none")]
1902    parse_mode: Option<ParseMode>,
1903    #[serde(skip_serializing_if = "Option::is_none")]
1904    show_caption_above_media: Option<bool>,
1905    #[serde(skip_serializing_if = "Option::is_none")]
1906    disable_notification: Option<bool>,
1907    #[serde(skip_serializing_if = "Option::is_none")]
1908    protect_content: Option<bool>,
1909    #[serde(skip_serializing_if = "Option::is_none")]
1910    reply_parameters: Option<ReplyParameters>,
1911    #[serde(skip_serializing_if = "Option::is_none")]
1912    reply_markup: Option<ReplyMarkup>,
1913}
1914
1915/// Builder for the [`sendPaidMedia`](https://core.telegram.org/bots/api#sendpaidmedia) method.
1916///
1917/// Sends paid media that users must pay Telegram Stars to view (up to 10 items).
1918///
1919/// The `media` parameter accepts `Vec<serde_json::Value>` until the `InputPaidMedia`
1920/// enum is defined in Priority 4.
1921pub struct SendPaidMedia {
1922    client: BotClient,
1923    params: SendPaidMediaParams,
1924}
1925
1926impl SendPaidMedia {
1927    pub(crate) fn new(
1928        client: BotClient,
1929        chat_id: impl Into<ChatId>,
1930        star_count: u32,
1931        media: Vec<serde_json::Value>,
1932    ) -> Self {
1933        Self {
1934            client,
1935            params: SendPaidMediaParams {
1936                chat_id: chat_id.into(),
1937                star_count,
1938                media,
1939                business_connection_id: None,
1940                payload: None,
1941                caption: None,
1942                parse_mode: None,
1943                show_caption_above_media: None,
1944                disable_notification: None,
1945                protect_content: None,
1946                reply_parameters: None,
1947                reply_markup: None,
1948            },
1949        }
1950    }
1951    /// Business connection ID for sending on behalf of a business account.
1952    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
1953        self.params.business_connection_id = Some(id.into());
1954        self
1955    }
1956    /// Bot-defined paid media payload (0–128 bytes); not shown to the user.
1957    pub fn payload(mut self, p: impl Into<String>) -> Self {
1958        self.params.payload = Some(p.into());
1959        self
1960    }
1961    /// Sets the caption (0–1024 characters).
1962    pub fn caption(mut self, c: impl Into<String>) -> Self {
1963        self.params.caption = Some(c.into());
1964        self
1965    }
1966    /// Sets the caption parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
1967    pub fn parse_mode(mut self, m: ParseMode) -> Self {
1968        self.params.parse_mode = Some(m);
1969        self
1970    }
1971    /// Shows the caption above the media instead of below it.
1972    pub fn show_caption_above_media(mut self, v: bool) -> Self {
1973        self.params.show_caption_above_media = Some(v);
1974        self
1975    }
1976    /// Sends the message silently — the recipient receives no notification sound.
1977    pub fn disable_notification(mut self, v: bool) -> Self {
1978        self.params.disable_notification = Some(v);
1979        self
1980    }
1981    /// Protects the message from being forwarded or saved.
1982    pub fn protect_content(mut self, v: bool) -> Self {
1983        self.params.protect_content = Some(v);
1984        self
1985    }
1986    /// Reply parameters for this message.
1987    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
1988        self.params.reply_parameters = Some(rp);
1989        self
1990    }
1991    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1992    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
1993        self.params.reply_markup = Some(m.into());
1994        self
1995    }
1996}
1997
1998impl_into_future!(SendPaidMedia, Message, "sendPaidMedia");
1999
2000// ─── sendGame ─────────────────────────────────────────────────────────────────
2001
2002#[derive(Serialize)]
2003struct SendGameParams {
2004    chat_id: i64,
2005    game_short_name: String,
2006    #[serde(skip_serializing_if = "Option::is_none")]
2007    business_connection_id: Option<String>,
2008    #[serde(skip_serializing_if = "Option::is_none")]
2009    message_thread_id: Option<i64>,
2010    #[serde(skip_serializing_if = "Option::is_none")]
2011    direct_messages_topic_id: Option<i64>,
2012    #[serde(skip_serializing_if = "Option::is_none")]
2013    disable_notification: Option<bool>,
2014    #[serde(skip_serializing_if = "Option::is_none")]
2015    protect_content: Option<bool>,
2016    #[serde(skip_serializing_if = "Option::is_none")]
2017    reply_parameters: Option<ReplyParameters>,
2018    #[serde(skip_serializing_if = "Option::is_none")]
2019    reply_markup: Option<rustigram_types::keyboard::InlineKeyboardMarkup>,
2020}
2021
2022/// Builder for the [`sendGame`](https://core.telegram.org/bots/api#sendgame) method.
2023///
2024/// Note: `chat_id` is an integer — games can't be sent to channel direct messages
2025/// chats or channel chats.
2026pub struct SendGame {
2027    client: BotClient,
2028    params: SendGameParams,
2029}
2030
2031impl SendGame {
2032    pub(crate) fn new(client: BotClient, chat_id: i64, game_short_name: impl Into<String>) -> Self {
2033        Self {
2034            client,
2035            params: SendGameParams {
2036                chat_id,
2037                game_short_name: game_short_name.into(),
2038                business_connection_id: None,
2039                message_thread_id: None,
2040                direct_messages_topic_id: None,
2041                disable_notification: None,
2042                protect_content: None,
2043                reply_parameters: None,
2044                reply_markup: None,
2045            },
2046        }
2047    }
2048    /// Business connection ID for sending on behalf of a business account.
2049    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
2050        self.params.business_connection_id = Some(id.into());
2051        self
2052    }
2053    /// Forum topic thread ID.
2054    pub fn message_thread_id(mut self, id: i64) -> Self {
2055        self.params.message_thread_id = Some(id);
2056        self
2057    }
2058    /// Identifier of a direct messages chat topic.
2059    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2060        self.params.direct_messages_topic_id = Some(id);
2061        self
2062    }
2063    /// Sends the message silently — the recipient receives no notification sound.
2064    pub fn disable_notification(mut self, v: bool) -> Self {
2065        self.params.disable_notification = Some(v);
2066        self
2067    }
2068    /// Protects the message from being forwarded or saved.
2069    pub fn protect_content(mut self, v: bool) -> Self {
2070        self.params.protect_content = Some(v);
2071        self
2072    }
2073    /// Reply parameters for this message.
2074    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2075        self.params.reply_parameters = Some(rp);
2076        self
2077    }
2078    /// Attaches an inline keyboard. The first button must launch the game.
2079    pub fn reply_markup(mut self, m: rustigram_types::keyboard::InlineKeyboardMarkup) -> Self {
2080        self.params.reply_markup = Some(m);
2081        self
2082    }
2083}
2084
2085impl_into_future!(SendGame, Message, "sendGame");
2086
2087// ─── sendChecklist ────────────────────────────────────────────────────────────
2088
2089#[derive(Serialize)]
2090struct SendChecklistParams {
2091    business_connection_id: String,
2092    chat_id: i64,
2093    checklist: rustigram_types::checklist::InputChecklist,
2094    #[serde(skip_serializing_if = "Option::is_none")]
2095    direct_messages_topic_id: Option<i64>,
2096    #[serde(skip_serializing_if = "Option::is_none")]
2097    disable_notification: Option<bool>,
2098    #[serde(skip_serializing_if = "Option::is_none")]
2099    protect_content: Option<bool>,
2100    #[serde(skip_serializing_if = "Option::is_none")]
2101    message_effect_id: Option<String>,
2102    #[serde(skip_serializing_if = "Option::is_none")]
2103    reply_parameters: Option<ReplyParameters>,
2104    #[serde(skip_serializing_if = "Option::is_none")]
2105    reply_markup: Option<rustigram_types::keyboard::InlineKeyboardMarkup>,
2106    #[serde(skip_serializing_if = "Option::is_none")]
2107    suggested_post_parameters: Option<SuggestedPostParameters>,
2108}
2109
2110/// Builder for the [`sendChecklist`](https://core.telegram.org/bots/api#sendchecklist) method.
2111///
2112/// Business bots only — sends a checklist on behalf of a connected business account.
2113/// Requires the `can_reply` business bot right.
2114pub struct SendChecklist {
2115    client: BotClient,
2116    params: SendChecklistParams,
2117}
2118
2119impl SendChecklist {
2120    pub(crate) fn new(
2121        client: BotClient,
2122        business_connection_id: impl Into<String>,
2123        chat_id: i64,
2124        checklist: rustigram_types::checklist::InputChecklist,
2125    ) -> Self {
2126        Self {
2127            client,
2128            params: SendChecklistParams {
2129                business_connection_id: business_connection_id.into(),
2130                chat_id,
2131                checklist,
2132                direct_messages_topic_id: None,
2133                disable_notification: None,
2134                protect_content: None,
2135                message_effect_id: None,
2136                reply_parameters: None,
2137                reply_markup: None,
2138                suggested_post_parameters: None,
2139            },
2140        }
2141    }
2142    /// Identifier of a direct messages chat topic.
2143    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2144        self.params.direct_messages_topic_id = Some(id);
2145        self
2146    }
2147    /// Sends the message silently — the recipient receives no notification sound.
2148    pub fn disable_notification(mut self, v: bool) -> Self {
2149        self.params.disable_notification = Some(v);
2150        self
2151    }
2152    /// Protects the message from being forwarded or saved.
2153    pub fn protect_content(mut self, v: bool) -> Self {
2154        self.params.protect_content = Some(v);
2155        self
2156    }
2157    /// Unique identifier of the message effect to add to the message.
2158    pub fn message_effect_id(mut self, id: impl Into<String>) -> Self {
2159        self.params.message_effect_id = Some(id.into());
2160        self
2161    }
2162    /// Reply parameters for this message.
2163    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2164        self.params.reply_parameters = Some(rp);
2165        self
2166    }
2167    /// Attaches an inline keyboard to the message.
2168    pub fn reply_markup(mut self, m: rustigram_types::keyboard::InlineKeyboardMarkup) -> Self {
2169        self.params.reply_markup = Some(m);
2170        self
2171    }
2172    /// Suggested post parameters for channel direct messages chats.
2173    pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self {
2174        self.params.suggested_post_parameters = Some(params);
2175        self
2176    }
2177}
2178
2179impl_into_future!(SendChecklist, Message, "sendChecklist");