496 lines
18 KiB
Go
496 lines
18 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
|
|
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
)
|
|
|
|
type DB struct {
|
|
*sql.DB
|
|
}
|
|
|
|
func New(cfg *config.Config) (*DB, error) {
|
|
db, err := sql.Open("pgx", cfg.DatabaseDSN())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open database: %w", err)
|
|
}
|
|
|
|
if err := db.Ping(); err != nil {
|
|
return nil, fmt.Errorf("ping database: %w", err)
|
|
}
|
|
|
|
return &DB{db}, nil
|
|
}
|
|
|
|
func (d *DB) RunMigrations() error {
|
|
_, err := d.ExecContext(context.Background(), migrationsSQL)
|
|
return err
|
|
}
|
|
|
|
const migrationsSQL = `
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
username VARCHAR(32) NOT NULL UNIQUE,
|
|
display_name VARCHAR(32),
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
password_hash VARCHAR(255),
|
|
avatar TEXT,
|
|
bio VARCHAR(250) DEFAULT '',
|
|
accent_color VARCHAR(7) DEFAULT '',
|
|
status VARCHAR(16) NOT NULL DEFAULT 'offline',
|
|
status_text VARCHAR(128) DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token VARCHAR(255) NOT NULL UNIQUE,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS servers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(100) NOT NULL,
|
|
icon TEXT,
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS channels (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
name VARCHAR(100) NOT NULL,
|
|
type VARCHAR(16) NOT NULL DEFAULT 'text',
|
|
category VARCHAR(100) NOT NULL DEFAULT 'general',
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (server_id, name)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS members (
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (user_id, server_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
content TEXT NOT NULL,
|
|
reply_to UUID REFERENCES messages(id) ON DELETE SET NULL,
|
|
edited_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS roles (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
name VARCHAR(100) NOT NULL,
|
|
color VARCHAR(7),
|
|
permissions BIGINT NOT NULL DEFAULT 0,
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS member_roles (
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (user_id, role_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_messages_channel_created ON messages(channel_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);
|
|
CREATE INDEX IF NOT EXISTS idx_members_server_user ON members(server_id, user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS reactions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
emoji VARCHAR(64) NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (message_id, user_id, emoji)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_reactions_message ON reactions(message_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS invites (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
code VARCHAR(16) NOT NULL UNIQUE,
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
created_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
expires_at TIMESTAMPTZ,
|
|
max_uses INTEGER,
|
|
use_count INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_invites_code ON invites(code);
|
|
|
|
CREATE TABLE IF NOT EXISTS bots (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(100) NOT NULL,
|
|
avatar TEXT,
|
|
description TEXT DEFAULT '',
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token VARCHAR(64) NOT NULL UNIQUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS bot_servers (
|
|
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
added_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (bot_id, server_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS slash_commands (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
name VARCHAR(32) NOT NULL,
|
|
description TEXT DEFAULT '',
|
|
options JSONB DEFAULT '[]',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (server_id, name)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS webhooks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
name VARCHAR(100) NOT NULL,
|
|
token VARCHAR(64) NOT NULL,
|
|
avatar TEXT,
|
|
created_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_bots_owner ON bots(owner_id);
|
|
CREATE INDEX IF NOT EXISTS idx_bots_token ON bots(token);
|
|
CREATE INDEX IF NOT EXISTS idx_bot_servers_bot ON bot_servers(bot_id);
|
|
CREATE INDEX IF NOT EXISTS idx_slash_commands_server ON slash_commands(server_id, name);
|
|
CREATE INDEX IF NOT EXISTS idx_webhooks_channel ON webhooks(channel_id);
|
|
CREATE INDEX IF NOT EXISTS idx_webhooks_token ON webhooks(token);
|
|
|
|
-- Add token_hash column for secure webhook token storage
|
|
ALTER TABLE webhooks ADD COLUMN IF NOT EXISTS token_hash VARCHAR(64);
|
|
CREATE INDEX IF NOT EXISTS idx_webhooks_token_hash ON webhooks(token_hash);
|
|
|
|
-- Ensure reply_to column exists on messages (added after initial table creation)
|
|
ALTER TABLE messages ADD COLUMN IF NOT EXISTS reply_to UUID REFERENCES messages(id) ON DELETE SET NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
endpoint TEXT NOT NULL,
|
|
p256dh TEXT NOT NULL,
|
|
auth TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (user_id, endpoint)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS webauthn_credentials (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
credential_id BYTEA NOT NULL UNIQUE,
|
|
public_key BYTEA NOT NULL,
|
|
aaguid UUID,
|
|
sign_count BIGINT NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user ON push_subscriptions(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_webauthn_credentials_user ON webauthn_credentials(user_id);
|
|
|
|
-- Per-channel notification settings
|
|
CREATE TABLE IF NOT EXISTS notification_settings (
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
level VARCHAR(16) NOT NULL DEFAULT 'all',
|
|
PRIMARY KEY (user_id, channel_id)
|
|
);
|
|
|
|
-- Read receipts (per-user, per-channel)
|
|
CREATE TABLE IF NOT EXISTS read_states (
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
last_read_message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (user_id, channel_id)
|
|
);
|
|
|
|
-- Direct message conversations
|
|
CREATE TABLE IF NOT EXISTS conversations (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
type VARCHAR(16) NOT NULL DEFAULT 'dm',
|
|
name VARCHAR(100),
|
|
created_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS conversation_members (
|
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (conversation_id, user_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS conversation_messages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
content VARCHAR(4000) NOT NULL,
|
|
edited_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_conversation_members_user ON conversation_members(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_conversation_messages_conv_created ON conversation_messages(conversation_id, created_at DESC);
|
|
|
|
-- Moderation
|
|
CREATE TABLE IF NOT EXISTS bans (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
banned_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
reason TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (server_id, user_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS server_mutes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
muted_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
reason TEXT,
|
|
expires_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (server_id, user_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_bans_server ON bans(server_id);
|
|
CREATE INDEX IF NOT EXISTS idx_server_mutes_server ON server_mutes(server_id);
|
|
|
|
-- Link embeds
|
|
CREATE TABLE IF NOT EXISTS embeds (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
url TEXT NOT NULL,
|
|
title TEXT,
|
|
description TEXT,
|
|
image_url TEXT,
|
|
site_name TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_embeds_message ON embeds(message_id);
|
|
|
|
-- Channel slowmode
|
|
ALTER TABLE channels ADD COLUMN IF NOT EXISTS slowmode_seconds INTEGER NOT NULL DEFAULT 0;
|
|
|
|
-- Audit log
|
|
CREATE TABLE IF NOT EXISTS audit_log (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
action_type VARCHAR(32) NOT NULL,
|
|
target_type VARCHAR(32),
|
|
target_id VARCHAR(64),
|
|
reason TEXT,
|
|
changes JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_audit_log_server_created ON audit_log(server_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(server_id, action_type);
|
|
|
|
-- Channel permission overrides
|
|
CREATE TABLE IF NOT EXISTS channel_overrides (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
target_type VARCHAR(8) NOT NULL CHECK (target_type IN ('role', 'user')),
|
|
target_id UUID NOT NULL,
|
|
allow_bitflags BIGINT NOT NULL DEFAULT 0,
|
|
deny_bitflags BIGINT NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (channel_id, target_type, target_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_channel_overrides_channel ON channel_overrides(channel_id);
|
|
CREATE INDEX IF NOT EXISTS idx_channel_overrides_target ON channel_overrides(channel_id, target_type, target_id);
|
|
|
|
-- Forum tags
|
|
CREATE TABLE IF NOT EXISTS forum_tags (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
name VARCHAR(64) NOT NULL,
|
|
emoji VARCHAR(32),
|
|
color VARCHAR(7),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (channel_id, name)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_forum_tags_channel ON forum_tags(channel_id);
|
|
|
|
-- Thread tag assignments (for forum posts)
|
|
CREATE TABLE IF NOT EXISTS channel_tags (
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
tag_id UUID NOT NULL REFERENCES forum_tags(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (channel_id, tag_id)
|
|
);
|
|
|
|
-- Threads: reuse channels table by adding parent_channel_id; thread messages are still messages scoped to the thread channel.
|
|
ALTER TABLE channels ADD COLUMN IF NOT EXISTS parent_channel_id UUID REFERENCES channels(id) ON DELETE CASCADE;
|
|
ALTER TABLE channels ADD COLUMN IF NOT EXISTS archived_at TIMESTAMPTZ;
|
|
ALTER TABLE channels ADD COLUMN IF NOT EXISTS auto_archive_duration INTEGER NOT NULL DEFAULT 1440;
|
|
ALTER TABLE channels ADD COLUMN IF NOT EXISTS message_count INTEGER NOT NULL DEFAULT 0;
|
|
ALTER TABLE channels ADD COLUMN IF NOT EXISTS last_message_at TIMESTAMPTZ;
|
|
CREATE INDEX IF NOT EXISTS idx_channels_parent ON channels(parent_channel_id);
|
|
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS banner_url TEXT;
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS tagline VARCHAR(128);
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS social_links JSONB DEFAULT '[]';
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS pronouns VARCHAR(40);
|
|
|
|
-- Badges
|
|
CREATE TABLE IF NOT EXISTS badges (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
server_id UUID REFERENCES servers(id) ON DELETE CASCADE,
|
|
name VARCHAR(64) NOT NULL,
|
|
icon TEXT,
|
|
description TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_badges_server ON badges(server_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_badges (
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
badge_id UUID NOT NULL REFERENCES badges(id) ON DELETE CASCADE,
|
|
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
granted_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
PRIMARY KEY (user_id, badge_id)
|
|
);
|
|
|
|
-- Block list
|
|
CREATE TABLE IF NOT EXISTS blocks (
|
|
blocker_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
blocked_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (blocker_id, blocked_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_blocks_blocker ON blocks(blocker_id);
|
|
CREATE INDEX IF NOT EXISTS idx_blocks_blocked ON blocks(blocked_id);
|
|
|
|
-- Calendar events
|
|
CREATE TABLE IF NOT EXISTS events (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
title VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
start_time TIMESTAMPTZ NOT NULL,
|
|
end_time TIMESTAMPTZ,
|
|
recurrence_rule VARCHAR(255),
|
|
location VARCHAR(255),
|
|
color VARCHAR(7),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_events_channel_time ON events(channel_id, start_time);
|
|
|
|
CREATE TABLE IF NOT EXISTS event_rsvps (
|
|
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
status VARCHAR(16) NOT NULL CHECK (status IN ('going', 'maybe', 'no')),
|
|
responded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (event_id, user_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_event_rsvps_event ON event_rsvps(event_id);
|
|
|
|
-- Documents (wiki/docs channels)
|
|
CREATE TABLE IF NOT EXISTS documents (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
title VARCHAR(255) NOT NULL,
|
|
content TEXT NOT NULL DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_documents_channel ON documents(channel_id, updated_at DESC);
|
|
|
|
-- List items (task lists)
|
|
CREATE TABLE IF NOT EXISTS list_items (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
title VARCHAR(511) NOT NULL,
|
|
status VARCHAR(16) NOT NULL DEFAULT 'todo',
|
|
position INT NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_list_items_channel ON list_items(channel_id, position);
|
|
|
|
-- Weekly availability
|
|
CREATE TABLE IF NOT EXISTS availability (
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
day_of_week VARCHAR(9) NOT NULL CHECK (day_of_week IN ('monday','tuesday','wednesday','thursday','friday','saturday','sunday')),
|
|
start_time TIME NOT NULL,
|
|
end_time TIME NOT NULL,
|
|
PRIMARY KEY (user_id, server_id, day_of_week, start_time)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_availability_server ON availability(server_id, day_of_week);
|
|
|
|
-- Message full-text search
|
|
ALTER TABLE messages ADD COLUMN IF NOT EXISTS search_vector tsvector;
|
|
CREATE INDEX IF NOT EXISTS idx_messages_search ON messages USING GIN(search_vector);
|
|
|
|
CREATE OR REPLACE FUNCTION messages_search_update() RETURNS trigger AS $$
|
|
BEGIN
|
|
NEW.search_vector := to_tsvector('english', COALESCE(NEW.content, ''));
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS messages_search_trigger ON messages;
|
|
CREATE TRIGGER messages_search_trigger
|
|
BEFORE INSERT OR UPDATE ON messages
|
|
FOR EACH ROW EXECUTE FUNCTION messages_search_update();
|
|
|
|
-- Server groups (sub-server sections for organizing channels)
|
|
CREATE TABLE IF NOT EXISTS server_groups (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
name VARCHAR(100) NOT NULL,
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (server_id, name)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_server_groups_server ON server_groups(server_id);
|
|
ALTER TABLE channels ADD COLUMN IF NOT EXISTS group_id UUID REFERENCES server_groups(id) ON DELETE SET NULL;
|
|
`
|
|
|