668 lines
22 KiB
JavaScript
668 lines
22 KiB
JavaScript
const path = require("path");
|
|
const fs = require("fs");
|
|
const Database = require("better-sqlite3");
|
|
|
|
const dataDir = process.env.LUMI_DATA_DIR
|
|
? path.resolve(process.env.LUMI_DATA_DIR)
|
|
: path.join(__dirname, "..", "..", "data");
|
|
const dbPath = path.join(dataDir, "app.db");
|
|
|
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
|
|
const db = new Database(dbPath);
|
|
db.pragma("journal_mode = WAL");
|
|
|
|
function migrate() {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS custom_themes (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
base_theme_id TEXT NOT NULL,
|
|
values_json TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
username TEXT NOT NULL,
|
|
avatar TEXT,
|
|
last_login INTEGER NOT NULL,
|
|
created_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_profiles (
|
|
id TEXT PRIMARY KEY,
|
|
internal_username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_identities (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
provider TEXT NOT NULL,
|
|
provider_user_id TEXT NOT NULL,
|
|
display_name TEXT,
|
|
avatar TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
UNIQUE(provider, provider_user_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS mod_role_periods (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
start_at INTEGER NOT NULL,
|
|
end_at INTEGER
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS linked_accounts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
provider TEXT NOT NULL,
|
|
provider_user_id TEXT NOT NULL,
|
|
display_name TEXT,
|
|
access_token TEXT,
|
|
refresh_token TEXT,
|
|
expires_at INTEGER,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
UNIQUE(user_id, provider)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS plugins (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
version TEXT,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
source TEXT,
|
|
path TEXT NOT NULL,
|
|
installed_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS plugin_settings (
|
|
plugin_id TEXT NOT NULL,
|
|
key TEXT NOT NULL,
|
|
value TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
PRIMARY KEY (plugin_id, key)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS stats (
|
|
user_id TEXT PRIMARY KEY,
|
|
messages INTEGER NOT NULL DEFAULT 0,
|
|
commands INTEGER NOT NULL DEFAULT 0,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_age_intervals (
|
|
id TEXT PRIMARY KEY,
|
|
platform TEXT NOT NULL,
|
|
platform_user_id TEXT NOT NULL,
|
|
scope_id TEXT NOT NULL,
|
|
statistic_type TEXT NOT NULL,
|
|
start_at INTEGER NOT NULL,
|
|
end_at INTEGER,
|
|
start_source TEXT NOT NULL,
|
|
start_precision TEXT NOT NULL DEFAULT 'millisecond',
|
|
start_authoritative INTEGER NOT NULL DEFAULT 0,
|
|
end_source TEXT,
|
|
end_precision TEXT,
|
|
end_authoritative INTEGER,
|
|
observed_at INTEGER NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS user_age_intervals_one_open_idx
|
|
ON user_age_intervals (platform, platform_user_id, scope_id, statistic_type)
|
|
WHERE end_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS user_age_intervals_lookup_idx
|
|
ON user_age_intervals (platform, scope_id, statistic_type, platform_user_id, start_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_age_events (
|
|
platform TEXT NOT NULL,
|
|
event_id TEXT NOT NULL,
|
|
processed_at INTEGER NOT NULL,
|
|
PRIMARY KEY (platform, event_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS custom_pages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
title TEXT NOT NULL,
|
|
nav_label TEXT,
|
|
content TEXT NOT NULL,
|
|
content_css TEXT NOT NULL DEFAULT '',
|
|
format TEXT NOT NULL DEFAULT 'html',
|
|
role TEXT NOT NULL DEFAULT 'public',
|
|
show_in_nav INTEGER NOT NULL DEFAULT 0,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS custom_commands (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
trigger TEXT NOT NULL UNIQUE,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
response TEXT NOT NULL,
|
|
platform TEXT NOT NULL DEFAULT 'both',
|
|
mode TEXT NOT NULL DEFAULT 'plain',
|
|
language TEXT NOT NULL DEFAULT 'js',
|
|
code TEXT,
|
|
preview_text TEXT,
|
|
preview_status TEXT,
|
|
preview_error TEXT,
|
|
preview_generated_at INTEGER,
|
|
preview_dynamic_segments TEXT NOT NULL DEFAULT '[]',
|
|
random_replies_json TEXT NOT NULL DEFAULT '[]',
|
|
conditional_replies_json TEXT NOT NULL DEFAULT '[]',
|
|
conditional_fuzzy INTEGER NOT NULL DEFAULT 1,
|
|
rng_enabled INTEGER NOT NULL DEFAULT 0,
|
|
rng_min INTEGER NOT NULL DEFAULT 1,
|
|
rng_max INTEGER NOT NULL DEFAULT 100,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS command_usage (
|
|
command_id TEXT PRIMARY KEY,
|
|
count INTEGER NOT NULL DEFAULT 0,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS command_user_usage (
|
|
command_id TEXT NOT NULL,
|
|
user_id TEXT NOT NULL,
|
|
count INTEGER NOT NULL DEFAULT 0,
|
|
updated_at INTEGER NOT NULL,
|
|
PRIMARY KEY (command_id, user_id),
|
|
FOREIGN KEY (user_id) REFERENCES user_profiles(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_command_user_usage_user
|
|
ON command_user_usage(user_id, count DESC, updated_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS command_groups (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
policy_json TEXT NOT NULL DEFAULT '{}',
|
|
is_default INTEGER NOT NULL DEFAULT 0,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS command_group_members (
|
|
group_id TEXT NOT NULL,
|
|
command_key TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
PRIMARY KEY (group_id, command_key),
|
|
FOREIGN KEY (group_id) REFERENCES command_groups(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS command_policies (
|
|
command_key TEXT PRIMARY KEY,
|
|
policy_json TEXT NOT NULL DEFAULT '{}',
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS command_policy_uses (
|
|
id TEXT NOT NULL,
|
|
command_key TEXT NOT NULL,
|
|
rule_key TEXT NOT NULL,
|
|
user_id TEXT,
|
|
stream_session_id TEXT,
|
|
used_at INTEGER NOT NULL,
|
|
PRIMARY KEY (id, rule_key)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS command_policy_uses_window_idx
|
|
ON command_policy_uses (rule_key, user_id, used_at);
|
|
CREATE INDEX IF NOT EXISTS command_policy_uses_stream_idx
|
|
ON command_policy_uses (rule_key, stream_session_id, user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS command_stream_sessions (
|
|
stream_key TEXT PRIMARY KEY,
|
|
session_id TEXT NOT NULL,
|
|
provider_stream_id TEXT,
|
|
started_at INTEGER NOT NULL,
|
|
last_seen_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
level TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
details TEXT,
|
|
source TEXT NOT NULL DEFAULT 'core',
|
|
category TEXT NOT NULL DEFAULT 'general',
|
|
event TEXT,
|
|
request_id TEXT,
|
|
created_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS logs_created_at_idx ON logs (created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS feedback_entries (
|
|
id TEXT PRIMARY KEY,
|
|
submitter_id TEXT NOT NULL,
|
|
summary TEXT NOT NULL,
|
|
category TEXT NOT NULL,
|
|
severity TEXT NOT NULL,
|
|
scope_type TEXT NOT NULL,
|
|
scope_label TEXT,
|
|
target_metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
current_url TEXT,
|
|
page_title TEXT,
|
|
description TEXT NOT NULL,
|
|
steps_to_reproduce TEXT,
|
|
expected_behavior TEXT,
|
|
actual_behavior TEXT,
|
|
diagnostics_json TEXT NOT NULL DEFAULT '{}',
|
|
screenshot_path TEXT,
|
|
screenshot_mime TEXT,
|
|
screenshot_size INTEGER,
|
|
status TEXT NOT NULL DEFAULT 'new',
|
|
admin_reply TEXT,
|
|
assigned_admin_id TEXT,
|
|
linked_todo TEXT,
|
|
linked_issue TEXT,
|
|
linked_correction TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
last_activity_at INTEGER NOT NULL,
|
|
deleted_at INTEGER
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS feedback_entries_submitter_idx ON feedback_entries (submitter_id);
|
|
CREATE INDEX IF NOT EXISTS feedback_entries_status_idx ON feedback_entries (status);
|
|
CREATE INDEX IF NOT EXISTS feedback_entries_last_activity_idx ON feedback_entries (last_activity_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS feedback_comments (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
feedback_id TEXT NOT NULL,
|
|
actor_id TEXT NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
body TEXT NOT NULL,
|
|
visible_to_submitter INTEGER NOT NULL DEFAULT 1,
|
|
created_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS feedback_comments_feedback_idx ON feedback_comments (feedback_id, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS feedback_status_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
feedback_id TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
actor_id TEXT,
|
|
note TEXT,
|
|
created_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS feedback_status_history_feedback_idx ON feedback_status_history (feedback_id, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS feedback_view_state (
|
|
user_id TEXT PRIMARY KEY,
|
|
last_seen_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS feedback_support (
|
|
feedback_id TEXT NOT NULL,
|
|
user_id TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
PRIMARY KEY (feedback_id, user_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS feedback_support_feedback_idx ON feedback_support (feedback_id, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS feedback_merges (
|
|
source_feedback_id TEXT PRIMARY KEY,
|
|
target_feedback_id TEXT NOT NULL,
|
|
merged_by TEXT,
|
|
reason TEXT,
|
|
created_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS feedback_merges_target_idx ON feedback_merges (target_feedback_id, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS feedback_attachments (
|
|
id TEXT PRIMARY KEY,
|
|
feedback_id TEXT NOT NULL,
|
|
storage_path TEXT NOT NULL,
|
|
original_name TEXT,
|
|
mime TEXT NOT NULL,
|
|
size INTEGER NOT NULL,
|
|
kind TEXT NOT NULL DEFAULT 'attachment',
|
|
created_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS feedback_attachments_feedback_idx ON feedback_attachments (feedback_id, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS overlays (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
canvas_width INTEGER NOT NULL DEFAULT 1920,
|
|
canvas_height INTEGER NOT NULL DEFAULT 1080,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
active_scene_id TEXT,
|
|
public_token_hash TEXT NOT NULL UNIQUE,
|
|
public_token_encrypted TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS overlays_sort_idx ON overlays (sort_order, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS overlay_scenes (
|
|
id TEXT PRIMARY KEY,
|
|
overlay_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
public_token_hash TEXT NOT NULL UNIQUE,
|
|
public_token_encrypted TEXT NOT NULL,
|
|
obs_scene_name TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
FOREIGN KEY (overlay_id) REFERENCES overlays(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS overlay_scenes_overlay_idx ON overlay_scenes (overlay_id, sort_order, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS overlay_modules (
|
|
id TEXT PRIMARY KEY,
|
|
scene_id TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
config_json TEXT NOT NULL DEFAULT '{}',
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
FOREIGN KEY (scene_id) REFERENCES overlay_scenes(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS overlay_modules_scene_idx ON overlay_modules (scene_id, sort_order, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS overlay_event_hooks (
|
|
id TEXT PRIMARY KEY,
|
|
overlay_id TEXT NOT NULL,
|
|
event_type TEXT NOT NULL,
|
|
action_type TEXT NOT NULL,
|
|
module_id TEXT NOT NULL,
|
|
duration_seconds REAL NOT NULL DEFAULT 0,
|
|
filters_json TEXT NOT NULL DEFAULT '{}',
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
FOREIGN KEY (overlay_id) REFERENCES overlays(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (module_id) REFERENCES overlay_modules(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS overlay_event_hooks_event_idx ON overlay_event_hooks (event_type, enabled);
|
|
CREATE INDEX IF NOT EXISTS overlay_event_hooks_overlay_idx ON overlay_event_hooks (overlay_id, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS content_resources (
|
|
id TEXT PRIMARY KEY,
|
|
display_name TEXT NOT NULL,
|
|
original_name TEXT NOT NULL,
|
|
stored_name TEXT NOT NULL UNIQUE,
|
|
mime TEXT NOT NULL,
|
|
extension TEXT NOT NULL,
|
|
category TEXT NOT NULL,
|
|
preview_kind TEXT NOT NULL,
|
|
size INTEGER NOT NULL,
|
|
checksum_sha256 TEXT NOT NULL,
|
|
access_level TEXT NOT NULL DEFAULT 'locked',
|
|
public_token_hash TEXT UNIQUE,
|
|
public_token_encrypted TEXT,
|
|
uploaded_by TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS content_resources_created_idx ON content_resources (created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS content_resources_access_idx ON content_resources (access_level, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS content_resources_category_idx ON content_resources (category, created_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS overlay_obs_settings (
|
|
overlay_id TEXT PRIMARY KEY,
|
|
enabled INTEGER NOT NULL DEFAULT 0,
|
|
provider TEXT NOT NULL DEFAULT 'local_obs_websocket',
|
|
endpoint TEXT NOT NULL DEFAULT 'ws://127.0.0.1:4455',
|
|
password_encrypted TEXT,
|
|
sync_direction TEXT NOT NULL DEFAULT 'none',
|
|
reconnect INTEGER NOT NULL DEFAULT 1,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
FOREIGN KEY (overlay_id) REFERENCES overlays(id) ON DELETE CASCADE
|
|
);
|
|
`);
|
|
|
|
const columns = db
|
|
.prepare("PRAGMA table_info(custom_commands)")
|
|
.all()
|
|
.map((column) => column.name);
|
|
|
|
if (!columns.includes("mode")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN mode TEXT NOT NULL DEFAULT 'plain'");
|
|
}
|
|
if (!columns.includes("description")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN description TEXT NOT NULL DEFAULT ''");
|
|
}
|
|
if (!columns.includes("language")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN language TEXT NOT NULL DEFAULT 'js'");
|
|
}
|
|
if (!columns.includes("code")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN code TEXT");
|
|
}
|
|
if (!columns.includes("platform")) {
|
|
db.exec(
|
|
"ALTER TABLE custom_commands ADD COLUMN platform TEXT NOT NULL DEFAULT 'both'"
|
|
);
|
|
}
|
|
if (!columns.includes("preview_text")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN preview_text TEXT");
|
|
}
|
|
if (!columns.includes("preview_status")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN preview_status TEXT");
|
|
}
|
|
if (!columns.includes("preview_error")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN preview_error TEXT");
|
|
}
|
|
if (!columns.includes("preview_generated_at")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN preview_generated_at INTEGER");
|
|
}
|
|
if (!columns.includes("preview_dynamic_segments")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN preview_dynamic_segments TEXT NOT NULL DEFAULT '[]'");
|
|
}
|
|
if (!columns.includes("random_replies_json")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN random_replies_json TEXT NOT NULL DEFAULT '[]'");
|
|
}
|
|
if (!columns.includes("conditional_replies_json")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN conditional_replies_json TEXT NOT NULL DEFAULT '[]'");
|
|
}
|
|
if (!columns.includes("conditional_fuzzy")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN conditional_fuzzy INTEGER NOT NULL DEFAULT 1");
|
|
}
|
|
|
|
const commandGroupColumns = db
|
|
.prepare("PRAGMA table_info(command_groups)")
|
|
.all()
|
|
.map((column) => column.name);
|
|
if (!commandGroupColumns.includes("is_default")) {
|
|
db.exec("ALTER TABLE command_groups ADD COLUMN is_default INTEGER NOT NULL DEFAULT 0");
|
|
}
|
|
if (!columns.includes("rng_enabled")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN rng_enabled INTEGER NOT NULL DEFAULT 0");
|
|
}
|
|
if (!columns.includes("rng_min")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN rng_min INTEGER NOT NULL DEFAULT 1");
|
|
}
|
|
if (!columns.includes("rng_max")) {
|
|
db.exec("ALTER TABLE custom_commands ADD COLUMN rng_max INTEGER NOT NULL DEFAULT 100");
|
|
}
|
|
|
|
const pageColumns = db
|
|
.prepare("PRAGMA table_info(custom_pages)")
|
|
.all()
|
|
.map((column) => column.name);
|
|
|
|
if (!pageColumns.includes("content_css")) {
|
|
db.exec(
|
|
"ALTER TABLE custom_pages ADD COLUMN content_css TEXT NOT NULL DEFAULT ''"
|
|
);
|
|
}
|
|
if (!pageColumns.includes("format")) {
|
|
db.exec(
|
|
"ALTER TABLE custom_pages ADD COLUMN format TEXT NOT NULL DEFAULT 'html'"
|
|
);
|
|
}
|
|
|
|
const profileColumns = db
|
|
.prepare("PRAGMA table_info(user_profiles)")
|
|
.all()
|
|
.map((column) => column.name);
|
|
|
|
if (!profileColumns.includes("username_updated_at")) {
|
|
db.exec("ALTER TABLE user_profiles ADD COLUMN username_updated_at INTEGER");
|
|
}
|
|
|
|
const feedbackColumns = db
|
|
.prepare("PRAGMA table_info(feedback_entries)")
|
|
.all()
|
|
.map((column) => column.name);
|
|
if (!feedbackColumns.includes("screenshot_path")) {
|
|
db.exec("ALTER TABLE feedback_entries ADD COLUMN screenshot_path TEXT");
|
|
}
|
|
if (!feedbackColumns.includes("screenshot_mime")) {
|
|
db.exec("ALTER TABLE feedback_entries ADD COLUMN screenshot_mime TEXT");
|
|
}
|
|
if (!feedbackColumns.includes("screenshot_size")) {
|
|
db.exec("ALTER TABLE feedback_entries ADD COLUMN screenshot_size INTEGER");
|
|
}
|
|
|
|
const overlayColumns = db
|
|
.prepare("PRAGMA table_info(overlays)")
|
|
.all()
|
|
.map((column) => column.name);
|
|
if (!overlayColumns.includes("canvas_width")) {
|
|
db.exec("ALTER TABLE overlays ADD COLUMN canvas_width INTEGER NOT NULL DEFAULT 1920");
|
|
}
|
|
if (!overlayColumns.includes("canvas_height")) {
|
|
db.exec("ALTER TABLE overlays ADD COLUMN canvas_height INTEGER NOT NULL DEFAULT 1080");
|
|
}
|
|
|
|
const logColumns = db
|
|
.prepare("PRAGMA table_info(logs)")
|
|
.all()
|
|
.map((column) => column.name);
|
|
if (!logColumns.includes("source")) {
|
|
db.exec("ALTER TABLE logs ADD COLUMN source TEXT NOT NULL DEFAULT 'core'");
|
|
}
|
|
if (!logColumns.includes("category")) {
|
|
db.exec("ALTER TABLE logs ADD COLUMN category TEXT NOT NULL DEFAULT 'general'");
|
|
}
|
|
if (!logColumns.includes("event")) {
|
|
db.exec("ALTER TABLE logs ADD COLUMN event TEXT");
|
|
}
|
|
if (!logColumns.includes("request_id")) {
|
|
db.exec("ALTER TABLE logs ADD COLUMN request_id TEXT");
|
|
}
|
|
db.exec("CREATE INDEX IF NOT EXISTS logs_source_created_at_idx ON logs (source, created_at)");
|
|
db.exec("CREATE INDEX IF NOT EXISTS logs_category_created_at_idx ON logs (category, created_at)");
|
|
|
|
migrateLegacyUsers();
|
|
}
|
|
|
|
function migrateLegacyUsers() {
|
|
const legacyUsers = db
|
|
.prepare("SELECT id, username, avatar FROM users")
|
|
.all();
|
|
if (!legacyUsers.length) {
|
|
return;
|
|
}
|
|
|
|
const now = Date.now();
|
|
const mapping = new Map();
|
|
|
|
for (const legacy of legacyUsers) {
|
|
const existingIdentity = db
|
|
.prepare(
|
|
"SELECT user_id FROM user_identities WHERE provider = 'discord' AND provider_user_id = ?"
|
|
)
|
|
.get(legacy.id);
|
|
|
|
if (existingIdentity?.user_id) {
|
|
mapping.set(legacy.id, existingIdentity.user_id);
|
|
continue;
|
|
}
|
|
|
|
const username = generateUniqueLegacyUsername(legacy.username);
|
|
const userId = cryptoRandomId();
|
|
db.prepare(
|
|
"INSERT INTO user_profiles (id, internal_username, created_at, updated_at) VALUES (?, ?, ?, ?)"
|
|
).run(userId, username, now, now);
|
|
db.prepare(
|
|
"INSERT INTO user_identities (user_id, provider, provider_user_id, display_name, avatar, created_at, updated_at) VALUES (?, 'discord', ?, ?, ?, ?, ?)"
|
|
).run(userId, legacy.id, legacy.username, legacy.avatar, now, now);
|
|
mapping.set(legacy.id, userId);
|
|
}
|
|
|
|
for (const [legacyId, userId] of mapping.entries()) {
|
|
db.prepare("UPDATE stats SET user_id = ? WHERE user_id = ?").run(
|
|
userId,
|
|
legacyId
|
|
);
|
|
db.prepare("UPDATE linked_accounts SET user_id = ? WHERE user_id = ?").run(
|
|
userId,
|
|
legacyId
|
|
);
|
|
}
|
|
}
|
|
|
|
function generateUniqueLegacyUsername(name) {
|
|
const base = (name || "user").trim() || "user";
|
|
if (isLegacyUsernameAvailable(base)) {
|
|
return base;
|
|
}
|
|
let suffix = 2;
|
|
let candidate = `${base}-${suffix}`;
|
|
while (!isLegacyUsernameAvailable(candidate)) {
|
|
suffix += 1;
|
|
candidate = `${base}-${suffix}`;
|
|
}
|
|
return candidate;
|
|
}
|
|
|
|
function isLegacyUsernameAvailable(name) {
|
|
const row = db
|
|
.prepare(
|
|
"SELECT id FROM user_profiles WHERE internal_username = ? LIMIT 1"
|
|
)
|
|
.get(name);
|
|
return !row;
|
|
}
|
|
|
|
function cryptoRandomId() {
|
|
return require("crypto").randomUUID();
|
|
}
|
|
|
|
module.exports = {
|
|
db,
|
|
migrate
|
|
};
|