4.2 KiB
Module developer API
This is the reference for extending echoIRCd. The server has five extension points, all ordinary Rust trait objects compiled into the binary — there is no plugin ABI and no dynamic loading:
| You want to… | Implement | Registered in | Reference |
|---|---|---|---|
| Add a command | Command |
modules/mod.rs::module_commands() |
commands |
| Add a channel/user mode | ChanMode / UserMode |
mode.rs::CHAN_MODES / USER_MODES |
modes |
| Hook lifecycle events | Module |
modules/mod.rs::default_modules() |
modules |
| Store per-user / per-server state | Extensible typemap |
— | server |
| Call into the server | the Server API |
— | server |
The mental model — read this first
Everything runs on one thread. A single core thread owns every User and
Channel. Your command handlers, mode handlers, and module hooks are all called
on that thread with &mut Server. That means:
- Your code is plain synchronous Rust. No
async, no.await, noSend + 'staticfutures, noArc<Mutex<…>>. You read and mutateServerdirectly. - You must never block. A slow operation (a KDF hash, a network call, a big disk write) would freeze the whole server. Offload it — see off-core work — and handle the result as an event.
- State is safe by construction. Users and channels are referenced by
Uid/ channel-key handles, not pointers, so there are no dangling references.
Project conventions
Modules in this codebase follow a few hard rules — match them:
- One module per file in
src/modules/. A module is self-contained; it does not add fields toServerorConfig. - State goes in the
Extensibletypemap, not in new struct fields — attach per-user data toUser.ext, per-server (and per-channel, keyed by name) data toServer.ext. It is dropped automatically with its owner. See per-entity state. - Read settings through the config accessors (
conf,conf_all,conf_num,conf_bool) — never hardcode a tunable value; expose it as a config key with a literal default. - Register in the table, don't touch the parser or the dispatcher. Adding a command / mode / module is one new file plus one line in a registration table.
- Stay self-contained. A module shouldn't pull in a heavy new dependency — the primitives you'll reach for (an HTTP client, a regex engine, base64, the hashing/KDF helpers) already live in the tree; reuse them.
Write your first module in five steps
A module that logs connects and quits — the canonical template
(src/modules/snoop.rs):
1. Create the file src/modules/hello.rs:
//! hello — a tiny example module.
use crate::module::Module;
use crate::server::Server;
use crate::Uid;
pub struct Hello;
impl Module for Hello {
fn name(&self) -> &'static str {
"hello"
}
fn on_user_connect(&mut self, srv: &mut Server, uid: Uid) {
if let Some(u) = srv.users.get(&uid) {
srv.snotice(&format!("hello: {} connected", u.nick));
}
}
}
2. Declare the file in src/modules/mod.rs:
pub mod hello;
3. Register the module in the same file's default_modules():
pub fn default_modules() -> Vec<Box<dyn Module>> {
vec![
// …existing modules…
Box::new(hello::Hello),
]
}
4. (Only if it adds a command) expose a commands() function from your module
and chain it into module_commands() — see commands.
5. Build and test:
cargo build && cargo test
That's it — hello is now a first-class part of the server.
Where to go next
- The
Moduletrait — every lifecycle hook and how to block actions. - Commands — add a command and reply to clients.
- Modes — add a channel or user mode.
- The
ServerAPI — sending, lookups, permissions, config, state, and off-core work.