diff --git a/.gitignore b/.gitignore index e87fbcc..5919b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ chatBot/.env chatBot/commands/ai.js config.json .idea +AIBot \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8f547ff --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +`LinkUp` is currently in` active development` and `not fully launched`. + +To launch the App in Dev Mode: + +`git clone https://git.ssh.surf/snxraven/LinkUp-P2P-Chat.git` + +`cd LinkUp-P2P-Chat` + +`npm i; npm i pear -g;` + +Lastly - run the app: +`pear dev -s /tmp/tmp_pear` diff --git a/app.js b/app.js index 30b0d47..cacbaa7 100644 --- a/app.js +++ b/app.js @@ -6,16 +6,22 @@ import Hyperdrive from 'hyperdrive'; import Corestore from 'corestore'; import { EventEmitter } from 'events'; import fs from "fs"; +import handleCommand from './commands.js'; +const agentAvatarPath = './assets/agent.png'; +let agentAvatar = ''; const storagePath = `./storage/`; const store = new Corestore(storagePath); const drive = new Hyperdrive(store); await drive.ready(); -let swarm; -let registeredUsers = JSON.parse(localStorage.getItem('registeredUsers')) || {}; -let peerCount = 0; +// Load the agent avatar once when the app starts +if (fs.existsSync(agentAvatarPath)) { + const avatarBuffer = fs.readFileSync(agentAvatarPath); + agentAvatar = `data:image/png;base64,${b4a.toString(avatarBuffer, 'base64')}`; +} + let activeRooms = []; const eventEmitter = new EventEmitter(); @@ -26,7 +32,8 @@ let servePort; let config = { userName: '', userAvatar: '', - rooms: [] + rooms: [], + registeredUsers: {} }; // Store messages for each room @@ -37,14 +44,81 @@ function getRandomPort() { return Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152; } +function currentTopic() { + return document.querySelector('#chat-room-topic').innerText; +} + +function getCurrentPeerCount() { + const topic = currentTopic(); + const room = activeRooms.find(room => room.topic === topic); + return room ? room.swarm.connections.size : 0; +} + +function updatePeerCount() { + const peerCountElement = document.querySelector('#peers-count'); + if (peerCountElement) { + peerCountElement.textContent = getCurrentPeerCount(); // Display the actual peer count + } +} + +async function joinRoom(topicStr) { + const topicBuffer = b4a.from(topicStr, 'hex'); + addRoomToList(topicStr); + await joinSwarm(topicBuffer); +} + +async function createRoom(alias) { + const topicBuffer = crypto.randomBytes(32); + const topic = b4a.toString(topicBuffer, 'hex'); + addRoomToList(topic, alias); + await joinSwarm(topicBuffer); +} + +async function listFiles() { + const files = []; + for await (const entry of drive.readdir('/files')) { + files.push(entry); + } + return files; +} +async function deleteFile(filename) { + await drive.del(`/files/${filename}`); +} + async function initialize() { - swarm = new Hyperswarm(); + try { + servePort = getRandomPort(); + const serve = new ServeDrive({ port: servePort, get: ({ key, filename, version }) => drive }); + await serve.ready(); + console.log('Listening on http://localhost:' + serve.address().port); - servePort = getRandomPort(); - const serve = new ServeDrive({ port: servePort, get: ({ key, filename, version }) => drive }); - await serve.ready(); - console.log('Listening on http://localhost:' + serve.address().port); + // Event listeners setup + setupEventListeners(); + const configExists = fs.existsSync("./config.json"); + if (configExists) { + loadConfigFromFile(); + renderRoomList(); + await connectToAllRooms(); + } + + if (!configExists) { + document.querySelector('#register').classList.remove('hidden'); + } + + eventEmitter.on('onMessage', async (messageObj) => { + handleIncomingMessage(messageObj); + }); + + document.addEventListener("DOMContentLoaded", (event) => { + hljs.highlightAll(); + }); + } catch (error) { + console.error('Error during initialization:', error); + } +} + +function setupEventListeners() { const registerForm = document.querySelector('#register-form'); const selectAvatarButton = document.querySelector('#select-avatar'); const createChatRoomButton = document.querySelector('#create-chat-room'); @@ -54,6 +128,7 @@ async function initialize() { const removeRoomBtn = document.querySelector('#remove-room-btn'); const attachFileButton = document.getElementById('attach-file'); const fileInput = document.getElementById('file-input'); + const talkButton = document.getElementById('talk-btn'); if (registerForm) { registerForm.addEventListener('submit', registerUser); @@ -77,7 +152,7 @@ async function initialize() { } if (removeRoomBtn) { removeRoomBtn.addEventListener('click', () => { - const topic = document.querySelector('#chat-room-topic').innerText; + const topic = currentTopic(); leaveRoom(topic); }); } @@ -87,101 +162,155 @@ async function initialize() { if (fileInput) { fileInput.addEventListener('change', handleFileInput); } - - const configExists = fs.existsSync("./config.json"); - if (configExists) { - config = JSON.parse(fs.readFileSync("./config.json", 'utf8')); - console.log("Read config from file:", config); - // Update port in URLs - config.userAvatar = updatePortInUrl(config.userAvatar); - config.rooms.forEach(room => { - room.alias = room.alias || truncateHash(room.topic); - }); - for (let user in registeredUsers) { - registeredUsers[user] = updatePortInUrl(registeredUsers[user]); - } - - renderRoomList(); // Render the room list with aliases - - // Connect to all rooms on startup - for (const room of config.rooms) { - const topicBuffer = b4a.from(room.topic, 'hex'); - await joinSwarm(topicBuffer); - } + if (talkButton) { + setupTalkButton(); } +} - const registerDiv = document.querySelector('#register'); - if (registerDiv && !configExists) { - registerDiv.classList.remove('hidden'); - } +function handleIncomingMessage(messageObj) { + console.log('Received message:', messageObj); // Debugging log - eventEmitter.on('onMessage', async (messageObj) => { - console.log('Received message:', messageObj); // Debugging log - - if (messageObj.type === 'icon') { - const username = messageObj.username; - if (messageObj.avatar) { - const avatarBuffer = b4a.from(messageObj.avatar, 'base64'); - await drive.put(`/icons/${username}.png`, avatarBuffer); - updateIcon(username, avatarBuffer); - } else { - console.error('Received icon message with missing avatar data:', messageObj); - } - } else if (messageObj.type === 'file') { - if (messageObj.file && messageObj.fileName) { - const fileBuffer = b4a.from(messageObj.file, 'base64'); - await drive.put(`/files/${messageObj.fileName}`, fileBuffer); + if (messageObj.type === 'icon') { + const username = messageObj.username; + if (messageObj.avatar) { + const avatarBuffer = b4a.from(messageObj.avatar, 'base64'); + drive.put(`/icons/${username}.png`, avatarBuffer); + updateIcon(username, avatarBuffer); + } else { + console.error('Received icon message with missing avatar data:', messageObj); + } + } else if (messageObj.type === 'file') { + if (messageObj.file && messageObj.fileName) { + const fileBuffer = b4a.from(messageObj.file, 'base64'); + drive.put(`/files/${messageObj.fileName}`, fileBuffer).then(() => { const fileUrl = `http://localhost:${servePort}/files/${messageObj.fileName}`; addFileMessage(messageObj.name, messageObj.fileName, updatePortInUrl(fileUrl), messageObj.fileType, updatePortInUrl(messageObj.avatar), messageObj.topic); - } else { - console.error('Received file message with missing file data or fileName:', messageObj); - } - } else if (messageObj.type === 'message') { - onMessageAdded(messageObj.name, messageObj.message, messageObj.avatar, messageObj.topic); - } else { - console.error('Received unknown message type:', messageObj); - } - }); - - swarm.on('connection', async (connection, info) => { - peerCount++; - updatePeerCount(); - console.log('Peer connected, current peer count:', peerCount); - - // Send the current user's icon to the new peer - const iconBuffer = await drive.get(`/icons/${config.userName}.png`); - if (iconBuffer) { - const iconMessage = JSON.stringify({ - type: 'icon', - username: config.userName, - avatar: b4a.toString(iconBuffer, 'base64'), }); - connection.write(iconMessage); + } else { + console.error('Received file message with missing file data or fileName:', messageObj); } + } else if (messageObj.type === 'message') { + onMessageAdded(messageObj.name, messageObj.message, messageObj.avatar, messageObj.topic, messageObj.timestamp); + } else if (messageObj.type === 'audio') { + const audioBuffer = b4a.from(messageObj.audio, 'base64'); + const filePath = `/audio/${Date.now()}.webm`; + drive.put(filePath, audioBuffer).then(() => { + const audioUrl = `http://localhost:${servePort}${filePath}`; + addAudioMessage(messageObj.name, audioUrl, messageObj.avatar, messageObj.topic); + }); + } else { + console.error('Received unknown message type:', messageObj); + } +} - connection.on('data', (data) => { - const messageObj = JSON.parse(data.toString()); - eventEmitter.emit('onMessage', messageObj); +async function handleConnection(connection, info) { + console.log('New connection', info); + + // Sending the icon immediately upon connection + const iconBuffer = await drive.get(`/icons/${config.userName}.png`); + if (iconBuffer) { + const iconMessage = JSON.stringify({ + type: 'icon', + username: config.userName, + avatar: b4a.toString(iconBuffer, 'base64'), + }); + console.log('Sending icon to new peer:', iconMessage); + connection.write(iconMessage); + } else { + console.error('Icon not found for user:', config.userName); + } + + connection.on('data', (data) => { + const messageObj = JSON.parse(data.toString()); + eventEmitter.emit('onMessage', messageObj); + }); + + connection.on('close', () => { + console.log('Connection closed', info); + updatePeerCount(); + }); + + connection.on('error', (error) => { + console.error('Connection error', error); + if (error.code === 'ETIMEDOUT') { + retryConnection(info.topicBuffer); + } + }); + + updatePeerCount(); +} + +function retryConnection(topicBuffer) { + const topic = b4a.toString(topicBuffer, 'hex'); + const room = activeRooms.find(room => room.topic === topic); + if (room) { + console.log('Retrying connection to room:', topic); + room.swarm.leave(topicBuffer); + joinSwarm(topicBuffer).catch((error) => { + console.error('Failed to rejoin room after timeout:', error); + }); + } +} + +function setupTalkButton() { + const talkButton = document.getElementById('talk-btn'); + if (!talkButton) return; + + let mediaRecorder; + let audioChunks = []; + + talkButton.addEventListener('mousedown', async () => { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + mediaRecorder = new MediaRecorder(stream); + mediaRecorder.start(); + + mediaRecorder.addEventListener('dataavailable', event => { + audioChunks.push(event.data); }); - connection.on('close', () => { - peerCount--; - updatePeerCount(); - console.log('Peer disconnected, current peer count:', peerCount); + mediaRecorder.addEventListener('stop', async () => { + const audioBlob = new Blob(audioChunks); + audioChunks = []; + + const arrayBuffer = await audioBlob.arrayBuffer(); + const buffer = new Uint8Array(arrayBuffer); + + const topic = currentTopic(); + const filePath = `/audio/${Date.now()}.webm`; + await drive.put(filePath, buffer); + + const audioUrl = `http://localhost:${servePort}${filePath}`; + + const audioMessage = { + type: 'audio', + name: config.userName, + audio: b4a.toString(buffer, 'base64'), + audioType: audioBlob.type, + avatar: updatePortInUrl(config.userAvatar), + topic: topic + }; + + console.log('Sending audio message:', audioMessage); // Debugging log + + const peers = [...activeRooms.find(room => room.topic === topic).swarm.connections]; + for (const peer of peers) { + peer.write(JSON.stringify(audioMessage)); + } + + addAudioMessage(config.userName, audioUrl, config.userAvatar, topic); }); }); - swarm.on('error', (err) => { - console.error('Swarm error:', err); + talkButton.addEventListener('mouseup', () => { + if (mediaRecorder) { + mediaRecorder.stop(); + } }); - swarm.on('close', () => { - console.log('Swarm closed'); - }); - - // Initialize highlight.js once the DOM is fully loaded - document.addEventListener("DOMContentLoaded", (event) => { - hljs.highlightAll(); + talkButton.addEventListener('mouseleave', () => { + if (mediaRecorder && mediaRecorder.state === 'recording') { + mediaRecorder.stop(); + } }); } @@ -189,7 +318,7 @@ function registerUser(e) { e.preventDefault(); const regUsername = document.querySelector('#reg-username').value; - if (registeredUsers[regUsername]) { + if (config.registeredUsers[regUsername]) { alert('Username already taken. Please choose another.'); return; } @@ -201,8 +330,8 @@ function registerUser(e) { const buffer = new Uint8Array(event.target.result); await drive.put(`/icons/${regUsername}.png`, buffer); config.userAvatar = `http://localhost:${servePort}/icons/${regUsername}.png`; // Set the correct URL - registeredUsers[regUsername] = `http://localhost:${servePort}/icons/${regUsername}.png`; // Use placeholder URL - localStorage.setItem('registeredUsers', JSON.stringify(registeredUsers)); + config.registeredUsers[regUsername] = `http://localhost:${servePort}/icons/${regUsername}.png`; // Use placeholder URL + writeConfigToFile("./config.json"); continueRegistration(regUsername); }; reader.readAsArrayBuffer(avatarFile); @@ -240,7 +369,15 @@ async function createChatRoom() { async function joinChatRoom(e) { e.preventDefault(); - const topicStr = document.querySelector('#join-chat-room-topic').value; + const topicStr = document.querySelector('#join-chat-room-topic').value.trim(); + + // Validate the topic string + const isValidTopic = /^[0-9a-fA-F]{64}$/.test(topicStr); + if (!isValidTopic) { + alert('Invalid topic format. Please enter a 64-character hexadecimal string.'); + return; + } + const topicBuffer = b4a.from(topicStr, 'hex'); addRoomToList(topicStr); await joinSwarm(topicBuffer); @@ -249,28 +386,38 @@ async function joinChatRoom(e) { async function joinSwarm(topicBuffer) { const topic = b4a.toString(topicBuffer, 'hex'); if (!activeRooms.some(room => room.topic === topic)) { - const discovery = swarm.join(topicBuffer, { client: true, server: true }); - await discovery.flushed(); + try { + const swarm = new Hyperswarm(); + const discovery = swarm.join(topicBuffer, { client: true, server: true }); + await discovery.flushed(); - activeRooms.push({ topic, discovery }); + swarm.on('connection', (connection, info) => { + handleConnection(connection, info); + }); - console.log('Joined room:', topic); // Debugging log + activeRooms.push({ topic, swarm, discovery }); - renderMessagesForRoom(topic); + console.log('Joined room:', topic); // Debugging log + + renderMessagesForRoom(topic); + updatePeerCount(); + } catch (error) { + console.error('Error joining swarm for topic:', topic, error); + } } } -function addRoomToList(topic) { +function addRoomToList(topic, alias) { const roomList = document.querySelector('#room-list'); const roomItem = document.createElement('li'); - roomItem.textContent = truncateHash(topic); + roomItem.textContent = alias || truncateHash(topic); roomItem.dataset.topic = topic; roomItem.addEventListener('dblclick', () => enterEditMode(roomItem)); roomItem.addEventListener('click', () => switchRoom(topic)); roomList.appendChild(roomItem); - config.rooms.push({ topic, alias: truncateHash(topic) }); + config.rooms.push({ topic, alias: alias || truncateHash(topic) }); writeConfigToFile("./config.json"); } @@ -315,8 +462,7 @@ function exitEditMode(roomItem, input, topic) { } // Check if the edited room is the current room in view - const currentTopic = document.querySelector('#chat-room-topic').innerText; - if (currentTopic === topic) { + if (currentTopic() === topic) { const chatRoomName = document.querySelector('#chat-room-name'); if (chatRoomName) { chatRoomName.innerText = newAlias; @@ -349,6 +495,7 @@ function switchRoom(topic) { clearMessages(); renderMessagesForRoom(topic); + updatePeerCount(); // Show chat UI elements document.querySelector('#chat').classList.remove('hidden'); @@ -359,7 +506,7 @@ function leaveRoom(topic) { const roomIndex = activeRooms.findIndex(room => room.topic === topic); if (roomIndex !== -1) { const room = activeRooms[roomIndex]; - room.discovery.destroy(); + room.swarm.destroy(); activeRooms.splice(roomIndex, 1); } @@ -379,16 +526,34 @@ function leaveRoom(topic) { } } -function sendMessage(e) { + +async function sendMessage(e) { e.preventDefault(); const message = document.querySelector('#message').value; document.querySelector('#message').value = ''; - const topic = document.querySelector('#chat-room-topic').innerText; + const topic = currentTopic(); + const timestamp = Date.now(); + + if (message.startsWith('~')) { + // Handle command + await handleCommand(message, { + eventEmitter, + currentTopic, + clearMessages, + addMessage: (from, message, avatar, topic) => onMessageAdded(from, message, avatar, topic, timestamp), + joinRoom, + leaveRoom, + createRoom, + listFiles, + deleteFile + }); + return; + } console.log('Sending message:', message); // Debugging log - onMessageAdded(config.userName, message, config.userAvatar, topic); + onMessageAdded(config.userName, message, config.userAvatar, topic, timestamp); const messageObj = JSON.stringify({ type: 'message', @@ -396,10 +561,10 @@ function sendMessage(e) { message, avatar: config.userAvatar, topic: topic, - timestamp: Date.now() + timestamp: timestamp }); - const peers = [...swarm.connections]; + const peers = [...activeRooms.find(room => room.topic === topic).swarm.connections]; for (const peer of peers) { peer.write(messageObj); } @@ -415,7 +580,7 @@ async function handleFileInput(event) { await drive.put(filePath, buffer); const fileUrl = `http://localhost:${servePort}${filePath}`; - const topic = document.querySelector('#chat-room-topic').innerText; + const topic = currentTopic(); const fileMessage = { type: 'file', @@ -429,7 +594,7 @@ async function handleFileInput(event) { console.log('Sending file message:', fileMessage); // Debugging log - const peers = [...swarm.connections]; + const peers = [...activeRooms.find(room => room.topic === topic).swarm.connections]; for (const peer of peers) { peer.write(JSON.stringify(fileMessage)); } @@ -440,34 +605,13 @@ async function handleFileInput(event) { } } -function sendFileMessage(name, fileUrl, fileType, avatar) { - const fileName = fileUrl.split('/').pop(); - const messageObj = JSON.stringify({ - type: 'file', - name, - fileName, - fileUrl, - fileType, - avatar, - topic: document.querySelector('#chat-room-topic').innerText, - timestamp: Date.now(), - }); - - const peers = [...swarm.connections]; - for (const peer of peers) { - peer.write(messageObj); - } - - addFileMessage(name, fileName, fileUrl, fileType, avatar, document.querySelector('#chat-room-topic').innerText); -} - function addFileMessage(from, fileName, fileUrl, fileType, avatar, topic) { console.log('Adding file message:', { from, fileName, fileUrl, fileType, avatar, topic }); // Debugging log const $div = document.createElement('div'); $div.classList.add('message'); const $img = document.createElement('img'); - $img.src = avatar || 'https://via.placeholder.com/40'; + $img.src = updatePortInUrl(avatar) || 'https://via.placeholder.com/40'; $img.classList.add('avatar'); $div.appendChild($img); @@ -481,7 +625,7 @@ function addFileMessage(from, fileName, fileUrl, fileType, avatar, topic) { if (fileType.startsWith('image/')) { const $image = document.createElement('img'); - $image.src = fileUrl; + $image.src = updatePortInUrl(fileUrl); $image.alt = fileName; $image.classList.add('attached-image'); $content.appendChild($image); @@ -500,19 +644,11 @@ function addFileMessage(from, fileName, fileUrl, fileType, avatar, topic) { $div.appendChild($content); // Only render the message if it's for the current room - const currentTopic = document.querySelector('#chat-room-topic').innerText; - if (currentTopic === topic) { + if (currentTopic() === topic) { document.querySelector('#messages').appendChild($div); scrollToBottom(); } else { - console.log(`Message for topic ${topic} not rendered because current topic is ${currentTopic}`); // Debugging log - } -} - -function updatePeerCount() { - const peerCountElement = document.querySelector('#peers-count'); - if (peerCountElement) { - peerCountElement.textContent = peerCount; // Display the actual peer count + console.log(`Message for topic ${topic} not rendered because current topic is ${currentTopic()}`); // Debugging log } } @@ -521,20 +657,20 @@ function scrollToBottom() { container.scrollTop = container.scrollHeight; } -function onMessageAdded(from, message, avatar, topic) { +function onMessageAdded(from, message, avatar, topic, timestamp) { console.log('Adding message:', { from, message, avatar, topic }); // Debugging log const messageObj = { from, message, - avatar + avatar, + timestamp: timestamp || Date.now() }; // Add the message to the store addMessageToStore(topic, messageObj); // Only render messages for the current room - const currentTopic = document.querySelector('#chat-room-topic').innerText; - if (currentTopic === topic) { + if (currentTopic() === topic) { const $div = document.createElement('div'); $div.classList.add('message'); @@ -553,6 +689,34 @@ function onMessageAdded(from, message, avatar, topic) { const $text = document.createElement('div'); $text.classList.add('message-text'); + if (message.includes('Available files:')) { + const files = message.split('\n').slice(1); // Skip the "Available files:" line + const fileList = document.createElement('ul'); + + files.forEach(file => { + file = file.replace("- ", "") + const listItem = document.createElement('li'); + const fileButton = document.createElement('button'); + fileButton.textContent = file.trim(); + fileButton.onclick = () => downloadFile(file.trim()); + + const deleteButton = document.createElement('button'); + deleteButton.textContent = 'Delete'; + deleteButton.onclick = () => { + console.log("file to delete: ", file); + deleteFile(file); + listItem.remove(); + }; + + + listItem.appendChild(fileButton); + listItem.appendChild(deleteButton); + fileList.appendChild(listItem); + }); + + $text.appendChild(fileList); + } else { + const md = window.markdownit({ highlight: function (str, lang) { if (lang && hljs.getLanguage(lang)) { @@ -563,9 +727,9 @@ function onMessageAdded(from, message, avatar, topic) { return ''; // use external default escaping } }); - const markdownContent = md.render(message); $text.innerHTML = markdownContent; + } $content.appendChild($header); $content.appendChild($text); @@ -574,7 +738,54 @@ function onMessageAdded(from, message, avatar, topic) { document.querySelector('#messages').appendChild($div); scrollToBottom(); } else { - console.log(`Message for topic ${topic} not rendered because current topic is ${currentTopic}`); // Debugging log + console.log(`Message for topic ${topic} not rendered because current topic is ${currentTopic()}`); // Debugging log + } +} + +function downloadFile(filename) { + const fileUrl = `http://localhost:${servePort}/files/${filename}`; + const a = document.createElement('a'); + a.href = fileUrl; + a.download = filename; + a.click(); +} + + +function addAudioMessage(from, audioUrl, avatar, topic) { + console.log('Adding audio message:', { from, audioUrl, avatar, topic }); // Debugging log + const $div = document.createElement('div'); + $div.classList.add('message'); + + const $img = document.createElement('img'); + $img.src = updatePortInUrl(avatar) || 'https://via.placeholder.com/40'; + $img.classList.add('avatar'); + $div.appendChild($img); + + const $content = document.createElement('div'); + $content.classList.add('message-content'); + + const $header = document.createElement('div'); + $header.classList.add('message-header'); + $header.textContent = from; + $content.appendChild($header); + + const $audio = document.createElement('audio'); + $audio.controls = true; + if (from !== config.userName) { + $audio.autoplay = true; // Add autoplay attribute for peers only + } + $audio.src = updatePortInUrl(audioUrl); + $audio.classList.add('attached-audio'); + $content.appendChild($audio); + + $div.appendChild($content); + + // Only render the message if it's for the current room + if (currentTopic() === topic) { + document.querySelector('#messages').appendChild($div); + scrollToBottom(); + } else { + console.log(`Message for topic ${topic} not rendered because current topic is ${currentTopic()}`); // Debugging log } } @@ -594,11 +805,26 @@ async function updateIcon(username, avatarBuffer) { } } +function clearMessagesCMD() { + const messagesContainer = document.querySelector('#messages'); + while (messagesContainer.firstChild) { + messagesContainer.removeChild(messagesContainer.firstChild); + } + + // Clear the messages from the store for the current room + const topic = currentTopic(); + messagesStore[topic] = []; +} + function clearMessages() { const messagesContainer = document.querySelector('#messages'); while (messagesContainer.firstChild) { messagesContainer.removeChild(messagesContainer.firstChild); } + + // Clear the messages from the store for the current room + // const topic = currentTopic(); + // messagesStore[topic] = []; } function toggleSetupView() { @@ -643,7 +869,7 @@ function renderMessagesForRoom(topic) { // Fetch and render messages for the selected room const messages = getMessagesForRoom(topic); messages.forEach(message => { - onMessageAdded(message.from, message.message, message.avatar, topic); + onMessageAdded(message.from, message.message, message.avatar, topic, message.timestamp); }); } @@ -655,10 +881,43 @@ function addMessageToStore(topic, messageObj) { if (!messagesStore[topic]) { messagesStore[topic] = []; } - messagesStore[topic].push(messageObj); + + // Check for duplicates using a combination of message content and timestamp + const isDuplicate = messagesStore[topic].some(msg => + msg.from === messageObj.from && + msg.message === messageObj.message && + msg.timestamp === messageObj.timestamp + ); + + if (!isDuplicate) { + messagesStore[topic].push(messageObj); + } else { + console.log('Duplicate message detected:', messageObj); // Debugging log + } +} + +function loadConfigFromFile() { + config = JSON.parse(fs.readFileSync("./config.json", 'utf8')); + console.log("Read config from file:", config); + // Update port in URLs + config.userAvatar = updatePortInUrl(config.userAvatar); + config.rooms.forEach(room => { + room.alias = room.alias || truncateHash(room.topic); + }); + for (let user in config.registeredUsers) { + config.registeredUsers[user] = updatePortInUrl(config.registeredUsers[user]); + } +} + +async function connectToAllRooms() { + // Connect to all rooms on startup + for (const room of config.rooms) { + const topicBuffer = b4a.from(room.topic, 'hex'); + await joinSwarm(topicBuffer); + } } // Call this function when loading the rooms initially renderRoomList(); -initialize(); +initialize(); \ No newline at end of file diff --git a/assets/agent.png b/assets/agent.png new file mode 100644 index 0000000..edcb376 Binary files /dev/null and b/assets/agent.png differ diff --git a/chatBot/bot.js b/chatBot/bot.js index f6ed08e..3ef3d0e 100644 --- a/chatBot/bot.js +++ b/chatBot/bot.js @@ -64,7 +64,6 @@ loadCommands().then(commands => { }); bot.joinChatRoom(process.env.LINKUP_ROOM_ID); - bot.joinChatRoom(process.env.LINKUP_ROOM_ID2); }).catch(error => { console.error('Error loading commands:', error); -}); +}); \ No newline at end of file diff --git a/chatBot/includes/Client.js b/chatBot/includes/Client.js index 6c76306..45f3ec7 100644 --- a/chatBot/includes/Client.js +++ b/chatBot/includes/Client.js @@ -7,7 +7,7 @@ import FileMessage from "./FileMessage.js"; class Client extends EventEmitter { constructor(botName) { super(); - if(!botName) return console.error("Bot Name is not defined!"); + 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 @@ -48,6 +48,10 @@ class Client extends EventEmitter { } 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 }); diff --git a/chatBot/includes/FileMessage.js b/chatBot/includes/FileMessage.js index b4a78f4..aa055fe 100644 --- a/chatBot/includes/FileMessage.js +++ b/chatBot/includes/FileMessage.js @@ -1,5 +1,5 @@ class FileMessage { - public FileMessage(peerName, fileName, fileUrl, fileType, peerAvatar, topic, timestamp) { + constructor(peerName, fileName, fileUrl, fileType, peerAvatar, topic, timestamp) { this.peerName = peerName; this.fileName = fileName; this.fileUrl = fileUrl; @@ -9,7 +9,7 @@ class FileMessage { this.timestamp = timestamp; } - public toJsonString() { + toJsonString() { return JSON.stringify({ type: 'file', name: this.peerName, @@ -23,4 +23,4 @@ class FileMessage { } } -export default FileMessage; \ No newline at end of file +export default FileMessage; diff --git a/chatBot/includes/TextMessage.js b/chatBot/includes/TextMessage.js index b691113..fa07783 100644 --- a/chatBot/includes/TextMessage.js +++ b/chatBot/includes/TextMessage.js @@ -1,5 +1,5 @@ class TextMessage { - public TextMessage(peerName, peerAvatar, topic, message, timestamp) { + constructor(peerName, peerAvatar, topic, message, timestamp) { this.peerName = peerName; this.peerAvatar = peerAvatar; this.topic = topic; @@ -7,7 +7,7 @@ class TextMessage { this.timestamp = timestamp; } - public toJsonString() { + toJsonString() { return JSON.stringify({ type: 'message', name: this.peerName, @@ -18,9 +18,9 @@ class TextMessage { }); } - public static new(bot, message) { + static new(bot, message) { return new TextMessage(bot.botName, "", bot.currentTopic, message, Date.now()); } } -export default TextMessage; \ No newline at end of file +export default TextMessage; diff --git a/commands.js b/commands.js new file mode 100644 index 0000000..2d6b26f --- /dev/null +++ b/commands.js @@ -0,0 +1,56 @@ +// commands.js +import b4a from 'b4a'; +import fs from 'fs'; + +const agentAvatarPath = './assets/agent.png'; +let agentAvatar = ''; + +// Load the agent avatar once when the module is imported +if (fs.existsSync(agentAvatarPath)) { + const avatarBuffer = fs.readFileSync(agentAvatarPath); + agentAvatar = `data:image/png;base64,${b4a.toString(avatarBuffer, 'base64')}`; +} + +export default async function handleCommand(command, context) { + const { eventEmitter, currentTopic, clearMessages, addMessage, joinRoom, leaveRoom, createRoom, listFiles } = context; + + const args = command.trim().split(' '); + const cmd = args[0].toLowerCase(); + const restArgs = args.slice(1).join(' '); + + switch (cmd) { + case '~clear': + clearMessages(); + break; + case '~ping': + addMessage('LinkUp', 'pong', agentAvatar, currentTopic()); + break; + case '~help': + addMessage('LinkUp', 'Available commands:\n- ~clear\n- ~ping\n- ~help\n- ~join [topic]\n- ~leave\n- ~create [alias]\n- ~list-files', agentAvatar, currentTopic()); + break; + case '~join': + if (restArgs) { + await joinRoom(restArgs); + } else { + addMessage('LinkUp', 'Usage: ~join [topic]', agentAvatar, currentTopic()); + } + break; + case '~leave': + leaveRoom(currentTopic()); + break; + case '~create': + if (restArgs) { + await createRoom(restArgs); + } else { + addMessage('LinkUp', 'Usage: ~create [alias]', agentAvatar, currentTopic()); + } + break; + case '~list-files': + const files = await listFiles(); + const fileList = files.length > 0 ? files.map(file => `- ${file}`).join('\n') : 'No files available'; + addMessage('LinkUp', `Available files:\n${fileList}`, agentAvatar, currentTopic()); + break; + default: + console.log('Unknown command:', command); + } +} diff --git a/index.html b/index.html index 0f31c1b..d918404 100644 --- a/index.html +++ b/index.html @@ -62,6 +62,7 @@ + @@ -90,7 +91,7 @@ if (chatRoomTopic) { const topic = chatRoomTopic.innerText; navigator.clipboard.writeText(topic).then(() => { - alert('Topic copied to clipboard!'); + console.log('Topic copied to clipboard:', topic); }).catch(err => { console.error('Failed to copy topic:', err); }); diff --git a/style.css b/style.css index 6567450..6d0f986 100644 --- a/style.css +++ b/style.css @@ -17,23 +17,28 @@ body { padding: 3px 7px; font-size: 14px; font-weight: bold; - color: #fff; - background-color: #7289da; + color: #ffffff; + background-color: #464343; border: none; border-radius: 3px; text-decoration: none; cursor: pointer; - } + transition: background-color 0.3s ease, transform 0.3s ease; +} - .mini-button:hover { - background-color: #0056b3; - } +.mini-button:hover { + background-color: #181717; + transform: scale(1.05); +} .bold { font-weight: bold; - } +} -pear-ctrl[data-platform="darwin"] { float: right; margin-top: 4px; } +pear-ctrl[data-platform="darwin"] { + float: right; + margin-top: 4px; +} pear-ctrl { margin-top: 9px; @@ -50,7 +55,7 @@ pear-ctrl:after { left: 0; top: 0; width: 100%; - background-color: #B0D94413; + background-color: #1f1f1f; filter: drop-shadow(2px 10px 6px #888); } @@ -58,7 +63,6 @@ main { display: flex; flex: 1; overflow: hidden; - /* Ensure no overflow in main */ } #sidebar { @@ -107,27 +111,21 @@ main { #messages-container { flex: 1; overflow-y: auto; - /* Allow vertical scrolling */ overflow-x: hidden; - /* Hide horizontal scrolling */ width: 100%; } #messages-container::-webkit-scrollbar { width: 8px; - /* Set the width of the scrollbar */ } #messages-container::-webkit-scrollbar-thumb { - background-color: #b0d944; - /* Set the color of the scrollbar thumb */ + background-color: #464343; border-radius: 10px; - /* Round the corners of the scrollbar thumb */ } #messages-container::-webkit-scrollbar-track { background: #2e2e2e; - /* Set the color of the scrollbar track */ } #message-form { @@ -142,19 +140,15 @@ main { margin-right: 0.5rem; padding-right: 0.5rem; height: 1.5rem; - /* Initial single line height */ overflow-y: hidden; - /* Hide scrollbar */ } #message:focus { height: auto; - /* Allow the textarea to grow dynamically when focused */ } #message:empty { height: 1.5rem; - /* Ensure single line height when empty */ } #sidebar button { @@ -162,15 +156,17 @@ main { padding: 10px; margin-bottom: 10px; cursor: pointer; - background-color: #7289da; + background-color: #3e3c3c; border: none; color: white; font-size: 14px; border-radius: 5px; + transition: background-color 0.3s ease, transform 0.3s ease; } #sidebar button:hover { - background-color: #5b6eae; + background-color: #191919; + transform: scale(1.05); } #remove-room-btn { @@ -181,10 +177,12 @@ main { color: white; cursor: pointer; border-radius: 5px; + transition: background-color 0.3s ease, transform 0.3s ease; } #remove-room-btn:hover { background-color: #c03535; + transform: scale(1.05); } .hidden { @@ -221,53 +219,69 @@ header { padding: 0.5rem; border-radius: 4px; cursor: pointer; - transition: background-color 0.3s ease; + transition: background-color 0.3s ease, transform 0.3s ease; } .window-controls button:hover { background-color: #3e3e3e; + transform: scale(1.05); } /* Button and input styles */ button, input, textarea { - border: 1px solid #b0d944; + border: 1px solid #464343; background-color: #333; - color: #b0d944; + color: #e0e0e0; padding: 0.5rem; - /* Reduced padding */ font-family: 'Roboto Mono', monospace; font-size: 1rem; line-height: 1.25rem; - /* Adjusted line height */ border-radius: 4px; - transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, transform 0.3s ease; resize: none; - /* Prevent resizing */ overflow-y: hidden; - /* Hide scrollbar */ +} + +button:hover, +input[type="submit"]:hover { + background-color: #191919; + transform: scale(1.05); } textarea { height: auto; - /* Allow the textarea to grow dynamically */ } textarea:focus { outline: none; - /* Remove focus outline */ } textarea::placeholder { color: #a0a0a0; } -#attach-file, #message-form input[type="submit"] { - padding: 0.5rem 1rem; /* Add padding to buttons */ - margin-left: 0.5rem; /* Add margin between buttons */ +#attach-file, +#message-form input[type="submit"] { + padding: 0.5rem 1rem; + margin-left: 0.5rem; } +#talk-btn { + padding: 0.5rem 1rem; + margin-left: 0.5rem; + font-size: 14px; + border-radius: 5px; + cursor: pointer; +} + +#talk-btn:active { + color: #fff; + background-color: #f04747; +} + + /* Main container styles */ main { display: flex; @@ -297,7 +311,7 @@ main { #or { margin: 1.5rem 0; - color: #b0d944; + color: #e0e0e0; } #loading { @@ -341,6 +355,12 @@ main { #join-chat-room-container button { margin-left: 0.5rem; + transition: background-color 0.3s ease, transform 0.3s ease; +} + +#join-chat-room-container button:hover { + background-color: #191919; + transform: scale(1.05); } #details { @@ -352,17 +372,21 @@ main { border-radius: 4px; width: 100%; box-sizing: border-box; - /* Added to ensure box model includes padding */ } #details>div { display: flex; flex-direction: column; - /* Allow peers count to stack */ } #submit-button { margin-left: 1rem; + transition: background-color 0.3s ease, transform 0.3s ease; +} + +#submit-button:hover { + background-color: #191919; + transform: scale(1.05); } #messages { @@ -370,7 +394,6 @@ main { min-height: 200px; overflow-y: auto; padding: 0.5rem; - /* Reduced padding */ background-color: #262626; border-radius: 4px; display: flex; @@ -413,7 +436,6 @@ main { display: flex; align-items: flex-start; margin-bottom: 0.5rem; - /* Reduced margin */ } .message img.avatar { @@ -421,24 +443,21 @@ main { height: 32px; border-radius: 50%; margin-right: 0.5rem; - border: 2px solid #b0d944; + border: 2px solid #464343; } .message-content { max-width: 70%; background-color: #2e2e2e; padding: 0.5rem; - /* Reduced padding */ border-radius: 8px; - /* Reduced border radius */ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } .message-header { font-weight: bold; - color: #b0d944; + color: #e0e0e0; font-size: 0.9rem; - /* Reduced font size */ } .message-text { @@ -450,6 +469,82 @@ main { height: auto; margin-top: 0.5rem; border-radius: 4px; - /* Removed circular border */ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } + +/* Updated Room List Styles */ +#room-list { + list-style: none; + padding: 0; + margin: 0; +} + +#room-list li { + padding: 10px; + margin-bottom: 10px; + background-color: #3a3f44; + border-radius: 5px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: background-color 0.3s ease; +} + +#room-list li:hover { + background-color: #4a5258; +} + +#room-list li span { + flex: 1; +} + +#room-list li .edit-icon, +#room-list li .delete-icon { + margin-left: 10px; + color: #e0e0e0; + cursor: pointer; + transition: color 0.3s ease; +} + +#room-list li .edit-icon:hover, +#room-list li .delete-icon:hover { + color: #a0a0a0; +} + +/* Style for Edit Mode Input Box */ +#room-list li input[type="text"] { + background-color: #2e2e2e; + border: 1px solid #464343; + border-radius: 5px; + color: #e0e0e0; + padding: 5px; + width: calc(100% - 40px); + margin-right: 10px; + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; +} + +#room-list li input[type="text"]:focus { + outline: none; + background-color: #3a3a3a; +} + +/* Link styles */ +a { + color: #e0e0e0; /* Base color for links matching the text color */ + text-decoration: none; /* Remove underline */ + transition: color 0.3s ease, text-decoration 0.3s ease; +} + +a:hover { + color: #b0b0b0; /* Lighter color on hover */ + text-decoration: underline; /* Underline on hover */ +} + +a:active { + color: #a0a0a0; /* Slightly darker color on active */ +} + +a:visited { + color: #b0b0b0; /* Different color for visited links */ +}