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日 |
#180 in 异步
每月 35 次下载
140KB
2.5K SLoC
Vesper 框架
vesper
是一个旨在由 twilight 使用的 slash 命令框架
注意:该框架是新的,可能存在一些问题,所有贡献都受到欢迎
此软件包独立于 twilight 生态系统
vesper
是一个使用 slash 命令的命令框架,它主要提供变量参数解析。
解析是通过 Parse
特性完成的,因此用户可以为其自己的类型实现解析。
参数解析以命名的方式进行,这意味着在 Discord 上显示的参数名称会被解析为处理器函数中同名的参数。
框架本身 不 会自行启动任何任务,因此您可能需要在调用 tokio::spawn
之前将其包装在 Arc
中,并调用 .process
方法。
使用示例
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>
作为第一个参数
也可以使用不可变引用,但框架会在底层将其转换为可变引用。
该框架支持 chat
、message
和 user
命令,让我们看看每个命令
聊天命令
#[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允许以简单的方式实现这一点,为此,框架提供了一个名为derive的宏。这个宏的命名方式与Parse
特质相同,并且只能在枚举中定义选项。也可以使用#[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_CHANNELS
和MANAGE_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
默认支持SubCommands
和SubCommandGroups
。
为了举例,假设我们创建了一个以下命令
#[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();
}
创建子命令组
子命令组与子命令非常相似,它们的创建方式几乎相同,但与直接使用 .add_command
不同,我们需要在使用 .group
注册组之前。
#[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();
}
钩子
有三种可用的钩子,分别是 before
、after
和 error_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
是框架持有数据的类型,T
和 E
是命令的返回类型,以 Result<T, E>
的形式,但是指定自定义类型是可选的,对于不想有自定义错误的用户,框架提供了 DefaultCommandResult
和 DefaultError
。
根据框架中指定的泛型,after
、error_handler
和 check
钩子参数的类型相应改变,因此它们的签名可以解释如下
After 钩子
async fn(&mut SlashContext</* Some type */>, &str, Option<Result<T, E>>)
Error handler 钩子
async fn(&mut SlashContext</* Some type */>, E)
命令检查
async fn(&mut SlashContext</* Some type */>) -> Result<bool, E>
请注意,这些不是真正的签名,因为函数返回 Box
ed futures。
模态
从版本 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` 派生宏派生出 `modal` 特性,这使得我们能够创建模态框,然后我们可以使用 #[modal(..)}
属性来修改如何向用户展示它们。要查看允许的属性完整列表,请查看 宏声明。
目前,仅允许 String
和 Option<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(())
}
依赖项
~12–20MB
~281K SLoC