Add a module (plugin) system for bot features
Introduce a `Module` trait (src/bot/module.rs): a feature declares the commands it provides and turns each invocation into a `Vec<Action>`, without touching the connection. Modules are pure — context in, actions out — so they are unit-testable in isolation and avoid borrowing the `Bot` that owns them. Bot::new builds the module set and a command -> module route table; the `commands` dispatcher routes to the owning module and executes its actions, and `help` is auto-generated from every module's CommandSpec metadata. The existing ping/echo/hello move into a `builtins` module, and a stateful `dice` module (`roll [NdM]`, with its own dependency-free xorshift PRNG) is added as a worked example. Each network gets its own module set, so stateful modules keep independent per-network state with no locking. Adding a feature is now a new file in bot/modules/ plus one line in modules::all(). Tests: new tests/modules.rs drives modules through the trait with no network; end-to-end dispatch and auto-help verified against a fake server. No new deps.
This commit is contained in:
parent
a15fb4f9a9
commit
a5e58493d8
8 changed files with 426 additions and 21 deletions
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use std::io;
|
||||
|
||||
use super::module::{Action, Command};
|
||||
use super::Bot;
|
||||
use crate::irc::{now_unix, parse_server_time, Message};
|
||||
|
||||
|
|
@ -83,7 +84,8 @@ impl Bot {
|
|||
self.handle_command(&reply_to, sender, &command, &args)
|
||||
}
|
||||
|
||||
/// The command table. Add new bot commands here.
|
||||
/// Route a command to the module that declares it and execute the actions
|
||||
/// it returns. `help` is handled here, aggregating every module's commands.
|
||||
fn handle_command(
|
||||
&mut self,
|
||||
reply_to: &str,
|
||||
|
|
@ -91,22 +93,54 @@ impl Bot {
|
|||
command: &str,
|
||||
args: &[&str],
|
||||
) -> io::Result<()> {
|
||||
match command {
|
||||
"ping" => self.conn.privmsg(reply_to, "pong")?,
|
||||
"echo" => self.conn.privmsg(reply_to, &args.join(" "))?,
|
||||
"hello" => {
|
||||
let text = format!("hello, {sender}!");
|
||||
self.conn.privmsg(reply_to, &text)?;
|
||||
if command == "help" {
|
||||
let text = self.help_text(args.first().copied());
|
||||
return self.conn.privmsg(reply_to, &text);
|
||||
}
|
||||
|
||||
let Some(&idx) = self.routes.get(command) else {
|
||||
return Ok(()); // Unknown command: stay silent rather than spam channels.
|
||||
};
|
||||
|
||||
let cmd = Command {
|
||||
sender,
|
||||
reply_to,
|
||||
name: command,
|
||||
args,
|
||||
prefix: &self.config.command_prefix,
|
||||
network: &self.config.name,
|
||||
};
|
||||
let actions = self.modules[idx].on_command(&cmd);
|
||||
for action in actions {
|
||||
match action {
|
||||
Action::Reply(text) => self.conn.privmsg(reply_to, &text)?,
|
||||
Action::Raw(line) => self.conn.send_raw(&line)?,
|
||||
}
|
||||
"help" => {
|
||||
let p = &self.config.command_prefix;
|
||||
let text = format!("commands: {p}ping, {p}echo <text>, {p}hello, {p}help");
|
||||
self.conn.privmsg(reply_to, &text)?;
|
||||
}
|
||||
_ => {} // Unknown command: stay silent rather than spam channels.
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build the `help` reply: with no argument, list every module's commands;
|
||||
/// with a command name, show that command's description.
|
||||
fn help_text(&self, arg: Option<&str>) -> String {
|
||||
let p = &self.config.command_prefix;
|
||||
if let Some(name) = arg {
|
||||
for m in &self.modules {
|
||||
if let Some(spec) = m.commands().iter().find(|s| s.name == name) {
|
||||
return format!("{p}{}: {}", spec.usage, spec.about);
|
||||
}
|
||||
}
|
||||
return format!("no such command '{name}'; try {p}help");
|
||||
}
|
||||
let mut items: Vec<String> = Vec::new();
|
||||
for m in &self.modules {
|
||||
for spec in m.commands() {
|
||||
items.push(format!("{p}{}", spec.usage));
|
||||
}
|
||||
}
|
||||
items.push(format!("{p}help"));
|
||||
format!("commands: {}", items.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
/// Channels start with one of the standard channel-type prefixes.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue