Reorganise the engine into a concern-split module tree
engine/mod.rs had grown past 5k lines, most of it one test module and a few large method clusters. Lift the tests into engine/tests.rs, and move the cohesive method groups into sibling files — sasl, kicker (chat policing), register (account-authority relay), and dispatch (service routing and fantasy). The Engine type, its master event handler, shared helpers, and the small state structs stay in mod.rs; each sibling adds its own impl Engine block. Pure reorganisation: no behaviour change, tests unchanged and green.
This commit is contained in:
parent
0c7976e80d
commit
6f76f9722c
6 changed files with 4194 additions and 4177 deletions
125
src/engine/dispatch.rs
Normal file
125
src/engine/dispatch.rs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
use super::*;
|
||||
|
||||
impl Engine {
|
||||
// Route a PRIVMSG addressed to a service (by uid or nick) into that service,
|
||||
// handing it the sender's resolved nick, login state, and the account store.
|
||||
pub(crate) fn dispatch(&mut self, from: &str, to: &str, text: &str) -> Vec<NetAction> {
|
||||
let nick = self.network.nick_of(from).unwrap_or(from).to_string();
|
||||
let account = self.network.account_of(from).map(str::to_string);
|
||||
let privs = account.as_deref().map(|a| self.oper_privs(a)).unwrap_or_default();
|
||||
let mut ctx = ServiceCtx::default();
|
||||
// Mark the log so we can tell exactly which events this command appends.
|
||||
let audit_mark = self.db.log_len();
|
||||
let sender = Sender { uid: from, nick: &nick, account: account.as_deref(), privs };
|
||||
if to.starts_with('#') || to.starts_with('&') {
|
||||
// A community !votekick/!voteban is handled on its own; otherwise the
|
||||
// line runs through fantasy (!op …), the kickers, and triggers.
|
||||
if let Some(vote_actions) = self.handle_vote(from, to, text) {
|
||||
ctx.actions.extend(vote_actions);
|
||||
} else {
|
||||
self.fantasy(&sender, to, text, &mut ctx);
|
||||
let kicks = self.kicker_check(from, to, text);
|
||||
let kicked = kicks.iter().any(|a| matches!(a, NetAction::Kick { .. }));
|
||||
ctx.actions.extend(kicks);
|
||||
// Don't reward a line the bot just kicked for with a response.
|
||||
if !kicked {
|
||||
if let Some(resp) = self.trigger_response(from, to, text) {
|
||||
ctx.actions.push(resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Record activity in bot channels (surfaced by StatServ).
|
||||
if self.db.channel(to).is_some_and(|c| c.assigned_bot.is_some()) {
|
||||
self.network.record_line(to, &nick);
|
||||
self.bump("botserv.messages");
|
||||
}
|
||||
} else {
|
||||
// A services ignore silences a user's commands entirely — but an oper
|
||||
// can never be ignored (else they could lock themselves out).
|
||||
if !privs.any() {
|
||||
let host = self.network.host_of(from).unwrap_or("").to_string();
|
||||
if self.db.is_ignored(&nick, &host) {
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
let mut matched: Option<String> = None;
|
||||
{
|
||||
let Self { services, network, db, .. } = self;
|
||||
for svc in services.iter_mut() {
|
||||
if to.eq_ignore_ascii_case(svc.uid()) || to.eq_ignore_ascii_case(svc.nick()) {
|
||||
let args: Vec<&str> = text.split_whitespace().collect();
|
||||
svc.on_command(&sender, &args, &mut ctx, network, db);
|
||||
matched = Some(svc.nick().to_ascii_lowercase());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(nick) = matched {
|
||||
self.bump(&format!("{nick}.command"));
|
||||
}
|
||||
}
|
||||
// Fold any counters the command recorded into the shared registry.
|
||||
for key in std::mem::take(&mut ctx.stats) {
|
||||
self.bump(&key);
|
||||
}
|
||||
// A command may have changed the bot registry (BotServ BOT ADD/DEL).
|
||||
let mut out = ctx.actions;
|
||||
out.extend(self.reconcile_bots());
|
||||
// Announce whatever this command changed to the staff audit channel.
|
||||
out.extend(self.audit_feed(audit_mark, &nick, account.as_deref()));
|
||||
out
|
||||
}
|
||||
|
||||
// fantasy word becomes the command and the channel is injected as its first
|
||||
// argument — then re-source the resulting actions from the bot, so the bot is
|
||||
// the visible actor.
|
||||
fn fantasy(&mut self, sender: &Sender, chan: &str, text: &str, ctx: &mut ServiceCtx) {
|
||||
let Some(rest) = text.strip_prefix(FANTASY_PREFIX) else { return };
|
||||
let mut words = rest.split_whitespace();
|
||||
let Some(cmd) = words.next() else { return };
|
||||
// Fantasy only works through an assigned, live bot.
|
||||
let Some(botnick) = self.db.channel(chan).and_then(|c| c.assigned_bot.clone()) else { return };
|
||||
let Some(botuid) = self.network.uid_by_nick(&botnick).map(str::to_string) else { return };
|
||||
|
||||
// A dice command (!roll/!calc/…) goes to DiceServ, if it's loaded, and its
|
||||
// reply is spoken into the channel by the bot; everything else is a channel
|
||||
// command handled by ChanServ.
|
||||
if matches!(cmd.to_ascii_uppercase().as_str(), "ROLL" | "CALC" | "EXROLL" | "EXCALC") {
|
||||
let Some(dsuid) = self.service_uid("DiceServ") else { return };
|
||||
let mark = ctx.actions.len();
|
||||
let args: Vec<&str> = std::iter::once(cmd).chain(words).collect();
|
||||
self.route_to(&dsuid, sender, &args, ctx);
|
||||
// Turn DiceServ's private notices into the bot speaking to the channel.
|
||||
for a in ctx.actions[mark..].iter_mut() {
|
||||
if let NetAction::Notice { text, .. } = a {
|
||||
*a = NetAction::Privmsg { from: botuid.clone(), to: chan.to_string(), text: std::mem::take(text) };
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(csuid) = self.service_uid("ChanServ") else { return };
|
||||
// Rewrite `!cmd args…` into `CMD #channel args…` for ChanServ.
|
||||
let mark = ctx.actions.len();
|
||||
let args: Vec<&str> = [cmd, chan].into_iter().chain(words).collect();
|
||||
self.route_to(&csuid, sender, &args, ctx);
|
||||
// Make the bot the source of everything ChanServ just emitted.
|
||||
for a in ctx.actions[mark..].iter_mut() {
|
||||
resource_action(a, &csuid, &botuid);
|
||||
}
|
||||
}
|
||||
|
||||
// The uid of a loaded service by nick, if present.
|
||||
fn service_uid(&self, nick: &str) -> Option<String> {
|
||||
self.services.iter().find(|s| s.nick().eq_ignore_ascii_case(nick)).map(|s| s.uid().to_string())
|
||||
}
|
||||
|
||||
// Dispatch a command to a specific service (by uid), borrowing the disjoint
|
||||
// engine fields it needs.
|
||||
fn route_to(&mut self, uid: &str, sender: &Sender, args: &[&str], ctx: &mut ServiceCtx) {
|
||||
let Self { services, network, db, .. } = self;
|
||||
if let Some(svc) = services.iter_mut().find(|s| s.uid() == uid) {
|
||||
svc.on_command(sender, args, ctx, network, db);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue