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(v) = self.opts.disable_notification { form = form.text("disable_notification", v.to_string()); }
1574                            if let Some(v) = &self.opts.reply_markup { form = form.text("reply_markup", serde_json::to_string(v).unwrap()); }
1575                            if let Some(p) = &self.opts.suggested_post_parameters { form = form.text("suggested_post_parameters", serde_json::to_string(p).unwrap()); }
1576
1577                            $(
1578                                if let Some(ref v) = self.$extra_field {
1579                                    form = form.text(stringify!($extra_field), v.to_string());
1580                                }
1581                            )*
1582
1583                            self.client.post_multipart($method, form).await
1584                        }
1585                        _ => {
1586                            let mut extra = serde_json::json!({});
1587                            $(
1588                                if let Some(ref v) = self.$extra_field {
1589                                    extra[stringify!($extra_field)] = serde_json::json!(v);
1590                                }
1591                            )*
1592                            let body = media_json_body(&self.chat_id, $field, self.file.as_str(), &self.opts, extra);
1593                            self.client.post_json($method, &body).await
1594                        }
1595                    }
1596                })
1597            }
1598        }
1599    };
1600}
1601
1602media_sender!(
1603    /// Builder for the [`sendAudio`](https://core.telegram.org/bots/api#sendaudio) method.
1604    SendAudio,      "audio",      "sendAudio",      Message, [duration: u32, performer: String, title: String]);
1605media_sender!(
1606    /// Builder for the [`sendDocument`](https://core.telegram.org/bots/api#senddocument) method.
1607    SendDocument,  "document",   "sendDocument",  Message, [disable_content_type_detection: bool]);
1608media_sender!(
1609    /// Builder for the [`sendVideo`](https://core.telegram.org/bots/api#sendvideo) method.
1610    SendVideo,      "video",      "sendVideo",      Message, [duration: u32, width: u32, height: u32, supports_streaming: bool, cover: String, start_timestamp: i64]);
1611media_sender!(
1612    /// Builder for the [`sendAnimation`](https://core.telegram.org/bots/api#sendanimation) method.
1613    SendAnimation, "animation",  "sendAnimation", Message, [duration: u32, width: u32, height: u32]);
1614media_sender!(
1615    /// Builder for the [`sendVoice`](https://core.telegram.org/bots/api#sendvoice) method.
1616    SendVoice,      "voice",      "sendVoice",      Message, [duration: u32]);
1617media_sender!(
1618    /// Builder for the [`sendVideoNote`](https://core.telegram.org/bots/api#sendvideonote) method.
1619    SendVideoNote, "video_note", "sendVideoNote", Message, [duration: u32, length: u32]);
1620media_sender!(
1621    /// Builder for the [`sendSticker`](https://core.telegram.org/bots/api#sendsticker) method.
1622    SendSticker,   "sticker",    "sendSticker",    Message, [emoji: String]);
1623
1624// ─── deleteMessage / deleteMessages ──────────────────────────────────────────
1625
1626#[derive(Serialize)]
1627struct DeleteMessageParams {
1628    chat_id: ChatId,
1629    message_id: i64,
1630}
1631
1632/// Builder for the [`deleteMessage`](https://core.telegram.org/bots/api#deletemessage) method.
1633pub struct DeleteMessage {
1634    client: BotClient,
1635    params: DeleteMessageParams,
1636}
1637impl DeleteMessage {
1638    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, message_id: i64) -> Self {
1639        Self {
1640            client,
1641            params: DeleteMessageParams {
1642                chat_id: chat_id.into(),
1643                message_id,
1644            },
1645        }
1646    }
1647}
1648impl_into_future!(DeleteMessage, bool, "deleteMessage");
1649
1650#[derive(Serialize)]
1651struct DeleteMessagesParams {
1652    chat_id: ChatId,
1653    message_ids: Vec<i64>,
1654}
1655
1656/// Builder for the [`deleteMessages`](https://core.telegram.org/bots/api#deletemessages) method.
1657pub struct DeleteMessages {
1658    client: BotClient,
1659    params: DeleteMessagesParams,
1660}
1661impl DeleteMessages {
1662    pub(crate) fn new(
1663        client: BotClient,
1664        chat_id: impl Into<ChatId>,
1665        message_ids: Vec<i64>,
1666    ) -> Self {
1667        Self {
1668            client,
1669            params: DeleteMessagesParams {
1670                chat_id: chat_id.into(),
1671                message_ids,
1672            },
1673        }
1674    }
1675}
1676impl_into_future!(DeleteMessages, bool, "deleteMessages");
1677
1678// ─── stopPoll ─────────────────────────────────────────────────────────────────
1679
1680#[derive(Serialize)]
1681struct StopPollParams {
1682    chat_id: ChatId,
1683    message_id: i64,
1684    #[serde(skip_serializing_if = "Option::is_none")]
1685    reply_markup: Option<rustigram_types::keyboard::InlineKeyboardMarkup>,
1686}
1687
1688/// Builder for the [`stopPoll`](https://core.telegram.org/bots/api#stoppoll) method.
1689pub struct StopPoll {
1690    client: BotClient,
1691    params: StopPollParams,
1692}
1693impl StopPoll {
1694    pub(crate) fn new(client: BotClient, chat_id: impl Into<ChatId>, message_id: i64) -> Self {
1695        Self {
1696            client,
1697            params: StopPollParams {
1698                chat_id: chat_id.into(),
1699                message_id,
1700                reply_markup: None,
1701            },
1702        }
1703    }
1704    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
1705    pub fn reply_markup(mut self, m: rustigram_types::keyboard::InlineKeyboardMarkup) -> Self {
1706        self.params.reply_markup = Some(m);
1707        self
1708    }
1709}
1710impl_into_future!(StopPoll, rustigram_types::poll::Poll, "stopPoll");
1711
1712// ─── answerCallbackQuery ──────────────────────────────────────────────────────
1713
1714#[derive(Serialize)]
1715struct AnswerCallbackQueryParams {
1716    callback_query_id: String,
1717    #[serde(skip_serializing_if = "Option::is_none")]
1718    text: Option<String>,
1719    #[serde(skip_serializing_if = "Option::is_none")]
1720    show_alert: Option<bool>,
1721    #[serde(skip_serializing_if = "Option::is_none")]
1722    url: Option<String>,
1723    #[serde(skip_serializing_if = "Option::is_none")]
1724    cache_time: Option<u32>,
1725}
1726
1727/// Builder for the [`answerCallbackQuery`](https://core.telegram.org/bots/api#answercallbackquery) method.
1728pub struct AnswerCallbackQuery {
1729    client: BotClient,
1730    params: AnswerCallbackQueryParams,
1731}
1732impl AnswerCallbackQuery {
1733    pub(crate) fn new(client: BotClient, callback_query_id: impl Into<String>) -> Self {
1734        Self {
1735            client,
1736            params: AnswerCallbackQueryParams {
1737                callback_query_id: callback_query_id.into(),
1738                text: None,
1739                show_alert: None,
1740                url: None,
1741                cache_time: None,
1742            },
1743        }
1744    }
1745    /// The text of the notification shown to the user. 0–200 characters.
1746    pub fn text(mut self, t: impl Into<String>) -> Self {
1747        self.params.text = Some(t.into());
1748        self
1749    }
1750    /// Shows an alert dialog instead of a toast notification for the callback answer.
1751    pub fn show_alert(mut self, v: bool) -> Self {
1752        self.params.show_alert = Some(v);
1753        self
1754    }
1755    /// Sets the URL to open when the callback button answer is tapped.
1756    pub fn url(mut self, u: impl Into<String>) -> Self {
1757        self.params.url = Some(u.into());
1758        self
1759    }
1760    /// Sets how long the callback answer may be cached on the client in seconds.
1761    pub fn cache_time(mut self, secs: u32) -> Self {
1762        self.params.cache_time = Some(secs);
1763        self
1764    }
1765    /// Shorthand for `.text(t).show_alert(true)` — shows a popup alert to the user.
1766    pub fn alert(self, text: impl Into<String>) -> Self {
1767        self.text(text).show_alert(true)
1768    }
1769}
1770impl_into_future!(AnswerCallbackQuery, bool, "answerCallbackQuery");
1771// ─── forwardMessages ──────────────────────────────────────────────────────────
1772
1773#[derive(Serialize)]
1774struct ForwardMessagesParams {
1775    chat_id: ChatId,
1776    from_chat_id: ChatId,
1777    message_ids: Vec<i64>,
1778    #[serde(skip_serializing_if = "Option::is_none")]
1779    message_thread_id: Option<i64>,
1780    #[serde(skip_serializing_if = "Option::is_none")]
1781    direct_messages_topic_id: Option<i64>,
1782    #[serde(skip_serializing_if = "Option::is_none")]
1783    disable_notification: Option<bool>,
1784    #[serde(skip_serializing_if = "Option::is_none")]
1785    protect_content: Option<bool>,
1786}
1787
1788/// Builder for the [`forwardMessages`](https://core.telegram.org/bots/api#forwardmessages) method.
1789///
1790/// Forwards 1–100 messages at once, preserving album grouping.
1791/// Returns a `Vec<MessageId>` of the sent messages.
1792pub struct ForwardMessages {
1793    client: BotClient,
1794    params: ForwardMessagesParams,
1795}
1796
1797impl ForwardMessages {
1798    pub(crate) fn new(
1799        client: BotClient,
1800        chat_id: impl Into<ChatId>,
1801        from_chat_id: impl Into<ChatId>,
1802        message_ids: Vec<i64>,
1803    ) -> Self {
1804        Self {
1805            client,
1806            params: ForwardMessagesParams {
1807                chat_id: chat_id.into(),
1808                from_chat_id: from_chat_id.into(),
1809                message_ids,
1810                message_thread_id: None,
1811                direct_messages_topic_id: None,
1812                disable_notification: None,
1813                protect_content: None,
1814            },
1815        }
1816    }
1817    /// Forum topic thread ID.
1818    pub fn message_thread_id(mut self, id: i64) -> Self {
1819        self.params.message_thread_id = Some(id);
1820        self
1821    }
1822    /// Identifier of a direct messages chat topic.
1823    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1824        self.params.direct_messages_topic_id = Some(id);
1825        self
1826    }
1827    /// Sends the messages silently — recipients receive no notification sound.
1828    pub fn disable_notification(mut self, v: bool) -> Self {
1829        self.params.disable_notification = Some(v);
1830        self
1831    }
1832    /// Protects the messages from being forwarded or saved.
1833    pub fn protect_content(mut self, v: bool) -> Self {
1834        self.params.protect_content = Some(v);
1835        self
1836    }
1837}
1838
1839impl_into_future!(
1840    ForwardMessages,
1841    Vec<rustigram_types::message::MessageId>,
1842    "forwardMessages"
1843);
1844
1845// ─── copyMessages ─────────────────────────────────────────────────────────────
1846
1847#[derive(Serialize)]
1848struct CopyMessagesParams {
1849    chat_id: ChatId,
1850    from_chat_id: ChatId,
1851    message_ids: Vec<i64>,
1852    #[serde(skip_serializing_if = "Option::is_none")]
1853    message_thread_id: Option<i64>,
1854    #[serde(skip_serializing_if = "Option::is_none")]
1855    direct_messages_topic_id: Option<i64>,
1856    #[serde(skip_serializing_if = "Option::is_none")]
1857    disable_notification: Option<bool>,
1858    #[serde(skip_serializing_if = "Option::is_none")]
1859    protect_content: Option<bool>,
1860    #[serde(skip_serializing_if = "Option::is_none")]
1861    remove_caption: Option<bool>,
1862}
1863
1864/// Builder for the [`copyMessages`](https://core.telegram.org/bots/api#copymessages) method.
1865///
1866/// Copies 1–100 messages without a forward link, preserving album grouping.
1867/// Returns a `Vec<MessageId>` of the sent messages.
1868pub struct CopyMessages {
1869    client: BotClient,
1870    params: CopyMessagesParams,
1871}
1872
1873impl CopyMessages {
1874    pub(crate) fn new(
1875        client: BotClient,
1876        chat_id: impl Into<ChatId>,
1877        from_chat_id: impl Into<ChatId>,
1878        message_ids: Vec<i64>,
1879    ) -> Self {
1880        Self {
1881            client,
1882            params: CopyMessagesParams {
1883                chat_id: chat_id.into(),
1884                from_chat_id: from_chat_id.into(),
1885                message_ids,
1886                message_thread_id: None,
1887                direct_messages_topic_id: None,
1888                disable_notification: None,
1889                protect_content: None,
1890                remove_caption: None,
1891            },
1892        }
1893    }
1894    /// Forum topic thread ID.
1895    pub fn message_thread_id(mut self, id: i64) -> Self {
1896        self.params.message_thread_id = Some(id);
1897        self
1898    }
1899    /// Identifier of a direct messages chat topic.
1900    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
1901        self.params.direct_messages_topic_id = Some(id);
1902        self
1903    }
1904    /// Sends the messages silently — recipients receive no notification sound.
1905    pub fn disable_notification(mut self, v: bool) -> Self {
1906        self.params.disable_notification = Some(v);
1907        self
1908    }
1909    /// Protects the messages from being forwarded or saved.
1910    pub fn protect_content(mut self, v: bool) -> Self {
1911        self.params.protect_content = Some(v);
1912        self
1913    }
1914    /// Copies the messages without their captions.
1915    pub fn remove_caption(mut self, v: bool) -> Self {
1916        self.params.remove_caption = Some(v);
1917        self
1918    }
1919}
1920
1921impl_into_future!(
1922    CopyMessages,
1923    Vec<rustigram_types::message::MessageId>,
1924    "copyMessages"
1925);
1926
1927// ─── sendVenue ────────────────────────────────────────────────────────────────
1928
1929#[derive(Serialize)]
1930struct SendVenueParams {
1931    chat_id: ChatId,
1932    latitude: f64,
1933    longitude: f64,
1934    title: String,
1935    address: String,
1936    #[serde(skip_serializing_if = "Option::is_none")]
1937    message_thread_id: Option<i64>,
1938    #[serde(skip_serializing_if = "Option::is_none")]
1939    direct_messages_topic_id: Option<i64>,
1940    #[serde(skip_serializing_if = "Option::is_none")]
1941    foursquare_id: Option<String>,
1942    #[serde(skip_serializing_if = "Option::is_none")]
1943    foursquare_type: Option<String>,
1944    #[serde(skip_serializing_if = "Option::is_none")]
1945    google_place_id: Option<String>,
1946    #[serde(skip_serializing_if = "Option::is_none")]
1947    google_place_type: Option<String>,
1948    #[serde(skip_serializing_if = "Option::is_none")]
1949    disable_notification: Option<bool>,
1950    #[serde(skip_serializing_if = "Option::is_none")]
1951    protect_content: Option<bool>,
1952    #[serde(skip_serializing_if = "Option::is_none")]
1953    reply_parameters: Option<ReplyParameters>,
1954    #[serde(skip_serializing_if = "Option::is_none")]
1955    reply_markup: Option<ReplyMarkup>,
1956}
1957
1958/// Builder for the [`sendVenue`](https://core.telegram.org/bots/api#sendvenue) method.
1959pub struct SendVenue {
1960    client: BotClient,
1961    params: SendVenueParams,
1962}
1963
1964impl SendVenue {
1965    pub(crate) fn new(
1966        client: BotClient,
1967        chat_id: impl Into<ChatId>,
1968        latitude: f64,
1969        longitude: f64,
1970        title: impl Into<String>,
1971        address: impl Into<String>,
1972    ) -> Self {
1973        Self {
1974            client,
1975            params: SendVenueParams {
1976                chat_id: chat_id.into(),
1977                latitude,
1978                longitude,
1979                title: title.into(),
1980                address: address.into(),
1981                message_thread_id: None,
1982                direct_messages_topic_id: None,
1983                foursquare_id: None,
1984                foursquare_type: None,
1985                google_place_id: None,
1986                google_place_type: None,
1987                disable_notification: None,
1988                protect_content: None,
1989                reply_parameters: None,
1990                reply_markup: None,
1991            },
1992        }
1993    }
1994    /// Forum topic thread ID.
1995    pub fn message_thread_id(mut self, id: i64) -> Self {
1996        self.params.message_thread_id = Some(id);
1997        self
1998    }
1999    /// Identifier of a direct messages chat topic.
2000    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2001        self.params.direct_messages_topic_id = Some(id);
2002        self
2003    }
2004    /// Sets the Foursquare identifier of the venue.
2005    pub fn foursquare_id(mut self, id: impl Into<String>) -> Self {
2006        self.params.foursquare_id = Some(id.into());
2007        self
2008    }
2009    /// Sets the Foursquare type of the venue (e.g. `"arts_entertainment/aquarium"`).
2010    pub fn foursquare_type(mut self, t: impl Into<String>) -> Self {
2011        self.params.foursquare_type = Some(t.into());
2012        self
2013    }
2014    /// Sets the Google Places identifier of the venue.
2015    pub fn google_place_id(mut self, id: impl Into<String>) -> Self {
2016        self.params.google_place_id = Some(id.into());
2017        self
2018    }
2019    /// Sets the Google Places type of the venue.
2020    pub fn google_place_type(mut self, t: impl Into<String>) -> Self {
2021        self.params.google_place_type = Some(t.into());
2022        self
2023    }
2024    /// Sends the message silently — the recipient receives no notification sound.
2025    pub fn disable_notification(mut self, v: bool) -> Self {
2026        self.params.disable_notification = Some(v);
2027        self
2028    }
2029    /// Protects the message from being forwarded or saved.
2030    pub fn protect_content(mut self, v: bool) -> Self {
2031        self.params.protect_content = Some(v);
2032        self
2033    }
2034    /// Reply parameters for this message.
2035    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2036        self.params.reply_parameters = Some(rp);
2037        self
2038    }
2039    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
2040    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
2041        self.params.reply_markup = Some(m.into());
2042        self
2043    }
2044}
2045
2046impl_into_future!(SendVenue, Message, "sendVenue");
2047
2048// ─── sendMediaGroup ───────────────────────────────────────────────────────────
2049
2050#[derive(Serialize)]
2051struct SendMediaGroupParams {
2052    chat_id: ChatId,
2053    /// Array of `InputMedia` objects (photo, video, audio, or document).
2054    ///
2055    /// Uses `serde_json::Value` until the `InputMedia` enum is defined in
2056    /// Priority 4. Pass the result of `serde_json::to_value(&your_input_media_vec)`.
2057    media: Vec<serde_json::Value>,
2058    #[serde(skip_serializing_if = "Option::is_none")]
2059    message_thread_id: Option<i64>,
2060    #[serde(skip_serializing_if = "Option::is_none")]
2061    direct_messages_topic_id: Option<i64>,
2062    #[serde(skip_serializing_if = "Option::is_none")]
2063    business_connection_id: Option<String>,
2064    #[serde(skip_serializing_if = "Option::is_none")]
2065    disable_notification: Option<bool>,
2066    #[serde(skip_serializing_if = "Option::is_none")]
2067    protect_content: Option<bool>,
2068    #[serde(skip_serializing_if = "Option::is_none")]
2069    reply_parameters: Option<ReplyParameters>,
2070}
2071
2072/// Builder for the [`sendMediaGroup`](https://core.telegram.org/bots/api#sendmediagroup) method.
2073///
2074/// Sends a group of photos, videos, documents, or audios as an album (2–10 items).
2075///
2076/// The `media` parameter accepts `Vec<serde_json::Value>` until the `InputMedia`
2077/// enum is defined in Priority 4. Construct items with `serde_json::json!({...})`
2078/// or `serde_json::to_value(&input_media)`.
2079pub struct SendMediaGroup {
2080    client: BotClient,
2081    params: SendMediaGroupParams,
2082}
2083
2084impl SendMediaGroup {
2085    pub(crate) fn new(
2086        client: BotClient,
2087        chat_id: impl Into<ChatId>,
2088        media: Vec<serde_json::Value>,
2089    ) -> Self {
2090        Self {
2091            client,
2092            params: SendMediaGroupParams {
2093                chat_id: chat_id.into(),
2094                media,
2095                message_thread_id: None,
2096                direct_messages_topic_id: None,
2097                business_connection_id: None,
2098                disable_notification: None,
2099                protect_content: None,
2100                reply_parameters: None,
2101            },
2102        }
2103    }
2104    /// Forum topic thread ID.
2105    pub fn message_thread_id(mut self, id: i64) -> Self {
2106        self.params.message_thread_id = Some(id);
2107        self
2108    }
2109    /// Identifier of a direct messages chat topic.
2110    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2111        self.params.direct_messages_topic_id = Some(id);
2112        self
2113    }
2114    /// Business connection ID for sending on behalf of a business account.
2115    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
2116        self.params.business_connection_id = Some(id.into());
2117        self
2118    }
2119    /// Sends the messages silently — recipients receive no notification sound.
2120    pub fn disable_notification(mut self, v: bool) -> Self {
2121        self.params.disable_notification = Some(v);
2122        self
2123    }
2124    /// Protects the messages from being forwarded or saved.
2125    pub fn protect_content(mut self, v: bool) -> Self {
2126        self.params.protect_content = Some(v);
2127        self
2128    }
2129    /// Reply parameters for this message.
2130    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2131        self.params.reply_parameters = Some(rp);
2132        self
2133    }
2134}
2135
2136impl_into_future!(SendMediaGroup, Vec<Message>, "sendMediaGroup");
2137
2138// ─── sendPaidMedia ────────────────────────────────────────────────────────────
2139
2140#[derive(Serialize)]
2141struct SendPaidMediaParams {
2142    chat_id: ChatId,
2143    star_count: u32,
2144    /// Array of `InputPaidMedia` objects (photo or video).
2145    ///
2146    /// Uses `serde_json::Value` until the `InputPaidMedia` enum is defined in
2147    /// Priority 4. Pass the result of `serde_json::to_value(&your_paid_media_vec)`.
2148    media: Vec<serde_json::Value>,
2149    #[serde(skip_serializing_if = "Option::is_none")]
2150    business_connection_id: Option<String>,
2151    #[serde(skip_serializing_if = "Option::is_none")]
2152    payload: Option<String>,
2153    #[serde(skip_serializing_if = "Option::is_none")]
2154    caption: Option<String>,
2155    #[serde(skip_serializing_if = "Option::is_none")]
2156    parse_mode: Option<ParseMode>,
2157    #[serde(skip_serializing_if = "Option::is_none")]
2158    show_caption_above_media: Option<bool>,
2159    #[serde(skip_serializing_if = "Option::is_none")]
2160    disable_notification: Option<bool>,
2161    #[serde(skip_serializing_if = "Option::is_none")]
2162    protect_content: Option<bool>,
2163    #[serde(skip_serializing_if = "Option::is_none")]
2164    reply_parameters: Option<ReplyParameters>,
2165    #[serde(skip_serializing_if = "Option::is_none")]
2166    reply_markup: Option<ReplyMarkup>,
2167}
2168
2169/// Builder for the [`sendPaidMedia`](https://core.telegram.org/bots/api#sendpaidmedia) method.
2170///
2171/// Sends paid media that users must pay Telegram Stars to view (up to 10 items).
2172///
2173/// The `media` parameter accepts `Vec<serde_json::Value>` until the `InputPaidMedia`
2174/// enum is defined in Priority 4.
2175pub struct SendPaidMedia {
2176    client: BotClient,
2177    params: SendPaidMediaParams,
2178}
2179
2180impl SendPaidMedia {
2181    pub(crate) fn new(
2182        client: BotClient,
2183        chat_id: impl Into<ChatId>,
2184        star_count: u32,
2185        media: Vec<serde_json::Value>,
2186    ) -> Self {
2187        Self {
2188            client,
2189            params: SendPaidMediaParams {
2190                chat_id: chat_id.into(),
2191                star_count,
2192                media,
2193                business_connection_id: None,
2194                payload: None,
2195                caption: None,
2196                parse_mode: None,
2197                show_caption_above_media: None,
2198                disable_notification: None,
2199                protect_content: None,
2200                reply_parameters: None,
2201                reply_markup: None,
2202            },
2203        }
2204    }
2205    /// Business connection ID for sending on behalf of a business account.
2206    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
2207        self.params.business_connection_id = Some(id.into());
2208        self
2209    }
2210    /// Bot-defined paid media payload (0–128 bytes); not shown to the user.
2211    pub fn payload(mut self, p: impl Into<String>) -> Self {
2212        self.params.payload = Some(p.into());
2213        self
2214    }
2215    /// Sets the caption (0–1024 characters).
2216    pub fn caption(mut self, c: impl Into<String>) -> Self {
2217        self.params.caption = Some(c.into());
2218        self
2219    }
2220    /// Sets the caption parse mode (`MarkdownV2`, `HTML`, or `Markdown`).
2221    pub fn parse_mode(mut self, m: ParseMode) -> Self {
2222        self.params.parse_mode = Some(m);
2223        self
2224    }
2225    /// Shows the caption above the media instead of below it.
2226    pub fn show_caption_above_media(mut self, v: bool) -> Self {
2227        self.params.show_caption_above_media = Some(v);
2228        self
2229    }
2230    /// Sends the message silently — the recipient receives no notification sound.
2231    pub fn disable_notification(mut self, v: bool) -> Self {
2232        self.params.disable_notification = Some(v);
2233        self
2234    }
2235    /// Protects the message from being forwarded or saved.
2236    pub fn protect_content(mut self, v: bool) -> Self {
2237        self.params.protect_content = Some(v);
2238        self
2239    }
2240    /// Reply parameters for this message.
2241    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2242        self.params.reply_parameters = Some(rp);
2243        self
2244    }
2245    /// Attaches a reply markup (inline keyboard, reply keyboard, etc.).
2246    pub fn reply_markup(mut self, m: impl Into<ReplyMarkup>) -> Self {
2247        self.params.reply_markup = Some(m.into());
2248        self
2249    }
2250}
2251
2252impl_into_future!(SendPaidMedia, Message, "sendPaidMedia");
2253
2254// ─── sendGame ─────────────────────────────────────────────────────────────────
2255
2256#[derive(Serialize)]
2257struct SendGameParams {
2258    chat_id: i64,
2259    game_short_name: String,
2260    #[serde(skip_serializing_if = "Option::is_none")]
2261    business_connection_id: Option<String>,
2262    #[serde(skip_serializing_if = "Option::is_none")]
2263    message_thread_id: Option<i64>,
2264    #[serde(skip_serializing_if = "Option::is_none")]
2265    direct_messages_topic_id: Option<i64>,
2266    #[serde(skip_serializing_if = "Option::is_none")]
2267    disable_notification: Option<bool>,
2268    #[serde(skip_serializing_if = "Option::is_none")]
2269    protect_content: Option<bool>,
2270    #[serde(skip_serializing_if = "Option::is_none")]
2271    reply_parameters: Option<ReplyParameters>,
2272    #[serde(skip_serializing_if = "Option::is_none")]
2273    reply_markup: Option<rustigram_types::keyboard::InlineKeyboardMarkup>,
2274}
2275
2276/// Builder for the [`sendGame`](https://core.telegram.org/bots/api#sendgame) method.
2277///
2278/// Note: `chat_id` is an integer — games can't be sent to channel direct messages
2279/// chats or channel chats.
2280pub struct SendGame {
2281    client: BotClient,
2282    params: SendGameParams,
2283}
2284
2285impl SendGame {
2286    pub(crate) fn new(client: BotClient, chat_id: i64, game_short_name: impl Into<String>) -> Self {
2287        Self {
2288            client,
2289            params: SendGameParams {
2290                chat_id,
2291                game_short_name: game_short_name.into(),
2292                business_connection_id: None,
2293                message_thread_id: None,
2294                direct_messages_topic_id: None,
2295                disable_notification: None,
2296                protect_content: None,
2297                reply_parameters: None,
2298                reply_markup: None,
2299            },
2300        }
2301    }
2302    /// Business connection ID for sending on behalf of a business account.
2303    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
2304        self.params.business_connection_id = Some(id.into());
2305        self
2306    }
2307    /// Forum topic thread ID.
2308    pub fn message_thread_id(mut self, id: i64) -> Self {
2309        self.params.message_thread_id = Some(id);
2310        self
2311    }
2312    /// Identifier of a direct messages chat topic.
2313    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2314        self.params.direct_messages_topic_id = Some(id);
2315        self
2316    }
2317    /// Sends the message silently — the recipient receives no notification sound.
2318    pub fn disable_notification(mut self, v: bool) -> Self {
2319        self.params.disable_notification = Some(v);
2320        self
2321    }
2322    /// Protects the message from being forwarded or saved.
2323    pub fn protect_content(mut self, v: bool) -> Self {
2324        self.params.protect_content = Some(v);
2325        self
2326    }
2327    /// Reply parameters for this message.
2328    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2329        self.params.reply_parameters = Some(rp);
2330        self
2331    }
2332    /// Attaches an inline keyboard. The first button must launch the game.
2333    pub fn reply_markup(mut self, m: rustigram_types::keyboard::InlineKeyboardMarkup) -> Self {
2334        self.params.reply_markup = Some(m);
2335        self
2336    }
2337}
2338
2339impl_into_future!(SendGame, Message, "sendGame");
2340
2341// ─── sendChecklist ────────────────────────────────────────────────────────────
2342
2343#[derive(Serialize)]
2344struct SendChecklistParams {
2345    business_connection_id: String,
2346    chat_id: i64,
2347    checklist: rustigram_types::checklist::InputChecklist,
2348    #[serde(skip_serializing_if = "Option::is_none")]
2349    direct_messages_topic_id: Option<i64>,
2350    #[serde(skip_serializing_if = "Option::is_none")]
2351    disable_notification: Option<bool>,
2352    #[serde(skip_serializing_if = "Option::is_none")]
2353    protect_content: Option<bool>,
2354    #[serde(skip_serializing_if = "Option::is_none")]
2355    message_effect_id: Option<String>,
2356    #[serde(skip_serializing_if = "Option::is_none")]
2357    reply_parameters: Option<ReplyParameters>,
2358    #[serde(skip_serializing_if = "Option::is_none")]
2359    reply_markup: Option<rustigram_types::keyboard::InlineKeyboardMarkup>,
2360    #[serde(skip_serializing_if = "Option::is_none")]
2361    suggested_post_parameters: Option<SuggestedPostParameters>,
2362}
2363
2364/// Builder for the [`sendChecklist`](https://core.telegram.org/bots/api#sendchecklist) method.
2365///
2366/// Business bots only — sends a checklist on behalf of a connected business account.
2367/// Requires the `can_reply` business bot right.
2368pub struct SendChecklist {
2369    client: BotClient,
2370    params: SendChecklistParams,
2371}
2372
2373impl SendChecklist {
2374    pub(crate) fn new(
2375        client: BotClient,
2376        business_connection_id: impl Into<String>,
2377        chat_id: i64,
2378        checklist: rustigram_types::checklist::InputChecklist,
2379    ) -> Self {
2380        Self {
2381            client,
2382            params: SendChecklistParams {
2383                business_connection_id: business_connection_id.into(),
2384                chat_id,
2385                checklist,
2386                direct_messages_topic_id: None,
2387                disable_notification: None,
2388                protect_content: None,
2389                message_effect_id: None,
2390                reply_parameters: None,
2391                reply_markup: None,
2392                suggested_post_parameters: None,
2393            },
2394        }
2395    }
2396    /// Identifier of a direct messages chat topic.
2397    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2398        self.params.direct_messages_topic_id = Some(id);
2399        self
2400    }
2401    /// Sends the message silently — the recipient receives no notification sound.
2402    pub fn disable_notification(mut self, v: bool) -> Self {
2403        self.params.disable_notification = Some(v);
2404        self
2405    }
2406    /// Protects the message from being forwarded or saved.
2407    pub fn protect_content(mut self, v: bool) -> Self {
2408        self.params.protect_content = Some(v);
2409        self
2410    }
2411    /// Unique identifier of the message effect to add to the message.
2412    pub fn message_effect_id(mut self, id: impl Into<String>) -> Self {
2413        self.params.message_effect_id = Some(id.into());
2414        self
2415    }
2416    /// Reply parameters for this message.
2417    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2418        self.params.reply_parameters = Some(rp);
2419        self
2420    }
2421    /// Attaches an inline keyboard to the message.
2422    pub fn reply_markup(mut self, m: rustigram_types::keyboard::InlineKeyboardMarkup) -> Self {
2423        self.params.reply_markup = Some(m);
2424        self
2425    }
2426    /// Suggested post parameters for channel direct messages chats.
2427    pub fn suggested_post_parameters(mut self, params: SuggestedPostParameters) -> Self {
2428        self.params.suggested_post_parameters = Some(params);
2429        self
2430    }
2431}
2432
2433impl_into_future!(SendChecklist, Message, "sendChecklist");
2434
2435// ─── sendRichMessage ──────────────────────────────────────────────────────────
2436
2437#[derive(Serialize)]
2438struct SendRichMessageParams {
2439    chat_id: ChatId,
2440    rich_message: rustigram_types::rich_message::InputRichMessage,
2441    #[serde(skip_serializing_if = "Option::is_none")]
2442    business_connection_id: Option<String>,
2443    #[serde(skip_serializing_if = "Option::is_none")]
2444    message_thread_id: Option<i64>,
2445    #[serde(skip_serializing_if = "Option::is_none")]
2446    direct_messages_topic_id: Option<i64>,
2447    #[serde(skip_serializing_if = "Option::is_none")]
2448    disable_notification: Option<bool>,
2449    #[serde(skip_serializing_if = "Option::is_none")]
2450    protect_content: Option<bool>,
2451    #[serde(skip_serializing_if = "Option::is_none")]
2452    allow_paid_broadcast: Option<bool>,
2453    #[serde(skip_serializing_if = "Option::is_none")]
2454    message_effect_id: Option<String>,
2455    #[serde(skip_serializing_if = "Option::is_none")]
2456    suggested_post_parameters: Option<SuggestedPostParameters>,
2457    #[serde(skip_serializing_if = "Option::is_none")]
2458    reply_parameters: Option<ReplyParameters>,
2459    #[serde(skip_serializing_if = "Option::is_none")]
2460    reply_markup: Option<rustigram_types::keyboard::ReplyMarkup>,
2461}
2462
2463/// Builder for the [`sendRichMessage`](https://core.telegram.org/bots/api#sendrichmessage) method.
2464pub struct SendRichMessage {
2465    client: BotClient,
2466    params: SendRichMessageParams,
2467}
2468
2469impl SendRichMessage {
2470    pub(crate) fn new(
2471        client: BotClient,
2472        chat_id: impl Into<ChatId>,
2473        rich_message: rustigram_types::rich_message::InputRichMessage,
2474    ) -> Self {
2475        Self {
2476            client,
2477            params: SendRichMessageParams {
2478                chat_id: chat_id.into(),
2479                rich_message,
2480                business_connection_id: None,
2481                message_thread_id: None,
2482                direct_messages_topic_id: None,
2483                disable_notification: None,
2484                protect_content: None,
2485                allow_paid_broadcast: None,
2486                message_effect_id: None,
2487                suggested_post_parameters: None,
2488                reply_parameters: None,
2489                reply_markup: None,
2490            },
2491        }
2492    }
2493
2494    /// Sets the business connection identifier.
2495    pub fn business_connection_id(mut self, id: impl Into<String>) -> Self {
2496        self.params.business_connection_id = Some(id.into());
2497        self
2498    }
2499    /// Sends the message to the specified topic thread.
2500    pub fn message_thread_id(mut self, id: i64) -> Self {
2501        self.params.message_thread_id = Some(id);
2502        self
2503    }
2504    /// Sends the message to the specified direct messages topic.
2505    pub fn direct_messages_topic_id(mut self, id: i64) -> Self {
2506        self.params.direct_messages_topic_id = Some(id);
2507        self
2508    }
2509    /// Sends the message silently (no notification sound).
2510    pub fn disable_notification(mut self, v: bool) -> Self {
2511        self.params.disable_notification = Some(v);
2512        self
2513    }
2514    /// Protects the message from being forwarded or saved.
2515    pub fn protect_content(mut self, v: bool) -> Self {
2516        self.params.protect_content = Some(v);
2517        self
2518    }
2519    /// Allows up to 1 000 messages per second by paying 0.1 Stars per message.
2520    pub fn allow_paid_broadcast(mut self, v: bool) -> Self {
2521        self.params.allow_paid_broadcast = Some(v);
2522        self
2523    }
2524    /// Unique identifier of the message effect to add to the message.
2525    pub fn message_effect_id(mut self, id: impl Into<String>) -> Self {
2526        self.params.message_effect_id = Some(id.into());
2527        self
2528    }
2529    /// Suggested post parameters for channel direct messages chats.
2530    pub fn suggested_post_parameters(mut self, p: SuggestedPostParameters) -> Self {
2531        self.params.suggested_post_parameters = Some(p);
2532        self
2533    }
2534    /// Reply parameters for this message.
2535    pub fn reply_parameters(mut self, rp: ReplyParameters) -> Self {
2536        self.params.reply_parameters = Some(rp);
2537        self
2538    }
2539    /// Attaches a reply markup to the message.
2540    pub fn reply_markup(mut self, m: rustigram_types::keyboard::ReplyMarkup) -> Self {
2541        self.params.reply_markup = Some(m);
2542        self
2543    }
2544}
2545
2546impl_into_future!(SendRichMessage, Message, "sendRichMessage");
2547
2548// ─── sendRichMessageDraft ─────────────────────────────────────────────────────
2549
2550#[derive(Serialize)]
2551struct SendRichMessageDraftParams {
2552    chat_id: i64,
2553    draft_id: i64,
2554    rich_message: rustigram_types::rich_message::InputRichMessage,
2555    #[serde(skip_serializing_if = "Option::is_none")]
2556    message_thread_id: Option<i64>,
2557}
2558
2559/// Builder for the [`sendRichMessageDraft`](https://core.telegram.org/bots/api#sendrichmessagedraft) method.
2560///
2561/// Streams a partial rich message as a 30-second ephemeral preview.
2562/// Once generation is complete, call [`SendRichMessage`] with the full content to persist it.
2563pub struct SendRichMessageDraft {
2564    client: BotClient,
2565    params: SendRichMessageDraftParams,
2566}
2567
2568impl SendRichMessageDraft {
2569    pub(crate) fn new(
2570        client: BotClient,
2571        chat_id: i64,
2572        draft_id: i64,
2573        rich_message: rustigram_types::rich_message::InputRichMessage,
2574    ) -> Self {
2575        Self {
2576            client,
2577            params: SendRichMessageDraftParams {
2578                chat_id,
2579                draft_id,
2580                rich_message,
2581                message_thread_id: None,
2582            },
2583        }
2584    }
2585
2586    /// Sends the draft to the specified topic thread.
2587    pub fn message_thread_id(mut self, id: i64) -> Self {
2588        self.params.message_thread_id = Some(id);
2589        self
2590    }
2591}
2592
2593impl_into_future!(SendRichMessageDraft, bool, "sendRichMessageDraft");