From a96da2066e083bd5ae288123b6f02680856035c0 Mon Sep 17 00:00:00 2001 From: MrMasrozYTLIVE <61359286+MrMasrozYTLIVE@users.noreply.github.com> Date: Fri, 12 Jul 2024 22:32:07 +0300 Subject: [PATCH] Added Command System --- .gitignore | 3 +- package.json | 8 +++-- src/Client.ts | 91 ++++++++++++++++++++++++++++++++++++++++++++++---- src/Command.ts | 16 +++++++++ test/Bot.ts | 12 +++++++ tsconfig.json | 23 +++++++++++++ 6 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 src/Command.ts create mode 100644 test/Bot.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 5857946..0db66c0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ package-lock.json node_modules docs -storage \ No newline at end of file +storage +test/ \ No newline at end of file diff --git a/package.json b/package.json index 8b1a3bf..dbb94b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "linkup-bot-lib", - "version": "1.1.0", + "version": "1.2", "main": "lib/Client.js", "types": "lib/*.ts", "scripts": { @@ -18,11 +18,13 @@ "corestore": "^6.18.2", "hyperdrive": "^11.8.1", "hyperswarm": "^4.7.15", - "serve-drive": "^5.0.8" + "serve-drive": "^5.0.8", + "glob": "7.1.6" }, "devDependencies": { "@types/b4a": "^1.6.4", "@types/node": "^20.14.2", - "typedoc-plugin-merge-modules": "^5.1.0" + "typedoc-plugin-merge-modules": "^5.1.0", + "@types/glob": "^8.1.0" } } diff --git a/src/Client.ts b/src/Client.ts index b86b845..4960a08 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -12,11 +12,17 @@ import ServeDrive from 'serve-drive'; import {IconMessage} from "./message/IconMessage"; import {TypedEventEmitter} from "./util/TypedEventEmitter"; import {LinkUpEvents} from "./LinkUpEvents"; +import {Command} from "./Command"; +import { glob } from "glob"; +import { normalize } from "path"; +import { promisify } from "util"; /** * This class is the core component of the bot system. It handles connections to the Hyperswarm network, manages message sending and receiving, and emits events for various actions. */ export class Client extends TypedEventEmitter { + public static _INSTANCE: Client; + public botName: string = ""; public servePort: number | null = 0; public storagePath: string | undefined; @@ -28,20 +34,21 @@ export class Client extends TypedEventEmitter { public botAvatar: string = ""; public iconMessage: IconMessage | undefined; public discovery: PeerDiscovery | undefined; + public commands: Command[] = [] + private commandPrefix: string; /** * @param botName The name of the bot. + * @param commandPrefix Prefix for bot commands * @since 1.0 * @constructor * @author snxraven */ - constructor(botName: string) { + constructor(botName: string, commandPrefix: string) { super(); - if (!botName) { - console.error("Bot Name is not defined!"); - return; - } + Client._INSTANCE = this; this.botName = botName; + this.commandPrefix = commandPrefix; this.swarm = new Hyperswarm(); this.joinedRooms = new Set(); // Track the rooms the bot has joined this.currentTopic = null; // Track the current topic @@ -75,6 +82,20 @@ export class Client extends TypedEventEmitter { console.log('HyperSwarm was shut down. Exiting the process with exit code 0.'); process.exit(0); }); + + this.on("onMessage", (msg: TextMessage) => { + const message = msg.message; + if(!message.startsWith(this.commandPrefix)) return; + const [commandName, ...args] = message.slice(this.commandPrefix.length).split(' '); + + const command = this.commands.find(c => c.options.name === commandName || c.options.aliases?.indexOf(commandName) !== -1); + if(command) { + console.log(`Executing command: ${command.options.name} (${command.options.aliases?.join(", ")}) with arguments: [${args.join(", ")}]`); + command.handler(this, msg, args); + } else { + console.warn(`Command not found: ${command}`); + } + }) } /** @@ -326,4 +347,62 @@ export class Client extends TypedEventEmitter { await this.swarm?.destroy(); console.log(`Bot ${this.botName} disconnected.`); } -} \ No newline at end of file + + /** + * @description Adds command to the bot Commands array + * @param command Command to register + * @since 1.2 + * @author MiTask + */ + registerCommand(command: Command) { + console.log(`Registering command "${command.options.name}" with aliases: [${command.options.aliases?.join(", ")}]`) + this.commands.push(command) + } + + /** + * @description Removes command from the bot Commands array + * @param command Command to unregister + * @since 1.2 + * @author MiTask + */ + unregisterCommand(command: Command) { + console.log(`Unregistering command "${command.options.name}"`) + this.commands = this.commands.filter(cmd => cmd.options.name !== command.options.name) + } + + /** + * @description Registers all classes that extend Command class on specified path + * @param path Path to search for commands (Must be full path. For example using __dirname) + * @since 1.2 + * @author MiTask + */ + public async registerCommands(path: String) { + const commands = await promisify(glob)(normalize(path + "/**/*.{ts,js}")); + for (const commandPath of commands) { + try { + let command: MaybeCommand = await import(commandPath); + if ('default' in command) command = command.default; + if (command.constructor.name === 'Object') command = Object.values(command)[0]; + + const instance = new (command as Constructor)(); + if (!instance.options || !instance.options.name) { + console.log(`Invalid command class (Missing options or options.name) at ${commandPath}`) + continue; + } + + this.registerCommand(instance) + } catch (e) { + if(e instanceof TypeError) { + console.warn(`Invalid command class at ${commandPath}`) + continue; + } + + const error = (e instanceof Error) ? e.message : String(e) + console.log(`Error during loading the command ${commandPath}:\n${error}`) + } + } + } +} + +export type Constructor = new (...args: any[]) => T; +export type MaybeCommand = Constructor | {default: Constructor} | {[k: string]: Constructor}; \ No newline at end of file diff --git a/src/Command.ts b/src/Command.ts new file mode 100644 index 0000000..7416128 --- /dev/null +++ b/src/Command.ts @@ -0,0 +1,16 @@ +import {Client} from "./Client" +import {TextMessage} from "./message/TextMessage"; + +export class Command { + constructor(public options: ICommandOption) { + + } + + handler(bot: Client, message: TextMessage, args: string[]) {} +} + +export interface ICommandOption { + name: string, + description?: string, + aliases?: string[] +} \ No newline at end of file diff --git a/test/Bot.ts b/test/Bot.ts new file mode 100644 index 0000000..4ade213 --- /dev/null +++ b/test/Bot.ts @@ -0,0 +1,12 @@ +import {Client} from "../src/Client"; + +const bot = new Client("testBot", ">>"); + +bot.registerCommands(__dirname).then(() => { + bot.on('onBotJoinRoom', () => { + console.log("Bot is ready!"); + bot.sendTextMessage("Bot is ready!"); + }); + + bot.joinChatRoom("fdc8aad933cde0d88f15cb395dfe2e24e1731d7622c890828d8eef9608e52437"); +}) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..917b043 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ESNext", + "sourceMap": true, + "declaration": true, + "declarationDir": "./lib/", + "outDir": "./lib/", + "allowJs": true, + "allowSyntheticDefaultImports": true, + "typeRoots": ["./types", "./node_modules/@types"], + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true + }, + "include": [ + "./src/**/*" + ], + "exclude": [ + "node_modules", + ] +} \ No newline at end of file