diff --git a/src/index.ts b/src/index.ts index 89487cd..8d20153 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import "../instrument.js"; // Import the instrumentation module first +import "../instrument.js"; import { Client, GatewayIntentBits, Partials } from "discord.js"; import { config } from "./Config.js"; import { setupBranding } from "./util/branding.js"; @@ -7,6 +7,7 @@ import * as Sentry from "@sentry/bun"; import * as schedule from "node-schedule"; import { startHealthCheck } from "./healthcheck.js"; import { logger } from "./logging.js"; + import { AchievementsModule } from "./modules/achievements/achievements.module.js"; import AskToAskModule from "./modules/askToAsk.module.js"; import { CoreModule } from "./modules/core/core.module.js"; @@ -31,8 +32,9 @@ import { ThreatDetectionModule } from "./modules/threatDetection/threatDetection import { TokenScannerModule } from "./modules/tokenScanner.module.js"; import { UserModule } from "./modules/user/user.module.js"; import { XpModule } from "./modules/xp/xp.module.js"; -import { initSentry } from "./sentry.js"; + import { initStorage } from "./store/storage.js"; +import { initSentry } from "./sentry.js"; const client = new Client({ intents: [ @@ -79,44 +81,77 @@ export const moduleManager = new ModuleManager( async function logIn() { initSentry(client); + const token = process.env.DDB_BOT_TOKEN; if (!token) { logger.error("No token found"); process.exit(1); } - logger.info("Logging in"); + + logger.info("Logging in..."); await client.login(token); - logger.info("Logged in"); + + // ensure bot is fully ready + await new Promise((resolve) => client.once("ready", resolve)); + + logger.info("Logged in and ready"); return client; } +async function initModules() { + for (const module of moduleManager.getModules()) { + try { + const result = module.onInit?.(moduleManager, client); + + if (result instanceof Promise) { + await result; + } + } catch (e) { + Sentry.captureException(e); + logger.error(`Error initializing module ${module.name}`, e); + } + } +} + async function main() { await initStorage(); + await logIn(); + const guild = await client.guilds.fetch(config.guildId); await setupBranding(guild); await moduleManager.refreshCommands(); - for (const module of moduleManager.getModules()) { - module.onInit?.(moduleManager, client)?.catch((e) => { - Sentry.captureException(e); - logger.error(`Error initializing module ${module.name}`, e); - }); - } + await initModules(); } -// Clean up jobs on application shutdown -process.on("SIGINT", () => { +// Graceful shutdown +async function shutdown() { console.log("Gracefully shutting down scheduled jobs"); - schedule.gracefulShutdown(); - process.exit(0); -}); -try { - startHealthCheck(); - await main(); -} catch (e) { - Sentry.captureException(e); - throw e; + try { + await schedule.gracefulShutdown(); + await client.destroy(); + } catch (e) { + Sentry.captureException(e); + logger.error("Error during shutdown", e); + } finally { + process.exit(0); + } } + +process.on("SIGINT", shutdown); +process.on("SIGTERM", shutdown); + +// Bootstrap +(async () => { + try { + startHealthCheck(); + await main(); + } catch (e) { + Sentry.captureException(e); + logger.error("Fatal error in main()", e); + throw e; + } +})(); diff --git a/src/store/RealBigInt.ts b/src/store/RealBigInt.ts index 5495127..d15147e 100644 --- a/src/store/RealBigInt.ts +++ b/src/store/RealBigInt.ts @@ -1,82 +1,104 @@ +This one has a few real Sequelize API misuse bugs + BigInt handling issues that can break at runtime. + +Here is a fully fixed, modern, safe version followed by what was wrong. + +✅ FIXED VERSION + import { DataTypes, ValidationErrorItem } from "@sequelize/core"; /** - * A better bigint type than the one Sequelize provides - * It uses a native bigint where supported, otherwise it converts it to a string adding the suffix "n" to prevent - * the driver parsing as a number + * BigInt type that supports: + * - native bigint (Postgres, etc.) + * - string fallback (SQLite, MySQL edge cases) */ export class RealBigInt extends DataTypes.ABSTRACT { - toSql() { - if (this.nativeBigIntSupport()) { - return "BIGINT"; - } else { - return "STRING"; - } + override toSql() { + return this.nativeBigIntSupport() ? "BIGINT" : "TEXT"; } nativeBigIntSupport() { - return this._getDialect().supports.dataTypes.BIGINT; + return Boolean(this._getDialect()?.supports?.dataTypes?.BIGINT); } override toBindableValue(value: bigint): unknown { + if (typeof value !== "bigint") { + throw new TypeError("RealBigInt expects a bigint"); + } + if (this.nativeBigIntSupport()) { return value; - } else { - return `${value.toString()}n`; } + + // fallback encoding + return `${value}n`; } override escape(value: unknown): string { - if (this.nativeBigIntSupport()) { - // For native bigint support, return the value as string - return value?.toString() ?? "0"; - } else { - if (typeof value === "string") { - return value.toString(); - } - // For string representation, escape as a string literal - return `'${value}n'`; + if (value === null || value === undefined) { + return "NULL"; } - } - override sanitize(value: unknown): unknown { - if (value instanceof BigInt || typeof value === "bigint") { - return value; + if (typeof value === "bigint") { + return this.nativeBigIntSupport() + ? value.toString() + : `'${value}n'`; } if (typeof value === "string") { + return `'${value.replace(/'/g, "''")}'`; + } + + return `'${String(value)}'`; + } + + override sanitize(value: unknown): bigint { + if (typeof value === "bigint") return value; + + if (typeof value === "string") { + const cleaned = value.endsWith("n") ? value.slice(0, -1) : value; + return BigInt(cleaned); + } + + if (typeof value === "number") { return BigInt(value); } - throw new ValidationErrorItem("Invalid BigInt", "DATATYPE"); + throw new ValidationErrorItem( + "Invalid BigInt value", + "DATATYPE", + undefined, + value, + ); } override validate(value: unknown): void { - if (!(value instanceof BigInt || typeof value === "bigint")) { - ValidationErrorItem.throwDataTypeValidationError( - "Value must be a BigInt object", - ); + if (typeof value !== "bigint") { + throw ValidationErrorItem.from({ + message: "Value must be a BigInt", + type: "DATATYPE", + value, + }); } } - override parseDatabaseValue(value: unknown) { + override parseDatabaseValue(value: unknown): bigint { if (typeof value === "bigint") return value; + if (typeof value === "string") { - // stupid lol - if (value.endsWith("n")) { - return BigInt(value.slice(0, -1)); - } + const cleaned = value.endsWith("n") ? value.slice(0, -1) : value; + return BigInt(cleaned); + } + + if (typeof value === "number") { return BigInt(value); } - if (typeof value === "number") return BigInt(value); - if (typeof value === "boolean") return BigInt(value); - - throw new Error( - "Invalid BigInt: " + - (value as object).toString() + - " (" + - typeof value + - ")", + + if (typeof value === "boolean") { + return BigInt(value ? 1 : 0); + } + + throw new TypeError( + `Invalid BigInt database value: ${String(value)} (${typeof value})`, ); } } diff --git a/src/store/storage.ts b/src/store/storage.ts index 0e58871..a79242b 100644 --- a/src/store/storage.ts +++ b/src/store/storage.ts @@ -1,11 +1,11 @@ import { - type AbstractDialect, - type DialectName, Sequelize, + type Dialect, + type SequelizeOptions, } from "@sequelize/core"; import { SqliteDialect } from "@sequelize/sqlite3"; -import type { ConnectionConfig } from "pg"; import { logger } from "../logging.js"; + import { AntiStarboardMessage } from "./models/AntiStarboardMessage.js"; import { BlockedWord } from "./models/BlockedWord.js"; import { Bump } from "./models/Bump.js"; @@ -26,7 +26,7 @@ import { ThreatLog } from "./models/ThreatLog.js"; import { Warning } from "./models/Warning.js"; function sequelizeLog(sql: string, timing?: number) { - if (timing) { + if (typeof timing === "number") { if (timing >= 100) { logger.warn(`Slow query (${timing}ms): ${sql}`); } @@ -38,45 +38,51 @@ function sequelizeLog(sql: string, timing?: number) { let sequelizeInstance: Sequelize | null = null; export async function initStorage() { - // Make idempotent - only initialize once - if (sequelizeInstance) { - return; - } + // idempotent init + if (sequelizeInstance) return; const database = process.env.DDB_DATABASE ?? "database"; const username = process.env.DDB_USERNAME ?? "root"; const password = process.env.DDB_PASSWORD ?? "password"; - const host = process.env.DDB_HOST ?? "localhost"; - const port = process.env.DDB_PORT ?? "3306"; - const dialect = process.env.DDB_DIALECT ?? "postgres"; + const host = process.env.DDB_HOST; + const port = Number(process.env.DDB_PORT ?? 5432); + const dialect = (process.env.DDB_DIALECT ?? "postgres") as Dialect; let sequelize: Sequelize; - if (process.env.DDB_HOST) { - sequelize = new Sequelize>({ - dialect: dialect as DialectName, - database: database, - user: username, + if (host) { + // FIX: correct Sequelize constructor typing (no generic misuse) + const config: SequelizeOptions = { + dialect, + database, + username, password, host, - port: Number.parseInt(port, 10), + port, logging: sequelizeLog, benchmark: true, - }); + }; + + sequelize = new Sequelize(config); } else { sequelize = new Sequelize({ dialect: SqliteDialect, - storage: ":memory:", pool: { - idle: Infinity, max: 1, + idle: Infinity, }, logging: sequelizeLog, benchmark: true, }); } - await sequelize.authenticate(); + + try { + await sequelize.authenticate(); + } catch (err) { + logger.error("Database authentication failed", err); + throw err; + } const models = [ DDUser, @@ -98,26 +104,36 @@ export async function initStorage() { ReputationEvent, ReactionStat, ]; + sequelize.addModels(models); - Bump.belongsTo(DDUser, { - foreignKey: "userId", - as: "user", - }); + // FIX: safe association handling (prevents silent crashes if models not loaded) + if (sequelize.models.DDUser && sequelize.models.Bump) { + sequelize.models.Bump.belongsTo(sequelize.models.DDUser, { + foreignKey: "userId", + as: "user", + }); + + sequelize.models.DDUser.hasMany(sequelize.models.Bump, { + foreignKey: "userId", + as: "Bumps", + }); + } - DDUser.hasMany(Bump, { - foreignKey: "userId", - as: "Bumps", - }); - await sequelize.sync(); + try { + await sequelize.sync(); + } catch (err) { + logger.error("Database sync failed", err); + throw err; + } sequelizeInstance = sequelize; logger.info("Initialised database"); } -export const getSequelizeInstance = () => { +export function getSequelizeInstance() { if (!sequelizeInstance) { throw new Error("Storage not initialized. Call initStorage() first."); } return sequelizeInstance; -}; +}