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}