1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
/*
* Copyright (c) 2026 Ankit Chaubey <ankitchaubey.dev@gmail.com>
* https://github.com/ankit-chaubey
*
* Project: ferogram
* Website: https://ferogram.dev
*
* Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
* https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
* <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
* This file may not be copied, modified, or distributed except according
* to those terms.
*/
use ferogram_tl_types as tl;
/// Builder for composing outgoing messages.
///
/// ```rust,no_run
/// # #[cfg(feature = "parsers")]
/// # {
/// use ferogram::InputMessage;
///
/// // plain text
/// let msg = InputMessage::text("Hello!");
///
/// // markdown
/// let msg = InputMessage::markdown("**bold** and _italic_");
///
/// // HTML
/// let msg = InputMessage::html("<b>bold</b> and <i>italic</i>");
///
/// // with options
/// let msg = InputMessage::markdown("**Hello**")
/// .silent(true)
/// .reply_to(Some(42));
/// # }
/// ```
#[derive(Clone, Default)]
pub struct InputMessage {
pub text: String,
pub reply_to: Option<i32>,
/// Land this message in a specific forum topic thread (the `top_msg_id`
/// of the topic, see [`crate::Client::get_forum_topics`]). Set via
/// [`InputMessage::topic_id`].
///
/// `messages.sendMessage`/`sendMedia` have no dedicated topic field -
/// topic routing rides on `reply_to`, so setting this alone (without
/// `reply_to`) makes the message reply to the topic's root message,
/// which is how official clients post into a topic "bare".
pub top_msg_id: Option<i32>,
pub silent: bool,
pub background: bool,
pub clear_draft: bool,
pub no_webpage: bool,
/// Prevent recipients from forwarding this message.
pub noforwards: bool,
/// Reorder the sender's installed sticker sets to put a set used in
/// this message first, like the official clients do.
pub update_stickersets_order: bool,
/// Bypass paid-messages flood limits by paying Stars.
pub allow_paid_floodskip: bool,
/// Show media above the caption instead of below (Telegram ≥ 10.3).\
pub invert_media: bool,
/// Schedule to send when the user goes online (`schedule_date = 0x7FFFFFFE`).\
pub schedule_once_online: bool,
pub entities: Option<Vec<tl::enums::MessageEntity>>,
pub reply_markup: Option<tl::enums::ReplyMarkup>,
pub schedule_date: Option<i32>,
/// Repeat the scheduled send every N seconds. Only meaningful with
/// `schedule_date` set.
pub schedule_repeat_period: Option<i32>,
/// Associate with a business-account quick reply shortcut, by ID.
pub quick_reply_shortcut_id: Option<i32>,
/// Send as a different identity you're allowed to send as (e.g. an
/// anonymous admin identity, or a linked channel) instead of yourself.
pub send_as: Option<crate::PeerRef>,
/// Attach a message effect (Premium sticker/emoji effect) by its ID.
pub effect: Option<i64>,
/// Stars you're willing to pay if the destination charges for messages.
pub allow_paid_stars: Option<i64>,
/// Send as a channel "suggested post" instead of posting directly.
pub suggested_post: Option<tl::enums::SuggestedPost>,
/// Attached media to send alongside the message.
/// Use [`InputMessage::copy_media`] to attach media copied from an existing message.
pub media: Option<tl::enums::InputMedia>,
/// Structured rich-text content (headings, tables, code blocks, etc).
/// Use [`InputMessage::rich_text`] to attach `PageBlock`s, e.g. from
/// [`crate::parsers::parse_rich_markdown`].
pub rich_message: Option<tl::enums::InputRichMessage>,
}
/// Options for forwarding messages.
///
/// Used by [`crate::Client::forward_messages`] and
/// `IncomingMessage::forward_to_ex`. All fields default to `false`/`None`,
/// matching every field Telegram's `messages.forwardMessages` accepts - if
/// Telegram adds a new one later, add it here rather than hardcoding it.
#[derive(Default, Clone)]
pub struct ForwardOptions {
/// Send silently (no notification for recipient).
pub silent: bool,
/// Send as a background message (doesn't bump the chat to the top).
pub background: bool,
/// Also forward the sender's high score, for messages containing a game.
pub with_my_score: bool,
/// Strip the original author attribution (`Forwarded from …`).
pub drop_author: bool,
/// Remove captions from forwarded media.
pub drop_media_captions: bool,
/// Prevent recipients from forwarding the message further.
pub noforwards: bool,
/// Reply to an existing message in the destination chat.
pub reply_to: Option<i32>,
/// Forward into this forum topic thread (the `top_msg_id` of the topic,
/// see [`crate::Client::get_forum_topics`]). Needed when the
/// destination is a supergroup that has forum topics enabled - without
/// it, forwarded messages land outside any topic instead of the one
/// you meant.
pub topic_id: Option<i32>,
/// Schedule forwarding for this Unix timestamp (seconds).
pub schedule_date: Option<i32>,
/// Repeat the scheduled forward every N seconds. Only meaningful with
/// `schedule_date` set.
pub schedule_repeat_period: Option<i32>,
/// Forward as a different identity you're allowed to send as (e.g. an
/// anonymous admin identity, or a linked channel) instead of yourself.
pub send_as: Option<crate::PeerRef>,
/// Attach a message effect (Premium sticker/emoji effect) by its ID.
pub effect: Option<i64>,
/// Start the forwarded video's preview at this timestamp (seconds).
pub video_timestamp: Option<i32>,
/// Stars you're willing to pay if the destination charges for messages.
pub allow_paid_stars: Option<i64>,
/// Bypass paid-messages flood limits by paying Stars.
pub allow_paid_floodskip: bool,
/// Associate the forward with a business-account quick reply shortcut.
pub quick_reply_shortcut: Option<tl::enums::InputQuickReplyShortcut>,
/// Forward as a channel "suggested post" instead of posting directly.
pub suggested_post: Option<tl::enums::SuggestedPost>,
}
/// Options for copying messages (forward without the "Forwarded from" attribution).
///
/// Used by [`crate::Client::copy_messages`] and `IncomingMessage::copy`. This
/// is the same underlying `messages.forwardMessages` call as
/// [`ForwardOptions`], with `drop_author` always forced to `true` - which is
/// exactly what Telegram's own "copy" feature does under the hood.
#[derive(Default, Clone)]
pub struct CopyOptions {
/// Send silently (no notification for recipient).
pub silent: bool,
/// Send as a background message (doesn't bump the chat to the top).
pub background: bool,
/// Remove captions from copied media. Honored on both the cheap forward
/// path (via `drop_media_captions`) and the fetch-and-resend override
/// path - the latter only when `caption` is left `None`; an explicit
/// `caption` always wins over this flag.
pub drop_captions: bool,
/// Prevent recipients from forwarding the message further.
pub noforwards: bool,
/// Reply to an existing message in the destination chat.
pub reply_to: Option<i32>,
/// Copy into this forum topic thread (the `top_msg_id` of the topic,
/// see [`crate::Client::get_forum_topics`]).
pub topic_id: Option<i32>,
/// Schedule the copy for this Unix timestamp (seconds).
pub schedule_date: Option<i32>,
/// Repeat the scheduled copy every N seconds. Only meaningful with
/// `schedule_date` set.
pub schedule_repeat_period: Option<i32>,
/// Send as a different identity you're allowed to send as (e.g. an
/// anonymous admin identity, or a linked channel) instead of yourself.
pub send_as: Option<crate::PeerRef>,
/// Attach a message effect (Premium sticker/emoji effect) by its ID.
pub effect: Option<i64>,
/// Start the copied video's preview at this timestamp (seconds).
pub video_timestamp: Option<i32>,
/// Stars you're willing to pay if the destination charges for messages.
pub allow_paid_stars: Option<i64>,
/// Bypass paid-messages flood limits by paying Stars.
pub allow_paid_floodskip: bool,
/// Associate the copy with a business-account quick reply shortcut.
pub quick_reply_shortcut: Option<tl::enums::InputQuickReplyShortcut>,
/// Send as a channel "suggested post" instead of posting directly.
pub suggested_post: Option<tl::enums::SuggestedPost>,
/// Replace the copy's text/caption instead of keeping the original.
///
/// Setting this (or [`CopyOptions::reply_markup`]) routes the copy
/// through [`crate::Client::copy_message`]'s fetch-and-resend path
/// instead of the cheap `forward_messages` path: raw MTProto's
/// `messages.forwardMessages` has no field to override text or
/// caption, only `drop_media_captions` (keep or strip, nothing else),
/// so an override can only be done by fetching the source message and
/// sending its media as a brand-new message - the same way Telegram's
/// own Bot API implements `copyMessage` under the hood.
///
/// When left `None`, the source message's own text/caption is kept -
/// *and* its formatting entities (bold, italic, links, code, spoilers,
/// ...) are carried over with it. Set [`CopyOptions::caption_entities`]
/// alongside this field to give a new caption its own formatting.
pub caption: Option<String>,
/// Formatting entities for [`CopyOptions::caption`]. Ignored unless
/// `caption` is also set - when `caption` is `None`, the source
/// message's own entities are used instead, so there's nothing to
/// override here.
pub caption_entities: Option<Vec<tl::enums::MessageEntity>>,
/// Attach a reply markup to the copy instead of sending it bare.
///
/// Unlike Telegram's Bot API, this never falls back to the source
/// message's own reply markup when left `None` - buttons are commonly
/// tied to the original bot's callback handlers, so silently carrying
/// them over to a copy sent by a different client would produce dead
/// buttons. Pass the source's markup explicitly if you want to keep it.
///
/// Also routes through the fetch-and-resend path; see
/// [`CopyOptions::caption`].
pub reply_markup: Option<tl::enums::ReplyMarkup>,
}
impl From<CopyOptions> for ForwardOptions {
fn from(o: CopyOptions) -> Self {
ForwardOptions {
silent: o.silent,
background: o.background,
with_my_score: false,
drop_author: true,
drop_media_captions: o.drop_captions,
noforwards: o.noforwards,
reply_to: o.reply_to,
topic_id: o.topic_id,
schedule_date: o.schedule_date,
schedule_repeat_period: o.schedule_repeat_period,
send_as: o.send_as,
effect: o.effect,
video_timestamp: o.video_timestamp,
allow_paid_stars: o.allow_paid_stars,
allow_paid_floodskip: o.allow_paid_floodskip,
quick_reply_shortcut: o.quick_reply_shortcut,
suggested_post: o.suggested_post,
}
}
}
/// Selects which flavour of message link [`crate::Client::export_message_link`] should produce.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LinkKind {
/// A plain `t.me/channel/msgid` permalink (default).
#[default]
Normal,
/// A link that reveals the whole album / media group the message belongs to.
Grouped,
/// A link that opens the thread (comments) attached to a channel post.
Thread,
}
impl InputMessage {
/// Create a message with the given text.
pub fn text(text: impl Into<String>) -> Self {
Self {
text: text.into(),
..Default::default()
}
}
/// Create a message by parsing Telegram-flavoured markdown.
///
/// The markdown is stripped and the resulting plain text + entities are
/// set on the message. Supports `**bold**`, `_italic_`, `` `code` ``,
/// `[text](url)`, `||spoiler||`, `~~strike~~`, ``,
/// and backslash escapes.
///
/// ```rust,no_run
/// use ferogram::InputMessage;
///
/// let msg = InputMessage::markdown("**Hello** _world_!");
/// ```
#[cfg(feature = "parsers")]
pub fn markdown(text: impl AsRef<str>) -> Self {
let (plain, ents) = crate::parsers::parse_markdown(text.as_ref());
Self {
text: plain,
entities: if ents.is_empty() { None } else { Some(ents) },
..Default::default()
}
}
/// Create a message by parsing Telegram-compatible HTML.
///
/// Supports `<b>`, `<i>`, `<u>`, `<s>`, `<code>`, `<pre>`,
/// `<tg-spoiler>`, `<a href="...">`, `<tg-emoji emoji-id="...">`.
///
/// ```rust,no_run
/// use ferogram::InputMessage;
///
/// let msg = InputMessage::html("<b>Hello</b> <i>world</i>!");
/// ```
#[cfg(feature = "parsers")]
pub fn html(text: impl AsRef<str>) -> Self {
let (plain, ents) = crate::parsers::parse_html(text.as_ref());
Self {
text: plain,
entities: if ents.is_empty() { None } else { Some(ents) },
..Default::default()
}
}
/// Set the message text.
pub fn set_text(mut self, text: impl Into<String>) -> Self {
self.text = text.into();
self
}
/// Reply to a specific message ID.
pub fn reply_to(mut self, id: Option<i32>) -> Self {
self.reply_to = id;
self
}
/// Land this message in a specific forum topic thread (the `top_msg_id`
/// of the topic, see [`crate::Client::get_forum_topics`]).
///
/// Combine with [`InputMessage::reply_to`] to reply to a particular
/// message inside the topic; set alone, the message replies to the
/// topic's root message instead, landing it in the topic "bare" - the
/// same thing official clients do when you post into a topic without
/// quoting anything.
pub fn topic_id(mut self, id: Option<i32>) -> Self {
self.top_msg_id = id;
self
}
/// Send silently (no notification sound).
pub fn silent(mut self, v: bool) -> Self {
self.silent = v;
self
}
/// Send in background.
pub fn background(mut self, v: bool) -> Self {
self.background = v;
self
}
/// Clear the draft after sending.
pub fn clear_draft(mut self, v: bool) -> Self {
self.clear_draft = v;
self
}
/// Disable link preview.
pub fn no_webpage(mut self, v: bool) -> Self {
self.no_webpage = v;
self
}
/// Prevent recipients from forwarding this message.
pub fn noforwards(mut self, v: bool) -> Self {
self.noforwards = v;
self
}
/// Reorder the sender's installed sticker sets to put a set used in
/// this message first, like the official clients do.
pub fn update_stickersets_order(mut self, v: bool) -> Self {
self.update_stickersets_order = v;
self
}
/// Bypass paid-messages flood limits by paying Stars.
pub fn allow_paid_floodskip(mut self, v: bool) -> Self {
self.allow_paid_floodskip = v;
self
}
/// Show media above the caption rather than below (requires Telegram ≥ 10.3).
pub fn invert_media(mut self, v: bool) -> Self {
self.invert_media = v;
self
}
/// Schedule the message to be sent when the recipient comes online.
///
/// Mutually exclusive with `schedule_date`: calling this last wins.
/// Uses the Telegram magic value `0x7FFFFFFE`.
pub fn schedule_once_online(mut self) -> Self {
self.schedule_once_online = true;
self.schedule_date = None;
self
}
/// Attach formatting entities (bold, italic, code, links, etc).
pub fn entities(mut self, e: Vec<tl::enums::MessageEntity>) -> Self {
self.entities = Some(e);
self
}
/// Attach a reply markup (inline or reply keyboard).
pub fn reply_markup(mut self, rm: impl Into<tl::enums::ReplyMarkup>) -> Self {
self.reply_markup = Some(rm.into());
self
}
/// Schedule the message for a future Unix timestamp.
pub fn schedule_date(mut self, ts: Option<i32>) -> Self {
self.schedule_date = ts;
self
}
/// Repeat the scheduled send every N seconds. Only meaningful with
/// `schedule_date` set.
pub fn schedule_repeat_period(mut self, seconds: Option<i32>) -> Self {
self.schedule_repeat_period = seconds;
self
}
/// Associate this message with a business-account quick reply shortcut.
pub fn quick_reply_shortcut_id(mut self, id: Option<i32>) -> Self {
self.quick_reply_shortcut_id = id;
self
}
/// Send as a different identity you're allowed to send as (e.g. an
/// anonymous admin identity, or a linked channel) instead of yourself.
pub fn send_as(mut self, peer: impl Into<crate::PeerRef>) -> Self {
self.send_as = Some(peer.into());
self
}
/// Attach a message effect (Premium sticker/emoji effect) by its ID.
pub fn effect(mut self, id: Option<i64>) -> Self {
self.effect = id;
self
}
/// Set the amount of Stars you're willing to pay if the destination
/// charges for messages.
pub fn allow_paid_stars(mut self, stars: Option<i64>) -> Self {
self.allow_paid_stars = stars;
self
}
/// Send as a channel "suggested post" instead of posting directly.
pub fn suggested_post(mut self, post: tl::enums::SuggestedPost) -> Self {
self.suggested_post = Some(post);
self
}
/// Attach media copied from an existing message.
///
/// Accepts a raw `tl::enums::InputMedia`, or the builder returned by
/// [`crate::media::Document::to_input_media`] / [`crate::media::Photo::to_input_media`]
/// directly (both convert via `Into`).
///
/// When a `media` is set, the message is sent via `messages.SendMedia`
/// instead of `messages.SendMessage`.
///
/// ```rust,no_run
/// # use ferogram::{InputMessage, media::Document};
/// # fn example(doc: Document) {
/// let msg = InputMessage::text("Here is the file again")
/// .copy_media(doc.to_input_media());
/// # }
/// ```
pub fn copy_media(mut self, media: impl Into<tl::enums::InputMedia>) -> Self {
self.media = Some(media.into());
self
}
/// Remove any previously attached media.
pub fn clear_media(mut self) -> Self {
self.media = None;
self
}
/// Attach structured rich-text content (headings, tables, code blocks,
/// collapsible sections, etc), rendered as a full document inside
/// Telegram instead of flat text.
///
/// Pass the blocks returned by [`crate::parsers::parse_rich_markdown`] or
/// [`crate::parsers::parse_rich_html`].
///
/// ```rust,no_run
/// # #[cfg(feature = "parsers")]
/// # {
/// use ferogram::{InputMessage, parsers::parse_rich_markdown};
/// let blocks = parse_rich_markdown("# Hello\n\nWorld");
/// let msg = InputMessage::text("").rich_text(blocks);
/// # }
/// ```
pub fn rich_text(mut self, blocks: Vec<tl::enums::PageBlock>) -> Self {
self.rich_message = Some(tl::enums::InputRichMessage::InputRichMessage(
tl::types::InputRichMessage {
rtl: false,
noautolink: false,
blocks,
photos: None,
documents: None,
users: None,
},
));
self
}
pub(crate) fn reply_header(&self) -> Option<tl::enums::InputReplyTo> {
// reply_to_msg_id is required by the TL schema even when we're only
// routing into a topic with nothing specific to reply to - fall
// back to the topic's own root message ID in that case, same as
// official clients do when posting into a topic "bare".
let reply_to_msg_id = self.reply_to.or(self.top_msg_id)?;
Some(tl::enums::InputReplyTo::Message(
tl::types::InputReplyToMessage {
reply_to_msg_id,
top_msg_id: self.top_msg_id,
reply_to_peer_id: None,
quote_text: None,
quote_entities: None,
quote_offset: None,
monoforum_peer_id: None,
todo_item_id: None,
poll_option: None,
},
))
}
pub(crate) fn quick_reply_shortcut(&self) -> Option<tl::enums::InputQuickReplyShortcut> {
self.quick_reply_shortcut_id.map(|shortcut_id| {
tl::enums::InputQuickReplyShortcut::Id(tl::types::InputQuickReplyShortcutId {
shortcut_id,
})
})
}
}
impl From<&str> for InputMessage {
fn from(s: &str) -> Self {
Self::text(s)
}
}
impl From<String> for InputMessage {
fn from(s: String) -> Self {
Self::text(s)
}
}
/// Groups all invoice parameters for [`crate::Client::send_invoice`].
#[derive(Debug, Default, Clone)]
pub struct InvoiceOptions {
/// Three-letter ISO 4217 currency code (e.g. `"USD"`).
pub currency: String,
/// Line items: `(label, amount_in_smallest_units)`.
pub prices: Vec<(String, i64)>,
/// Optional URL of a photo to attach to the invoice.
pub photo_url: Option<String>,
/// Request the payer's full name.
pub need_name: bool,
/// Request the payer's phone number.
pub need_phone: bool,
/// Request the payer's email address.
pub need_email: bool,
/// Request the payer's shipping address.
pub need_shipping_address: bool,
/// Whether the final price depends on the shipping method.
pub is_flexible: bool,
}