composure/models/deserialize/
message.rs

1use core::str;
2
3use serde::{Deserialize, Serialize};
4use serde_repr::Deserialize_repr;
5
6use crate::models::{
7    ActionRow, Application, Attachment, Channel, Embed, Emoji, Interaction, Role,
8    RoleSubscriptionData, Snowflake, StickerItem, User,
9};
10
11/// [Message Structure](https://discord.com/developers/docs/resources/channel#message-object-message-structure)
12#[derive(Debug, Deserialize)]
13pub struct Message {
14    /// id of the message
15    pub id: Snowflake,
16
17    /// id of the channel the message was sent in
18    pub channel_id: Snowflake,
19
20    /// the author of this message (not guaranteed to be a valid user, see below)
21    pub author: User,
22
23    /// contents of the message
24    pub content: String,
25
26    /// when this message was sent
27    pub timestamp: String,
28
29    /// when this message was edited (or null if never)
30    pub edited_timestamp: Option<String>,
31
32    /// whether this was a TTS message
33    pub tts: bool,
34
35    /// whether this message mentions everyone
36    pub mention_everyone: bool,
37
38    /// users specifically mentioned in the message
39    pub mentions: Vec<User>,
40
41    /// roles specifically mentioned in this message
42    pub mention_roles: Vec<Role>,
43
44    /// channels specifically mentioned in this message
45    pub mention_channels: Option<Vec<ChannelMention>>,
46
47    /// any attached files
48    pub attachments: Vec<Attachment>,
49
50    /// any embedded content
51    pub embeds: Vec<Embed>,
52
53    /// reactions to the message
54    pub reactions: Option<Vec<Reaction>>,
55
56    // /// used for validating a message was sent
57    // pub nonce: Option<todo>,
58    /// whether this message is pinned
59    pub pinned: bool,
60
61    /// if the message is generated by a webhook, this is the webhook's id
62    pub webhook_id: Option<Snowflake>,
63
64    /// [type of message](https://discord.com/developers/docs/resources/channel#message-object-message-types)
65    #[serde(rename = "type")]
66    pub t: MessageType,
67
68    /// sent with Rich Presence-related chat embeds
69    pub activity: Option<MessageActivity>,
70
71    /// sent with Rich Presence-related chat embeds
72    pub application: Option<Application>,
73
74    /// if the message is an [Interaction](https://discord.com/developers/docs/interactions/receiving-and-responding) or application-owned webhook, this is the id of the application
75    pub application_id: Option<Snowflake>,
76
77    /// data showing the source of a crosspost, channel follow add, pin, or reply message
78    pub message_reference: Option<MessageReference>,
79
80    /// [message flags](https://discord.com/developers/docs/resources/channel#message-object-message-flags) combined as a [bitfield](https://en.wikipedia.org/wiki/Bit_field)
81    pub flags: Option<MessageFlags>,
82
83    // /// the message associated with the message_reference
84    // pub referenced_message: Option<Message>,
85    /// sent if the message is a response to an [Interaction](https://discord.com/developers/docs/interactions/receiving-and-responding)
86    pub interaction: Option<Interaction>,
87
88    /// the thread that was started from this message, includes [thread member](https://discord.com/developers/docs/resources/channel#thread-member-object) object
89    pub thread: Option<Channel>,
90
91    /// sent if the message contains components like buttons, action rows, or other interactive components
92    pub components: Option<Vec<ActionRow>>,
93
94    /// sent if the message contains stickers
95    pub sticker_items: Option<Vec<StickerItem>>,
96
97    /// A generally increasing integer (there may be gaps or duplicates) that represents the approximate position of the message in a thread, it can be used to estimate the relative position of the message in a thread in company with total_message_sent on parent thread
98    pub position: Option<i32>,
99
100    /// data of the role subscription purchase or renewal that prompted this ROLE_SUBSCRIPTION_PURCHASE message
101    pub role_subscription_data: Option<RoleSubscriptionData>,
102}
103/// [Channel Mention Object](https://discord.com/developers/docs/resources/channel#channel-mention-object)
104#[derive(Debug, Deserialize)]
105pub struct ChannelMention {
106    /// id of the channel
107    pub id: Snowflake,
108
109    /// id of the guild containing the channel
110    pub guild_id: Snowflake,
111
112    /// the [type of channel](https://discord.com/developers/docs/resources/channel#channel-object-channel-types)
113    #[serde(rename = "type")]
114    pub t: i32,
115
116    /// the name of the channel
117    pub name: String,
118}
119
120/// [Reaction Object](https://discord.com/developers/docs/resources/channel#reaction-object)
121#[derive(Debug, Deserialize)]
122pub struct Reaction {
123    /// times this emoji has been used to react
124    pub count: i32,
125
126    /// whether the current user reacted using this emoji
127    pub me: bool,
128
129    /// emoji information
130    pub emoji: Emoji,
131}
132
133/// [Message Types](https://discord.com/developers/docs/resources/channel#message-object-message-types)
134#[derive(Debug, Deserialize_repr)]
135#[repr(u8)]
136pub enum MessageType {
137    /// Deletable: true
138    Default = 0,
139
140    /// Deletable: false
141    RecipientAdd = 1,
142
143    /// Deletable: false
144    RecipientRemove = 2,
145
146    /// Deletable: false
147    Call = 3,
148
149    /// Deletable: false
150    ChannelNameChange = 4,
151
152    /// Deletable: false
153    ChannelIconChange = 5,
154
155    /// Deletable: true
156    ChannelPinnedMessage = 6,
157
158    /// Deletable: true
159    UserJoin = 7,
160
161    /// Deletable: true
162    GuildBoost = 8,
163
164    /// Deletable: true
165    GuildBoostTier1 = 9,
166
167    /// Deletable: true
168    GuildBoostTier2 = 10,
169
170    /// Deletable: true
171    GuildBoostTier3 = 11,
172
173    /// Deletable: true
174    ChannelFollowAdd = 12,
175
176    /// Deletable: false
177    GuildDiscoveryDisqualified = 14,
178
179    /// Deletable: false
180    GuildDiscoveryRequalified = 15,
181
182    /// Deletable: false
183    GuildDiscoveryGracePeriodInitialWarning = 16,
184
185    /// Deletable: false
186    GuildDiscoveryGracePeriodFinalWarning = 17,
187
188    /// Deletable: true
189    ThreadCreated = 18,
190
191    /// Deletable: true
192    Reply = 19,
193
194    /// Deletable: true
195    ChatInputCommand = 20,
196
197    /// Deletable: false
198    ThreadStarterMessage = 21,
199
200    /// Deletable: true
201    GuildInviteReminder = 22,
202
203    /// Deletable: true
204    ContextMenuCommand = 23,
205
206    /// Deletable: true, can only be deleted by members with MANAGE_MESSAGES permission
207    AutoModerationAction = 24,
208
209    /// Deletable: true
210    RoleSubscriptionPurchase = 25,
211
212    /// Deletable: true
213    InteractionPremiumUpsell = 26,
214
215    /// Deletable: true
216    StageStart = 27,
217
218    /// Deletable: true
219    StageEnd = 28,
220
221    /// Deletable: true
222    StageSpeaker = 29,
223
224    /// Deletable: true
225    StageTopic = 31,
226
227    /// Deletable: false
228    GuildApplicationPremiumSubscription = 32,
229}
230
231/// [Message Activity Structure](https://discord.com/developers/docs/resources/channel#message-object-message-activity-structure)
232#[derive(Debug, Deserialize)]
233pub struct MessageActivity {
234    /// [type of message activity](https://discord.com/developers/docs/resources/channel#message-object-message-activity-types)
235    #[serde(rename = "type")]
236    pub t: MessageActivityType,
237
238    /// party_id from a [Rich Presence event](https://discord.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields)
239    pub party_id: Option<String>,
240}
241
242/// [Message Activity Types](https://discord.com/developers/docs/resources/channel#message-object-message-activity-types)
243#[derive(Debug, Deserialize_repr)]
244#[repr(u8)]
245pub enum MessageActivityType {
246    Join = 1,
247
248    Spectate = 2,
249
250    Listen = 3,
251
252    JoinRequest = 5,
253}
254
255bitflags::bitflags! {
256    /// [Message Flags](https://discord.com/developers/docs/resources/channel#message-object-message-flags)
257    #[derive(Debug)]
258    pub struct MessageFlags: u16 {
259        /// this message has been published to subscribed channels (via Channel Following)
260        const Crossposted = 1 << 0;
261
262        /// this message originated from a message in another channel (via Channel Following)
263        const IsCrosspost = 1 << 1;
264
265        /// do not include any embeds when serializing this message
266        const SuppressEmbeds = 1 << 2;
267
268        /// the source message for this crosspost has been deleted (via Channel Following)
269        const SourceMessageDeleted = 1 << 3;
270
271        /// this message came from the urgent message system
272        const Urgent = 1 << 4;
273
274        /// this message has an associated thread, with the same id as the message
275        const HasThread = 1 << 5;
276
277        /// this message is only visible to the user who invoked the Interaction
278        const Ephemeral = 1 << 6;
279
280        /// this message is an Interaction Response and the bot is "thinking"
281        const Loading = 1 << 7;
282
283        /// this message failed to mention some roles and add their members to the thread
284        const FailedToMentionSomeRolesInThread = 1 << 8;
285
286        /// this message will not trigger push and desktop notifications
287        const SuppressNotifications = 1 << 12;
288
289        /// this message is a voice message
290        const IsVoiceMessage = 1 << 13;
291    }
292}
293
294impl Serialize for MessageFlags {
295    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
296    where
297        S: serde::Serializer,
298    {
299        self.bits().to_string().serialize(serializer)
300    }
301}
302
303impl<'de> Deserialize<'de> for MessageFlags {
304    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
305    where
306        D: serde::Deserializer<'de>,
307    {
308        let bit_str = String::deserialize(deserializer)?;
309        let bits = bit_str
310            .parse::<u16>()
311            .map_err(|e| serde::de::Error::custom(e))?;
312
313        // Permissions::from_bits(bits).ok_or(serde::de::Error::custom("Unexpected permissions flags"))
314        Ok(MessageFlags::from_bits_retain(bits))
315    }
316}
317
318/// [Message Reference Structure](https://discord.com/developers/docs/resources/channel#message-reference-object-message-reference-structure)
319#[derive(Debug, Deserialize)]
320pub struct MessageReference {
321    /// id of the originating message
322    pub message_id: Option<Snowflake>,
323
324    /// id of the originating message's channel
325    pub channel_id: Option<Snowflake>,
326
327    /// id of the originating message's guild
328    pub guild_id: Option<Snowflake>,
329
330    /// when sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true
331    pub fail_if_not_exists: Option<bool>,
332}
333
334#[cfg(test)]
335pub mod tests {
336    use crate::models::Component;
337
338    use super::*;
339
340    #[test]
341    pub fn button_component() {
342        let json = r#"{
343            "type": 1,
344            "components": [
345                {
346                    "type": 2,
347                    "label": "Click me!",
348                    "style": 1,
349                    "custom_id": "click_one"
350                }
351            ]
352        }"#;
353
354        let res = serde_json::from_str::<ActionRow>(json);
355
356        assert!(res.is_ok());
357
358        let action_row = res.unwrap();
359
360        assert_eq!(action_row.components.len(), 1);
361        assert!(matches!(action_row.components[0], Component::Button { .. }));
362    }
363
364    #[test]
365    pub fn select_menu_component() {
366        let json = r#" {
367            "type": 1,
368            "components": [
369                {
370                    "type": 3,
371                    "custom_id": "class_select_1",
372                    "options":[
373                        {
374                            "label": "Rogue",
375                            "value": "rogue",
376                            "description": "Sneak n stab",
377                            "emoji": {
378                                "name": "rogue",
379                                "id": "625891304148303894"
380                            }
381                        },
382                        {
383                            "label": "Mage",
384                            "value": "mage",
385                            "description": "Turn 'em into a sheep",
386                            "emoji": {
387                                "name": "mage",
388                                "id": "625891304081063986"
389                            }
390                        },
391                        {
392                            "label": "Priest",
393                            "value": "priest",
394                            "description": "You get heals when I'm done doing damage",
395                            "emoji": {
396                                "name": "priest",
397                                "id": "625891303795982337"
398                            }
399                        }
400                    ],
401                    "placeholder": "Choose a class",
402                    "min_values": 1,
403                    "max_values": 3
404                }
405            ]
406        }"#;
407
408        let res = serde_json::from_str::<ActionRow>(json);
409
410        assert!(res.is_ok());
411
412        let component = res.unwrap();
413
414        println!("{:#?}", component);
415    }
416}