echoIRCd/docs/api/modes.md

105 lines
3.8 KiB
Markdown

# 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.