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}