echoIRCd/docs/api
2026-08-19 14:37:22 +00:00
..
commands.md docs: add a module-developer API reference (docs/api/) — Command/Module/ChanMode/UserMode traits, the Server API surface, per-entity Extensible state, and a first-module tutorial 2026-08-13 02:55:56 +00:00
modes.md docs: add a module-developer API reference (docs/api/) — Command/Module/ChanMode/UserMode traits, the Server API surface, per-entity Extensible state, and a first-module tutorial 2026-08-13 02:55:56 +00:00
modules.md docs: add a module-developer API reference (docs/api/) — Command/Module/ChanMode/UserMode traits, the Server API surface, per-entity Extensible state, and a first-module tutorial 2026-08-13 02:55:56 +00:00
README.md docs: rework README + manual — refresh the feature set (services interface, rustls TLS backend, WebSocket, PROXY v1/v2, metrics + JSON-RPC endpoints), correct command/module/cap counts, and document the tls_backend/metrics_bind/rpc/sts config keys in configuration.md and the example 2026-08-19 14:37:22 +00:00
server.md docs: add a module-developer API reference (docs/api/) — Command/Module/ChanMode/UserMode traits, the Server API surface, per-entity Extensible state, and a first-module tutorial 2026-08-13 02:55:56 +00:00

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, no Send + 'static futures, no Arc<Mutex<…>>. You read and mutate Server directly.
  • 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:

  1. One module per file in src/modules/. A module is self-contained; it does not add fields to Server or Config.
  2. State goes in the Extensible typemap, not in new struct fields — attach per-user data to User.ext, per-server (and per-channel, keyed by name) data to Server.ext. It is dropped automatically with its owner. See per-entity state.
  3. 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.
  4. 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.
  5. 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 Module trait — every lifecycle hook and how to block actions.
  • Commands — add a command and reply to clients.
  • Modes — add a channel or user mode.
  • The Server API — sending, lookups, permissions, config, state, and off-core work.