#command-arguments #slash-command #twilight #discord #async #arguments-parser

vesper-macros

Zephyrus 使用的过程宏

4 个版本 (破坏性)

0.13.0 2024 年 2 月 5 日
0.12.0 2023 年 12 月 28 日
0.11.0 2023 年 9 月 22 日
0.0.0 2023 年 5 月 18 日

#2000 in 异步

Download history 5/week @ 2024-03-13 1/week @ 2024-03-20 7/week @ 2024-03-27 17/week @ 2024-04-03 3/week @ 2024-04-10 33/week @ 2024-04-17 5/week @ 2024-04-24 1/week @ 2024-05-22 5/week @ 2024-05-29 3/week @ 2024-06-05 16/week @ 2024-06-12 35/week @ 2024-06-19 25/week @ 2024-06-26

每月 79 次下载
用于 vesper

MIT 许可证

95KB
1.5K SLoC

Vesper 框架 Crate

vesper 是一个用于 twilight 的命令框架

注意:该框架较新,可能存在一些问题,所有贡献都受欢迎

此包独立于 twilight 生态系统


vesper 是一个使用斜杠命令的命令框架,主要提供变量参数解析。

解析是通过 Parse 特性完成的,因此用户可以实现他们自己类型的解析。

参数解析以命名的方式进行,这意味着在 Discord 上显示的参数名称将被解析为在处理函数中以相同方式命名的参数。

框架本身不会自行启动任何任务,因此您可能需要在调用 .process 方法之前将其包装在 Arc 中并调用 tokio :: spawn


使用示例

use std::sync::Arc;
use futures_util::StreamExt;
use twilight_gateway::{stream::{self, ShardEventStream}, Config};
use twilight_http::Client;
use twilight_model::gateway::event::Event;
use twilight_model::gateway::Intents;
use twilight_model::http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType};
use twilight_model::id::Id;
use twilight_model::id::marker::{ApplicationMarker, GuildMarker};
use vesper::prelude::*;

#[command]
#[description = "Says hello"]
async fn hello(ctx: &mut SlashContext<()>) -> DefaultCommandResult {
    ctx.interaction_client.create_response(
        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 handle_events(http_client: Arc<Client>, mut events: ShardEventStream, app_id: Id<ApplicationMarker>) {
    let framework = Arc::new(Framework::builder(http_client, app_id, ())
        .command(hello)
        .build());

    // vesper 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;
                });
            },
            _ => (),
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let token = std::env::var("DISCORD_TOKEN")?;
    let app_id = Id::<ApplicationMarker>::new(std::env::var("APP_ID")?.parse()?);
    let client = Arc::new(Client::new(token.clone()));

    let config = Config::new(token, Intents::empty());
    let mut shards = stream::create_recommended(
        &client,
        config,
        |_, builder| builder.build()
    ).await.unwrap().collect::<Vec<_>>();
    let mut shard_stream = ShardEventStream::new(shards.iter_mut());

    handle_events(client, shard_stream, app_id).await;

    Ok(())
}

使用指南


创建命令

每个命令都是一个 async 函数,始终以 &mut SlashContext<T> 作为第一个参数

也可以使用非可变引用,但框架会在底层将其转换为可变引用。

该框架支持 chatmessageuser 命令,让我们来看看每个命令

聊天命令

#[command(chat)] // or #[command]
#[description = "This is the description of the command"]
async fn command(
    ctx: &mut SlashContext</* Your type of context*/>, // The context must always be the first parameter.
    #[description = "A description for the argument"] some_arg: String,
    #[rename = "other_arg"] #[description = "other description"] other: Option<Id<UserMarker>>
) -> DefaultCommandResult 
{
    // Command body
    
    Ok(())
}

用户命令

#[command(user)]
#[description = "This is the description of the command"]
async fn command(
    ctx: &mut SlashContext</* Your type of context*/>, // The context must always be the first parameter.
) -> DefaultCommandResult 
{
    // Command body
    
    Ok(())
}

消息命令

#[command(message)]
#[description = "This is the description of the command"]
async fn command(
    ctx: &mut SlashContext</* Your type of context*/>, // The context must always be the first parameter.
) -> DefaultCommandResult 
{
    // Command body
    
    Ok(())
}

如您所见,它们之间唯一的区别是使用 #[command({chat, user, message}) 和只有 chat 命令可以接受参数。

command 宏默认为 chat 命令,所以如果没有使用任何 {chat, user, message} 指定,则宏将其视为 chat 命令,因此 #[command] 等同于 #[command(chat)]

如果非聊天命令在其处理程序中接受参数,框架将允许它,但不会将它们发送到 Discord。

框架还提供了一个 #[only_guilds] 属性,将命令标记为仅可在公会中使用,以及一个 #[nsfw] 用于 nsfw 命令。

之前用作示例的相同命令可以通过以下方式标记为仅限于公会/nsfw:

#[command]
#[nsfw] // This command is now marked as nsfw
#[description = "This is the description of the command"]
async fn command(
    ctx: &mut SlashContext</* Your type of context*/>,
    #[description = "A description for the argument"] some_arg: String,
    #[rename = "other_arg"] #[description = "other description"] other: Option<Id<UserMarker>>
) -> DefaultCommandResult 
{
    // Command body
    
    Ok(())
}

#[command(chat)]
#[only_guilds] // This command is now only marked as only available inside of guilds
#[description = "This is the description of the command"]
async fn command(
    ctx: &mut SlashContext</* Your type of context*/>,
    #[description = "A description for the argument"] some_arg: String,
    #[rename = "other_arg"] #[description = "other description"] other: Option<Id<UserMarker>>
) -> DefaultCommandResult
{
    // Command body

    Ok(())
}

#[command(chat)]
#[only_guilds] // This command is now marked as nsfw and only available inside guilds
#[nsfw]
#[description = "This is the description of the command"]
async fn command(
    ctx: &mut SlashContext</* Your type of context*/>, // The context must always be the first parameter.
    #[description = "A description for the argument"] some_arg: String,
    #[rename = "other_arg"] #[description = "other description"] other: Option<Id<UserMarker>>
) -> DefaultCommandResult
{
    // Command body

    Ok(())
}

使用本地化

框架允许在命令及其参数中使用本地化,为此我们有了 #[localized_names]#[localized_descriptions] 属性,这些属性接受逗号分隔的项列表。让我们看看它们

区域设置必须有效,要查看它们,请参阅 Discord 区域设置参考

#[command]
#[localized_names("en-US" = "US name", "en-GB" = "GB name", "es-ES" = "Spanish name")]
#[localized_descriptions("en-US" = "US description", "en-GB" = "GB description", "es-ES" = "Spanish description")]
#[description = "My description"]
async fn my_localized_command(
    ctx: &mut SlashContext</* Data type */>,
    #[localized_names("en-US" = "US name", "en-GB" = "GB name", "es-ES" = "Spanish name")]
    #[description = "Another description"]
    #[localized_descriptions("en-US" = "US description", "en-GB" = "GB description", "es-ES" = "Spanish description")]
    my_argument: String
) -> DefaultCommandResult
{
    // Code here
    Ok(())
}

使用函数提供本地化

本地化也可以通过使用闭包或函数指针来设置,为此我们有了 #[localized_names_fn]#[localized_descriptions_fn]

这些函数必须具有以下签名

fn(&Framework<D, T, E>, &Command<D, T, E>) -> HashMap<String, String>

要直接使用闭包,必须像这样使用属性:#[localized_{names/descriptions}_fn = |f, c| ...]

要使用函数指针,该属性接受以下两种格式:#[localized_{names/descriptions}_fn = myfn]#[localized_{names/descriptions}_fn(myfn)]

命令函数

命令函数必须包含一个 description 属性,当用户尝试使用命令时,这个属性将在 Discord 中显示。

#[command] 宏还允许通过传递命令名称到属性中来重命名命令,例如:#[command({chat, user, message}, name = "Command name here")]。如果没有提供名称,命令将使用函数名称。

在创建 chat 命令时,如果使用 #[command] 的简写形式,重命名可以直接传递,例如:#[command("Command name")],这相当于:#[command(chat, name = "Command name")]

命令参数

命令参数与命令函数非常相似,它们也需要一个 #[description] 属性,当用户填写命令参数时,这个属性将在 Discord 中显示。

如示例所示,可以使用一个 #[rename] 属性,这将改变在 Discord 中看到的参数名称。如果没有使用该属性,参数将具有与函数中相同的名称。

参数也可以通过#[skip]属性来标记。标记为#[skip]的参数不允许使用#[description]#[rename]属性,并且在执行命令时不会被discord看到,但它们会被框架解析。这对于从交互中提取与命令输入无关的数据很有用。让我们看看一个例子

pub struct ExtractSomething {
    //...
}

#[async_trait]
impl Parse<T> for ExtractSomething
where T: Send + Sync
{
    async fn parse(
        http_client: &WrappedClient,
        data: &T,
        value: Option<&CommandOptionValue>, // <- will be empty since the option was not sent
        resolved: Option<&mut CommandInteractionDataResolved>
    ) -> Result<Self, ParseError>
    {
        // implement parsing logic
    }

    fn kind() -> CommandOptionType {
        // Since the struct will be marked as #[skip] this method won't be used.
        unreachable!()
    }
}

#[command]
#[description = "Something here"]
async fn my_command(
    ctx: &mut SlashContext</* Data type */>,
    #[skip] my_extractor: ExtractSomething // This won't be seen on discord, but will be parsed
) -> DefaultCommandResult
{
    // Command logic here
    Ok(())
}

重要:所有命令函数都必须将&mut SlashContext<T>作为第一个参数

将选择设置为命令参数

选择是slash命令的一个非常有用的功能,允许开发者设置一些用户必须从中选择的选择。

vesper允许以简单的方式实现这一点,为此,框架提供了一个名为Parse trait的derive宏。这个宏的命名方式与Parse trait相同,并且只能用于枚举中定义选项。这里也允许使用#[parse(rename)]属性来重命名,并允许更改在discord中看到的选项名称。

#[derive(Parse)]
enum Choices {
    First,
    Second,
    Third,
    #[parse(rename = "Forth")]
    Other
}

#[command]
#[description = "Some description"]
async fn choices(
    ctx: &mut SlashContext<()>,
    #[description = "Some description"] choice: Choices
) -> DefaultCommandResult
{
    // Command body
    Ok(())
}

自动完成命令

使用vesper可以轻松实现自动完成用户输入,只需使用框架提供的autocomplete宏即可。

下面,让我们看看这个例子。我们将以一个空命令为基础

#[command]
#[description = "Some description"]
async fn some_command(
    ctx: &mut SlashCommand</* Some type */>,
    #[autocomplete = "autocomplete_arg"] #[description = "Some description"] arg: String
) -> DefaultCommandResult
{
    // Logic goes here
    Ok(())
}

你可能已经注意到了,我们在arg参数上添加了一个autocomplete属性。指定在其上的输入必须指向一个带有#[autocomplete]属性的函数,如下所示

#[autocomplete]
async fn autocomplete_arg(ctx: AutocompleteContext</* Some type */>) -> Option<InteractionResponseData> {
    // Function body
}

自动完成函数必须有一个AutocompleteContext<T>作为唯一参数,它允许你访问框架中存储的数据,同时允许你访问原始交互、框架的http客户端和用户输入(如果存在)。

权限

要指定运行命令所需的权限,只需在声明命令时使用#[required_permissions]属性,或者在声明命令组时使用.required_permissions方法。

该属性接受逗号分隔的列表作为输入,该列表包含twilight的权限。让我们看看创建需要MANAGE_CHANNELSMANAGE_MESSAGES权限的命令会是什么样子

#[command]
#[description = "Super cool command"]
#[required_permissions(MANAGE_CHANNELS, MANAGE_MESSAGES)]
async fn super_cool_command(ctx: &mut SlashContext</* Your type */>) -> DefaultCommandResult {
    // Body
    Ok(())
}

命令组

vesper默认支持SubCommandsSubCommandGroups

为了举例,假设我们创建了以下命令

#[command]
#[description = "Something"]
async fn something(ctx: &mut SlashContext</* Your type */>) -> DefaultCommandResult {
    // Command block
    Ok(())
}

有了这个,我们现在可以创建子命令和子命令组

创建子命令

要创建子命令,你需要创建一个组,然后你可以添加所有子命令。

#[tokio::main]
async fn main() {
    let framework = Framework::builder()
        .group(|g| {
            g.name("<GROUP_NAME>")
                .description("<GROUP_DESCRIPTION>")
                .add_command(something)
                .add_command(..)
                ..
        })
        .build();
}

创建子命令分组

子命令分组与子命令非常相似,它们几乎以相同的方式创建,但是我们需要在注册分组之前使用 .group 而不是直接使用 .add_command

#[tokio::main]
async fn main() {
    let framework = Framework::builder()
        .group(|g| {
            g.name("<GROUP_NAME>")
                .description("<GROUP_DESCRIPTION>")
                .group(|sub| { // With this we have created a subcommand group.
                    sub.name("<SUBGROUP_NAME>")
                        .description("<SUBGROUP_DESCRIPTION>")
                        .add_command(something)
                        .add_command(..)
                        ..
                })
        })
        .build();
}

钩子

有三种可用的钩子,分别是 beforeaftererror_handler

before(之前)

before 钩子在命令之前触发,并必须返回一个 bool 值,表示是否执行该命令。

#[before]
async fn before_hook(ctx: &mut SlashContext</*Your type*/>, command_name: &str) -> bool {
    // Do something
    
    true // <- if we return true, the command will be executed normally.
}

after(之后)

after 钩子在命令执行后触发,并提供命令的结果。

#[after]
async fn after_hook(ctx: &mut SlashContext</* Your type */>, command_name: &str, result: Option<DefaultCommandResult>) {
    // Do something with the result.
}

特定错误处理

命令可以有特定的错误处理器。当错误处理器被设置到命令上时,如果命令(或其检查中的任何一个)失败,错误处理器将被调用,并且 after 钩子将接收 None 作为第三个参数。然而,如果命令执行没有引发错误而完成,则 after 钩子将接收命令的结果。

让我们看看一个简单的实现示例

#[error_handler]
async fn handle_ban_error(_ctx: &mut SlashContext</* Some type */>, error: DefaultError) {
    println!("The ban command had an error");
    
    // Handle the error
}


#[command]
#[description = "Tries to ban the bot itself, raising an error"]
#[error_handler(handle_ban_error)]
async fn ban_itself(ctx: &mut SlashContext</* Some type */>) -> DefaultCommandResult {
    // A bot cannot ban itself, so this will result in an error.
    ctx.http_client().ban(ctx.interaction.guild_id.unwrap(), Id::new(ctx.application_id.get()))
        .await?;
    
    Ok(())
}

由于命令总是会失败,因为机器人无法将自己封禁,所以错误处理器会在每次命令执行时被调用,因此如果设置了,将 None 传递给 after 钩子。


检查

检查与 Before 钩子非常相似,但与它不同,它们不是全局的。相反,它们需要分配给每个命令。

让我们看看如何使用它

让我们创建一些检查,如下所示

#[check]
async fn only_guilds(ctx: &mut SlashContext</* Some type */>) -> Result<bool, DefaultError> {
    // Only execute the command if we are inside a guild.
    Ok(ctx.interaction.guild_id.is_some())
}

#[check]
async fn other_check(_ctx: &mut SlashContext</* Some type */>) -> Result<bool, DefaultError> {
    // Some other check here.
    Ok(true)
}

然后我们可以使用 check 属性将它们分配给我们的命令,该属性接受一个逗号分隔的检查列表

#[command]
#[description = "Some description"]
#[checks(only_guilds, other_check)]
async fn my_command(ctx: &mut SlashContext</* Some type */>) -> DefaultCommandResult {
    // Do something
    Ok(())
}

使用自定义返回类型

框架允许用户指定从命令/检查执行中返回的类型。框架的定义如下所示

pub struct Framework<D, T = (), E = DefaultError>

其中 D 是框架持有的数据类型,TE 是命令的返回类型,形式为 Result<T, E>,但是指定自定义类型是可选的,框架为那些不想有自定义错误的人提供了 DefaultCommandResultDefaultError

aftererror_handlercheck 钩子参数的类型相应地更改,以符合框架中指定的泛型,因此它们的签名可以解释如下

after 钩子

async fn(&mut SlashContext</* Some type */>, &str, Option<Result<T, E>>)

错误处理器钩子

async fn(&mut SlashContext</* Some type */>, E)

命令检查

async fn(&mut SlashContext</* Some type */>) -> Result<bool, E>

请注意,这些不是真正的签名,因为函数返回 Box 包裹的 future。

模态

从版本 0.8.0 开始,框架提供了一个 derive 宏,使模态尽可能简单。让我们看看一个示例

use vesper::prelude::*;

#[derive(Modal, Debug)]
#[modal(title = "Test modal")]
struct MyModal {
    field: String,
    #[modal(paragraph, label = "Paragraph")]
    paragraph: String,
    #[modal(placeholder = "This is an optional field")]
    optional: Option<String>
}

#[command]
#[description = "My command description"]
async fn my_command(ctx: &mut SlashContext</* Some type */>) -> DefaultCommandResult {
    let modal_waiter = ctx.create_modal::<MyModal>().await?;
    let output = modal_waiter.await?;
    
    println!("{output:?}");
    
    Ok(())
}

在这里,`Modal` derive 宏导出模态 trait,允许我们创建它们,然后我们可以使用 #[modal(..)} 属性来修改它将如何显示给用户。要查看允许的属性列表,请参阅宏声明

目前,仅允许 StringOption<String> 字段。

批量命令覆盖

如果您想使用 Discord 的 批量覆盖全局应用命令 端点,可能还需要配合一个 命令锁定文件,您将需要使用 Framework#twilight_commands

注意 这需要 bulk 功能。

fn create_framework(
    http_client: Arc<Client>,
    app_id: Id<ApplicationMarker>
) -> Framework<()> {
    Framework::builder(http_client, app_id, ())
        .command(hello)
        .build()
}

fn create_lockfile(framework: Framework<()>) -> Result<()> {
    let commands = framework.twilight_commands();
    let content = serde_json::to_string_pretty(&commands)?;

    let path = concat!(env!("CARGO_MANIFEST_DIR"), "/commands.lock.json").to_string();
	std::fs::write(path, content).unwrap();

    Ok(())
}

依赖项

~0.6–1.1MB
~25K SLoC