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}