Enforce channel keys, invites, +s and +H on the IRC listener (closes #122)
check / check (push) Canceled after 0s

The IRC listener's JOIN now passes the channel key on, INVITE records
the invite, LIST and WHOIS hide +s channels from non-members with the
queries the HTTP API uses, and PRIVMSG to a +H channel gets 404.

The INVITE checks and the channel hashcash check move from the HTTP
handlers into the service, so both transports run the same code. An IRC
client cannot attach a stamp, so its PRIVMSG to a +H channel always
fails that check. The unfiltered channel list query, now unused, is
removed.

Model: opus-5-5
This commit is contained in:
2026-10-20 12:03:18 +00:00
parent 1308769a25
commit 03deb8d9aa
7 changed files with 424 additions and 450 deletions
+28 -27
View File
@@ -1277,17 +1277,17 @@ Inspired by IRC, simplified. See [MODE](#mode--query-and-change-modes) for how
to set them; `b`, `k` and `l` can be set only over the HTTP API, and `n` only on
the IRC listener.
| Mode | Name | Meaning | Status |
| ---- | ----------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `+b` | Ban | Prevents matching hostmasks from joining or sending (parameter: `nick!user@host` mask with wildcards) | **Enforced** |
| `+i` | Invite-only | Only invited users can join; use `INVITE` to invite | **Enforced**; only an invite made over the HTTP API lets a user in |
| `+k` | Channel key | Requires a password to join (parameter: key string) | **Enforced**; the IRC listener sends no key, so its users cannot join a channel that has one |
| `+l` | User limit | Maximum number of members allowed in the channel (parameter: integer) | **Enforced** |
| `+m` | Moderated | Only voiced (`+v`) users and operators (`+o`) can send | **Enforced** |
| `+n` | No external | Only channel members can send messages to the channel (default: ON) | Shown only: non-members can never send to a channel, whether or not `+n` is set |
| `+s` | Secret | Channel hidden from LIST and WHOIS for non-members | **Enforced** by `LIST` and `WHOIS` over the HTTP API; not by `GET /api/v1/channels` or the IRC listener |
| `+t` | Topic lock | Only operators can change the topic (default: ON) | **Enforced** |
| `+H` | Hashcash | Requires proof-of-work for PRIVMSG (parameter: bits, e.g. `+H 20`) | **Enforced** over the HTTP API only |
| Mode | Name | Meaning | Status |
| ---- | ----------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `+b` | Ban | Prevents matching hostmasks from joining or sending (parameter: `nick!user@host` mask with wildcards) | **Enforced** |
| `+i` | Invite-only | Only invited users can join; use `INVITE` to invite | **Enforced** |
| `+k` | Channel key | Requires a password to join (parameter: key string) | **Enforced** |
| `+l` | User limit | Maximum number of members allowed in the channel (parameter: integer) | **Enforced** |
| `+m` | Moderated | Only voiced (`+v`) users and operators (`+o`) can send | **Enforced** |
| `+n` | No external | Only channel members can send messages to the channel (default: ON) | Shown only: non-members can never send to a channel, whether or not `+n` is set |
| `+s` | Secret | Channel hidden from LIST and WHOIS for non-members | **Enforced** by `LIST` and `WHOIS`; not by `GET /api/v1/channels` |
| `+t` | Topic lock | Only operators can change the topic (default: ON) | **Enforced** |
| `+H` | Hashcash | Requires proof-of-work for PRIVMSG (parameter: bits, e.g. `+H 20`) | **Enforced**; the IRC listener refuses every PRIVMSG to the channel, as it cannot carry a stamp |
**User channel modes (set per-user per-channel):**
@@ -1312,10 +1312,9 @@ bans. Bans prevent both joining and sending messages.
These ban all users from example.com, remove that ban, and list all bans
(RPL_BANLIST 367, RPL_ENDOFBANLIST 368).
**Invite-only (+i):** When set, users must be invited before joining. Over the
HTTP API, `INVITE` records an invite that is consumed on JOIN; any member can
invite, and on a `+i` channel only an operator can. `INVITE` on the IRC listener
tells the user about the invite but records none.
**Invite-only (+i):** When set, users must be invited before joining. `INVITE`
records an invite that is consumed on JOIN; any member can invite, and on a `+i`
channel only an operator can.
```json
{"command": "MODE", "to": "#channel", "body": ["+i"]}
@@ -1323,7 +1322,9 @@ tells the user about the invite but records none.
```
**Channel key (+k):** Requires a password to join the channel. Over the HTTP API
the key is `body[0]` of the `JOIN`.
the key is `body[0]` of the `JOIN`. On the IRC listener it follows the channel
name, as in `JOIN #channel secretpass`; `JOIN #a,#b keya,keyb` gives each
channel its own key.
```json
{"command": "MODE", "to": "#channel", "body": ["+k", "secretpass"]}
@@ -1338,9 +1339,8 @@ the key is `body[0]` of the `JOIN`.
{"command": "MODE", "to": "#channel", "body": ["-l"]}
```
**Secret (+s):** The HTTP API's `LIST` hides the channel from non-members, and
its `WHOIS` leaves it out of the channel list when the querier is not in the
same channel.
**Secret (+s):** `LIST` hides the channel from non-members, and `WHOIS` leaves
it out of the channel list when the querier is not in the same channel.
**Join errors:** a user who is banned gets ERR_BANNEDFROMCHAN (474), one not
invited to a `+i` channel ERR_INVITEONLYCHAN (473), one with a missing or wrong
@@ -1351,10 +1351,10 @@ key ERR_BADCHANNELKEY (475), and one joining a full channel ERR_CHANNELISFULL
`KICK #channel nick [:reason]`. The kicked user and all channel members receive
the KICK message.
**NOTICE:** Over the HTTP API, a NOTICE gets no RPL_AWAY and no hashcash check
on `+H` channels, but it gets the same error numerics as PRIVMSG (411, 412, 401,
403, 404). The IRC listener handles NOTICE exactly as PRIVMSG, so a NOTICE to a
user who is away gets RPL_AWAY there.
**NOTICE:** A NOTICE gets no hashcash check on `+H` channels, but it gets the
same error numerics as PRIVMSG (411, 412, 401, 403, 404). Over the HTTP API it
gets no RPL_AWAY; the IRC listener sends RPL_AWAY for a NOTICE to a user who is
away, as it does for PRIVMSG.
**ISUPPORT:** The server advertises `PREFIX=(ov)@+` in RPL_ISUPPORT (005), with
`CHANMODES=b,k,Hl,imnst` over the HTTP API and `CHANMODES=,,H,imnst` on the IRC
@@ -1362,10 +1362,11 @@ listener.
### Per-Channel Hashcash (Anti-Spam)
Channels can require hashcash proof-of-work for every `PRIVMSG` sent over the
HTTP API; the IRC listener does not check it. This is an anti-spam mechanism:
channel operators set a difficulty level, and clients must compute a
proof-of-work stamp bound to the specific channel and message before sending.
Channels can require hashcash proof-of-work for every `PRIVMSG`. This is an
anti-spam mechanism: channel operators set a difficulty level, and clients must
compute a proof-of-work stamp bound to the specific channel and message before
sending. An IRC listener client has no way to attach a stamp, so its `PRIVMSG`
to such a channel always gets ERR_CANNOTSENDTOCHAN (404).
**Setting the requirement:**