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