Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 56 additions & 21 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand All @@ -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: [
Expand Down Expand Up @@ -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;
}
})();
110 changes: 66 additions & 44 deletions src/store/RealBigInt.ts
Original file line number Diff line number Diff line change
@@ -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<bigint> {
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})`,
);
}
}
Loading
Loading