use crate::message::ChannelMessage;
use async_trait::async_trait;
use log::{debug, error, info, warn};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[async_trait]
pub trait PrefixCommand: Send + Sync {
async fn execute(
&self,
message: &ChannelMessage,
args: Vec<&str>,
) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>>;
fn description(&self) -> &str;
fn aliases(&self) -> Vec<&str> {
vec![]
}
}
pub struct PrefixListener {
prefix: String,
commands: Arc<RwLock<HashMap<String, Box<dyn PrefixCommand>>>>,
case_sensitive: bool,
}
impl PrefixListener {
pub fn new(prefix: &str) -> Self {
info!("Creating new PrefixListener with prefix: '{}'", prefix);
Self {
prefix: prefix.to_string(),
commands: Arc::new(RwLock::new(HashMap::new())),
case_sensitive: false,
}
}
pub fn new_case_sensitive(prefix: &str) -> Self {
info!(
"Creating new case-sensitive PrefixListener with prefix: '{}'",
prefix
);
Self {
prefix: prefix.to_string(),
commands: Arc::new(RwLock::new(HashMap::new())),
case_sensitive: true,
}
}
pub async fn register_command(&self, name: &str, command: Box<dyn PrefixCommand>) {
let mut commands = self.commands.write().await;
let key = if self.case_sensitive {
name.to_string()
} else {
name.to_lowercase()
};
info!(
"Registering command '{}' with prefix '{}'",
name, self.prefix
);
commands.insert(key.clone(), command);
if let Some(cmd) = commands.get(&key) {
let aliases = cmd.aliases();
for alias in aliases {
let _alias_key = if self.case_sensitive {
alias.to_string()
} else {
alias.to_lowercase()
};
debug!("Registering alias '{}' for command '{}'", alias, name);
}
}
debug!("Total commands registered: {}", commands.len());
}
pub async fn unregister_command(&self, name: &str) {
let mut commands = self.commands.write().await;
let key = if self.case_sensitive {
name.to_string()
} else {
name.to_lowercase()
};
if commands.remove(&key).is_some() {
info!("Unregistered command '{}'", name);
} else {
warn!("Attempted to unregister non-existent command '{}'", name);
}
}
pub async fn handle_message(
&self,
message: &ChannelMessage,
) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
if !message.content.starts_with(&self.prefix) {
return Ok(None);
}
debug!(
"Message matches prefix '{}': {}",
self.prefix, message.content
);
let content_without_prefix = &message.content[self.prefix.len()..];
let parts: Vec<&str> = content_without_prefix.split_whitespace().collect();
if parts.is_empty() {
debug!("No command specified after prefix");
return Ok(None);
}
let command_name = parts[0];
let args = if parts.len() > 1 { &parts[1..] } else { &[] };
let key = if self.case_sensitive {
command_name.to_string()
} else {
command_name.to_lowercase()
};
debug!(
"Looking for command '{}' (normalized: '{}') with {} args",
command_name,
key,
args.len()
);
let commands = self.commands.read().await;
if let Some(command) = commands.get(&key) {
info!(
"Executing command '{}' for user {} with args: {:?}",
command_name, message.author.id, args
);
match command.execute(message, args.to_vec()).await {
Ok(response) => {
if response.is_some() {
debug!("Command '{}' returned response", command_name);
} else {
debug!("Command '{}' returned no response", command_name);
}
Ok(response)
}
Err(e) => {
error!("Error executing command '{}': {}", command_name, e);
Err(e)
}
}
} else {
debug!(
"Unknown command '{}' with prefix '{}'",
command_name, self.prefix
);
Ok(None)
}
}
pub async fn list_commands(&self) -> Vec<String> {
let commands = self.commands.read().await;
commands.keys().cloned().collect()
}
pub async fn get_command_help(&self, command_name: &str) -> Option<String> {
let key = if self.case_sensitive {
command_name.to_string()
} else {
command_name.to_lowercase()
};
let commands = self.commands.read().await;
commands.get(&key).map(|cmd| cmd.description().to_string())
}
pub fn prefix(&self) -> &str {
&self.prefix
}
}
pub struct HelpCommand {
listener: Arc<PrefixListener>,
}
impl HelpCommand {
pub fn new(listener: Arc<PrefixListener>) -> Self {
Self { listener }
}
}
#[async_trait]
impl PrefixCommand for HelpCommand {
async fn execute(
&self,
_message: &ChannelMessage,
args: Vec<&str>,
) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
if args.is_empty() {
let commands = self.listener.list_commands().await;
if commands.is_empty() {
Ok(Some("No commands available.".to_string()))
} else {
let mut response = format!(
"Available commands (prefix: `{}`):\n",
self.listener.prefix()
);
for command in commands {
response.push_str(&format!("• `{}{}`\n", self.listener.prefix(), command));
}
response.push_str(&format!(
"\nUse `{}help <command>` for detailed help.",
self.listener.prefix()
));
Ok(Some(response))
}
} else {
let command_name = args[0];
if let Some(help_text) = self.listener.get_command_help(command_name).await {
Ok(Some(format!(
"**{}{}**: {}",
self.listener.prefix(),
command_name,
help_text
)))
} else {
Ok(Some(format!(
"Command `{}{}` not found.",
self.listener.prefix(),
command_name
)))
}
}
}
fn description(&self) -> &str {
"Show available commands or get help for a specific command"
}
fn aliases(&self) -> Vec<&str> {
vec!["h", "?"]
}
}
pub struct PingCommand;
#[async_trait]
impl PrefixCommand for PingCommand {
async fn execute(
&self,
_message: &ChannelMessage,
_args: Vec<&str>,
) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
Ok(Some("Pong! 🏓".to_string()))
}
fn description(&self) -> &str {
"Test if the bot is responding"
}
fn aliases(&self) -> Vec<&str> {
vec!["pong"]
}
}
pub struct EchoPrefixCommand;
#[async_trait]
impl PrefixCommand for EchoPrefixCommand {
async fn execute(
&self,
_message: &ChannelMessage,
args: Vec<&str>,
) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
if args.is_empty() {
Ok(Some("Please provide text to echo!".to_string()))
} else {
Ok(Some(args.join(" ")))
}
}
fn description(&self) -> &str {
"Echo back the provided text"
}
fn aliases(&self) -> Vec<&str> {
vec!["repeat", "say"]
}
}