Files
chat/README.md

330 lines
11 KiB
Markdown

# chat
A modern IRC-inspired chat server written in Go. Decouples session state from
transport connections, enabling mobile-friendly persistent sessions over HTTP.
The **HTTP API is the primary interface**. It's designed to be simple enough
that writing a terminal IRC-style client against it is straightforward — just
`curl` and `jq` get you surprisingly far. The server also ships an embedded
web client as a convenience/reference implementation, but the API comes first.
## Motivation
IRC is in decline because session state is tied to the TCP connection. In a
mobile-first world, that's a nonstarter. Not everyone wants to run a bouncer
or pay for IRCCloud.
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
- Provides IRC-like semantics: channels, nicks, topics, modes
- Uses structured JSON messages with arbitrary extensibility
## Architecture
### Transport: HTTP only
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.
- **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)
The entire read/write loop for a client is two endpoints. Everything else is
channel management and history.
### 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
#### Messages
Every message is a structured JSON object:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"ts": "2026-02-09T20:00:00.000Z",
"from": "nick",
"to": "#channel",
"type": "message",
"body": "Hello, world!",
"meta": {}
}
```
Fields:
- `id` — Server-assigned UUID, globally unique
- `ts` — Server-assigned timestamp (ISO 8601)
- `from` — Sender nick
- `to` — Destination: channel name (`#foo`) or nick (for DMs)
- `type` — Message type: `message`, `action`, `notice`, `join`, `part`, `quit`,
`topic`, `mode`, `nick`, `system`
- `body` — Message content (UTF-8 text)
- `meta` — Arbitrary extensible metadata (JSON object). Can carry:
- Cryptographic signatures
- Rich content hints (URLs, embeds)
- Client-specific extensions
- Reactions, edits, threading references
### API Endpoints
All endpoints accept and return `application/json`. Authenticated endpoints
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
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)
4. `POST /api/v1/messages` — send to `"#channel"` or `"nick"`
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 \
-d '{"nick":"alice"}' | jq -r .token)
# Join a channel
curl -s -X POST http://localhost:8080/api/v1/channels/join \
-H "Authorization: Bearer $TOKEN" \
-d '{"channel":"#general"}'
# Send a message
curl -s -X POST http://localhost:8080/api/v1/messages \
-H "Authorization: Bearer $TOKEN" \
-d '{"to":"#general","content":"hello world"}'
# Poll for messages (long-poll)
curl -s http://localhost:8080/api/v1/messages?after=0 \
-H "Authorization: Bearer $TOKEN"
```
#### Registration
```
POST /api/v1/register — Create account { "nick": "..." } → { id, nick, token }
```
#### State
```
GET /api/v1/state — User state: nick, id, and list of joined channels
Replaces separate /me and /channels endpoints
```
#### Messages (unified stream)
```
GET /api/v1/messages — Single message stream (long-poll supported)
All message types: channel, DM, notices, events
Query params: ?after=<message-id>&timeout=30
POST /api/v1/messages — Send a message
Body: { "to": "#channel" or "nick", "content": "..." }
```
#### History
```
GET /api/v1/history — Fetch history for a target (channel or DM)
Query params: ?target=#channel&before=<id>&limit=50
For DMs: ?target=nick&before=<id>&limit=50
```
#### Channels
```
GET /api/v1/channels/all — List all server channels
POST /api/v1/channels/join — Join a channel { "channel": "#name" }
DELETE /api/v1/channels/{name} — Part (leave) a channel
GET /api/v1/channels/{name}/members — Channel member list
```
#### Server Info
```
GET /api/v1/server — Server info (name, MOTD)
GET /.well-known/healthcheck.json — Health check
```
### Federation (Server-to-Server)
Servers can link to form a network, similar to IRC server linking:
```
POST /api/v1/federation/link — Establish server link (mutual auth via shared key)
POST /api/v1/federation/relay — Relay messages between linked servers
GET /api/v1/federation/status — Link status
```
Federation uses the same HTTP+JSON transport. Messages are relayed between
servers so users on different servers can share channels.
### Channel Modes
Inspired by IRC but simplified:
| Mode | Meaning |
|------|---------|
| `+i` | Invite-only |
| `+m` | Moderated (only voiced users can send) |
| `+s` | Secret (hidden from channel list) |
| `+t` | Topic locked (only ops can change) |
| `+n` | No external messages |
User channel modes: `+o` (operator), `+v` (voice)
### Configuration
Via environment variables (Viper), following gohttpserver conventions:
| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `8080` | Listen port |
| `DBURL` | `""` | SQLite/Postgres connection string |
| `DEBUG` | `false` | Debug mode |
| `MAX_HISTORY` | `10000` | Max messages per channel history |
| `SESSION_TIMEOUT` | `86400` | Session idle timeout (seconds) |
| `MAX_MESSAGE_SIZE` | `4096` | Max message body size (bytes) |
| `MOTD` | `""` | Message of the day |
| `SERVER_NAME` | hostname | Server display name |
| `FEDERATION_KEY` | `""` | Shared key for server linking |
### Storage
SQLite by default (single-file, zero-config), with Postgres support for
larger deployments. Tables:
- `users` — accounts and auth tokens
- `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
- `server_links` — federation peer configuration
### Project Structure
Following [gohttpserver CONVENTIONS.md](https://git.eeqj.de/sneak/gohttpserver/src/branch/main/CONVENTIONS.md):
```
chat/
├── cmd/
│ └── chat/
│ └── main.go
├── internal/
│ ├── config/
│ │ └── config.go
│ ├── database/
│ │ └── database.go
│ ├── globals/
│ │ └── globals.go
│ ├── handlers/
│ │ ├── handlers.go
│ │ ├── auth.go
│ │ ├── channels.go
│ │ ├── federation.go
│ │ ├── healthcheck.go
│ │ ├── messages.go
│ │ └── users.go
│ ├── healthcheck/
│ │ └── healthcheck.go
│ ├── logger/
│ │ └── logger.go
│ ├── middleware/
│ │ └── middleware.go
│ ├── models/
│ │ ├── channel.go
│ │ ├── message.go
│ │ └── user.go
│ ├── queue/
│ │ └── queue.go
│ └── server/
│ ├── server.go
│ ├── http.go
│ └── routes.go
├── go.mod
├── go.sum
├── Makefile
├── Dockerfile
├── CONVENTIONS.md → (copy from gohttpserver)
└── README.md
```
### Required Libraries
Per gohttpserver conventions:
| Purpose | Library |
|---------|---------|
| DI | `go.uber.org/fx` |
| Router | `github.com/go-chi/chi` |
| Logging | `log/slog` (stdlib) |
| Config | `github.com/spf13/viper` |
| Env | `github.com/joho/godotenv/autoload` |
| CORS | `github.com/go-chi/cors` |
| 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. It
demonstrates the API and provides a quick way to test the server in a browser.
The primary intended clients are IRC-style terminal applications and bots
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. **HTTP is the only transport** — no WebSockets, no raw TCP, no protocol
negotiation. HTTP is universal, proxy-friendly, and works everywhere.
3. **Server holds state** — clients are stateless. Reconnect, switch devices,
lose connectivity — your messages are waiting.
4. **Structured messages** — JSON with extensible metadata. Enables signatures,
rich content, client extensions without protocol changes.
5. **Simple deployment** — single binary, SQLite default, zero mandatory
external dependencies.
6. **No eternal logs** — history rotates. Chat should be ephemeral by default.
7. **Federation optional** — single server works standalone. Linking is opt-in.
## Status
**Implementation in progress.** Core API is functional with SQLite storage and
embedded web client.
## License
MIT