grammers_client/types/
input_message.rs

1// Copyright 2020 - developers of the `grammers` project.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8use super::attributes::Attribute;
9use crate::types::{Media, ReplyMarkup, Uploaded};
10use grammers_tl_types as tl;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13// https://github.com/telegramdesktop/tdesktop/blob/e7fbcce9d9f0a8944eb2c34e74bd01b8776cb891/Telegram/SourceFiles/data/data_scheduled_messages.h#L52
14const SCHEDULE_ONCE_ONLINE: i32 = 0x7ffffffe;
15
16/// Construct and send rich text messages with various options.
17#[derive(Clone, Default)]
18pub struct InputMessage {
19    pub(crate) background: bool,
20    pub(crate) clear_draft: bool,
21    pub(crate) entities: Vec<tl::enums::MessageEntity>,
22    pub(crate) invert_media: bool,
23    pub(crate) link_preview: bool,
24    pub(crate) reply_markup: Option<tl::enums::ReplyMarkup>,
25    pub(crate) reply_to: Option<i32>,
26    pub(crate) schedule_date: Option<i32>,
27    pub(crate) silent: bool,
28    pub(crate) text: String,
29    pub(crate) media: Option<tl::enums::InputMedia>,
30    media_ttl: Option<i32>,
31    mime_type: Option<String>,
32}
33
34impl InputMessage {
35    /// Creates a new empty message for input.
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Whether to "send this message as a background message".
41    ///
42    /// This description is taken from <https://core.telegram.org/method/messages.sendMessage>.
43    pub fn background(mut self, background: bool) -> Self {
44        self.background = background;
45        self
46    }
47
48    /// Whether the draft in this peer, if any, should be cleared.
49    pub fn clear_draft(mut self, clear_draft: bool) -> Self {
50        self.clear_draft = clear_draft;
51        self
52    }
53
54    /// Replaces the plaintext in the message.
55    ///
56    /// The caller must ensure that formatting entities remain valid for the given text.
57    /// If you need to update formatting entities, call method [`InputMessage::fmt_entities`].
58    ///
59    /// <div class="warning">
60    /// Note that this method does not modify formatting entities, which may break
61    /// formatting or cause out-of-bounds errors if entities do not match the given text.
62    /// </div>
63    pub fn text<T>(mut self, s: T) -> Self
64    where
65        T: Into<String>,
66    {
67        self.text = s.into();
68        self
69    }
70
71    /// The formatting entities within the message (such as bold, italics, etc.).
72    pub fn fmt_entities<I>(mut self, entities: I) -> Self
73    where
74        I: IntoIterator<Item = tl::enums::MessageEntity>,
75    {
76        self.entities = entities.into_iter().collect();
77        self
78    }
79
80    /// Builds a new message from the given markdown-formatted string as the
81    /// message contents and entities.
82    ///
83    /// Note that Telegram only supports a very limited subset of entities:
84    /// bold, italic, underline, strikethrough, code blocks, pre blocks and inline links (inline
85    /// links with this format `tg://user?id=12345678` will be replaced with inline mentions when
86    /// possible).
87    #[cfg(feature = "markdown")]
88    pub fn markdown<T>(mut self, s: T) -> Self
89    where
90        T: AsRef<str>,
91    {
92        let (text, entities) = crate::parsers::parse_markdown_message(s.as_ref());
93        self.text = text;
94        self.entities = entities;
95        self
96    }
97
98    /// Builds a new message from the given HTML-formatted string as the
99    /// message contents and entities.
100    ///
101    /// Note that Telegram only supports a very limited subset of entities:
102    /// bold, italic, underline, strikethrough, code blocks, pre blocks and inline links (inline
103    /// links with this format `tg://user?id=12345678` will be replaced with inline mentions when
104    /// possible).
105    #[cfg(feature = "html")]
106    pub fn html<T>(mut self, s: T) -> Self
107    where
108        T: AsRef<str>,
109    {
110        let (text, entities) = crate::parsers::parse_html_message(s.as_ref());
111        self.text = text;
112        self.entities = entities;
113        self
114    }
115
116    /// Whether the media will be inverted.
117    ///
118    /// If inverted, photos, videos, and documents will appear at the bottom and link previews at the top of the message.
119    pub fn invert_media(mut self, invert_media: bool) -> Self {
120        self.invert_media = invert_media;
121        self
122    }
123
124    /// Whether the link preview be shown for the message.
125    ///
126    /// This has no effect when sending media, which cannot contain a link preview.
127    pub fn link_preview(mut self, link_preview: bool) -> Self {
128        self.link_preview = link_preview;
129        self
130    }
131
132    /// Defines the suggested reply markup for the message (such as adding inline buttons).
133    /// This will be displayed below the message.
134    ///
135    /// Only bot accounts can make use of the reply markup feature (a user attempting to send a
136    /// message with a reply markup will result in the markup being ignored by Telegram).
137    ///
138    /// The user is free to ignore the markup and continue sending usual text messages.
139    ///
140    /// See [`crate::reply_markup`] for the different available markups along with how
141    /// they behave.
142    pub fn reply_markup<RM: ReplyMarkup>(mut self, markup: &RM) -> Self {
143        self.reply_markup = Some(markup.to_reply_markup().raw);
144        self
145    }
146
147    /// The message identifier to which this message should reply to, if any.
148    ///
149    /// Otherwise, this message will not be a reply to any other.
150    pub fn reply_to(mut self, reply_to: Option<i32>) -> Self {
151        self.reply_to = reply_to;
152        self
153    }
154
155    /// If set to a distant enough future time, the message won't be sent immediately,
156    /// and instead it will be scheduled to be automatically sent at a later time.
157    ///
158    /// This scheduling is done server-side, and may not be accurate to the second.
159    ///
160    /// Bot accounts cannot schedule messages.
161    pub fn schedule_date(mut self, schedule_date: Option<SystemTime>) -> Self {
162        self.schedule_date = schedule_date.map(|t| {
163            t.duration_since(UNIX_EPOCH)
164                .map(|d| d.as_secs() as i32)
165                .unwrap_or(0)
166        });
167        self
168    }
169
170    /// Schedule the message to be sent once the person comes online.
171    ///
172    /// This only works in private chats, and only if the person has their
173    /// last seen visible.
174    ///
175    /// Bot accounts cannot schedule messages.
176    pub fn schedule_once_online(mut self) -> Self {
177        self.schedule_date = Some(SCHEDULE_ONCE_ONLINE);
178        self
179    }
180
181    /// Whether the message should notify people or not.
182    ///
183    /// Defaults to `false`, which means it will notify them. Set it to `true`
184    /// to alter this behaviour.
185    pub fn silent(mut self, silent: bool) -> Self {
186        self.silent = silent;
187        self
188    }
189
190    /// Include the uploaded file as a photo in the message.
191    ///
192    /// The Telegram server will compress the image and convert it to JPEG format if necessary.
193    ///
194    /// The text will be the caption of the photo, which may be empty for no caption.
195    pub fn photo(mut self, file: Uploaded) -> Self {
196        self.media = Some(
197            (tl::types::InputMediaUploadedPhoto {
198                spoiler: false,
199                file: file.raw,
200                stickers: None,
201                ttl_seconds: self.media_ttl,
202            })
203            .into(),
204        );
205        self
206    }
207
208    /// Include an external photo in the message.
209    ///
210    /// The Telegram server will download and compress the image and convert it to JPEG format if
211    /// necessary.
212    ///
213    /// The text will be the caption of the photo, which may be empty for no caption.
214    pub fn photo_url(mut self, url: impl Into<String>) -> Self {
215        self.media = Some(
216            (tl::types::InputMediaPhotoExternal {
217                spoiler: false,
218                url: url.into(),
219                ttl_seconds: self.media_ttl,
220            })
221            .into(),
222        );
223        self
224    }
225
226    /// Include the uploaded file as a document in the message.
227    ///
228    /// You can use this to send videos, stickers, audios, or uncompressed photos.
229    ///
230    /// The text will be the caption of the document, which may be empty for no caption.
231    pub fn document(mut self, file: Uploaded) -> Self {
232        let mime_type = self.get_file_mime(&file);
233        let file_name = file.name().to_string();
234        self.media = Some(
235            (tl::types::InputMediaUploadedDocument {
236                nosound_video: false,
237                force_file: false,
238                spoiler: false,
239                file: file.raw,
240                thumb: None,
241                mime_type,
242                attributes: vec![(tl::types::DocumentAttributeFilename { file_name }).into()],
243                stickers: None,
244                ttl_seconds: self.media_ttl,
245                video_cover: None,
246                video_timestamp: None,
247            })
248            .into(),
249        );
250        self
251    }
252
253    /// Include a media in the message using the raw TL types.
254    ///
255    /// You can use this to send any media using the raw TL types that don't have
256    /// a specific method in this builder such as Dice, Polls, etc.
257    ///
258    /// This can also be used to send media with a file reference, see `InputMediaDocument`
259    /// and `InputMediaPhoto` in the `grammers-tl-types` crate.
260    ///
261    /// The text will be the caption of the media, which may be empty for no caption.
262    pub fn media<M: Into<tl::enums::InputMedia>>(mut self, media: M) -> Self {
263        self.media = Some(media.into());
264        self
265    }
266
267    /// Include the video file with thumb in the message.
268    ///
269    /// The text will be the caption of the document, which may be empty for no caption.
270    ///
271    /// # Examples
272    ///
273    /// ```
274    /// async fn f(client: &mut grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
275    ///     use grammers_client::{InputMessage};
276    ///
277    ///     let video = client.upload_file("video.mp4").await?;
278    ///     let thumb = client.upload_file("thumb.png").await?;
279    ///     let message = InputMessage::new().text("").document(video).thumbnail(thumb);
280    ///     Ok(())
281    /// }
282    /// ```
283    pub fn thumbnail(mut self, thumb: Uploaded) -> Self {
284        if let Some(tl::enums::InputMedia::UploadedDocument(document)) = &mut self.media {
285            document.thumb = Some(thumb.raw);
286        }
287        self
288    }
289
290    /// Include an external file as a document in the message.
291    ///
292    /// You can use this to send videos, stickers, audios, or uncompressed photos.
293    ///
294    /// The Telegram server will be the one that downloads and includes the document as media.
295    ///
296    /// The text will be the caption of the document, which may be empty for no caption.
297    pub fn document_url(mut self, url: impl Into<String>) -> Self {
298        self.media = Some(
299            (tl::types::InputMediaDocumentExternal {
300                spoiler: false,
301                url: url.into(),
302                ttl_seconds: self.media_ttl,
303                video_cover: None,
304                video_timestamp: None,
305            })
306            .into(),
307        );
308        self
309    }
310
311    /// Add additional attributes to the message.
312    ///
313    /// This must be called *after* setting a file.
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// # async fn f(client: &mut grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
319    /// # let audio = client.upload_file("audio.flac").await?;
320    /// #
321    /// use std::time::Duration;
322    /// use grammers_client::{types::Attribute, InputMessage};
323    ///
324    /// let message = InputMessage::new().text("").document(audio).attribute(
325    ///    Attribute::Audio {
326    ///        duration: Duration::new(123, 0),
327    ///        title: Some("Hello".to_string()),
328    ///        performer: Some("World".to_string()),
329    ///    }
330    /// );
331    /// # Ok(())
332    /// # }
333    /// ```
334    pub fn attribute(mut self, attr: Attribute) -> Self {
335        if let Some(tl::enums::InputMedia::UploadedDocument(document)) = &mut self.media {
336            document.attributes.push(attr.into());
337        }
338        self
339    }
340
341    /// Copy media from an existing message.
342    ///
343    /// You can use this to send media from another message without re-uploading it.
344    pub fn copy_media(mut self, media: &Media) -> Self {
345        self.media = media.to_raw_input_media();
346        self
347    }
348
349    /// Include the uploaded file as a document file in the message.
350    ///
351    /// You can use this to send any type of media as a simple document file.
352    ///
353    /// The text will be the caption of the file, which may be empty for no caption.
354    pub fn file(mut self, file: Uploaded) -> Self {
355        let mime_type = self.get_file_mime(&file);
356        let file_name = file.name().to_string();
357        self.media = Some(
358            (tl::types::InputMediaUploadedDocument {
359                nosound_video: false,
360                force_file: true,
361                spoiler: false,
362                file: file.raw,
363                thumb: None,
364                mime_type,
365                attributes: vec![(tl::types::DocumentAttributeFilename { file_name }).into()],
366                stickers: None,
367                ttl_seconds: self.media_ttl,
368                video_cover: None,
369                video_timestamp: None,
370            })
371            .into(),
372        );
373        self
374    }
375
376    /// Change the media's Time To Live (TTL).
377    ///
378    /// For example, this enables you to send a `photo` that can only be viewed for a certain
379    /// amount of seconds before it expires.
380    ///
381    /// Not all media supports this feature.
382    ///
383    /// This method should be called before setting any media, else it won't have any effect.
384    pub fn media_ttl(mut self, seconds: i32) -> Self {
385        self.media_ttl = if seconds < 0 { None } else { Some(seconds) };
386        self
387    }
388
389    /// Change the media's mime type.
390    ///
391    /// This method will override the mime type that would otherwise be automatically inferred
392    /// from the extension of the used file
393    ///
394    /// If no mime type is set and it cannot be inferred, the mime type will be
395    /// "application/octet-stream".
396    ///
397    /// This method should be called before setting any media, else it won't have any effect.
398    pub fn mime_type(mut self, mime_type: &str) -> Self {
399        self.mime_type = Some(mime_type.to_string());
400        self
401    }
402
403    /// Return the mime type string for the given file.
404    fn get_file_mime(&self, file: &Uploaded) -> String {
405        if let Some(mime) = self.mime_type.as_ref() {
406            mime.clone()
407        } else if let Some(mime) = mime_guess::from_path(file.name()).first() {
408            mime.essence_str().to_string()
409        } else {
410            "application/octet-stream".to_string()
411        }
412    }
413}
414
415impl<S> From<S> for InputMessage
416where
417    S: Into<String>,
418{
419    fn from(text: S) -> Self {
420        Self::new().text(text)
421    }
422}
423
424impl From<super::Message> for InputMessage {
425    fn from(message: super::Message) -> Self {
426        let (text, entities, media) = match message.raw {
427            tl::enums::Message::Empty(_) => (None, None, None),
428            tl::enums::Message::Message(message) => {
429                (Some(message.message), message.entities, message.media)
430            }
431            tl::enums::Message::Service(_) => (None, None, None),
432        };
433
434        Self {
435            text: text.unwrap_or_default(),
436            entities: entities.unwrap_or_default(),
437            media: media
438                .and_then(Media::from_raw)
439                .and_then(|m| m.to_raw_input_media()),
440            ..Default::default()
441        }
442    }
443}