Join Gate (account age, avatar, advertising and username filters, bot checks), raid detection with invite pausing, anti-nuke quarantine of admins who mass delete, manual and automatic lockdowns, a decaying heat score against spam, role and channel snapshots with restore, and alert DMs to the owner and trusted users. Everything that weakens protection is limited to the owner and the trusted list. Co-Authored-By: Claude Opus 5.5 <[email protected]>
390 lines
15 KiB
SQL
390 lines
15 KiB
SQL
-- PostgreSQL schema for Velox
|
|
-- Use BIGINT for Discord snowflakes, INT for smaller counters, BOOLEAN properly.
|
|
|
|
CREATE TABLE IF NOT EXISTS levels (
|
|
guild BIGINT NOT NULL,
|
|
user_id BIGINT NOT NULL,
|
|
level INT NOT NULL DEFAULT 0,
|
|
xp BIGINT NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (guild, user_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS twitch (
|
|
twitch_user TEXT NOT NULL,
|
|
guild_id BIGINT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'not live',
|
|
PRIMARY KEY (twitch_user, guild_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS levelsettings (
|
|
guild_id BIGINT PRIMARY KEY,
|
|
levelsys BOOLEAN NOT NULL DEFAULT FALSE,
|
|
message TEXT
|
|
);
|
|
|
|
-- Role rewards per level (supports multiple rewards per guild)
|
|
CREATE TABLE IF NOT EXISTS levelrewards (
|
|
guild_id BIGINT NOT NULL,
|
|
levelreq INT NOT NULL,
|
|
role BIGINT NOT NULL,
|
|
PRIMARY KEY (guild_id, levelreq)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS welcome (
|
|
guild_id BIGINT PRIMARY KEY,
|
|
welcome_channel_id BIGINT,
|
|
welcome_message TEXT,
|
|
welcome_dm TEXT,
|
|
welcome_gif_url TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS levelup (
|
|
guild_id BIGINT PRIMARY KEY,
|
|
levelup_channel_id BIGINT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS twitch_config (
|
|
guild_id BIGINT PRIMARY KEY,
|
|
twitch_channel_id BIGINT,
|
|
enabled BOOLEAN NOT NULL DEFAULT TRUE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS rps (
|
|
guild_id BIGINT NOT NULL,
|
|
user_id BIGINT NOT NULL,
|
|
score INT NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (guild_id, user_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS defaultrole (
|
|
guild_id BIGINT PRIMARY KEY,
|
|
role_id BIGINT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS logsettings (
|
|
guild_id BIGINT PRIMARY KEY,
|
|
log_channel_id BIGINT,
|
|
is_enabled BOOLEAN NOT NULL DEFAULT FALSE
|
|
);
|
|
|
|
-- Meeting rooms (voice lobby -> auto-created temporary voice channels)
|
|
CREATE TABLE IF NOT EXISTS meeting_settings (
|
|
guild_id BIGINT PRIMARY KEY,
|
|
lobby_channel_id BIGINT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS meeting_temp_channels (
|
|
guild_id BIGINT NOT NULL,
|
|
channel_id BIGINT NOT NULL,
|
|
created_by BIGINT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (guild_id, channel_id)
|
|
);
|
|
|
|
-- Projects & members
|
|
CREATE TABLE IF NOT EXISTS projects (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
guild_id BIGINT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_by BIGINT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS project_members (
|
|
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
user_id BIGINT NOT NULL,
|
|
is_creator BOOLEAN NOT NULL DEFAULT FALSE,
|
|
PRIMARY KEY (project_id, user_id)
|
|
);
|
|
|
|
--
|
|
-- Scheduling (one-to-one sessions)
|
|
--
|
|
CREATE TABLE IF NOT EXISTS schedules (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
guild_id BIGINT NOT NULL,
|
|
requester_id BIGINT NOT NULL,
|
|
invitee_id BIGINT NOT NULL,
|
|
scheduled_at TIMESTAMPTZ NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending', -- pending, accepted, declined, reschedule_requested
|
|
description TEXT,
|
|
reminder_sent BOOLEAN NOT NULL DEFAULT FALSE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
-- Reschedule negotiation: a proposed new time awaiting the counterpart's
|
|
-- response, kept separate from scheduled_at until accepted.
|
|
proposed_at TIMESTAMPTZ,
|
|
proposed_by BIGINT,
|
|
-- Snapshot of status before entering reschedule_requested, so a decline
|
|
-- knows whether to revert to 'pending' or 'accepted'.
|
|
pre_reschedule_status TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS schedule_messages (
|
|
schedule_id BIGINT NOT NULL REFERENCES schedules(id) ON DELETE CASCADE,
|
|
message_id BIGINT NOT NULL,
|
|
channel_id BIGINT NOT NULL,
|
|
is_dm BOOLEAN NOT NULL DEFAULT TRUE,
|
|
PRIMARY KEY (schedule_id, message_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_settings (
|
|
user_id BIGINT PRIMARY KEY,
|
|
timezone TEXT NOT NULL
|
|
);
|
|
|
|
--
|
|
-- Music (dashboard queue view/control - see velox-dashboard-api's
|
|
-- 000010_music migration, this file mirrors it)
|
|
--
|
|
CREATE TABLE IF NOT EXISTS music_settings (
|
|
guild_id BIGINT PRIMARY KEY,
|
|
is_enabled BOOLEAN NOT NULL DEFAULT FALSE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS music_now_playing (
|
|
guild_id BIGINT PRIMARY KEY,
|
|
track_title TEXT NOT NULL,
|
|
track_url TEXT NOT NULL,
|
|
duration_seconds INT NOT NULL,
|
|
started_at TIMESTAMPTZ NOT NULL,
|
|
paused BOOLEAN NOT NULL DEFAULT FALSE,
|
|
volume INT NOT NULL DEFAULT 100,
|
|
repeat_song BOOLEAN NOT NULL DEFAULT FALSE,
|
|
repeat_queue BOOLEAN NOT NULL DEFAULT FALSE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS music_queue (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
guild_id BIGINT NOT NULL,
|
|
position INT NOT NULL,
|
|
track_title TEXT NOT NULL,
|
|
track_url TEXT NOT NULL,
|
|
requested_by BIGINT NOT NULL,
|
|
added_at TIMESTAMPTZ DEFAULT NOW(),
|
|
client_id UUID NOT NULL DEFAULT gen_random_uuid(),
|
|
UNIQUE (guild_id, client_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS music_commands (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
guild_id BIGINT NOT NULL,
|
|
command_type TEXT NOT NULL CHECK (command_type IN ('skip', 'pause', 'resume', 'set_volume', 'remove_track', 'reorder', 'repeat_song', 'repeat_queue', 'add_song', 'stop')),
|
|
payload JSONB,
|
|
requested_by BIGINT NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
processed_at TIMESTAMPTZ
|
|
);
|
|
|
|
-- Tickets (mirror of velox-dashboard-api migration 000016_tickets)
|
|
CREATE TABLE IF NOT EXISTS ticket_settings (
|
|
guild_id BIGINT PRIMARY KEY,
|
|
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
channel_id BIGINT,
|
|
log_channel_id BIGINT,
|
|
panel_message_id BIGINT,
|
|
panel_title TEXT NOT NULL DEFAULT 'Need help?',
|
|
panel_description TEXT NOT NULL DEFAULT 'Pick a category below to open a private ticket with the staff team.',
|
|
max_open_per_user INT NOT NULL DEFAULT 1 CHECK (max_open_per_user BETWEEN 1 AND 10),
|
|
next_number INT NOT NULL DEFAULT 1
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ticket_categories (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
guild_id BIGINT NOT NULL,
|
|
name TEXT NOT NULL CHECK (char_length(name) BETWEEN 1 AND 40),
|
|
emoji TEXT,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
staff_role_id BIGINT,
|
|
position INT NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tickets (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
guild_id BIGINT NOT NULL,
|
|
number INT NOT NULL,
|
|
category_id BIGINT REFERENCES ticket_categories (id) ON DELETE SET NULL,
|
|
category_name TEXT NOT NULL,
|
|
thread_id BIGINT NOT NULL UNIQUE,
|
|
intro_message_id BIGINT,
|
|
opener_id BIGINT NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'closed')),
|
|
claimed_by BIGINT,
|
|
closed_by BIGINT,
|
|
close_reason TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
closed_at TIMESTAMPTZ,
|
|
UNIQUE (guild_id, number)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ticket_commands (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
guild_id BIGINT NOT NULL,
|
|
command_type TEXT NOT NULL CHECK (command_type IN ('post_panel')),
|
|
requested_by BIGINT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
processed_at TIMESTAMPTZ
|
|
);
|
|
|
|
-- Security: anti-raid and anti-nuke (mirror of velox-dashboard-api migration 000017_security)
|
|
CREATE TABLE IF NOT EXISTS security_settings (
|
|
guild_id BIGINT PRIMARY KEY,
|
|
-- Where alerts go, and who gets pinged for raids and nukes.
|
|
alert_channel_id BIGINT,
|
|
alert_role_id BIGINT,
|
|
|
|
-- Join Gate: checks run on every join, each with its own action.
|
|
joingate_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
joingate_no_avatar TEXT NOT NULL DEFAULT 'off' CHECK (joingate_no_avatar IN ('off', 'timeout', 'kick', 'ban')),
|
|
joingate_new_account TEXT NOT NULL DEFAULT 'off' CHECK (joingate_new_account IN ('off', 'timeout', 'kick', 'ban')),
|
|
joingate_min_account_age_days INT NOT NULL DEFAULT 7 CHECK (joingate_min_account_age_days BETWEEN 1 AND 365),
|
|
joingate_advertising_name TEXT NOT NULL DEFAULT 'off' CHECK (joingate_advertising_name IN ('off', 'timeout', 'kick', 'ban')),
|
|
joingate_username_filter TEXT NOT NULL DEFAULT 'off' CHECK (joingate_username_filter IN ('off', 'timeout', 'kick', 'ban')),
|
|
joingate_username_patterns TEXT[] NOT NULL DEFAULT '{}',
|
|
joingate_unverified_bot TEXT NOT NULL DEFAULT 'off' CHECK (joingate_unverified_bot IN ('off', 'kick', 'ban')),
|
|
-- A bot added by someone who isn't the owner or trusted.
|
|
joingate_unauthorized_bot TEXT NOT NULL DEFAULT 'kick' CHECK (joingate_unauthorized_bot IN ('off', 'kick', 'ban')),
|
|
|
|
-- Raid mode: raid_join_count joins within raid_join_seconds starts it; it
|
|
-- ends after raid_quiet_minutes without a join, or when staff end it.
|
|
antiraid_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
raid_join_count INT NOT NULL DEFAULT 10 CHECK (raid_join_count BETWEEN 3 AND 100),
|
|
raid_join_seconds INT NOT NULL DEFAULT 10 CHECK (raid_join_seconds BETWEEN 5 AND 300),
|
|
raid_action TEXT NOT NULL DEFAULT 'kick' CHECK (raid_action IN ('alert', 'timeout', 'kick', 'ban')),
|
|
raid_pause_invites BOOLEAN NOT NULL DEFAULT TRUE,
|
|
raid_quiet_minutes INT NOT NULL DEFAULT 10 CHECK (raid_quiet_minutes BETWEEN 1 AND 120),
|
|
|
|
-- Anti-nuke: anyone who isn't the owner or trusted and goes over a limit
|
|
-- within antinuke_window_seconds is quarantined. Limits are per action
|
|
-- (channel_delete, channel_create, role_delete, role_create, ban, kick,
|
|
-- webhook_create); a limit of 0 means the action is never allowed.
|
|
antinuke_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
antinuke_window_seconds INT NOT NULL DEFAULT 10 CHECK (antinuke_window_seconds BETWEEN 5 AND 300),
|
|
antinuke_limits JSONB NOT NULL DEFAULT '{"channel_delete": 2, "channel_create": 5, "role_delete": 2, "role_create": 5, "ban": 3, "kick": 5, "webhook_create": 3}',
|
|
-- Also lock every channel when a nuke is stopped.
|
|
antinuke_lock_server BOOLEAN NOT NULL DEFAULT FALSE,
|
|
quarantine_role_id BIGINT,
|
|
|
|
-- Restore: snapshots of roles and channels are taken every few hours; a
|
|
-- restore recreates what's missing. Automatic after a stopped nuke when
|
|
-- antinuke_auto_restore is on, otherwise by the owner or trusted.
|
|
antinuke_auto_restore BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
-- Heat: every message adds heat to its author (more for repeats,
|
|
-- mentions, emoji walls, links, blocked words; an invite link maxes it),
|
|
-- and heat cools down over time. Reaching the maximum deletes the message
|
|
-- and applies heat_action. heat_strictness scales how fast heat builds.
|
|
heat_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
heat_strictness TEXT NOT NULL DEFAULT 'medium' CHECK (heat_strictness IN ('low', 'medium', 'high')),
|
|
heat_action TEXT NOT NULL DEFAULT 'timeout' CHECK (heat_action IN ('timeout', 'kick', 'ban')),
|
|
heat_timeout_minutes INT NOT NULL DEFAULT 10 CHECK (heat_timeout_minutes BETWEEN 1 AND 10080),
|
|
-- Each new offence within a day doubles the timeout (capped at 28 days).
|
|
heat_escalate BOOLEAN NOT NULL DEFAULT TRUE,
|
|
heat_blocked_words TEXT[] NOT NULL DEFAULT '{}',
|
|
-- Heat panic: this many members maxing out within a minute times out
|
|
-- anyone who joined in the last day as soon as they speak, for 10
|
|
-- minutes. 0 turns it off.
|
|
heat_panic_members INT NOT NULL DEFAULT 3 CHECK (heat_panic_members BETWEEN 0 AND 50),
|
|
-- Mention flood: this many mentions by non-exempt members within 30
|
|
-- seconds locks every channel. 0 turns it off.
|
|
heat_lockdown_mentions INT NOT NULL DEFAULT 0 CHECK (heat_lockdown_mentions BETWEEN 0 AND 1000),
|
|
|
|
-- Join lockdown (/lockdown joins): every new member is kicked or banned.
|
|
join_lock TEXT NOT NULL DEFAULT 'off' CHECK (join_lock IN ('off', 'kick', 'ban'))
|
|
);
|
|
|
|
-- Users and roles exempt from anti-nuke, who may also manage its settings.
|
|
-- The server owner is always trusted and never listed here.
|
|
CREATE TABLE IF NOT EXISTS security_trusted (
|
|
guild_id BIGINT NOT NULL,
|
|
target_id BIGINT NOT NULL,
|
|
kind TEXT NOT NULL CHECK (kind IN ('user', 'role')),
|
|
added_by BIGINT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (guild_id, target_id)
|
|
);
|
|
|
|
-- Members currently quarantined, with the roles they had, so releasing them
|
|
-- gives every role back.
|
|
CREATE TABLE IF NOT EXISTS security_quarantines (
|
|
guild_id BIGINT NOT NULL,
|
|
user_id BIGINT NOT NULL,
|
|
previous_role_ids BIGINT[] NOT NULL DEFAULT '{}',
|
|
reason TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (guild_id, user_id)
|
|
);
|
|
|
|
-- Channels the bot locked, with @everyone's permission overwrite from before,
|
|
-- so unlocking puts back exactly what was there.
|
|
CREATE TABLE IF NOT EXISTS security_lockdowns (
|
|
guild_id BIGINT NOT NULL,
|
|
channel_id BIGINT NOT NULL,
|
|
had_overwrite BOOLEAN NOT NULL,
|
|
previous_allow BIGINT NOT NULL DEFAULT 0,
|
|
previous_deny BIGINT NOT NULL DEFAULT 0,
|
|
server_wide BOOLEAN NOT NULL DEFAULT FALSE,
|
|
locked_by BIGINT NOT NULL,
|
|
locked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (guild_id, channel_id)
|
|
);
|
|
|
|
-- A raid in progress, so raid mode survives a bot restart: the quiet timer
|
|
-- carries on and the summary still lists everyone caught.
|
|
CREATE TABLE IF NOT EXISTS security_raids (
|
|
guild_id BIGINT PRIMARY KEY,
|
|
started_at TIMESTAMPTZ NOT NULL,
|
|
last_join_at TIMESTAMPTZ NOT NULL,
|
|
invites_paused BOOLEAN NOT NULL DEFAULT FALSE,
|
|
caught_total INT NOT NULL DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS security_raid_caught (
|
|
guild_id BIGINT NOT NULL,
|
|
user_id BIGINT NOT NULL,
|
|
action TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
caught_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (guild_id, user_id)
|
|
);
|
|
|
|
-- Snapshots of a guild's roles and channels for restoring after a nuke. The
|
|
-- bot keeps the last three per guild.
|
|
CREATE TABLE IF NOT EXISTS security_snapshots (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
guild_id BIGINT NOT NULL,
|
|
data JSONB NOT NULL,
|
|
roles INT NOT NULL,
|
|
channels INT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS security_snapshots_guild_idx ON security_snapshots (guild_id, created_at DESC);
|
|
|
|
-- What the system did, for the dashboard's history.
|
|
CREATE TABLE IF NOT EXISTS security_events (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
guild_id BIGINT NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
actor_id BIGINT,
|
|
target_id BIGINT,
|
|
detail TEXT NOT NULL DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS security_events_guild_idx ON security_events (guild_id, created_at DESC);
|
|
|
|
-- Dashboard -> bot requests, same pattern as music_commands.
|
|
CREATE TABLE IF NOT EXISTS security_commands (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
guild_id BIGINT NOT NULL,
|
|
command_type TEXT NOT NULL CHECK (command_type IN ('end_raid', 'release_quarantine', 'unlock_server', 'unlock_joins', 'snapshot', 'restore')),
|
|
payload JSONB,
|
|
requested_by BIGINT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
processed_at TIMESTAMPTZ
|
|
);
|