Skip to main content

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