Return whether command handled message

This commit is contained in:
Joscha 2023-02-27 12:41:49 +01:00
parent a6331d50b8
commit 4479126500
12 changed files with 142 additions and 47 deletions

View file

@ -15,6 +15,13 @@ Procedure when bumping the version number:
### Added ### Added
- `bot::botrulez::Uptime` now implements `bot::command::Command` - `bot::botrulez::Uptime` now implements `bot::command::Command`
- `bot::commands::Commands::fallthrough`
- `bot::commands::Commands::set_fallthrough`
### Changed
- `bot::command::ClapCommand::execute` now returns a `Result<bool, E>` instead of a `Result<(), E>`
- `bot::command::Command::execute` now returns a `Result<bool, E>` instead of a `Result<(), E>`
- `bot::commands::Commands::handle_packet` now returns a `Result<bool, E>` instead of a `Result<(), E>`
## v0.3.1 - 2023-02-26 ## v0.3.1 - 2023-02-26

View file

@ -34,10 +34,10 @@ impl ClapCommand<Bot, conn::Error> for Kill {
msg: &Message, msg: &Message,
ctx: &Context, ctx: &Context,
bot: &mut Bot, bot: &mut Bot,
) -> Result<(), conn::Error> { ) -> Result<bool, conn::Error> {
bot.stop = true; bot.stop = true;
ctx.reply(msg.id, "/me dies").await?; ctx.reply(msg.id, "/me dies").await?;
Ok(()) Ok(true)
} }
} }
@ -61,14 +61,14 @@ impl ClapCommand<Bot, conn::Error> for Test {
msg: &Message, msg: &Message,
ctx: &Context, ctx: &Context,
_bot: &mut Bot, _bot: &mut Bot,
) -> Result<(), conn::Error> { ) -> Result<bool, conn::Error> {
let content = if args.amount == 1 { let content = if args.amount == 1 {
format!("/me did {} test", args.amount) format!("/me did {} test", args.amount)
} else { } else {
format!("/me did {} tests", args.amount) format!("/me did {} tests", args.amount)
}; };
ctx.reply(msg.id, content).await?; ctx.reply(msg.id, content).await?;
Ok(()) Ok(true)
} }
} }

View file

@ -50,12 +50,20 @@ where
B: HasDescriptions + Send, B: HasDescriptions + Send,
E: From<conn::Error>, E: From<conn::Error>,
{ {
async fn execute(&self, arg: &str, msg: &Message, ctx: &Context, bot: &mut B) -> Result<(), E> { async fn execute(
&self,
arg: &str,
msg: &Message,
ctx: &Context,
bot: &mut B,
) -> Result<bool, E> {
if arg.trim().is_empty() { if arg.trim().is_empty() {
let reply = self.formulate_reply(ctx, bot); let reply = self.formulate_reply(ctx, bot);
ctx.reply(msg.id, reply).await?; ctx.reply(msg.id, reply).await?;
Ok(true)
} else {
Ok(false)
} }
Ok(())
} }
} }
@ -77,9 +85,9 @@ where
msg: &Message, msg: &Message,
ctx: &Context, ctx: &Context,
bot: &mut B, bot: &mut B,
) -> Result<(), E> { ) -> Result<bool, E> {
let reply = self.formulate_reply(ctx, bot); let reply = self.formulate_reply(ctx, bot);
ctx.reply(msg.id, reply).await?; ctx.reply(msg.id, reply).await?;
Ok(()) Ok(true)
} }
} }

View file

@ -30,11 +30,13 @@ where
msg: &Message, msg: &Message,
ctx: &Context, ctx: &Context,
_bot: &mut B, _bot: &mut B,
) -> Result<(), E> { ) -> Result<bool, E> {
if arg.trim().is_empty() { if arg.trim().is_empty() {
ctx.reply(msg.id, &self.0).await?; ctx.reply(msg.id, &self.0).await?;
Ok(true)
} else {
Ok(false)
} }
Ok(())
} }
} }
@ -55,8 +57,8 @@ where
msg: &Message, msg: &Message,
ctx: &Context, ctx: &Context,
_bot: &mut B, _bot: &mut B,
) -> Result<(), E> { ) -> Result<bool, E> {
ctx.reply(msg.id, &self.0).await?; ctx.reply(msg.id, &self.0).await?;
Ok(()) Ok(true)
} }
} }

View file

@ -24,11 +24,13 @@ where
msg: &Message, msg: &Message,
ctx: &Context, ctx: &Context,
_bot: &mut B, _bot: &mut B,
) -> Result<(), E> { ) -> Result<bool, E> {
if arg.trim().is_empty() { if arg.trim().is_empty() {
ctx.reply(msg.id, &self.0).await?; ctx.reply(msg.id, &self.0).await?;
Ok(true)
} else {
Ok(false)
} }
Ok(())
} }
} }
@ -49,8 +51,8 @@ where
msg: &Message, msg: &Message,
ctx: &Context, ctx: &Context,
_bot: &mut B, _bot: &mut B,
) -> Result<(), E> { ) -> Result<bool, E> {
ctx.reply(msg.id, &self.0).await?; ctx.reply(msg.id, &self.0).await?;
Ok(()) Ok(true)
} }
} }

View file

@ -81,12 +81,20 @@ where
B: HasStartTime + Send, B: HasStartTime + Send,
E: From<conn::Error>, E: From<conn::Error>,
{ {
async fn execute(&self, arg: &str, msg: &Message, ctx: &Context, bot: &mut B) -> Result<(), E> { async fn execute(
&self,
arg: &str,
msg: &Message,
ctx: &Context,
bot: &mut B,
) -> Result<bool, E> {
if arg.trim().is_empty() { if arg.trim().is_empty() {
let reply = self.formulate_reply(ctx, bot, false); let reply = self.formulate_reply(ctx, bot, false);
ctx.reply(msg.id, reply).await?; ctx.reply(msg.id, reply).await?;
Ok(true)
} else {
Ok(false)
} }
Ok(())
} }
} }
@ -112,9 +120,9 @@ where
msg: &Message, msg: &Message,
ctx: &Context, ctx: &Context,
bot: &mut B, bot: &mut B,
) -> Result<(), E> { ) -> Result<bool, E> {
let reply = self.formulate_reply(ctx, bot, args.connected); let reply = self.formulate_reply(ctx, bot, args.connected);
ctx.reply(msg.id, reply).await?; ctx.reply(msg.id, reply).await?;
Ok(()) Ok(true)
} }
} }

View file

@ -54,5 +54,11 @@ pub trait Command<B, E> {
None None
} }
async fn execute(&self, arg: &str, msg: &Message, ctx: &Context, bot: &mut B) -> Result<(), E>; async fn execute(
&self,
arg: &str,
msg: &Message,
ctx: &Context,
bot: &mut B,
) -> Result<bool, E>;
} }

View file

@ -65,15 +65,21 @@ where
Some(format!("{}{} - {inner}", self.prefix, self.name)) Some(format!("{}{} - {inner}", self.prefix, self.name))
} }
async fn execute(&self, arg: &str, msg: &Message, ctx: &Context, bot: &mut B) -> Result<(), E> { async fn execute(
&self,
arg: &str,
msg: &Message,
ctx: &Context,
bot: &mut B,
) -> Result<bool, E> {
// TODO Replace with let-else // TODO Replace with let-else
let (name, rest) = match parse_command(arg, &self.prefix) { let (name, rest) = match parse_command(arg, &self.prefix) {
Some(parsed) => parsed, Some(parsed) => parsed,
None => return Ok(()), None => return Ok(false),
}; };
if name != self.name { if name != self.name {
return Ok(()); return Ok(false);
} }
self.inner.execute(rest, msg, ctx, bot).await self.inner.execute(rest, msg, ctx, bot).await
@ -112,22 +118,28 @@ where
Some(format!("{}{} - {inner}", self.prefix, self.name)) Some(format!("{}{} - {inner}", self.prefix, self.name))
} }
async fn execute(&self, arg: &str, msg: &Message, ctx: &Context, bot: &mut B) -> Result<(), E> { async fn execute(
&self,
arg: &str,
msg: &Message,
ctx: &Context,
bot: &mut B,
) -> Result<bool, E> {
// TODO Replace with let-else // TODO Replace with let-else
let (name, rest) = match parse_command(arg, &self.prefix) { let (name, rest) = match parse_command(arg, &self.prefix) {
Some(parsed) => parsed, Some(parsed) => parsed,
None => return Ok(()), None => return Ok(false),
}; };
if name != self.name { if name != self.name {
return Ok(()); return Ok(false);
} }
if parse_specific(rest).is_some() { if parse_specific(rest).is_some() {
// The command looks like a specific command. If we treated it like // The command looks like a specific command. If we treated it like
// a general command match, we would interpret other bots' specific // a general command match, we would interpret other bots' specific
// commands as general commands. // commands as general commands.
return Ok(()); return Ok(false);
} }
self.inner.execute(rest, msg, ctx, bot).await self.inner.execute(rest, msg, ctx, bot).await
@ -167,25 +179,31 @@ where
Some(format!("{}{} @{nick} - {inner}", self.prefix, self.name)) Some(format!("{}{} @{nick} - {inner}", self.prefix, self.name))
} }
async fn execute(&self, arg: &str, msg: &Message, ctx: &Context, bot: &mut B) -> Result<(), E> { async fn execute(
&self,
arg: &str,
msg: &Message,
ctx: &Context,
bot: &mut B,
) -> Result<bool, E> {
// TODO Replace with let-else // TODO Replace with let-else
let (name, rest) = match parse_command(arg, &self.prefix) { let (name, rest) = match parse_command(arg, &self.prefix) {
Some(parsed) => parsed, Some(parsed) => parsed,
None => return Ok(()), None => return Ok(false),
}; };
if name != self.name { if name != self.name {
return Ok(()); return Ok(false);
} }
// TODO Replace with let-else // TODO Replace with let-else
let (nick, rest) = match parse_specific(rest) { let (nick, rest) = match parse_specific(rest) {
Some(parsed) => parsed, Some(parsed) => parsed,
None => return Ok(()), None => return Ok(false),
}; };
if nick::normalize(nick) != nick::normalize(&ctx.joined.session.name) { if nick::normalize(nick) != nick::normalize(&ctx.joined.session.name) {
return Ok(()); return Ok(false);
} }
self.inner.execute(rest, msg, ctx, bot).await self.inner.execute(rest, msg, ctx, bot).await

View file

@ -16,7 +16,7 @@ pub trait ClapCommand<B, E> {
msg: &Message, msg: &Message,
ctx: &Context, ctx: &Context,
bot: &mut B, bot: &mut B,
) -> Result<(), E>; ) -> Result<bool, E>;
} }
/// Parse bash-like quoted arguments separated by whitespace. /// Parse bash-like quoted arguments separated by whitespace.
@ -110,12 +110,18 @@ where
C::Args::command().get_about().map(|s| format!("{s}")) C::Args::command().get_about().map(|s| format!("{s}"))
} }
async fn execute(&self, arg: &str, msg: &Message, ctx: &Context, bot: &mut B) -> Result<(), E> { async fn execute(
&self,
arg: &str,
msg: &Message,
ctx: &Context,
bot: &mut B,
) -> Result<bool, E> {
let mut args = match parse_quoted_args(arg) { let mut args = match parse_quoted_args(arg) {
Ok(args) => args, Ok(args) => args,
Err(err) => { Err(err) => {
ctx.reply(msg.id, err).await?; ctx.reply(msg.id, err).await?;
return Ok(()); return Ok(true);
} }
}; };
@ -127,7 +133,7 @@ where
Ok(args) => args, Ok(args) => args,
Err(err) => { Err(err) => {
ctx.reply(msg.id, format!("{}", err.render())).await?; ctx.reply(msg.id, format!("{}", err.render())).await?;
return Ok(()); return Ok(true);
} }
}; };

View file

@ -17,7 +17,13 @@ where
None None
} }
async fn execute(&self, arg: &str, msg: &Message, ctx: &Context, bot: &mut B) -> Result<(), E> { async fn execute(
&self,
arg: &str,
msg: &Message,
ctx: &Context,
bot: &mut B,
) -> Result<bool, E> {
self.0.execute(arg, msg, ctx, bot).await self.0.execute(arg, msg, ctx, bot).await
} }
} }

View file

@ -29,11 +29,17 @@ where
Some(format!("{} - {inner}", self.prefix)) Some(format!("{} - {inner}", self.prefix))
} }
async fn execute(&self, arg: &str, msg: &Message, ctx: &Context, bot: &mut B) -> Result<(), E> { async fn execute(
&self,
arg: &str,
msg: &Message,
ctx: &Context,
bot: &mut B,
) -> Result<bool, E> {
if let Some(rest) = arg.trim_start().strip_prefix(&self.prefix) { if let Some(rest) = arg.trim_start().strip_prefix(&self.prefix) {
self.inner.execute(rest, msg, ctx, bot).await self.inner.execute(rest, msg, ctx, bot).await
} else { } else {
Ok(()) Ok(false)
} }
} }
} }

View file

@ -7,11 +7,32 @@ use super::instance::{InstanceConfig, Snapshot};
pub struct Commands<B, E> { pub struct Commands<B, E> {
commands: Vec<Box<dyn Command<B, E> + Send + Sync>>, commands: Vec<Box<dyn Command<B, E> + Send + Sync>>,
fallthrough: bool,
} }
impl<B, E> Commands<B, E> { impl<B, E> Commands<B, E> {
pub fn new() -> Self { pub fn new() -> Self {
Self { commands: vec![] } Self {
commands: vec![],
fallthrough: false,
}
}
/// Whether further commands should be executed after a command returns
/// `true`.
///
/// If disabled, commands are run until the first command that returns
/// `true`. If enabled, all commands are run irrespective of their return
/// values.
pub fn fallthrough(&self) -> bool {
self.fallthrough
}
/// Set whether fallthrough is active.
///
/// See [`Self::fallthrough`] for more details.
pub fn set_fallthrough(&mut self, active: bool) {
self.fallthrough = active;
} }
pub fn add<C>(&mut self, command: C) pub fn add<C>(&mut self, command: C)
@ -28,21 +49,22 @@ impl<B, E> Commands<B, E> {
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
/// Returns `true` if a command was found and executed, `false` otherwise. /// Returns `true` if one or more commands returned `true`, `false`
/// otherwise.
pub async fn handle_packet( pub async fn handle_packet(
&self, &self,
config: &InstanceConfig, config: &InstanceConfig,
packet: &ParsedPacket, packet: &ParsedPacket,
snapshot: &Snapshot, snapshot: &Snapshot,
bot: &mut B, bot: &mut B,
) -> Result<(), E> { ) -> Result<bool, E> {
let msg = match &packet.content { let msg = match &packet.content {
Ok(Data::SendEvent(SendEvent(msg))) => msg, Ok(Data::SendEvent(SendEvent(msg))) => msg,
_ => return Ok(()), _ => return Ok(false),
}; };
let joined = match &snapshot.state { let joined = match &snapshot.state {
conn::State::Joining(_) => return Ok(()), conn::State::Joining(_) => return Ok(false),
conn::State::Joined(joined) => joined.clone(), conn::State::Joined(joined) => joined.clone(),
}; };
@ -52,11 +74,15 @@ impl<B, E> Commands<B, E> {
joined, joined,
}; };
let mut handled = false;
for command in &self.commands { for command in &self.commands {
command.execute(&msg.content, msg, &ctx, bot).await?; handled = handled || command.execute(&msg.content, msg, &ctx, bot).await?;
if !self.fallthrough && handled {
break;
}
} }
Ok(()) Ok(handled)
} }
} }