use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use autumn_web::{AutumnError, AutumnResult};
use crate::client::DiscordClient;
use crate::error::{Error, Result};
use crate::types::{
CommandOption, CommandRegistration, Interaction, InteractionResponse, MessagePayload,
};
pub type CommandFuture =
Pin<Box<dyn Future<Output = AutumnResult<InteractionResponse>> + Send + 'static>>;
pub type CommandHandler = Arc<dyn Fn(CommandContext) -> CommandFuture + Send + Sync + 'static>;
#[derive(Clone)]
pub struct CommandContext {
pub interaction: Interaction,
pub client: DiscordClient,
}
impl CommandContext {
#[must_use]
pub fn command_name(&self) -> Option<&str> {
self.interaction.command_name()
}
}
pub trait IntoCommandHandler {
fn into_handler(self) -> CommandHandler;
}
impl<F, Fut> IntoCommandHandler for F
where
F: Fn(CommandContext) -> Fut + Send + Sync + 'static,
Fut: Future<Output = AutumnResult<InteractionResponse>> + Send + 'static,
{
fn into_handler(self) -> CommandHandler {
Arc::new(move |ctx| Box::pin(self(ctx)))
}
}
#[derive(Clone)]
pub struct Command {
name: String,
description: String,
options: Vec<CommandOption>,
handler: CommandHandler,
}
impl Command {
#[must_use]
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
options: Vec::new(),
handler: Arc::new(|_| {
Box::pin(async {
Ok(InteractionResponse::channel_message(
MessagePayload::content("no handler registered"),
))
})
}),
}
}
#[must_use]
pub fn option(mut self, option: CommandOption) -> Self {
self.options.push(option);
self
}
#[must_use]
pub fn handler(mut self, handler: impl IntoCommandHandler) -> Self {
self.handler = handler.into_handler();
self
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
pub fn validate(&self) -> Result<()> {
if self.name.is_empty() || self.name.len() > 32 {
return Err(Error::Config(format!(
"command name {:?} must be 1-32 chars",
self.name
)));
}
let valid = self
.name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_');
if !valid {
return Err(Error::Config(format!(
"command name {:?} must be lowercase letters, digits, '-', or '_'",
self.name
)));
}
if self.description.is_empty() || self.description.len() > 100 {
return Err(Error::Config(format!(
"command {:?} description must be 1-100 chars",
self.name
)));
}
Ok(())
}
#[must_use]
pub fn registration(&self) -> CommandRegistration {
CommandRegistration {
name: self.name.clone(),
description: self.description.clone(),
options: self.options.clone(),
}
}
}
#[derive(Default)]
pub struct CommandRegistry {
commands: HashMap<String, Command>,
}
impl CommandRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, command: Command) -> Result<()> {
command.validate()?;
self.commands.insert(command.name.clone(), command);
Ok(())
}
#[must_use]
pub fn command_names(&self) -> Vec<&str> {
let mut names: Vec<&str> = self.commands.keys().map(String::as_str).collect();
names.sort_unstable();
names
}
#[must_use]
pub fn registrations(&self) -> Vec<CommandRegistration> {
let mut registrations: Vec<CommandRegistration> =
self.commands.values().map(Command::registration).collect();
registrations.sort_by(|a, b| a.name.cmp(&b.name));
registrations
}
pub async fn dispatch(&self, ctx: CommandContext) -> AutumnResult<InteractionResponse> {
let name = ctx
.command_name()
.ok_or_else(|| AutumnError::bad_request_msg("interaction carries no command name"))?;
let command = self
.commands
.get(name)
.ok_or_else(|| Error::UnknownCommand(name.to_owned()).to_autumn_error())?;
(command.handler)(ctx).await
}
}
#[cfg(test)]
mod tests;