grammers_client/types/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#[cfg(any(feature = "markdown", feature = "html"))]
9use crate::parsers;
10use crate::types::reactions::InputReactions;
11use crate::types::{Downloadable, InputMessage, Media, Photo};
12use crate::ChatMap;
13use crate::{types, Client};
14use crate::{utils, InputMedia};
15use chrono::{DateTime, Utc};
16use grammers_mtsender::InvocationError;
17use grammers_session::PackedChat;
18use grammers_tl_types as tl;
19use std::fmt;
20use std::io;
21use std::path::Path;
22use std::sync::Arc;
23use types::Chat;
24
25/// Represents a Telegram message, which includes text messages, messages with media, and service
26/// messages.
27///
28/// This message should be treated as a snapshot in time, that is, if the message is edited while
29/// using this object, those changes won't alter this structure.
30#[derive(Clone)]
31pub struct Message {
32 // Message services are a trimmed-down version of normal messages, but with `action`.
33 //
34 // Using `enum` just for that would clutter all methods with `match`, so instead service
35 // messages are interpreted as messages and their action stored separatedly.
36 pub raw: tl::types::Message,
37 pub raw_action: Option<tl::enums::MessageAction>,
38 pub(crate) client: Client,
39 // When fetching messages or receiving updates, a set of chats will be present. A single
40 // server response contains a lot of chats, and some might be related to deep layers of
41 // a message action for instance. Keeping the entire set like this allows for cheaper clones
42 // and moves, and saves us from worrying about picking out all the chats we care about.
43 pub(crate) chats: Arc<ChatMap>,
44}
45
46impl Message {
47 pub fn from_raw(
48 client: &Client,
49 message: tl::enums::Message,
50 chats: &Arc<ChatMap>,
51 ) -> Option<Self> {
52 match message {
53 // Don't even bother to expose empty messages to the user, even if they have an ID.
54 tl::enums::Message::Empty(_) => None,
55 tl::enums::Message::Message(msg) => Some(Message {
56 raw: msg,
57 raw_action: None,
58 client: client.clone(),
59 chats: Arc::clone(chats),
60 }),
61 tl::enums::Message::Service(msg) => Some(Message {
62 raw: tl::types::Message {
63 out: msg.out,
64 mentioned: msg.mentioned,
65 media_unread: msg.media_unread,
66 silent: msg.silent,
67 post: msg.post,
68 from_scheduled: false,
69 legacy: msg.legacy,
70 edit_hide: false,
71 pinned: false,
72 noforwards: false,
73 invert_media: false,
74 id: msg.id,
75 from_id: msg.from_id,
76 from_boosts_applied: None,
77 peer_id: msg.peer_id,
78 saved_peer_id: None,
79 fwd_from: None,
80 via_bot_id: None,
81 reply_to: msg.reply_to,
82 date: msg.date,
83 message: String::new(),
84 media: None,
85 reply_markup: None,
86 entities: None,
87 views: None,
88 forwards: None,
89 replies: None,
90 edit_date: None,
91 post_author: None,
92 grouped_id: None,
93 restriction_reason: None,
94 ttl_period: msg.ttl_period,
95 reactions: None,
96 quick_reply_shortcut_id: None,
97 via_business_bot_id: None,
98 offline: false,
99 effect: None,
100 factcheck: None,
101 },
102 raw_action: Some(msg.action),
103 client: client.clone(),
104 chats: Arc::clone(chats),
105 }),
106 }
107 }
108
109 pub fn from_raw_short_updates(
110 client: &Client,
111 updates: tl::types::UpdateShortSentMessage,
112 input: InputMessage,
113 chat: PackedChat,
114 ) -> Self {
115 Self {
116 raw: tl::types::Message {
117 out: updates.out,
118 mentioned: false,
119 media_unread: false,
120 silent: input.silent,
121 post: false, // TODO true if sent to broadcast channel
122 from_scheduled: false,
123 legacy: false,
124 edit_hide: false,
125 pinned: false,
126 noforwards: false, // TODO true if channel has noforwads?
127 invert_media: input.invert_media,
128 id: updates.id,
129 from_id: None, // TODO self
130 from_boosts_applied: None,
131 peer_id: chat.to_peer(),
132 saved_peer_id: None,
133 fwd_from: None,
134 via_bot_id: None,
135 reply_to: input.reply_to.map(|reply_to_msg_id| {
136 tl::types::MessageReplyHeader {
137 reply_to_scheduled: false,
138 forum_topic: false,
139 quote: false,
140 reply_to_msg_id: Some(reply_to_msg_id),
141 reply_to_peer_id: None,
142 reply_from: None,
143 reply_media: None,
144 reply_to_top_id: None,
145 quote_text: None,
146 quote_entities: None,
147 quote_offset: None,
148 }
149 .into()
150 }),
151 date: updates.date,
152 message: input.text,
153 media: updates.media,
154 reply_markup: input.reply_markup,
155 entities: updates.entities,
156 views: None,
157 forwards: None,
158 replies: None,
159 edit_date: None,
160 post_author: None,
161 grouped_id: None,
162 restriction_reason: None,
163 ttl_period: updates.ttl_period,
164 reactions: None,
165 quick_reply_shortcut_id: None,
166 via_business_bot_id: None,
167 offline: false,
168 effect: None,
169 factcheck: None,
170 },
171 raw_action: None,
172 client: client.clone(),
173 chats: ChatMap::single(Chat::unpack(chat)),
174 }
175 }
176
177 /// Whether the message is outgoing (i.e. you sent this message to some other chat) or
178 /// incoming (i.e. someone else sent it to you or the chat).
179 pub fn outgoing(&self) -> bool {
180 self.raw.out
181 }
182
183 /// Whether you were mentioned in this message or not.
184 ///
185 /// This includes @username mentions, text mentions, and messages replying to one of your
186 /// previous messages (even if it contains no mention in the message text).
187 pub fn mentioned(&self) -> bool {
188 self.raw.mentioned
189 }
190
191 /// Whether you have read the media in this message or not.
192 ///
193 /// Most commonly, these are voice notes that you have not played yet.
194 pub fn media_unread(&self) -> bool {
195 self.raw.media_unread
196 }
197
198 /// Whether the message should notify people with sound or not.
199 pub fn silent(&self) -> bool {
200 self.raw.silent
201 }
202
203 /// Whether this message is a post in a broadcast channel or not.
204 pub fn post(&self) -> bool {
205 self.raw.post
206 }
207
208 /// Whether this message was originated from a previously-scheduled message or not.
209 pub fn from_scheduled(&self) -> bool {
210 self.raw.from_scheduled
211 }
212
213 // `legacy` is not exposed, though it can be if it proves to be useful
214
215 /// Whether the edited mark of this message is edited should be hidden (e.g. in GUI clients)
216 /// or shown.
217 pub fn edit_hide(&self) -> bool {
218 self.raw.edit_hide
219 }
220
221 /// Whether this message is currently pinned or not.
222 pub fn pinned(&self) -> bool {
223 self.raw.pinned
224 }
225
226 /// The ID of this message.
227 ///
228 /// Message identifiers are counters that start at 1 and grow by 1 for each message produced.
229 ///
230 /// Every channel has its own unique message counter. This counter is the same for all users,
231 /// but unique to each channel.
232 ///
233 /// Every account has another unique message counter which is used for private conversations
234 /// and small group chats. This means different accounts will likely have different message
235 /// identifiers for the same message in a private conversation or small group chat. This also
236 /// implies that the message identifier alone is enough to uniquely identify the message,
237 /// without the need to know the chat ID.
238 ///
239 /// **You cannot use the message ID of User A when running as User B**, unless this message
240 /// belongs to a megagroup or broadcast channel. Beware of this when using methods like
241 /// [`Client::delete_messages`], which **cannot** validate the chat where the message
242 /// should be deleted for those cases.
243 pub fn id(&self) -> i32 {
244 self.raw.id
245 }
246
247 /// The sender of this message, if any.
248 pub fn sender(&self) -> Option<types::Chat> {
249 self.raw
250 .from_id
251 .as_ref()
252 .or({
253 // Incoming messages in private conversations don't include `from_id` since
254 // layer 119, but the sender can only be the chat we're in.
255 if !self.raw.out && matches!(self.raw.peer_id, tl::enums::Peer::User(_)) {
256 Some(&self.raw.peer_id)
257 } else {
258 None
259 }
260 })
261 .map(|from| utils::always_find_entity(from, &self.chats, &self.client))
262 }
263
264 /// The chat where this message was sent to.
265 ///
266 /// This might be the user you're talking to for private conversations, or the group or
267 /// channel where the message was sent.
268 pub fn chat(&self) -> types::Chat {
269 utils::always_find_entity(&self.raw.peer_id, &self.chats, &self.client)
270 }
271
272 /// If this message was forwarded from a previous message, return the header with information
273 /// about that forward.
274 pub fn forward_header(&self) -> Option<tl::enums::MessageFwdHeader> {
275 self.raw.fwd_from.clone()
276 }
277
278 /// If this message was sent @via some inline bot, return the bot's user identifier.
279 pub fn via_bot_id(&self) -> Option<i64> {
280 self.raw.via_bot_id
281 }
282
283 /// If this message is replying to a previous message, return the header with information
284 /// about that reply.
285 pub fn reply_header(&self) -> Option<tl::enums::MessageReplyHeader> {
286 self.raw.reply_to.clone()
287 }
288
289 /// The date when this message was produced.
290 pub fn date(&self) -> DateTime<Utc> {
291 utils::date(self.raw.date)
292 }
293
294 /// The message's text.
295 ///
296 /// For service messages, this will be the empty strings.
297 ///
298 /// If the message has media, this text is the caption commonly displayed underneath it.
299 pub fn text(&self) -> &str {
300 &self.raw.message
301 }
302
303 /// Like [`text`](Self::text), but with the [`fmt_entities`](Self::fmt_entities)
304 /// applied to produce a markdown string instead.
305 ///
306 /// Some formatting entities automatically added by Telegram, such as bot commands or
307 /// clickable emails, are ignored in the generated string, as those do not need to be
308 /// sent for Telegram to include them in the message.
309 ///
310 /// Formatting entities which cannot be represented in CommonMark without resorting to HTML,
311 /// such as underline, are also ignored.
312 #[cfg(feature = "markdown")]
313 pub fn markdown_text(&self) -> String {
314 if let Some(entities) = self.raw.entities.as_ref() {
315 parsers::generate_markdown_message(&self.raw.message, entities)
316 } else {
317 self.raw.message.clone()
318 }
319 }
320
321 /// Like [`text`](Self::text), but with the [`fmt_entities`](Self::fmt_entities)
322 /// applied to produce a HTML string instead.
323 ///
324 /// Some formatting entities automatically added by Telegram, such as bot commands or
325 /// clickable emails, are ignored in the generated string, as those do not need to be
326 /// sent for Telegram to include them in the message.
327 #[cfg(feature = "html")]
328 pub fn html_text(&self) -> String {
329 if let Some(entities) = self.raw.entities.as_ref() {
330 parsers::generate_html_message(&self.raw.message, entities)
331 } else {
332 self.raw.message.clone()
333 }
334 }
335
336 /// The media displayed by this message, if any.
337 ///
338 /// This not only includes photos or videos, but also contacts, polls, documents, locations
339 /// and many other types.
340 pub fn media(&self) -> Option<types::Media> {
341 self.raw.media.clone().and_then(|x| Media::from_raw(x))
342 }
343
344 /// If the message has a reply markup (which can happen for messages produced by bots),
345 /// returns said markup.
346 pub fn reply_markup(&self) -> Option<tl::enums::ReplyMarkup> {
347 self.raw.reply_markup.clone()
348 }
349
350 /// The formatting entities used to format this message, such as bold, italic, with their
351 /// offsets and lengths.
352 pub fn fmt_entities(&self) -> Option<&Vec<tl::enums::MessageEntity>> {
353 // TODO correct the offsets and lengths to match the byte offsets
354 self.raw.entities.as_ref()
355 }
356
357 /// How many views does this message have, when applicable.
358 ///
359 /// The same user account can contribute to increment this counter indefinitedly, however
360 /// there is a server-side cooldown limitting how fast it can happen (several hours).
361 pub fn view_count(&self) -> Option<i32> {
362 self.raw.views
363 }
364
365 /// How many times has this message been forwarded, when applicable.
366 pub fn forward_count(&self) -> Option<i32> {
367 self.raw.forwards
368 }
369
370 /// How many replies does this message have, when applicable.
371 pub fn reply_count(&self) -> Option<i32> {
372 match &self.raw.replies {
373 None => None,
374 Some(replies) => {
375 let tl::enums::MessageReplies::Replies(replies) = replies;
376 Some(replies.replies)
377 }
378 }
379 }
380
381 /// React to this message.
382 ///
383 /// # Examples
384 ///
385 /// ```
386 /// # async fn f(message: grammers_client::types::Message, client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
387 /// message.react("👍").await?;
388 /// # Ok(())
389 /// # }
390 /// ```
391 ///
392 /// Make animation big & Add to recent
393 ///
394 /// ```
395 /// # async fn f(message: grammers_client::types::Message, client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
396 /// use grammers_client::types::InputReactions;
397 ///
398 /// let reactions = InputReactions::emoticon("🤯").big().add_to_recent();
399 ///
400 /// message.react(reactions).await?;
401 /// # Ok(())
402 /// # }
403 /// ```
404 ///
405 /// Remove reactions
406 ///
407 /// ```
408 /// # async fn f(message: grammers_client::types::Message, client: grammers_client::Client) -> Result<(), Box<dyn std::error::Error>> {
409 /// use grammers_client::types::InputReactions;
410 ///
411 /// message.react(InputReactions::remove()).await?;
412 /// # Ok(())
413 /// # }
414 /// ```
415 pub async fn react<R: Into<InputReactions>>(
416 &self,
417 reactions: R,
418 ) -> Result<(), InvocationError> {
419 self.client
420 .send_reactions(self.chat(), self.id(), reactions)
421 .await?;
422 Ok(())
423 }
424
425 /// How many reactions does this message have, when applicable.
426 pub fn reaction_count(&self) -> Option<i32> {
427 match &self.raw.reactions {
428 None => None,
429 Some(reactions) => {
430 let tl::enums::MessageReactions::Reactions(reactions) = reactions;
431 let count = reactions
432 .results
433 .iter()
434 .map(|reaction: &tl::enums::ReactionCount| {
435 let tl::enums::ReactionCount::Count(reaction) = reaction;
436 reaction.count
437 })
438 .sum();
439 Some(count)
440 }
441 }
442 }
443
444 /// The date when this message was last edited.
445 pub fn edit_date(&self) -> Option<DateTime<Utc>> {
446 self.raw.edit_date.map(utils::date)
447 }
448
449 /// If this message was sent to a channel, return the name used by the author to post it.
450 pub fn post_author(&self) -> Option<&str> {
451 self.raw.post_author.as_ref().map(|author| author.as_ref())
452 }
453
454 /// If this message belongs to a group of messages, return the unique identifier for that
455 /// group.
456 ///
457 /// This applies to albums of media, such as multiple photos grouped together.
458 ///
459 /// Note that there may be messages sent in between the messages forming a group.
460 pub fn grouped_id(&self) -> Option<i64> {
461 self.raw.grouped_id
462 }
463
464 /// A list of reasons on why this message is restricted.
465 ///
466 /// The message is not restricted if the return value is `None`.
467 pub fn restriction_reason(&self) -> Option<&Vec<tl::enums::RestrictionReason>> {
468 self.raw.restriction_reason.as_ref()
469 }
470
471 /// If this message is a service message, return the service action that occured.
472 pub fn action(&self) -> Option<&tl::enums::MessageAction> {
473 self.raw_action.as_ref()
474 }
475
476 /// If this message is replying to another message, return the replied message ID.
477 pub fn reply_to_message_id(&self) -> Option<i32> {
478 if let Some(tl::enums::MessageReplyHeader::Header(m)) = &self.raw.reply_to {
479 m.reply_to_msg_id
480 } else {
481 None
482 }
483 }
484
485 /// Fetch the message that this message is replying to, or `None` if this message is not a
486 /// reply to a previous message.
487 ///
488 /// Shorthand for `Client::get_reply_to_message`.
489 pub async fn get_reply(&self) -> Result<Option<Self>, InvocationError> {
490 self.client
491 .clone() // TODO don't clone
492 .get_reply_to_message(self)
493 .await
494 }
495
496 /// Respond to this message by sending a new message in the same chat, but without directly
497 /// replying to it.
498 ///
499 /// Shorthand for `Client::send_message`.
500 pub async fn respond<M: Into<InputMessage>>(
501 &self,
502 message: M,
503 ) -> Result<Self, InvocationError> {
504 self.client.send_message(&self.chat(), message).await
505 }
506
507 /// Respond to this message by sending a album in the same chat, but without directly
508 /// replying to it.
509 ///
510 /// Shorthand for `Client::send_album`.
511 pub async fn respond_album(
512 &self,
513 medias: Vec<InputMedia>,
514 ) -> Result<Vec<Option<Self>>, InvocationError> {
515 self.client.send_album(&self.chat(), medias).await
516 }
517
518 /// Directly reply to this message by sending a new message in the same chat that replies to
519 /// it. This methods overrides the `reply_to` on the `InputMessage` to point to `self`.
520 ///
521 /// Shorthand for `Client::send_message`.
522 pub async fn reply<M: Into<InputMessage>>(&self, message: M) -> Result<Self, InvocationError> {
523 let message = message.into();
524 self.client
525 .send_message(&self.chat(), message.reply_to(Some(self.raw.id)))
526 .await
527 }
528
529 /// Directly reply to this message by sending a album in the same chat that replies to
530 /// it. This methods overrides the `reply_to` on the first `InputMedia` to point to `self`.
531 ///
532 /// Shorthand for `Client::send_album`.
533 pub async fn reply_album(
534 &self,
535 mut medias: Vec<InputMedia>,
536 ) -> Result<Vec<Option<Self>>, InvocationError> {
537 medias.first_mut().unwrap().reply_to = Some(self.raw.id);
538 self.client.send_album(&self.chat(), medias).await
539 }
540
541 /// Forward this message to another (or the same) chat.
542 ///
543 /// Shorthand for `Client::forward_messages`. If you need to forward multiple messages
544 /// at once, consider using that method instead.
545 pub async fn forward_to<C: Into<PackedChat>>(&self, chat: C) -> Result<Self, InvocationError> {
546 // TODO return `Message`
547 // When forwarding a single message, if it fails, Telegram should respond with RPC error.
548 // If it succeeds we will have the single forwarded message present which we can unwrap.
549 self.client
550 .forward_messages(chat, &[self.raw.id], &self.chat())
551 .await
552 .map(|mut msgs| msgs.pop().unwrap().unwrap())
553 }
554
555 /// Edit this message to change its text or media.
556 ///
557 /// Shorthand for `Client::edit_message`.
558 pub async fn edit<M: Into<InputMessage>>(&self, new_message: M) -> Result<(), InvocationError> {
559 self.client
560 .edit_message(&self.chat(), self.raw.id, new_message)
561 .await
562 }
563
564 /// Delete this message for everyone.
565 ///
566 /// Shorthand for `Client::delete_messages`. If you need to delete multiple messages
567 /// at once, consider using that method instead.
568 pub async fn delete(&self) -> Result<(), InvocationError> {
569 self.client
570 .delete_messages(&self.chat(), &[self.raw.id])
571 .await
572 .map(drop)
573 }
574
575 /// Mark this message and all messages above it as read.
576 ///
577 /// Unlike `Client::mark_as_read`, this method only will mark the chat as read up to
578 /// this message, not the entire chat.
579 pub async fn mark_as_read(&self) -> Result<(), InvocationError> {
580 let chat = self.chat().pack();
581 if let Some(channel) = chat.try_to_input_channel() {
582 self.client
583 .invoke(&tl::functions::channels::ReadHistory {
584 channel,
585 max_id: self.raw.id,
586 })
587 .await
588 .map(drop)
589 } else {
590 self.client
591 .invoke(&tl::functions::messages::ReadHistory {
592 peer: chat.to_input_peer(),
593 max_id: self.raw.id,
594 })
595 .await
596 .map(drop)
597 }
598 }
599
600 /// Pin this message in the chat.
601 ///
602 /// Shorthand for `Client::pin_message`.
603 pub async fn pin(&self) -> Result<(), InvocationError> {
604 self.client.pin_message(&self.chat(), self.raw.id).await
605 }
606
607 /// Unpin this message from the chat.
608 ///
609 /// Shorthand for `Client::unpin_message`.
610 pub async fn unpin(&self) -> Result<(), InvocationError> {
611 self.client.unpin_message(&self.chat(), self.raw.id).await
612 }
613
614 /// Refetch this message, mutating all of its properties in-place.
615 ///
616 /// No changes will be made to the message if it fails to be fetched.
617 ///
618 /// Shorthand for `Client::get_messages_by_id`.
619 pub async fn refetch(&self) -> Result<(), InvocationError> {
620 // When fetching a single message, if it fails, Telegram should respond with RPC error.
621 // If it succeeds we will have the single message present which we can unwrap.
622 self.client
623 .get_messages_by_id(&self.chat(), &[self.raw.id])
624 .await?
625 .pop()
626 .unwrap()
627 .unwrap();
628 todo!("actually mutate self after get_messages_by_id returns `Message`")
629 }
630
631 /// Download the message media in this message if applicable.
632 ///
633 /// Returns `true` if there was media to download, or `false` otherwise.
634 ///
635 /// Shorthand for `Client::download_media`.
636 pub async fn download_media<P: AsRef<Path>>(&self, path: P) -> Result<bool, io::Error> {
637 // TODO probably encode failed download in error
638 if let Some(media) = self.media() {
639 self.client
640 .download_media(&Downloadable::Media(media), path)
641 .await
642 .map(|_| true)
643 } else {
644 Ok(false)
645 }
646 }
647
648 /// Get photo attached to the message if any.
649 pub fn photo(&self) -> Option<Photo> {
650 if let Media::Photo(photo) = self.media()? {
651 return Some(photo);
652 }
653
654 None
655 }
656}
657
658impl fmt::Debug for Message {
659 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
660 f.debug_struct("Message")
661 .field("id", &self.id())
662 .field("outgoing", &self.outgoing())
663 .field("date", &self.date())
664 .field("text", &self.text())
665 .field("chat", &self.chat())
666 .field("sender", &self.sender())
667 .field("reply_to_message_id", &self.reply_to_message_id())
668 .field("via_bot_id", &self.via_bot_id())
669 .field("media", &self.media())
670 .field("mentioned", &self.mentioned())
671 .field("media_unread", &self.media_unread())
672 .field("silent", &self.silent())
673 .field("post", &self.post())
674 .field("from_scheduled", &self.from_scheduled())
675 .field("edit_hide", &self.edit_hide())
676 .field("pinned", &self.pinned())
677 .field("forward_header", &self.forward_header())
678 .field("reply_header", &self.reply_header())
679 .field("reply_markup", &self.reply_markup())
680 .field("fmt_entities", &self.fmt_entities())
681 .field("view_count", &self.view_count())
682 .field("forward_count", &self.forward_count())
683 .field("reply_count", &self.reply_count())
684 .field("edit_date", &self.edit_date())
685 .field("post_author", &self.post_author())
686 .field("grouped_id", &self.grouped_id())
687 .field("restriction_reason", &self.restriction_reason())
688 .field("action", &self.action())
689 .finish()
690 }
691}