forked from snxraven/LinkUp-P2P-Chat
Compare commits
No commits in common. "main" and "main" have entirely different histories.
4
app.js
4
app.js
@ -992,10 +992,6 @@ function updatePortInUrl(url) {
|
|||||||
console.error('Invalid URL format:', url);
|
console.error('Invalid URL format:', url);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
if (url === '') {
|
|
||||||
console.error('Empty URL:', url);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
urlObj.port = servePort;
|
urlObj.port = servePort;
|
||||||
return urlObj.toString();
|
return urlObj.toString();
|
||||||
|
211
chatBot/includes/Client.js
Normal file
211
chatBot/includes/Client.js
Normal file
@ -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;
|
27
chatBot/includes/message/AudioMessage.js
Normal file
27
chatBot/includes/message/AudioMessage.js
Normal file
@ -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;
|
29
chatBot/includes/message/FileMessage.js
Normal file
29
chatBot/includes/message/FileMessage.js
Normal file
@ -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;
|
20
chatBot/includes/message/IconMessage.js
Normal file
20
chatBot/includes/message/IconMessage.js
Normal file
@ -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;
|
21
chatBot/includes/message/Message.js
Normal file
21
chatBot/includes/message/Message.js
Normal file
@ -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;
|
21
chatBot/includes/message/TextMessage.js
Normal file
21
chatBot/includes/message/TextMessage.js
Normal file
@ -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;
|
@ -31,7 +31,6 @@
|
|||||||
"hypercore-crypto": "^3.4.1",
|
"hypercore-crypto": "^3.4.1",
|
||||||
"hyperdrive": "^11.8.1",
|
"hyperdrive": "^11.8.1",
|
||||||
"hyperswarm": "^4.7.14",
|
"hyperswarm": "^4.7.14",
|
||||||
"linkup-bot-lib": "^1.2.0",
|
|
||||||
"localdrive": "^1.11.4",
|
"localdrive": "^1.11.4",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"marked": "^12.0.2",
|
"marked": "^12.0.2",
|
||||||
|
Loading…
Reference in New Issue
Block a user