Zephyrus
is a slash command framework meant to be used by twilight
Note: The framework is new and might have some problems, all contributions are appreciated
This crate is independent from the twilight ecosystem
The framework is experimental and the API might change.
Zephyrus
is a command framework which uses slash commands, it mainly offers variable argument parsing.
Parsing is done with the Parse
trait, so users can implement the parsing of their own types.
Argument parsing is done in a named way, this means the argument name shown on discord gets parsed into the arguments named the same way in the handler function.
The framework itself doesn't spawn any tasks by itself, so you might want to wrap it in an Arc
and call
tokio::spawn
before calling the .process
method.
```rust use std::sync::Arc; use futures::StreamExt; use twilightgateway::{Cluster, cluster::Events}; use twilighthttp::Client; use twilightmodel::gateway::event::Event; use twilightmodel::gateway::Intents; use twilightmodel::http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}; use twilightmodel::id::Id; use twilight_model::id::marker::{ApplicationMarker, GuildMarker}; use zephyrus::prelude::*;
async fn hello(ctx: &SlashContext<()>) -> DefaultCommandResult { ctx.interactionclient.createresponse( ctx.interaction.id, &ctx.interaction.token, &InteractionResponse { kind: InteractionResponseType::ChannelMessageWithSource, data: Some(InteractionResponseData { content: Some(String::from("Hello world")), ..Default::default() }) } ).await?;
Ok(())
}
async fn handleevents(httpclient: Arc
// Zephyrus can register commands in guilds or globally.
framework.register_guild_commands(Id::<GuildMarker>::new("<GUILD_ID>")).await.unwrap();
while let Some((_, event)) = events.next().await {
match event {
Event::InteractionCreate(i) => {
let clone = Arc::clone(&framework);
tokio::spawn(async move {
let inner = i.0;
clone.process(inner).await;
});
},
_ => (),
}
}
}
async fn main() -> Result<(), Box
let (cluster, events) = Cluster::builder(token, Intents::empty())
.http_client(Arc::clone(&client))
.build()
.await?;
cluster.up().await;
handle_events(client, events, app_id).await;
Ok(())
} ```
Every command is an async
function, having always as the first parameter a &SlashContext<T>
```rust
async fn command(
ctx: &SlashContext* Your type of context*/>, // The context must always be the first parameter.
#[description = "A description for the argument"] somearg: String,
#[rename = "otherarg"] #[description = "other description"] other: Option
Ok(())
} ```
Command functions must include a description
attribute, which will be seen in discord when the user tries to use the command.
The #[command]
attribute also allows to rename the command by passing the name of the command to the attribute like
#[command("Command name here")]
. If the name is not provided, the command will use the function name.
Command arguments are very similar to command functions, they also need a #[description]
attribute that will be seen
in discord by the user when filling up the command argument.
As shown in the example, a #[rename]
attribute can also be used, this will change the name of the argument seen in
discord. If the attribute is not used, the argument will have the same name as in the function.
Important: All command functions must have as the first parameter a &SlashContext<T>
Choices are a very useful feature of slash commands, allowing the developer to set some choices from which the user has to choose.
Zephyrus allows doing this in an easy way, to allow this, a derive macro is provided by the framework. This macro is
named the same way as Parse
trait and can only be used in enums to define the options. Renaming is also allowed here
by using the #[rename]
attribute and allows to change the option name seen in discord.
```rust
enum Choices { First, Second, Third, #[rename = "Forth"] Other }
async fn choices( ctx: &SlashContext<()>, #[description = "Some description"] choice: Choices ) -> DefaultCommandResult { // Command body Ok(()) } ```
Autocomplete user input is made easy with Zephyrus
, just use the autocomplete
macro provided by the framework.
Here, take a look at this example. We'll use as the base an empty command like this
```rust
async fn somecommand( ctx: &SlashCommand* Some type */>, #[autocomplete = "autocompletearg"] #[description = "Some description"] arg: String ) -> DefaultCommandResult { // Logic goes here Ok(()) } ```
As you may have noticed, we added an autocomplete
attribute to the argument arg
. The input specified on it must
point to a function marked with the #[autocomplete]
attribute like this one:
```rust
async fn autocomplete_arg(ctx: AutocompleteContext* Some type */>) -> Option
Autocompleting functions must have an AutocompleteContext<T>
as the sole parameter, it allows you to access to the
data stored at the framework while also allowing you to access the raw interaction, the framework's http client and the
user input, if exists.
To specify required permissions to run a command, just use the #[required_permissions]
attribute when declaring
a command, or the .required_permissions
method when declaring a command group.
The attribute accepts as input a comma separated list of
twilight's permissions. Let's take
a look at what it would look like to create a command needing MANAGE_CHANNELS
and MANAGE_MESSAGES
permissions:
```rust
async fn supercoolcommand(ctx: &SlashContext* Your type */>) -> DefaultCommandResult { // Body Ok(()) } ```
Zephyrus
supports both SubCommands
and SubCommandGroups
by default.
To give examples, let's say we have created the following command:
```rust
async fn something(ctx: &SlashContext* Your type */>) -> DefaultCommandResult { // Command block Ok(()) } ```
With this we can now create both subcommands and subcommand groups
To create a subcommand you need to create a group, then you can add all the subcommands.
```rust
async fn main() {
let framework = Framework::builder()
.group(|g| {
g.name("
Subcommand groups are very similar to subcommands, they are created almost the same way, but instead of using
.add_command
directly, we have to use .group
before to register a group.
```rust
async fn main() {
let framework = Framework::builder()
.group(|g| {
g.name("
There are three hooks available, before
, after
and error_handler
.
The before hook is triggered before the command and has to return a bool
indicating if the command should be executed or not.
```rust
async fn beforehook(ctx: &SlashContext*Your type*/>, commandname: &str) -> bool { // Do something
true // <- if we return true, the command will be executed normally.
} ```
The after hook is triggered after the command execution, and it provides the result of the command.
```rust
async fn afterhook(ctx: &SlashContext* Your type */>, commandname: &str, result: Option
Commands can have specific error handlers. When an error handler is set to a command, if the command (or any of its checks)
fails, the error handler will be called, and the after
hook will receive None
as the third argument. However, in case
the command execution finishes without raising errors, the after
hook will receive the result of the command.
Let's take a look at a simple implementation:
```rust
async fn handlebanerror(_ctx: &SlashContext* Some type */>, error: DefaultError) { println!("The ban command had an error");
// Handle the error
}
async fn banitself(ctx: &SlashContext* Some type */>) -> DefaultCommandResult { // A bot cannot ban itself, so this will result in an error. ctx.httpclient().ban(ctx.interaction.guildid.unwrap(), Id::new(ctx.applicationid.get())) .await?;
Ok(())
} ```
Since the command will always fail because a bot cannot ban itself, the error handler will be called everytime the command
executes, thus passing None
to the after
hook if set.
Checks are pretty similar to the Before
hook, but unlike it, they are not global. Instead, they need to be assigned
to each command.
Let's take a look on how to use it:
Let's create some checks like this:
```rust
async fn onlyguilds(ctx: &SlashContext* Some type */>) -> Result
async fn othercheck(ctx: &SlashContext* Some type */>) -> Result
Then we can assign them to our command using the check
attribute, which accepts a comma separated list of checks:
```rust
async fn my_command(ctx: &SlashContext* Some type */>) -> DefaultCommandResult { // Do something Ok(()) } ```
The framework allows the user to specify what types to return from command/checks execution. The framework definition is like this:
rust
pub struct Framework<D, T = (), E = DefaultError>
Where D
is the type of the data held by the framework and T
and E
are the return types of a command in form of
Result<T, E>
, however, specifying custom types is optional, and the framework provides a DefaultCommandResult
and
DefaultError
for those who don't want to have a custom error.
The types of the after
, error_handler
and check
hook arguments change accordingly to the generics specified in
the framework, so their signatures could be interpreted like this:
After hook:
rust
async fn(&SlashContext</* Some type */>, &str, Option<Result<T, E>>)
Error handler hook:
rust
async fn(&SlashContext</* Some type */>, E)
Command checks:
rust
async fn(&SlashContext</* Some type */>) -> Result<bool, E>
Note that those are not the real signatures, since the functions return Box
ed futures.