Make Command system more flexible
This commit is contained in:
parent
2719ab3301
commit
ff886efd78
6 changed files with 289 additions and 261 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue