Files
chat/internal/db/schema/001_initial.sql
clawbot db3d23c224
All checks were successful
check / check (push) Successful in 6s
feat: add username/hostname support with IRC hostmask format (#82)
## Summary

Adds username and hostname support to sessions, enabling standard IRC hostmask format (`nick!user@host`) for WHOIS, WHO, and future `+b` ban matching.

closes #81

## Changes

### Schema (`001_initial.sql`)
- Added `username TEXT NOT NULL DEFAULT ''` and `hostname TEXT NOT NULL DEFAULT ''` columns to the `sessions` table

### Database layer (`internal/db/`)
- `CreateSession` now accepts `username` and `hostname` parameters; username defaults to nick if empty
- `RegisterUser` now accepts `username` and `hostname` parameters
- New `SessionHostInfo` type and `GetSessionHostInfo` query to retrieve username/hostname for a session
- `MemberInfo` now includes `Username` and `Hostname` fields
- `ChannelMembers` query updated to return username/hostname
- New `FormatHostmask(nick, username, hostname)` helper that produces `nick!user@host` format
- New `Hostmask()` method on `MemberInfo`

### Handler layer (`internal/handlers/`)
- Session creation (`POST /api/v1/session`) accepts optional `username` field; resolves hostname via reverse DNS of connecting client IP (respects `X-Forwarded-For` and `X-Real-IP` headers)
- Registration (`POST /api/v1/register`) accepts optional `username` field with the same hostname resolution
- Username validation regex: `^[a-zA-Z0-9_\-\[\]\\^{}|` + "\`" + `]{1,32}$`
- WHOIS (`311 RPL_WHOISUSER`) now returns the real username and hostname instead of nick/servername
- WHO (`352 RPL_WHOREPLY`) now returns the real username and hostname instead of nick/servername
- Extracted `validateHashcash` and `resolveUsername` helpers to keep functions under the linter's `funlen` limit
- Extracted `executeRegister` helper for the same reason
- Reverse DNS uses `(*net.Resolver).LookupAddr` with a 3-second timeout context

### Tests
- `TestCreateSessionWithUserHost` — verifies username/hostname are stored and retrievable
- `TestCreateSessionDefaultUsername` — verifies empty username defaults to nick
- `TestGetSessionHostInfoNotFound` — verifies error on nonexistent session
- `TestFormatHostmask` — verifies `nick!user@host` formatting
- `TestFormatHostmaskDefaults` — verifies fallback when username/hostname empty
- `TestMemberInfoHostmask` — verifies `Hostmask()` method on `MemberInfo`
- `TestChannelMembersIncludeUserHost` — verifies `ChannelMembers` returns username/hostname
- `TestRegisterUserWithUserHost` — verifies registration stores username/hostname
- `TestRegisterUserDefaultUsername` — verifies registration defaults username to nick
- `TestWhoisShowsHostInfo` — integration test verifying WHOIS returns the correct username
- `TestWhoShowsHostInfo` — integration test verifying WHO returns the correct username
- `TestSessionUsernameDefault` — integration test verifying default username in WHOIS
- All existing tests updated for new `CreateSession`/`RegisterUser` signatures

### README
- New "Hostmask" section documenting the `nick!user@host` format
- Updated session creation and registration API docs with the new `username` field
- Updated WHOIS/WHO numeric examples to show real username/hostname
- Updated sessions schema table with new columns

## Docker build

`docker build .` passes cleanly (lint, format, tests, build).

Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Co-authored-by: clawbot <clawbot@eeqj.de>
Reviewed-on: #82
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-20 06:53:35 +01:00

88 lines
3.3 KiB
SQL

-- Chat server schema (pre-1.0 consolidated)
PRAGMA foreign_keys = ON;
-- Sessions: each session is a user identity (nick + optional password + signing key)
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uuid TEXT NOT NULL UNIQUE,
nick TEXT NOT NULL UNIQUE,
username TEXT NOT NULL DEFAULT '',
hostname TEXT NOT NULL DEFAULT '',
ip TEXT NOT NULL DEFAULT '',
is_oper INTEGER NOT NULL DEFAULT 0,
password_hash TEXT NOT NULL DEFAULT '',
signing_key TEXT NOT NULL DEFAULT '',
away_message TEXT NOT NULL DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_sessions_uuid ON sessions(uuid);
-- Clients: each session can have multiple connected clients
CREATE TABLE IF NOT EXISTS clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uuid TEXT NOT NULL UNIQUE,
session_id INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
token TEXT NOT NULL UNIQUE,
ip TEXT NOT NULL DEFAULT '',
hostname TEXT NOT NULL DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_clients_token ON clients(token);
CREATE INDEX IF NOT EXISTS idx_clients_session ON clients(session_id);
-- Channels
CREATE TABLE IF NOT EXISTS channels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
topic TEXT NOT NULL DEFAULT '',
topic_set_by TEXT NOT NULL DEFAULT '',
topic_set_at DATETIME,
hashcash_bits INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Channel members
CREATE TABLE IF NOT EXISTS channel_members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
session_id INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
joined_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(channel_id, session_id)
);
-- Messages: IRC envelope format
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uuid TEXT NOT NULL UNIQUE,
command TEXT NOT NULL DEFAULT 'PRIVMSG',
msg_from TEXT NOT NULL DEFAULT '',
msg_to TEXT NOT NULL DEFAULT '',
params TEXT NOT NULL DEFAULT '[]',
body TEXT NOT NULL DEFAULT '[]',
meta TEXT NOT NULL DEFAULT '{}',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_messages_to_id ON messages(msg_to, id);
CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at);
-- Spent hashcash tokens for replay prevention (1-year TTL)
CREATE TABLE IF NOT EXISTS spent_hashcash (
id INTEGER PRIMARY KEY AUTOINCREMENT,
stamp_hash TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_spent_hashcash_created ON spent_hashcash(created_at);
-- Per-client message queues for fan-out delivery
CREATE TABLE IF NOT EXISTS client_queues (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(client_id, message_id)
);
CREATE INDEX IF NOT EXISTS idx_client_queues_client ON client_queues(client_id, id);