1 Commits
Author SHA1 Message Date
clawbot 24d0900dab Bring README.md in line with the modes and the schema (closes #119)
check / check (push) Canceled after 0s
The MODE section described a query-only command. It now covers channel
and user mode queries and changes on the HTTP API and the IRC listener,
the letters each accepts, what is broadcast, and the error numerics.
The schema section now lists every table and column in
001_initial.sql.

Every other statement this change touches was checked against the code:
polled numerics carry their name in command and their number in code;
command errors are numerics with HTTP 200; the broker is keyed by
session; WAL is never on; a hostname falls back to the IP and is
public; and the IRC listener handles modes, NOTICE, INVITE, +s and +H
more narrowly, with gaps in its bridge to the HTTP API.

Model: opus-5-5
2026-10-08 08:42:57 +00:00
+75 -54
View File
@@ -216,25 +216,27 @@ Each session has an IRC-style hostmask composed of three parts:
- **nick** — the user's current nick (changes with `NICK` command) - **nick** — the user's current nick (changes with `NICK` command)
- **username** — an ident-like identifier set at session creation (optional - **username** — an ident-like identifier set at session creation (optional
`username` field in the session request; defaults to the nick) `username` field in the session request; defaults to the nick)
- **hostname** — automatically resolved via reverse DNS of the connecting - **hostname** — the reverse DNS name of the creating client's IP address,
client's IP address at session creation time looked up at session creation time, or the IP address itself when the lookup
gives no name
- **ip** — the real IP address of the session creator, extracted from - **ip** — the real IP address of the session creator, extracted from
`X-Forwarded-For`, `X-Real-IP`, or `RemoteAddr` `X-Forwarded-For`, `X-Real-IP`, or `RemoteAddr`
Each **client connection** (created at session creation or login) also stores Each **client connection** (created at session creation or login) also stores
its own **ip** and **hostname**, allowing the server to track the network origin its own **ip** and **hostname**, found the same way. The session's hostname is
of each individual client independently from the session. Client-level IP and the hostname of the client that created it, and every user can see it: in WHOIS
hostname are **not displayed to regular users**. They are only visible to (311), WHO (352), USERHOST (302), NAMES (353) over the HTTP API, and
**server operators** (o-line) via `RPL_WHOISACTUALLY` (338) when the oper `GET /api/v1/channels/{name}/members`. Only `RPL_WHOISACTUALLY` (338) is for
performs a WHOIS on a user. **server operators** (o-line) alone: it gives the IP address and hostname of the
target's newest client.
The hostmask appears in: The hostmask appears in:
- **WHOIS** (`311 RPL_WHOISUSER`) — `params` contains - **WHOIS** (`311 RPL_WHOISUSER`) — `params` contains
`[nick, username, hostname, "*"]` `[nick, username, hostname, "*"]`
- **WHOIS (oper-only)** (`338 RPL_WHOISACTUALLY`) — when the querier is a server - **WHOIS (oper-only)** (`338 RPL_WHOISACTUALLY`) — when the querier is a server
operator, includes the target's current client IP and hostname (HTTP API only; operator, includes the IP address and hostname of the target's newest client
the IRC listener's WHOIS does not send it) (HTTP API only; the IRC listener's WHOIS does not send it)
- **WHO** (`352 RPL_WHOREPLY`) — `params` contains - **WHO** (`352 RPL_WHOREPLY`) — `params` contains
`[channel, username, hostname, server, nick, flags]` `[channel, username, hostname, server, nick, flags]`
@@ -438,7 +440,8 @@ The entire read/write loop for a client is two endpoints. Everything else
``` ```
┌─ Client ──────────────────────────────────────────────────┐ ┌─ Client ──────────────────────────────────────────────────┐
│ │ │ │
│ 1. POST /api/v1/session {"nick":"alice"} │ │ 1. POST /api/v1/session │
│ {"nick":"alice","pow_token":"<hashcash stamp>"} │
│ → Set-Cookie: neoirc_auth=<random_hex>; HttpOnly; ... │ │ → Set-Cookie: neoirc_auth=<random_hex>; HttpOnly; ... │
│ → {"id":1, "nick":"alice"} │ │ → {"id":1, "nick":"alice"} │
│ │ │ │
@@ -471,7 +474,8 @@ The entire read/write loop for a client is two endpoints. Everything else
``` ```
┌─ Client A ────────────────────────────────────────────────┐ ┌─ Client A ────────────────────────────────────────────────┐
│ │ │ │
│ 1. POST /api/v1/session {"nick":"alice"} │ │ 1. POST /api/v1/session │
│ {"nick":"alice","pow_token":"<hashcash stamp>"} │
│ → Set-Cookie: neoirc_auth=<cookie_a>; HttpOnly; ... │ │ → Set-Cookie: neoirc_auth=<cookie_a>; HttpOnly; ... │
│ → {"id":1, "nick":"alice"} │ │ → {"id":1, "nick":"alice"} │
│ │ │ │
@@ -1079,8 +1083,8 @@ RPL_WHOISCHANNELS (319), and RPL_ENDOFWHOIS (318).
If the querying user is a **server operator** (authenticated via `OPER`), the If the querying user is a **server operator** (authenticated via `OPER`), the
response over the HTTP API additionally includes RPL_WHOISACTUALLY (338) with response over the HTTP API additionally includes RPL_WHOISACTUALLY (338) with
the target's current client IP address and hostname. The IRC listener's WHOIS the IP address and hostname of the target's newest client. The IRC listener's
does not send it. WHOIS does not send it.
**C2S:** **C2S:**
@@ -1122,7 +1126,8 @@ LUSERS replies are also sent automatically during connection registration.
Authenticate as a server operator (o-line). On success, the session gains oper Authenticate as a server operator (o-line). On success, the session gains oper
privileges: it can use `KILL` and `WALLOPS`, and over the HTTP API its WHOIS privileges: it can use `KILL` and `WALLOPS`, and over the HTTP API its WHOIS
responses include more (the target user's current client IP and hostname). responses include more (the IP address and hostname of the target user's newest
client).
**C2S:** **C2S:**
@@ -1346,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 `KICK #channel nick [:reason]`. The kicked user and all channel members receive
the KICK message. the KICK message.
**NOTICE:** Follows RFC 2812 over the HTTP API — NOTICE never triggers **NOTICE:** Over the HTTP API, a NOTICE gets no RPL_AWAY and no hashcash check
auto-replies (including RPL_AWAY), and skips hashcash validation on +H channels on `+H` channels, but it gets the same error numerics as PRIVMSG (411, 412, 401,
(servers and services use NOTICE). On the IRC listener, a NOTICE to a user who 403, 404). The IRC listener handles NOTICE exactly as PRIVMSG, so a NOTICE to a
is away does get RPL_AWAY. user who is away gets RPL_AWAY there.
**ISUPPORT:** The server advertises `PREFIX=(ov)@+` in RPL_ISUPPORT (005), with **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 `CHANMODES=b,k,Hl,imnst` over the HTTP API and `CHANMODES=,,H,imnst` on the IRC
@@ -1448,9 +1453,11 @@ difficulty is advertised via `GET /api/v1/server` in the `hashcash_bits` field.
| `pow_token` | string | Conditional | Hashcash stamp (required when server has `hashcash_bits` > 0) | | `pow_token` | string | Conditional | Hashcash stamp (required when server has `hashcash_bits` > 0) |
The `username` field sets the user portion of the IRC hostmask The `username` field sets the user portion of the IRC hostmask
(`nick!user@host`). The hostname is automatically resolved via reverse DNS of (`nick!user@host`). The hostname is the reverse DNS name of the connecting
the connecting client's IP address at session creation time. Together these form client's IP address, looked up at session creation time, or the IP address
the hostmask used in WHOIS, WHO, and ban matching (`+b`). itself when the lookup gives no name; every user can see it (see
[Hostmask](#hostmask-nickuserhost)). Together these form the hostmask used in
WHOIS, WHO, NAMES, and ban matching (`+b`).
**Response:** `201 Created` **Response:** `201 Created`
@@ -1729,12 +1736,12 @@ reference with all required and optional fields.
All IRC commands return HTTP 200 OK, usually with `{"status": "error"}` when the All IRC commands return HTTP 200 OK, usually with `{"status": "error"}` when the
reply is an error numeric (a WHOIS for an unknown nick still gets reply is an error numeric (a WHOIS for an unknown nick still gets
`{"status": "ok"}`), and otherwise a status of the command's own (see each `{"status": "ok"}`), and otherwise a status of the command's own (see each
command). IRC-level success and error responses are delivered as **numeric command). **Numeric replies**, for success and for errors, are delivered through
replies** through the message queue (see the message queue (see [Numeric Reply Codes](#numeric-reply-codes-s2c-only)); a
[Numeric Reply Codes](#numeric-reply-codes-s2c-only)); `PING` is the exception, command that has none, such as a successful `PRIVMSG`, is answered by the HTTP
its `PONG` is the HTTP response body. HTTP error codes (4xx/5xx) are reserved response alone, and `PING`'s `PONG` is the HTTP response body. HTTP error codes
for transport-level problems: malformed JSON (400), missing/invalid auth cookies (4xx/5xx) are reserved for transport-level problems: malformed JSON (400),
(401), and server errors (500). missing/invalid auth cookies (401), and server errors (500).
**HTTP errors (transport-level only):** **HTTP errors (transport-level only):**
@@ -2401,36 +2408,36 @@ applied; `001_initial.sql` creates the tables below.
#### `sessions` #### `sessions`
| Column | Type | Description | | Column | Type | Description |
| --------------- | -------- | ------------------------------------------------------------- | | --------------- | -------- | ------------------------------------------------------------------------------- |
| `id` | INTEGER | Primary key (auto-increment) | | `id` | INTEGER | Primary key (auto-increment) |
| `uuid` | TEXT | Unique session UUID | | `uuid` | TEXT | Unique session UUID |
| `nick` | TEXT | Unique nick | | `nick` | TEXT | Unique nick |
| `username` | TEXT | IRC ident/username portion of the hostmask (defaults to nick) | | `username` | TEXT | IRC ident/username portion of the hostmask (defaults to nick) |
| `hostname` | TEXT | Reverse DNS hostname of the connecting client IP | | `hostname` | TEXT | Reverse DNS name of the creating client's IP, or the IP itself when it has none |
| `ip` | TEXT | Real IP address of the session creator | | `ip` | TEXT | Real IP address of the session creator |
| `is_oper` | INTEGER | Server operator, user mode `+o` (0 = no, 1 = yes) | | `is_oper` | INTEGER | Server operator, user mode `+o` (0 = no, 1 = yes) |
| `is_wallops` | INTEGER | User mode `+w`, receives `WALLOPS` (0 = no, 1 = yes) | | `is_wallops` | INTEGER | User mode `+w`, receives `WALLOPS` (0 = no, 1 = yes) |
| `password_hash` | TEXT | bcrypt hash (empty string for anonymous sessions) | | `password_hash` | TEXT | bcrypt hash (empty string for anonymous sessions) |
| `signing_key` | TEXT | Public signing key; nothing sets or reads it yet | | `signing_key` | TEXT | Public signing key; nothing sets or reads it yet |
| `away_message` | TEXT | Away message (empty string if not away) | | `away_message` | TEXT | Away message (empty string if not away) |
| `created_at` | DATETIME | Session creation time | | `created_at` | DATETIME | Session creation time |
| `last_seen` | DATETIME | Last login or authenticated HTTP API request (or creation) | | `last_seen` | DATETIME | Last login or authenticated HTTP API request (or creation) |
Index on `(uuid)`. Index on `(uuid)`.
#### `clients` #### `clients`
| Column | Type | Description | | Column | Type | Description |
| ------------ | -------- | ---------------------------------------------------------- | | ------------ | -------- | ----------------------------------------------------------------------- |
| `id` | INTEGER | Primary key (auto-increment) | | `id` | INTEGER | Primary key (auto-increment) |
| `uuid` | TEXT | Unique client UUID | | `uuid` | TEXT | Unique client UUID |
| `session_id` | INTEGER | FK → sessions.id (cascade delete) | | `session_id` | INTEGER | FK → sessions.id (cascade delete) |
| `token` | TEXT | Auth cookie value (SHA-256 hash of the 64-hex-char cookie) | | `token` | TEXT | Auth cookie value (SHA-256 hash of the 64-hex-char cookie) |
| `ip` | TEXT | Real IP address of this client connection | | `ip` | TEXT | Real IP address of this client connection |
| `hostname` | TEXT | Reverse DNS hostname of this client connection | | `hostname` | TEXT | Reverse DNS name of this client's IP, or the IP itself when it has none |
| `created_at` | DATETIME | Client creation time | | `created_at` | DATETIME | Client creation time |
| `last_seen` | DATETIME | Last authenticated HTTP API request (or creation) | | `last_seen` | DATETIME | Last authenticated HTTP API request (or creation) |
Indexes on `(token)` and `(session_id)`. Indexes on `(token)` and `(session_id)`.
@@ -2671,8 +2678,22 @@ IRC_LISTEN_ADDR: ""
Messages sent by IRC clients appear in channels visible to HTTP/JSON API clients Messages sent by IRC clients appear in channels visible to HTTP/JSON API clients
and vice versa. The IRC listener and HTTP API share the same database, broker, and vice versa. The IRC listener and HTTP API share the same database, broker,
and session infrastructure. A user connected via IRC and a user connected via and session infrastructure, so a user connected via IRC and a user connected via
the HTTP API can communicate in the same channels seamlessly. the HTTP API can talk in the same channels, with these gaps today:
- An IRC listener user receives only the first line of a multi-line `body`.
- A `+k`, `+l` or `+b` change made over the HTTP API reaches IRC listener
members without its key, limit or mask.
- The IRC listener's `JOIN` accepts any channel name, while over the HTTP API a
name that is not `#` followed by 1–63 letters, digits, `_` or `-` gets
ERR_NOSUCHCHANNEL (403), so HTTP API users cannot join such a channel.
- The IRC listener does not pass on a `PRIVMSG`, `NOTICE`, `JOIN`, `PART`,
`NICK` or `QUIT` whose sender's nick matches the user's own nick ignoring
case, so an IRC listener user `alice` never sees these from `Alice`, although
nicks are case-sensitive.
https://git.eeqj.de/sneak/neoirc/issues/127 tracks the first three and
https://git.eeqj.de/sneak/neoirc/issues/128 the last.
### Docker Usage ### Docker Usage