echoIRCd/docs/api/README.md

109 lines
4.2 KiB
Markdown

# 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`](commands.md) | `modules/mod.rs::module_commands()` | [commands](commands.md) |
| Add a channel/user mode | [`ChanMode`](modes.md) / [`UserMode`](modes.md) | `mode.rs::CHAN_MODES` / `USER_MODES` | [modes](modes.md) |
| Hook lifecycle events | [`Module`](modules.md) | `modules/mod.rs::default_modules()` | [modules](modules.md) |
| Store per-user / per-server state | [`Extensible`](server.md#per-entity-state) typemap | — | [server](server.md) |
| Call into the server | the [`Server`](server.md) API | — | [server](server.md) |
## 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](server.md#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](server.md#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`:
```rust
//! 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`:
```rust
pub mod hello;
```
**3. Register the module** in the same file's `default_modules()`:
```rust
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](commands.md).
**5. Build and test:**
```sh
cargo build && cargo test
```
That's it — `hello` is now a first-class part of the server.
## Where to go next
- [The `Module` trait](modules.md) — every lifecycle hook and how to block actions.
- [Commands](commands.md) — add a command and reply to clients.
- [Modes](modes.md) — add a channel or user mode.
- [The `Server` API](server.md) — sending, lookups, permissions, config, state,
and off-core work.