linkup-bot-lib
Version:
403 lines (402 loc) • 17.4 kB
JavaScript
"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;