Make Command system more flexible

This commit is contained in:
Joscha 2023-01-26 17:51:04 +01:00
parent 2719ab3301
commit ff886efd78
6 changed files with 289 additions and 261 deletions

View file

@ -1,153 +1,31 @@
use std::collections::HashMap;
use crate::api::packet::ParsedPacket;
use crate::api::{Data, SendEvent};
use crate::conn;
use super::command::{Command, Context, Kind};
use super::command::{Command, Context};
use super::instance::{InstanceConfig, Snapshot};
fn normalize_specific_nick(nick: &str) -> String {
Kind::specific_nick(nick).to_lowercase()
}
/// Parse leading whitespace followed by an `!`-initiated command.
///
/// Returns the command name and the remaining text with one leading whitespace
/// removed. The remaining text may be the empty string.
fn parse_command(text: &str) -> Option<(&str, &str)> {
let text = text.trim_start();
let text = text.strip_prefix('!')?;
let (name, rest) = text.split_once(char::is_whitespace).unwrap_or((text, ""));
if name.is_empty() {
return None;
}
Some((name, rest))
}
/// Parse leading whitespace followed by an `@`-initiated nick.
///
/// Returns the nick and the remaining text with one leading whitespace removed.
/// The remaining text may be the empty string.
fn parse_specific(text: &str) -> Option<(&str, &str)> {
let text = text.trim_start();
let text = text.strip_prefix('@')?;
let (name, rest) = text.split_once(char::is_whitespace).unwrap_or((text, ""));
if name.is_empty() {
return None;
}
Some((name, rest))
}
pub struct CommandInfo {
pub kind: Kind,
pub name: String,
pub description: Option<String>,
pub visible: bool,
}
struct CommandWrapper<B, E> {
command: Box<dyn Command<B, E>>,
visible: bool,
}
pub struct Commands<B, E> {
global: HashMap<String, CommandWrapper<B, E>>,
general: HashMap<String, CommandWrapper<B, E>>,
specific: HashMap<String, CommandWrapper<B, E>>,
commands: Vec<Box<dyn Command<B, E> + Send + Sync>>,
}
impl<B, E> Commands<B, E> {
pub fn new() -> Self {
Self {
global: HashMap::new(),
general: HashMap::new(),
specific: HashMap::new(),
}
Self { commands: vec![] }
}
/// Global commands always respond. They override any specific or general
/// commands of the same name.
///
/// Use this if your bot "owns" the command and no other bot uses it.
pub fn global<S, C>(mut self, name: S, command: C, visible: bool) -> Self
pub fn add<C>(&mut self, command: C)
where
S: ToString,
C: Command<B, E> + 'static,
C: Command<B, E> + Send + Sync + 'static,
{
let command = Box::new(command);
let info = CommandWrapper { command, visible };
self.global.insert(name.to_string(), info);
self
self.commands.push(Box::new(command));
}
/// General commands only respond if no nick is specified.
///
/// Use this if your or any other bot has a specific command of the same
/// name.
pub fn general<S, C>(mut self, name: S, command: C, visible: bool) -> Self
where
S: ToString,
C: Command<B, E> + 'static,
{
let command = Box::new(command);
let info = CommandWrapper { command, visible };
self.general.insert(name.to_string(), info);
self
}
/// Specific commands only respond if the bot's current nick is specified.
pub fn specific<S, C>(mut self, name: S, command: C, visible: bool) -> Self
where
S: ToString,
C: Command<B, E> + 'static,
{
let command = Box::new(command);
let info = CommandWrapper { command, visible };
self.specific.insert(name.to_string(), info);
self
}
pub fn descriptions(&self) -> Vec<CommandInfo> {
let mut keys = (self.global.keys())
.chain(self.general.keys())
.chain(self.specific.keys())
.collect::<Vec<_>>();
keys.sort_unstable();
keys.dedup();
let mut result = vec![];
for name in keys {
if let Some(wrapper) = self.global.get(name) {
result.push(CommandInfo {
name: name.clone(),
kind: Kind::Global,
visible: wrapper.visible,
description: wrapper.command.description(),
});
continue; // Shadows general and specific commands
}
if let Some(wrapper) = self.general.get(name) {
result.push(CommandInfo {
name: name.clone(),
kind: Kind::General,
visible: wrapper.visible,
description: wrapper.command.description(),
});
}
if let Some(wrapper) = self.specific.get(name) {
result.push(CommandInfo {
name: name.clone(),
kind: Kind::Specific,
visible: wrapper.visible,
description: wrapper.command.description(),
});
}
}
result
pub fn descriptions(&self, ctx: &Context) -> Vec<String> {
self.commands
.iter()
.filter_map(|c| c.description(ctx))
.collect::<Vec<_>>()
}
/// Returns `true` if a command was found and executed, `false` otherwise.
@ -157,63 +35,28 @@ impl<B, E> Commands<B, E> {
packet: &ParsedPacket,
snapshot: &Snapshot,
bot: &mut B,
) -> Result<bool, E> {
) -> Result<(), E> {
let msg = match &packet.content {
Ok(Data::SendEvent(SendEvent(msg))) => msg,
_ => return Ok(false),
_ => return Ok(()),
};
let joined = match &snapshot.state {
conn::State::Joining(_) => return Ok(false),
conn::State::Joining(_) => return Ok(()),
conn::State::Joined(joined) => joined.clone(),
};
let (cmd_name, rest) = match parse_command(&msg.content) {
Some(parsed) => parsed,
None => return Ok(false),
};
let mut ctx = Context {
name: cmd_name.to_string(),
kind: Kind::Global,
let ctx = Context {
config: config.clone(),
conn_tx: snapshot.conn_tx.clone(),
joined,
};
if let Some(wrapper) = self.global.get(cmd_name) {
ctx.kind = Kind::Global;
wrapper.command.execute(rest, msg, &ctx, bot).await?;
return Ok(true);
for command in &self.commands {
command.execute(&msg.content, msg, &ctx, bot).await?;
}
if let Some((cmd_nick, rest)) = parse_specific(rest) {
if let Some(wrapper) = self.specific.get(cmd_name) {
let nick_norm = normalize_specific_nick(&ctx.joined.session.name);
let cmd_nick_norm = normalize_specific_nick(cmd_nick);
if nick_norm == cmd_nick_norm {
ctx.kind = Kind::Specific;
wrapper.command.execute(rest, msg, &ctx, bot).await?;
return Ok(true);
}
}
// The command looks like a specific command. If we treated it like
// a general command just because the nick doesn't match, we would
// interpret other bots' specific commands as general commands.
//
// To call a specific command with a mention as its first positional
// argument, -- can be used.
return Ok(false);
}
if let Some(wrapper) = self.general.get(cmd_name) {
ctx.kind = Kind::General;
wrapper.command.execute(rest, msg, &ctx, bot).await?;
return Ok(true);
}
Ok(false)
Ok(())
}
}
@ -222,36 +65,3 @@ impl<B, E> Default for Commands<B, E> {
Self::new()
}
}
#[cfg(test)]
mod test {
use super::{parse_command, parse_specific};
#[test]
fn test_parse_command() {
assert_eq!(parse_command("!foo"), Some(("foo", "")));
assert_eq!(parse_command(" !foo"), Some(("foo", "")));
assert_eq!(parse_command("!foo "), Some(("foo", " ")));
assert_eq!(parse_command(" !foo "), Some(("foo", " ")));
assert_eq!(parse_command("!foo @bar"), Some(("foo", "@bar")));
assert_eq!(parse_command("!foo @bar"), Some(("foo", " @bar")));
assert_eq!(parse_command("!foo @bar "), Some(("foo", "@bar ")));
assert_eq!(parse_command("! foo @bar"), None);
assert_eq!(parse_command("!"), None);
assert_eq!(parse_command("?foo"), None);
}
#[test]
fn test_parse_specific() {
assert_eq!(parse_specific("@foo"), Some(("foo", "")));
assert_eq!(parse_specific(" @foo"), Some(("foo", "")));
assert_eq!(parse_specific("@foo "), Some(("foo", " ")));
assert_eq!(parse_specific(" @foo "), Some(("foo", " ")));
assert_eq!(parse_specific("@foo !bar"), Some(("foo", "!bar")));
assert_eq!(parse_specific("@foo !bar"), Some(("foo", " !bar")));
assert_eq!(parse_specific("@foo !bar "), Some(("foo", "!bar ")));
assert_eq!(parse_specific("@ foo !bar"), None);
assert_eq!(parse_specific("@"), None);
assert_eq!(parse_specific("?foo"), None);
}
}