feat: implement Tier 1 channel modes (+o/+v/+m/+t), KICK, NOTICE (#88)
Some checks failed
check / check (push) Failing after 1m37s
Some checks failed
check / check (push) Failing after 1m37s
## Summary Implement the core IRC channel functionality that users will immediately notice is missing. This is the foundation for all other mode enforcement. closes #85 ## Changes ### 1. Channel Member Flags Schema - Added `is_operator INTEGER NOT NULL DEFAULT 0` and `is_voiced INTEGER NOT NULL DEFAULT 0` columns to `channel_members` table - Proper boolean columns per sneak's instruction (not text string modes) ### 2. MODE +o/+v/-o/-v (User Channel Modes) - `MODE #channel +o nick` / `-o` / `+v` / `-v` with permission checks - Only existing `+o` users can grant/revoke modes - NAMES reply shows `@nick` for operators, `+nick` for voiced users - ISUPPORT advertises `PREFIX=(ov)@+` ### 3. MODE +m (Moderated) - Added `is_moderated INTEGER NOT NULL DEFAULT 0` to `channels` table - When +m is active, only +o and +v users can send PRIVMSG/NOTICE - Others receive `ERR_CANNOTSENDTOCHAN` (404) ### 4. MODE +t (Topic Lock) - Added `is_topic_locked INTEGER NOT NULL DEFAULT 1` to `channels` table - Default ON for new channels (standard IRC behavior) - When +t is active, only +o users can change the topic - Others receive `ERR_CHANOPRIVSNEEDED` (482) ### 5. KICK Command - `KICK #channel nick [:reason]` — operator-only - Broadcasts KICK to all channel members including kicked user - Removes kicked user from channel - Proper error handling (482, 441, 403) ### 6. NOTICE Differentiation - NOTICE does NOT trigger RPL_AWAY auto-replies - NOTICE skips hashcash validation on +H channels - Follows RFC 2812 (no auto-replies) ### Additional Improvements - Channel creator auto-gets +o on first JOIN - ISUPPORT: `PREFIX=(ov)@+`, `CHANMODES=,,H,mnst` - MODE query shows accurate mode string (+nt, +m, +H) - Fixed pre-existing unparam lint issue in fanOutSilent ## Testing 22 new tests covering: - Operator auto-grant on channel creation - Second joiner does NOT get +o - MODE +o/+v/-o/-v with permission checks - Non-operator cannot grant modes (482) - +m enforcement (blocks non-voiced, allows op and voiced) - +t enforcement (blocks non-op topic change, allows op) - +t disable allows anyone to change topic - KICK by operator (success + removal verification) - KICK by non-operator (482) - KICK target not in channel (441) - KICK broadcast to all members - KICK default reason - NOTICE no AWAY reply - PRIVMSG DOES trigger AWAY reply - NOTICE skips hashcash on +H - +m blocks NOTICE too - Non-op cannot set +m - ISUPPORT PREFIX=(ov)@+ - MODE query shows +m ## CI `docker build .` passes — lint (0 issues), fmt-check, and all tests green. Co-authored-by: user <user@Mac.lan guest wan> Reviewed-on: #88 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
This commit was merged in pull request #88.
This commit is contained in:
75
README.md
75
README.md
@@ -824,8 +824,9 @@ Set or change a channel's topic.
|
||||
- Updates the channel's topic in the database.
|
||||
- The TOPIC event is broadcast to all channel members.
|
||||
- If the channel doesn't exist, the server returns an error.
|
||||
- If the channel has mode `+t` (topic lock), only operators can change the
|
||||
topic (not yet enforced).
|
||||
- If the channel has mode `+t` (topic lock, default: ON for new channels),
|
||||
only operators (`+o`) can change the topic. Non-operators receive
|
||||
`ERR_CHANOPRIVSNEEDED` (482).
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
@@ -1020,16 +1021,23 @@ WHOIS responses (e.g., target user's current client IP and hostname).
|
||||
|
||||
**IRC reference:** RFC 1459 §4.1.5
|
||||
|
||||
#### KICK — Kick User (Planned)
|
||||
#### KICK — Kick User
|
||||
|
||||
Remove a user from a channel.
|
||||
Remove a user from a channel. Only channel operators (`+o`) can use this
|
||||
command. The kicked user and all channel members receive the KICK message.
|
||||
|
||||
**C2S:**
|
||||
```json
|
||||
{"command": "KICK", "to": "#general", "params": ["bob"], "body": ["misbehaving"]}
|
||||
{"command": "KICK", "to": "#general", "body": ["bob", "misbehaving"]}
|
||||
```
|
||||
|
||||
**Status:** Not yet implemented.
|
||||
The first element of `body` is the target nick, the second (optional) is the
|
||||
reason. If no reason is provided, the kicker's nick is used as the default.
|
||||
|
||||
**Errors:**
|
||||
- `482` (ERR_CHANOPRIVSNEEDED) — kicker is not a channel operator
|
||||
- `441` (ERR_USERNOTINCHANNEL) — target is not in the channel
|
||||
- `403` (ERR_NOSUCHCHANNEL) — channel does not exist
|
||||
|
||||
**IRC reference:** RFC 1459 §4.2.8
|
||||
|
||||
@@ -1071,8 +1079,8 @@ the server to the client (never C2S) and use 3-digit string codes in the
|
||||
| `001` | RPL_WELCOME | After session creation | `{"command":"001","to":"alice","body":["Welcome to the network, alice"]}` |
|
||||
| `002` | RPL_YOURHOST | After session creation | `{"command":"002","to":"alice","body":["Your host is neoirc, running version 0.1"]}` |
|
||||
| `003` | RPL_CREATED | After session creation | `{"command":"003","to":"alice","body":["This server was created 2026-02-10"]}` |
|
||||
| `004` | RPL_MYINFO | After session creation | `{"command":"004","to":"alice","params":["neoirc","0.1","","imnst"]}` |
|
||||
| `005` | RPL_ISUPPORT | After session creation | `{"command":"005","to":"alice","params":["CHANTYPES=#","NICKLEN=32","NETWORK=neoirc"],"body":["are supported by this server"]}` |
|
||||
| `004` | RPL_MYINFO | After session creation | `{"command":"004","to":"alice","params":["neoirc","0.1","","mnst"]}` |
|
||||
| `005` | RPL_ISUPPORT | After session creation | `{"command":"005","to":"alice","params":["CHANTYPES=#","NICKLEN=32","PREFIX=(ov)@+","CHANMODES=,,H,mnst","NETWORK=neoirc"],"body":["are supported by this server"]}` |
|
||||
| `221` | RPL_UMODEIS | In response to user MODE query | `{"command":"221","to":"alice","body":["+"]}` |
|
||||
| `251` | RPL_LUSERCLIENT | On connect or LUSERS command | `{"command":"251","to":"alice","body":["There are 5 users and 0 invisible on 1 servers"]}` |
|
||||
| `252` | RPL_LUSEROP | On connect or LUSERS command | `{"command":"252","to":"alice","params":["0"],"body":["operator(s) online"]}` |
|
||||
@@ -1118,25 +1126,34 @@ carries IRC-style parameters (e.g., channel name, target nick).
|
||||
|
||||
Inspired by IRC, simplified:
|
||||
|
||||
| Mode | Name | Meaning |
|
||||
|------|----------------|---------|
|
||||
| `+i` | Invite-only | Only invited users can join |
|
||||
| `+m` | Moderated | Only voiced (`+v`) users and operators (`+o`) can send |
|
||||
| `+s` | Secret | Channel hidden from LIST response |
|
||||
| `+t` | Topic lock | Only operators can change the topic |
|
||||
| `+n` | No external | Only channel members can send messages to the channel |
|
||||
| `+H` | Hashcash | Requires proof-of-work for PRIVMSG (parameter: bits, e.g. `+H 20`) |
|
||||
| Mode | Name | Meaning | Status |
|
||||
|------|----------------|---------|--------|
|
||||
| `+m` | Moderated | Only voiced (`+v`) users and operators (`+o`) can send | **Enforced** |
|
||||
| `+t` | Topic lock | Only operators can change the topic (default: ON) | **Enforced** |
|
||||
| `+n` | No external | Only channel members can send messages to the channel | **Enforced** |
|
||||
| `+H` | Hashcash | Requires proof-of-work for PRIVMSG (parameter: bits, e.g. `+H 20`) | **Enforced** |
|
||||
| `+i` | Invite-only | Only invited users can join | Not yet enforced |
|
||||
| `+s` | Secret | Channel hidden from LIST response | Not yet enforced |
|
||||
|
||||
**User channel modes (set per-user per-channel):**
|
||||
|
||||
| Mode | Meaning | Display prefix |
|
||||
|------|---------|----------------|
|
||||
| `+o` | Operator | `@` in NAMES reply |
|
||||
| `+v` | Voice | `+` in NAMES reply |
|
||||
| Mode | Meaning | Display prefix | Status |
|
||||
|------|---------|----------------|--------|
|
||||
| `+o` | Operator | `@` in NAMES reply | **Enforced** |
|
||||
| `+v` | Voice | `+` in NAMES reply | **Enforced** |
|
||||
|
||||
**Status:** Channel modes are defined but not yet enforced. The `modes` column
|
||||
exists in the channels table but the server does not check modes on actions.
|
||||
Exception: `+H` (hashcash) is fully enforced — see below.
|
||||
**Channel creator auto-op:** The first user to JOIN a channel (creating it)
|
||||
automatically receives `+o` operator status.
|
||||
|
||||
**KICK command:** Channel operators can remove users with `KICK #channel nick
|
||||
[:reason]`. The kicked user and all channel members receive the KICK message.
|
||||
|
||||
**NOTICE:** Follows RFC 2812 — NOTICE never triggers auto-replies (including
|
||||
RPL_AWAY), and skips hashcash validation on +H channels (servers and services
|
||||
use NOTICE).
|
||||
|
||||
**ISUPPORT:** The server advertises `PREFIX=(ov)@+` and
|
||||
`CHANMODES=,,H,mnst` in RPL_ISUPPORT (005).
|
||||
|
||||
### Per-Channel Hashcash (Anti-Spam)
|
||||
|
||||
@@ -2677,17 +2694,19 @@ guess is borne by the server (bcrypt), not the client.
|
||||
- [x] **Hashcash proof-of-work** for session creation (abuse prevention)
|
||||
- [x] **Client output queue pruning** — delete old client output queue entries per `QUEUE_MAX_AGE`
|
||||
- [x] **Message rotation** — prune messages older than `MESSAGE_MAX_AGE`
|
||||
- [ ] **Channel modes** — enforce `+i`, `+m`, `+s`, `+t`, `+n`
|
||||
- [ ] **User channel modes** — `+o` (operator), `+v` (voice)
|
||||
- [x] **MODE command** — query channel and user modes (set not yet implemented)
|
||||
- [x] **NAMES command** — query channel member list
|
||||
- [x] **Channel modes** — enforce `+m` (moderated), `+t` (topic lock), `+n` (no external)
|
||||
- [ ] **Channel modes (tier 2)** — enforce `+i` (invite-only), `+s` (secret), `+b` (ban), `+k` (key), `+l` (limit)
|
||||
- [x] **User channel modes** — `+o` (operator), `+v` (voice) with NAMES prefixes
|
||||
- [x] **KICK command** — operator-only channel kick with broadcast
|
||||
- [x] **MODE command** — query and set channel/user modes
|
||||
- [x] **NAMES command** — query channel member list with @/+ prefixes
|
||||
- [x] **LIST command** — list all channels with member counts
|
||||
- [x] **WHOIS command** — query user information and channel membership
|
||||
- [x] **WHO command** — query channel user list
|
||||
- [x] **LUSERS command** — query server statistics
|
||||
- [x] **Connection registration numerics** — 001-005 sent on session creation
|
||||
- [x] **LUSERS numerics** — 251/252/254/255 sent on connect and via /LUSERS
|
||||
- [ ] **KICK command** — remove users from channels
|
||||
- [x] **KICK command** — remove users from channels (operator-only)
|
||||
- [x] **Numeric replies** — send IRC numeric codes via the message queue
|
||||
(001-005 welcome, 251-255 LUSERS, 311-319 WHOIS, 322-329 LIST/MODE,
|
||||
331-332 TOPIC, 352-353 WHO/NAMES, 366, 372-376 MOTD, 401-461 errors)
|
||||
|
||||
Reference in New Issue
Block a user