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
111
docs/api/modules.md
Normal file
111
docs/api/modules.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# 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.
|
||||
|
||||
```rust
|
||||
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](server.md#per-entity-state) 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`:
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
```rust
|
||||
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`](server.md#off-core-work) rather than blocking.
|
||||
|
||||
```rust
|
||||
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`:
|
||||
|
||||
```rust
|
||||
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](commands.md) if your module also adds commands, and
|
||||
[the `Server` API](server.md) for everything you can call from a hook.
|
||||
Loading…
Add table
Add a link
Reference in a new issue