393 lines
12 KiB
JavaScript
393 lines
12 KiB
JavaScript
const path = require("path");
|
|
const fs = require("fs");
|
|
const Database = require("better-sqlite3");
|
|
|
|
const dataDir = 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 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,
|
|
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 '[]',
|
|
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 logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
level TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
details 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_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);
|
|
`);
|
|
|
|
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("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 '[]'");
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
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
|
|
};
|