From 0f8d931927d3835b399ce7c8e2137333b94f1f17 Mon Sep 17 00:00:00 2001 From: MrMasrozYTLIVE <61359286+MrMasrozYTLIVE@users.noreply.github.com> Date: Sat, 15 Jun 2024 16:39:59 +0300 Subject: [PATCH] Initial commit again --- .gitignore | 3 +- Client.js | 211 ++++++++++++++++++++++++++++++++++++++++ message/AudioMessage.js | 27 +++++ message/FileMessage.js | 29 ++++++ message/IconMessage.js | 20 ++++ message/Message.js | 21 ++++ message/TextMessage.js | 21 ++++ package.json | 19 ++++ 8 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 Client.js create mode 100644 message/AudioMessage.js create mode 100644 message/FileMessage.js create mode 100644 message/IconMessage.js create mode 100644 message/Message.js create mode 100644 message/TextMessage.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore index a60beb0..981f7b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea/ -package-lock.json \ No newline at end of file +package-lock.json +node_modules \ No newline at end of file diff --git a/Client.js b/Client.js new file mode 100644 index 0000000..6a48679 --- /dev/null +++ b/Client.js @@ -0,0 +1,211 @@ +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; diff --git a/message/AudioMessage.js b/message/AudioMessage.js new file mode 100644 index 0000000..fd0dce6 --- /dev/null +++ b/message/AudioMessage.js @@ -0,0 +1,27 @@ +import Message from "./Message.js"; +import b4a from "b4a"; + +class AudioMessage extends Message { + constructor(peerName, peerAvatar, topic, timestamp, audioUrl, audioType, audioData) { + super("audio", peerName, peerAvatar, topic, timestamp); + this.audioUrl = audioUrl; + this.audioType = audioType; + this.audioData = audioData; // Add audio data property + } + + toJsonString() { + return JSON.stringify({ + ...this.toJson(), + audioUrl: this.audioUrl, + audioType: this.audioType, + audio: this.audioData // Include audio data in JSON + }); + } + + static new(bot, audioUrl, audioType, audioBuffer) { + const audioData = b4a.toString(audioBuffer, 'base64'); // Convert audio buffer to base64 + return new AudioMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), audioUrl, audioType, audioData); + } +} + +export default AudioMessage; diff --git a/message/FileMessage.js b/message/FileMessage.js new file mode 100644 index 0000000..a9be736 --- /dev/null +++ b/message/FileMessage.js @@ -0,0 +1,29 @@ +import Message from "./Message.js"; +import b4a from "b4a"; + +class FileMessage extends Message { + constructor(peerName, peerAvatar, topic, timestamp, fileName, fileUrl, fileType, fileData) { + super("file", peerName, peerAvatar, topic, timestamp); + this.fileName = fileName; + this.fileUrl = fileUrl; + this.fileType = fileType; + this.fileData = fileData; // Add file data property + } + + toJsonString() { + return JSON.stringify({ + ...this.toJson(), + fileName: this.fileName, + fileUrl: this.fileUrl, + fileType: this.fileType, + file: this.fileData // Include file data in JSON + }); + } + + static new(bot, fileName, fileUrl, fileType, fileBuffer) { + const fileData = b4a.toString(fileBuffer, 'base64'); // Convert file buffer to base64 + return new FileMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), fileName, fileUrl, fileType, fileData); + } +} + +export default FileMessage; diff --git a/message/IconMessage.js b/message/IconMessage.js new file mode 100644 index 0000000..f3e9bd5 --- /dev/null +++ b/message/IconMessage.js @@ -0,0 +1,20 @@ +import Message from "./Message.js"; +import b4a from "b4a"; + +class IconMessage extends Message { + constructor(peerName, peerAvatar, timestamp) { + super("icon", peerName, peerAvatar, null, timestamp); + } + + toJsonString() { + return JSON.stringify({ + ...this.toJson() + }); + } + + static new(bot, avatarBuffer) { + return new IconMessage(bot.botName, b4a.toString(avatarBuffer, 'base64'), Date.now()); + } +} + +export default IconMessage; diff --git a/message/Message.js b/message/Message.js new file mode 100644 index 0000000..fbb2fff --- /dev/null +++ b/message/Message.js @@ -0,0 +1,21 @@ +class Message { + constructor(messageType, peerName, peerAvatar, topic, timestamp) { + this.type = messageType; + this.peerName = peerName; + this.peerAvatar = peerAvatar; + this.topic = topic; + this.timestamp = timestamp; + } + + toJson() { + return { + type: this.type, + name: this.peerName, + avatar: this.peerAvatar, + topic: this.topic, + timestamp: this.timestamp + }; + } +} + +export default Message; diff --git a/message/TextMessage.js b/message/TextMessage.js new file mode 100644 index 0000000..f4a68c0 --- /dev/null +++ b/message/TextMessage.js @@ -0,0 +1,21 @@ +import Message from "./Message.js"; + +class TextMessage extends Message { + constructor(peerName, peerAvatar, topic, timestamp, message) { + super("message", peerName, peerAvatar, topic, timestamp); + this.message = message; + } + + toJsonString() { + return JSON.stringify({ + ...this.toJson(), + message: this.message, + }); + } + + static new(bot, message) { + return new TextMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), message); + } +} + +export default TextMessage; diff --git a/package.json b/package.json new file mode 100644 index 0000000..4a29f32 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "linkup-bot-lib", + "version": "1.0.0", + "main": "Client.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "b4a": "^1.6.6", + "corestore": "^6.18.2", + "hyperdrive": "^11.8.1", + "hyperswarm": "^4.7.15", + "serve-drive": "^5.0.8" + } +}