UNPKG

linkup-bot-lib

Version:

403 lines (402 loc) 17.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Client = void 0; const path_1 = __importDefault(require("path")); const hyperswarm_1 = __importDefault(require("hyperswarm")); const TextMessage_1 = require("./message/TextMessage"); const FileMessage_1 = require("./message/FileMessage"); const AudioMessage_1 = require("./message/AudioMessage"); const corestore_1 = __importDefault(require("corestore")); const hyperdrive_1 = __importDefault(require("hyperdrive")); const fs_1 = __importDefault(require("fs")); // @ts-ignore const serve_drive_1 = __importDefault(require("serve-drive")); const IconMessage_1 = require("./message/IconMessage"); const TypedEventEmitter_1 = require("./util/TypedEventEmitter"); const glob_1 = require("glob"); const path_2 = require("path"); const util_1 = require("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. */ class Client extends TypedEventEmitter_1.TypedEventEmitter { botName = ""; servePort = 0; storagePath; swarm; drive; store; joinedRooms; currentTopic = null; botAvatar = ""; iconMessage; discovery; commands = []; commandPrefix; /** * @param botName The name of the bot. * @param commandPrefix Prefix for bot commands * @since 1.0 * @constructor * @author snxraven */ constructor(botName, commandPrefix) { super(); this.botName = botName; this.commandPrefix = commandPrefix; this.swarm = new hyperswarm_1.default(); this.joinedRooms = new Set(); // Track the rooms the bot has joined this.currentTopic = null; // Track the current topic // Initialize Corestore and Hyperdrive this.storagePath = './storage/'; this.store = new corestore_1.default(this.storagePath); this.drive = new hyperdrive_1.default(this.store); // Initialize ServeDrive this.servePort = null; this.initializeServeDrive(); this.setupSwarm(); process.on('exit', () => { console.log('EXIT signal received. Shutting down HyperSwarm...'); this.destroy(); }); process.on('SIGTERM', async () => { console.log('SIGTERM signal received. Shutting down HyperSwarm...'); await this.destroy(); console.log('HyperSwarm was shut down. Exiting the process with exit code 0.'); process.exit(0); }); process.on('SIGINT', async () => { console.log('SIGINT signal received. Shutting down HyperSwarm...'); await this.destroy(); console.log('HyperSwarm was shut down. Exiting the process with exit code 0.'); process.exit(0); }); this.on("onMessage", (msg) => { 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}`); } }); } /** * @description Initializes the ServeDrive for serving files and audio. * @since 1.0 * @author snxraven */ async initializeServeDrive() { try { this.servePort = this.getRandomPort(); const serve = new serve_drive_1.default({ port: this.servePort, // @ts-ignore get: ({ key, filename, version }) => this.drive }); await serve.ready(); console.log('ServeDrive listening on port:', this.servePort); } catch (error) { console.error('Error initializing ServeDrive:', error); } } /** * @description Returns a random port number. * @since 1.0 * @author snxraven * @return Random port number. */ getRandomPort() { return Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152; } /** * @description Fetches and sets the bot's avatar from a local file. * @param filePath path to the local avatar file. * @since 1.0 * @author snxraven */ async fetchAvatar(filePath) { try { await this.drive?.ready(); const iconBuffer = fs_1.default.readFileSync(filePath); await this.drive?.put(`/icons/${this.botName}.png`, iconBuffer); this.botAvatar = `http://localhost:${this.servePort}/icons/${this.botName}.png`; // Cache the icon message this.iconMessage = IconMessage_1.IconMessage.new(this, iconBuffer); } catch (error) { console.error('Error fetching avatar:', error); } } /** * @description Sets up the Hyperswarm network and connection handlers. * @since 1.0 * @author snxraven */ setupSwarm() { this.swarm?.on('connection', (peer) => { // Send the cached icon message to the new peer if (this.iconMessage) { peer.write(this.iconMessage.toJsonString()); } peer.on('data', async (message) => { const messageObj = JSON.parse(message.toString()); if (this.joinedRooms?.has(messageObj.topic)) { // Process message only if it is from a joined room this.currentTopic = messageObj.topic; // Set the current topic from the incoming message const msgType = messageObj.type; const peerName = messageObj.name; // Changed from name to userName const peerAvatar = messageObj.avatar; const timestamp = messageObj.timestamp; if (msgType === "message") this.emit('onMessage', new TextMessage_1.TextMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.message)); if (msgType === "file") { const fileBuffer = await this.drive?.get(`/files/${messageObj.fileName}`); /** * Triggered when a file message is received. * * @event Client#onFile * @property peer - HyperSwarm peer object * @property {FileMessage} FileMessage - Class with all of the information about received file * @example * const bot = new Client("MyBot"); * bot.on('onFile', (peer, message) => { * console.log(`Received file from ${message.peerName}`); * }); */ this.emit('onFile', new FileMessage_1.FileMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.fileName, `http://localhost:${this.servePort}/files/${messageObj.fileName}`, messageObj.fileType, messageObj.fileData)); } if (msgType === "icon") /** * Triggered when an icon message is received. * * @event Client#onIcon * @property peer - HyperSwarm peer object * @property {IconMessage} IconMessage - Class with all of the information about received peer icon * @example * const bot = new Client("MyBot"); * bot.on('onIcon', (peer, message) => { * console.log(`Received new Icon from ${message.peerName}`); * }); */ this.emit('onIcon', new IconMessage_1.IconMessage(peerName, peerAvatar, timestamp)); if (msgType === "audio") { const audioBuffer = await this.drive?.get(`/audio/${messageObj.audioName}`); /** * Triggered when an audio message is received. * * @event Client#onAudio * @property peer - HyperSwarm peer object * @property {AudioMessage} AudioMessage - Class with all of the information about received audio file * @example * ```js * const bot = new Client("MyBot"); * bot.on('onAudio', (peer, message) => { * console.log(`Received audio file from ${message.peerName}`); * }); * ``` */ this.emit('onAudio', new AudioMessage_1.AudioMessage(peerName, peerAvatar, this.currentTopic, timestamp, `http://localhost:${this.servePort}/audio/${messageObj.audioName}`, messageObj.audioType, messageObj.audioData)); } } }); peer.on('error', (err) => { this.emit('onError', err); console.error(`Connection error: ${err}`); }); }); // @ts-ignore this.swarm.on("update", () => { console.log(`Connections count: ${this.swarm?.connections.size} / Peers count: ${this.swarm?.peers.size}`); }); } /** * @description Joins a specified chat room. * @since 1.0 * @author snxraven * @param chatRoomID Chat room topic string */ joinChatRoom(chatRoomID) { if (!chatRoomID) { console.error("Invalid chat room ID!"); return; } this.joinedRooms?.add(chatRoomID); // Add the room to the list of joined rooms this.currentTopic = chatRoomID; // Store the current topic this.discovery = this.swarm?.join(Buffer.from(chatRoomID, 'hex'), { client: true, server: true }); this.discovery?.flushed().then(() => { console.log(`Bot ${this.botName} joined the chat room.`); this.emit('onBotJoinRoom', chatRoomID); }); } /** * @description Sends a text message. * @since 1.0 * @author MiTask * @param message Text message to send to the bot's current chat room. */ sendTextMessage(message) { console.log(`Preparing to send text message: ${message}`); this.sendMessage(TextMessage_1.TextMessage.new(this, message)); } /** * @description Sends a file message. * @since 1.0 * @author snxraven * @param filePath Path to the file to send. * @param fileType Type of the file to send. */ async sendFileMessage(filePath, fileType) { try { await this.drive?.ready(); const fileBuffer = fs_1.default.readFileSync(filePath); const fileName = path_1.default.basename(filePath); await this.drive?.put(`/files/${fileName}`, fileBuffer); const fileUrl = `http://localhost:${this.servePort}/files/${fileName}`; const fileMessage = FileMessage_1.FileMessage.new(this, fileName, fileUrl, fileType, fileBuffer); // Pass fileBuffer to the new method this.sendMessage(fileMessage); } catch (error) { console.error('Error sending file message:', error); } } /** * @description Sends an audio message. * @since 1.0 * @author snxraven * @param filePath Path to the audio file to send. * @param audioType Type of the audio file to send. */ async sendAudioMessage(filePath, audioType) { try { await this.drive?.ready(); const audioBuffer = fs_1.default.readFileSync(filePath); const audioName = path_1.default.basename(filePath); await this.drive?.put(`/audio/${audioName}`, audioBuffer); const audioUrl = `http://localhost:${this.servePort}/audio/${audioName}`; const audioMessage = AudioMessage_1.AudioMessage.new(this, audioUrl, audioType, audioBuffer); // Pass audioBuffer to the new method this.sendMessage(audioMessage); } catch (error) { console.error('Error sending audio message:', error); } } /** * @description Sends a generic message. * @since 1.0 * @author MiTask * @param message Message class (TextMessage, FileMessage or AudioMessage) */ sendMessage(message) { console.log("Sending message:", message); const data = message.toJsonString(); const peers = [...this.swarm?.connections]; if (peers.length === 0) { console.warn("No active peer connections found."); return; } console.log(`Sending message to ${peers.length} peers.`); for (const peer of peers) { try { peer.write(data); console.log(`Message sent to peer: ${peer.remoteAddress}`); } catch (error) { console.error(`Failed to send message to peer: ${peer.remoteAddress}`, error); } } } /** * @description Disconnects the bot and shuts down the Hyperswarm network. * @since 1.0 * @author snxraven */ async destroy() { await this.swarm?.destroy(); 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) { 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) { 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 */ async registerCommands(path) { const commands = await (0, util_1.promisify)(glob_1.glob)((0, path_2.normalize)(path + "/**/*.{ts,js}")); for (const commandPath of commands) { try { let command = await Promise.resolve(`${commandPath}`).then(s => __importStar(require(s))); if ('default' in command) command = command.default; if (command.constructor.name === 'Object') command = Object.values(command)[0]; const instance = new 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}`); } } } } exports.Client = Client;