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
|
|
@ -1,10 +1,10 @@
|
|||
//! Bot behavior: the [`Bot`] state, registration, and the event loop.
|
||||
//!
|
||||
//! This layer is transport-agnostic in spirit: it drives an [`crate::irc::Connection`]
|
||||
//! and turns incoming [`Message`]s into actions. The behaviour is split across
|
||||
//! sibling modules — capability negotiation and SASL live in [`caps`], and
|
||||
//! message routing plus the command table live in [`commands`]. To add a
|
||||
//! feature you usually only touch [`commands`].
|
||||
//! Behaviour is split across sibling modules: capability negotiation and SASL
|
||||
//! live in [`caps`], and message routing in [`commands`]. Features live behind
|
||||
//! the [`Module`] plugin trait ([`module`]) and are registered in [`modules`],
|
||||
//! so adding a command is a new file in `bot/modules/` plus one line in
|
||||
//! [`modules::all`] — no changes to the core dispatch.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io;
|
||||
|
|
@ -14,8 +14,13 @@ use crate::irc::{Connection, Message};
|
|||
|
||||
mod caps;
|
||||
mod commands;
|
||||
pub mod module;
|
||||
pub mod modules;
|
||||
|
||||
/// The bot: owns its configuration and connection, and runs one session.
|
||||
pub use module::{Action, Command, CommandSpec, Module};
|
||||
|
||||
/// The bot: owns its configuration, connection, and feature modules, and runs
|
||||
/// one session.
|
||||
pub struct Bot {
|
||||
config: Config,
|
||||
conn: Connection,
|
||||
|
|
@ -31,11 +36,31 @@ pub struct Bot {
|
|||
batches: HashMap<String, String>,
|
||||
/// Whether the SASL exchange has begun this session.
|
||||
sasl_started: bool,
|
||||
/// Registered feature modules (a fresh set per network/session).
|
||||
modules: Vec<Box<dyn Module>>,
|
||||
/// Command word -> index into `modules`, built from each module's specs.
|
||||
routes: HashMap<&'static str, usize>,
|
||||
}
|
||||
|
||||
impl Bot {
|
||||
pub fn new(config: Config, conn: Connection) -> Bot {
|
||||
let current_nick = config.nick.clone();
|
||||
|
||||
// Build the module set and the command -> module route table.
|
||||
let mods = modules::all();
|
||||
let mut routes: HashMap<&'static str, usize> = HashMap::new();
|
||||
for (i, m) in mods.iter().enumerate() {
|
||||
for spec in m.commands() {
|
||||
if routes.insert(spec.name, i).is_some() {
|
||||
crate::log(&format!(
|
||||
"module '{}' overrides command '{}'",
|
||||
m.name(),
|
||||
spec.name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bot {
|
||||
config,
|
||||
conn,
|
||||
|
|
@ -45,6 +70,8 @@ impl Bot {
|
|||
enabled_caps: HashSet::new(),
|
||||
batches: HashMap::new(),
|
||||
sasl_started: false,
|
||||
modules: mods,
|
||||
routes,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue