slashies 0.1.3

Slashies helps to reduce the boiler plate code needed to create slash commands for a Discord bot.
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
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
//! A simple way to create slash commands for Discord bots
//!
//! Slashies helps to reduce the boiler plate code needed to create slashcommands for a Discord bot.
//! It is built on top of [`Serenity`]. It focuses on providing traits that you can derive using the
//! macros crate for most straightforward use cases, but gives you the escape hatch of implementing
//! these traits yourself if you want to do something more complex.
//!
//! [`Serenity`]: serenity
//!
//! Make sure to read the [Discord documentation](https://discord.com/developers/docs/interactions/application-commands)
//! on slash commands to understand the general concepts like interactions.
//!
//! With Slashies, you can create a slash command in four easy steps:
//! ```no_run
//! # use slashies::*;
//! # use slashies::parsable::*;
//! # use slashies_macros::*;
//! # use serenity::async_trait;
//! # use serenity::prelude::*;
//! # use serenity::model::prelude::*;
//! # use serenity::model::prelude::application_command::*;
//! // 1. Create a struct representing the arguments for the command and derive/implement the
//! // Command trait
//!
//! /// Greet a user
//! #[derive(Debug, Command)]
//! #[name = "greet"]
//! struct HelloCommand {
//!     /// The user to greet
//!     user: UserInput,
//! }
//!
//! // 2. Implement the ApplicationCommandInteractionHandler trait to define what happens when you
//! // call the command
//! #[async_trait]
//! impl ApplicationCommandInteractionHandler for HelloCommand {
//!    async fn invoke(
//!        &self,
//!        ctx: &Context,
//!        command: &ApplicationCommandInteraction,
//!    ) -> Result<(), InvocationError> {
//!        let nickname = self.user.member.as_ref().map(|pm| pm.nick.as_ref()).flatten();
//!        let greeting = if let Some(nick) = nickname {
//!            format!("Hello {} aka {}", self.user.user.name, nick)
//!        } else {
//!            format!("Hello {}", self.user.user.name)
//!        };
//!        command
//!            .create_interaction_response(&ctx.http, |response| {
//!                response
//!                    .kind(InteractionResponseType::ChannelMessageWithSource)
//!                    .interaction_response_data(|message| message.content(greeting))
//!            })
//!            .await
//!            .map_err(|_| InvocationError)
//!    }
//! }
//!
//! // 3. Add the command to an enum that implements the Commands trait, representing all the
//! // commands for the bot
//! #[derive(Debug, Commands)]
//! enum BotCommands {
//!     Hello(HelloCommand),
//! }
//!
//! // 4. Add the basic code to register the command via a macro and handle interactions
//! struct Handler;
//!
//! #[async_trait]
//! impl EventHandler for Handler {
//!     async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
//!         match interaction {
//!             Interaction::ApplicationCommand(command_interaction) => {
//!                 BotCommands::parse(&ctx, &command_interaction)
//!                     .expect("Failed to parse command")
//!                     .invoke(&ctx, &command_interaction)
//!                     .await
//!                     .expect("Failed to invoke command");
//!             }
//!             _ => (),
//!         }
//!     }
//!
//!     async fn ready(&self, ctx: Context, ready: Ready) {
//!         register_commands!(&ctx, None, [HelloCommand])
//!             .expect("Unable to register commands");
//!     }
//! }
//!
//! #[tokio::main]
//! async fn main() {
//!     let token = std::env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
//!     let application_id = std::env::var("DISCORD_USER_ID")
//!         .expect("Expected a user id in the environment")
//!         .parse::<u64>()
//!         .expect("Invalid user id");
//!     let mut client = Client::builder(&token, GatewayIntents::empty())
//!         .event_handler(Handler)
//!         .application_id(application_id)
//!         .await
//!         .expect("Err creating client");
//!
//!     if let Err(why) = client.start().await {
//!         println!("Client error: {:?}", why);
//!     }
//! }
//! ```

#![warn(missing_docs)]
use serenity::{
    async_trait,
    builder::{CreateApplicationCommand, CreateApplicationCommandOption},
    client::Context,
    model::{
        channel::Message,
        interactions::{
            application_command::{
                ApplicationCommandInteraction, ApplicationCommandInteractionDataOption,
            },
            message_component::MessageComponentInteraction,
        },
    },
};

/// This module contains logic for parsing Discord types from interactions into rust types
pub mod parsable;

/// An error that occured while trying to parse a command
#[derive(Debug, Clone)]
pub enum ParseError {
    /// A required option was missing
    MissingOption,
    /// An option was malformed
    InvalidOption,
    /// The command was not one we know about
    UnknownCommand,
}

/// An error that occured while trying to invoke a command
#[derive(Debug, Clone)]
pub struct InvocationError;

/// This trait provides the methods needed to parse and register a slash command.
///
/// For most use cases, just derive it via the macros crate:
/// ```
/// # use slashies::*;
/// # use slashies::parsable::*;
/// # use slashies_macros::*;
/// # use serenity::async_trait;
/// # use serenity::prelude::*;
/// # use serenity::model::prelude::application_command::*;
/// /// Greet a user
/// #[derive(Debug, Command)]
/// #[name = "greet"]
/// struct HelloCommand {
///     /// The user to greet
///     user: UserInput,
/// }
/// # #[async_trait]
/// # impl ApplicationCommandInteractionHandler for HelloCommand {
/// #    async fn invoke(
/// #        &self,
/// #        ctx: &Context,
/// #        command: &ApplicationCommandInteraction,
/// #    ) -> Result<(), InvocationError> {
/// #        unimplemented!()
/// #    }
/// # }
/// ```
/// To derive the trait, you must provide the following (see the example above):
/// - Docstrings for the struct and all fields (these will be used for the
/// descriptions of the command and its options)
/// - The name of the command via the `name` attribute
///
/// All fields must implement the [`parsable::ParsableCommandOption`] trait - see the docs for the
/// trait for a list of types supported out of the box.
///
/// You may also provide additional attributes to specify more complex behaviours for the command
/// options:
///
/// | Attribute     | Explanation                                                                                                         | Examples                                                 | Applicable Discord types |
/// |---------------|---------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|--------------------------|
/// | choice        | Limits the user's input to specific choices - use the attribute on the field multiple times, once for each choice.  | `#[choice("Action")]` `#[choice("First", 1)]`            | STRING, INTEGER, NUMBER  |
/// | min           | Limits the user's input to be at least this value.                                                                  | `#[min = 0.0]`                                           | INTEGER, NUMBER          |
/// | max           | Limits the user's input to be at most this value.                                                                   | `#[max = 10.0]`                                          | INTEGER, NUMBER          |
/// | channel_types | Limits the user's choice of channels to specific types of channels                                                  | `#[channel_types(ChannelType::Text, ChannelType::News)]` | CHANNEL                  |
///
/// For how to work with subcommands, see the documentation for the [`SubCommand`] trait
pub trait Command: ApplicationCommandInteractionHandler + Sized {
    /// Try to parse the interaction as this type of command
    fn parse(command: &ApplicationCommandInteraction) -> Result<Self, ParseError>;
    /// Register this command so that it can be used
    fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand;
    /// The name of the command
    fn name() -> String;
}

/// This trait provides the functions necessary to parse and register a subcommand for a slash
/// command.
///
/// For most use cases:
/// ```
/// # use serenity::async_trait;
/// # use serenity::prelude::*;
/// # use serenity::model::interactions::*;
/// # use serenity::model::prelude::application_command::*;
/// # use slashies::*;
/// # use slashies_macros::*;
/// # use slashies::parsable::*;
/// // 1. Create the subcommand in the same way you would create a Command, but derive the
/// // SubCommand trait instead
/// // (Remember to implement ApplicationCommandInteractionHandler)
///
/// #[derive(Debug, SubCommand)]
/// struct TestSubCommandOne {
///     /// A number
///     number: f64,
/// }
/// # #[async_trait]
/// # impl ApplicationCommandInteractionHandler for TestSubCommandOne {
/// #     async fn invoke(
/// #         &self,
/// #         ctx: &Context,
/// #         command: &ApplicationCommandInteraction,
/// #     ) -> Result<(), InvocationError> {
/// #         unimplemented!()
/// #     }
/// # }
///
/// #[derive(Debug, SubCommand)]
/// struct TestSubCommandTwo;
/// # #[async_trait]
/// # impl ApplicationCommandInteractionHandler for TestSubCommandTwo {
/// #     async fn invoke(
/// #         &self,
/// #         ctx: &Context,
/// #         command: &ApplicationCommandInteraction,
/// #     ) -> Result<(), InvocationError> {
/// #         unimplemented!()
/// #     }
/// # }
///
/// // 2. Create an enum with a variant for each subcommand and derive the Command and
/// // ApplicationCommandInteractionHandler traits:
///
/// /// A test command to show subcommands
/// #[derive(Debug, Command, ApplicationCommandInteractionHandler)]
/// #[name = "test"]
/// enum TestCommand {
///     /// The first subcommand
///     #[name = "one"]
///     One(TestSubCommandOne),
///
///     /// The second subcommand
///     #[name = "two"]
///     Two(TestSubCommandTwo),
/// }
/// ```
///
/// If there is a lot of shared behaviour between the subcommands, you may wish to directly implement
/// the [`ApplicationCommandInteractionHandler`] trait for this [`Command`] enum rather than for each
/// [`SubCommand`].
///
/// To organize subcommands into groups, see the [`SubCommandGroup`] trait
pub trait SubCommand: Sized {
    /// Try to parse this from a command option
    fn parse(option: Option<&ApplicationCommandInteractionDataOption>) -> Result<Self, ParseError>;
    /// Register any sub options for this subcommand
    fn register_sub_options(
        option: &mut CreateApplicationCommandOption,
    ) -> &mut CreateApplicationCommandOption;
}

/// This trait provides the functions necessary to parse and register a subcommand group for a slash
/// command.
///
/// For most use cases:
/// ```
/// # use serenity::async_trait;
/// # use serenity::prelude::*;
/// # use serenity::model::interactions::*;
/// # use serenity::model::prelude::application_command::*;
/// # use slashies::*;
/// # use slashies_macros::*;
/// # use slashies::parsable::*;
/// #
/// // 1. Create the "leaf" subcommands as normal (see SubCommand docs)
/// # #[derive(Debug, SubCommand)]
/// # struct TestSubCommandOne;
/// # #[async_trait]
/// # impl ApplicationCommandInteractionHandler for TestSubCommandOne {
/// #     async fn invoke(
/// #         &self,
/// #         ctx: &Context,
/// #         command: &ApplicationCommandInteraction,
/// #     ) -> Result<(), InvocationError> {
/// #         unimplemented!()
/// #     }
/// # }
/// # #[derive(Debug, SubCommand)]
/// # struct TestSubCommandTwo;
/// # #[async_trait]
/// # impl ApplicationCommandInteractionHandler for TestSubCommandTwo {
/// #     async fn invoke(
/// #         &self,
/// #         ctx: &Context,
/// #         command: &ApplicationCommandInteraction,
/// #     ) -> Result<(), InvocationError> {
/// #         unimplemented!()
/// #     }
/// # }
/// # #[derive(Debug, SubCommand)]
/// # struct TestSubCommandThree;
/// # #[async_trait]
/// # impl ApplicationCommandInteractionHandler for TestSubCommandThree {
/// #     async fn invoke(
/// #         &self,
/// #         ctx: &Context,
/// #         command: &ApplicationCommandInteraction,
/// #     ) -> Result<(), InvocationError> {
/// #         unimplemented!()
/// #     }
/// # }
/// // 2. Create an enum with a variant for each subcommand in the subcommand group and derive the
/// // SubCommandGroup and ApplicationCommandInteractionHandler traits:
///
/// /// A test command to show subcommands
/// #[derive(Debug, SubCommandGroup, ApplicationCommandInteractionHandler)]
/// #[name = "test sub command group"]
/// enum TestSubCommandGroup {
///     /// The first subcommand
///     #[name = "one"]
///     One(TestSubCommandOne),
///
///     /// The second subcommand
///     #[name = "two"]
///     Two(TestSubCommandTwo),
/// }
///
/// // 3. Create an enum with a variant for each subcommand / subcommand group and derive the
/// // Command and ApplicationCommandInteractionHandler traits:
///
/// /// A test command to show subcommands
/// #[derive(Debug, Command, ApplicationCommandInteractionHandler)]
/// #[name = "test"]
/// enum TestCommand {
///     /// The subcommand group
///     #[subcommandgroup]
///     #[name = "group"]
///     Group(TestSubCommandGroup),
///
///     /// A regular subcommand
///     #[name = "three"]
///     Three(TestSubCommandThree),
/// }
/// ```
/// Note that you can mix subcommands and subcommand groups in a command as in the example above.
pub trait SubCommandGroup: Sized {
    /// Try to parse this from a command option
    fn parse(option: Option<&ApplicationCommandInteractionDataOption>) -> Result<Self, ParseError>;
    /// Register any sub options for this subcommand group
    fn register_sub_options(
        option: &mut CreateApplicationCommandOption,
    ) -> &mut CreateApplicationCommandOption;
}

/// This trait provides a function to receive and respond to slash command interactions.
///
/// Typically you will want to respond using [`create_interaction_response`] - see the [`serenity`]
/// docs for more info.
///
/// [`create_interaction_response`]: serenity::model::interactions::application_command::ApplicationCommandInteraction::create_interaction_response
/// ```
/// # use serenity::async_trait;
/// # use serenity::prelude::*;
/// # use serenity::model::interactions::*;
/// # use serenity::model::prelude::application_command::*;
/// # use slashies::*;
/// # use slashies::parsable::*;
/// # struct HelloCommand {
/// #     user: UserInput,
/// # }
///
/// #[async_trait]
/// impl ApplicationCommandInteractionHandler for HelloCommand {
///     async fn invoke(
///         &self,
///         ctx: &Context,
///         command: &ApplicationCommandInteraction,
///     ) -> Result<(), InvocationError> {
///         let nickname = self.user.member.as_ref().map(|pm| pm.nick.as_ref()).flatten();
///         let greeting = if let Some(nick) = nickname {
///             format!("Hello {} aka {}", self.user.user.name, nick)
///         } else {
///             format!("Hello {}", self.user.user.name)
///         };
///         command
///             .create_interaction_response(&ctx.http, |response| {
///                 response
///                     .kind(InteractionResponseType::ChannelMessageWithSource)
///                     .interaction_response_data(|message| message.content(greeting))
///             })
///             .await
///             .map_err(|_| InvocationError)
///     }
/// }
/// ```
#[async_trait]
pub trait ApplicationCommandInteractionHandler {
    /// Invoke the command
    async fn invoke(
        &self,
        ctx: &Context,
        command: &ApplicationCommandInteraction,
    ) -> Result<(), InvocationError>;
}

/// This trait provides a function to receive and respond to message component interactions.
///
/// TODO: more docs around handler map context
#[async_trait]
pub trait MessageComponentInteractionHandler {
    /// Handle the message component interaction
    async fn invoke(
        &mut self,
        ctx: &Context,
        interaction: &MessageComponentInteraction,
        original_message: &mut Message,
    );
}

/// This trait should be derived for an enum with a variant for each command.
/// This will implement the boilerplate to:
/// - Parse an interaction into a specific command based on the command name
/// - Delegate the invocation of a command to the specific enum variant
///
/// ```
/// # use slashies::*;
/// # use slashies_macros::*;
/// # use serenity::prelude::*;
/// # use serenity::model::prelude::application_command::*;
/// # #[derive(Debug, Command)]
/// # #[name = "greet"]
/// # /// Greet a user
/// # struct HelloCommand;
/// # #[serenity::async_trait]
/// # impl ApplicationCommandInteractionHandler for HelloCommand {
/// #     async fn invoke(
/// #         &self,
/// #         ctx: &Context,
/// #         command: &ApplicationCommandInteraction,
/// #     ) -> Result<(), InvocationError> {
/// #     unimplemented!()
/// #     }
/// # }
/// #[derive(Debug, Commands)]
/// enum BotCommands {
///     Hello(HelloCommand),
/// }
/// ```
#[async_trait]
pub trait Commands: Sized {
    /// Parse an interaction into a specific command
    fn parse(ctx: &Context, command: &ApplicationCommandInteraction) -> Result<Self, ParseError>;

    /// Invoke the command
    async fn invoke(
        &self,
        ctx: &Context,
        command_interaction: &ApplicationCommandInteraction,
    ) -> Result<(), InvocationError>;
}

/// Register a set of commands (either globally or to a specific guild)
///
/// Note: register each [`Command`] here rather than the [`Commands`] enum. This gives you the
/// flexibility to have some commands registered globally and others registered only in specific
/// guilds.
///
/// Examples:
/// ```no_run
/// # use slashies::*;
/// # use slashies_macros::*;
/// # use serenity::async_trait;
/// # use serenity::prelude::*;
/// # use serenity::model::prelude::*;
/// # use serenity::model::prelude::application_command::*;
/// # /// Greet a user
/// # #[derive(Debug, Command)]
/// # #[name = "greet"]
/// # struct HelloCommand;
/// # #[async_trait]
/// # impl ApplicationCommandInteractionHandler for HelloCommand {
/// #    async fn invoke(
/// #        &self,
/// #        ctx: &Context,
/// #        command: &ApplicationCommandInteraction,
/// #    ) -> Result<(), InvocationError> {
/// #     unimplemented!()
/// #     }
/// # }
/// # /// Another command
/// # #[derive(Debug, Command)]
/// # #[name = "next"]
/// # struct NextCommand;
/// # #[async_trait]
/// # impl ApplicationCommandInteractionHandler for NextCommand {
/// #    async fn invoke(
/// #        &self,
/// #        ctx: &Context,
/// #        command: &ApplicationCommandInteraction,
/// #    ) -> Result<(), InvocationError> {
/// #     unimplemented!()
/// #     }
/// # }
/// # /// Another command
/// # #[derive(Debug, Command)]
/// # #[name = "other"]
/// # struct OtherCommand;
/// # #[async_trait]
/// # impl ApplicationCommandInteractionHandler for OtherCommand {
/// #    async fn invoke(
/// #        &self,
/// #        ctx: &Context,
/// #        command: &ApplicationCommandInteraction,
/// #    ) -> Result<(), InvocationError> {
/// #         unimplemented!()
/// #     }
/// # }
/// # async fn test(ctx: Context) {
/// let guild_id = Some(GuildId(0));
///
/// // Register a command to a guild
/// register_commands!(&ctx, guild_id, [HelloCommand]);
///
/// // Register multiple commands
/// register_commands!(&ctx, guild_id, [HelloCommand, NextCommand, OtherCommand]);
///
/// // Register a global command
/// register_commands!(&ctx, None, [HelloCommand]);
/// # }
/// ```
#[macro_export]
macro_rules! register_commands {
    ($ctx:expr, $guild_id:expr, [$($cmdType:ty),+]) => {{
        if let Some(guild_id) = $guild_id {
            serenity::model::prelude::GuildId::set_application_commands(&guild_id, &$ctx.http, |commands_builder| {
                commands_builder
                $(
                    .create_application_command(|command| <$cmdType as slashies::Command>::register(command))
                )*
            })
            .await
        } else {
            serenity::model::interactions::application_command::ApplicationCommand::set_global_application_commands(&$ctx.http, |commands_builder| {
                commands_builder
                $(
                    .create_application_command(|command| <$cmdType as slashies::Command>::register(command))
                )*
            })
            .await
        }
    }};
}