Skip to main content

revolt_models/v0/
messages.rs

1use std::time::SystemTime;
2
3use indexmap::{IndexMap, IndexSet};
4use revolt_config::config;
5
6#[cfg(feature = "validator")]
7use validator::Validate;
8
9#[cfg(feature = "rocket")]
10use rocket::{FromForm, FromFormField};
11
12use iso8601_timestamp::Timestamp;
13
14use super::{Channel, Embed, File, Member, MessageWebhook, User, Webhook, RE_COLOUR};
15
16auto_derived_partial!(
17    /// Message
18    pub struct Message {
19        /// Unique Id
20        #[serde(rename = "_id")]
21        pub id: String,
22        /// Unique value generated by client sending this message
23        #[serde(skip_serializing_if = "Option::is_none")]
24        pub nonce: Option<String>,
25        /// Id of the channel this message was sent in
26        pub channel: String,
27        /// Id of the user or webhook that sent this message
28        pub author: String,
29        /// The user that sent this message
30        #[serde(skip_serializing_if = "Option::is_none")]
31        pub user: Option<User>,
32        /// The member that sent this message
33        #[serde(skip_serializing_if = "Option::is_none")]
34        pub member: Option<Member>,
35        /// The webhook that sent this message
36        #[serde(skip_serializing_if = "Option::is_none")]
37        pub webhook: Option<MessageWebhook>,
38        /// Message content
39        #[serde(skip_serializing_if = "Option::is_none")]
40        pub content: Option<String>,
41        /// System message
42        #[serde(skip_serializing_if = "Option::is_none")]
43        pub system: Option<SystemMessage>,
44        /// Array of attachments
45        #[serde(skip_serializing_if = "Option::is_none")]
46        pub attachments: Option<Vec<File>>,
47        /// Time at which this message was last edited
48        #[serde(skip_serializing_if = "Option::is_none")]
49        pub edited: Option<Timestamp>,
50        /// Attached embeds to this message
51        #[serde(skip_serializing_if = "Option::is_none")]
52        pub embeds: Option<Vec<Embed>>,
53        /// Array of user ids mentioned in this message
54        #[serde(skip_serializing_if = "Option::is_none")]
55        pub mentions: Option<Vec<String>>,
56        /// Array of role ids mentioned in this message
57        #[serde(skip_serializing_if = "Option::is_none")]
58        pub role_mentions: Option<Vec<String>>,
59        /// Array of message ids this message is replying to
60        #[serde(skip_serializing_if = "Option::is_none")]
61        pub replies: Option<Vec<String>>,
62        /// Hashmap of emoji IDs to array of user IDs
63        #[serde(skip_serializing_if = "IndexMap::is_empty", default)]
64        pub reactions: IndexMap<String, IndexSet<String>>,
65        /// Information about how this message should be interacted with
66        #[serde(skip_serializing_if = "Interactions::is_default", default)]
67        pub interactions: Interactions,
68        /// Name and / or avatar overrides for this message
69        #[serde(skip_serializing_if = "Option::is_none")]
70        pub masquerade: Option<Masquerade>,
71        /// Whether or not the message in pinned
72        #[serde(skip_serializing_if = "crate::if_option_false")]
73        pub pinned: Option<bool>,
74
75        /// Bitfield of message flags
76        ///
77        /// https://docs.rs/revolt-models/latest/revolt_models/v0/enum.MessageFlags.html
78        #[cfg_attr(
79            feature = "serde",
80            serde(skip_serializing_if = "crate::if_zero_u32", default)
81        )]
82        pub flags: u32,
83    },
84    "PartialMessage"
85);
86
87auto_derived!(
88    /// Bulk Message Response
89    #[serde(untagged)]
90    pub enum BulkMessageResponse {
91        JustMessages(
92            /// List of messages
93            Vec<Message>,
94        ),
95        MessagesAndUsers {
96            /// List of messages
97            messages: Vec<Message>,
98            /// List of users
99            users: Vec<User>,
100            /// List of members
101            #[serde(skip_serializing_if = "Option::is_none")]
102            members: Option<Vec<Member>>,
103        },
104    }
105
106    /// System Event
107    #[serde(tag = "type")]
108    pub enum SystemMessage {
109        #[serde(rename = "text")]
110        Text { content: String },
111        #[serde(rename = "user_added")]
112        UserAdded { id: String, by: String },
113        #[serde(rename = "user_remove")]
114        UserRemove { id: String, by: String },
115        #[serde(rename = "user_joined")]
116        UserJoined { id: String },
117        #[serde(rename = "user_left")]
118        UserLeft { id: String },
119        #[serde(rename = "user_kicked")]
120        UserKicked { id: String },
121        #[serde(rename = "user_banned")]
122        UserBanned { id: String },
123        #[serde(rename = "channel_renamed")]
124        ChannelRenamed { name: String, by: String },
125        #[serde(rename = "channel_description_changed")]
126        ChannelDescriptionChanged { by: String },
127        #[serde(rename = "channel_icon_changed")]
128        ChannelIconChanged { by: String },
129        #[serde(rename = "channel_ownership_changed")]
130        ChannelOwnershipChanged { from: String, to: String },
131        #[serde(rename = "message_pinned")]
132        MessagePinned { id: String, by: String },
133        #[serde(rename = "message_unpinned")]
134        MessageUnpinned { id: String, by: String },
135        #[serde(rename = "call_started")]
136        CallStarted {
137            by: String,
138            finished_at: Option<Timestamp>,
139        },
140    }
141
142    /// Name and / or avatar override information
143    #[cfg_attr(feature = "validator", derive(Validate))]
144    pub struct Masquerade {
145        /// Replace the display name shown on this message
146        #[serde(skip_serializing_if = "Option::is_none")]
147        #[validate(length(min = 1, max = 32))]
148        pub name: Option<String>,
149        /// Replace the avatar shown on this message (URL to image file)
150        #[serde(skip_serializing_if = "Option::is_none")]
151        #[validate(length(min = 1, max = 256))]
152        pub avatar: Option<String>,
153        /// Replace the display role colour shown on this message
154        ///
155        /// Must have `ManageRole` permission to use
156        #[serde(skip_serializing_if = "Option::is_none")]
157        #[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
158        pub colour: Option<String>,
159    }
160
161    /// Information to guide interactions on this message
162    #[derive(Default)]
163    pub struct Interactions {
164        /// Reactions which should always appear and be distinct
165        #[serde(skip_serializing_if = "Option::is_none", default)]
166        pub reactions: Option<IndexSet<String>>,
167        /// Whether reactions should be restricted to the given list
168        ///
169        /// Can only be set to true if reactions list is of at least length 1
170        #[serde(skip_serializing_if = "crate::if_false", default)]
171        pub restrict_reactions: bool,
172    }
173
174    /// Appended Information
175    pub struct AppendMessage {
176        /// Additional embeds to include in this message
177        #[serde(skip_serializing_if = "Option::is_none")]
178        pub embeds: Option<Vec<Embed>>,
179    }
180
181    /// Message Sort
182    ///
183    /// Sort used for retrieving messages
184    #[derive(Default)]
185    #[cfg_attr(feature = "rocket", derive(FromFormField))]
186    pub enum MessageSort {
187        /// Sort by the most relevant messages
188        #[default]
189        Relevance,
190        /// Sort by the newest messages first
191        Latest,
192        /// Sort by the oldest messages first
193        Oldest,
194    }
195
196    /// Push Notification
197    pub struct PushNotification {
198        /// Known author name
199        pub author: String,
200        /// URL to author avatar
201        pub icon: String,
202        /// URL to first matching attachment
203        #[serde(skip_serializing_if = "Option::is_none")]
204        pub image: Option<String>,
205        /// Message content or system message information
206        pub body: String,
207        /// The raw body, if the body has been rendered
208        #[serde(skip_serializing_if = "Option::is_none")]
209        pub raw_body: Option<String>,
210        /// Unique tag, usually the channel ID
211        pub tag: String,
212        /// Timestamp at which this notification was created
213        pub timestamp: u64,
214        /// URL to open when clicking notification
215        pub url: String,
216        /// The message object itself, to send to clients for processing
217        pub message: Message,
218        /// The channel object itself, for clients to process
219        pub channel: Channel,
220    }
221
222    /// Representation of a text embed before it is sent.
223    #[derive(Default)]
224    #[cfg_attr(feature = "validator", derive(Validate))]
225    pub struct SendableEmbed {
226        #[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
227        pub icon_url: Option<String>,
228        #[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
229        pub url: Option<String>,
230        #[cfg_attr(feature = "validator", validate(length(min = 1, max = 100)))]
231        pub title: Option<String>,
232        #[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))]
233        pub description: Option<String>,
234        pub media: Option<String>,
235        #[cfg_attr(
236            feature = "validator",
237            validate(length(min = 1, max = 128), regex = "RE_COLOUR")
238        )]
239        pub colour: Option<String>,
240    }
241
242    /// What this message should reply to and how
243    pub struct ReplyIntent {
244        /// Message Id
245        pub id: String,
246        /// Whether this reply should mention the message's author
247        pub mention: bool,
248        /// Whether to error if the referenced message doesn't exist.
249        /// Otherwise, send a message without this reply.
250        /// Default is true.
251        pub fail_if_not_exists: Option<bool>,
252    }
253
254    /// Message to send
255    #[cfg_attr(feature = "validator", derive(Validate))]
256    pub struct DataMessageSend {
257        /// Unique token to prevent duplicate message sending
258        ///
259        /// **This is deprecated and replaced by `Idempotency-Key`!**
260        #[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
261        pub nonce: Option<String>,
262
263        /// Message content to send
264        #[cfg_attr(feature = "validator", validate(length(min = 0)))]
265        pub content: Option<String>,
266        /// Attachments to include in message
267        pub attachments: Option<Vec<String>>,
268        /// Messages to reply to
269        pub replies: Option<Vec<ReplyIntent>>,
270        /// Embeds to include in message
271        ///
272        /// Text embed content contributes to the content length cap
273        #[cfg_attr(feature = "validator", validate)]
274        pub embeds: Option<Vec<SendableEmbed>>,
275        /// Masquerade to apply to this message
276        #[cfg_attr(feature = "validator", validate)]
277        pub masquerade: Option<Masquerade>,
278        /// Information about how this message should be interacted with
279        pub interactions: Option<Interactions>,
280
281        /// Bitfield of message flags
282        ///
283        /// https://docs.rs/revolt-models/latest/revolt_models/v0/enum.MessageFlags.html
284        pub flags: Option<u32>,
285    }
286
287    /// Options for querying messages
288    #[cfg_attr(feature = "validator", derive(Validate))]
289    #[cfg_attr(feature = "rocket", derive(FromForm))]
290    pub struct OptionsQueryMessages {
291        /// Maximum number of messages to fetch
292        ///
293        /// For fetching nearby messages, this is \`(limit + 2)\`.
294        #[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
295        pub limit: Option<i64>,
296        /// Message id before which messages should be fetched
297        #[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
298        pub before: Option<String>,
299        /// Message id after which messages should be fetched
300        #[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
301        pub after: Option<String>,
302        /// Message sort direction
303        pub sort: Option<MessageSort>,
304        /// Message id to search around
305        ///
306        /// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
307        /// It will also take half of limit rounded as the limits to each side.
308        /// It also fetches the message ID specified.
309        #[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
310        pub nearby: Option<String>,
311        /// Whether to include user (and member, if server channel) objects
312        pub include_users: Option<bool>,
313    }
314
315    /// Options for searching for messages
316    #[cfg_attr(feature = "validator", derive(Validate))]
317    pub struct DataMessageSearch {
318        /// Full-text search query
319        ///
320        /// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information.
321        #[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
322        pub query: Option<String>,
323        /// Whether to only search for pinned messages, cannot be sent with `query`.
324        pub pinned: Option<bool>,
325
326        /// Maximum number of messages to fetch
327        #[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
328        pub limit: Option<i64>,
329        /// Message id before which messages should be fetched
330        #[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
331        pub before: Option<String>,
332        /// Message id after which messages should be fetched
333        #[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
334        pub after: Option<String>,
335        /// Message sort direction
336        ///
337        /// By default, it will be sorted by latest.
338        #[cfg_attr(feature = "serde", serde(default = "MessageSort::default"))]
339        pub sort: MessageSort,
340        /// Whether to include user (and member, if server channel) objects
341        pub include_users: Option<bool>,
342    }
343
344    /// Changes to make to message
345    #[cfg_attr(feature = "validator", derive(Validate))]
346    pub struct DataEditMessage {
347        /// New message content
348        #[cfg_attr(feature = "validator", validate(length(min = 1)))]
349        pub content: Option<String>,
350        /// Embeds to include in the message
351        #[cfg_attr(feature = "validator", validate(length(min = 0, max = 10)))]
352        pub embeds: Option<Vec<SendableEmbed>>,
353    }
354
355    /// Options for bulk deleting messages
356    #[cfg_attr(
357        feature = "validator",
358        cfg_attr(feature = "validator", derive(Validate))
359    )]
360    pub struct OptionsBulkDelete {
361        /// Message IDs
362        #[validate(length(min = 1, max = 100))]
363        pub ids: Vec<String>,
364    }
365
366    /// Options for removing reaction
367    #[cfg_attr(feature = "rocket", derive(FromForm))]
368    pub struct OptionsUnreact {
369        /// Remove a specific user's reaction
370        pub user_id: Option<String>,
371        /// Remove all reactions
372        pub remove_all: Option<bool>,
373    }
374
375    /// Message flag bitfield
376    #[repr(u32)]
377    pub enum MessageFlags {
378        /// Message will not send push / desktop notifications
379        SuppressNotifications = 1,
380        /// Message will mention all users who can see the channel
381        MentionsEveryone = 2,
382        /// Message will mention all users who are online and can see the channel.
383        /// This cannot be true if MentionsEveryone is true
384        MentionsOnline = 3,
385    }
386
387    /// Optional fields on message
388    pub enum FieldsMessage {
389        Pinned,
390    }
391);
392
393/// Message Author Abstraction
394#[derive(Clone)]
395pub enum MessageAuthor<'a> {
396    User(&'a User),
397    Webhook(&'a Webhook),
398    System {
399        username: &'a str,
400        avatar: Option<&'a str>,
401    },
402}
403
404impl Interactions {
405    /// Check if default initialisation of fields
406    pub fn is_default(&self) -> bool {
407        !self.restrict_reactions && self.reactions.is_none()
408    }
409}
410
411impl MessageAuthor<'_> {
412    pub fn id(&self) -> &str {
413        match self {
414            MessageAuthor::User(user) => &user.id,
415            MessageAuthor::Webhook(webhook) => &webhook.id,
416            MessageAuthor::System { .. } => "00000000000000000000000000",
417        }
418    }
419
420    pub fn avatar(&self) -> Option<&str> {
421        match self {
422            MessageAuthor::User(user) => user.avatar.as_ref().map(|file| file.id.as_str()),
423            MessageAuthor::Webhook(webhook) => webhook.avatar.as_ref().map(|file| file.id.as_str()),
424            MessageAuthor::System { avatar, .. } => *avatar,
425        }
426    }
427
428    pub fn username(&self) -> &str {
429        match self {
430            MessageAuthor::User(user) => &user.username,
431            MessageAuthor::Webhook(webhook) => &webhook.name,
432            MessageAuthor::System { username, .. } => username,
433        }
434    }
435}
436
437impl From<SystemMessage> for String {
438    fn from(s: SystemMessage) -> String {
439        match s {
440            SystemMessage::Text { content } => content,
441            SystemMessage::UserAdded { .. } => "User added to the channel.".to_string(),
442            SystemMessage::UserRemove { .. } => "User removed from the channel.".to_string(),
443            SystemMessage::UserJoined { .. } => "User joined the channel.".to_string(),
444            SystemMessage::UserLeft { .. } => "User left the channel.".to_string(),
445            SystemMessage::UserKicked { .. } => "User kicked from the channel.".to_string(),
446            SystemMessage::UserBanned { .. } => "User banned from the channel.".to_string(),
447            SystemMessage::ChannelRenamed { .. } => "Channel renamed.".to_string(),
448            SystemMessage::ChannelDescriptionChanged { .. } => {
449                "Channel description changed.".to_string()
450            }
451            SystemMessage::ChannelIconChanged { .. } => "Channel icon changed.".to_string(),
452            SystemMessage::ChannelOwnershipChanged { .. } => {
453                "Channel ownership changed.".to_string()
454            }
455            SystemMessage::MessagePinned { .. } => "Message pinned.".to_string(),
456            SystemMessage::MessageUnpinned { .. } => "Message unpinned.".to_string(),
457            SystemMessage::CallStarted { .. } => "Call started.".to_string(),
458        }
459    }
460}
461
462impl PushNotification {
463    /// Create a new notification from a given message, author and channel ID
464    pub async fn from(msg: Message, author: Option<MessageAuthor<'_>>, channel: Channel) -> Self {
465        let config = config().await;
466
467        let icon = if let Some(author) = &author {
468            if let Some(avatar) = author.avatar() {
469                format!("{}/avatars/{}", config.hosts.autumn, avatar)
470            } else {
471                format!("{}/users/{}/default_avatar", config.hosts.api, author.id())
472            }
473        } else {
474            format!("{}/assets/logo.png", config.hosts.app)
475        };
476
477        let image = msg.attachments.as_ref().and_then(|attachments| {
478            attachments
479                .first()
480                .map(|v| format!("{}/attachments/{}", config.hosts.autumn, v.id))
481        });
482
483        let body = if let Some(ref sys) = msg.system {
484            sys.clone().into()
485        } else if let Some(ref text) = msg.content {
486            text.clone()
487        } else if let Some(text) = msg.embeds.as_ref().and_then(|embeds| match embeds.first() {
488            Some(Embed::Image(_)) => Some("Sent an image".to_string()),
489            Some(Embed::Video(_)) => Some("Sent a video".to_string()),
490            Some(Embed::Text(e)) => e
491                .description
492                .clone()
493                .or(e.title.clone().or(Some("Empty Embed".to_string()))),
494            Some(Embed::Website(e)) => e.title.clone().or(e
495                .description
496                .clone()
497                .or(e.site_name.clone().or(Some("Empty Embed".to_string())))),
498            Some(Embed::None) => Some("Empty Message".to_string()), // ???
499            None => Some("Empty Message".to_string()),              // ??
500        }) {
501            text
502        } else {
503            "Empty Message".to_string()
504        };
505
506        let timestamp = SystemTime::now()
507            .duration_since(SystemTime::UNIX_EPOCH)
508            .expect("Time went backwards")
509            .as_secs();
510
511        Self {
512            author: author
513                .map(|x| x.username().to_string())
514                .unwrap_or_else(|| "Revolt".to_string()),
515            icon,
516            image,
517            body,
518            raw_body: None,
519            tag: channel.id().to_string(),
520            timestamp,
521            url: format!("{}/channel/{}/{}", config.hosts.app, channel.id(), msg.id),
522            message: msg,
523            channel,
524        }
525    }
526}