echoIRCd/docs/api/modules.md

4.3 KiB

The Module trait

A module hooks lifecycle events. Implement crate::module::Module on a struct and register it in default_modules(). Every method has a default, so implement only the hooks you need.

pub trait Module: Send {
    fn name(&self) -> &'static str;

    // pre-hooks — fired inline, can Deny the action
    fn on_user_register(&mut self, srv: &mut Server, uid: Uid) -> ModResult { ModResult::Passthru }
    fn on_pre_command(&mut self, srv: &mut Server, uid: Uid, cmd: &str, params: &[String]) -> ModResult { ModResult::Passthru }
    fn on_pre_message(&mut self, srv: &mut Server, uid: Uid, target: &str, text: &str) -> ModResult { ModResult::Passthru }

    // notify-hooks — informational, fired after the fact
    fn on_user_connect(&mut self, srv: &mut Server, uid: Uid) {}
    fn on_post_command(&mut self, srv: &mut Server, uid: Uid, cmd: &str) {}
    fn on_join(&mut self, srv: &mut Server, uid: Uid, chan: &str) {}
    fn on_part(&mut self, srv: &mut Server, uid: Uid, chan: &str, reason: &str) {}
    fn on_user_quit(&mut self, srv: &mut Server, uid: Uid, reason: &str) {}
    fn on_tick(&mut self, srv: &mut Server) {}
}

Note the receiver is &mut self: unlike commands and modes (which are stateless &self handlers), a module instance can hold its own fields. In practice, prefer the Extensible typemap for per-user / per-channel state so it lives and dies with its owner; use self fields only for module-global state.

Pre-hooks (can deny)

Pre-hooks run inline, before the action they gate, and return a ModResult:

pub enum ModResult {
    Passthru,  // no opinion — let other modules and the core decide
    Allow,     // force-allow: skip the remaining checks
    Deny,      // block the action
}
Hook Fires Deny effect
on_user_register last gate before a client finishes registration refuses the connection
on_pre_command before any command runs swallows the command silently
on_pre_message before a PRIVMSG / NOTICE is delivered drops the message

Return Deny to block, Allow to force it through (bypassing other checks), or Passthru to abstain. When you Deny, send the user an explanation yourself (e.g. srv.numeric(...) or srv.fail(...)), since the core just stops.

fn on_pre_message(&mut self, srv: &mut Server, uid: Uid, target: &str, text: &str) -> ModResult {
    if text.contains("badword") && !srv.is_oper(uid) {
        srv.numeric(uid, 404, &format!("{target} :Message blocked"));
        return ModResult::Deny;
    }
    ModResult::Passthru
}

Notify-hooks (informational)

Notify-hooks run after the event, drained from a queue once the triggering command finishes. They can't block, but they get &mut Server, so they can act — send lines, force a join, update state.

Hook Fires when
on_user_connect a client has fully registered
on_post_command after a command completes
on_join a user joined a channel
on_part a user left a channel
on_user_quit a user is disconnecting (still exists during the call)
on_tick the background timer, every TICK_SECS

Because notify-hooks fire from a queue, a hook can itself cause more events (e.g. force a join) without re-entering the module list — no surprises.

Timed work

Use on_tick for periodic jobs (expiring entries, saving state, scoring). It runs on the core thread, so keep it cheap; for a big write, hand it to disk_write rather than blocking.

fn on_tick(&mut self, srv: &mut Server) {
    let store = srv.ext.get_or_insert_with::<MyStore>(MyStore::default);
    store.expire(crate::server::now());
    // persist off-core so a slow disk can't stall the core:
    srv.disk_write(format!("{}.mystore", srv.conf_path), store.serialize());
}

Registering

Add your module to src/modules/mod.rs:

pub mod mymod;                       // declare the file
// …in default_modules():
Box::new(mymod::MyMod),              // add to the vec

Order in the vec is the order pre-hooks are consulted; the first Deny (or Allow) wins.

See commands if your module also adds commands, and the Server API for everything you can call from a hook.