Add embedded web chat client (closes #7) #8

Merged
clawbot merged 22 commits from feature/web-client into main 2026-02-11 03:02:42 +01:00
72 changed files with 963 additions and 1149 deletions
Showing only changes of commit dfb1636be5 - Show all commits

227
README.md
View File

@@ -18,10 +18,87 @@ This project builds a chat server that:
- Holds session state server-side (message queues, presence, channel membership)
- Exposes a minimal, clean HTTP+JSON API — easy to build clients against
- Supports multiple concurrent connections per user session
- Supports multiple concurrent clients per user session
- Provides IRC-like semantics: channels, nicks, topics, modes
- Uses structured JSON messages with IRC command names and numeric reply codes
## Design Decisions
### Identity & Sessions — No Accounts
There are no accounts, no registration, no passwords. Identity is a signing
key; a nick is just a display name. The two are decoupled.
- **Session creation**: client connects → server assigns a **session UUID**
(user identity for this server), a **client UUID** (this specific device),
and an **opaque auth token** (random bytes, not JWT).
- The auth token implicitly identifies the client. Clients present it via
`Authorization: Bearer <token>`.
- Nicks are changeable; the session UUID is the stable identity.
- Server-assigned UUIDs — clients do not choose their own IDs.
### Multi-Client Model
A single user session can have multiple clients (phone, laptop, terminal).
- Each client gets a **separate server-to-client (S2C) message queue**.
- The server fans out all S2C messages to every active client queue for that
user session.
- `GET /api/v1/messages` delivers from the calling client's specific queue,
identified by the auth token.
- Client queues have **independent expiry/pruning** — one client going offline
doesn't affect others.
```
User (session UUID)
├── Client A (client UUID, token, queue)
├── Client B (client UUID, token, queue)
└── Client C (client UUID, token, queue)
```
### Message Immutability
Messages are **immutable** — no editing, no deletion by clients. This is a
deliberate design choice that enables cryptographic signing: if a message could
be modified after signing, signatures would be meaningless.
### Message Delivery
- **Long-poll timeout**: 15 seconds
- **Queue depth**: server-configurable, default at least 48 hours worth of
messages
- **No delivery/read receipts** except in DMs
- **Bodies are structured** objects or arrays (never raw strings) — enables
deterministic canonicalization via RFC 8785 JCS for signing
### Crypto & Signing
- Servers **relay signatures verbatim** — signatures are key/value metadata on
message objects (`meta.sig`, `meta.alg`). Servers do not verify them.
- Clients handle key authentication via **TOFU** (trust on first use).
- **No key revocation mechanism** — keep your keys safe.
- **PUBKEY** message type for distributing signing keys to channel members.
- **E2E encryption for DMs** is planned for 1.0.
### Channels
- **Any user can create channels** — joining a nonexistent channel creates it,
like IRC.
- **Ephemeral** — channels disappear when the last member leaves.
- No channel size limits.
- No channel-level encryption.
### Federation
- **Manual server linking only** — no autodiscovery, no mesh. Operators
explicitly configure server links.
- Servers relay messages (including signatures) verbatim.
### Web Client
The SPA web client is a **convenience UI**. The primary interface is IRC-style
client apps talking directly to the HTTP API.
## Architecture
### Transport: HTTP only
@@ -30,7 +107,8 @@ All client↔server and server↔server communication uses HTTP/1.1+ with JSON
request/response bodies. No WebSockets, no raw TCP, no gRPC — just plain HTTP.
- **Client polling**: Clients long-poll `GET /api/v1/messages` — server holds
the connection until messages arrive or timeout. One endpoint for everything.
the connection for up to 15 seconds until messages arrive or timeout.
One endpoint for everything.
- **Client sending**: `POST /api/v1/messages` with a `to` field. That's it.
- **Server federation**: Servers exchange messages via HTTP to enable multi-server
networks (like IRC server linking)
@@ -38,6 +116,33 @@ request/response bodies. No WebSockets, no raw TCP, no gRPC — just plain HTTP.
The entire read/write loop for a client is two endpoints. Everything else is
channel management and history.
### Session Model
```
┌─────────────────────────────────┐
│ User Session (UUID) │
│ nick: "alice" │
│ signing key: ed25519:... │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Client A │ │ Client B │ ... │
│ │ UUID │ │ UUID │ │
│ │ token │ │ token │ │
│ │ queue │ │ queue │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────┘
```
- **User session**: server-assigned UUID. Represents a user on this server.
Has a nick (changeable, unique per server at any point in time).
- **Client**: each device/connection gets its own UUID and opaque auth token.
The token is the credential — present it to authenticate.
- **Queue**: each client has an independent S2C message queue. The server fans
out messages to all active client queues for the session.
Sessions persist across disconnects. Messages queue until retrieved. Client
queues expire independently after a configurable idle timeout.
### Message Protocol
All messages use **IRC command names and numeric reply codes** from RFC 1459/2812.
@@ -72,7 +177,7 @@ This enables:
|----------|-------------|
| PRIVMSG | Send message to channel or user |
| NOTICE | Send notice (no auto-reply expected) |
| JOIN | Join a channel |
| JOIN | Join a channel (creates it if nonexistent) |
| PART | Leave a channel |
| QUIT | Disconnect from server |
| NICK | Change nickname |
@@ -96,7 +201,7 @@ All C2S commands may be echoed back as S2C (relayed to other users), plus:
| Code | Name | Description |
|------|-------------------|-------------|
| 001 | RPL_WELCOME | Welcome after registration |
| 001 | RPL_WELCOME | Welcome after session creation |
| 002 | RPL_YOURHOST | Server host information |
| 322 | RPL_LIST | Channel list entry |
| 353 | RPL_NAMREPLY | Names list for a channel |
@@ -144,6 +249,8 @@ complete index.
### Canonicalization and Signing
Messages support optional cryptographic signatures for integrity verification.
Servers relay signatures verbatim without verifying them — verification is
purely a client-side concern.
#### Canonicalization (RFC 8785 JCS)
@@ -179,34 +286,9 @@ under canonicalization.
{"command": "PUBKEY", "from": "alice", "body": {"alg": "ed25519", "key": "base64-encoded-pubkey"}}
```
Servers SHOULD relay PUBKEY messages to all channel members. Clients SHOULD
cache public keys and use them to verify `meta.sig` on incoming messages.
Key distribution is trust-on-first-use (TOFU) by default.
### Core Concepts
#### Users
- Identified by a unique user ID (UUID)
- Authenticate via token (issued at registration or login)
- Have a nick (changeable, unique per server at any point in time)
- Maintain a persistent message queue on the server
#### Sessions
- A session represents an authenticated user's connection context
- Session state is **server-held**, not connection-bound
- Multiple devices can share a session (messages delivered to all)
- Sessions persist across disconnects — messages queue until retrieved
- Sessions expire after a configurable idle timeout (default 24h)
#### Channels
- Named with `#` prefix (e.g. `#general`)
- Have a topic, mode flags, and member list
- Messages to a channel are queued for all members
- Channel history is stored server-side (configurable depth)
- No eternal logging by default — history rotates
Servers relay PUBKEY messages to all channel members. Clients cache public keys
and use them to verify `meta.sig` on incoming messages. Key distribution is
trust-on-first-use (TOFU). There is no key revocation mechanism.
### API Endpoints
@@ -216,9 +298,9 @@ require `Authorization: Bearer <token>` header.
The API is the primary interface — designed for IRC-style clients. The entire
client loop is:
1. `POST /api/v1/register` get a token
1. `POST /api/v1/session` — create a session, get a token
2. `GET /api/v1/state` — see who you are and what channels you're in
3. `GET /api/v1/messages?after=0` — long-poll for all messages (channel, DM, system)
3. `GET /api/v1/messages?timeout=15` — long-poll for all messages (channel, DM, system)
4. `POST /api/v1/messages` — send to `"#channel"` or `"nick"`
That's the core. Everything else (join, part, history, members) is ancillary.
@@ -226,11 +308,11 @@ That's the core. Everything else (join, part, history, members) is ancillary.
#### Quick example (curl)
```bash
# Register
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/register \
# Create a session (get session UUID, client UUID, and auth token)
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/session \
-d '{"nick":"alice"}' | jq -r .token)
# Join a channel
# Join a channel (creates it if it doesn't exist)
curl -s -X POST http://localhost:8080/api/v1/messages \
-H "Authorization: Bearer $TOKEN" \
-d '{"command":"JOIN","to":"#general"}'
@@ -240,34 +322,41 @@ curl -s -X POST http://localhost:8080/api/v1/messages \
-H "Authorization: Bearer $TOKEN" \
-d '{"command":"PRIVMSG","to":"#general","body":["hello world"]}'
# Poll for messages (long-poll)
curl -s http://localhost:8080/api/v1/messages?after=0 \
# Poll for messages (long-poll, 15s timeout)
curl -s "http://localhost:8080/api/v1/messages?timeout=15" \
-H "Authorization: Bearer $TOKEN"
```
#### Registration
#### Session
```
POST /api/v1/register — Create account { "nick": "..." } → { id, nick, token }
POST /api/v1/session — Create session { "nick": "..." }
→ { session_id, client_id, nick, token }
Token is opaque (random), not JWT.
Token implicitly identifies the client.
```
#### State
```
GET /api/v1/state — User state: nick, id, and list of joined channels
Replaces separate /me and /channels endpoints
GET /api/v1/state — User state: nick, session_id, client_id,
and list of joined channels
```
#### Messages (unified stream)
```
GET /api/v1/messages — Single message stream (long-poll supported)
GET /api/v1/messages — Single message stream (long-poll, 15s timeout)
All message types: channel, DM, notices, events
Query params: ?after=<message-id>&timeout=30
Delivers from the calling client's queue
(identified by auth token)
Query params: ?after=<message-id>&timeout=15
POST /api/v1/messages — Send a message (IRC command in body)
Body: { "command": "PRIVMSG", "to": "#channel", "body": ["..."] }
```
Messages are immutable — no edit or delete endpoints.
#### History
```
@@ -281,7 +370,9 @@ GET /api/v1/history — Fetch history for a target (channel or DM)
```
GET /api/v1/channels/all — List all server channels
POST /api/v1/channels/join — Join a channel { "channel": "#name" }
Creates the channel if it doesn't exist.
DELETE /api/v1/channels/{name} — Part (leave) a channel
Channel is destroyed when last member leaves.
GET /api/v1/channels/{name}/members — Channel member list
```
@@ -294,7 +385,8 @@ GET /.well-known/healthcheck.json — Health check
### Federation (Server-to-Server)
Servers can link to form a network, similar to IRC server linking:
Servers can link to form a network, similar to IRC server linking. Links are
**manually configured** — there is no autodiscovery.
```
POST /api/v1/federation/link — Establish server link (mutual auth via shared key)
@@ -303,8 +395,8 @@ GET /api/v1/federation/status — Link status
```
Federation uses the same HTTP+JSON transport. S2S messages use the RELAY, LINK,
UNLINK, SYNC, PING, and PONG commands. Messages are relayed between servers so
users on different servers can share channels.
UNLINK, SYNC, PING, and PONG commands. Messages (including signatures) are
relayed verbatim between servers so users on different servers can share channels.
### Channel Modes
@@ -331,7 +423,9 @@ Via environment variables (Viper), following gohttpserver conventions:
| `DEBUG` | `false` | Debug mode |
| `MAX_HISTORY` | `10000` | Max messages per channel history |
| `SESSION_TIMEOUT` | `86400` | Session idle timeout (seconds) |
| `QUEUE_MAX_AGE` | `172800` | Max client queue age in seconds (default 48h) |
| `MAX_MESSAGE_SIZE` | `4096` | Max message body size (bytes) |
| `LONG_POLL_TIMEOUT` | `15` | Long-poll timeout in seconds |
| `MOTD` | `""` | Message of the day |
| `SERVER_NAME` | hostname | Server display name |
| `FEDERATION_KEY` | `""` | Shared key for server linking |
@@ -341,11 +435,12 @@ Via environment variables (Viper), following gohttpserver conventions:
SQLite by default (single-file, zero-config), with Postgres support for
larger deployments. Tables:
- `users`accounts and auth tokens
- `sessions`user sessions (UUID, nick, created_at)
- `clients` — client records (UUID, session_id, token_hash, last_seen)
- `channels` — channel metadata and modes
- `channel_members` — membership and user modes
- `messages` — message history (rotated per `MAX_HISTORY`)
- `message_queue` — per-user pending delivery queue
- `client_queues` — per-client pending delivery queues
- `server_links` — federation peer configuration
### Project Structure
@@ -398,30 +493,30 @@ Per gohttpserver conventions:
| Metrics | `github.com/prometheus/client_golang` |
| DB | `modernc.org/sqlite` + `database/sql` |
### Web Client
The server embeds a single-page web client (Preact) served at `/`. This is a
**convenience/reference implementation** — not the primary interface. The
primary intended clients are IRC-style terminal applications, bots, and custom
clients talking directly to the HTTP API.
### Design Principles
1. **API-first** — the HTTP API is the product. Clients are thin. If you can't
build a working IRC-style TUI client in an afternoon, the API is too complex.
2. **IRC semantics over HTTP** — command names and numeric codes from RFC 1459/2812.
2. **No accounts** — identity is a signing key, nick is a display name. No
registration, no passwords. Session creation is instant.
3. **IRC semantics over HTTP** — command names and numeric codes from RFC 1459/2812.
Familiar to anyone who's built IRC clients or bots.
3. **HTTP is the only transport** — no WebSockets, no raw TCP, no protocol
4. **HTTP is the only transport** — no WebSockets, no raw TCP, no protocol
negotiation. HTTP is universal, proxy-friendly, and works everywhere.
4. **Server holds state** — clients are stateless. Reconnect, switch devices,
lose connectivity — your messages are waiting.
5. **Structured messages** — JSON with extensible metadata. Bodies are always
5. **Server holds state** — clients are stateless. Reconnect, switch devices,
lose connectivity — your messages are waiting in your client queue.
6. **Structured messages** — JSON with extensible metadata. Bodies are always
objects or arrays for deterministic canonicalization (JCS) and signing.
6. **Simple deployment** — single binary, SQLite default, zero mandatory
7. **Immutable messages** — no editing, no deletion. Fits naturally with
cryptographic signatures.
8. **Simple deployment** — single binary, SQLite default, zero mandatory
external dependencies.
7. **No eternal logs** — history rotates. Chat should be ephemeral by default.
8. **Federation optional** — single server works standalone. Linking is opt-in.
9. **Signable messages** — optional Ed25519 signatures with TOFU key distribution.
9. **No eternal logs** — history rotates. Chat should be ephemeral by default.
Channels disappear when empty.
10. **Federation optional** — single server works standalone. Linking is manual
and opt-in.
11. **Signable messages** — optional Ed25519 signatures with TOFU key
distribution. Servers relay signatures without verification.
## Status

View File

@@ -1,81 +1,87 @@
# Message Schema Index
# Message Schemas
JSON Schema (draft 2020-12) definitions for the IRC-style message protocol.
JSON Schema definitions (draft 2020-12) for the chat protocol. Messages use
**IRC command names and numeric reply codes** (RFC 1459/2812) encoded as JSON
over HTTP.
All messages share a common envelope defined in
[`message.schema.json`](message.schema.json).
## Envelope
## Base Envelope
Every message is a JSON object with a `command` field. The format maps directly
to IRC wire format:
| Field | Type | Required | Description |
|-----------|-----------------|----------|-------------|
| `command` | string | ✓ | IRC command name or numeric reply code |
| `from` | string | | Sender nick or server name |
| `to` | string | | Destination channel or nick |
| `params` | array\<string\> | | Additional IRC-style parameters |
| `body` | array \| object | varies | Message body (never a raw string) |
| `meta` | object | | Extensible metadata (signatures, etc.) |
| `id` | string (uuid) | | Server-assigned message ID |
| `ts` | string (date-time) | | Server-assigned timestamp |
```
IRC: :nick PRIVMSG #channel :hello world
JSON: {"command": "PRIVMSG", "from": "nick", "to": "#channel", "body": "hello world"}
## Client-to-Server (C2S)
IRC: :server 353 nick = #channel :user1 @op1 +voice1
JSON: {"command": "353", "to": "nick", "params": ["=", "#channel"], "body": "user1 @op1 +voice1"}
```
| Command | Schema | Description |
|----------|--------|-------------|
| PRIVMSG | [`c2s/privmsg.schema.json`](c2s/privmsg.schema.json) | Send message to channel or user |
| NOTICE | [`c2s/notice.schema.json`](c2s/notice.schema.json) | Send a notice |
| JOIN | [`c2s/join.schema.json`](c2s/join.schema.json) | Join a channel |
| PART | [`c2s/part.schema.json`](c2s/part.schema.json) | Leave a channel |
| QUIT | [`c2s/quit.schema.json`](c2s/quit.schema.json) | Disconnect |
| NICK | [`c2s/nick.schema.json`](c2s/nick.schema.json) | Change nick |
| MODE | [`c2s/mode.schema.json`](c2s/mode.schema.json) | Set/query modes |
| TOPIC | [`c2s/topic.schema.json`](c2s/topic.schema.json) | Set/query topic |
| KICK | [`c2s/kick.schema.json`](c2s/kick.schema.json) | Kick user |
| PING | [`c2s/ping.schema.json`](c2s/ping.schema.json) | Client keepalive |
| PUBKEY | [`c2s/pubkey.schema.json`](c2s/pubkey.schema.json) | Announce public key |
Common fields (see `message.json` for full schema):
## Server-to-Client (S2C)
| Field | Type | Description |
|-----------|----------|-------------------------------------------------------|
| `id` | integer | Server-assigned ID (monotonically increasing) |
| `command` | string | IRC command or 3-digit numeric code |
| `from` | string | Source nick or server name (IRC prefix) |
| `to` | string | Target: #channel or nick |
| `params` | string[] | Middle parameters (mainly for numerics) |
| `body` | string | Trailing parameter (message text) |
| `ts` | string | ISO 8601 timestamp (server-assigned, not in raw IRC) |
| `meta` | object | Extensible metadata (not in raw IRC) |
### Named Commands
## Commands
| Command | Schema | Description |
|----------|--------|-------------|
| PRIVMSG | [`s2c/privmsg.schema.json`](s2c/privmsg.schema.json) | Relayed message |
| NOTICE | [`s2c/notice.schema.json`](s2c/notice.schema.json) | Server or user notice |
| JOIN | [`s2c/join.schema.json`](s2c/join.schema.json) | User joined channel |
| PART | [`s2c/part.schema.json`](s2c/part.schema.json) | User left channel |
| QUIT | [`s2c/quit.schema.json`](s2c/quit.schema.json) | User disconnected |
| NICK | [`s2c/nick.schema.json`](s2c/nick.schema.json) | Nick change |
| MODE | [`s2c/mode.schema.json`](s2c/mode.schema.json) | Mode change |
| TOPIC | [`s2c/topic.schema.json`](s2c/topic.schema.json) | Topic change |
| KICK | [`s2c/kick.schema.json`](s2c/kick.schema.json) | User kicked |
| PONG | [`s2c/pong.schema.json`](s2c/pong.schema.json) | Server pong |
| PUBKEY | [`s2c/pubkey.schema.json`](s2c/pubkey.schema.json) | Relayed public key |
| ERROR | [`s2c/error.schema.json`](s2c/error.schema.json) | Server error |
IRC commands used for client↔server and server↔server communication.
### Numeric Replies
| Command | File | RFC | Description |
|-----------|---------------------------|-----------|--------------------------------|
| `PRIVMSG` | `commands/PRIVMSG.json` | 1459 §4.4.1 | Message to channel or user |
| `NOTICE` | `commands/NOTICE.json` | 1459 §4.4.2 | Notice (no auto-reply) |
| `JOIN` | `commands/JOIN.json` | 1459 §4.2.1 | Join a channel |
| `PART` | `commands/PART.json` | 1459 §4.2.2 | Leave a channel |
| `QUIT` | `commands/QUIT.json` | 1459 §4.1.6 | User disconnected |
| `NICK` | `commands/NICK.json` | 1459 §4.1.2 | Change nickname |
| `TOPIC` | `commands/TOPIC.json` | 1459 §4.2.4 | Get/set channel topic |
| `MODE` | `commands/MODE.json` | 1459 §4.2.3 | Set channel/user modes |
| `KICK` | `commands/KICK.json` | 1459 §4.2.8 | Kick user from channel |
| `PING` | `commands/PING.json` | 1459 §4.6.2 | Keepalive |
| `PONG` | `commands/PONG.json` | 1459 §4.6.3 | Keepalive response |
| Code | Name | Schema | Description |
|------|--------------------|--------|-------------|
| 001 | RPL_WELCOME | [`s2c/001.schema.json`](s2c/001.schema.json) | Welcome after registration |
| 002 | RPL_YOURHOST | [`s2c/002.schema.json`](s2c/002.schema.json) | Server host info |
| 322 | RPL_LIST | [`s2c/322.schema.json`](s2c/322.schema.json) | Channel list entry |
| 353 | RPL_NAMREPLY | [`s2c/353.schema.json`](s2c/353.schema.json) | Names list |
| 366 | RPL_ENDOFNAMES | [`s2c/366.schema.json`](s2c/366.schema.json) | End of names list |
| 372 | RPL_MOTD | [`s2c/372.schema.json`](s2c/372.schema.json) | MOTD line |
| 375 | RPL_MOTDSTART | [`s2c/375.schema.json`](s2c/375.schema.json) | Start of MOTD |
| 376 | RPL_ENDOFMOTD | [`s2c/376.schema.json`](s2c/376.schema.json) | End of MOTD |
| 401 | ERR_NOSUCHNICK | [`s2c/401.schema.json`](s2c/401.schema.json) | No such nick/channel |
| 403 | ERR_NOSUCHCHANNEL | [`s2c/403.schema.json`](s2c/403.schema.json) | No such channel |
| 433 | ERR_NICKNAMEINUSE | [`s2c/433.schema.json`](s2c/433.schema.json) | Nick in use |
## Numeric Replies
## Server-to-Server (S2S)
Three-digit codes for server responses, per IRC convention.
| Command | Schema | Description |
|---------|--------|-------------|
| RELAY | [`s2s/relay.schema.json`](s2s/relay.schema.json) | Relay message to linked server |
| LINK | [`s2s/link.schema.json`](s2s/link.schema.json) | Establish server link |
| UNLINK | [`s2s/unlink.schema.json`](s2s/unlink.schema.json) | Tear down link |
| SYNC | [`s2s/sync.schema.json`](s2s/sync.schema.json) | Synchronize state |
| PING | [`s2s/ping.schema.json`](s2s/ping.schema.json) | Server ping |
| PONG | [`s2s/pong.schema.json`](s2s/pong.schema.json) | Server pong |
### Success / Informational (0xx3xx)
| Code | Name | File | Description |
|-------|-------------------|-----------------------|--------------------------------|
| `001` | RPL_WELCOME | `numerics/001.json` | Welcome after session creation |
| `002` | RPL_YOURHOST | `numerics/002.json` | Server host info |
| `003` | RPL_CREATED | `numerics/003.json` | Server creation date |
| `004` | RPL_MYINFO | `numerics/004.json` | Server info and modes |
| `322` | RPL_LIST | `numerics/322.json` | Channel list entry |
| `323` | RPL_LISTEND | `numerics/323.json` | End of channel list |
| `332` | RPL_TOPIC | `numerics/332.json` | Channel topic |
| `353` | RPL_NAMREPLY | `numerics/353.json` | Channel member list |
| `366` | RPL_ENDOFNAMES | `numerics/366.json` | End of NAMES list |
| `372` | RPL_MOTD | `numerics/372.json` | MOTD line |
| `375` | RPL_MOTDSTART | `numerics/375.json` | Start of MOTD |
| `376` | RPL_ENDOFMOTD | `numerics/376.json` | End of MOTD |
### Errors (4xx)
| Code | Name | File | Description |
|-------|----------------------|-----------------------|--------------------------------|
| `401` | ERR_NOSUCHNICK | `numerics/401.json` | No such nick/channel |
| `403` | ERR_NOSUCHCHANNEL | `numerics/403.json` | No such channel |
| `433` | ERR_NICKNAMEINUSE | `numerics/433.json` | Nickname already in use |
| `442` | ERR_NOTONCHANNEL | `numerics/442.json` | Not on that channel |
| `482` | ERR_CHANOPRIVSNEEDED | `numerics/482.json` | Not channel operator |
## Federation (S2S)
Server-to-server messages use the same command format. Federated servers relay
messages with an additional `origin` field in `meta` to track the source server.
The PING/PONG commands serve as inter-server keepalives. State sync after link
establishment uses a burst of JOIN, NICK, TOPIC, and MODE commands.

View File

@@ -1,23 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/join.schema.json",
"title": "JOIN (C2S)",
"description": "Join a channel",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "JOIN"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Not used"
}
},
"required": [
"command",
"to"
]
}

View File

@@ -1,23 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/kick.schema.json",
"title": "KICK (C2S)",
"description": "Kick user from channel",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "KICK"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Kick reason"
}
},
"required": [
"command",
"to"
]
}

View File

@@ -1,23 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/mode.schema.json",
"title": "MODE (C2S)",
"description": "Set/query modes",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "MODE"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Mode params"
}
},
"required": [
"command",
"to"
]
}

View File

@@ -1,23 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/nick.schema.json",
"title": "NICK (C2S)",
"description": "Request nick change",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "NICK"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Not used"
}
},
"required": [
"command",
"to"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/notice.schema.json",
"title": "NOTICE (C2S)",
"description": "Send a notice (no auto-reply expected)",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "NOTICE"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Text lines"
}
},
"required": [
"command",
"to",
"body"
]
}

View File

@@ -1,23 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/part.schema.json",
"title": "PART (C2S)",
"description": "Leave a channel",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "PART"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Part message"
}
},
"required": [
"command",
"to"
]
}

View File

@@ -1,22 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/ping.schema.json",
"title": "PING (C2S)",
"description": "Client keepalive",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "PING"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Ping token"
}
},
"required": [
"command"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/privmsg.schema.json",
"title": "PRIVMSG (C2S)",
"description": "Send message to channel or user",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "PRIVMSG"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Text lines"
}
},
"required": [
"command",
"to",
"body"
]
}

View File

@@ -1,33 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/pubkey.schema.json",
"title": "PUBKEY (C2S)",
"description": "Announce public signing key",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "PUBKEY"
},
"body": {
"type": "object",
"required": [
"alg",
"key"
],
"properties": {
"alg": {
"type": "string",
"description": "Key algorithm (e.g. ed25519)"
},
"key": {
"type": "string",
"description": "Base64-encoded public key"
}
}
}
},
"required": [
"command",
"body"
]
}

View File

@@ -1,22 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/quit.schema.json",
"title": "QUIT (C2S)",
"description": "Disconnect from server",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "QUIT"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Quit message"
}
},
"required": [
"command"
]
}

View File

@@ -1,23 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/topic.schema.json",
"title": "TOPIC (C2S)",
"description": "Set/query topic",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "TOPIC"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Topic lines"
}
},
"required": [
"command",
"to"
]
}

16
schema/commands/JOIN.json Normal file
View File

@@ -0,0 +1,16 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/commands/JOIN.json",
"title": "JOIN",
"description": "Join a channel. C2S: request to join. S2C: notification that a user joined. RFC 1459 §4.2.1.",
"$ref": "../message.json",
"properties": {
"command": { "const": "JOIN" },
"from": { "type": "string", "description": "Nick that joined (S2C)." },
"to": { "type": "string", "description": "Channel name.", "pattern": "^#[a-zA-Z0-9_-]+$" }
},
"required": ["command", "to"],
"examples": [
{ "command": "JOIN", "from": "alice", "to": "#general" }
]
}

34
schema/commands/KICK.json Normal file
View File

@@ -0,0 +1,34 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/commands/KICK.json",
"title": "KICK",
"description": "Kick a user from a channel. RFC 1459 §4.2.8.",
"$ref": "../message.json",
"properties": {
"command": { "const": "KICK" },
"from": {
"type": "string",
"description": "Nick that performed the kick."
},
"to": {
"type": "string",
"description": "Channel name.",
"pattern": "^#[a-zA-Z0-9_-]+$"
},
"params": {
"type": "array",
"items": { "type": "string" },
"description": "Kicked nick. e.g. [\"alice\"].",
"minItems": 1,
"maxItems": 1
},
"body": {
"type": "string",
"description": "Optional kick reason."
}
},
"required": ["command", "to", "params"],
"examples": [
{ "command": "KICK", "from": "op1", "to": "#general", "params": ["troll"], "body": "Behave" }
]
}

29
schema/commands/MODE.json Normal file
View File

@@ -0,0 +1,29 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/commands/MODE.json",
"title": "MODE",
"description": "Set or query channel/user modes. RFC 1459 §4.2.3.",
"$ref": "../message.json",
"properties": {
"command": { "const": "MODE" },
"from": {
"type": "string",
"description": "Nick that set the mode (S2C only)."
},
"to": {
"type": "string",
"description": "Channel name.",
"pattern": "^#[a-zA-Z0-9_-]+$"
},
"params": {
"type": "array",
"items": { "type": "string" },
"description": "Mode string and optional target nick. e.g. [\"+o\", \"alice\"].",
"examples": [["+o", "alice"], ["-m"], ["+v", "bob"]]
}
},
"required": ["command", "to", "params"],
"examples": [
{ "command": "MODE", "from": "op1", "to": "#general", "params": ["+o", "alice"] }
]
}

16
schema/commands/NICK.json Normal file
View File

@@ -0,0 +1,16 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/commands/NICK.json",
"title": "NICK",
"description": "Change nickname. C2S: request new nick. S2C: notification of nick change. RFC 1459 §4.1.2.",
"$ref": "../message.json",
"properties": {
"command": { "const": "NICK" },
"from": { "type": "string", "description": "Old nick (S2C)." },
"body": { "type": "string", "description": "New nick.", "minLength": 1, "maxLength": 32, "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$" }
},
"required": ["command", "body"],
"examples": [
{ "command": "NICK", "from": "oldnick", "body": "newnick" }
]
}

View File

@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/commands/NOTICE.json",
"title": "NOTICE",
"description": "Send a notice. Like PRIVMSG but must not trigger automatic replies. RFC 1459 §4.4.2.",
"$ref": "../message.json",
"properties": {
"command": { "const": "NOTICE" },
"from": { "type": "string" },
"to": { "type": "string", "description": "Target: #channel, nick, or * (global)." },
"body": { "type": "string", "description": "Notice text." }
},
"required": ["command", "to", "body"],
"examples": [
{ "command": "NOTICE", "from": "server.example.com", "to": "*", "body": "Server restarting in 5 minutes" }
]
}

17
schema/commands/PART.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/commands/PART.json",
"title": "PART",
"description": "Leave a channel. C2S: request to leave. S2C: notification that a user left. RFC 1459 §4.2.2.",
"$ref": "../message.json",
"properties": {
"command": { "const": "PART" },
"from": { "type": "string", "description": "Nick that left (S2C)." },
"to": { "type": "string", "description": "Channel name.", "pattern": "^#[a-zA-Z0-9_-]+$" },
"body": { "type": "string", "description": "Optional part reason." }
},
"required": ["command", "to"],
"examples": [
{ "command": "PART", "from": "alice", "to": "#general", "body": "later" }
]
}

18
schema/commands/PING.json Normal file
View File

@@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/commands/PING.json",
"title": "PING",
"description": "Keepalive. C2S or S2S. Server responds with PONG. RFC 1459 §4.6.2.",
"$ref": "../message.json",
"properties": {
"command": { "const": "PING" },
"body": {
"type": "string",
"description": "Opaque token to be echoed in PONG."
}
},
"required": ["command"],
"examples": [
{ "command": "PING", "body": "1707580000" }
]
}

22
schema/commands/PONG.json Normal file
View File

@@ -0,0 +1,22 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/commands/PONG.json",
"title": "PONG",
"description": "Keepalive response. S2C or S2S. RFC 1459 §4.6.3.",
"$ref": "../message.json",
"properties": {
"command": { "const": "PONG" },
"from": {
"type": "string",
"description": "Responding server name."
},
"body": {
"type": "string",
"description": "Echoed token from PING."
}
},
"required": ["command"],
"examples": [
{ "command": "PONG", "from": "server.example.com", "body": "1707580000" }
]
}

View File

@@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/commands/PRIVMSG.json",
"title": "PRIVMSG",
"description": "Send a message to a channel or user. C2S: client sends to server. S2C: server relays to recipients. RFC 1459 §4.4.1.",
"$ref": "../message.json",
"properties": {
"command": { "const": "PRIVMSG" },
"from": { "type": "string", "description": "Sender nick (set by server on relay)." },
"to": { "type": "string", "description": "Target: #channel or nick.", "examples": ["#general", "alice"] },
"body": { "type": "string", "description": "Message text.", "minLength": 1 }
},
"required": ["command", "to", "body"],
"examples": [
{ "command": "PRIVMSG", "from": "bob", "to": "#general", "body": "hello world" },
{ "command": "PRIVMSG", "from": "bob", "to": "alice", "body": "hey" }
]
}

16
schema/commands/QUIT.json Normal file
View File

@@ -0,0 +1,16 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/commands/QUIT.json",
"title": "QUIT",
"description": "User disconnected. S2C only. RFC 1459 §4.1.6.",
"$ref": "../message.json",
"properties": {
"command": { "const": "QUIT" },
"from": { "type": "string", "description": "Nick that quit." },
"body": { "type": "string", "description": "Optional quit reason." }
},
"required": ["command", "from"],
"examples": [
{ "command": "QUIT", "from": "alice", "body": "Connection reset" }
]
}

View File

@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/commands/TOPIC.json",
"title": "TOPIC",
"description": "Get or set channel topic. C2S: set topic (body present) or query (body absent). S2C: topic change notification. RFC 1459 §4.2.4.",
"$ref": "../message.json",
"properties": {
"command": { "const": "TOPIC" },
"from": { "type": "string", "description": "Nick that changed the topic (S2C)." },
"to": { "type": "string", "description": "Channel name.", "pattern": "^#[a-zA-Z0-9_-]+$" },
"body": { "type": "string", "description": "New topic text. Empty string clears the topic.", "maxLength": 512 }
},
"required": ["command", "to"],
"examples": [
{ "command": "TOPIC", "from": "alice", "to": "#general", "body": "Welcome to the chat" }
]
}

46
schema/message.json Normal file
View File

@@ -0,0 +1,46 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/message.json",
"title": "IRC Message Envelope",
"description": "Base envelope for all messages. Mirrors IRC wire format (RFC 1459/2812) encoded as JSON over HTTP. The 'command' field carries either an IRC command name (PRIVMSG, JOIN, etc.) or a three-digit numeric reply code (001, 353, 433, etc.).",
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Server-assigned message ID, monotonically increasing. Present on all server-originated messages."
},
"command": {
"type": "string",
"description": "IRC command name (PRIVMSG, JOIN, NICK, etc.) or three-digit numeric reply code (001, 353, 433, etc.).",
"examples": ["PRIVMSG", "JOIN", "001", "353", "433"]
},
"from": {
"type": "string",
"description": "Source — nick for user messages, server name for server messages. Equivalent to IRC prefix."
},
"to": {
"type": "string",
"description": "Target — channel (#name) or nick. Equivalent to first IRC parameter for most commands."
},
"params": {
"type": "array",
"items": { "type": "string" },
"description": "Additional parameters (used primarily by numeric replies). Equivalent to IRC middle parameters."
},
"body": {
"type": "string",
"description": "Message body / trailing parameter. Equivalent to IRC trailing parameter (after the colon)."
},
"ts": {
"type": "string",
"format": "date-time",
"description": "Server-assigned timestamp (ISO 8601). Not present in original IRC; added for HTTP transport."
},
"meta": {
"type": "object",
"description": "Extensible metadata (signatures, rich content hints, etc.). Not present in original IRC.",
"additionalProperties": true
}
},
"required": ["command"]
}

View File

@@ -1,67 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/message.schema.json",
"title": "Chat Message Envelope",
"description": "Base message envelope. Bodies MUST be objects or arrays (never raw strings) for deterministic canonicalization (RFC 8785 JCS) and signing.",
"type": "object",
"required": [
"command"
],
"properties": {
"id": {
"type": "string",
"format": "uuid",
"description": "Server-assigned UUID"
},
"ts": {
"type": "string",
"format": "date-time",
"description": "Server-assigned timestamp (ISO 8601)"
},
"command": {
"type": "string",
"description": "IRC command name or numeric reply code"
},
"from": {
"type": "string",
"description": "Sender nick or server name"
},
"to": {
"type": "string",
"description": "Destination: channel (#foo) or nick"
},
"params": {
"type": "array",
"items": {
"type": "string"
},
"description": "Additional IRC-style parameters"
},
"body": {
"oneOf": [
{
"type": "array"
},
{
"type": "object"
}
],
"description": "Message body (array or object, never raw string)"
},
"meta": {
"type": "object",
"description": "Extensible metadata",
"properties": {
"sig": {
"type": "string",
"description": "Cryptographic signature (base64)"
},
"alg": {
"type": "string",
"description": "Signature algorithm (e.g. ed25519)"
}
}
}
},
"additionalProperties": false
}

20
schema/numerics/001.json Normal file
View File

@@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/001.json",
"title": "001 RPL_WELCOME",
"description": "Welcome message sent after successful session creation. RFC 2812 §5.1.",
"$ref": "../message.json",
"properties": {
"command": { "const": "001" },
"to": { "type": "string", "description": "Target nick." },
"body": {
"type": "array",
"items": { "type": "string" },
"description": "Welcome text lines."
}
},
"required": ["command", "to", "body"],
"examples": [
{ "command": "001", "to": "alice", "body": ["Welcome to the network, alice"] }
]
}

20
schema/numerics/002.json Normal file
View File

@@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/002.json",
"title": "002 RPL_YOURHOST",
"description": "Server host info sent after session creation. RFC 2812 §5.1.",
"$ref": "../message.json",
"properties": {
"command": { "const": "002" },
"to": { "type": "string" },
"body": {
"type": "array",
"items": { "type": "string" },
"description": "Host info lines."
}
},
"required": ["command", "to", "body"],
"examples": [
{ "command": "002", "to": "alice", "body": ["Your host is chat.example.com, running version 0.1.0"] }
]
}

36
schema/numerics/003.json Normal file
View File

@@ -0,0 +1,36 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/003.json",
"title": "003 RPL_CREATED",
"description": "Server creation date. RFC 2812 \u00a75.1.",
"$ref": "../message.json",
"properties": {
"command": {
"const": "003"
},
"to": {
"type": "string"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Text lines."
}
},
"required": [
"command",
"to",
"body"
],
"examples": [
{
"command": "003",
"to": "alice",
"body": [
"This server was created 2026-02-01"
]
}
]
}

39
schema/numerics/004.json Normal file
View File

@@ -0,0 +1,39 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/004.json",
"title": "004 RPL_MYINFO",
"description": "Server info (name, version, available modes). RFC 2812 \u00a75.1.",
"$ref": "../message.json",
"properties": {
"command": {
"const": "004"
},
"to": {
"type": "string"
},
"params": {
"type": "array",
"items": {
"type": "string"
},
"description": "[server_name, version, user_modes, channel_modes]."
}
},
"required": [
"command",
"to",
"params"
],
"examples": [
{
"command": "004",
"to": "alice",
"params": [
"chat.example.com",
"0.1.0",
"o",
"imnst+ov"
]
}
]
}

47
schema/numerics/322.json Normal file
View File

@@ -0,0 +1,47 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/322.json",
"title": "322 RPL_LIST",
"description": "Channel list entry. One per channel in response to LIST. RFC 1459 \u00a76.2.",
"$ref": "../message.json",
"properties": {
"command": {
"const": "322"
},
"to": {
"type": "string"
},
"params": {
"type": "array",
"items": {
"type": "string"
},
"description": "[channel, visible_count]."
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Channel topic."
}
},
"required": [
"command",
"to",
"params"
],
"examples": [
{
"command": "322",
"to": "alice",
"params": [
"#general",
"12"
],
"body": [
"General discussion"
]
}
]
}

13
schema/numerics/323.json Normal file
View File

@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/323.json",
"title": "323 RPL_LISTEND",
"description": "End of channel list. RFC 1459 §6.2.",
"$ref": "../message.json",
"properties": {
"command": { "const": "323" },
"to": { "type": "string" },
"body": { "const": "End of /LIST" }
},
"required": ["command", "to"]
}

47
schema/numerics/332.json Normal file
View File

@@ -0,0 +1,47 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/332.json",
"title": "332 RPL_TOPIC",
"description": "Channel topic (sent on JOIN or TOPIC query). RFC 1459 \u00a76.2.",
"$ref": "../message.json",
"properties": {
"command": {
"const": "332"
},
"to": {
"type": "string"
},
"params": {
"type": "array",
"items": {
"type": "string"
},
"description": "[channel]."
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Topic text."
}
},
"required": [
"command",
"to",
"params",
"body"
],
"examples": [
{
"command": "332",
"to": "alice",
"params": [
"#general"
],
"body": [
"Welcome to the chat"
]
}
]
}

48
schema/numerics/353.json Normal file
View File

@@ -0,0 +1,48 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/353.json",
"title": "353 RPL_NAMREPLY",
"description": "Channel member list. Sent on JOIN or NAMES query. RFC 1459 \u00a76.2.",
"$ref": "../message.json",
"properties": {
"command": {
"const": "353"
},
"to": {
"type": "string"
},
"params": {
"type": "array",
"items": {
"type": "string"
},
"description": "[channel_type, channel]. channel_type: = (public), * (private), @ (secret)."
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Space-separated list of nicks. Prefixed with @ for ops, + for voiced."
}
},
"required": [
"command",
"to",
"params",
"body"
],
"examples": [
{
"command": "353",
"to": "alice",
"params": [
"=",
"#general"
],
"body": [
"@op1 alice bob +voiced1"
]
}
]
}

18
schema/numerics/366.json Normal file
View File

@@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/366.json",
"title": "366 RPL_ENDOFNAMES",
"description": "End of NAMES list. RFC 1459 §6.2.",
"$ref": "../message.json",
"properties": {
"command": { "const": "366" },
"to": { "type": "string" },
"params": {
"type": "array",
"items": { "type": "string" },
"description": "[channel]."
},
"body": { "const": "End of /NAMES list" }
},
"required": ["command", "to", "params"]
}

36
schema/numerics/372.json Normal file
View File

@@ -0,0 +1,36 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/372.json",
"title": "372 RPL_MOTD",
"description": "MOTD line. One message per line of the MOTD. RFC 2812 \u00a75.1.",
"$ref": "../message.json",
"properties": {
"command": {
"const": "372"
},
"to": {
"type": "string"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "MOTD line text (prefixed with '- ')."
}
},
"required": [
"command",
"to",
"body"
],
"examples": [
{
"command": "372",
"to": "alice",
"body": [
"- Welcome to our server!"
]
}
]
}

26
schema/numerics/375.json Normal file
View File

@@ -0,0 +1,26 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/375.json",
"title": "375 RPL_MOTDSTART",
"description": "Start of MOTD. RFC 2812 \u00a75.1.",
"$ref": "../message.json",
"properties": {
"command": {
"const": "375"
},
"to": {
"type": "string"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Text lines."
}
},
"required": [
"command",
"to"
]
}

13
schema/numerics/376.json Normal file
View File

@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/376.json",
"title": "376 RPL_ENDOFMOTD",
"description": "End of MOTD. RFC 2812 §5.1.",
"$ref": "../message.json",
"properties": {
"command": { "const": "376" },
"to": { "type": "string" },
"body": { "const": "End of /MOTD command" }
},
"required": ["command", "to"]
}

21
schema/numerics/401.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/401.json",
"title": "401 ERR_NOSUCHNICK",
"description": "No such nick/channel. RFC 1459 §6.1.",
"$ref": "../message.json",
"properties": {
"command": { "const": "401" },
"to": { "type": "string" },
"params": {
"type": "array",
"items": { "type": "string" },
"description": "[target_nick]."
},
"body": { "const": "No such nick/channel" }
},
"required": ["command", "to", "params"],
"examples": [
{ "command": "401", "to": "alice", "params": ["bob"], "body": "No such nick/channel" }
]
}

21
schema/numerics/403.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/403.json",
"title": "403 ERR_NOSUCHCHANNEL",
"description": "No such channel. RFC 1459 §6.1.",
"$ref": "../message.json",
"properties": {
"command": { "const": "403" },
"to": { "type": "string" },
"params": {
"type": "array",
"items": { "type": "string" },
"description": "[channel_name]."
},
"body": { "const": "No such channel" }
},
"required": ["command", "to", "params"],
"examples": [
{ "command": "403", "to": "alice", "params": ["#nonexistent"], "body": "No such channel" }
]
}

21
schema/numerics/433.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/433.json",
"title": "433 ERR_NICKNAMEINUSE",
"description": "Nickname is already in use. RFC 1459 §6.1.",
"$ref": "../message.json",
"properties": {
"command": { "const": "433" },
"to": { "type": "string" },
"params": {
"type": "array",
"items": { "type": "string" },
"description": "[requested_nick]."
},
"body": { "const": "Nickname is already in use" }
},
"required": ["command", "to", "params"],
"examples": [
{ "command": "433", "to": "*", "params": ["alice"], "body": "Nickname is already in use" }
]
}

18
schema/numerics/442.json Normal file
View File

@@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/442.json",
"title": "442 ERR_NOTONCHANNEL",
"description": "You're not on that channel. RFC 1459 §6.1.",
"$ref": "../message.json",
"properties": {
"command": { "const": "442" },
"to": { "type": "string" },
"params": {
"type": "array",
"items": { "type": "string" },
"description": "[channel]."
},
"body": { "const": "You're not on that channel" }
},
"required": ["command", "to", "params"]
}

18
schema/numerics/482.json Normal file
View File

@@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/482.json",
"title": "482 ERR_CHANOPRIVSNEEDED",
"description": "You're not channel operator. RFC 1459 §6.1.",
"$ref": "../message.json",
"properties": {
"command": { "const": "482" },
"to": { "type": "string" },
"params": {
"type": "array",
"items": { "type": "string" },
"description": "[channel]."
},
"body": { "const": "You're not channel operator" }
},
"required": ["command", "to", "params"]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/001.schema.json",
"title": "001 RPL_WELCOME (S2C)",
"description": "Welcome message after registration",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "001"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Response lines"
}
},
"required": [
"command",
"to",
"body"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/002.schema.json",
"title": "002 RPL_YOURHOST (S2C)",
"description": "Server host information",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "002"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Response lines"
}
},
"required": [
"command",
"to",
"body"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/322.schema.json",
"title": "322 RPL_LIST (S2C)",
"description": "Channel list entry",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "322"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Response lines"
}
},
"required": [
"command",
"to",
"body"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/353.schema.json",
"title": "353 RPL_NAMREPLY (S2C)",
"description": "Names list for a channel",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "353"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Response lines"
}
},
"required": [
"command",
"to",
"body"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/366.schema.json",
"title": "366 RPL_ENDOFNAMES (S2C)",
"description": "End of names list",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "366"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Response lines"
}
},
"required": [
"command",
"to",
"body"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/372.schema.json",
"title": "372 RPL_MOTD (S2C)",
"description": "Message of the day line",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "372"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Response lines"
}
},
"required": [
"command",
"to",
"body"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/375.schema.json",
"title": "375 RPL_MOTDSTART (S2C)",
"description": "Start of MOTD",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "375"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Response lines"
}
},
"required": [
"command",
"to",
"body"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/376.schema.json",
"title": "376 RPL_ENDOFMOTD (S2C)",
"description": "End of MOTD",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "376"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Response lines"
}
},
"required": [
"command",
"to",
"body"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/401.schema.json",
"title": "401 ERR_NOSUCHNICK (S2C)",
"description": "No such nick or channel",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "401"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Response lines"
}
},
"required": [
"command",
"to",
"body"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/403.schema.json",
"title": "403 ERR_NOSUCHCHANNEL (S2C)",
"description": "No such channel",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "403"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Response lines"
}
},
"required": [
"command",
"to",
"body"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/433.schema.json",
"title": "433 ERR_NICKNAMEINUSE (S2C)",
"description": "Nickname already in use",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "433"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Response lines"
}
},
"required": [
"command",
"to",
"body"
]
}

View File

@@ -1,23 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/error.schema.json",
"title": "ERROR (S2C)",
"description": "Server error",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "ERROR"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Error lines"
}
},
"required": [
"command",
"body"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/join.schema.json",
"title": "JOIN (S2C)",
"description": "User joined a channel",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "JOIN"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Not used"
}
},
"required": [
"command",
"from",
"to"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/kick.schema.json",
"title": "KICK (S2C)",
"description": "User kicked from channel",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "KICK"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Kick reason"
}
},
"required": [
"command",
"from",
"to"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/mode.schema.json",
"title": "MODE (S2C)",
"description": "Mode change notification",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "MODE"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Mode params"
}
},
"required": [
"command",
"from",
"to"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/nick.schema.json",
"title": "NICK (S2C)",
"description": "User changed nick",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "NICK"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Not used"
}
},
"required": [
"command",
"from",
"to"
]
}

View File

@@ -1,23 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/notice.schema.json",
"title": "NOTICE (S2C)",
"description": "Server or user notice",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "NOTICE"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Text lines"
}
},
"required": [
"command",
"body"
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/part.schema.json",
"title": "PART (S2C)",
"description": "User left a channel",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "PART"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Part message"
}
},
"required": [
"command",
"from",
"to"
]
}

View File

@@ -1,22 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/pong.schema.json",
"title": "PONG (S2C)",
"description": "Server keepalive response",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "PONG"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Pong token"
}
},
"required": [
"command"
]
}

View File

@@ -1,25 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/privmsg.schema.json",
"title": "PRIVMSG (S2C)",
"description": "Relayed message from a user",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "PRIVMSG"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Text lines"
}
},
"required": [
"command",
"from",
"to",
"body"
]
}

View File

@@ -1,34 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/pubkey.schema.json",
"title": "PUBKEY (S2C)",
"description": "Relayed public key announcement",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "PUBKEY"
},
"body": {
"type": "object",
"required": [
"alg",
"key"
],
"properties": {
"alg": {
"type": "string",
"description": "Key algorithm (e.g. ed25519)"
},
"key": {
"type": "string",
"description": "Base64-encoded public key"
}
}
}
},
"required": [
"command",
"from",
"body"
]
}

View File

@@ -1,23 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/quit.schema.json",
"title": "QUIT (S2C)",
"description": "User disconnected",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "QUIT"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Quit message"
}
},
"required": [
"command",
"from"
]
}

View File

@@ -1,25 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/topic.schema.json",
"title": "TOPIC (S2C)",
"description": "Topic change notification",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "TOPIC"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Topic lines"
}
},
"required": [
"command",
"from",
"to",
"body"
]
}

View File

@@ -1,20 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2s/link.schema.json",
"title": "LINK (S2S)",
"description": "Establish server link",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "LINK"
},
"body": {
"type": "object",
"description": "Link parameters"
}
},
"required": [
"command",
"body"
]
}

View File

@@ -1,22 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2s/ping.schema.json",
"title": "PING (S2S)",
"description": "Server-to-server keepalive",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "PING"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Ping token"
}
},
"required": [
"command"
]
}

View File

@@ -1,22 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2s/pong.schema.json",
"title": "PONG (S2S)",
"description": "Server-to-server keepalive response",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "PONG"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Pong token"
}
},
"required": [
"command"
]
}

View File

@@ -1,20 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2s/relay.schema.json",
"title": "RELAY (S2S)",
"description": "Relay message to linked server",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "RELAY"
},
"body": {
"type": "object",
"description": "Wrapped message"
}
},
"required": [
"command",
"body"
]
}

View File

@@ -1,20 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2s/sync.schema.json",
"title": "SYNC (S2S)",
"description": "Synchronize state between servers",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "SYNC"
},
"body": {
"type": "object",
"description": "State data"
}
},
"required": [
"command",
"body"
]
}

View File

@@ -1,22 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2s/unlink.schema.json",
"title": "UNLINK (S2S)",
"description": "Tear down server link",
"$ref": "../message.schema.json",
"properties": {
"command": {
"const": "UNLINK"
},
"body": {
"type": "array",
"items": {
"type": "string"
},
"description": "Unlink reason"
}
},
"required": [
"command"
]
}