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, InputMedia, InputPaidMedia};
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    #[serde(skip_serializing_if = "Option::is_none")]
242    message_effect_id: Option<String>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    suggested_post_parameters: Option<SuggestedPostParameters>,
245}
246
247/// Builder for the [`forwardMessage`](https://core.telegram.org/bots/api#forwardmessage) method.
248pub struct ForwardMessage {
249    client: BotClient,
250    params: ForwardMessageParams,
251}
252
253impl ForwardMessage {
254    pub(crate) fn new(
255        client: BotClient,
256        chat_id: impl Into<ChatId>,
257        from_chat_id: impl Into<ChatId>,
258        message_id: i64,
259    ) -> Self {
260        Self {
261            client,
262            params: ForwardMessageParams {
263                chat_id: chat_id.into(),
264                from_chat_id: from_chat_id.into(),
265                message_id,
266                message_thread_id: None,
267                direct_messages_topic_id: None,
268                video_start_timestamp: None,
269                disable_notification: None,
270                protect_content: None,
271                message_effect_id: None,
272                suggested_post_parameters: None,
273            },
274        }
275    }
276    /// Forum topic thread ID.
277    pub fn message_thread_id(mut self, id: i64) -> Self {
278        self.params.message_thread_id = Some(id);
279        self
280    }
281    /// Identifier of a direct messages chat topic.
282    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
283        self.params.direct_messages_topic_id = Some(id);
284        self
285    }
286    /// New start timestamp for a forwarded video.
287    pub fn video_start_timestamp(mut self, ts: i64) -> Self {
288        self.params.video_start_timestamp = Some(ts);
289        self
290    }
291    /// Sends the message silently — the recipient receives no notification sound.
292    pub fn disable_notification(mut self, v: bool) -> Self {
293        self.params.disable_notification = Some(v);
294        self
295    }
296    /// Protects the message from being forwarded or saved.
297    pub fn protect_content(mut self, v: bool) -> Self {
298        self.params.protect_content = Some(v);
299        self
300    }
301    /// Attaches a message effect (animated emoji reaction) to the message.
302    pub fn message_effect_id(mut self, v: impl Into<String>) -> Self {
303        self.params.message_effect_id = Some(v.into());
304        self
305    }
306    /// Suggested post parameters for channel direct messages chats.
307    pub fn suggested_post_parameters(mut self, v: SuggestedPostParameters) -> Self {
308        self.params.suggested_post_parameters = Some(v);
309        self
310    }
311}
312
313impl_into_future!(ForwardMessage, Message, "forwardMessage");
314
315// ─── copyMessage ──────────────────────────────────────────────────────────────
316
317#[derive(Serialize)]
318struct CopyMessageParams {
319    chat_id: ChatId,
320    from_chat_id: ChatId,
321    message_id: i64,
322    #[serde(skip_serializing_if = "Option::is_none")]
323    message_thread_id: Option<i64>,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    direct_messages_topic_id: Option<i64>,
326    #[serde(skip_serializing_if = "Option::is_none")]
327    video_start_timestamp: Option<i64>,
328    #[serde(skip_serializing_if = "Option::is_none")]
329    caption: Option<String>,
330    #[serde(skip_serializing_if = "Option::is_none")]
331    parse_mode: Option<ParseMode>,
332    #[serde(skip_serializing_if = "Option::is_none")]
333    caption_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
334    #[serde(skip_serializing_if = "Option::is_none")]
335    show_caption_above_media: Option<bool>,
336    #[serde(skip_serializing_if = "Option::is_none")]
337    disable_notification: Option<bool>,
338    #[serde(skip_serializing_if = "Option::is_none")]
339    protect_content: Option<bool>,
340    #[serde(skip_serializing_if = "Option::is_none")]
341    reply_parameters: Option<ReplyParameters>,
342    #[serde(skip_serializing_if = "Option::is_none")]
343    reply_markup: Option<ReplyMarkup>,
344    #[serde(skip_serializing_if = "Option::is_none")]
345    allow_paid_broadcast: Option<bool>,
346    #[serde(skip_serializing_if = "Option::is_none")]
347    message_effect_id: Option<String>,
348    #[serde(skip_serializing_if = "Option::is_none")]
349    suggested_post_parameters: Option<SuggestedPostParameters>,
350}
351
352/// Builder for the [`copyMessage`](https://core.telegram.org/bots/api#copymessage) method.
353pub struct CopyMessage {
354    client: BotClient,
355    params: CopyMessageParams,
356}
357
358impl CopyMessage {
359    pub(crate) fn new(
360        client: BotClient,
361        chat_id: impl Into<ChatId>,
362        from_chat_id: impl Into<ChatId>,
363        message_id: i64,
364    ) -> Self {
365        Self {
366            client,
367            params: CopyMessageParams {
368                chat_id: chat_id.into(),
369                from_chat_id: from_chat_id.into(),
370                message_id,
371                message_thread_id: None,
372                direct_messages_topic_id: None,
373                video_start_timestamp: None,
374                caption: None,
375                parse_mode: None,
376                caption_entities: None,
377                show_caption_above_media: None,
378                disable_notification: None,
379                protect_content: None,
380                reply_parameters: None,
381                reply_markup: None,
382                allow_paid_broadcast: None,
383                message_effect_id: None,
384                suggested_post_parameters: None,
385            },
386        }
387    }
388    /// Forum topic thread ID.
389    pub fn message_thread_id(mut self, id: i64) -> Self {
390        self.params.message_thread_id = Some(id);
391        self
392    }
393    /// Identifier of a direct messages chat topic.
394    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
395        self.params.direct_messages_topic_id = Some(id);
396        self
397    }
398    /// New start timestamp for a copied video.
399    pub fn video_start_timestamp(mut self, ts: i64) -> Self {
400        self.params.video_start_timestamp = Some(ts);
401        self
402    }
403    /// Sets the caption (0–1024 characters) for media messages.
404    pub fn caption(mut self, c: impl Into<String>) -> Self {
405        self.params.caption = Some(c.into());
406        self
407    }
408    /// Sets the text parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
409    pub fn parse_mode(mut self, m: ParseMode) -> Self {
410        self.params.parse_mode = Some(m);
411        self
412    }
413    /// Sends the message silently — the recipient receives no notification sound.
414    pub fn disable_notification(mut self, v: bool) -> Self {
415        self.params.disable_notification = Some(v);
416        self
417    }
418    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
419    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
420        self.params.reply_markup = Some(m.into());
421        self
422    }
423    /// Allows sending to large audiences at the cost of Telegram Stars.
424    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
425        self.params.allow_paid_broadcast = Some(v);
426        self
427    }
428    /// Attaches a message effect (animated emoji reaction) to the message.
429    pub fn message_effect_id(mut self, v: impl Into<String>) -> Self {
430        self.params.message_effect_id = Some(v.into());
431        self
432    }
433    /// Suggested post parameters for channel direct messages chats.
434    pub fn suggested_post_parameters(mut self, v: SuggestedPostParameters) -> Self {
435        self.params.suggested_post_parameters = Some(v);
436        self
437    }
438    /// Special entities in the caption, in place of `parse_mode`.
439    pub fn caption_entities(
440        mut self,
441        entities: Vec<rustigram_types::message::MessageEntity>,
442    ) -> Self {
443        self.params.caption_entities = Some(entities);
444        self
445    }
446    /// Shows the caption above the media instead of below it.
447    pub fn show_caption_above_media(mut self, v: bool) -> Self {
448        self.params.show_caption_above_media = Some(v);
449        self
450    }
451    /// Protects the message from being forwarded or saved.
452    pub fn protect_content(mut self, v: bool) -> Self {
453        self.params.protect_content = Some(v);
454        self
455    }
456    /// Reply parameters for this message.
457    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
458        self.params.reply_parameters = Some(rp);
459        self
460    }
461}
462
463impl_into_future!(
464    CopyMessage,
465    rustigram_types::message::MessageId,
466    "copyMessage"
467);
468
469// ─── sendChatAction ───────────────────────────────────────────────────────────
470
471#[derive(Serialize)]
472struct SendChatActionParams {
473    chat_id: ChatId,
474    action: ChatAction,
475    #[serde(skip_serializing_if = "Option::is_none")]
476    business_connection_id: Option<String>,
477    #[serde(skip_serializing_if = "Option::is_none")]
478    message_thread_id: Option<i64>,
479}
480
481#[derive(Serialize, Clone, Copy)]
482/// The chat action to display while the bot is preparing a response.
483#[serde(rename_all = "snake_case")]
484pub enum ChatAction {
485    /// Indicates the bot is composing a message.
486    Typing,
487    /// Indicates the bot is uploading a photo.
488    UploadPhoto,
489    /// Indicates the bot is recording a video.
490    RecordVideo,
491    /// Indicates the bot is uploading a video.
492    UploadVideo,
493    /// Indicates the bot is recording a voice note.
494    RecordVoice,
495    /// Indicates the bot is uploading a voice note.
496    UploadVoice,
497    /// Indicates the bot is uploading a document.
498    UploadDocument,
499    /// Indicates the bot is choosing a sticker.
500    ChooseSticker,
501    /// Indicates the bot is finding a location.
502    FindLocation,
503    /// Indicates the bot is recording a video note.
504    RecordVideoNote,
505    /// Indicates the bot is uploading a video note.
506    UploadVideoNote,
507}
508
509/// Builder for the [`sendChatAction`](https://core.telegram.org/bots/api#sendchataction) method.
510pub struct SendChatAction {
511    client: BotClient,
512    params: SendChatActionParams,
513}
514
515impl SendChatAction {
516    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, action: ChatAction) -> Self {
517        Self {
518            client,
519            params: SendChatActionParams {
520                chat_id: chat_id.into(),
521                action,
522                business_connection_id: None,
523                message_thread_id: None,
524            },
525        }
526    }
527    /// Business connection ID for sending on behalf of a business account.
528    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
529        self.params.business_connection_id = Some(id.into());
530        self
531    }
532    /// Forum topic thread ID.
533    pub fn message_thread_id(mut self, id: i64) -> Self {
534        self.params.message_thread_id = Some(id);
535        self
536    }
537}
538
539impl_into_future!(SendChatAction, bool, "sendChatAction");
540
541// ─── sendDice ─────────────────────────────────────────────────────────────────
542
543#[derive(Serialize)]
544struct SendDiceParams {
545    chat_id: ChatId,
546    #[serde(skip_serializing_if = "Option::is_none")]
547    emoji: Option<String>,
548    #[serde(skip_serializing_if = "Option::is_none")]
549    message_thread_id: Option<i64>,
550    #[serde(skip_serializing_if = "Option::is_none")]
551    direct_messages_topic_id: Option<i64>,
552    #[serde(skip_serializing_if = "Option::is_none")]
553    disable_notification: Option<bool>,
554    #[serde(skip_serializing_if = "Option::is_none")]
555    protect_content: Option<bool>,
556    #[serde(skip_serializing_if = "Option::is_none")]
557    reply_parameters: Option<ReplyParameters>,
558    #[serde(skip_serializing_if = "Option::is_none")]
559    reply_markup: Option<ReplyMarkup>,
560    #[serde(skip_serializing_if = "Option::is_none")]
561    business_connection_id: Option<String>,
562    #[serde(skip_serializing_if = "Option::is_none")]
563    allow_paid_broadcast: Option<bool>,
564    #[serde(skip_serializing_if = "Option::is_none")]
565    message_effect_id: Option<String>,
566    #[serde(skip_serializing_if = "Option::is_none")]
567    suggested_post_parameters: Option<SuggestedPostParameters>,
568}
569
570/// Builder for the [`sendDice`](https://core.telegram.org/bots/api#senddice) method.
571pub struct SendDice {
572    client: BotClient,
573    params: SendDiceParams,
574}
575
576impl SendDice {
577    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>) -> Self {
578        Self {
579            client,
580            params: SendDiceParams {
581                chat_id: chat_id.into(),
582                emoji: None,
583                message_thread_id: None,
584                direct_messages_topic_id: None,
585                disable_notification: None,
586                protect_content: None,
587                reply_parameters: None,
588                reply_markup: None,
589                business_connection_id: None,
590                allow_paid_broadcast: None,
591                message_effect_id: None,
592                suggested_post_parameters: None,
593            },
594        }
595    }
596    /// The dice/emoji to animate. One of 🎲 🎯 🏀 ⚽ 🎳 🎰.
597    pub fn emoji(mut self, e: impl Into<String>) -> Self {
598        self.params.emoji = Some(e.into());
599        self
600    }
601    /// Forum topic thread ID.
602    pub fn message_thread_id(mut self, id: i64) -> Self {
603        self.params.message_thread_id = Some(id);
604        self
605    }
606    /// Identifier of a direct messages chat topic.
607    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
608        self.params.direct_messages_topic_id = Some(id);
609        self
610    }
611    /// Sends the message silently — the recipient receives no notification sound.
612    pub fn disable_notification(mut self, v: bool) -> Self {
613        self.params.disable_notification = Some(v);
614        self
615    }
616    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
617    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
618        self.params.reply_markup = Some(m.into());
619        self
620    }
621    /// Business connection ID for acting on behalf of a business account.
622    pub fn business_connection_id(mut self, v: impl Into<String>) -> Self {
623        self.params.business_connection_id = Some(v.into());
624        self
625    }
626    /// Allows sending to large audiences at the cost of Telegram Stars.
627    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
628        self.params.allow_paid_broadcast = Some(v);
629        self
630    }
631    /// Attaches a message effect (animated emoji reaction) to the message.
632    pub fn message_effect_id(mut self, v: impl Into<String>) -> Self {
633        self.params.message_effect_id = Some(v.into());
634        self
635    }
636    /// Suggested post parameters for channel direct messages chats.
637    pub fn suggested_post_parameters(mut self, v: SuggestedPostParameters) -> Self {
638        self.params.suggested_post_parameters = Some(v);
639        self
640    }
641    /// Protects the message from being forwarded or saved.
642    pub fn protect_content(mut self, v: bool) -> Self {
643        self.params.protect_content = Some(v);
644        self
645    }
646    /// Reply parameters for this message.
647    pub fn reply_parameters(mut self, v: ReplyParameters) -> Self {
648        self.params.reply_parameters = Some(v);
649        self
650    }
651}
652
653impl_into_future!(SendDice, Message, "sendDice");
654
655// ─── sendLocation ─────────────────────────────────────────────────────────────
656
657#[derive(Serialize)]
658struct SendLocationParams {
659    chat_id: ChatId,
660    latitude: f64,
661    longitude: f64,
662    #[serde(skip_serializing_if = "Option::is_none")]
663    message_thread_id: Option<i64>,
664    #[serde(skip_serializing_if = "Option::is_none")]
665    direct_messages_topic_id: Option<i64>,
666    #[serde(skip_serializing_if = "Option::is_none")]
667    horizontal_accuracy: Option<f64>,
668    #[serde(skip_serializing_if = "Option::is_none")]
669    live_period: Option<u32>,
670    #[serde(skip_serializing_if = "Option::is_none")]
671    heading: Option<u16>,
672    #[serde(skip_serializing_if = "Option::is_none")]
673    proximity_alert_radius: Option<u32>,
674    #[serde(skip_serializing_if = "Option::is_none")]
675    disable_notification: Option<bool>,
676    #[serde(skip_serializing_if = "Option::is_none")]
677    protect_content: Option<bool>,
678    #[serde(skip_serializing_if = "Option::is_none")]
679    reply_parameters: Option<ReplyParameters>,
680    #[serde(skip_serializing_if = "Option::is_none")]
681    reply_markup: Option<ReplyMarkup>,
682    #[serde(skip_serializing_if = "Option::is_none")]
683    receiver_user_id: Option<i64>,
684    #[serde(skip_serializing_if = "Option::is_none")]
685    callback_query_id: Option<String>,
686    #[serde(skip_serializing_if = "Option::is_none")]
687    business_connection_id: Option<String>,
688    #[serde(skip_serializing_if = "Option::is_none")]
689    allow_paid_broadcast: Option<bool>,
690    #[serde(skip_serializing_if = "Option::is_none")]
691    message_effect_id: Option<String>,
692    #[serde(skip_serializing_if = "Option::is_none")]
693    suggested_post_parameters: Option<SuggestedPostParameters>,
694}
695
696/// Builder for the [`sendLocation`](https://core.telegram.org/bots/api#sendlocation) method.
697pub struct SendLocation {
698    client: BotClient,
699    params: SendLocationParams,
700}
701
702impl SendLocation {
703    pub(crate) fn new(
704        client: BotClient,
705        chat_id: impl Into<ChatId>,
706        latitude: f64,
707        longitude: f64,
708    ) -> Self {
709        Self {
710            client,
711            params: SendLocationParams {
712                chat_id: chat_id.into(),
713                latitude,
714                longitude,
715                message_thread_id: None,
716                direct_messages_topic_id: None,
717                horizontal_accuracy: None,
718                live_period: None,
719                heading: None,
720                proximity_alert_radius: None,
721                disable_notification: None,
722                protect_content: None,
723                reply_parameters: None,
724                reply_markup: None,
725                receiver_user_id: None,
726                callback_query_id: None,
727                business_connection_id: None,
728                allow_paid_broadcast: None,
729                message_effect_id: None,
730                suggested_post_parameters: 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 radius of uncertainty for the location, in metres (0–1500).
745    pub fn horizontal_accuracy(mut self, v: f64) -> Self {
746        self.params.horizontal_accuracy = Some(v);
747        self
748    }
749    /// Sets how long the location stays live, in seconds (60–86400), or
750    /// `0x7FFFFFFF` for indefinitely editable live locations. Must be `0`
751    /// for ephemeral messages.
752    pub fn live_period(mut self, v: u32) -> Self {
753        self.params.live_period = Some(v);
754        self
755    }
756    /// Sets the direction of movement in degrees (1–360).
757    pub fn heading(mut self, v: u16) -> Self {
758        self.params.heading = Some(v);
759        self
760    }
761    /// Sets the maximum distance in metres for proximity alerts.
762    pub fn proximity_alert_radius(mut self, v: u32) -> Self {
763        self.params.proximity_alert_radius = Some(v);
764        self
765    }
766    /// Sends the message silently — the recipient receives no notification sound.
767    pub fn disable_notification(mut self, v: bool) -> Self {
768        self.params.disable_notification = Some(v);
769        self
770    }
771    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
772    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
773        self.params.reply_markup = Some(m.into());
774        self
775    }
776    /// For outgoing ephemeral messages — the user who will receive the message.
777    pub fn receiver_user_id(mut self, id: i64) -> Self {
778        self.params.receiver_user_id = Some(id);
779        self
780    }
781    /// For outgoing ephemeral messages — the callback query that triggered it, if any.
782    pub fn callback_query_id(mut self, id: impl Into<String>) -> Self {
783        self.params.callback_query_id = Some(id.into());
784        self
785    }
786    /// Business connection ID for acting on behalf of a business account.
787    pub fn business_connection_id(mut self, v: impl Into<String>) -> Self {
788        self.params.business_connection_id = Some(v.into());
789        self
790    }
791    /// Allows sending to large audiences at the cost of Telegram Stars.
792    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
793        self.params.allow_paid_broadcast = Some(v);
794        self
795    }
796    /// Attaches a message effect (animated emoji reaction) to the message.
797    pub fn message_effect_id(mut self, v: impl Into<String>) -> Self {
798        self.params.message_effect_id = Some(v.into());
799        self
800    }
801    /// Suggested post parameters for channel direct messages chats.
802    pub fn suggested_post_parameters(mut self, v: SuggestedPostParameters) -> Self {
803        self.params.suggested_post_parameters = Some(v);
804        self
805    }
806    /// Protects the message from being forwarded or saved.
807    pub fn protect_content(mut self, v: bool) -> Self {
808        self.params.protect_content = Some(v);
809        self
810    }
811    /// Reply parameters for this message.
812    pub fn reply_parameters(mut self, v: ReplyParameters) -> Self {
813        self.params.reply_parameters = Some(v);
814        self
815    }
816}
817
818impl_into_future!(SendLocation, Message, "sendLocation");
819
820// ─── sendContact ──────────────────────────────────────────────────────────────
821
822#[derive(Serialize)]
823struct SendContactParams {
824    chat_id: ChatId,
825    phone_number: String,
826    first_name: String,
827    #[serde(skip_serializing_if = "Option::is_none")]
828    last_name: Option<String>,
829    #[serde(skip_serializing_if = "Option::is_none")]
830    vcard: Option<String>,
831    #[serde(skip_serializing_if = "Option::is_none")]
832    message_thread_id: Option<i64>,
833    #[serde(skip_serializing_if = "Option::is_none")]
834    direct_messages_topic_id: Option<i64>,
835    #[serde(skip_serializing_if = "Option::is_none")]
836    disable_notification: Option<bool>,
837    #[serde(skip_serializing_if = "Option::is_none")]
838    protect_content: Option<bool>,
839    #[serde(skip_serializing_if = "Option::is_none")]
840    reply_parameters: Option<ReplyParameters>,
841    #[serde(skip_serializing_if = "Option::is_none")]
842    reply_markup: Option<ReplyMarkup>,
843    #[serde(skip_serializing_if = "Option::is_none")]
844    receiver_user_id: Option<i64>,
845    #[serde(skip_serializing_if = "Option::is_none")]
846    callback_query_id: Option<String>,
847    #[serde(skip_serializing_if = "Option::is_none")]
848    business_connection_id: Option<String>,
849    #[serde(skip_serializing_if = "Option::is_none")]
850    allow_paid_broadcast: Option<bool>,
851    #[serde(skip_serializing_if = "Option::is_none")]
852    message_effect_id: Option<String>,
853    #[serde(skip_serializing_if = "Option::is_none")]
854    suggested_post_parameters: Option<SuggestedPostParameters>,
855}
856
857/// Builder for the [`sendContact`](https://core.telegram.org/bots/api#sendcontact) method.
858pub struct SendContact {
859    client: BotClient,
860    params: SendContactParams,
861}
862
863impl SendContact {
864    pub(crate) fn new(
865        client: BotClient,
866        chat_id: impl Into<ChatId>,
867        phone_number: impl Into<String>,
868        first_name: impl Into<String>,
869    ) -> Self {
870        Self {
871            client,
872            params: SendContactParams {
873                chat_id: chat_id.into(),
874                phone_number: phone_number.into(),
875                first_name: first_name.into(),
876                last_name: None,
877                vcard: None,
878                message_thread_id: None,
879                direct_messages_topic_id: None,
880                disable_notification: None,
881                protect_content: None,
882                reply_parameters: None,
883                reply_markup: None,
884                receiver_user_id: None,
885                callback_query_id: None,
886                business_connection_id: None,
887                allow_paid_broadcast: None,
888                message_effect_id: None,
889                suggested_post_parameters: None,
890            },
891        }
892    }
893    /// Forum topic thread ID.
894    pub fn message_thread_id(mut self, id: i64) -> Self {
895        self.params.message_thread_id = Some(id);
896        self
897    }
898    /// Identifier of a direct messages chat topic.
899    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
900        self.params.direct_messages_topic_id = Some(id);
901        self
902    }
903    /// Sets the last name of the contact.
904    pub fn last_name(mut self, v: impl Into<String>) -> Self {
905        self.params.last_name = Some(v.into());
906        self
907    }
908    /// Sets the vCard data of the contact.
909    pub fn vcard(mut self, v: impl Into<String>) -> Self {
910        self.params.vcard = Some(v.into());
911        self
912    }
913    /// Sends the message silently — the recipient receives no notification sound.
914    pub fn disable_notification(mut self, v: bool) -> Self {
915        self.params.disable_notification = Some(v);
916        self
917    }
918    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
919    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
920        self.params.reply_markup = Some(m.into());
921        self
922    }
923    /// For outgoing ephemeral messages — the user who will receive the message.
924    pub fn receiver_user_id(mut self, id: i64) -> Self {
925        self.params.receiver_user_id = Some(id);
926        self
927    }
928    /// For outgoing ephemeral messages — the callback query that triggered it, if any.
929    pub fn callback_query_id(mut self, id: impl Into<String>) -> Self {
930        self.params.callback_query_id = Some(id.into());
931        self
932    }
933    /// Business connection ID for acting on behalf of a business account.
934    pub fn business_connection_id(mut self, v: impl Into<String>) -> Self {
935        self.params.business_connection_id = Some(v.into());
936        self
937    }
938    /// Allows sending to large audiences at the cost of Telegram Stars.
939    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
940        self.params.allow_paid_broadcast = Some(v);
941        self
942    }
943    /// Attaches a message effect (animated emoji reaction) to the message.
944    pub fn message_effect_id(mut self, v: impl Into<String>) -> Self {
945        self.params.message_effect_id = Some(v.into());
946        self
947    }
948    /// Suggested post parameters for channel direct messages chats.
949    pub fn suggested_post_parameters(mut self, v: SuggestedPostParameters) -> Self {
950        self.params.suggested_post_parameters = Some(v);
951        self
952    }
953    /// Protects the message from being forwarded or saved.
954    pub fn protect_content(mut self, v: bool) -> Self {
955        self.params.protect_content = Some(v);
956        self
957    }
958    /// Reply parameters for this message.
959    pub fn reply_parameters(mut self, v: ReplyParameters) -> Self {
960        self.params.reply_parameters = Some(v);
961        self
962    }
963}
964
965impl_into_future!(SendContact, Message, "sendContact");
966
967// ─── sendPoll ─────────────────────────────────────────────────────────────────
968
969#[derive(Serialize)]
970struct SendPollParams {
971    chat_id: ChatId,
972    question: String,
973    options: Vec<InputPollOption>,
974    #[serde(skip_serializing_if = "Option::is_none")]
975    question_parse_mode: Option<ParseMode>,
976    #[serde(skip_serializing_if = "Option::is_none")]
977    question_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
978    #[serde(skip_serializing_if = "Option::is_none")]
979    message_thread_id: Option<i64>,
980    #[serde(skip_serializing_if = "Option::is_none")]
981    direct_messages_topic_id: Option<i64>,
982    #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
983    poll_type: Option<rustigram_types::poll::PollType>,
984    #[serde(skip_serializing_if = "Option::is_none")]
985    is_anonymous: Option<bool>,
986    #[serde(skip_serializing_if = "Option::is_none")]
987    allows_multiple_answers: Option<bool>,
988    #[serde(skip_serializing_if = "Option::is_none")]
989    allows_revoting: Option<bool>,
990    #[serde(skip_serializing_if = "Option::is_none")]
991    correct_option_ids: Option<Vec<u8>>,
992    #[serde(skip_serializing_if = "Option::is_none")]
993    explanation: Option<String>,
994    #[serde(skip_serializing_if = "Option::is_none")]
995    explanation_parse_mode: Option<ParseMode>,
996    #[serde(skip_serializing_if = "Option::is_none")]
997    explanation_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
998    #[serde(skip_serializing_if = "Option::is_none")]
999    open_period: Option<u32>,
1000    #[serde(skip_serializing_if = "Option::is_none")]
1001    close_date: Option<i64>,
1002    #[serde(skip_serializing_if = "Option::is_none")]
1003    is_closed: Option<bool>,
1004    #[serde(skip_serializing_if = "Option::is_none")]
1005    shuffle_options: Option<bool>,
1006    #[serde(skip_serializing_if = "Option::is_none")]
1007    allow_adding_options: Option<bool>,
1008    #[serde(skip_serializing_if = "Option::is_none")]
1009    hide_results_until_closes: Option<bool>,
1010    #[serde(skip_serializing_if = "Option::is_none")]
1011    description: Option<String>,
1012    #[serde(skip_serializing_if = "Option::is_none")]
1013    description_parse_mode: Option<ParseMode>,
1014    #[serde(skip_serializing_if = "Option::is_none")]
1015    description_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
1016    #[serde(skip_serializing_if = "Option::is_none")]
1017    disable_notification: Option<bool>,
1018    #[serde(skip_serializing_if = "Option::is_none")]
1019    protect_content: Option<bool>,
1020    #[serde(skip_serializing_if = "Option::is_none")]
1021    reply_parameters: Option<ReplyParameters>,
1022    #[serde(skip_serializing_if = "Option::is_none")]
1023    reply_markup: Option<ReplyMarkup>,
1024    #[serde(skip_serializing_if = "Option::is_none")]
1025    suggested_post_parameters: Option<SuggestedPostParameters>,
1026    #[serde(skip_serializing_if = "Option::is_none")]
1027    members_only: Option<bool>,
1028    #[serde(skip_serializing_if = "Option::is_none")]
1029    country_codes: Option<Vec<String>>,
1030    #[serde(skip_serializing_if = "Option::is_none")]
1031    media: Option<rustigram_types::poll::InputPollMedia>,
1032    #[serde(skip_serializing_if = "Option::is_none")]
1033    explanation_media: Option<rustigram_types::poll::InputPollMedia>,
1034    #[serde(skip_serializing_if = "Option::is_none")]
1035    business_connection_id: Option<String>,
1036    #[serde(skip_serializing_if = "Option::is_none")]
1037    allow_paid_broadcast: Option<bool>,
1038    #[serde(skip_serializing_if = "Option::is_none")]
1039    message_effect_id: Option<String>,
1040}
1041
1042/// Builder for the [`sendPoll`](https://core.telegram.org/bots/api#sendpoll) method.
1043pub struct SendPoll {
1044    client: BotClient,
1045    params: SendPollParams,
1046}
1047
1048impl SendPoll {
1049    pub(crate) fn new(
1050        client: BotClient,
1051        chat_id: impl Into<ChatId>,
1052        question: impl Into<String>,
1053        options: Vec<InputPollOption>,
1054    ) -> Self {
1055        Self {
1056            client,
1057            params: SendPollParams {
1058                chat_id: chat_id.into(),
1059                question: question.into(),
1060                options,
1061                question_parse_mode: None,
1062                question_entities: None,
1063                message_thread_id: None,
1064                direct_messages_topic_id: None,
1065                poll_type: None,
1066                is_anonymous: None,
1067                allows_multiple_answers: None,
1068                allows_revoting: None,
1069                correct_option_ids: None,
1070                explanation: None,
1071                explanation_parse_mode: None,
1072                explanation_entities: None,
1073                open_period: None,
1074                close_date: None,
1075                is_closed: None,
1076                shuffle_options: None,
1077                allow_adding_options: None,
1078                hide_results_until_closes: None,
1079                description: None,
1080                description_parse_mode: None,
1081                description_entities: None,
1082                disable_notification: None,
1083                protect_content: None,
1084                reply_parameters: None,
1085                reply_markup: None,
1086                suggested_post_parameters: None,
1087                members_only: None,
1088                country_codes: None,
1089                media: None,
1090                explanation_media: None,
1091                business_connection_id: None,
1092                allow_paid_broadcast: None,
1093                message_effect_id: None,
1094            },
1095        }
1096    }
1097    /// Forum topic thread ID.
1098    pub fn message_thread_id(mut self, id: i64) -> Self {
1099        self.params.message_thread_id = Some(id);
1100        self
1101    }
1102    /// Identifier of a direct messages chat topic.
1103    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1104        self.params.direct_messages_topic_id = Some(id);
1105        self
1106    }
1107    /// Sets whether the poll is anonymous.
1108    pub fn is_anonymous(mut self, v: bool) -> Self {
1109        self.params.is_anonymous = Some(v);
1110        self
1111    }
1112    /// Allows voters to select multiple answers.
1113    pub fn allows_multiple_answers(mut self, v: bool) -> Self {
1114        self.params.allows_multiple_answers = Some(v);
1115        self
1116    }
1117    /// Allows voters to change their vote.
1118    pub fn allows_revoting(mut self, v: bool) -> Self {
1119        self.params.allows_revoting = Some(v);
1120        self
1121    }
1122    /// Converts the poll to a quiz with the given correct option indices.
1123    pub fn quiz(mut self, ids: Vec<u8>) -> Self {
1124        self.params.poll_type = Some(rustigram_types::poll::PollType::Quiz);
1125        self.params.correct_option_ids = Some(ids);
1126        self
1127    }
1128    /// Convenience method for a quiz with a single correct option.
1129    pub fn quiz_single(self, id: u8) -> Self {
1130        self.quiz(vec![id])
1131    }
1132    /// Sets the explanation text shown after a quiz answer.
1133    pub fn explanation(mut self, text: impl Into<String>) -> Self {
1134        self.params.explanation = Some(text.into());
1135        self
1136    }
1137    /// Sets the parse mode for the explanation.
1138    pub fn explanation_parse_mode(mut self, mode: ParseMode) -> Self {
1139        self.params.explanation_parse_mode = Some(mode);
1140        self
1141    }
1142    /// Sets entities for the explanation.
1143    pub fn explanation_entities(mut self, e: Vec<rustigram_types::message::MessageEntity>) -> Self {
1144        self.params.explanation_entities = Some(e);
1145        self
1146    }
1147    /// Sets how long the poll stays open in seconds (5–2628000).
1148    pub fn open_period(mut self, secs: u32) -> Self {
1149        self.params.open_period = Some(secs);
1150        self
1151    }
1152    /// Sets the Unix timestamp when the poll closes automatically.
1153    pub fn close_date(mut self, ts: i64) -> Self {
1154        self.params.close_date = Some(ts);
1155        self
1156    }
1157    /// Sets whether the options should be shuffled.
1158    pub fn shuffle_options(mut self, v: bool) -> Self {
1159        self.params.shuffle_options = Some(v);
1160        self
1161    }
1162    /// Allows users to add their own options to the poll.
1163    pub fn allow_adding_options(mut self, v: bool) -> Self {
1164        self.params.allow_adding_options = Some(v);
1165        self
1166    }
1167    /// Hides the poll results until it's closed.
1168    pub fn hide_results_until_closes(mut self, v: bool) -> Self {
1169        self.params.hide_results_until_closes = Some(v);
1170        self
1171    }
1172    /// Sets the poll description (0-1024 chars).
1173    pub fn description(mut self, d: impl Into<String>) -> Self {
1174        self.params.description = Some(d.into());
1175        self
1176    }
1177    /// Sets description parse mode.
1178    pub fn description_parse_mode(mut self, mode: ParseMode) -> Self {
1179        self.params.description_parse_mode = Some(mode);
1180        self
1181    }
1182    /// Sets description entities.
1183    pub fn description_entities(mut self, e: Vec<rustigram_types::message::MessageEntity>) -> Self {
1184        self.params.description_entities = Some(e);
1185        self
1186    }
1187    /// Sets the question parse mode.
1188    pub fn question_parse_mode(mut self, mode: ParseMode) -> Self {
1189        self.params.question_parse_mode = Some(mode);
1190        self
1191    }
1192    /// Sets question entities.
1193    pub fn question_entities(mut self, e: Vec<rustigram_types::message::MessageEntity>) -> Self {
1194        self.params.question_entities = Some(e);
1195        self
1196    }
1197    /// Sends the message silently — the recipient receives no notification sound.
1198    pub fn disable_notification(mut self, v: bool) -> Self {
1199        self.params.disable_notification = Some(v);
1200        self
1201    }
1202    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1203    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
1204        self.params.reply_markup = Some(m.into());
1205        self
1206    }
1207    /// Suggested post parameters for channel direct messages chats.
1208    pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self {
1209        self.params.suggested_post_parameters = Some(params);
1210        self
1211    }
1212    /// Pass `true` to limit voting to users who have been members of the chat for more than
1213    /// 24 hours; for channel chats only.
1214    pub fn members_only(mut self, v: bool) -> Self {
1215        self.params.members_only = Some(v);
1216        self
1217    }
1218    /// Two-letter ISO 3166-1 alpha-2 country codes for countries from which users can vote; channels only.
1219    pub fn country_codes(mut self, codes: Vec<impl Into<String>>) -> Self {
1220        self.params.country_codes = Some(codes.into_iter().map(Into::into).collect());
1221        self
1222    }
1223
1224    /// Media added to the poll description.
1225    pub fn media(mut self, m: rustigram_types::poll::InputPollMedia) -> Self {
1226        self.params.media = Some(m);
1227        self
1228    }
1229
1230    /// Media added to the quiz explanation.
1231    pub fn explanation_media(mut self, m: rustigram_types::poll::InputPollMedia) -> Self {
1232        self.params.explanation_media = Some(m);
1233        self
1234    }
1235    /// Business connection ID for acting on behalf of a business account.
1236    pub fn business_connection_id(mut self, v: impl Into<String>) -> Self {
1237        self.params.business_connection_id = Some(v.into());
1238        self
1239    }
1240    /// Allows sending to large audiences at the cost of Telegram Stars.
1241    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
1242        self.params.allow_paid_broadcast = Some(v);
1243        self
1244    }
1245    /// Attaches a message effect (animated emoji reaction) to the message.
1246    pub fn message_effect_id(mut self, v: impl Into<String>) -> Self {
1247        self.params.message_effect_id = Some(v.into());
1248        self
1249    }
1250    /// Sends the poll already closed, so it cannot be voted on.
1251    pub fn is_closed(mut self, v: bool) -> Self {
1252        self.params.is_closed = Some(v);
1253        self
1254    }
1255    /// Protects the message from being forwarded or saved.
1256    pub fn protect_content(mut self, v: bool) -> Self {
1257        self.params.protect_content = Some(v);
1258        self
1259    }
1260    /// Reply parameters for this message.
1261    pub fn reply_parameters(mut self, v: ReplyParameters) -> Self {
1262        self.params.reply_parameters = Some(v);
1263        self
1264    }
1265}
1266
1267impl_into_future!(SendPoll, Message, "sendPoll");
1268
1269// ─── sendMessageDraft ─────────────────────────────────────────────────────────
1270
1271#[derive(Serialize)]
1272struct SendMessageDraftParams {
1273    chat_id: ChatId,
1274    draft_id: i64,
1275    /// Optional per the spec — a draft may carry only media, or none at all.
1276    /// The constructor still takes text since that is the common case; use
1277    /// [`SendMessageDraft::clear_text`] to send without any.
1278    #[serde(skip_serializing_if = "Option::is_none")]
1279    text: Option<String>,
1280    #[serde(skip_serializing_if = "Option::is_none")]
1281    message_thread_id: Option<i64>,
1282    #[serde(skip_serializing_if = "Option::is_none")]
1283    parse_mode: Option<ParseMode>,
1284    #[serde(skip_serializing_if = "Option::is_none")]
1285    entities: Option<Vec<rustigram_types::message::MessageEntity>>,
1286}
1287
1288/// Builder for the [`sendMessageDraft`](https://core.telegram.org/bots/api#sendmessagedraft) method.
1289/// Streams a partial message to the user while it is being generated (Bot API 9.5+).
1290pub struct SendMessageDraft {
1291    client: BotClient,
1292    params: SendMessageDraftParams,
1293}
1294
1295impl SendMessageDraft {
1296    pub(crate) fn new(
1297        client: BotClient,
1298        chat_id: impl Into<ChatId>,
1299        draft_id: i64,
1300        text: impl Into<String>,
1301    ) -> Self {
1302        Self {
1303            client,
1304            params: SendMessageDraftParams {
1305                chat_id: chat_id.into(),
1306                draft_id,
1307                text: Some(text.into()),
1308                message_thread_id: None,
1309                parse_mode: None,
1310                entities: None,
1311            },
1312        }
1313    }
1314    /// Sets the text parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
1315    pub fn parse_mode(mut self, m: ParseMode) -> Self {
1316        self.params.parse_mode = Some(m);
1317        self
1318    }
1319    /// Sets custom message entities instead of using a parse mode.
1320    pub fn entities(mut self, e: Vec<rustigram_types::message::MessageEntity>) -> Self {
1321        self.params.entities = Some(e);
1322        self
1323    }
1324    /// Sends the draft with no text at all, which the API permits.
1325    pub fn clear_text(mut self) -> Self {
1326        self.params.text = None;
1327        self
1328    }
1329    /// Forum topic thread ID.
1330    pub fn message_thread_id(mut self, v: i64) -> Self {
1331        self.params.message_thread_id = Some(v);
1332        self
1333    }
1334}
1335
1336impl_into_future!(SendMessageDraft, bool, "sendMessageDraft");
1337
1338// ─── File-sending builders ────────────────────────────────────────────────────
1339//
1340// Photo, Audio, Document, Video, Animation, Voice, VideoNote each share a
1341// similar shape but differ in field names and constraints. We use a common
1342// pattern: store the InputFile and an optional Form for multipart, and build
1343// the form lazily in `IntoFuture`.
1344
1345/// Writes every [`MediaSendOptions`] field onto a multipart form.
1346///
1347/// Media senders take one of two paths: an `InputFile::Bytes` upload builds a
1348/// multipart form, while a file_id or URL goes through `media_json_body`. Those
1349/// two were maintained by hand and had drifted — the multipart side silently
1350/// dropped `protect_content`, `allow_paid_broadcast`, `reply_parameters`,
1351/// `caption_entities`, and `show_caption_above_media`, so a `.reply_to(..)` on
1352/// an uploaded photo never reached Telegram. Both paths now enumerate the same
1353/// struct, so a field added to `MediaSendOptions` cannot go missing from one.
1354fn apply_media_opts(mut form: Form, opts: &MediaSendOptions) -> Form {
1355    fn json_text(form: Form, key: &'static str, value: &impl Serialize) -> Form {
1356        // Plain data types; serialisation cannot realistically fail, and the
1357        // guard keeps a panic out of library code.
1358        match serde_json::to_string(value) {
1359            Ok(json) => form.text(key, json),
1360            Err(_) => form,
1361        }
1362    }
1363
1364    if let Some(v) = &opts.business_connection_id {
1365        form = form.text("business_connection_id", v.clone());
1366    }
1367    if let Some(v) = opts.message_thread_id {
1368        form = form.text("message_thread_id", v.to_string());
1369    }
1370    if let Some(v) = opts.direct_messages_topic_id {
1371        form = form.text("direct_messages_topic_id", v.to_string());
1372    }
1373    if let Some(v) = &opts.caption {
1374        form = form.text("caption", v.clone());
1375    }
1376    if let Some(v) = &opts.parse_mode {
1377        form = form.text("parse_mode", format!("{v:?}"));
1378    }
1379    if let Some(v) = &opts.caption_entities {
1380        form = json_text(form, "caption_entities", v);
1381    }
1382    if let Some(v) = opts.show_caption_above_media {
1383        form = form.text("show_caption_above_media", v.to_string());
1384    }
1385    if let Some(v) = opts.has_spoiler {
1386        form = form.text("has_spoiler", v.to_string());
1387    }
1388    if let Some(v) = opts.disable_notification {
1389        form = form.text("disable_notification", v.to_string());
1390    }
1391    if let Some(v) = opts.protect_content {
1392        form = form.text("protect_content", v.to_string());
1393    }
1394    if let Some(v) = opts.allow_paid_broadcast {
1395        form = form.text("allow_paid_broadcast", v.to_string());
1396    }
1397    if let Some(v) = &opts.message_effect_id {
1398        form = form.text("message_effect_id", v.clone());
1399    }
1400    if let Some(v) = &opts.reply_parameters {
1401        form = json_text(form, "reply_parameters", v);
1402    }
1403    if let Some(v) = &opts.reply_markup {
1404        form = json_text(form, "reply_markup", v);
1405    }
1406    if let Some(v) = &opts.suggested_post_parameters {
1407        form = json_text(form, "suggested_post_parameters", v);
1408    }
1409    if let Some(v) = opts.receiver_user_id {
1410        form = form.text("receiver_user_id", v.to_string());
1411    }
1412    if let Some(v) = &opts.callback_query_id {
1413        form = form.text("callback_query_id", v.clone());
1414    }
1415    form
1416}
1417
1418/// Common optional parameters shared by most media-send methods.
1419#[derive(Default)]
1420pub struct MediaSendOptions {
1421    /// Business connection ID for sending on behalf of a business account.
1422    pub business_connection_id: Option<String>,
1423    /// Forum topic thread ID.
1424    pub message_thread_id: Option<i64>,
1425    /// Identifier of a direct messages chat topic.
1426    pub direct_messages_topic_id: Option<i64>,
1427    /// Sets the caption (0–1024 characters) for media messages.
1428    pub caption: Option<String>,
1429    /// Parse mode for the caption.
1430    pub parse_mode: Option<ParseMode>,
1431    /// Special entities in the caption.
1432    pub caption_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
1433    /// Shows the caption above the media instead of below it.
1434    pub show_caption_above_media: Option<bool>,
1435    /// Marks the media as a spoiler — blurs it until the user taps to reveal.
1436    pub has_spoiler: Option<bool>,
1437    /// Sends the message silently — the recipient receives no notification sound.
1438    pub disable_notification: Option<bool>,
1439    /// Protects the message from being forwarded or saved.
1440    pub protect_content: Option<bool>,
1441    /// Allows sending to large audiences at the cost of Telegram Stars.
1442    pub allow_paid_broadcast: Option<bool>,
1443    /// Attaches a message effect (animated emoji reaction) to the message.
1444    pub message_effect_id: Option<String>,
1445    /// Reply parameters for this message.
1446    pub reply_parameters: Option<ReplyParameters>,
1447    /// Reply markup attached to the message.
1448    pub reply_markup: Option<ReplyMarkup>,
1449    /// Suggested post parameters for channel direct messages chats.
1450    pub suggested_post_parameters: Option<SuggestedPostParameters>,
1451    /// For outgoing ephemeral messages — identifier of the user who will
1452    /// receive the message; group and supergroup chats only. Delivery is not
1453    /// guaranteed, especially if the user is offline.
1454    pub receiver_user_id: Option<i64>,
1455    /// For outgoing ephemeral messages — identifier of the callback query
1456    /// that triggered the message, if any.
1457    pub callback_query_id: Option<String>,
1458}
1459
1460/// Builds the JSON body for a simple (non-file-upload) part of a media send.
1461fn media_json_body(
1462    chat_id: &ChatId,
1463    media_field: &str,
1464    media_value: &str,
1465    opts: &MediaSendOptions,
1466    extra: serde_json::Value,
1467) -> serde_json::Value {
1468    let mut map = serde_json::json!({
1469        "chat_id": chat_id,
1470        media_field: media_value,
1471    });
1472    let obj = map.as_object_mut().unwrap();
1473    if let Some(v) = &opts.business_connection_id {
1474        obj.insert("business_connection_id".to_owned(), serde_json::json!(v));
1475    }
1476    if let Some(v) = &opts.message_thread_id {
1477        obj.insert("message_thread_id".to_owned(), serde_json::json!(v));
1478    }
1479    if let Some(v) = &opts.direct_messages_topic_id {
1480        obj.insert("direct_messages_topic_id".to_owned(), serde_json::json!(v));
1481    }
1482    if let Some(v) = &opts.caption {
1483        obj.insert("caption".to_owned(), serde_json::json!(v));
1484    }
1485    if let Some(v) = &opts.parse_mode {
1486        obj.insert("parse_mode".to_owned(), serde_json::json!(v));
1487    }
1488    if let Some(v) = &opts.caption_entities {
1489        obj.insert("caption_entities".to_owned(), serde_json::json!(v));
1490    }
1491    if let Some(v) = opts.show_caption_above_media {
1492        obj.insert("show_caption_above_media".to_owned(), serde_json::json!(v));
1493    }
1494    if let Some(v) = opts.has_spoiler {
1495        obj.insert("has_spoiler".to_owned(), serde_json::json!(v));
1496    }
1497    if let Some(v) = opts.disable_notification {
1498        obj.insert("disable_notification".to_owned(), serde_json::json!(v));
1499    }
1500    if let Some(v) = opts.protect_content {
1501        obj.insert("protect_content".to_owned(), serde_json::json!(v));
1502    }
1503    if let Some(v) = opts.allow_paid_broadcast {
1504        obj.insert("allow_paid_broadcast".to_owned(), serde_json::json!(v));
1505    }
1506    if let Some(v) = &opts.message_effect_id {
1507        obj.insert("message_effect_id".to_owned(), serde_json::json!(v));
1508    }
1509    if let Some(v) = &opts.reply_parameters {
1510        obj.insert("reply_parameters".to_owned(), serde_json::json!(v));
1511    }
1512    if let Some(v) = &opts.reply_markup {
1513        obj.insert("reply_markup".to_owned(), serde_json::json!(v));
1514    }
1515    if let Some(v) = &opts.suggested_post_parameters {
1516        obj.insert("suggested_post_parameters".to_owned(), serde_json::json!(v));
1517    }
1518    if let Some(v) = opts.receiver_user_id {
1519        obj.insert("receiver_user_id".to_owned(), serde_json::json!(v));
1520    }
1521    if let Some(v) = &opts.callback_query_id {
1522        obj.insert("callback_query_id".to_owned(), serde_json::json!(v));
1523    }
1524    if let serde_json::Value::Object(extra_obj) = extra {
1525        for (k, v) in extra_obj {
1526            obj.insert(k, v);
1527        }
1528    }
1529    map
1530}
1531
1532// ─── sendPhoto ────────────────────────────────────────────────────────────────
1533
1534/// Builder for the [`sendPhoto`](https://core.telegram.org/bots/api#sendphoto) method.
1535pub struct SendPhoto {
1536    client: BotClient,
1537    chat_id: ChatId,
1538    photo: InputFile,
1539    opts: MediaSendOptions,
1540}
1541
1542impl SendPhoto {
1543    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, photo: InputFile) -> Self {
1544        Self {
1545            client,
1546            chat_id: chat_id.into(),
1547            photo,
1548            opts: MediaSendOptions::default(),
1549        }
1550    }
1551    /// Business connection ID for sending on behalf of a business account.
1552    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
1553        self.opts.business_connection_id = Some(id.into());
1554        self
1555    }
1556    /// Forum topic thread ID.
1557    pub fn message_thread_id(mut self, id: i64) -> Self {
1558        self.opts.message_thread_id = Some(id);
1559        self
1560    }
1561    /// Identifier of a direct messages chat topic.
1562    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1563        self.opts.direct_messages_topic_id = Some(id);
1564        self
1565    }
1566    /// Sets the caption (0–1024 characters) for media messages.
1567    pub fn caption(mut self, c: impl Into<String>) -> Self {
1568        self.opts.caption = Some(c.into());
1569        self
1570    }
1571    /// Sets the caption parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
1572    pub fn parse_mode(mut self, m: ParseMode) -> Self {
1573        self.opts.parse_mode = Some(m);
1574        self
1575    }
1576    /// Marks the media as a spoiler — blurs it until the user taps to reveal.
1577    pub fn has_spoiler(mut self, v: bool) -> Self {
1578        self.opts.has_spoiler = Some(v);
1579        self
1580    }
1581    /// Special entities in the caption, in place of `parse_mode`.
1582    pub fn caption_entities(
1583        mut self,
1584        entities: Vec<rustigram_types::message::MessageEntity>,
1585    ) -> Self {
1586        self.opts.caption_entities = Some(entities);
1587        self
1588    }
1589    /// Shows the caption above the media instead of below it.
1590    pub fn show_caption_above_media(mut self, v: bool) -> Self {
1591        self.opts.show_caption_above_media = Some(v);
1592        self
1593    }
1594    /// Sends the message silently — the recipient receives no notification sound.
1595    pub fn disable_notification(mut self, v: bool) -> Self {
1596        self.opts.disable_notification = Some(v);
1597        self
1598    }
1599    /// Protects the message from being forwarded or saved.
1600    pub fn protect_content(mut self, v: bool) -> Self {
1601        self.opts.protect_content = Some(v);
1602        self
1603    }
1604    /// Allows sending to large audiences at the cost of Telegram Stars.
1605    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
1606        self.opts.allow_paid_broadcast = Some(v);
1607        self
1608    }
1609    /// Reply parameters for this message.
1610    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
1611        self.opts.reply_parameters = Some(rp);
1612        self
1613    }
1614    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1615    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
1616        self.opts.reply_markup = Some(m.into());
1617        self
1618    }
1619    /// Suggested post parameters for channel direct messages chats.
1620    pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self {
1621        self.opts.suggested_post_parameters = Some(params);
1622        self
1623    }
1624    /// For outgoing ephemeral messages — the user who will receive the message.
1625    pub fn receiver_user_id(mut self, id: i64) -> Self {
1626        self.opts.receiver_user_id = Some(id);
1627        self
1628    }
1629    /// For outgoing ephemeral messages — the callback query that triggered it, if any.
1630    pub fn callback_query_id(mut self, id: impl Into<String>) -> Self {
1631        self.opts.callback_query_id = Some(id.into());
1632        self
1633    }
1634    /// Attaches a message effect (animated emoji reaction) to the message.
1635    pub fn message_effect_id(mut self, id: impl Into<String>) -> Self {
1636        self.opts.message_effect_id = Some(id.into());
1637        self
1638    }
1639}
1640
1641impl IntoFuture for SendPhoto {
1642    type Output = Result<Message>;
1643    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1644
1645    fn into_future(self) -> Self::IntoFuture {
1646        Box::pin(async move {
1647            match &self.photo {
1648                InputFile::Bytes {
1649                    filename,
1650                    data,
1651                    mime_type,
1652                } => {
1653                    let part = Part::bytes(data.clone())
1654                        .file_name(filename.clone())
1655                        .mime_str(mime_type)
1656                        .map_err(|e| crate::error::Error::Decode(e.to_string()))?;
1657                    let mut form = Form::new().part("photo", part);
1658                    form = form.text("chat_id", self.chat_id.to_string());
1659                    form = apply_media_opts(form, &self.opts);
1660                    self.client.post_multipart("sendPhoto", form).await
1661                }
1662                _ => {
1663                    let body = media_json_body(
1664                        &self.chat_id,
1665                        "photo",
1666                        self.photo.as_str(),
1667                        &self.opts,
1668                        serde_json::Value::Null,
1669                    );
1670                    self.client.post_json("sendPhoto", &body).await
1671                }
1672            }
1673        })
1674    }
1675}
1676
1677// ─── sendLivePhoto ────────────────────────────────────────────────────────────
1678
1679/// Builder for the [`sendLivePhoto`](https://core.telegram.org/bots/api#sendlivephoto) method.
1680pub struct SendLivePhoto {
1681    client: BotClient,
1682    chat_id: ChatId,
1683    live_photo: InputFile,
1684    photo: InputFile,
1685    opts: MediaSendOptions,
1686}
1687
1688impl SendLivePhoto {
1689    pub(crate) fn new(
1690        client: BotClient,
1691        chat_id: impl Into<ChatId>,
1692        live_photo: InputFile,
1693        photo: InputFile,
1694    ) -> Self {
1695        Self {
1696            client,
1697            chat_id: chat_id.into(),
1698            live_photo,
1699            photo,
1700            opts: MediaSendOptions::default(),
1701        }
1702    }
1703    /// Business connection ID for sending on behalf of a business account.
1704    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
1705        self.opts.business_connection_id = Some(id.into());
1706        self
1707    }
1708    /// Forum topic thread ID.
1709    pub fn message_thread_id(mut self, id: i64) -> Self {
1710        self.opts.message_thread_id = Some(id);
1711        self
1712    }
1713    /// Identifier of a direct messages chat topic.
1714    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1715        self.opts.direct_messages_topic_id = Some(id);
1716        self
1717    }
1718    /// Sets the caption (0–1024 characters).
1719    pub fn caption(mut self, c: impl Into<String>) -> Self {
1720        self.opts.caption = Some(c.into());
1721        self
1722    }
1723    /// Sets the caption parse mode.
1724    pub fn parse_mode(mut self, m: ParseMode) -> Self {
1725        self.opts.parse_mode = Some(m);
1726        self
1727    }
1728    /// Shows the caption above the media instead of below it.
1729    pub fn show_caption_above_media(mut self, v: bool) -> Self {
1730        self.opts.show_caption_above_media = Some(v);
1731        self
1732    }
1733    /// Covers the live photo with a spoiler animation.
1734    pub fn has_spoiler(mut self, v: bool) -> Self {
1735        self.opts.has_spoiler = Some(v);
1736        self
1737    }
1738    /// Special entities in the caption, in place of `parse_mode`.
1739    pub fn caption_entities(
1740        mut self,
1741        entities: Vec<rustigram_types::message::MessageEntity>,
1742    ) -> Self {
1743        self.opts.caption_entities = Some(entities);
1744        self
1745    }
1746    /// Sends the message as ephemeral, visible only to this user.
1747    pub fn receiver_user_id(mut self, id: i64) -> Self {
1748        self.opts.receiver_user_id = Some(id);
1749        self
1750    }
1751    /// Identifier of the callback query this ephemeral message answers.
1752    pub fn callback_query_id(mut self, id: impl Into<String>) -> Self {
1753        self.opts.callback_query_id = Some(id.into());
1754        self
1755    }
1756    /// Sends the message silently.
1757    pub fn disable_notification(mut self, v: bool) -> Self {
1758        self.opts.disable_notification = Some(v);
1759        self
1760    }
1761    /// Protects the message from being forwarded or saved.
1762    pub fn protect_content(mut self, v: bool) -> Self {
1763        self.opts.protect_content = Some(v);
1764        self
1765    }
1766    /// Allows sending to large audiences at the cost of Telegram Stars.
1767    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
1768        self.opts.allow_paid_broadcast = Some(v);
1769        self
1770    }
1771    /// Attaches a message effect (private chats only).
1772    pub fn message_effect_id(mut self, id: impl Into<String>) -> Self {
1773        self.opts.message_effect_id = Some(id.into());
1774        self
1775    }
1776    /// Reply parameters for this message.
1777    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
1778        self.opts.reply_parameters = Some(rp);
1779        self
1780    }
1781    /// Attaches a reply markup.
1782    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
1783        self.opts.reply_markup = Some(m.into());
1784        self
1785    }
1786    /// Suggested post parameters for channel direct messages chats.
1787    pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self {
1788        self.opts.suggested_post_parameters = Some(params);
1789        self
1790    }
1791}
1792
1793impl IntoFuture for SendLivePhoto {
1794    type Output = Result<Message>;
1795    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1796
1797    fn into_future(self) -> Self::IntoFuture {
1798        Box::pin(async move {
1799            let lp_bytes = self.live_photo.requires_multipart();
1800            let ph_bytes = self.photo.requires_multipart();
1801
1802            if lp_bytes || ph_bytes {
1803                let mut form = Form::new();
1804                form = form.text("chat_id", self.chat_id.to_string());
1805
1806                if let InputFile::Bytes {
1807                    filename,
1808                    data,
1809                    mime_type,
1810                } = self.live_photo
1811                {
1812                    let part = Part::bytes(data)
1813                        .file_name(filename)
1814                        .mime_str(&mime_type)
1815                        .map_err(|e| crate::error::Error::Decode(e.to_string()))?;
1816                    form = form.part("live_photo", part);
1817                } else {
1818                    form = form.text("live_photo", self.live_photo.as_str().to_owned());
1819                }
1820
1821                if let InputFile::Bytes {
1822                    filename,
1823                    data,
1824                    mime_type,
1825                } = self.photo
1826                {
1827                    let part = Part::bytes(data)
1828                        .file_name(filename)
1829                        .mime_str(&mime_type)
1830                        .map_err(|e| crate::error::Error::Decode(e.to_string()))?;
1831                    form = form.part("photo", part);
1832                } else {
1833                    form = form.text("photo", self.photo.as_str().to_owned());
1834                }
1835
1836                form = apply_media_opts(form, &self.opts);
1837
1838                self.client.post_multipart("sendLivePhoto", form).await
1839            } else {
1840                let mut body = media_json_body(
1841                    &self.chat_id,
1842                    "live_photo",
1843                    self.live_photo.as_str(),
1844                    &self.opts,
1845                    serde_json::json!({}),
1846                );
1847                body.as_object_mut()
1848                    .unwrap()
1849                    .insert("photo".to_owned(), serde_json::json!(self.photo.as_str()));
1850                self.client.post_json("sendLivePhoto", &body).await
1851            }
1852        })
1853    }
1854}
1855
1856// ─── Macro for simpler media senders (Audio, Document, Video, Animation, Voice, VideoNote, Sticker)
1857
1858/// One caption-family setter, emitted only for the methods the spec allows it on.
1859///
1860/// These five are not universal: `sendVideoNote` and `sendSticker` take no
1861/// caption at all, and only `sendVideo` and `sendAnimation` accept a spoiler or
1862/// a caption above the media. Exposing them uniformly gave callers two setters
1863/// Telegram ignores and withheld three it honours.
1864macro_rules! caption_setter {
1865    (caption) => {
1866        /// Sets the caption (0–1024 characters).
1867        pub fn caption(mut self, c: impl Into<String>) -> Self {
1868            self.opts.caption = Some(c.into());
1869            self
1870        }
1871    };
1872    (parse_mode) => {
1873        /// Parse mode for the caption.
1874        pub fn parse_mode(mut self, m: ParseMode) -> Self {
1875            self.opts.parse_mode = Some(m);
1876            self
1877        }
1878    };
1879    (caption_entities) => {
1880        /// Special entities in the caption, in place of `parse_mode`.
1881        pub fn caption_entities(
1882            mut self,
1883            entities: Vec<rustigram_types::message::MessageEntity>,
1884        ) -> Self {
1885            self.opts.caption_entities = Some(entities);
1886            self
1887        }
1888    };
1889    (show_caption_above_media) => {
1890        /// Shows the caption above the media instead of below it.
1891        pub fn show_caption_above_media(mut self, v: bool) -> Self {
1892            self.opts.show_caption_above_media = Some(v);
1893            self
1894        }
1895    };
1896    (has_spoiler) => {
1897        /// Marks the media as a spoiler — blurs it until the user taps to reveal.
1898        pub fn has_spoiler(mut self, v: bool) -> Self {
1899            self.opts.has_spoiler = Some(v);
1900            self
1901        }
1902    };
1903}
1904
1905macro_rules! media_sender {
1906    ($(#[$doc:meta])* $name:ident, $field:literal, $method:literal, $return_ty:ty,
1907     [$($extra_field:ident: $extra_ty:ty),*], [$($caption_opt:ident),*]) => {
1908        $(#[$doc])*
1909        pub struct $name {
1910            /// The API client to use for sending the request.
1911            client: BotClient,
1912            /// Unique identifier for the target chat or username of the target channel.
1913            chat_id: ChatId,
1914            /// The file to send. Can be a file ID, URL, or new upload.
1915            file: InputFile,
1916            /// Common optional parameters for media sending.
1917            opts: MediaSendOptions,
1918            /// Extra optional parameters specific to this media type.
1919            $($extra_field: Option<$extra_ty>,)*
1920        }
1921
1922        impl $name {
1923            pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, file: InputFile) -> Self {
1924                Self {
1925                    client,
1926                    chat_id: chat_id.into(),
1927                    file,
1928                    opts: MediaSendOptions::default(),
1929                    $($extra_field: None,)*
1930                }
1931            }
1932            /// Business connection ID for sending on behalf of a business account.
1933            pub fn business_connection_id(mut self, id: impl Into<String>) -> Self { self.opts.business_connection_id = Some(id.into()); self }
1934            /// Forum topic thread ID.
1935            pub fn message_thread_id(mut self, id: i64) -> Self { self.opts.message_thread_id = Some(id); self }
1936            /// Identifier of a direct messages chat topic.
1937            pub fn direct_messages_topic_id(mut self, id: i64) -> Self { self.opts.direct_messages_topic_id = Some(id); self }
1938            // The caption-family setters this method actually takes. Each
1939            // carries its own doc comment inside `caption_setter!` — a doc
1940            // comment out here would attach to the macro invocation, which
1941            // rustdoc drops, and clippy rejects under `-D warnings`.
1942            $(caption_setter!($caption_opt);)*
1943            /// Sends the message silently — the recipient receives no notification sound.
1944            pub fn disable_notification(mut self, v: bool) -> Self { self.opts.disable_notification = Some(v); self }
1945            /// Attaches a message effect (animated emoji reaction) to the message.
1946            pub fn message_effect_id(mut self, id: impl Into<String>) -> Self { self.opts.message_effect_id = Some(id.into()); self }
1947            /// Protects the message from being forwarded or saved.
1948            pub fn protect_content(mut self, v: bool) -> Self { self.opts.protect_content = Some(v); self }
1949            /// Allows sending to large audiences at the cost of Telegram Stars.
1950            pub fn allow_paid_broadcast(mut self, v: bool) -> Self { self.opts.allow_paid_broadcast = Some(v); self }
1951            /// Reply parameters for this message.
1952            pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self { self.opts.reply_parameters = Some(rp); self }
1953            /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1954            pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self { self.opts.reply_markup = Some(m.into()); self }
1955            /// Suggested post parameters for channel direct messages chats.
1956            pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self { self.opts.suggested_post_parameters = Some(params); self }
1957            /// For outgoing ephemeral messages — the user who will receive the message.
1958            pub fn receiver_user_id(mut self, id: i64) -> Self { self.opts.receiver_user_id = Some(id); self }
1959            /// For outgoing ephemeral messages — the callback query that triggered it, if any.
1960            pub fn callback_query_id(mut self, id: impl Into<String>) -> Self { self.opts.callback_query_id = Some(id.into()); self }
1961
1962            $(
1963                #[doc = concat!("Sets the ", stringify!($extra_field), " for the media.")]
1964                pub fn $extra_field(mut self, v: $extra_ty) -> Self {
1965                    self.$extra_field = Some(v);
1966                    self
1967                }
1968            )*
1969        }
1970
1971        impl IntoFuture for $name {
1972            type Output = Result<$return_ty>;
1973            type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
1974
1975            fn into_future(self) -> Self::IntoFuture {
1976                Box::pin(async move {
1977                    match &self.file {
1978                        InputFile::Bytes { filename, data, mime_type } => {
1979                            let part = Part::bytes(data.clone())
1980                                .file_name(filename.clone())
1981                                .mime_str(mime_type)
1982                                .map_err(|e| crate::error::Error::Decode(e.to_string()))?;
1983                            let mut form = Form::new().part($field, part);
1984                            form = form.text("chat_id", self.chat_id.to_string());
1985                            // The shared helper, not a copy of its field list.
1986                            // This block used to enumerate the options by hand
1987                            // and covered ten of the seventeen, so every builder
1988                            // this macro generates silently dropped the other
1989                            // seven on a byte upload — protect_content and
1990                            // reply_parameters among them.
1991                            form = apply_media_opts(form, &self.opts);
1992
1993                            $(
1994                                if let Some(ref v) = self.$extra_field {
1995                                    form = form.text(stringify!($extra_field), v.to_string());
1996                                }
1997                            )*
1998
1999                            self.client.post_multipart($method, form).await
2000                        }
2001                        _ => {
2002                            let mut extra = serde_json::json!({});
2003                            $(
2004                                if let Some(ref v) = self.$extra_field {
2005                                    extra[stringify!($extra_field)] = serde_json::json!(v);
2006                                }
2007                            )*
2008                            let body = media_json_body(&self.chat_id, $field, self.file.as_str(), &self.opts, extra);
2009                            self.client.post_json($method, &body).await
2010                        }
2011                    }
2012                })
2013            }
2014        }
2015    };
2016}
2017
2018media_sender!(
2019    /// Builder for the [`sendAudio`](https://core.telegram.org/bots/api#sendaudio) method.
2020    SendAudio,      "audio",      "sendAudio",      Message, [duration: u32, performer: String, title: String, thumbnail: String], [caption, parse_mode, caption_entities]);
2021media_sender!(
2022    /// Builder for the [`sendDocument`](https://core.telegram.org/bots/api#senddocument) method.
2023    SendDocument,  "document",   "sendDocument",  Message, [disable_content_type_detection: bool, thumbnail: String], [caption, parse_mode, caption_entities]);
2024media_sender!(
2025    /// Builder for the [`sendVideo`](https://core.telegram.org/bots/api#sendvideo) method.
2026    SendVideo,      "video",      "sendVideo",      Message, [duration: u32, width: u32, height: u32, supports_streaming: bool, cover: String, start_timestamp: i64, thumbnail: String], [caption, parse_mode, caption_entities, show_caption_above_media, has_spoiler]);
2027media_sender!(
2028    /// Builder for the [`sendAnimation`](https://core.telegram.org/bots/api#sendanimation) method.
2029    SendAnimation, "animation",  "sendAnimation", Message, [duration: u32, width: u32, height: u32, thumbnail: String], [caption, parse_mode, caption_entities, show_caption_above_media, has_spoiler]);
2030media_sender!(
2031    /// Builder for the [`sendVoice`](https://core.telegram.org/bots/api#sendvoice) method.
2032    SendVoice,      "voice",      "sendVoice",      Message, [duration: u32], [caption, parse_mode, caption_entities]);
2033media_sender!(
2034    /// Builder for the [`sendVideoNote`](https://core.telegram.org/bots/api#sendvideonote) method.
2035    SendVideoNote, "video_note", "sendVideoNote", Message, [duration: u32, length: u32, thumbnail: String], []);
2036media_sender!(
2037    /// Builder for the [`sendSticker`](https://core.telegram.org/bots/api#sendsticker) method.
2038    SendSticker,   "sticker",    "sendSticker",    Message, [emoji: String], []);
2039
2040// ─── deleteMessage / deleteMessages ──────────────────────────────────────────
2041
2042#[derive(Serialize)]
2043struct DeleteMessageParams {
2044    chat_id: ChatId,
2045    message_id: i64,
2046}
2047
2048/// Builder for the [`deleteMessage`](https://core.telegram.org/bots/api#deletemessage) method.
2049pub struct DeleteMessage {
2050    client: BotClient,
2051    params: DeleteMessageParams,
2052}
2053impl DeleteMessage {
2054    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, message_id: i64) -> Self {
2055        Self {
2056            client,
2057            params: DeleteMessageParams {
2058                chat_id: chat_id.into(),
2059                message_id,
2060            },
2061        }
2062    }
2063}
2064impl_into_future!(DeleteMessage, bool, "deleteMessage");
2065
2066#[derive(Serialize)]
2067struct DeleteMessagesParams {
2068    chat_id: ChatId,
2069    message_ids: Vec<i64>,
2070}
2071
2072/// Builder for the [`deleteMessages`](https://core.telegram.org/bots/api#deletemessages) method.
2073pub struct DeleteMessages {
2074    client: BotClient,
2075    params: DeleteMessagesParams,
2076}
2077impl DeleteMessages {
2078    pub(crate) fn new(
2079        client: BotClient,
2080        chat_id: impl Into<ChatId>,
2081        message_ids: Vec<i64>,
2082    ) -> Self {
2083        Self {
2084            client,
2085            params: DeleteMessagesParams {
2086                chat_id: chat_id.into(),
2087                message_ids,
2088            },
2089        }
2090    }
2091}
2092impl_into_future!(DeleteMessages, bool, "deleteMessages");
2093
2094// ─── deleteEphemeralMessage ───────────────────────────────────────────────────
2095
2096#[derive(Serialize)]
2097struct DeleteEphemeralMessageParams {
2098    chat_id: ChatId,
2099    receiver_user_id: i64,
2100    ephemeral_message_id: i64,
2101}
2102
2103/// Builder for the [`deleteEphemeralMessage`](https://core.telegram.org/bots/api#deleteephemeralmessage) method.
2104///
2105/// Note that it is not guaranteed that the user will receive the message
2106/// deletion event, especially if they are offline.
2107pub struct DeleteEphemeralMessage {
2108    client: BotClient,
2109    params: DeleteEphemeralMessageParams,
2110}
2111impl DeleteEphemeralMessage {
2112    pub(crate) fn new(
2113        client: BotClient,
2114        chat_id: impl Into<ChatId>,
2115        receiver_user_id: i64,
2116        ephemeral_message_id: i64,
2117    ) -> Self {
2118        Self {
2119            client,
2120            params: DeleteEphemeralMessageParams {
2121                chat_id: chat_id.into(),
2122                receiver_user_id,
2123                ephemeral_message_id,
2124            },
2125        }
2126    }
2127}
2128impl_into_future!(DeleteEphemeralMessage, bool, "deleteEphemeralMessage");
2129
2130// ─── stopPoll ─────────────────────────────────────────────────────────────────
2131
2132#[derive(Serialize)]
2133struct StopPollParams {
2134    chat_id: ChatId,
2135    message_id: i64,
2136    #[serde(skip_serializing_if = "Option::is_none")]
2137    reply_markup: Option<rustigram_types::keyboard::InlineKeyboardMarkup>,
2138    #[serde(skip_serializing_if = "Option::is_none")]
2139    business_connection_id: Option<String>,
2140}
2141
2142/// Builder for the [`stopPoll`](https://core.telegram.org/bots/api#stoppoll) method.
2143pub struct StopPoll {
2144    client: BotClient,
2145    params: StopPollParams,
2146}
2147impl StopPoll {
2148    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, message_id: i64) -> Self {
2149        Self {
2150            client,
2151            params: StopPollParams {
2152                chat_id: chat_id.into(),
2153                message_id,
2154                reply_markup: None,
2155                business_connection_id: None,
2156            },
2157        }
2158    }
2159    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
2160    pub fn reply_markup(mut self, m: rustigram_types::keyboard::InlineKeyboardMarkup) -> Self {
2161        self.params.reply_markup = Some(m);
2162        self
2163    }
2164    /// Business connection ID for acting on behalf of a business account.
2165    pub fn business_connection_id(mut self, v: impl Into<String>) -> Self {
2166        self.params.business_connection_id = Some(v.into());
2167        self
2168    }
2169}
2170impl_into_future!(StopPoll, rustigram_types::poll::Poll, "stopPoll");
2171
2172// ─── answerCallbackQuery ──────────────────────────────────────────────────────
2173
2174#[derive(Serialize)]
2175struct AnswerCallbackQueryParams {
2176    callback_query_id: String,
2177    #[serde(skip_serializing_if = "Option::is_none")]
2178    text: Option<String>,
2179    #[serde(skip_serializing_if = "Option::is_none")]
2180    show_alert: Option<bool>,
2181    #[serde(skip_serializing_if = "Option::is_none")]
2182    url: Option<String>,
2183    #[serde(skip_serializing_if = "Option::is_none")]
2184    cache_time: Option<u32>,
2185}
2186
2187/// Builder for the [`answerCallbackQuery`](https://core.telegram.org/bots/api#answercallbackquery) method.
2188pub struct AnswerCallbackQuery {
2189    client: BotClient,
2190    params: AnswerCallbackQueryParams,
2191}
2192impl AnswerCallbackQuery {
2193    pub(crate) fn new(client: BotClient, callback_query_id: impl Into<String>) -> Self {
2194        Self {
2195            client,
2196            params: AnswerCallbackQueryParams {
2197                callback_query_id: callback_query_id.into(),
2198                text: None,
2199                show_alert: None,
2200                url: None,
2201                cache_time: None,
2202            },
2203        }
2204    }
2205    /// The text of the notification shown to the user. 0–200 characters.
2206    pub fn text(mut self, t: impl Into<String>) -> Self {
2207        self.params.text = Some(t.into());
2208        self
2209    }
2210    /// Shows an alert dialog instead of a toast notification for the callback answer.
2211    pub fn show_alert(mut self, v: bool) -> Self {
2212        self.params.show_alert = Some(v);
2213        self
2214    }
2215    /// Sets the URL to open when the callback button answer is tapped.
2216    pub fn url(mut self, u: impl Into<String>) -> Self {
2217        self.params.url = Some(u.into());
2218        self
2219    }
2220    /// Sets how long the callback answer may be cached on the client in seconds.
2221    pub fn cache_time(mut self, secs: u32) -> Self {
2222        self.params.cache_time = Some(secs);
2223        self
2224    }
2225    /// Shorthand for `.text(t).show_alert(true)` — shows a popup alert to the user.
2226    pub fn alert(self, text: impl Into<String>) -> Self {
2227        self.text(text).show_alert(true)
2228    }
2229}
2230impl_into_future!(AnswerCallbackQuery, bool, "answerCallbackQuery");
2231// ─── forwardMessages ──────────────────────────────────────────────────────────
2232
2233#[derive(Serialize)]
2234struct ForwardMessagesParams {
2235    chat_id: ChatId,
2236    from_chat_id: ChatId,
2237    message_ids: Vec<i64>,
2238    #[serde(skip_serializing_if = "Option::is_none")]
2239    message_thread_id: Option<i64>,
2240    #[serde(skip_serializing_if = "Option::is_none")]
2241    direct_messages_topic_id: Option<i64>,
2242    #[serde(skip_serializing_if = "Option::is_none")]
2243    disable_notification: Option<bool>,
2244    #[serde(skip_serializing_if = "Option::is_none")]
2245    protect_content: Option<bool>,
2246}
2247
2248/// Builder for the [`forwardMessages`](https://core.telegram.org/bots/api#forwardmessages) method.
2249///
2250/// Forwards 1–100 messages at once, preserving album grouping.
2251/// Returns a `Vec<MessageId>` of the sent messages.
2252pub struct ForwardMessages {
2253    client: BotClient,
2254    params: ForwardMessagesParams,
2255}
2256
2257impl ForwardMessages {
2258    pub(crate) fn new(
2259        client: BotClient,
2260        chat_id: impl Into<ChatId>,
2261        from_chat_id: impl Into<ChatId>,
2262        message_ids: Vec<i64>,
2263    ) -> Self {
2264        Self {
2265            client,
2266            params: ForwardMessagesParams {
2267                chat_id: chat_id.into(),
2268                from_chat_id: from_chat_id.into(),
2269                message_ids,
2270                message_thread_id: None,
2271                direct_messages_topic_id: None,
2272                disable_notification: None,
2273                protect_content: None,
2274            },
2275        }
2276    }
2277    /// Forum topic thread ID.
2278    pub fn message_thread_id(mut self, id: i64) -> Self {
2279        self.params.message_thread_id = Some(id);
2280        self
2281    }
2282    /// Identifier of a direct messages chat topic.
2283    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2284        self.params.direct_messages_topic_id = Some(id);
2285        self
2286    }
2287    /// Sends the messages silently — recipients receive no notification sound.
2288    pub fn disable_notification(mut self, v: bool) -> Self {
2289        self.params.disable_notification = Some(v);
2290        self
2291    }
2292    /// Protects the messages from being forwarded or saved.
2293    pub fn protect_content(mut self, v: bool) -> Self {
2294        self.params.protect_content = Some(v);
2295        self
2296    }
2297}
2298
2299impl_into_future!(
2300    ForwardMessages,
2301    Vec<rustigram_types::message::MessageId>,
2302    "forwardMessages"
2303);
2304
2305// ─── copyMessages ─────────────────────────────────────────────────────────────
2306
2307#[derive(Serialize)]
2308struct CopyMessagesParams {
2309    chat_id: ChatId,
2310    from_chat_id: ChatId,
2311    message_ids: Vec<i64>,
2312    #[serde(skip_serializing_if = "Option::is_none")]
2313    message_thread_id: Option<i64>,
2314    #[serde(skip_serializing_if = "Option::is_none")]
2315    direct_messages_topic_id: Option<i64>,
2316    #[serde(skip_serializing_if = "Option::is_none")]
2317    disable_notification: Option<bool>,
2318    #[serde(skip_serializing_if = "Option::is_none")]
2319    protect_content: Option<bool>,
2320    #[serde(skip_serializing_if = "Option::is_none")]
2321    remove_caption: Option<bool>,
2322}
2323
2324/// Builder for the [`copyMessages`](https://core.telegram.org/bots/api#copymessages) method.
2325///
2326/// Copies 1–100 messages without a forward link, preserving album grouping.
2327/// Returns a `Vec<MessageId>` of the sent messages.
2328pub struct CopyMessages {
2329    client: BotClient,
2330    params: CopyMessagesParams,
2331}
2332
2333impl CopyMessages {
2334    pub(crate) fn new(
2335        client: BotClient,
2336        chat_id: impl Into<ChatId>,
2337        from_chat_id: impl Into<ChatId>,
2338        message_ids: Vec<i64>,
2339    ) -> Self {
2340        Self {
2341            client,
2342            params: CopyMessagesParams {
2343                chat_id: chat_id.into(),
2344                from_chat_id: from_chat_id.into(),
2345                message_ids,
2346                message_thread_id: None,
2347                direct_messages_topic_id: None,
2348                disable_notification: None,
2349                protect_content: None,
2350                remove_caption: None,
2351            },
2352        }
2353    }
2354    /// Forum topic thread ID.
2355    pub fn message_thread_id(mut self, id: i64) -> Self {
2356        self.params.message_thread_id = Some(id);
2357        self
2358    }
2359    /// Identifier of a direct messages chat topic.
2360    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2361        self.params.direct_messages_topic_id = Some(id);
2362        self
2363    }
2364    /// Sends the messages silently — recipients receive no notification sound.
2365    pub fn disable_notification(mut self, v: bool) -> Self {
2366        self.params.disable_notification = Some(v);
2367        self
2368    }
2369    /// Protects the messages from being forwarded or saved.
2370    pub fn protect_content(mut self, v: bool) -> Self {
2371        self.params.protect_content = Some(v);
2372        self
2373    }
2374    /// Copies the messages without their captions.
2375    pub fn remove_caption(mut self, v: bool) -> Self {
2376        self.params.remove_caption = Some(v);
2377        self
2378    }
2379}
2380
2381impl_into_future!(
2382    CopyMessages,
2383    Vec<rustigram_types::message::MessageId>,
2384    "copyMessages"
2385);
2386
2387// ─── sendVenue ────────────────────────────────────────────────────────────────
2388
2389#[derive(Serialize)]
2390struct SendVenueParams {
2391    chat_id: ChatId,
2392    latitude: f64,
2393    longitude: f64,
2394    title: String,
2395    address: String,
2396    #[serde(skip_serializing_if = "Option::is_none")]
2397    message_thread_id: Option<i64>,
2398    #[serde(skip_serializing_if = "Option::is_none")]
2399    direct_messages_topic_id: Option<i64>,
2400    #[serde(skip_serializing_if = "Option::is_none")]
2401    foursquare_id: Option<String>,
2402    #[serde(skip_serializing_if = "Option::is_none")]
2403    foursquare_type: Option<String>,
2404    #[serde(skip_serializing_if = "Option::is_none")]
2405    google_place_id: Option<String>,
2406    #[serde(skip_serializing_if = "Option::is_none")]
2407    google_place_type: Option<String>,
2408    #[serde(skip_serializing_if = "Option::is_none")]
2409    disable_notification: Option<bool>,
2410    #[serde(skip_serializing_if = "Option::is_none")]
2411    protect_content: Option<bool>,
2412    #[serde(skip_serializing_if = "Option::is_none")]
2413    reply_parameters: Option<ReplyParameters>,
2414    #[serde(skip_serializing_if = "Option::is_none")]
2415    reply_markup: Option<ReplyMarkup>,
2416    #[serde(skip_serializing_if = "Option::is_none")]
2417    receiver_user_id: Option<i64>,
2418    #[serde(skip_serializing_if = "Option::is_none")]
2419    callback_query_id: Option<String>,
2420    #[serde(skip_serializing_if = "Option::is_none")]
2421    business_connection_id: Option<String>,
2422    #[serde(skip_serializing_if = "Option::is_none")]
2423    allow_paid_broadcast: Option<bool>,
2424    #[serde(skip_serializing_if = "Option::is_none")]
2425    message_effect_id: Option<String>,
2426    #[serde(skip_serializing_if = "Option::is_none")]
2427    suggested_post_parameters: Option<SuggestedPostParameters>,
2428}
2429
2430/// Builder for the [`sendVenue`](https://core.telegram.org/bots/api#sendvenue) method.
2431pub struct SendVenue {
2432    client: BotClient,
2433    params: SendVenueParams,
2434}
2435
2436impl SendVenue {
2437    pub(crate) fn new(
2438        client: BotClient,
2439        chat_id: impl Into<ChatId>,
2440        latitude: f64,
2441        longitude: f64,
2442        title: impl Into<String>,
2443        address: impl Into<String>,
2444    ) -> Self {
2445        Self {
2446            client,
2447            params: SendVenueParams {
2448                chat_id: chat_id.into(),
2449                latitude,
2450                longitude,
2451                title: title.into(),
2452                address: address.into(),
2453                message_thread_id: None,
2454                direct_messages_topic_id: None,
2455                foursquare_id: None,
2456                foursquare_type: None,
2457                google_place_id: None,
2458                google_place_type: None,
2459                disable_notification: None,
2460                protect_content: None,
2461                reply_parameters: None,
2462                reply_markup: None,
2463                receiver_user_id: None,
2464                callback_query_id: None,
2465                business_connection_id: None,
2466                allow_paid_broadcast: None,
2467                message_effect_id: None,
2468                suggested_post_parameters: None,
2469            },
2470        }
2471    }
2472    /// Forum topic thread ID.
2473    pub fn message_thread_id(mut self, id: i64) -> Self {
2474        self.params.message_thread_id = Some(id);
2475        self
2476    }
2477    /// Identifier of a direct messages chat topic.
2478    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2479        self.params.direct_messages_topic_id = Some(id);
2480        self
2481    }
2482    /// Sets the Foursquare identifier of the venue.
2483    pub fn foursquare_id(mut self, id: impl Into<String>) -> Self {
2484        self.params.foursquare_id = Some(id.into());
2485        self
2486    }
2487    /// Sets the Foursquare type of the venue (e.g. `"arts_entertainment/aquarium"`).
2488    pub fn foursquare_type(mut self, t: impl Into<String>) -> Self {
2489        self.params.foursquare_type = Some(t.into());
2490        self
2491    }
2492    /// Sets the Google Places identifier of the venue.
2493    pub fn google_place_id(mut self, id: impl Into<String>) -> Self {
2494        self.params.google_place_id = Some(id.into());
2495        self
2496    }
2497    /// Sets the Google Places type of the venue.
2498    pub fn google_place_type(mut self, t: impl Into<String>) -> Self {
2499        self.params.google_place_type = Some(t.into());
2500        self
2501    }
2502    /// Sends the message silently — the recipient receives no notification sound.
2503    pub fn disable_notification(mut self, v: bool) -> Self {
2504        self.params.disable_notification = Some(v);
2505        self
2506    }
2507    /// Protects the message from being forwarded or saved.
2508    pub fn protect_content(mut self, v: bool) -> Self {
2509        self.params.protect_content = Some(v);
2510        self
2511    }
2512    /// Reply parameters for this message.
2513    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2514        self.params.reply_parameters = Some(rp);
2515        self
2516    }
2517    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
2518    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
2519        self.params.reply_markup = Some(m.into());
2520        self
2521    }
2522    /// For outgoing ephemeral messages — the user who will receive the message.
2523    pub fn receiver_user_id(mut self, id: i64) -> Self {
2524        self.params.receiver_user_id = Some(id);
2525        self
2526    }
2527    /// For outgoing ephemeral messages — the callback query that triggered it, if any.
2528    pub fn callback_query_id(mut self, id: impl Into<String>) -> Self {
2529        self.params.callback_query_id = Some(id.into());
2530        self
2531    }
2532    /// Business connection ID for acting on behalf of a business account.
2533    pub fn business_connection_id(mut self, v: impl Into<String>) -> Self {
2534        self.params.business_connection_id = Some(v.into());
2535        self
2536    }
2537    /// Allows sending to large audiences at the cost of Telegram Stars.
2538    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
2539        self.params.allow_paid_broadcast = Some(v);
2540        self
2541    }
2542    /// Attaches a message effect (animated emoji reaction) to the message.
2543    pub fn message_effect_id(mut self, v: impl Into<String>) -> Self {
2544        self.params.message_effect_id = Some(v.into());
2545        self
2546    }
2547    /// Suggested post parameters for channel direct messages chats.
2548    pub fn suggested_post_parameters(mut self, v: SuggestedPostParameters) -> Self {
2549        self.params.suggested_post_parameters = Some(v);
2550        self
2551    }
2552}
2553
2554impl_into_future!(SendVenue, Message, "sendVenue");
2555
2556// ─── sendMediaGroup ───────────────────────────────────────────────────────────
2557
2558#[derive(Serialize)]
2559struct SendMediaGroupParams {
2560    chat_id: ChatId,
2561    /// Array of `InputMedia` objects (photo, video, audio, or document).
2562    /// Pass the result of `serde_json::to_value(&your_input_media_vec)`.
2563    media: Vec<InputMedia>,
2564    #[serde(skip_serializing_if = "Option::is_none")]
2565    message_thread_id: Option<i64>,
2566    #[serde(skip_serializing_if = "Option::is_none")]
2567    direct_messages_topic_id: Option<i64>,
2568    #[serde(skip_serializing_if = "Option::is_none")]
2569    business_connection_id: Option<String>,
2570    #[serde(skip_serializing_if = "Option::is_none")]
2571    disable_notification: Option<bool>,
2572    #[serde(skip_serializing_if = "Option::is_none")]
2573    protect_content: Option<bool>,
2574    #[serde(skip_serializing_if = "Option::is_none")]
2575    reply_parameters: Option<ReplyParameters>,
2576    #[serde(skip_serializing_if = "Option::is_none")]
2577    allow_paid_broadcast: Option<bool>,
2578    #[serde(skip_serializing_if = "Option::is_none")]
2579    message_effect_id: Option<String>,
2580}
2581
2582/// Builder for the [`sendMediaGroup`](https://core.telegram.org/bots/api#sendmediagroup) method.
2583///
2584/// Sends a group of photos, videos, documents, or audios as an album (2–10 items).
2585///
2586pub struct SendMediaGroup {
2587    client: BotClient,
2588    params: SendMediaGroupParams,
2589}
2590
2591impl SendMediaGroup {
2592    pub(crate) fn new(
2593        client: BotClient,
2594        chat_id: impl Into<ChatId>,
2595        media: Vec<InputMedia>,
2596    ) -> Self {
2597        Self {
2598            client,
2599            params: SendMediaGroupParams {
2600                chat_id: chat_id.into(),
2601                media,
2602                message_thread_id: None,
2603                direct_messages_topic_id: None,
2604                business_connection_id: None,
2605                disable_notification: None,
2606                protect_content: None,
2607                reply_parameters: None,
2608                allow_paid_broadcast: None,
2609                message_effect_id: None,
2610            },
2611        }
2612    }
2613    /// Forum topic thread ID.
2614    pub fn message_thread_id(mut self, id: i64) -> Self {
2615        self.params.message_thread_id = Some(id);
2616        self
2617    }
2618    /// Identifier of a direct messages chat topic.
2619    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2620        self.params.direct_messages_topic_id = Some(id);
2621        self
2622    }
2623    /// Business connection ID for sending on behalf of a business account.
2624    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
2625        self.params.business_connection_id = Some(id.into());
2626        self
2627    }
2628    /// Sends the messages silently — recipients receive no notification sound.
2629    pub fn disable_notification(mut self, v: bool) -> Self {
2630        self.params.disable_notification = Some(v);
2631        self
2632    }
2633    /// Protects the messages from being forwarded or saved.
2634    pub fn protect_content(mut self, v: bool) -> Self {
2635        self.params.protect_content = Some(v);
2636        self
2637    }
2638    /// Reply parameters for this message.
2639    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2640        self.params.reply_parameters = Some(rp);
2641        self
2642    }
2643    /// Allows sending to large audiences at the cost of Telegram Stars.
2644    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
2645        self.params.allow_paid_broadcast = Some(v);
2646        self
2647    }
2648    /// Attaches a message effect (animated emoji reaction) to the message.
2649    pub fn message_effect_id(mut self, v: impl Into<String>) -> Self {
2650        self.params.message_effect_id = Some(v.into());
2651        self
2652    }
2653}
2654
2655impl_into_future!(SendMediaGroup, Vec<Message>, "sendMediaGroup");
2656
2657// ─── sendPaidMedia ────────────────────────────────────────────────────────────
2658
2659#[derive(Serialize)]
2660struct SendPaidMediaParams {
2661    chat_id: ChatId,
2662    star_count: u32,
2663    /// Array of `InputPaidMedia` objects (photo or video).
2664    /// Pass the result of `serde_json::to_value(&your_paid_media_vec)`.
2665    media: Vec<InputPaidMedia>,
2666    #[serde(skip_serializing_if = "Option::is_none")]
2667    business_connection_id: Option<String>,
2668    #[serde(skip_serializing_if = "Option::is_none")]
2669    payload: Option<String>,
2670    #[serde(skip_serializing_if = "Option::is_none")]
2671    caption: Option<String>,
2672    #[serde(skip_serializing_if = "Option::is_none")]
2673    parse_mode: Option<ParseMode>,
2674    #[serde(skip_serializing_if = "Option::is_none")]
2675    show_caption_above_media: Option<bool>,
2676    #[serde(skip_serializing_if = "Option::is_none")]
2677    disable_notification: Option<bool>,
2678    #[serde(skip_serializing_if = "Option::is_none")]
2679    protect_content: Option<bool>,
2680    #[serde(skip_serializing_if = "Option::is_none")]
2681    reply_parameters: Option<ReplyParameters>,
2682    #[serde(skip_serializing_if = "Option::is_none")]
2683    reply_markup: Option<ReplyMarkup>,
2684    #[serde(skip_serializing_if = "Option::is_none")]
2685    allow_paid_broadcast: Option<bool>,
2686    #[serde(skip_serializing_if = "Option::is_none")]
2687    caption_entities: Option<Vec<rustigram_types::message::MessageEntity>>,
2688    #[serde(skip_serializing_if = "Option::is_none")]
2689    direct_messages_topic_id: Option<i64>,
2690    #[serde(skip_serializing_if = "Option::is_none")]
2691    message_thread_id: Option<i64>,
2692    #[serde(skip_serializing_if = "Option::is_none")]
2693    suggested_post_parameters: Option<SuggestedPostParameters>,
2694}
2695
2696/// Builder for the [`sendPaidMedia`](https://core.telegram.org/bots/api#sendpaidmedia) method.
2697///
2698/// Sends paid media that users must pay Telegram Stars to view (up to 10 items).
2699///
2700pub struct SendPaidMedia {
2701    client: BotClient,
2702    params: SendPaidMediaParams,
2703}
2704
2705impl SendPaidMedia {
2706    pub(crate) fn new(
2707        client: BotClient,
2708        chat_id: impl Into<ChatId>,
2709        star_count: u32,
2710        media: Vec<InputPaidMedia>,
2711    ) -> Self {
2712        Self {
2713            client,
2714            params: SendPaidMediaParams {
2715                chat_id: chat_id.into(),
2716                star_count,
2717                media,
2718                business_connection_id: None,
2719                payload: None,
2720                caption: None,
2721                parse_mode: None,
2722                show_caption_above_media: None,
2723                disable_notification: None,
2724                protect_content: None,
2725                reply_parameters: None,
2726                reply_markup: None,
2727                allow_paid_broadcast: None,
2728                caption_entities: None,
2729                direct_messages_topic_id: None,
2730                message_thread_id: None,
2731                suggested_post_parameters: None,
2732            },
2733        }
2734    }
2735    /// Business connection ID for sending on behalf of a business account.
2736    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
2737        self.params.business_connection_id = Some(id.into());
2738        self
2739    }
2740    /// Bot-defined paid media payload (0–128 bytes); not shown to the user.
2741    pub fn payload(mut self, p: impl Into<String>) -> Self {
2742        self.params.payload = Some(p.into());
2743        self
2744    }
2745    /// Sets the caption (0–1024 characters).
2746    pub fn caption(mut self, c: impl Into<String>) -> Self {
2747        self.params.caption = Some(c.into());
2748        self
2749    }
2750    /// Sets the caption parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
2751    pub fn parse_mode(mut self, m: ParseMode) -> Self {
2752        self.params.parse_mode = Some(m);
2753        self
2754    }
2755    /// Shows the caption above the media instead of below it.
2756    pub fn show_caption_above_media(mut self, v: bool) -> Self {
2757        self.params.show_caption_above_media = Some(v);
2758        self
2759    }
2760    /// Sends the message silently — the recipient receives no notification sound.
2761    pub fn disable_notification(mut self, v: bool) -> Self {
2762        self.params.disable_notification = Some(v);
2763        self
2764    }
2765    /// Protects the message from being forwarded or saved.
2766    pub fn protect_content(mut self, v: bool) -> Self {
2767        self.params.protect_content = Some(v);
2768        self
2769    }
2770    /// Reply parameters for this message.
2771    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2772        self.params.reply_parameters = Some(rp);
2773        self
2774    }
2775    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
2776    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
2777        self.params.reply_markup = Some(m.into());
2778        self
2779    }
2780    /// Allows sending to large audiences at the cost of Telegram Stars.
2781    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
2782        self.params.allow_paid_broadcast = Some(v);
2783        self
2784    }
2785    /// Special entities in the caption, in place of `parse_mode`.
2786    pub fn caption_entities(mut self, v: Vec<rustigram_types::message::MessageEntity>) -> Self {
2787        self.params.caption_entities = Some(v);
2788        self
2789    }
2790    /// Identifier of a direct messages chat topic.
2791    pub fn direct_messages_topic_id(mut self, v: i64) -> Self {
2792        self.params.direct_messages_topic_id = Some(v);
2793        self
2794    }
2795    /// Forum topic thread ID.
2796    pub fn message_thread_id(mut self, v: i64) -> Self {
2797        self.params.message_thread_id = Some(v);
2798        self
2799    }
2800    /// Suggested post parameters for channel direct messages chats.
2801    pub fn suggested_post_parameters(mut self, v: SuggestedPostParameters) -> Self {
2802        self.params.suggested_post_parameters = Some(v);
2803        self
2804    }
2805}
2806
2807impl_into_future!(SendPaidMedia, Message, "sendPaidMedia");
2808
2809// ─── sendGame ─────────────────────────────────────────────────────────────────
2810
2811#[derive(Serialize)]
2812struct SendGameParams {
2813    chat_id: i64,
2814    game_short_name: String,
2815    #[serde(skip_serializing_if = "Option::is_none")]
2816    business_connection_id: Option<String>,
2817    #[serde(skip_serializing_if = "Option::is_none")]
2818    message_thread_id: Option<i64>,
2819    #[serde(skip_serializing_if = "Option::is_none")]
2820    direct_messages_topic_id: Option<i64>,
2821    #[serde(skip_serializing_if = "Option::is_none")]
2822    disable_notification: Option<bool>,
2823    #[serde(skip_serializing_if = "Option::is_none")]
2824    protect_content: Option<bool>,
2825    #[serde(skip_serializing_if = "Option::is_none")]
2826    reply_parameters: Option<ReplyParameters>,
2827    #[serde(skip_serializing_if = "Option::is_none")]
2828    reply_markup: Option<rustigram_types::keyboard::InlineKeyboardMarkup>,
2829    #[serde(skip_serializing_if = "Option::is_none")]
2830    allow_paid_broadcast: Option<bool>,
2831    #[serde(skip_serializing_if = "Option::is_none")]
2832    message_effect_id: Option<String>,
2833}
2834
2835/// Builder for the [`sendGame`](https://core.telegram.org/bots/api#sendgame) method.
2836///
2837/// Note: `chat_id` is an integer — games can't be sent to channel direct messages
2838/// chats or channel chats.
2839pub struct SendGame {
2840    client: BotClient,
2841    params: SendGameParams,
2842}
2843
2844impl SendGame {
2845    pub(crate) fn new(client: BotClient, chat_id: i64, game_short_name: impl Into<String>) -> Self {
2846        Self {
2847            client,
2848            params: SendGameParams {
2849                chat_id,
2850                game_short_name: game_short_name.into(),
2851                business_connection_id: None,
2852                message_thread_id: None,
2853                direct_messages_topic_id: None,
2854                disable_notification: None,
2855                protect_content: None,
2856                reply_parameters: None,
2857                reply_markup: None,
2858                allow_paid_broadcast: None,
2859                message_effect_id: None,
2860            },
2861        }
2862    }
2863    /// Business connection ID for sending on behalf of a business account.
2864    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
2865        self.params.business_connection_id = Some(id.into());
2866        self
2867    }
2868    /// Forum topic thread ID.
2869    pub fn message_thread_id(mut self, id: i64) -> Self {
2870        self.params.message_thread_id = Some(id);
2871        self
2872    }
2873    /// Identifier of a direct messages chat topic.
2874    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2875        self.params.direct_messages_topic_id = Some(id);
2876        self
2877    }
2878    /// Sends the message silently — the recipient receives no notification sound.
2879    pub fn disable_notification(mut self, v: bool) -> Self {
2880        self.params.disable_notification = Some(v);
2881        self
2882    }
2883    /// Protects the message from being forwarded or saved.
2884    pub fn protect_content(mut self, v: bool) -> Self {
2885        self.params.protect_content = Some(v);
2886        self
2887    }
2888    /// Reply parameters for this message.
2889    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2890        self.params.reply_parameters = Some(rp);
2891        self
2892    }
2893    /// Attaches an inline keyboard. The first button must launch the game.
2894    pub fn reply_markup(mut self, m: rustigram_types::keyboard::InlineKeyboardMarkup) -> Self {
2895        self.params.reply_markup = Some(m);
2896        self
2897    }
2898    /// Allows sending to large audiences at the cost of Telegram Stars.
2899    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
2900        self.params.allow_paid_broadcast = Some(v);
2901        self
2902    }
2903    /// Attaches a message effect (animated emoji reaction) to the message.
2904    pub fn message_effect_id(mut self, v: impl Into<String>) -> Self {
2905        self.params.message_effect_id = Some(v.into());
2906        self
2907    }
2908}
2909
2910impl_into_future!(SendGame, Message, "sendGame");
2911
2912// ─── sendChecklist ────────────────────────────────────────────────────────────
2913
2914#[derive(Serialize)]
2915struct SendChecklistParams {
2916    business_connection_id: String,
2917    chat_id: i64,
2918    checklist: rustigram_types::checklist::InputChecklist,
2919    #[serde(skip_serializing_if = "Option::is_none")]
2920    direct_messages_topic_id: Option<i64>,
2921    #[serde(skip_serializing_if = "Option::is_none")]
2922    disable_notification: Option<bool>,
2923    #[serde(skip_serializing_if = "Option::is_none")]
2924    protect_content: Option<bool>,
2925    #[serde(skip_serializing_if = "Option::is_none")]
2926    message_effect_id: Option<String>,
2927    #[serde(skip_serializing_if = "Option::is_none")]
2928    reply_parameters: Option<ReplyParameters>,
2929    #[serde(skip_serializing_if = "Option::is_none")]
2930    reply_markup: Option<rustigram_types::keyboard::InlineKeyboardMarkup>,
2931    #[serde(skip_serializing_if = "Option::is_none")]
2932    suggested_post_parameters: Option<SuggestedPostParameters>,
2933}
2934
2935/// Builder for the [`sendChecklist`](https://core.telegram.org/bots/api#sendchecklist) method.
2936///
2937/// Business bots only — sends a checklist on behalf of a connected business account.
2938/// Requires the `can_reply` business bot right.
2939pub struct SendChecklist {
2940    client: BotClient,
2941    params: SendChecklistParams,
2942}
2943
2944impl SendChecklist {
2945    pub(crate) fn new(
2946        client: BotClient,
2947        business_connection_id: impl Into<String>,
2948        chat_id: i64,
2949        checklist: rustigram_types::checklist::InputChecklist,
2950    ) -> Self {
2951        Self {
2952            client,
2953            params: SendChecklistParams {
2954                business_connection_id: business_connection_id.into(),
2955                chat_id,
2956                checklist,
2957                direct_messages_topic_id: None,
2958                disable_notification: None,
2959                protect_content: None,
2960                message_effect_id: None,
2961                reply_parameters: None,
2962                reply_markup: None,
2963                suggested_post_parameters: None,
2964            },
2965        }
2966    }
2967    /// Identifier of a direct messages chat topic.
2968    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2969        self.params.direct_messages_topic_id = Some(id);
2970        self
2971    }
2972    /// Sends the message silently — the recipient receives no notification sound.
2973    pub fn disable_notification(mut self, v: bool) -> Self {
2974        self.params.disable_notification = Some(v);
2975        self
2976    }
2977    /// Protects the message from being forwarded or saved.
2978    pub fn protect_content(mut self, v: bool) -> Self {
2979        self.params.protect_content = Some(v);
2980        self
2981    }
2982    /// Unique identifier of the message effect to add to the message.
2983    pub fn message_effect_id(mut self, id: impl Into<String>) -> Self {
2984        self.params.message_effect_id = Some(id.into());
2985        self
2986    }
2987    /// Reply parameters for this message.
2988    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2989        self.params.reply_parameters = Some(rp);
2990        self
2991    }
2992    /// Attaches an inline keyboard to the message.
2993    pub fn reply_markup(mut self, m: rustigram_types::keyboard::InlineKeyboardMarkup) -> Self {
2994        self.params.reply_markup = Some(m);
2995        self
2996    }
2997    /// Suggested post parameters for channel direct messages chats.
2998    pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self {
2999        self.params.suggested_post_parameters = Some(params);
3000        self
3001    }
3002}
3003
3004impl_into_future!(SendChecklist, Message, "sendChecklist");
3005
3006// ─── sendRichMessage ──────────────────────────────────────────────────────────
3007
3008#[derive(Serialize)]
3009struct SendRichMessageParams {
3010    chat_id: ChatId,
3011    rich_message: rustigram_types::rich_message::InputRichMessage,
3012    #[serde(skip_serializing_if = "Option::is_none")]
3013    business_connection_id: Option<String>,
3014    #[serde(skip_serializing_if = "Option::is_none")]
3015    message_thread_id: Option<i64>,
3016    #[serde(skip_serializing_if = "Option::is_none")]
3017    direct_messages_topic_id: Option<i64>,
3018    #[serde(skip_serializing_if = "Option::is_none")]
3019    disable_notification: Option<bool>,
3020    #[serde(skip_serializing_if = "Option::is_none")]
3021    protect_content: Option<bool>,
3022    #[serde(skip_serializing_if = "Option::is_none")]
3023    allow_paid_broadcast: Option<bool>,
3024    #[serde(skip_serializing_if = "Option::is_none")]
3025    message_effect_id: Option<String>,
3026    #[serde(skip_serializing_if = "Option::is_none")]
3027    suggested_post_parameters: Option<SuggestedPostParameters>,
3028    #[serde(skip_serializing_if = "Option::is_none")]
3029    reply_parameters: Option<ReplyParameters>,
3030    #[serde(skip_serializing_if = "Option::is_none")]
3031    reply_markup: Option<rustigram_types::keyboard::ReplyMarkup>,
3032}
3033
3034/// Builder for the [`sendRichMessage`](https://core.telegram.org/bots/api#sendrichmessage) method.
3035pub struct SendRichMessage {
3036    client: BotClient,
3037    params: SendRichMessageParams,
3038}
3039
3040impl SendRichMessage {
3041    pub(crate) fn new(
3042        client: BotClient,
3043        chat_id: impl Into<ChatId>,
3044        rich_message: rustigram_types::rich_message::InputRichMessage,
3045    ) -> Self {
3046        Self {
3047            client,
3048            params: SendRichMessageParams {
3049                chat_id: chat_id.into(),
3050                rich_message,
3051                business_connection_id: None,
3052                message_thread_id: None,
3053                direct_messages_topic_id: None,
3054                disable_notification: None,
3055                protect_content: None,
3056                allow_paid_broadcast: None,
3057                message_effect_id: None,
3058                suggested_post_parameters: None,
3059                reply_parameters: None,
3060                reply_markup: None,
3061            },
3062        }
3063    }
3064
3065    /// Sets the business connection identifier.
3066    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
3067        self.params.business_connection_id = Some(id.into());
3068        self
3069    }
3070    /// Sends the message to the specified topic thread.
3071    pub fn message_thread_id(mut self, id: i64) -> Self {
3072        self.params.message_thread_id = Some(id);
3073        self
3074    }
3075    /// Sends the message to the specified direct messages topic.
3076    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
3077        self.params.direct_messages_topic_id = Some(id);
3078        self
3079    }
3080    /// Sends the message silently (no notification sound).
3081    pub fn disable_notification(mut self, v: bool) -> Self {
3082        self.params.disable_notification = Some(v);
3083        self
3084    }
3085    /// Protects the message from being forwarded or saved.
3086    pub fn protect_content(mut self, v: bool) -> Self {
3087        self.params.protect_content = Some(v);
3088        self
3089    }
3090    /// Allows up to 1 000 messages per second by paying 0.1 Stars per message.
3091    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
3092        self.params.allow_paid_broadcast = Some(v);
3093        self
3094    }
3095    /// Unique identifier of the message effect to add to the message.
3096    pub fn message_effect_id(mut self, id: impl Into<String>) -> Self {
3097        self.params.message_effect_id = Some(id.into());
3098        self
3099    }
3100    /// Suggested post parameters for channel direct messages chats.
3101    pub fn suggested_post_parameters(mut self, p: SuggestedPostParameters) -> Self {
3102        self.params.suggested_post_parameters = Some(p);
3103        self
3104    }
3105    /// Reply parameters for this message.
3106    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
3107        self.params.reply_parameters = Some(rp);
3108        self
3109    }
3110    /// Attaches a reply markup to the message.
3111    pub fn reply_markup(mut self, m: rustigram_types::keyboard::ReplyMarkup) -> Self {
3112        self.params.reply_markup = Some(m);
3113        self
3114    }
3115}
3116
3117impl_into_future!(SendRichMessage, Message, "sendRichMessage");
3118
3119// ─── sendRichMessageDraft ─────────────────────────────────────────────────────
3120
3121#[derive(Serialize)]
3122struct SendRichMessageDraftParams {
3123    chat_id: i64,
3124    draft_id: i64,
3125    rich_message: rustigram_types::rich_message::InputRichMessage,
3126    #[serde(skip_serializing_if = "Option::is_none")]
3127    message_thread_id: Option<i64>,
3128}
3129
3130/// Builder for the [`sendRichMessageDraft`](https://core.telegram.org/bots/api#sendrichmessagedraft) method.
3131///
3132/// Streams a partial rich message as a 30-second ephemeral preview.
3133/// Once generation is complete, call [`SendRichMessage`] with the full content to persist it.
3134pub struct SendRichMessageDraft {
3135    client: BotClient,
3136    params: SendRichMessageDraftParams,
3137}
3138
3139impl SendRichMessageDraft {
3140    pub(crate) fn new(
3141        client: BotClient,
3142        chat_id: i64,
3143        draft_id: i64,
3144        rich_message: rustigram_types::rich_message::InputRichMessage,
3145    ) -> Self {
3146        Self {
3147            client,
3148            params: SendRichMessageDraftParams {
3149                chat_id,
3150                draft_id,
3151                rich_message,
3152                message_thread_id: None,
3153            },
3154        }
3155    }
3156
3157    /// Sends the draft to the specified topic thread.
3158    pub fn message_thread_id(mut self, id: i64) -> Self {
3159        self.params.message_thread_id = Some(id);
3160        self
3161    }
3162}
3163
3164impl_into_future!(SendRichMessageDraft, bool, "sendRichMessageDraft");
3165
3166#[cfg(test)]
3167mod tests {
3168    use super::*;
3169    use crate::client::BotClient;
3170
3171    fn client() -> BotClient {
3172        BotClient::from_token("123456:test-token-for-unit-tests").unwrap()
3173    }
3174
3175    /// The four parameters Telegram added API-wide had only ever been applied to
3176    /// `send_message`; these are the siblings that were missing them.
3177    #[test]
3178    fn api_wide_parameters_serialize_on_the_json_path() {
3179        let contact = SendContact::new(client(), 1_i64, "+100", "A")
3180            .business_connection_id("biz")
3181            .allow_paid_broadcast(true)
3182            .message_effect_id("effect")
3183            .suggested_post_parameters(SuggestedPostParameters {
3184                price: None,
3185                send_date: Some(1_700_000_000),
3186            });
3187        let json = serde_json::to_value(&contact.params).unwrap();
3188
3189        assert_eq!(json["business_connection_id"], "biz");
3190        assert_eq!(json["allow_paid_broadcast"], true);
3191        assert_eq!(json["message_effect_id"], "effect");
3192        assert!(json.get("suggested_post_parameters").is_some());
3193    }
3194
3195    /// Unset optional parameters must stay off the wire entirely.
3196    #[test]
3197    fn unset_parameters_are_omitted() {
3198        let dice = SendDice::new(client(), 1_i64);
3199        let json = serde_json::to_value(&dice.params).unwrap();
3200        for key in [
3201            "business_connection_id",
3202            "allow_paid_broadcast",
3203            "message_effect_id",
3204            "suggested_post_parameters",
3205        ] {
3206            assert!(
3207                json.get(key).is_none(),
3208                "{key} should be omitted when unset"
3209            );
3210        }
3211    }
3212
3213    /// The spec marks this optional; it used to be a bare `String`, so a draft
3214    /// carrying only media could not be expressed.
3215    #[test]
3216    fn message_draft_text_can_be_cleared() {
3217        let draft = SendMessageDraft::new(client(), 1_i64, 7, "hello");
3218        assert_eq!(
3219            serde_json::to_value(&draft.params).unwrap()["text"],
3220            "hello"
3221        );
3222
3223        let empty = SendMessageDraft::new(client(), 1_i64, 7, "hello").clear_text();
3224        assert!(serde_json::to_value(&empty.params)
3225            .unwrap()
3226            .get("text")
3227            .is_none());
3228    }
3229
3230    /// Regression guard for the multipart path. Both send paths must enumerate
3231    /// the same options struct: the hand-written form used to drop five fields,
3232    /// so `.reply_to(..)` on an uploaded photo silently never reached Telegram.
3233    #[test]
3234    fn multipart_and_json_paths_cover_the_same_options() {
3235        let source = include_str!("sending.rs");
3236        let struct_body = source
3237            .split("pub struct MediaSendOptions {")
3238            .nth(1)
3239            .and_then(|s| s.split("\n}").next())
3240            .expect("MediaSendOptions struct");
3241        let fields: Vec<&str> = struct_body
3242            .lines()
3243            .filter_map(|l| l.trim().strip_prefix("pub "))
3244            .filter_map(|l| l.split(':').next())
3245            .collect();
3246        assert_eq!(
3247            fields.len(),
3248            17,
3249            "field count changed; update both send paths"
3250        );
3251
3252        // Both helpers, not one. This guard previously read only
3253        // `apply_media_opts` while its name claimed both paths, and
3254        // `message_effect_id` was duly added to the multipart form and left out
3255        // of the JSON body — a settable option that never reached Telegram on
3256        // every send by file_id or URL.
3257        for (helper, path) in [
3258            ("fn apply_media_opts(", "multipart form"),
3259            ("fn media_json_body(", "JSON body"),
3260        ] {
3261            let body = source
3262                .split(helper)
3263                .nth(1)
3264                .and_then(|s| s.split("\nfn ").next())
3265                .unwrap_or_else(|| panic!("{helper} body"));
3266            for field in &fields {
3267                assert!(
3268                    body.contains(&format!("opts.{field}")),
3269                    "`{field}` is settable but never written to the {path}"
3270                );
3271            }
3272        }
3273    }
3274}