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            })
206            .into(),
207        );
208        self
209    }
210
211    /// Include an external photo in the message.
212    ///
213    /// The Telegram server will download and compress the image and convert it to JPEG format if
214    /// necessary.
215    ///
216    /// The text will be the caption of the photo, which may be empty for no caption.
217    pub fn photo_url(mut self, url: impl Into<String>) -> Self {
218        self.media = Some(
219            (tl::types::InputMediaPhotoExternal {
220                spoiler: false,
221                url: url.into(),
222                ttl_seconds: self.media_ttl,
223            })
224            .into(),
225        );
226        self
227    }
228
229    /// Include the uploaded file as a document in the message.
230    ///
231    /// You can use this to send videos, stickers, audios, or uncompressed photos.
232    ///
233    /// The text will be the caption of the document, which may be empty for no caption.
234    pub fn document(mut self, file: Uploaded) -> Self {
235        let mime_type = self.get_file_mime(&file);
236        let file_name = file.name().to_string();
237        self.media = Some(
238            (tl::types::InputMediaUploadedDocument {
239                nosound_video: false,
240                force_file: false,
241                spoiler: false,
242                file: file.raw,
243                thumb: None,
244                mime_type,
245                attributes: vec![(tl::types::DocumentAttributeFilename { file_name }).into()],
246                stickers: None,
247                ttl_seconds: self.media_ttl,
248                video_cover: None,
249                video_timestamp: None,
250            })
251            .into(),
252        );
253        self
254    }
255
256    /// Include a media in the message using the raw TL types.
257    ///
258    /// You can use this to send any media using the raw TL types that don't have
259    /// a specific method in this builder such as Dice, Polls, etc.
260    ///
261    /// This can also be used to send media with a file reference, see `InputMediaDocument`
262    /// and `InputMediaPhoto` in the `grammers-tl-types` crate.
263    ///
264    /// The text will be the caption of the media, which may be empty for no caption.
265    pub fn media<M: Into<tl::enums::InputMedia>>(mut self, media: M) -> Self {
266        self.media = Some(media.into());
267        self
268    }
269
270    /// Include the video file with thumb in the message.
271    ///
272    /// The text will be the caption of the document, which may be empty for no caption.
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// async fn f(client: &mut grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
278    ///     use grammers_client::message::InputMessage;
279    ///
280    ///     let video = client.upload_file("video.mp4").await?;
281    ///     let thumb = client.upload_file("thumb.png").await?;
282    ///     let message = InputMessage::new().text("").document(video).thumbnail(thumb);
283    ///     Ok(())
284    /// }
285    /// ```
286    pub fn thumbnail(mut self, thumb: Uploaded) -> Self {
287        if let Some(tl::enums::InputMedia::UploadedDocument(document)) = &mut self.media {
288            document.thumb = Some(thumb.raw);
289        }
290        self
291    }
292
293    /// Include an external file as a document in the message.
294    ///
295    /// You can use this to send videos, stickers, audios, or uncompressed photos.
296    ///
297    /// The Telegram server will be the one that downloads and includes the document as media.
298    ///
299    /// The text will be the caption of the document, which may be empty for no caption.
300    pub fn document_url(mut self, url: impl Into<String>) -> Self {
301        self.media = Some(
302            (tl::types::InputMediaDocumentExternal {
303                spoiler: false,
304                url: url.into(),
305                ttl_seconds: self.media_ttl,
306                video_cover: None,
307                video_timestamp: None,
308            })
309            .into(),
310        );
311        self
312    }
313
314    /// Add additional attributes to the message.
315    ///
316    /// This must be called *after* setting a file.
317    ///
318    /// # Examples
319    ///
320    /// ```
321    /// # async fn f(client: &mut grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
322    /// # let audio = client.upload_file("audio.flac").await?;
323    /// #
324    /// use std::time::Duration;
325    /// use grammers_client::media::Attribute;
326    /// use grammers_client::message::InputMessage;
327    ///
328    /// let message = InputMessage::new().text("").document(audio).attribute(
329    ///    Attribute::Audio {
330    ///        duration: Duration::new(123, 0),
331    ///        title: Some("Hello".to_string()),
332    ///        performer: Some("World".to_string()),
333    ///    }
334    /// );
335    /// # Ok(())
336    /// # }
337    /// ```
338    pub fn attribute(mut self, attr: Attribute) -> Self {
339        if let Some(tl::enums::InputMedia::UploadedDocument(document)) = &mut self.media {
340            document.attributes.push(attr.into());
341        }
342        self
343    }
344
345    /// Copy media from an existing message.
346    ///
347    /// You can use this to send media from another message without re-uploading it.
348    pub fn copy_media(mut self, media: &Media) -> Self {
349        self.media = media.to_raw_input_media();
350        self
351    }
352
353    /// Include the uploaded file as a document file in the message.
354    ///
355    /// You can use this to send any type of media as a simple document file.
356    ///
357    /// The text will be the caption of the file, which may be empty for no caption.
358    pub fn file(mut self, file: Uploaded) -> Self {
359        let mime_type = self.get_file_mime(&file);
360        let file_name = file.name().to_string();
361        self.media = Some(
362            (tl::types::InputMediaUploadedDocument {
363                nosound_video: false,
364                force_file: true,
365                spoiler: false,
366                file: file.raw,
367                thumb: None,
368                mime_type,
369                attributes: vec![(tl::types::DocumentAttributeFilename { file_name }).into()],
370                stickers: None,
371                ttl_seconds: self.media_ttl,
372                video_cover: None,
373                video_timestamp: None,
374            })
375            .into(),
376        );
377        self
378    }
379
380    /// Change the media's Time To Live (TTL).
381    ///
382    /// For example, this enables you to send a `photo` that can only be viewed for a certain
383    /// amount of seconds before it expires.
384    ///
385    /// Not all media supports this feature.
386    ///
387    /// This method should be called before setting any media, else it won't have any effect.
388    pub fn media_ttl(mut self, seconds: i32) -> Self {
389        self.media_ttl = if seconds < 0 { None } else { Some(seconds) };
390        self
391    }
392
393    /// Change the media's mime type.
394    ///
395    /// This method will override the mime type that would otherwise be automatically inferred
396    /// from the extension of the used file
397    ///
398    /// If no mime type is set and it cannot be inferred, the mime type will be
399    /// "application/octet-stream".
400    ///
401    /// This method should be called before setting any media, else it won't have any effect.
402    pub fn mime_type(mut self, mime_type: &str) -> Self {
403        self.mime_type = Some(mime_type.to_string());
404        self
405    }
406
407    /// Return the mime type string for the given file.
408    fn get_file_mime(&self, file: &Uploaded) -> String {
409        if let Some(mime) = self.mime_type.as_ref() {
410            mime.clone()
411        } else if let Some(mime) = mime_guess::from_path(file.name()).first() {
412            mime.essence_str().to_string()
413        } else {
414            "application/octet-stream".to_string()
415        }
416    }
417}
418
419impl<S> From<S> for InputMessage
420where
421    S: Into<String>,
422{
423    fn from(text: S) -> Self {
424        Self::new().text(text)
425    }
426}
427
428impl From<super::Message> for InputMessage {
429    fn from(message: super::Message) -> Self {
430        let (text, entities, media) = match message.raw {
431            tl::enums::Message::Empty(_) => (None, None, None),
432            tl::enums::Message::Message(message) => {
433                (Some(message.message), message.entities, message.media)
434            }
435            tl::enums::Message::Service(_) => (None, None, None),
436        };
437
438        Self {
439            text: text.unwrap_or_default(),
440            entities: entities.unwrap_or_default(),
441            media: media
442                .and_then(Media::from_raw)
443                .and_then(|m| m.to_raw_input_media()),
444            ..Default::default()
445        }
446    }
447}