composure 0.0.2

Discord bot framework for running on the edge
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
use core::str;

use serde::{Deserialize, Serialize};
use serde_repr::Deserialize_repr;

use crate::models::{
    ActionRow, Application, Attachment, Channel, Embed, Emoji, Interaction, Role,
    RoleSubscriptionData, Snowflake, StickerItem, User,
};

/// [Message Structure](https://discord.com/developers/docs/resources/channel#message-object-message-structure)
#[derive(Debug, Deserialize)]
pub struct Message {
    /// id of the message
    pub id: Snowflake,

    /// id of the channel the message was sent in
    pub channel_id: Snowflake,

    /// the author of this message (not guaranteed to be a valid user, see below)
    pub author: User,

    /// contents of the message
    pub content: String,

    /// when this message was sent
    pub timestamp: String,

    /// when this message was edited (or null if never)
    pub edited_timestamp: Option<String>,

    /// whether this was a TTS message
    pub tts: bool,

    /// whether this message mentions everyone
    pub mention_everyone: bool,

    /// users specifically mentioned in the message
    pub mentions: Vec<User>,

    /// roles specifically mentioned in this message
    pub mention_roles: Vec<Role>,

    /// channels specifically mentioned in this message
    pub mention_channels: Option<Vec<ChannelMention>>,

    /// any attached files
    pub attachments: Vec<Attachment>,

    /// any embedded content
    pub embeds: Vec<Embed>,

    /// reactions to the message
    pub reactions: Option<Vec<Reaction>>,

    // /// used for validating a message was sent
    // pub nonce: Option<todo>,
    /// whether this message is pinned
    pub pinned: bool,

    /// if the message is generated by a webhook, this is the webhook's id
    pub webhook_id: Option<Snowflake>,

    /// [type of message](https://discord.com/developers/docs/resources/channel#message-object-message-types)
    #[serde(rename = "type")]
    pub t: MessageType,

    /// sent with Rich Presence-related chat embeds
    pub activity: Option<MessageActivity>,

    /// sent with Rich Presence-related chat embeds
    pub application: Option<Application>,

    /// 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
    pub application_id: Option<Snowflake>,

    /// data showing the source of a crosspost, channel follow add, pin, or reply message
    pub message_reference: Option<MessageReference>,

    /// [message flags](https://discord.com/developers/docs/resources/channel#message-object-message-flags) combined as a [bitfield](https://en.wikipedia.org/wiki/Bit_field)
    pub flags: Option<MessageFlags>,

    // /// the message associated with the message_reference
    // pub referenced_message: Option<Message>,
    /// sent if the message is a response to an [Interaction](https://discord.com/developers/docs/interactions/receiving-and-responding)
    pub interaction: Option<Interaction>,

    /// the thread that was started from this message, includes [thread member](https://discord.com/developers/docs/resources/channel#thread-member-object) object
    pub thread: Option<Channel>,

    /// sent if the message contains components like buttons, action rows, or other interactive components
    pub components: Option<Vec<ActionRow>>,

    /// sent if the message contains stickers
    pub sticker_items: Option<Vec<StickerItem>>,

    /// 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
    pub position: Option<i32>,

    /// data of the role subscription purchase or renewal that prompted this ROLE_SUBSCRIPTION_PURCHASE message
    pub role_subscription_data: Option<RoleSubscriptionData>,
}
/// [Channel Mention Object](https://discord.com/developers/docs/resources/channel#channel-mention-object)
#[derive(Debug, Deserialize)]
pub struct ChannelMention {
    /// id of the channel
    pub id: Snowflake,

    /// id of the guild containing the channel
    pub guild_id: Snowflake,

    /// the [type of channel](https://discord.com/developers/docs/resources/channel#channel-object-channel-types)
    #[serde(rename = "type")]
    pub t: i32,

    /// the name of the channel
    pub name: String,
}

/// [Reaction Object](https://discord.com/developers/docs/resources/channel#reaction-object)
#[derive(Debug, Deserialize)]
pub struct Reaction {
    /// times this emoji has been used to react
    pub count: i32,

    /// whether the current user reacted using this emoji
    pub me: bool,

    /// emoji information
    pub emoji: Emoji,
}

/// [Message Types](https://discord.com/developers/docs/resources/channel#message-object-message-types)
#[derive(Debug, Deserialize_repr)]
#[repr(u8)]
pub enum MessageType {
    /// Deletable: true
    Default = 0,

    /// Deletable: false
    RecipientAdd = 1,

    /// Deletable: false
    RecipientRemove = 2,

    /// Deletable: false
    Call = 3,

    /// Deletable: false
    ChannelNameChange = 4,

    /// Deletable: false
    ChannelIconChange = 5,

    /// Deletable: true
    ChannelPinnedMessage = 6,

    /// Deletable: true
    UserJoin = 7,

    /// Deletable: true
    GuildBoost = 8,

    /// Deletable: true
    GuildBoostTier1 = 9,

    /// Deletable: true
    GuildBoostTier2 = 10,

    /// Deletable: true
    GuildBoostTier3 = 11,

    /// Deletable: true
    ChannelFollowAdd = 12,

    /// Deletable: false
    GuildDiscoveryDisqualified = 14,

    /// Deletable: false
    GuildDiscoveryRequalified = 15,

    /// Deletable: false
    GuildDiscoveryGracePeriodInitialWarning = 16,

    /// Deletable: false
    GuildDiscoveryGracePeriodFinalWarning = 17,

    /// Deletable: true
    ThreadCreated = 18,

    /// Deletable: true
    Reply = 19,

    /// Deletable: true
    ChatInputCommand = 20,

    /// Deletable: false
    ThreadStarterMessage = 21,

    /// Deletable: true
    GuildInviteReminder = 22,

    /// Deletable: true
    ContextMenuCommand = 23,

    /// Deletable: true, can only be deleted by members with MANAGE_MESSAGES permission
    AutoModerationAction = 24,

    /// Deletable: true
    RoleSubscriptionPurchase = 25,

    /// Deletable: true
    InteractionPremiumUpsell = 26,

    /// Deletable: true
    StageStart = 27,

    /// Deletable: true
    StageEnd = 28,

    /// Deletable: true
    StageSpeaker = 29,

    /// Deletable: true
    StageTopic = 31,

    /// Deletable: false
    GuildApplicationPremiumSubscription = 32,
}

/// [Message Activity Structure](https://discord.com/developers/docs/resources/channel#message-object-message-activity-structure)
#[derive(Debug, Deserialize)]
pub struct MessageActivity {
    /// [type of message activity](https://discord.com/developers/docs/resources/channel#message-object-message-activity-types)
    #[serde(rename = "type")]
    pub t: MessageActivityType,

    /// party_id from a [Rich Presence event](https://discord.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields)
    pub party_id: Option<String>,
}

/// [Message Activity Types](https://discord.com/developers/docs/resources/channel#message-object-message-activity-types)
#[derive(Debug, Deserialize_repr)]
#[repr(u8)]
pub enum MessageActivityType {
    Join = 1,

    Spectate = 2,

    Listen = 3,

    JoinRequest = 5,
}

bitflags::bitflags! {
    /// [Message Flags](https://discord.com/developers/docs/resources/channel#message-object-message-flags)
    #[derive(Debug)]
    pub struct MessageFlags: u16 {
        /// this message has been published to subscribed channels (via Channel Following)
        const Crossposted = 1 << 0;

        /// this message originated from a message in another channel (via Channel Following)
        const IsCrosspost = 1 << 1;

        /// do not include any embeds when serializing this message
        const SuppressEmbeds = 1 << 2;

        /// the source message for this crosspost has been deleted (via Channel Following)
        const SourceMessageDeleted = 1 << 3;

        /// this message came from the urgent message system
        const Urgent = 1 << 4;

        /// this message has an associated thread, with the same id as the message
        const HasThread = 1 << 5;

        /// this message is only visible to the user who invoked the Interaction
        const Ephemeral = 1 << 6;

        /// this message is an Interaction Response and the bot is "thinking"
        const Loading = 1 << 7;

        /// this message failed to mention some roles and add their members to the thread
        const FailedToMentionSomeRolesInThread = 1 << 8;

        /// this message will not trigger push and desktop notifications
        const SuppressNotifications = 1 << 12;

        /// this message is a voice message
        const IsVoiceMessage = 1 << 13;
    }
}

impl Serialize for MessageFlags {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        self.bits().to_string().serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for MessageFlags {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let bit_str = String::deserialize(deserializer)?;
        let bits = bit_str
            .parse::<u16>()
            .map_err(|e| serde::de::Error::custom(e))?;

        // Permissions::from_bits(bits).ok_or(serde::de::Error::custom("Unexpected permissions flags"))
        Ok(MessageFlags::from_bits_retain(bits))
    }
}

/// [Message Reference Structure](https://discord.com/developers/docs/resources/channel#message-reference-object-message-reference-structure)
#[derive(Debug, Deserialize)]
pub struct MessageReference {
    /// id of the originating message
    pub message_id: Option<Snowflake>,

    /// id of the originating message's channel
    pub channel_id: Option<Snowflake>,

    /// id of the originating message's guild
    pub guild_id: Option<Snowflake>,

    /// when sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true
    pub fail_if_not_exists: Option<bool>,
}

#[cfg(test)]
pub mod tests {
    use crate::models::Component;

    use super::*;

    #[test]
    pub fn button_component() {
        let json = r#"{
            "type": 1,
            "components": [
                {
                    "type": 2,
                    "label": "Click me!",
                    "style": 1,
                    "custom_id": "click_one"
                }
            ]
        }"#;

        let res = serde_json::from_str::<ActionRow>(json);

        assert!(res.is_ok());

        let action_row = res.unwrap();

        assert_eq!(action_row.components.len(), 1);
        assert!(matches!(action_row.components[0], Component::Button { .. }));
    }

    #[test]
    pub fn select_menu_component() {
        let json = r#" {
            "type": 1,
            "components": [
                {
                    "type": 3,
                    "custom_id": "class_select_1",
                    "options":[
                        {
                            "label": "Rogue",
                            "value": "rogue",
                            "description": "Sneak n stab",
                            "emoji": {
                                "name": "rogue",
                                "id": "625891304148303894"
                            }
                        },
                        {
                            "label": "Mage",
                            "value": "mage",
                            "description": "Turn 'em into a sheep",
                            "emoji": {
                                "name": "mage",
                                "id": "625891304081063986"
                            }
                        },
                        {
                            "label": "Priest",
                            "value": "priest",
                            "description": "You get heals when I'm done doing damage",
                            "emoji": {
                                "name": "priest",
                                "id": "625891303795982337"
                            }
                        }
                    ],
                    "placeholder": "Choose a class",
                    "min_values": 1,
                    "max_values": 3
                }
            ]
        }"#;

        let res = serde_json::from_str::<ActionRow>(json);

        assert!(res.is_ok());

        let component = res.unwrap();

        println!("{:#?}", component);
    }
}