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}