Initial spec: HTTP-based IRC replacement
This commit is contained in:
272
README.md
Normal file
272
README.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# chat
|
||||||
|
|
||||||
|
A modern IRC-inspired chat server written in Go. Decouples session state from
|
||||||
|
transport connections, enabling mobile-friendly persistent sessions over HTTP.
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
- Delivers messages over HTTP (JSON-RPC style)
|
||||||
|
- 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 poll for new messages via `GET` with long-polling
|
||||||
|
support (server holds the connection open until messages arrive or timeout)
|
||||||
|
- **Client sending**: Clients send messages/commands via `POST`
|
||||||
|
- **Server federation**: Servers exchange messages via HTTP to enable multi-server
|
||||||
|
networks (like IRC server linking)
|
||||||
|
|
||||||
|
### 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`.
|
||||||
|
|
||||||
|
#### Authentication
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/register — Create account (nick, password) → token
|
||||||
|
POST /api/v1/login — Authenticate → token
|
||||||
|
POST /api/v1/logout — Invalidate token
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Session & Messages
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/messages — Retrieve queued messages (long-poll supported)
|
||||||
|
Query params: ?after=<message-id>&timeout=30
|
||||||
|
POST /api/v1/messages — Send a message or command
|
||||||
|
GET /api/v1/history — Retrieve channel/DM history
|
||||||
|
Query params: ?target=#channel&before=<id>&limit=50
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Channels
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/channels — List joined channels
|
||||||
|
POST /api/v1/channels/join — Join a channel
|
||||||
|
POST /api/v1/channels/part — Leave a channel
|
||||||
|
GET /api/v1/channels/{name} — Channel info (topic, members, modes)
|
||||||
|
POST /api/v1/channels/{name}/topic — Set channel topic
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Users
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/users/me — Current user info
|
||||||
|
POST /api/v1/users/nick — Change nick
|
||||||
|
GET /api/v1/users/{nick} — User info (online status, idle time)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Server Info
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/server — Server info (name, version, MOTD, user count)
|
||||||
|
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` |
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
|
||||||
|
1. **HTTP is the only transport** — no WebSockets, no raw TCP, no protocol
|
||||||
|
negotiation. HTTP is universal, proxy-friendly, and works everywhere.
|
||||||
|
2. **Server holds state** — clients are stateless. Reconnect, switch devices,
|
||||||
|
lose connectivity — your messages are waiting.
|
||||||
|
3. **Structured messages** — JSON with extensible metadata. Enables signatures,
|
||||||
|
rich content, client extensions without protocol changes.
|
||||||
|
4. **Simple deployment** — single binary, SQLite default, zero mandatory
|
||||||
|
external dependencies.
|
||||||
|
5. **No eternal logs** — history rotates. Chat should be ephemeral by default.
|
||||||
|
6. **Federation optional** — single server works standalone. Linking is opt-in.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**Design phase.** This README is the spec. Implementation has not started.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
Reference in New Issue
Block a user