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
This commit is contained in:
parent
bd054f7721
commit
d795d1a59c
7 changed files with 569 additions and 1 deletions
110
docs/api/README.md
Normal file
110
docs/api/README.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# 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, no dynamic loading, and no `unsafe`:
|
||||
|
||||
| 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. **Original Rust only.** No `unsafe`, no C/FFI, no new dependencies, and no code
|
||||
copied or translated from another project. `scripts/native-rust-guard.sh`
|
||||
enforces this on every edit.
|
||||
|
||||
## 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, test, and check:**
|
||||
|
||||
```sh
|
||||
cargo build && cargo test
|
||||
bash scripts/native-rust-guard.sh src/modules/hello.rs
|
||||
```
|
||||
|
||||
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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue