Added Command System

This commit is contained in:
MrMasrozYTLIVE 2024-07-12 22:32:07 +03:00
parent 623645090d
commit a96da2066e
6 changed files with 143 additions and 10 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ package-lock.json
node_modules node_modules
docs docs
storage storage
test/

View File

@ -1,6 +1,6 @@
{ {
"name": "linkup-bot-lib", "name": "linkup-bot-lib",
"version": "1.1.0", "version": "1.2",
"main": "lib/Client.js", "main": "lib/Client.js",
"types": "lib/*.ts", "types": "lib/*.ts",
"scripts": { "scripts": {
@ -18,11 +18,13 @@
"corestore": "^6.18.2", "corestore": "^6.18.2",
"hyperdrive": "^11.8.1", "hyperdrive": "^11.8.1",
"hyperswarm": "^4.7.15", "hyperswarm": "^4.7.15",
"serve-drive": "^5.0.8" "serve-drive": "^5.0.8",
"glob": "7.1.6"
}, },
"devDependencies": { "devDependencies": {
"@types/b4a": "^1.6.4", "@types/b4a": "^1.6.4",
"@types/node": "^20.14.2", "@types/node": "^20.14.2",
"typedoc-plugin-merge-modules": "^5.1.0" "typedoc-plugin-merge-modules": "^5.1.0",
"@types/glob": "^8.1.0"
} }
} }

View File

@ -12,11 +12,17 @@ import ServeDrive from 'serve-drive';
import {IconMessage} from "./message/IconMessage"; import {IconMessage} from "./message/IconMessage";
import {TypedEventEmitter} from "./util/TypedEventEmitter"; import {TypedEventEmitter} from "./util/TypedEventEmitter";
import {LinkUpEvents} from "./LinkUpEvents"; 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. * 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<LinkUpEvents> { export class Client extends TypedEventEmitter<LinkUpEvents> {
public static _INSTANCE: Client;
public botName: string = ""; public botName: string = "";
public servePort: number | null = 0; public servePort: number | null = 0;
public storagePath: string | undefined; public storagePath: string | undefined;
@ -28,20 +34,21 @@ export class Client extends TypedEventEmitter<LinkUpEvents> {
public botAvatar: string = ""; public botAvatar: string = "";
public iconMessage: IconMessage | undefined; public iconMessage: IconMessage | undefined;
public discovery: PeerDiscovery | undefined; public discovery: PeerDiscovery | undefined;
public commands: Command[] = []
private commandPrefix: string;
/** /**
* @param botName The name of the bot. * @param botName The name of the bot.
* @param commandPrefix Prefix for bot commands
* @since 1.0 * @since 1.0
* @constructor * @constructor
* @author snxraven * @author snxraven
*/ */
constructor(botName: string) { constructor(botName: string, commandPrefix: string) {
super(); super();
if (!botName) { Client._INSTANCE = this;
console.error("Bot Name is not defined!");
return;
}
this.botName = botName; this.botName = botName;
this.commandPrefix = commandPrefix;
this.swarm = new Hyperswarm(); this.swarm = new Hyperswarm();
this.joinedRooms = new Set(); // Track the rooms the bot has joined this.joinedRooms = new Set(); // Track the rooms the bot has joined
this.currentTopic = null; // Track the current topic this.currentTopic = null; // Track the current topic
@ -75,6 +82,20 @@ export class Client extends TypedEventEmitter<LinkUpEvents> {
console.log('HyperSwarm was shut down. Exiting the process with exit code 0.'); console.log('HyperSwarm was shut down. Exiting the process with exit code 0.');
process.exit(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<LinkUpEvents> {
await this.swarm?.destroy(); await this.swarm?.destroy();
console.log(`Bot ${this.botName} disconnected.`); console.log(`Bot ${this.botName} disconnected.`);
} }
/**
* @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<Command>)();
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<T extends {} = {}> = new (...args: any[]) => T;
export type MaybeCommand = Constructor<Command> | {default: Constructor<Command>} | {[k: string]: Constructor<Command>};

16
src/Command.ts Normal file
View File

@ -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[]
}

12
test/Bot.ts Normal file
View File

@ -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");
})

23
tsconfig.json Normal file
View File

@ -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",
]
}