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}