import path from 'path'; import Hyperswarm from 'hyperswarm'; import EventEmitter from 'node:events'; import b4a from "b4a"; import TextMessage from "./message/TextMessage.js"; import FileMessage from "./message/FileMessage.js"; import AudioMessage from "./message/AudioMessage.js"; import Message from "./message/Message.js"; import IconMessage from "./message/IconMessage.js"; import Corestore from 'corestore'; import Hyperdrive from 'hyperdrive'; import fs from 'fs'; import ServeDrive from 'serve-drive'; class Client extends EventEmitter { constructor(botName) { super(); if (!botName) return console.error("Bot Name is not defined!"); this.botName = botName; this.swarm = new Hyperswarm(); 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(this.storagePath); this.drive = new Hyperdrive(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); }); } async initializeServeDrive() { try { this.servePort = this.getRandomPort(); const serve = new ServeDrive({ port: this.servePort, 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); } } getRandomPort() { return Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152; } async fetchAvatar(filePath) { try { await this.drive.ready(); const iconBuffer = fs.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.new(this, iconBuffer); } catch (error) { console.error('Error fetching avatar:', error); } } 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', peer, new TextMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.message)); if (msgType === "file") { const fileBuffer = await this.drive.get(`/files/${messageObj.fileName}`); this.emit('onFile', peer, new FileMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.fileName, `http://localhost:${this.servePort}/files/${messageObj.fileName}`, messageObj.fileType, messageObj.fileData)); } if (msgType === "icon") this.emit('onIcon', peer, new IconMessage(peerName, peerAvatar, timestamp)); if (msgType === "audio") { const audioBuffer = await this.drive.get(`/audio/${messageObj.audioName}`); this.emit('onAudio', peer, new AudioMessage(peerName, peerAvatar, this.currentTopic, timestamp, `http://localhost:${this.servePort}/audio/${messageObj.audioName}`, messageObj.audioType, messageObj.audioData)); } } }); peer.on('error', e => { this.emit('onError', e); console.error(`Connection error: ${e}`); }); }); this.swarm.on('update', () => { console.log(`Connections count: ${this.swarm.connections.size} / Peers count: ${this.swarm.peers.size}`); }); } joinChatRoom(chatRoomID) { if (!chatRoomID || typeof chatRoomID !== 'string') { return console.error("Invalid chat room ID!"); } 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'); }); } sendTextMessage(message) { console.log(`Preparing to send text message: ${message}`); this.sendMessage(TextMessage.new(this, message)); } async sendFileMessage(filePath, fileType) { try { await this.drive.ready(); const fileBuffer = fs.readFileSync(filePath); const fileName = path.basename(filePath); await this.drive.put(`/files/${fileName}`, fileBuffer); const fileUrl = `http://localhost:${this.servePort}/files/${fileName}`; const fileMessage = 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); } } async sendAudioMessage(filePath, audioType) { try { await this.drive.ready(); const audioBuffer = fs.readFileSync(filePath); const audioName = path.basename(filePath); await this.drive.put(`/audio/${audioName}`, audioBuffer); const audioUrl = `http://localhost:${this.servePort}/audio/${audioName}`; const audioMessage = AudioMessage.new(this, audioUrl, audioType, audioBuffer); // Pass audioBuffer to the new method this.sendMessage(audioMessage); } catch (error) { console.error('Error sending audio message:', error); } } sendMessage(message) { if (!(message instanceof Message)) { console.error(`message does not extend Message class (TextMessage, FileMessage, AudioMessage).`, message); return; } 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); } } } async destroy() { await this.swarm.destroy(); console.log(`Bot ${this.botName} disconnected.`); } } export default Client;