Compare commits
9 Commits
f0e8f5e939
...
f424336eba
| Author | SHA1 | Date | |
|---|---|---|---|
| f424336eba | |||
| efe2cb7458 | |||
| eb40c4d736 | |||
| 5d29e531e5 | |||
| 8e3f2c3904 | |||
| fd16528c07 | |||
| 7bb7d6abdb | |||
| edaf360e2e | |||
| d975547efe |
Generated
+122
@@ -20,6 +20,7 @@
|
|||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.6.1",
|
||||||
|
"rimraf": "^6.1.2",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.52.0"
|
"typescript-eslint": "^8.52.0"
|
||||||
@@ -806,6 +807,29 @@
|
|||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@isaacs/balanced-match": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@isaacs/brace-expansion": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/balanced-match": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sapphire/async-queue": {
|
"node_modules/@sapphire/async-queue": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
||||||
@@ -1695,6 +1719,24 @@
|
|||||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glob": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
|
||||||
|
"integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"minimatch": "^10.1.1",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"path-scurry": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -1708,6 +1750,22 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glob/node_modules/minimatch": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/brace-expansion": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
"version": "17.0.0",
|
"version": "17.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz",
|
||||||
@@ -1901,6 +1959,16 @@
|
|||||||
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "11.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||||
|
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-bytes.js": {
|
"node_modules/magic-bytes.js": {
|
||||||
"version": "1.12.1",
|
"version": "1.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz",
|
||||||
@@ -1920,6 +1988,16 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minipass": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -1984,6 +2062,13 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/package-json-from-dist": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0"
|
||||||
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -2017,6 +2102,23 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-scurry": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^11.0.0",
|
||||||
|
"minipass": "^7.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
@@ -2085,6 +2187,26 @@
|
|||||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rimraf": {
|
||||||
|
"version": "6.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz",
|
||||||
|
"integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^13.0.0",
|
||||||
|
"package-json-from-dist": "^1.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rimraf": "dist/esm/bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
|
|||||||
+3
-2
@@ -8,7 +8,7 @@
|
|||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"dev": "tsx watch src/index.ts",
|
"dev": "tsx watch src/index.ts",
|
||||||
"watch": "tsc --watch",
|
"watch": "tsc --watch",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rimraf dist",
|
||||||
"build:clean": "npm run clean && npm run build",
|
"build:clean": "npm run clean && npm run build",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"format": "prettier --check './src/**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc",
|
"format": "prettier --check './src/**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc",
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.6.1",
|
||||||
|
"rimraf": "^6.1.2",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.52.0"
|
"typescript-eslint": "^8.52.0"
|
||||||
@@ -38,4 +39,4 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"prettier": "^3.7.4"
|
"prettier": "^3.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-66
@@ -10,6 +10,7 @@ import {
|
|||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { Logger } from './utils/log';
|
import { Logger } from './utils/log';
|
||||||
import { config } from './utils/config';
|
import { config } from './utils/config';
|
||||||
|
import { CommandManager } from './commands';
|
||||||
|
|
||||||
type BotEventListeners = {
|
type BotEventListeners = {
|
||||||
messageCreate: (message: Message) => void;
|
messageCreate: (message: Message) => void;
|
||||||
@@ -20,9 +21,7 @@ type BotEventListener<K extends keyof BotEventListeners> = BotEventListeners[K];
|
|||||||
|
|
||||||
export class Bot {
|
export class Bot {
|
||||||
private client: Client | undefined;
|
private client: Client | undefined;
|
||||||
|
private cmdMgr: CommandManager;
|
||||||
private readonly token: string;
|
|
||||||
private readonly clientId: string;
|
|
||||||
|
|
||||||
private readonly log: Logger;
|
private readonly log: Logger;
|
||||||
|
|
||||||
@@ -30,73 +29,12 @@ export class Bot {
|
|||||||
[K in keyof BotEventListeners]?: BotEventListener<K>[];
|
[K in keyof BotEventListeners]?: BotEventListener<K>[];
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
/*
|
|
||||||
event methods
|
|
||||||
*/
|
|
||||||
public on<K extends keyof BotEventListeners>(
|
|
||||||
event: K,
|
|
||||||
listener: BotEventListener<K>
|
|
||||||
): void {
|
|
||||||
if (!this.eventListeners[event]) {
|
|
||||||
this.eventListeners[event] = [];
|
|
||||||
}
|
|
||||||
(this.eventListeners[event] as BotEventListener<K>[]).push(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public off<K extends keyof BotEventListeners>(
|
|
||||||
event: K,
|
|
||||||
listener: BotEventListener<K>
|
|
||||||
): boolean {
|
|
||||||
const listeners = this.eventListeners[event];
|
|
||||||
if (!listeners) return false;
|
|
||||||
|
|
||||||
const index = (listeners as BotEventListener<K>[]).indexOf(listener);
|
|
||||||
if (index > -1) {
|
|
||||||
(listeners as BotEventListener<K>[]).splice(index, 1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public once<K extends keyof BotEventListeners>(
|
|
||||||
event: K,
|
|
||||||
listener: BotEventListener<K>
|
|
||||||
): void {
|
|
||||||
const onceWrapper = ((...args: Parameters<BotEventListener<K>>) => {
|
|
||||||
this.off(event, onceWrapper as BotEventListener<K>);
|
|
||||||
(listener as (...args: unknown[]) => void)(...args);
|
|
||||||
}) as BotEventListener<K>;
|
|
||||||
|
|
||||||
this.on(event, onceWrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
private emit<K extends keyof BotEventListeners>(
|
|
||||||
event: K,
|
|
||||||
...args: Parameters<BotEventListener<K>>
|
|
||||||
): void {
|
|
||||||
const listeners = this.eventListeners[event];
|
|
||||||
if (listeners) {
|
|
||||||
for (const listener of listeners as BotEventListener<K>[]) {
|
|
||||||
try {
|
|
||||||
(listener as (...args: unknown[]) => void)(...args);
|
|
||||||
} catch (error) {
|
|
||||||
this.log.error(
|
|
||||||
`Error in event listener for ${String(event)}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
class methods
|
class methods
|
||||||
*/
|
*/
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.log = new Logger('Bot');
|
this.log = new Logger('Bot');
|
||||||
|
this.cmdMgr = new CommandManager('./commands');
|
||||||
this.token = config.token;
|
|
||||||
this.clientId = config.client_id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async init(): Promise<void> {
|
public async init(): Promise<void> {
|
||||||
@@ -105,6 +43,9 @@ export class Bot {
|
|||||||
if (this.client)
|
if (this.client)
|
||||||
throw new Error('Client already exists, was init called twice?');
|
throw new Error('Client already exists, was init called twice?');
|
||||||
|
|
||||||
|
this.log.info('Loading commands');
|
||||||
|
await this.cmdMgr.init();
|
||||||
|
|
||||||
this.log.info('Instantiating client');
|
this.log.info('Instantiating client');
|
||||||
this.client = new Client({
|
this.client = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
@@ -132,7 +73,7 @@ export class Bot {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.log.info('Logging in...');
|
this.log.info('Logging in...');
|
||||||
await this.client.login(this.token);
|
await this.client.login(config.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -196,6 +137,65 @@ export class Bot {
|
|||||||
this.emit('voiceStateUpdate', oldState, newState);
|
this.emit('voiceStateUpdate', oldState, newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
registerable event system
|
||||||
|
*/
|
||||||
|
public on<K extends keyof BotEventListeners>(
|
||||||
|
event: K,
|
||||||
|
listener: BotEventListener<K>
|
||||||
|
): void {
|
||||||
|
if (!this.eventListeners[event]) {
|
||||||
|
this.eventListeners[event] = [];
|
||||||
|
}
|
||||||
|
(this.eventListeners[event] as BotEventListener<K>[]).push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public off<K extends keyof BotEventListeners>(
|
||||||
|
event: K,
|
||||||
|
listener: BotEventListener<K>
|
||||||
|
): boolean {
|
||||||
|
const listeners = this.eventListeners[event];
|
||||||
|
if (!listeners) return false;
|
||||||
|
|
||||||
|
const index = (listeners as BotEventListener<K>[]).indexOf(listener);
|
||||||
|
if (index > -1) {
|
||||||
|
(listeners as BotEventListener<K>[]).splice(index, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public once<K extends keyof BotEventListeners>(
|
||||||
|
event: K,
|
||||||
|
listener: BotEventListener<K>
|
||||||
|
): void {
|
||||||
|
const onceWrapper = ((...args: Parameters<BotEventListener<K>>) => {
|
||||||
|
this.off(event, onceWrapper as BotEventListener<K>);
|
||||||
|
(listener as (...args: unknown[]) => void)(...args);
|
||||||
|
}) as BotEventListener<K>;
|
||||||
|
|
||||||
|
this.on(event, onceWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private emit<K extends keyof BotEventListeners>(
|
||||||
|
event: K,
|
||||||
|
...args: Parameters<BotEventListener<K>>
|
||||||
|
): void {
|
||||||
|
const listeners = this.eventListeners[event];
|
||||||
|
if (listeners) {
|
||||||
|
for (const listener of listeners as BotEventListener<K>[]) {
|
||||||
|
try {
|
||||||
|
(listener as (...args: unknown[]) => void)(...args);
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error(
|
||||||
|
`Error in event listener for ${String(event)}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
singleton logic
|
singleton logic
|
||||||
*/
|
*/
|
||||||
|
|||||||
+323
@@ -0,0 +1,323 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AutocompleteInteraction,
|
||||||
|
BaseInteraction,
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
GuildMember,
|
||||||
|
Interaction,
|
||||||
|
Message,
|
||||||
|
MessageFlags,
|
||||||
|
PermissionFlagsBits,
|
||||||
|
REST,
|
||||||
|
Routes,
|
||||||
|
SlashCommandOptionsOnlyBuilder,
|
||||||
|
VoiceState
|
||||||
|
} from 'discord.js';
|
||||||
|
import { Logger } from './utils/log';
|
||||||
|
import { config } from './utils/config';
|
||||||
|
import { Bot } from './bot';
|
||||||
|
|
||||||
|
export interface Command {
|
||||||
|
name?: string;
|
||||||
|
requiresAdmin: boolean;
|
||||||
|
ownerOnly?: boolean;
|
||||||
|
execute?: (interaction: ChatInputCommandInteraction) => Promise<void>;
|
||||||
|
autocomplete?: (interaction: AutocompleteInteraction) => Promise<void>;
|
||||||
|
messageListener?: (msg: Message) => Promise<void>;
|
||||||
|
voiceStateListener?: (
|
||||||
|
prevState: VoiceState,
|
||||||
|
newState: VoiceState
|
||||||
|
) => Promise<void>;
|
||||||
|
builder?: SlashCommandOptionsOnlyBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommandCategoryInfo {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommandCategory {
|
||||||
|
info: CommandCategoryInfo;
|
||||||
|
commands: Command[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CommandManager {
|
||||||
|
private readonly folderPath: string;
|
||||||
|
private readonly log: Logger;
|
||||||
|
|
||||||
|
private initialized: boolean = false;
|
||||||
|
private categories: Array<CommandCategory> = [];
|
||||||
|
|
||||||
|
/*
|
||||||
|
public
|
||||||
|
*/
|
||||||
|
public constructor(cmdFolder: string) {
|
||||||
|
this.folderPath = path.resolve(__dirname, cmdFolder);
|
||||||
|
this.log = new Logger('Command manager');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async init(): Promise<void> {
|
||||||
|
if (this.initialized)
|
||||||
|
throw new Error('CommandManager was already initialized');
|
||||||
|
this.initialized = true;
|
||||||
|
await this.populateCommands();
|
||||||
|
this.registerSlashCommands();
|
||||||
|
|
||||||
|
const bot = Bot.get;
|
||||||
|
|
||||||
|
bot.on('interactionCreate', (interaction: Interaction) => {
|
||||||
|
this.onInteraction(interaction);
|
||||||
|
});
|
||||||
|
bot.on('messageCreate', (message: Message) => {
|
||||||
|
this.onMessage(message);
|
||||||
|
});
|
||||||
|
bot.on('voiceStateUpdate', (oldState: VoiceState, newState: VoiceState) => {
|
||||||
|
this.onVoiceStateUpdate(oldState, newState);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCategories(): Array<CommandCategory> {
|
||||||
|
return this.categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(commandName: string): Command | null {
|
||||||
|
return this.getAll().find((cmd) => cmd.name === commandName) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAll(): Command[] {
|
||||||
|
return this.categories.flatMap((cat) => cat.commands);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllInteractable(): Command[] {
|
||||||
|
return this.getAll().filter((cmd) => !!cmd.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
internal
|
||||||
|
*/
|
||||||
|
private async populateCommands() {
|
||||||
|
if (!fs.existsSync(this.folderPath))
|
||||||
|
throw new Error(`Command directory not found: ${this.folderPath}`);
|
||||||
|
|
||||||
|
const categoryFolders = fs.readdirSync(this.folderPath);
|
||||||
|
|
||||||
|
for (const categoryFolder of categoryFolders) {
|
||||||
|
const catPath = path.join(this.folderPath, categoryFolder);
|
||||||
|
await this.processCategory(catPath, categoryFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processCategory(
|
||||||
|
catPath: string,
|
||||||
|
folderName: string
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(catPath);
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
this.log.warning(
|
||||||
|
'Skipping non-directory entry found on cmd folder (%s)',
|
||||||
|
folderName
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const catInfo = await this.loadCatInfo(catPath);
|
||||||
|
const commands = await this.loadCatCommands(catPath);
|
||||||
|
|
||||||
|
if (catInfo == undefined) {
|
||||||
|
this.log.warning('Folder %s was missing info, ignoring', folderName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.categories.push({
|
||||||
|
info: catInfo,
|
||||||
|
commands: commands
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('Error processing category %s: %s', folderName, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadCatInfo(
|
||||||
|
catPath: string
|
||||||
|
): Promise<CommandCategoryInfo | undefined> {
|
||||||
|
try {
|
||||||
|
const descriptorPath = path.join(catPath, 'category.json');
|
||||||
|
if (!fs.existsSync(descriptorPath))
|
||||||
|
throw new Error('Missing categoryinfo.json');
|
||||||
|
|
||||||
|
const content = await fs.promises.readFile(descriptorPath, 'utf-8');
|
||||||
|
return JSON.parse(content) as CommandCategoryInfo;
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('Error loading category info at %s: %s', catPath, err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadCatCommands(catPath: string): Promise<Array<Command>> {
|
||||||
|
const promises = fs
|
||||||
|
.readdirSync(catPath)
|
||||||
|
.filter((file) => this.isValidCommandFile(file))
|
||||||
|
.map(
|
||||||
|
async (file) => await this.attemptLoadCommand(path.join(catPath, file))
|
||||||
|
);
|
||||||
|
return (await Promise.all(promises)).filter((cmd): cmd is Command => !!cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
cmd parsing
|
||||||
|
*/
|
||||||
|
private isValidCommandFile(file: string): boolean {
|
||||||
|
return file.endsWith('.js') || file.endsWith('.ts');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async attemptLoadCommand(filePath: string): Promise<Command | null> {
|
||||||
|
try {
|
||||||
|
const module = await import(`file://${filePath}`);
|
||||||
|
const command =
|
||||||
|
module.default?.default || module.default || (module as Command);
|
||||||
|
return command;
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error(
|
||||||
|
`Error loading command ${path.basename(filePath)}: ${error}`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
misc functions
|
||||||
|
*/
|
||||||
|
private async registerSlashCommands(): Promise<void> {
|
||||||
|
this.log.info('Registering slash commands...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cmdJSONList = this.getAll()
|
||||||
|
.filter((cmd) => cmd.builder !== undefined)
|
||||||
|
.map((cmd) => cmd.builder?.toJSON())
|
||||||
|
.filter((cmd) => cmd !== undefined);
|
||||||
|
|
||||||
|
const rest = new REST({ version: '10' }).setToken(config.token as string);
|
||||||
|
await rest.put(Routes.applicationCommands(config.client_id), {
|
||||||
|
body: cmdJSONList
|
||||||
|
});
|
||||||
|
|
||||||
|
this.log.info('Registered %i commands', cmdJSONList.length);
|
||||||
|
} catch (err) {
|
||||||
|
this.log.warning(
|
||||||
|
'Error occurred while registering slash commands: %s',
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
interaction handling
|
||||||
|
*/
|
||||||
|
private async executeCommandInteraction(
|
||||||
|
interaction: ChatInputCommandInteraction
|
||||||
|
): Promise<void> {
|
||||||
|
const cmdName = interaction.commandName;
|
||||||
|
const command = this.get(cmdName);
|
||||||
|
if (!command)
|
||||||
|
return this.log.error(
|
||||||
|
'Attempted to execute non-existing command (%s)',
|
||||||
|
cmdName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (command.requiresAdmin) {
|
||||||
|
const member = interaction.member as GuildMember;
|
||||||
|
if (!member.permissions.has(PermissionFlagsBits.Administrator)) {
|
||||||
|
await interaction.reply({
|
||||||
|
content:
|
||||||
|
"You don't have the permissions required to execute this command.",
|
||||||
|
flags: MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.ownerOnly) {
|
||||||
|
const member = interaction.member as GuildMember;
|
||||||
|
if (member.id != config.owner_id) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: 'This command is restricted.',
|
||||||
|
flags: MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.execute) {
|
||||||
|
try {
|
||||||
|
await command.execute(interaction);
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error(
|
||||||
|
'Error occurred while executing command %s: %s',
|
||||||
|
cmdName,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else this.log.error('Command is missing execute method: %s', cmdName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async autocompleteCommandInteraction(
|
||||||
|
interaction: AutocompleteInteraction
|
||||||
|
): Promise<void> {
|
||||||
|
const cmdName = interaction.commandName;
|
||||||
|
const command = this.get(cmdName);
|
||||||
|
|
||||||
|
if (!command)
|
||||||
|
return this.log.error(
|
||||||
|
'Attempted to execute unexisting command (%s)',
|
||||||
|
cmdName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (command.autocomplete) {
|
||||||
|
try {
|
||||||
|
await command.autocomplete(interaction);
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error(
|
||||||
|
'Error occurred while autocompleting command %s: %s',
|
||||||
|
cmdName,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
this.log.error('Command is missing autocomplete method: %s', cmdName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
event listeners
|
||||||
|
*/
|
||||||
|
private async onInteraction(interaction: BaseInteraction): Promise<void> {
|
||||||
|
/*
|
||||||
|
cmd execution
|
||||||
|
*/
|
||||||
|
if (interaction.isChatInputCommand())
|
||||||
|
return this.executeCommandInteraction(interaction);
|
||||||
|
|
||||||
|
if (interaction.isAutocomplete())
|
||||||
|
return this.autocompleteCommandInteraction(interaction);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onMessage(message: Message<boolean>): Promise<void> {
|
||||||
|
if (message.author.bot) return;
|
||||||
|
|
||||||
|
for (const cmd of this.getAll())
|
||||||
|
if (cmd.messageListener) await cmd.messageListener(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onVoiceStateUpdate(
|
||||||
|
oldState: VoiceState,
|
||||||
|
newState: VoiceState
|
||||||
|
): Promise<void> {
|
||||||
|
for (const cmd of this.getAll())
|
||||||
|
if (cmd.voiceStateListener)
|
||||||
|
await cmd.voiceStateListener(oldState, newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-21
@@ -1,39 +1,28 @@
|
|||||||
{
|
{
|
||||||
// Visit https://aka.ms/tsconfig to read more about this file
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// File Layout
|
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
// Environment Settings
|
|
||||||
// See also https://aka.ms/tsconfig/module
|
|
||||||
"module": "nodenext",
|
"module": "nodenext",
|
||||||
"target": "es2024",
|
"target": "es2024",
|
||||||
"types": ["node"],
|
"types": [
|
||||||
// For nodejs:
|
"node"
|
||||||
// "lib": ["esnext"],
|
],
|
||||||
// "types": ["node"],
|
|
||||||
// and npm install -D @types/node
|
|
||||||
// Other Outputs
|
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
// Stricter Typechecking Options
|
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
// Style Options
|
|
||||||
// "noImplicitReturns": true,
|
|
||||||
// "noImplicitOverride": true,
|
|
||||||
// "noUnusedLocals": true,
|
|
||||||
// "noUnusedParameters": true,
|
|
||||||
// "noFallthroughCasesInSwitch": true,
|
|
||||||
// "noPropertyAccessFromIndexSignature": true,
|
|
||||||
// Recommended Options
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
// "verbatimModuleSyntax": true,
|
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
}
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"eslint.config.mts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user