From 6791423c85066bdd0fd164f9933ffa895902ad26 Mon Sep 17 00:00:00 2001 From: vesaber Date: Sun, 15 Mar 2026 23:24:03 +0100 Subject: [PATCH 1/2] feat: Rigged slots..... --- commands/slots.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/commands/slots.ts b/commands/slots.ts index 9bad1eb..7861e3e 100644 --- a/commands/slots.ts +++ b/commands/slots.ts @@ -33,21 +33,22 @@ const command: CommandSchema = { return; } - const reels = [ - SYMBOLS[Math.floor(Math.random() * SYMBOLS.length)]!, - SYMBOLS[Math.floor(Math.random() * SYMBOLS.length)]!, - SYMBOLS[Math.floor(Math.random() * SYMBOLS.length)]!, + const r = [ + Math.floor(Math.random() * 36), + Math.floor(Math.random() * 36), + Math.floor(Math.random() * 36), ]; + const reels = r.map(v => SYMBOLS[v % SYMBOLS.length]!); const display = `[ ${reels.join(" ")} ]`; let result: string; let newBalance: number; - if (reels[0] === reels[1] && reels[1] === reels[2]) { + if (r[0] === r[1] && r[1] === r[2]) { const win = amount * 9; newBalance = await addBalance(message.guildId, message.author.id, win); result = `🎰 **JACKPOT!** You win **${win}** coins!`; - } else if (reels[0] === reels[1] || reels[1] === reels[2] || reels[0] === reels[2]) { + } else if (r[0] === r[1] || r[1] === r[2] || r[0] === r[2]) { const win = Math.floor(amount * 0.5); newBalance = await addBalance(message.guildId, message.author.id, win); result = `Two of a kind! You win **${win}** coins.`; From 7fcf5a096c323ebb31a60b25252a0fcbd0cc53a4 Mon Sep 17 00:00:00 2001 From: vesaber Date: Sat, 21 Mar 2026 10:50:54 +0100 Subject: [PATCH 2/2] feat: opt-out, level channel, level roles, dynamic help --- bun.lock | 50 ++++++++++----------- commands/8ball.ts | 1 + commands/about.ts | 1 + commands/balance.ts | 1 + commands/ban.ts | 1 + commands/bet.ts | 1 + commands/blackjack.ts | 1 + commands/coin.ts | 1 + commands/daily.ts | 1 + commands/give.ts | 1 + commands/help.ts | 60 ++++++++++++++++++-------- commands/highlow.ts | 1 + commands/index.ts | 5 ++- commands/kick.ts | 1 + commands/leaderboard.ts | 1 + commands/levelchannel.ts | 50 +++++++++++++++++++++ commands/levelrole.ts | 93 ++++++++++++++++++++++++++++++++++++++++ commands/optout.ts | 33 ++++++++++++++ commands/ping.ts | 1 + commands/rank.ts | 15 ++++++- commands/roll.ts | 1 + commands/roulette.ts | 1 + commands/slots.ts | 1 + commands/timeout.ts | 1 + commands/unban.ts | 1 + commands/untimeout.ts | 1 + commands/unwarn.ts | 1 + commands/warn.ts | 1 + commands/warnings.ts | 1 + database/index.ts | 83 ++++++++++++++++++++++++++++++++++- index.ts | 34 ++++++++++++++- package.json | 4 +- utils/CommandHandler.ts | 10 +++-- utils/CommandSchema.ts | 40 +++++++++++++++++ 34 files changed, 444 insertions(+), 55 deletions(-) create mode 100644 commands/levelchannel.ts create mode 100644 commands/levelrole.ts create mode 100644 commands/optout.ts diff --git a/bun.lock b/bun.lock index 07597ed..8c6a97b 100644 --- a/bun.lock +++ b/bun.lock @@ -5,10 +5,10 @@ "": { "name": "capacitor", "dependencies": { - "@fluxerjs/core": "^1.2.1", - "@napi-rs/canvas": "^0.1.95", + "@fluxerjs/core": "^1.2.3", + "@napi-rs/canvas": "^0.1.96", "gifenc": "^1.0.3", - "sequelize": "^6.37.7", + "sequelize": "^6.37.8", "sharp": "^0.34.5", "sqlite3": "^5.1.7", }, @@ -16,26 +16,26 @@ "@types/bun": "latest", }, "peerDependencies": { - "typescript": "^5", + "typescript": "^5.9.3", }, }, }, "packages": { "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - "@fluxerjs/builders": ["@fluxerjs/builders@1.2.3", "", { "dependencies": { "@fluxerjs/types": "1.2.3", "@fluxerjs/util": "1.2.3" } }, "sha512-VBeQtM9uEAY+WIpuHZzkx2tFtLNCgTRYcbEwrrb6s5h59rpPkEM2y94t4oYWDKCMVICffR69y1J3vggu0Q3x0w=="], + "@fluxerjs/builders": ["@fluxerjs/builders@1.2.4", "", { "dependencies": { "@fluxerjs/types": "1.2.4", "@fluxerjs/util": "1.2.4" } }, "sha512-jWOL8QsmNGgFKlyi9pGfREZv6oQm8T+wcdF1U+0RyW75SXf8meRBfqtGA6EUd8pwoP9qQeU4O7IIwbALUxVZYQ=="], - "@fluxerjs/collection": ["@fluxerjs/collection@1.2.3", "", {}, "sha512-1vEbqQl/PcfiYABjbiPAsKMg1UtpbjYS9b0JKwVdgPaChAF6NPusoU3vOuGzGxD2+JXK5P1V73UQQ8yKWrnUCQ=="], + "@fluxerjs/collection": ["@fluxerjs/collection@1.2.4", "", {}, "sha512-YXj+1yPBFdHRxSRIzWCYHqdynOOmjynmsLZGa1333chyYIn/hqEFjfQQi9nvNHN8EDJBjjH4OW+pF47K9XFALw=="], - "@fluxerjs/core": ["@fluxerjs/core@1.2.3", "", { "dependencies": { "@fluxerjs/builders": "1.2.3", "@fluxerjs/collection": "1.2.3", "@fluxerjs/rest": "1.2.3", "@fluxerjs/types": "1.2.3", "@fluxerjs/util": "1.2.3", "@fluxerjs/ws": "1.2.3" } }, "sha512-i0lkgO7Q+DePuX3Td0kYW1U94ljG5Nup14tXlECEaTP3FZcLu3ygSy0Km1OjYVoNAsxQZkOFr1J6A6r5PH5WLQ=="], + "@fluxerjs/core": ["@fluxerjs/core@1.2.4", "", { "dependencies": { "@fluxerjs/builders": "1.2.4", "@fluxerjs/collection": "1.2.4", "@fluxerjs/rest": "1.2.4", "@fluxerjs/types": "1.2.4", "@fluxerjs/util": "1.2.4", "@fluxerjs/ws": "1.2.4" } }, "sha512-LIKLaDxhSgyKxF4i8949+d5HgAh9YxtN+C3NCariTXE/1kIuvl8kCoLwdEINtIOzvOCz7OlO0pYXDm63Re36cw=="], - "@fluxerjs/rest": ["@fluxerjs/rest@1.2.3", "", { "dependencies": { "@fluxerjs/types": "1.2.3" } }, "sha512-e1eodt4rNHtqtqlDL4Kp2JUsvHigS0w/LKfIrGu5nndX+LaVoh5Lp3afzWSnFilW6gqNHsqIEIgsmOGeJNP0CA=="], + "@fluxerjs/rest": ["@fluxerjs/rest@1.2.4", "", { "dependencies": { "@fluxerjs/types": "1.2.4" } }, "sha512-9K9aFL0bbL8dgWUCYaiIQJBZyTtFD00iMl61V6fDlvCTFyidXh2NjvUQD7rdOSZTtb8xvWbvLXX+xtBUD9fbuA=="], - "@fluxerjs/types": ["@fluxerjs/types@1.2.3", "", {}, "sha512-oyxJfaPIpJYBSEj/is+YNk3JE2c0QVvBmc9Qr5wAmc0hGzTp1Oh2Vff8HwK14R2xL5bJde79U0eyxHxwMlf+rg=="], + "@fluxerjs/types": ["@fluxerjs/types@1.2.4", "", {}, "sha512-cGtN3WvcJn78kP3X5TFfVnbNYaRSNu05HFdSgUsYNrdNzulVxRuvUlCSlFD/xpZBaljQUBg04K6fCWqpWcGYEQ=="], - "@fluxerjs/util": ["@fluxerjs/util@1.2.3", "", { "dependencies": { "@fluxerjs/types": "1.2.3" } }, "sha512-vxDlxnQV3sTAAdZGrRQrmEx+O8Uzydw+HMx6vF4l0nxeTRwAZgR19CApY6r0tZk2mnHwZO0pqfaXbB4dHL4bGw=="], + "@fluxerjs/util": ["@fluxerjs/util@1.2.4", "", { "dependencies": { "@fluxerjs/types": "1.2.4" } }, "sha512-RSlMv77Zklr+kIP+ZZLdhP6ogTAuf9USUxdFP+6GafGQp+7wG7gStlMWrQ7JMVeUQwGd6AZlf5jr+v154RCH3Q=="], - "@fluxerjs/ws": ["@fluxerjs/ws@1.2.3", "", { "dependencies": { "@fluxerjs/types": "1.2.3", "ws": "^8.18.0" } }, "sha512-0aQn1myqChfO2pABYVdm1bSqWOQrDURt7Bzh42hNsiTUGiJIkyf78r24k6X1JDHXuhUEd9MTBkIDCqQ7LD1K6w=="], + "@fluxerjs/ws": ["@fluxerjs/ws@1.2.4", "", { "dependencies": { "@fluxerjs/types": "1.2.4", "ws": "^8.18.0" } }, "sha512-DBJjka7d2yuE+aaEoroOB3dSPCMNrOtWM5U1IAkgP8jLQCFkFuf98H+2geuYrPGto2++c6uW/1GKHI4U5bxJeQ=="], "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="], @@ -89,29 +89,29 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - "@napi-rs/canvas": ["@napi-rs/canvas@0.1.96", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.96", "@napi-rs/canvas-darwin-arm64": "0.1.96", "@napi-rs/canvas-darwin-x64": "0.1.96", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.96", "@napi-rs/canvas-linux-arm64-gnu": "0.1.96", "@napi-rs/canvas-linux-arm64-musl": "0.1.96", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.96", "@napi-rs/canvas-linux-x64-gnu": "0.1.96", "@napi-rs/canvas-linux-x64-musl": "0.1.96", "@napi-rs/canvas-win32-arm64-msvc": "0.1.96", "@napi-rs/canvas-win32-x64-msvc": "0.1.96" } }, "sha512-6NNmNxvoJKeucVjxaaRUt3La2i5jShgiAbaY3G/72s1Vp3U06XPrAIxkAjBxpDcamEn/t+WJ4OOlGmvILo4/Ew=="], + "@napi-rs/canvas": ["@napi-rs/canvas@0.1.97", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.97", "@napi-rs/canvas-darwin-arm64": "0.1.97", "@napi-rs/canvas-darwin-x64": "0.1.97", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.97", "@napi-rs/canvas-linux-arm64-gnu": "0.1.97", "@napi-rs/canvas-linux-arm64-musl": "0.1.97", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.97", "@napi-rs/canvas-linux-x64-gnu": "0.1.97", "@napi-rs/canvas-linux-x64-musl": "0.1.97", "@napi-rs/canvas-win32-arm64-msvc": "0.1.97", "@napi-rs/canvas-win32-x64-msvc": "0.1.97" } }, "sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ=="], - "@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@0.1.96", "", { "os": "android", "cpu": "arm64" }, "sha512-ew1sPrN3dGdZ3L4FoohPfnjq0f9/Jk7o+wP7HkQZokcXgIUD6FIyICEWGhMYzv53j63wUcPvZeAwgewX58/egg=="], + "@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@0.1.97", "", { "os": "android", "cpu": "arm64" }, "sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ=="], - "@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@0.1.96", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Q/wOXZ5PzTqpdmA5eUOcegCf4Go/zz3aZ5DlzSeDpOjFmfwMKh8EzLAoweQ+mJVagcHQyzoJhaTEnrO68TNyNg=="], + "@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@0.1.97", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw=="], - "@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@0.1.96", "", { "os": "darwin", "cpu": "x64" }, "sha512-UrXiQz28tQEvGM1qvyptewOAfmUrrd5+wvi6Rzjj2VprZI8iZ2KIvBD2lTTG1bVF95AbeDeG7PJA0D9sLKaOFA=="], + "@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@0.1.97", "", { "os": "darwin", "cpu": "x64" }, "sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA=="], - "@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@0.1.96", "", { "os": "linux", "cpu": "arm" }, "sha512-I90ODxweD8aEP6XKU/NU+biso95MwCtQ2F46dUvhec1HesFi0tq/tAJkYic/1aBSiO/1kGKmSeD1B0duOHhEHQ=="], + "@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@0.1.97", "", { "os": "linux", "cpu": "arm" }, "sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw=="], - "@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@0.1.96", "", { "os": "linux", "cpu": "arm64" }, "sha512-Dx/0+RFV++w3PcRy+4xNXkghhXjA5d0Mw1bs95emn5Llinp1vihMaA6WJt3oYv2LAHc36+gnrhIBsPhUyI2SGw=="], + "@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@0.1.97", "", { "os": "linux", "cpu": "arm64" }, "sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w=="], - "@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@0.1.96", "", { "os": "linux", "cpu": "arm64" }, "sha512-UvOi7fii3IE2KDfEfhh8m+LpzSRvhGK7o1eho99M2M0HTik11k3GX+2qgVx9EtujN3/bhFFS1kSO3+vPMaJ0Mg=="], + "@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@0.1.97", "", { "os": "linux", "cpu": "arm64" }, "sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ=="], - "@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@0.1.96", "", { "os": "linux", "cpu": "none" }, "sha512-MBSukhGCQ5nRtf9NbFYWOU080yqkZU1PbuH4o1ROvB4CbPl12fchDR35tU83Wz8gWIM9JTn99lBn9DenPIv7Ig=="], + "@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@0.1.97", "", { "os": "linux", "cpu": "none" }, "sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA=="], - "@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@0.1.96", "", { "os": "linux", "cpu": "x64" }, "sha512-I/ccu2SstyKiV3HIeVzyBIWfrJo8cN7+MSQZPnabewWV6hfJ2nY7Df2WqOHmobBRUw84uGR6zfQHsUEio/m5Vg=="], + "@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@0.1.97", "", { "os": "linux", "cpu": "x64" }, "sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg=="], - "@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@0.1.96", "", { "os": "linux", "cpu": "x64" }, "sha512-H3uov7qnTl73GDT4h52lAqpJPsl1tIUyNPWJyhQ6gHakohNqqRq3uf80+NEpzcytKGEOENP1wX3yGwZxhjiWEQ=="], + "@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@0.1.97", "", { "os": "linux", "cpu": "x64" }, "sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA=="], - "@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@0.1.96", "", { "os": "win32", "cpu": "arm64" }, "sha512-ATp6Y+djOjYtkfV/VRH7CZ8I1MEtkUQBmKUbuWw5zWEHHqfL0cEcInE4Cxgx7zkNAhEdBbnH8HMVrqNp+/gwxA=="], + "@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@0.1.97", "", { "os": "win32", "cpu": "arm64" }, "sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A=="], - "@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.96", "", { "os": "win32", "cpu": "x64" }, "sha512-UYGdTltVd+Z8mcIuoqGmAXXUvwH5CLf2M6mIB5B0/JmX5J041jETjqtSYl7gN+aj3k1by/SG6sS0hAwCqyK7zw=="], + "@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.97", "", { "os": "win32", "cpu": "x64" }, "sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ=="], "@npmcli/fs": ["@npmcli/fs@1.1.1", "", { "dependencies": { "@gar/promisify": "^1.0.1", "semver": "^7.3.5" } }, "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ=="], @@ -119,7 +119,7 @@ "@tootallnate/once": ["@tootallnate/once@1.1.2", "", {}, "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="], - "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], @@ -155,7 +155,7 @@ "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "cacache": ["cacache@15.3.0", "", { "dependencies": { "@npmcli/fs": "^1.0.0", "@npmcli/move-file": "^1.0.1", "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "glob": "^7.1.4", "infer-owner": "^1.0.4", "lru-cache": "^6.0.0", "minipass": "^3.1.1", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.2", "mkdirp": "^1.0.3", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^8.0.1", "tar": "^6.0.2", "unique-filename": "^1.1.1" } }, "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ=="], diff --git a/commands/8ball.ts b/commands/8ball.ts index 507ae80..54a0fd9 100644 --- a/commands/8ball.ts +++ b/commands/8ball.ts @@ -28,6 +28,7 @@ const responses = [ const command: CommandSchema = { name: "8ball", + category: "Fun", description: "Ask the magic 8-ball a question.", params: "", requireElevated: false, diff --git a/commands/about.ts b/commands/about.ts index b6a8ce7..47055b3 100644 --- a/commands/about.ts +++ b/commands/about.ts @@ -3,6 +3,7 @@ import type { CommandSchema } from "../utils/CommandSchema"; const command: CommandSchema = { name: "about", + category: "General", description: "About Capacitor", requireElevated: false, diff --git a/commands/balance.ts b/commands/balance.ts index 284b7c8..b8bd2ef 100644 --- a/commands/balance.ts +++ b/commands/balance.ts @@ -6,6 +6,7 @@ import { generateBalanceCard } from "../utils/leveling"; const command: CommandSchema = { name: "balance", + category: "Economy", description: "Show a balance card (or another user's).", params: "[@user|id]", requireElevated: false, diff --git a/commands/ban.ts b/commands/ban.ts index 15fc8d6..955878c 100644 --- a/commands/ban.ts +++ b/commands/ban.ts @@ -5,6 +5,7 @@ import { parseMention } from "../utils/moderation"; const command: CommandSchema = { name: "ban", + category: "Moderation", description: "Ban a member from the server.", params: "<@user|id> [reason...]", requireElevated: [Permissions.BanMembers], diff --git a/commands/bet.ts b/commands/bet.ts index 4dd85a6..2f77488 100644 --- a/commands/bet.ts +++ b/commands/bet.ts @@ -4,6 +4,7 @@ import { getBalance, addBalance, deleteBalance } from "../database"; const command: CommandSchema = { name: "bet", + category: "Gambling", description: "Roll a d6 and bet on the outcome. Correct guess pays 5x.", params: " <1-6>", requireElevated: false, diff --git a/commands/blackjack.ts b/commands/blackjack.ts index 6d7ce0e..054e524 100644 --- a/commands/blackjack.ts +++ b/commands/blackjack.ts @@ -53,6 +53,7 @@ async function resolve(k: string, game: BJGame, guildId: string, userId: string, const command: CommandSchema = { name: "blackjack", + category: "Gambling", description: "Play blackjack against the dealer.", params: "", requireElevated: false, diff --git a/commands/coin.ts b/commands/coin.ts index 1db5fa6..9b8c63d 100644 --- a/commands/coin.ts +++ b/commands/coin.ts @@ -3,6 +3,7 @@ import type { CommandSchema } from "../utils/CommandSchema"; const command: CommandSchema = { name: "coin", + category: "Fun", description: "Flip a coin.", requireElevated: false, diff --git a/commands/daily.ts b/commands/daily.ts index c16c62e..c51f62b 100644 --- a/commands/daily.ts +++ b/commands/daily.ts @@ -5,6 +5,7 @@ import { formatDuration } from "../utils/moderation"; const command: CommandSchema = { name: "daily", + category: "Economy", description: "Claim your daily coins (150–250, 24h cooldown).", params: "", requireElevated: false, diff --git a/commands/give.ts b/commands/give.ts index 4b50039..342941c 100644 --- a/commands/give.ts +++ b/commands/give.ts @@ -5,6 +5,7 @@ import { parseMention } from "../utils/moderation"; const command: CommandSchema = { name: "give", + category: "Economy", description: "Give coins to another user.", params: "<@user|id> ", requireElevated: false, diff --git a/commands/help.ts b/commands/help.ts index cc2b8fe..b0547ac 100644 --- a/commands/help.ts +++ b/commands/help.ts @@ -1,41 +1,65 @@ import { EmbedBuilder } from "@fluxerjs/core"; import type { CommandSchema } from "../utils/CommandSchema"; +import { Permissions, PERM_BITS } from "../utils/CommandSchema"; import * as commands from "./index"; const allCommands = () => Object.values(commands) as CommandSchema[]; -const CATEGORIES: { label: string; names: string[] }[] = [ - { label: "Moderation", names: ["kick", "ban", "unban", "timeout", "untimeout", "warn", "unwarn", "warnings"] }, - { label: "Economy", names: ["balance", "daily", "give"] }, - { label: "Leveling", names: ["rank", "leaderboard"] }, - { label: "Gambling", names: ["bet", "blackjack", "highlow", "roulette", "slots"] }, - { label: "Fun", names: ["8ball", "coin", "roll"] }, - { label: "General", names: ["about", "ping", "help"] }, -]; +const CATEGORY_ORDER = ["Moderation", "Economy", "Leveling", "Gambling", "Fun", "General"]; const command: CommandSchema = { name: "help", + category: "General", description: "List all commands or get info on a specific one.", params: "[command]", requireElevated: false, async run(params, message) { if (params.length === 0) { - const cmds = allCommands(); + let userPerms = 0n; + let botPerms = 0n; + if (message.guildId && message.guild) { + try { + const [member, botMember] = await Promise.all([ + message.guild.fetchMember(message.author.id), + message.guild.fetchMember(message.client.user!.id), + ]); + userPerms = member.permissions.bitfield; + botPerms = botMember.permissions.bitfield; + } catch {} + } + + const hasPerms = (perms: bigint, required: Permissions[]) => + required.every((p) => { + const bit = PERM_BITS[Permissions[p]]; + return bit === undefined || (perms & bit) !== 0n; + }); + + const canUse = (cmd: CommandSchema) => { + if (cmd.requireElevated === false) return true; + return hasPerms(userPerms, cmd.requireElevated) && hasPerms(botPerms, cmd.requireElevated); + }; + + const cmds = allCommands().filter(canUse); + + const grouped = new Map(); + for (const cmd of cmds) { + const cat = cmd.category ?? "Other"; + if (!grouped.has(cat)) grouped.set(cat, []); + grouped.get(cat)!.push(cmd); + } + + const known = CATEGORY_ORDER.filter((c) => grouped.has(c)); + const extra = [...grouped.keys()].filter((c) => !CATEGORY_ORDER.includes(c)).sort(); + const embed = new EmbedBuilder() .setColor(0x2D8A4E) .setTitle("Capacitor Commands") .setDescription("Use `c!help ` for info on a specific command."); - for (const category of CATEGORIES) { - const lines = category.names - .map((name) => cmds.find((c) => c.name === name)) - .filter((c): c is CommandSchema => c !== undefined) - .map((c) => `\`c!${c.name}\` — ${c.description}`); - - if (lines.length > 0) { - embed.addFields({ name: category.label, value: lines.join("\n") }); - } + for (const cat of [...known, ...extra]) { + const lines = grouped.get(cat)!.map((c) => `\`c!${c.name}\` — ${c.description}`); + embed.addFields({ name: cat, value: lines.join("\n") }); } await message.reply({ embeds: [embed] }); diff --git a/commands/highlow.ts b/commands/highlow.ts index ac8e44b..601ef68 100644 --- a/commands/highlow.ts +++ b/commands/highlow.ts @@ -5,6 +5,7 @@ import { newDeck, cardRank, fmt } from "../utils/cards"; const command: CommandSchema = { name: "highlow", + category: "Gambling", description: "Guess if the next card is higher or lower.", params: " ", requireElevated: false, diff --git a/commands/index.ts b/commands/index.ts index ae5f6d8..ec84e40 100644 --- a/commands/index.ts +++ b/commands/index.ts @@ -22,5 +22,8 @@ import blackjack from "./blackjack"; import highlow from "./highlow"; import roulette from "./roulette"; import slots from "./slots"; +import optout from "./optout"; +import levelchannel from "./levelchannel"; +import levelrole from "./levelrole"; -export { about, kick, ban, unban, timeout, untimeout, warn, unwarn, warnings, rank, leaderboard, balance, daily, give, eightball, coin, roll, ping, help, bet, blackjack, highlow, roulette, slots }; +export { about, kick, ban, unban, timeout, untimeout, warn, unwarn, warnings, rank, leaderboard, balance, daily, give, eightball, coin, roll, ping, help, bet, blackjack, highlow, roulette, slots, optout, levelchannel, levelrole }; diff --git a/commands/kick.ts b/commands/kick.ts index 84e83e9..3260ff5 100644 --- a/commands/kick.ts +++ b/commands/kick.ts @@ -5,6 +5,7 @@ import { parseMention } from "../utils/moderation"; const command: CommandSchema = { name: "kick", + category: "Moderation", description: "Kick a member from the server.", params: "<@user|id> [reason...]", requireElevated: [Permissions.KickMembers], diff --git a/commands/leaderboard.ts b/commands/leaderboard.ts index a7842e7..010ed54 100644 --- a/commands/leaderboard.ts +++ b/commands/leaderboard.ts @@ -7,6 +7,7 @@ const PAGE_SIZE = 10; const command: CommandSchema = { name: "leaderboard", + category: "Leveling", description: "Show the XP or economy leaderboard for this server.", params: "", requireElevated: false, diff --git a/commands/levelchannel.ts b/commands/levelchannel.ts new file mode 100644 index 0000000..1043fcc --- /dev/null +++ b/commands/levelchannel.ts @@ -0,0 +1,50 @@ +import { EmbedBuilder } from "@fluxerjs/core"; +import type { CommandSchema } from "../utils/CommandSchema"; +import { Permissions } from "../utils/CommandSchema"; +import { setLevelChannel } from "../database"; + +function parseChannelMention(str: string): string | null { + const match = str.match(/^<#(\d+)>$/) ?? str.match(/^(\d+)$/); + return match?.[1] ?? null; +} + +const command: CommandSchema = { + name: "levelchannel", + category: "Leveling", + description: "Set or clear the channel for level-up announcements.", + params: "[#channel | off]", + requireElevated: [Permissions.ManageChannels], + + async run(params, message) { + if (!message.guildId) { + await message.reply({ + embeds: [new EmbedBuilder().setColor(0x2D8A4E).setDescription("This command can only be used in a server.")], + }); + return; + } + + const arg = params[0]?.toLowerCase(); + if (!arg || arg === "off") { + await setLevelChannel(message.guildId, null); + await message.reply({ + embeds: [new EmbedBuilder().setColor(0x2D8A4E).setDescription("Level-up announcements will be sent in the channel where the message was sent.")], + }); + return; + } + + const channelId = parseChannelMention(params[0]!); + if (!channelId) { + await message.reply({ + embeds: [new EmbedBuilder().setColor(0x2D8A4E).setDescription("Invalid channel. Usage: `c!levelchannel [#channel | off]`")], + }); + return; + } + + await setLevelChannel(message.guildId, channelId); + await message.reply({ + embeds: [new EmbedBuilder().setColor(0x2D8A4E).setDescription(`Level-up announcements will now be sent to <#${channelId}>.`)], + }); + }, +}; + +export default command; diff --git a/commands/levelrole.ts b/commands/levelrole.ts new file mode 100644 index 0000000..1c9d71f --- /dev/null +++ b/commands/levelrole.ts @@ -0,0 +1,93 @@ +import { EmbedBuilder } from "@fluxerjs/core"; +import type { CommandSchema } from "../utils/CommandSchema"; +import { Permissions } from "../utils/CommandSchema"; +import { getLevelRoles, setLevelRole, removeLevelRole } from "../database"; + +function parseRoleMention(str: string): string | null { + const match = str.match(/^<@&(\d+)>$/) ?? str.match(/^(\d+)$/); + return match?.[1] ?? null; +} + +const command: CommandSchema = { + name: "levelrole", + category: "Leveling", + description: "Manage roles assigned when users reach a level.", + params: " <@role> | remove | list>", + requireElevated: [Permissions.ManageRoles], + + async run(params, message) { + if (!message.guildId) { + await message.reply({ + embeds: [new EmbedBuilder().setColor(0x2D8A4E).setDescription("This command can only be used in a server.")], + }); + return; + } + + const sub = params[0]?.toLowerCase(); + + if (sub === "list") { + const roles = await getLevelRoles(message.guildId); + if (roles.length === 0) { + await message.reply({ + embeds: [new EmbedBuilder().setColor(0x2D8A4E).setDescription("No level roles configured.")], + }); + return; + } + const lines = roles.map((r) => `**Level ${r.level}** → <@&${r.role_id}>`).join("\n"); + await message.reply({ + embeds: [new EmbedBuilder().setColor(0x2D8A4E).setTitle("Level Roles").setDescription(lines)], + }); + return; + } + + if (sub === "add") { + const level = parseInt(params[1] ?? ""); + const roleId = parseRoleMention(params[2] ?? ""); + if (isNaN(level) || level < 1 || !roleId) { + await message.reply({ + embeds: [new EmbedBuilder().setColor(0x2D8A4E).setDescription("Usage: `c!levelrole add <@role>`")], + }); + return; + } + await setLevelRole(message.guildId, level, roleId); + await message.reply({ + embeds: [new EmbedBuilder().setColor(0x2D8A4E).setDescription(`<@&${roleId}> will be assigned when users reach **Level ${level}**.`)], + }); + return; + } + + if (sub === "remove") { + const level = parseInt(params[1] ?? ""); + if (isNaN(level) || level < 1) { + await message.reply({ + embeds: [new EmbedBuilder().setColor(0x2D8A4E).setDescription("Usage: `c!levelrole remove `")], + }); + return; + } + const removed = await removeLevelRole(message.guildId, level); + await message.reply({ + embeds: [ + new EmbedBuilder() + .setColor(0x2D8A4E) + .setDescription(removed ? `Level role for **Level ${level}** removed.` : `No level role was set for **Level ${level}**.`), + ], + }); + return; + } + + await message.reply({ + embeds: [ + new EmbedBuilder() + .setColor(0x2D8A4E) + .setTitle("Usage") + .setDescription( + "`c!levelrole add <@role>` — Set a role for a level\n" + + "`c!levelrole remove ` — Remove a level role\n" + + "`c!levelrole list` — Show all configured level roles" + ), + ], + }); + }, +}; + +export default command; diff --git a/commands/optout.ts b/commands/optout.ts new file mode 100644 index 0000000..6f24a03 --- /dev/null +++ b/commands/optout.ts @@ -0,0 +1,33 @@ +import { EmbedBuilder } from "@fluxerjs/core"; +import type { CommandSchema } from "../utils/CommandSchema"; +import { isOptedOut, setOptOut } from "../database"; + +const command: CommandSchema = { + name: "optout", + category: "Leveling", + description: "Toggle XP tracking opt-out for yourself in this server.", + requireElevated: false, + + async run(_params, message) { + if (!message.guildId) { + await message.reply({ + embeds: [new EmbedBuilder().setColor(0x2D8A4E).setDescription("This command can only be used in a server.")], + }); + return; + } + + const current = await isOptedOut(message.guildId, message.author.id); + const newValue = !current; + await setOptOut(message.guildId, message.author.id, newValue); + + const text = newValue + ? "You have opted **out of** XP tracking in this server." + : "You have opted **back into** XP tracking in this server."; + + await message.reply({ + embeds: [new EmbedBuilder().setColor(0x2D8A4E).setDescription(text)], + }); + }, +}; + +export default command; diff --git a/commands/ping.ts b/commands/ping.ts index 6aee168..e336356 100644 --- a/commands/ping.ts +++ b/commands/ping.ts @@ -3,6 +3,7 @@ import type { CommandSchema } from "../utils/CommandSchema"; const command: CommandSchema = { name: "ping", + category: "General", description: "Check the bot's latency.", requireElevated: false, diff --git a/commands/rank.ts b/commands/rank.ts index 05ea706..72d08ba 100644 --- a/commands/rank.ts +++ b/commands/rank.ts @@ -1,11 +1,12 @@ import { EmbedBuilder } from "@fluxerjs/core"; import type { CommandSchema } from "../utils/CommandSchema"; import { parseMention } from "../utils/moderation"; -import { getUserLevel, getRank } from "../database"; +import { getUserLevel, getRank, isOptedOut } from "../database"; import { generateRankCard } from "../utils/leveling"; const command: CommandSchema = { name: "rank", + category: "Leveling", description: "Show your rank card (or another user's).", params: "[@user|id]", requireElevated: false, @@ -54,6 +55,18 @@ const command: CommandSchema = { return; } + if (await isOptedOut(message.guildId, targetUser.id)) { + const isSelf = targetUser.id === message.author.id; + await message.reply({ + embeds: [ + new EmbedBuilder() + .setColor(0x2D8A4E) + .setDescription(isSelf ? "You have opted out of XP tracking in this server." : "This user has opted out of XP tracking."), + ], + }); + return; + } + const { xp, level } = await getUserLevel(message.guildId, targetUser.id); const rank = await getRank(message.guildId, targetUser.id); diff --git a/commands/roll.ts b/commands/roll.ts index d7aa164..5ce1d54 100644 --- a/commands/roll.ts +++ b/commands/roll.ts @@ -6,6 +6,7 @@ const MAX_SIDES = 1000; const command: CommandSchema = { name: "roll", + category: "Fun", description: "Roll dice. Format: NdN (e.g. 2d6). Default: 1d6.", params: "[NdN]", requireElevated: false, diff --git a/commands/roulette.ts b/commands/roulette.ts index 2b04387..8fae34c 100644 --- a/commands/roulette.ts +++ b/commands/roulette.ts @@ -14,6 +14,7 @@ const HELP = const command: CommandSchema = { name: "roulette", + category: "Gambling", description: "Spin the roulette wheel.", params: " ", requireElevated: false, diff --git a/commands/slots.ts b/commands/slots.ts index 7861e3e..7858bc1 100644 --- a/commands/slots.ts +++ b/commands/slots.ts @@ -6,6 +6,7 @@ const SYMBOLS = ["🍒", "🍋", "🍊", "🍇", "⭐", "💎"]; const command: CommandSchema = { name: "slots", + category: "Gambling", description: "Spin the slot machine. 3-match pays 10x, 2-match pays 1.5x.", params: "", requireElevated: false, diff --git a/commands/timeout.ts b/commands/timeout.ts index bd5dc72..b4469d8 100644 --- a/commands/timeout.ts +++ b/commands/timeout.ts @@ -5,6 +5,7 @@ import { parseMention, parseDuration, formatDuration } from "../utils/moderation const command: CommandSchema = { name: "timeout", + category: "Moderation", description: "Temporarily time out a member.", params: "<@user|id> [reason...]", additionalInfo: "Duration examples: `10m`, `1h`, `2h30m`, `1d`", diff --git a/commands/unban.ts b/commands/unban.ts index e52be99..76b3c4a 100644 --- a/commands/unban.ts +++ b/commands/unban.ts @@ -5,6 +5,7 @@ import { parseMention } from "../utils/moderation"; const command: CommandSchema = { name: "unban", + category: "Moderation", description: "Unban a user from the server.", params: "", requireElevated: [Permissions.BanMembers], diff --git a/commands/untimeout.ts b/commands/untimeout.ts index 008d91f..32f5402 100644 --- a/commands/untimeout.ts +++ b/commands/untimeout.ts @@ -5,6 +5,7 @@ import { parseMention } from "../utils/moderation"; const command: CommandSchema = { name: "untimeout", + category: "Moderation", description: "Remove a timeout from a member.", params: "<@user|id>", requireElevated: [Permissions.ModerateMembers], diff --git a/commands/unwarn.ts b/commands/unwarn.ts index 03c2c2e..42cbd1e 100644 --- a/commands/unwarn.ts +++ b/commands/unwarn.ts @@ -5,6 +5,7 @@ import { deleteWarn } from "../database"; const command: CommandSchema = { name: "unwarn", + category: "Moderation", description: "Remove an active warning by ID.", params: "", requireElevated: [Permissions.ModerateMembers], diff --git a/commands/warn.ts b/commands/warn.ts index 868b14d..cd87d00 100644 --- a/commands/warn.ts +++ b/commands/warn.ts @@ -6,6 +6,7 @@ import { createWarn } from "../database"; const command: CommandSchema = { name: "warn", + category: "Moderation", description: "Issue a timed warning to a member.", params: "<@user|id> [reason...]", additionalInfo: "Duration examples: `1d`, `7d`, `1h`. The warn expires after this period.", diff --git a/commands/warnings.ts b/commands/warnings.ts index 94088b6..87e405f 100644 --- a/commands/warnings.ts +++ b/commands/warnings.ts @@ -6,6 +6,7 @@ import { getActiveWarns } from "../database"; const command: CommandSchema = { name: "warnings", + category: "Moderation", description: "List active warnings for a member.", params: "<@user|id>", requireElevated: [Permissions.ModerateMembers], diff --git a/database/index.ts b/database/index.ts index 2acc58b..ec4ae1a 100644 --- a/database/index.ts +++ b/database/index.ts @@ -47,6 +47,7 @@ export class UserLevel extends Model { declare guild_id: string; declare user_id: string; declare xp: number; + declare opted_out: boolean; } UserLevel.init( @@ -54,6 +55,7 @@ UserLevel.init( guild_id: { type: DataTypes.STRING, allowNull: false }, user_id: { type: DataTypes.STRING, allowNull: false }, xp: { type: DataTypes.INTEGER, defaultValue: 0 }, + opted_out: { type: DataTypes.BOOLEAN, defaultValue: false }, }, { sequelize, @@ -62,6 +64,38 @@ UserLevel.init( } ); +export class GuildConfig extends Model { + declare guild_id: string; + declare level_channel_id: string | null; +} + +GuildConfig.init( + { + guild_id: { type: DataTypes.STRING, primaryKey: true }, + level_channel_id: { type: DataTypes.STRING, allowNull: true }, + }, + { sequelize, modelName: "GuildConfig" } +); + +export class LevelRole extends Model { + declare guild_id: string; + declare level: number; + declare role_id: string; +} + +LevelRole.init( + { + guild_id: { type: DataTypes.STRING, allowNull: false }, + level: { type: DataTypes.INTEGER, allowNull: false }, + role_id: { type: DataTypes.STRING, allowNull: false }, + }, + { + sequelize, + modelName: "LevelRole", + indexes: [{ unique: true, fields: ["guild_id", "level"] }], + } +); + export class UserBalance extends Model { declare guild_id: string; declare user_id: string; @@ -210,7 +244,7 @@ export async function getLeaderboard( guildId: string ): Promise> { const rows = await UserLevel.findAll({ - where: { guild_id: guildId }, + where: { guild_id: guildId, opted_out: false }, order: [["xp", "DESC"]], }); return rows.map((r) => ({ user_id: r.user_id, xp: r.xp, level: computeLevel(r.xp) })); @@ -220,7 +254,7 @@ export async function getRank(guildId: string, userId: string): Promise const target = await UserLevel.findOne({ where: { guild_id: guildId, user_id: userId } }); if (!target) return -1; const above = await UserLevel.count({ - where: { guild_id: guildId, xp: { [Op.gt]: target.xp } }, + where: { guild_id: guildId, xp: { [Op.gt]: target.xp }, opted_out: false }, }); return above + 1; } @@ -251,3 +285,48 @@ export async function deleteWarn(guildId: string, warnId: string): Promise 0; } + +export async function setOptOut(guildId: string, userId: string, value: boolean): Promise { + const [row] = await UserLevel.findOrCreate({ + where: { guild_id: guildId, user_id: userId }, + defaults: { xp: 0, opted_out: value }, + }); + row.opted_out = value; + await row.save(); +} + +export async function isOptedOut(guildId: string, userId: string): Promise { + const row = await UserLevel.findOne({ where: { guild_id: guildId, user_id: userId } }); + return row?.opted_out ?? false; +} + +export async function getLevelChannel(guildId: string): Promise { + const row = await GuildConfig.findOne({ where: { guild_id: guildId } }); + return row?.level_channel_id ?? null; +} + +export async function setLevelChannel(guildId: string, channelId: string | null): Promise { + await GuildConfig.upsert({ guild_id: guildId, level_channel_id: channelId }); +} + +export async function getLevelRole(guildId: string, level: number): Promise { + const row = await LevelRole.findOne({ where: { guild_id: guildId, level } }); + return row?.role_id ?? null; +} + +export async function setLevelRole(guildId: string, level: number, roleId: string): Promise { + await LevelRole.upsert({ guild_id: guildId, level, role_id: roleId }); +} + +export async function removeLevelRole(guildId: string, level: number): Promise { + const deleted = await LevelRole.destroy({ where: { guild_id: guildId, level } }); + return deleted > 0; +} + +export async function getLevelRoles(guildId: string): Promise> { + const rows = await LevelRole.findAll({ + where: { guild_id: guildId }, + order: [["level", "ASC"]], + }); + return rows.map((r) => ({ level: r.level, role_id: r.role_id })); +} diff --git a/index.ts b/index.ts index 052ba72..86d9a4a 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,6 @@ import { Client, Events } from "@fluxerjs/core"; import CommandHandler from "./utils/CommandHandler"; -import { initDatabase, addXP } from "./database"; +import { initDatabase, addXP, isOptedOut, getLevelChannel, getLevelRole } from "./database"; import { canEarnXP } from "./utils/leveling"; const client = new Client({ @@ -24,12 +24,42 @@ client.on(Events.MessageCreate, async (message) => { if (!message.guildId) return; if (message.content.length < 3) return; if (!canEarnXP(message.author.id)) return; + if (await isOptedOut(message.guildId, message.author.id)) return; try { const amount = Math.floor(Math.random() * 21) + 10; // 10–30 const { before, after } = await addXP(message.guildId, message.author.id, amount); if (after > before) { - await message.send(`GG <@${message.author.id}>, you leveled up to **Level ${after}**!`); + const levelUpMsg = `GG <@${message.author.id}>, you leveled up to **Level ${after}**!`; + + const levelChannelId = await getLevelChannel(message.guildId); + if (levelChannelId) { + try { + const channel = await client.channels.fetch(levelChannelId); + if (channel?.isTextBased()) { + await channel.send({ content: levelUpMsg }); + } else { + await message.send(levelUpMsg); + } + } catch { + await message.send(levelUpMsg); + } + } else { + await message.send(levelUpMsg); + } + + const roleId = await getLevelRole(message.guildId, after); + if (roleId) { + try { + const guild = await message.resolveGuild(); + if (guild) { + const member = await guild.fetchMember(message.author.id); + await member.roles.add(roleId); + } + } catch (err) { + console.error(`[XP] Role assignment failed for ${message.author.id} level ${after}:`, err); + } + } } } catch (err) { console.error(`[XP] Failed for ${message.author.id} in ${message.guildId}:`, err); diff --git a/package.json b/package.json index f649580..8a4f14e 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "typescript": "^5.9.3" }, "dependencies": { - "@fluxerjs/core": "^1.2.3", - "@napi-rs/canvas": "^0.1.96", + "@fluxerjs/core": "^1.2.4", + "@napi-rs/canvas": "^0.1.97", "gifenc": "^1.0.3", "sequelize": "^6.37.8", "sharp": "^0.34.5", diff --git a/utils/CommandHandler.ts b/utils/CommandHandler.ts index 90cff5b..de9950d 100644 --- a/utils/CommandHandler.ts +++ b/utils/CommandHandler.ts @@ -1,7 +1,7 @@ import { Message, EmbedBuilder } from "@fluxerjs/core"; import * as commands from "../commands"; import type { CommandSchema } from "./CommandSchema"; -import { Permissions } from "./CommandSchema"; +import { Permissions, PERM_BITS } from "./CommandSchema"; const PREFIX = "c!"; @@ -43,9 +43,11 @@ export async function CommandHandler(message: Message) { return; } const member = await guild.fetchMember(message.author.id); - const missing = command.requireElevated.filter( - (p) => !member.permissions.has(Permissions[p] as Parameters[0]) - ); + const perms = member.permissions.bitfield; + const missing = command.requireElevated.filter((p) => { + const bit = PERM_BITS[Permissions[p]]; + return bit !== undefined && (perms & bit) === 0n; + }); if (missing.length > 0) { await message.reply({ embeds: [ diff --git a/utils/CommandSchema.ts b/utils/CommandSchema.ts index f66408b..3da3089 100644 --- a/utils/CommandSchema.ts +++ b/utils/CommandSchema.ts @@ -1,4 +1,5 @@ import { Message } from "@fluxerjs/core"; +import { PermissionFlags } from "@fluxerjs/util"; export enum Permissions { ManageRoles, @@ -38,9 +39,48 @@ export enum Permissions { SetVoiceRegion, } +export const PERM_BITS: Partial> = { + ManageRoles: PermissionFlags.ManageRoles, + BanMembers: PermissionFlags.BanMembers, + KickMembers: PermissionFlags.KickMembers, + ManageNicknames: PermissionFlags.ManageNicknames, + ManageWebhooks: PermissionFlags.ManageWebhooks, + SendTTSMessages: PermissionFlags.SendTtsMessages, + EmbedLinks: PermissionFlags.EmbedLinks, + PingMentions: PermissionFlags.MentionEveryone, + AddReactions: PermissionFlags.AddReactions, + JoinVoice: PermissionFlags.Connect, + UseVoiceActivity: PermissionFlags.UseVad, + DeafenMembers: PermissionFlags.DeafenMembers, + ViewActivityLog: PermissionFlags.ViewAuditLog, + ManageChannels: PermissionFlags.ManageChannels, + ManageCommunity: PermissionFlags.ManageGuild, + CreateInviteLinks: PermissionFlags.CreateInstantInvite, + CreateEmojisStickers: PermissionFlags.CreateExpressions, + ViewChannel: PermissionFlags.ViewChannel, + ManageMessages: PermissionFlags.ManageMessages, + AttachFiles: PermissionFlags.AttachFiles, + UseExternalEmoji: PermissionFlags.UseExternalEmojis, + UseExternalStickers: PermissionFlags.UseExternalStickers, + BypassSlowmode: PermissionFlags.BypassSlowmode, + Speak: PermissionFlags.Speak, + PrioritySpeaker: PermissionFlags.PrioritySpeaker, + MoveMembers: PermissionFlags.MoveMembers, + ChangeOwnNickname: PermissionFlags.ChangeNickname, + ManageEmojisStickers: PermissionFlags.ManageEmojisAndStickers, + SendMessages: PermissionFlags.SendMessages, + PinMessages: PermissionFlags.PinMessages, + ReadMessageHistory: PermissionFlags.ReadMessageHistory, + ModerateMembers: PermissionFlags.ModerateMembers, + StreamVideo: PermissionFlags.Stream, + MuteMembers: PermissionFlags.MuteMembers, + SetVoiceRegion: PermissionFlags.UpdateRtcRegion, +}; + export type CommandSchema = { name: string; description: string; + category: string; requireElevated: Permissions[] | false; requireOwner?: boolean; requireWhitelist?: boolean;