Add help command
This commit is contained in:
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -343,6 +343,7 @@ dependencies = [
|
|||||||
"clap 4.5.27",
|
"clap 4.5.27",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"futures",
|
||||||
"humantime",
|
"humantime",
|
||||||
"log",
|
"log",
|
||||||
"poise",
|
"poise",
|
||||||
@@ -857,6 +858,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
@@ -879,6 +881,17 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
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]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ secrecy = "0.10.3"
|
|||||||
serenity = "0.12.4"
|
serenity = "0.12.4"
|
||||||
thiserror = "2.0.11"
|
thiserror = "2.0.11"
|
||||||
tokio = { version = "1.43.0", features = ["full"] }
|
tokio = { version = "1.43.0", features = ["full"] }
|
||||||
|
futures = "0.3.31"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["mysql", "postgres", "sqlite"]
|
default = ["mysql", "postgres", "sqlite"]
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ where
|
|||||||
|
|
||||||
let app_data = AppData {
|
let app_data = AppData {
|
||||||
repository_provider,
|
repository_provider,
|
||||||
|
qualified_command_names: commands::qualified_command_names(&commands),
|
||||||
};
|
};
|
||||||
|
|
||||||
let options = FrameworkOptions::<AppData<R>, AppError<R::BackendError>> {
|
let options = FrameworkOptions::<AppData<R>, AppError<R::BackendError>> {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub enum AppStartError {
|
|||||||
|
|
||||||
pub struct AppData<R> {
|
pub struct AppData<R> {
|
||||||
repository_provider: R,
|
repository_provider: R,
|
||||||
|
qualified_command_names: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
@@ -61,3 +62,9 @@ where
|
|||||||
self.repository_provider.get().await
|
self.repository_provider.get().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<R> AppData<R> {
|
||||||
|
pub fn qualified_command_names(&self) -> &[String] {
|
||||||
|
&self.qualified_command_names
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
173
cipher_discord_bot/src/commands/help.rs
Normal file
173
cipher_discord_bot/src/commands/help.rs
Normal 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(¶meter.name);
|
||||||
|
parameters_field_value.push('`');
|
||||||
|
|
||||||
|
if let Some(parameter_description) = ¶meter.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
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ use cipher_core::repository::RepositoryProvider;
|
|||||||
|
|
||||||
use crate::app::AppCommand;
|
use crate::app::AppCommand;
|
||||||
|
|
||||||
|
mod help;
|
||||||
mod ping;
|
mod ping;
|
||||||
mod profile;
|
mod profile;
|
||||||
|
|
||||||
@@ -10,7 +11,35 @@ where
|
|||||||
R: RepositoryProvider + Send + Sync + 'static,
|
R: RepositoryProvider + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
vec![
|
vec![
|
||||||
|
help::help(),
|
||||||
ping::ping(),
|
ping::ping(),
|
||||||
profile::profile(),
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user