Some checks failed
check / check (push) Failing after 1m31s
## Summary Implements the second tier of IRC channel features as described in [#86](#86). ## Features ### 1. Ban System (+b) - `channel_bans` table with mask, set_by, created_at - Add/remove/list bans via MODE +b/-b - Wildcard matching (`*!*@*.example.com`, `badnick!*@*`, etc.) - Ban enforcement on both JOIN and PRIVMSG - RPL_BANLIST (367) / RPL_ENDOFBANLIST (368) for ban listing ### 2. Invite-Only (+i) - `is_invite_only` column on channels table - INVITE command: operators can invite users - `channel_invites` table tracks pending invites - Invites consumed on successful JOIN - ERR_INVITEONLYCHAN (473) for uninvited JOIN attempts ### 3. Secret (+s) - `is_secret` column on channels table - Secret channels hidden from LIST for non-members - Secret channels hidden from WHOIS channel list for non-members ### 4. Channel Key (+k) - `channel_key` column on channels table - MODE +k sets key, MODE -k clears it - Key required on JOIN (`JOIN #channel key`) - ERR_BADCHANNELKEY (475) for wrong/missing key ### 5. User Limit (+l) - `user_limit` column on channels table (0 = no limit) - MODE +l sets limit, MODE -l removes it - ERR_CHANNELISFULL (471) when limit reached ## ISUPPORT Changes - CHANMODES updated to `b,k,Hl,imnst` - RPL_MYINFO modes updated to `ikmnostl` ## Tests ### Database-level tests: - Wildcard matching (10 patterns) - Ban CRUD operations - Session ban checking - Invite-only flag toggle - Invite CRUD + clearing - Secret channel filtering (LIST and WHOIS) - Channel key set/get/clear - User limit set/get/clear ### Handler-level tests: - Ban add/remove/list via MODE - Ban blocks JOIN - Ban blocks PRIVMSG - Invite-only JOIN rejection + INVITE acceptance - Secret channel hidden from LIST - Channel key required on JOIN - User limit enforcement - Mode string includes new modes - ISUPPORT updated CHANMODES - Non-operators cannot set any Tier 2 modes ## Schema Changes - Added `is_invite_only`, `is_secret`, `channel_key`, `user_limit` to `channels` table - Added `channel_bans` table - Added `channel_invites` table - All changes in `001_initial.sql` (pre-1.0.0 repo) closes #86 Co-authored-by: user <user@Mac.lan guest wan> Reviewed-on: #92 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
118 lines
4.6 KiB
SQL
118 lines
4.6 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,
|
|
is_moderated INTEGER NOT NULL DEFAULT 0,
|
|
is_topic_locked INTEGER NOT NULL DEFAULT 1,
|
|
is_invite_only INTEGER NOT NULL DEFAULT 0,
|
|
is_secret INTEGER NOT NULL DEFAULT 0,
|
|
channel_key TEXT NOT NULL DEFAULT '',
|
|
user_limit INTEGER NOT NULL DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- Channel bans
|
|
CREATE TABLE IF NOT EXISTS channel_bans (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
mask TEXT NOT NULL,
|
|
set_by TEXT NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(channel_id, mask)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_channel_bans_channel ON channel_bans(channel_id);
|
|
|
|
-- Channel invites (in-memory would be simpler but DB survives restarts)
|
|
CREATE TABLE IF NOT EXISTS channel_invites (
|
|
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,
|
|
invited_by TEXT NOT NULL DEFAULT '',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(channel_id, session_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_channel_invites_channel ON channel_invites(channel_id);
|
|
|
|
-- 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,
|
|
is_operator INTEGER NOT NULL DEFAULT 0,
|
|
is_voiced INTEGER NOT NULL DEFAULT 0,
|
|
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);
|