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.
|
||||
102
docs/api/commands.md
Normal file
102
docs/api/commands.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# The `Command` trait
|
||||
|
||||
A command is a stateless handler registered by name. Implement
|
||||
`crate::command::Command`:
|
||||
|
||||
```rust
|
||||
pub trait Command: Send {
|
||||
fn name(&self) -> &'static str; // upper-case; also the registry key
|
||||
fn min_params(&self) -> usize { 0 } // fewer args ⇒ core replies 461, skips you
|
||||
fn before_reg(&self) -> bool { false } // may it run before registration?
|
||||
fn handle(&self, srv: &mut Server, uid: Uid, params: &[String]) -> CmdResult;
|
||||
}
|
||||
|
||||
pub enum CmdResult { Ok, Fail } // Fail is for your own bookkeeping/logging
|
||||
```
|
||||
|
||||
The core does the boilerplate for you before `handle` is called:
|
||||
|
||||
- **Arity** — if the client sent fewer than `min_params` arguments, the core
|
||||
replies `461 ERR_NEEDMOREPARAMS` and never calls you.
|
||||
- **Registration gate** — unless `before_reg()` returns `true`, the command is
|
||||
refused until the client has registered. Only handshake commands
|
||||
(`NICK`/`USER`/`CAP`/`PING`/`QUIT` and the like) set `before_reg`.
|
||||
- **Module pre-hooks** — `on_pre_command` runs first and may `Deny` you.
|
||||
|
||||
Inside `handle`, `params` is the already-split argument list (the trailing
|
||||
`:parameter` is a single element). Return `CmdResult::Ok` / `Fail`.
|
||||
|
||||
## A minimal command
|
||||
|
||||
```rust
|
||||
use crate::command::{CmdResult, Command};
|
||||
use crate::server::Server;
|
||||
use crate::Uid;
|
||||
|
||||
pub struct Ping2;
|
||||
|
||||
impl Command for Ping2 {
|
||||
fn name(&self) -> &'static str { "PING2" }
|
||||
fn min_params(&self) -> usize { 1 }
|
||||
fn handle(&self, srv: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||
let nick = srv.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default();
|
||||
srv.send(uid, format!(":{} PONG {} :{}", srv.name, nick, params[0]));
|
||||
CmdResult::Ok
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Replying to the client
|
||||
|
||||
Use the [`Server`](server.md#sending) helpers rather than building raw lines by
|
||||
hand where you can:
|
||||
|
||||
- `srv.numeric(uid, code, rest)` — send a numeric reply (`005`, `461`, …).
|
||||
- `srv.send(uid, line)` — send a fully-formed protocol line.
|
||||
- `srv.fail(uid, command, code, desc)` / `warn(...)` / `note(...)` — IRCv3
|
||||
standard replies (`FAIL` / `WARN` / `NOTE`) for clients that support them.
|
||||
- `srv.snotice(msg)` — a server notice to opers (for staff-facing feedback).
|
||||
|
||||
## Registering
|
||||
|
||||
A module exposes its commands from a `commands()` function returning boxed
|
||||
handlers:
|
||||
|
||||
```rust
|
||||
pub fn commands() -> Vec<Box<dyn Command>> {
|
||||
vec![Box::new(Ping2)]
|
||||
}
|
||||
```
|
||||
|
||||
Then chain it into `module_commands()` in `src/modules/mod.rs`:
|
||||
|
||||
```rust
|
||||
pub fn module_commands() -> Vec<Box<dyn Command>> {
|
||||
filter::commands()
|
||||
// …existing chains…
|
||||
.chain(mymod::commands())
|
||||
.collect()
|
||||
}
|
||||
```
|
||||
|
||||
Command names are the registry key and must be unique and upper-case. The
|
||||
`abbreviation` feature lets clients invoke a command by a unique prefix, so avoid
|
||||
names that are prefixes of unrelated commands where it matters.
|
||||
|
||||
## Oper-only and gated commands
|
||||
|
||||
There is no separate "oper command" type — gate inside `handle`:
|
||||
|
||||
```rust
|
||||
fn handle(&self, srv: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
||||
if !srv.is_oper(uid) {
|
||||
srv.numeric(uid, 481, ":Permission Denied- You're not an IRC operator");
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
// …privileged work…
|
||||
CmdResult::Ok
|
||||
}
|
||||
```
|
||||
|
||||
See [the `Server` API](server.md) for lookups, permissions, config, and off-core
|
||||
work.
|
||||
105
docs/api/modes.md
Normal file
105
docs/api/modes.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Writing modes
|
||||
|
||||
Modes are stateless `&'static` handler objects. The `MODE` parser dispatches each
|
||||
letter to its handler, so adding a mode is a new handler plus one line in a table —
|
||||
you never touch the parser. See the [mode reference](../modes.md) for the existing
|
||||
letters (don't collide).
|
||||
|
||||
## User modes — the `UserMode` trait
|
||||
|
||||
```rust
|
||||
pub trait UserMode: Sync {
|
||||
fn letter(&self) -> char;
|
||||
/// Apply +/- to the user; return true if it took effect (so the change is echoed).
|
||||
fn apply(&self, s: &mut Server, uid: Uid, adding: bool) -> bool;
|
||||
}
|
||||
```
|
||||
|
||||
A user-mode handler is typically a zero-sized struct. Store the actual flag on the
|
||||
user (existing flags live in `UserFlags`; module-specific state goes in
|
||||
[`User.ext`](server.md#per-entity-state)).
|
||||
|
||||
```rust
|
||||
struct BotMode;
|
||||
impl UserMode for BotMode {
|
||||
fn letter(&self) -> char { 'B' }
|
||||
fn apply(&self, s: &mut Server, uid: Uid, adding: bool) -> bool {
|
||||
match s.users.get_mut(&uid) {
|
||||
Some(u) => { u.flags.bot = adding; true }
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
static BOT: BotMode = BotMode;
|
||||
```
|
||||
|
||||
Register it in `src/mode.rs` by adding `&BOT` to the `USER_MODES` slice.
|
||||
|
||||
## Channel modes — the `ChanMode` trait
|
||||
|
||||
```rust
|
||||
pub trait ChanMode: Sync {
|
||||
fn letter(&self) -> char;
|
||||
/// Whether this sign consumes an argument (taken only if one remains).
|
||||
fn wants_param(&self, adding: bool) -> bool;
|
||||
/// A list mode (like +b): a no-arg query is just viewing, so it needn't
|
||||
/// require operator rank. Default false.
|
||||
fn is_list(&self) -> bool { false }
|
||||
/// Apply +/- to channel `key` (display name `chan`) on behalf of `uid`.
|
||||
fn apply(&self, s: &mut Server, chan: &str, key: &str, uid: Uid,
|
||||
adding: bool, param: Option<&str>) -> Applied;
|
||||
}
|
||||
|
||||
pub enum Applied {
|
||||
No, // nothing to echo (no-op, rejected, or a list query)
|
||||
Yes(Option<String>), // echo the change; Some(param) appends a parameter
|
||||
}
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- `key` is the channel's lookup key (lower-cased name); `chan` is the display name.
|
||||
Look the channel up with `s.channels.get_mut(key)`.
|
||||
- `wants_param` controls whether the parser hands you a `param`. Return `true` for
|
||||
a mode that takes an argument (a key, a limit, a mask); parameterless flags
|
||||
return `false`.
|
||||
- Return `Applied::Yes(None)` for a flag that flipped, `Applied::Yes(Some(p))` to
|
||||
echo a parameter (e.g. the limit you set), or `Applied::No` if nothing changed
|
||||
or you rejected it.
|
||||
- **Enforce permissions yourself.** For an ordinary settable mode, check the
|
||||
caller's rank (`s.rank(uid, key) >= RANK_OP`) before applying and reply/refuse
|
||||
if they're not allowed.
|
||||
|
||||
```rust
|
||||
struct NoCtcp;
|
||||
impl ChanMode for NoCtcp {
|
||||
fn letter(&self) -> char { 'C' }
|
||||
fn wants_param(&self, _adding: bool) -> bool { false }
|
||||
fn apply(&self, s: &mut Server, _chan: &str, key: &str, uid: Uid,
|
||||
adding: bool, _param: Option<&str>) -> Applied {
|
||||
if s.rank(uid, key) < crate::channels::RANK_OP { return Applied::No; }
|
||||
match s.channels.get_mut(key) {
|
||||
Some(c) if c.modes.no_ctcp != adding => { c.modes.no_ctcp = adding; Applied::Yes(None) }
|
||||
_ => Applied::No,
|
||||
}
|
||||
}
|
||||
}
|
||||
static NO_CTCP: NoCtcp = NoCtcp;
|
||||
```
|
||||
|
||||
Register it in `src/mode.rs` by adding `&NO_CTCP` to the `CHAN_MODES` slice, and
|
||||
add its letter to the `CHANMODES=` group in the ISUPPORT string (`server.rs`) so
|
||||
clients learn about it.
|
||||
|
||||
## List modes
|
||||
|
||||
Set `is_list() -> true` and `wants_param() -> true`. A no-argument use is a list
|
||||
query (rank-free); an argument adds/removes an entry. The built-in list modes
|
||||
(`+b`, `+e`, `+I`, …) share a common `ListMode` handler parameterised by kind —
|
||||
follow that pattern for a new list.
|
||||
|
||||
## Named-mode access
|
||||
|
||||
Every channel mode is also reachable by long name through the `PROP` command
|
||||
(`namedmodes`) without extra work on your part — the mapping is derived from the
|
||||
registered handlers.
|
||||
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.
|
||||
138
docs/api/server.md
Normal file
138
docs/api/server.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# The `Server` API
|
||||
|
||||
`Server` is the whole server state, passed as `&mut Server` to every command,
|
||||
mode, and module hook. This is the surface you call from a module. Signatures are
|
||||
abbreviated; see `src/server.rs` for the exact ones.
|
||||
|
||||
## The data model
|
||||
|
||||
- **`Uid`** — an opaque handle for a user (not a pointer). Look users up with it;
|
||||
it can't dangle.
|
||||
- **`srv.users: HashMap<Uid, User>`** — every local and remote user. A `User` has
|
||||
`nick`, `ident`, `host`, `realname`, `account`, `flags`, `ext`, and more.
|
||||
- **`srv.channels: HashMap<String, Channel>`** — keyed by the **lower-cased** name.
|
||||
A `Channel` has its members, `modes` (a `ChanModes` struct), topic, and ban
|
||||
lists.
|
||||
- **`srv.name`, `srv.network`** — this server's name and the network name.
|
||||
- **`srv.conf_path`** — the path to the loaded config file (useful for sibling
|
||||
data files).
|
||||
|
||||
```rust
|
||||
let nick = srv.users.get(&uid).map(|u| u.nick.clone());
|
||||
let count = srv.channels.get(&key).map(|c| c.members.len());
|
||||
```
|
||||
|
||||
## Per-entity state
|
||||
|
||||
Attach your own typed state through the `Extensible` typemap instead of adding
|
||||
struct fields. It is keyed by Rust type and dropped automatically with its owner.
|
||||
|
||||
- **`srv.ext`** — per-server state. For **per-channel** state, key your value by
|
||||
channel name inside a map stored here (channels themselves have no `ext`).
|
||||
- **`srv.users[&uid].ext`** — per-user state.
|
||||
|
||||
```rust
|
||||
#[derive(Default)]
|
||||
struct Counter(u32);
|
||||
|
||||
let c = srv.ext.get_or_insert_with::<Counter>(Counter::default);
|
||||
c.0 += 1;
|
||||
|
||||
if let Some(u) = srv.users.get_mut(&uid) {
|
||||
u.ext.set(MyUserState { /* … */ });
|
||||
}
|
||||
```
|
||||
|
||||
`Extensible` methods: `get::<T>()`, `get_mut::<T>()`, `set::<T>(v)`,
|
||||
`get_or_insert_with::<T>(f)`, `take::<T>()`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Never hardcode a tunable — read it, with a literal default:
|
||||
|
||||
| Method | Returns |
|
||||
|--------|---------|
|
||||
| `srv.conf(key)` | `Option<&str>` — the single value, if set |
|
||||
| `srv.conf_all(key)` | all values for a repeatable key |
|
||||
| `srv.conf_bool(key, default)` | a `yes`/`on`/`true` flag |
|
||||
| `srv.conf_num(key, default)` | any `FromStr` number |
|
||||
|
||||
```rust
|
||||
let threshold = srv.conf_num("mymod_threshold", 8u32);
|
||||
let enabled = srv.conf_bool("mymod", false);
|
||||
for line in srv.conf_all("mymod_rule") { /* … */ }
|
||||
```
|
||||
|
||||
## Sending
|
||||
|
||||
| Method | Sends |
|
||||
|--------|-------|
|
||||
| `srv.send(uid, line)` | a fully-formed protocol line to one user |
|
||||
| `srv.numeric(uid, code, rest)` | a numeric reply (`005`, `461`, …) |
|
||||
| `srv.fail(uid, cmd, code, desc)` / `warn(...)` / `note(...)` | IRCv3 standard replies |
|
||||
| `srv.to_channel(key, line, except)` | a line to every member (optionally excluding one) |
|
||||
| `srv.snotice(msg)` | a server notice to subscribed opers |
|
||||
| `srv.announce(msg)` | a global notice to all users |
|
||||
| `srv.notify_peers(uid, line, want)` | send to a user's common-channel peers whose caps match |
|
||||
|
||||
```rust
|
||||
srv.numeric(uid, 481, ":Permission Denied- You're not an IRC operator");
|
||||
srv.to_channel(&key, format!(":{} NOTICE {} :hi", srv.name, chan), Some(uid));
|
||||
```
|
||||
|
||||
## Lookups & permissions
|
||||
|
||||
| Method | Result |
|
||||
|--------|--------|
|
||||
| `srv.is_oper(uid)` | is the user an IRC operator? |
|
||||
| `srv.rank(uid, key)` | the user's channel rank (compare to `RANK_*`) |
|
||||
| `srv.is_member(uid, key)` | is the user in the channel? |
|
||||
| `srv.extban_active(uid, key, kind)` | does an acting extban of `kind` apply to them here? |
|
||||
|
||||
Rank constants (from `crate::channels`): `RANK_OWNER`, `RANK_ADMIN`, `RANK_OP`,
|
||||
`RANK_HALFOP`, `RANK_VOICE`.
|
||||
|
||||
```rust
|
||||
if srv.rank(uid, &key) >= crate::channels::RANK_OP { /* ops-only */ }
|
||||
```
|
||||
|
||||
## Mutating users
|
||||
|
||||
| Method | Effect |
|
||||
|--------|--------|
|
||||
| `srv.change_host_ident(uid, new_ident, new_host)` | change a user's displayed ident/host (drives `chghost`) |
|
||||
| `srv.oper_up(uid)` | mark a user as an operator |
|
||||
| `srv.remove_user(uid, reason)` | disconnect a user cleanly |
|
||||
|
||||
## Off-core work
|
||||
|
||||
The core thread must never block. Offload slow work:
|
||||
|
||||
- **`srv.disk_write(path, contents)`** — fire-and-forget, coalescing, atomic
|
||||
(temp + rename) file write. Use this for saving state; a slow disk can't stall
|
||||
the core. **No result to handle** — the simplest offload.
|
||||
- **`srv.spawn_crypto(closure)`** — run a bounded, CPU-heavy job (a KDF hash) on a
|
||||
worker thread. Returns `false` if at capacity.
|
||||
- **`srv.spawn_http(...)`** — an outbound HTTP request on a worker thread.
|
||||
|
||||
`spawn_crypto` / `spawn_http` deliver their result back as a core `Event`, so
|
||||
wiring a *new* async result type touches the core event enum in `src/ircd.rs`;
|
||||
`disk_write` needs nothing extra. Prefer `disk_write` for persistence and
|
||||
`on_tick` for periodic jobs.
|
||||
|
||||
```rust
|
||||
// good: persist off-core from a hook
|
||||
srv.disk_write(format!("{}.mystore", srv.conf_path), store.serialize());
|
||||
```
|
||||
|
||||
## Time
|
||||
|
||||
`crate::server::now()` → unix seconds. `iso_time(secs)` / `parse_iso(s)` convert to
|
||||
and from ISO-8601 (used for `server-time` tags and timestamps).
|
||||
|
||||
## Rules of the road
|
||||
|
||||
- Do the work synchronously and quickly; **never block** — offload instead.
|
||||
- Reference users/channels by `Uid` / key; don't cache references across events.
|
||||
- Keep state in `ext`, read settings via `conf*`, and register in the tables. See
|
||||
the [conventions](README.md#project-conventions).
|
||||
Loading…
Add table
Add a link
Reference in a new issue