Add help command

This commit is contained in:
2025-01-30 02:34:03 +00:00
parent 417621d422
commit 982156e443
6 changed files with 224 additions and 0 deletions

13
Cargo.lock generated
View File

@@ -343,6 +343,7 @@ dependencies = [
"clap 4.5.27",
"dotenvy",
"env_logger",
"futures",
"humantime",
"log",
"poise",
@@ -857,6 +858,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@@ -879,6 +881,17 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"

View File

@@ -16,6 +16,7 @@ secrecy = "0.10.3"
serenity = "0.12.4"
thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["full"] }
futures = "0.3.31"
[features]
default = ["mysql", "postgres", "sqlite"]

View File

@@ -19,6 +19,7 @@ where
let app_data = AppData {
repository_provider,
qualified_command_names: commands::qualified_command_names(&commands),
};
let options = FrameworkOptions::<AppData<R>, AppError<R::BackendError>> {

View File

@@ -18,6 +18,7 @@ pub enum AppStartError {
pub struct AppData<R> {
repository_provider: R,
qualified_command_names: Vec<String>,
}
#[derive(Debug, thiserror::Error)]
@@ -61,3 +62,9 @@ where
self.repository_provider.get().await
}
}
impl<R> AppData<R> {
pub fn qualified_command_names(&self) -> &[String] {
&self.qualified_command_names
}
}

View File

@@ -0,0 +1,173 @@
use futures::Stream;
use cipher_core::repository::RepositoryProvider;
use poise::CreateReply;
use serenity::all::Color;
use serenity::all::CreateEmbed;
use serenity::futures::StreamExt;
use crate::app::AppCommand;
use crate::app::AppContext;
use crate::app::AppError;
async fn autocomplete_command<'a, R> (
ctx: AppContext<'a, R, R::BackendError>,
partial: &'a str,
) -> impl Stream<Item = String> + 'a
where
R: RepositoryProvider,
{
futures::stream::iter(ctx.data().qualified_command_names())
.filter(move |name| futures::future::ready(name.contains(partial)))
.map(|name| name.to_string())
}
/// Show help message.
#[poise::command(slash_command)]
pub async fn help<R: RepositoryProvider + Sync>(
ctx: AppContext<'_, R, R::BackendError>,
#[rename = "command"]
#[description = "Command to get help for."]
#[autocomplete = "autocomplete_command"]
option_query: Option<String>,
#[description = "Show hidden commands. Defaults to False."] all: Option<bool>,
#[description = "Hide reply from other users. Defaults to True."] ephemeral: Option<bool>,
) -> Result<(), AppError<R::BackendError>> {
let embed = if let Some(query) = &option_query {
let option_command = poise::find_command(
&ctx.framework().options.commands,
query,
true,
&mut Vec::new(),
);
if let Some((command, _, _)) = option_command {
command_help_embed(command, all.unwrap_or(false))
} else {
CreateEmbed::new()
.title("Help")
.description(format!("Could not find command `/{}`", query))
.color(Color::BLURPLE)
}
} else {
root_help_embed(&ctx, all.unwrap_or(false))
};
let reply = CreateReply::default()
.embed(embed)
.ephemeral(ephemeral.unwrap_or(true));
ctx.send(reply).await?;
Ok(())
}
fn root_help_embed<R>(ctx: &AppContext<'_, R, R::BackendError>, all: bool) -> CreateEmbed
where
R: RepositoryProvider,
{
let mut commands_field_value = String::new();
for command in &ctx.framework().options.commands {
if !all && command.hide_in_help {
continue;
}
commands_field_value.push('`');
commands_field_value.push('/');
commands_field_value.push_str(&command.name);
commands_field_value.push('`');
if let Some(command_description) = &command.description {
commands_field_value.push_str(" - ");
commands_field_value.push_str(command_description);
}
commands_field_value.push('\n');
}
commands_field_value.pop();
let mut embed = CreateEmbed::new()
.title("Help")
.color(Color::BLURPLE);
if !commands_field_value.is_empty() {
embed = embed.field("Commands", commands_field_value, false);
}
embed = embed.field("More", "For information on specific commands `/help <command>`.", false);
embed
}
fn command_help_embed<R>(command: &AppCommand<R, R::BackendError>, all: bool) -> CreateEmbed
where
R: RepositoryProvider,
{
let mut required_parameters_field_value = String::new();
let mut optional_parameters_field_value = String::new();
for parameter in &command.parameters {
let parameters_field_value = match parameter.required {
true => &mut required_parameters_field_value,
false => &mut optional_parameters_field_value,
};
parameters_field_value.push('`');
parameters_field_value.push_str(&parameter.name);
parameters_field_value.push('`');
if let Some(parameter_description) = &parameter.description {
parameters_field_value.push_str(" - ");
parameters_field_value.push_str(parameter_description);
}
parameters_field_value.push('\n');
}
required_parameters_field_value.pop(); // Removes final newline
optional_parameters_field_value.pop(); // Removes final newline
let mut subcommands_field_value = String::new();
for subcommand in &command.subcommands {
if !all && subcommand.hide_in_help {
continue;
}
subcommands_field_value.push('`');
subcommands_field_value.push_str(&subcommand.name);
subcommands_field_value.push('`');
if let Some(command_description) = &subcommand.description {
subcommands_field_value.push_str(" - ");
subcommands_field_value.push_str(command_description);
}
subcommands_field_value.push('\n');
}
subcommands_field_value.pop(); // Removes final newline
let mut embed = CreateEmbed::new()
.title(format!("Help `/{}`", command.qualified_name))
.color(Color::BLURPLE);
if let Some(category) = &command.category {
embed = embed.field("Category", category, false);
}
if !required_parameters_field_value.is_empty() {
embed = embed.field("Required Parameters", required_parameters_field_value, false);
}
if !optional_parameters_field_value.is_empty() {
embed = embed.field("Optional Parameters", optional_parameters_field_value, false);
}
if !subcommands_field_value.is_empty() {
embed = embed.field("Subcommands", subcommands_field_value, false);
}
if let Some(description) = &command.description {
embed = embed.description(description);
}
embed = embed.field("More", "For information on specific commands `/help <command>`.", false);
embed
}

View File

@@ -2,6 +2,7 @@ use cipher_core::repository::RepositoryProvider;
use crate::app::AppCommand;
mod help;
mod ping;
mod profile;
@@ -10,7 +11,35 @@ where
R: RepositoryProvider + Send + Sync + 'static,
{
vec![
help::help(),
ping::ping(),
profile::profile(),
]
}
pub fn qualified_command_names<R>(commands: &[AppCommand<R, R::BackendError>]) -> Vec<String>
where
R: RepositoryProvider,
{
let mut prefix = String::new();
let mut names = Vec::new();
qualified_command_names_inner(&mut prefix, commands, &mut names);
names
}
fn qualified_command_names_inner<R>(prefix: &mut String, commands: &[AppCommand<R, R::BackendError>], names: &mut Vec<String>)
where
R: RepositoryProvider,
{
for command in commands {
if command.subcommands.is_empty() {
names.push(format!("{}{}", prefix, command.qualified_name.clone()));
} else {
let old_len = prefix.len();
prefix.push_str(&command.qualified_name);
prefix.push(' ');
qualified_command_names_inner(prefix, &command.subcommands, names);
prefix.truncate(old_len);
}
}
}