Skip to main content

rustigram_api/methods/
sending.rs

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