import Hyperswarm from 'hyperswarm'; import crypto from 'hypercore-crypto'; import b4a from 'b4a'; import ServeDrive from 'serve-drive'; 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(); // 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(); // Define servePort at the top level let servePort; // Object to store all the information we want to save let config = { userName: '', userAvatar: '', guilds: {}, registeredUsers: {} }; // Store messages for each room let messagesStore = {}; // Function to get a random port between 49152 and 65535 function getRandomPort() { return Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152; } function currentGuildTopic() { return document.querySelector('#chat-guild-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 processGuild(guildData) { const parsedData = JSON.parse(guildData); config.guilds[parsedData.guildTopic] = { alias: parsedData.guildAlias, rooms: parsedData.rooms, owner: parsedData.owner }; writeConfigToFile("./config.json"); renderGuildList(); await joinGuild(parsedData.guildTopic); } async function joinGuild(guildTopic) { const guild = config.guilds[guildTopic]; if (guild) { for (const room of guild.rooms) { await joinRoom(guildTopic, room.topic, room.alias); } } } async function joinRoom(guildTopic, roomTopic, alias) { const topicBuffer = b4a.from(roomTopic, 'hex'); addRoomToList(guildTopic, roomTopic, alias); await joinSwarm(topicBuffer); } async function createRoom(guildTopic, alias) { const roomTopicBuffer = crypto.randomBytes(32); const roomTopic = b4a.toString(roomTopicBuffer, 'hex'); config.guilds[guildTopic].rooms.push({ topic: roomTopic, alias: alias || truncateHash(roomTopic) }); writeConfigToFile("./config.json"); addRoomToList(guildTopic, roomTopic, alias); await joinSwarm(roomTopicBuffer); // Synchronize the new room with other peers const roomMessage = JSON.stringify({ type: 'room', guildTopic, room: { topic: roomTopic, alias: alias || truncateHash(roomTopic) } }); const guildSwarm = activeRooms.find(room => room.topic === guildTopic); if (guildSwarm) { const peers = [...guildSwarm.swarm.connections]; for (const peer of peers) { peer.write(roomMessage); } } } 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() { 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); // Event listeners setup setupEventListeners(); const configExists = fs.existsSync("./config.json"); if (configExists) { loadConfigFromFile(); renderGuildList(); await connectToAllRooms(); await joinAllGuilds(); // Ensure the app joins all guilds on startup } if (!configExists) { document.querySelector('#register').classList.remove('hidden'); } eventEmitter.on('onMessage', async (messageObj) => { handleIncomingMessage(messageObj); }); document.addEventListener("DOMContentLoaded", (event) => { hljs.highlightAll(); }); document.addEventListener('createGuild', (event) => { const { guildName } = event.detail; createGuild(guildName); }); document.addEventListener('addRoomToGuild', (event) => { const { guildTopic, roomName } = event.detail; createRoom(guildTopic, roomName); }); document.addEventListener('manageGuild', (event) => { const { guildTopic } = event.detail; openManageGuildModal(guildTopic); }); document.addEventListener('switchRoom', (event) => { const { guildTopic, roomTopic } = event.detail; if (!roomTopic) { console.error('Invalid room topic:', roomTopic); return; } console.log('Event switchRoom with roomTopic:', roomTopic); switchRoom(guildTopic, roomTopic); }); document.addEventListener('joinGuildRequest', (event) => { const { guildTopic } = event.detail; joinGuildRequest(guildTopic); }); } catch (error) { console.error('Error during initialization:', error); } } async function joinAllGuilds() { for (const guildTopic in config.guilds) { await joinGuildRequest(guildTopic); } } function setupEventListeners() { const registerForm = document.querySelector('#register-form'); const selectAvatarButton = document.querySelector('#select-avatar'); const createChatRoomButton = document.querySelector('#create-chat-room'); const joinChatRoomButton = document.querySelector('#join-chat-room'); const messageForm = document.querySelector('#message-form'); const toggleSetupBtn = document.querySelector('#toggle-setup-btn'); 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'); const joinGuildBtn = document.getElementById('join-guild'); if (registerForm) { registerForm.addEventListener('submit', registerUser); } if (selectAvatarButton) { selectAvatarButton.addEventListener('click', () => { document.querySelector('#avatar-file').click(); }); } if (createChatRoomButton) { createChatRoomButton.addEventListener('click', () => { const guildTopic = currentGuildTopic(); createRoom(guildTopic); }); } if (joinChatRoomButton) { joinChatRoomButton.addEventListener('click', joinChatRoom); } if (messageForm) { messageForm.addEventListener('submit', sendMessage); } if (toggleSetupBtn) { toggleSetupBtn.addEventListener('click', toggleSetupView); } if (removeRoomBtn) { removeRoomBtn.addEventListener('click', () => { const topic = currentTopic(); leaveRoom(topic); }); } if (attachFileButton) { attachFileButton.addEventListener('click', () => fileInput.click()); } if (fileInput) { fileInput.addEventListener('change', handleFileInput); } if (talkButton) { setupTalkButton(); } if (joinGuildBtn) { joinGuildBtn.addEventListener('click', () => { const guildTopic = document.getElementById('join-guild-topic').value.trim(); if (guildTopic) { joinGuildRequest(guildTopic); } }); } // Add event listeners only for room items document.querySelectorAll('.room-item').forEach(item => { item.addEventListener('click', () => { const guildTopic = item.dataset.guildTopic; const roomTopic = item.dataset.topic; if (!roomTopic) { console.error('Invalid room topic for item:', item); return; } switchRoom(guildTopic, roomTopic); }); }); } function handleIncomingMessage(messageObj, connection) { console.log('Received message:', messageObj); // Debugging log if (messageObj.type === 'icon') { const username = messageObj.name; if (messageObj.avatar) { try { const avatarBuffer = b4a.from(messageObj.avatar, 'base64'); drive.put(`/icons/${username}.png`, avatarBuffer).then(() => { console.log(`Icon stored for user: ${username}`); // Debugging log updateIcon(username, avatarBuffer); }).catch(error => { console.error(`Failed to store icon for user ${username}:`, error); }); } catch (error) { console.error('Error processing avatar data:', error); } } 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); }).catch(error => { console.error(`Failed to store file ${messageObj.fileName}:`, error); }); } 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); }).catch(error => { console.error(`Failed to store audio message:`, error); }); } else if (messageObj.type === 'guild') { const guildData = messageObj.guildData; processGuild(guildData); } else if (messageObj.type === 'room') { const { guildTopic, room } = messageObj; if (config.guilds[guildTopic]) { config.guilds[guildTopic].rooms.push(room); writeConfigToFile("./config.json"); renderGuildList(); joinRoom(guildTopic, room.topic, room.alias); } } else if (messageObj.type === 'remove-room') { const { guildTopic, roomTopic } = messageObj; if (config.guilds[guildTopic]) { const roomIndex = config.guilds[guildTopic].rooms.findIndex(room => room.topic === roomTopic); if (roomIndex !== -1) { config.guilds[guildTopic].rooms.splice(roomIndex, 1); writeConfigToFile("./config.json"); renderGuildList(); leaveRoom(roomTopic); } } } else if (messageObj.type === 'rename-room') { const { guildTopic, roomTopic, newAlias } = messageObj; if (config.guilds[guildTopic]) { const room = config.guilds[guildTopic].rooms.find(room => room.topic === roomTopic); if (room) { room.alias = newAlias; writeConfigToFile("./config.json"); // Synchronize the room rename with other peers const renameMessage = JSON.stringify({ type: 'rename-room', guildTopic, roomTopic: topic, newAlias }); const guildSwarm = activeRooms.find(room => room.topic === guildTopic); if (guildSwarm) { const peers = [...guildSwarm.swarm.connections]; for (const peer of peers) { peer.write(renameMessage); } } } } } else if (messageObj.type === 'guildJoin') { const { guildTopic, guildData } = messageObj; if (!config.guilds[guildTopic]) { config.guilds[guildTopic] = guildData; writeConfigToFile("./config.json"); renderGuildList(); joinGuild(guildTopic); } } else if (messageObj.type === 'guildRequest') { const guildTopic = messageObj.guildTopic; const guild = config.guilds[guildTopic]; if (guild) { const guildResponseMessage = JSON.stringify({ type: 'guildResponse', guildData: JSON.stringify({ guildTopic, guildAlias: guild.alias, rooms: guild.rooms, owner: guild.owner }) }); connection.write(guildResponseMessage); } } else if (messageObj.type === 'guildResponse') { const guildData = messageObj.guildData; processGuild(guildData); } else { console.error('Received unknown message type:', 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'), timestamp: Date.now() }); console.log('Sending icon to new peer:', iconMessage); connection.write(iconMessage); } else { console.error('Icon not found for user:', config.userName); } // Sending the guilds and rooms information for (const guildTopic in config.guilds) { const guild = config.guilds[guildTopic]; const guildMessage = JSON.stringify({ type: 'guild', guildData: JSON.stringify({ guildTopic, guildAlias: guild.alias, rooms: guild.rooms, owner: guild.owner }) }); console.log('Sending guild information to new peer:', guildMessage); connection.write(guildMessage); } connection.on('data', (data) => { const messageObj = JSON.parse(data.toString()); if (messageObj.type === 'guildJoin') { const guildData = config.guilds[messageObj.guildTopic]; if (guildData) { const guildJoinMessage = JSON.stringify({ type: 'guildJoin', guildTopic: messageObj.guildTopic, guildData: guildData }); connection.write(guildJoinMessage); } } else { eventEmitter.emit('onMessage', messageObj, connection); } }); 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) { if (!topicBuffer || !topicBuffer.buffer) { console.error('Invalid topicBuffer:', topicBuffer); return; } 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); }); 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, avatar: updatePortInUrl(config.userAvatar), topic: topic, timestamp: Date.now(), audio: b4a.toString(buffer, 'base64'), audioType: audioBlob.type }; 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); }); }); talkButton.addEventListener('mouseup', () => { if (mediaRecorder) { mediaRecorder.stop(); } }); talkButton.addEventListener('mouseleave', () => { if (mediaRecorder && mediaRecorder.state === 'recording') { mediaRecorder.stop(); } }); } function registerUser(e) { e.preventDefault(); const regUsername = document.querySelector('#reg-username').value; if (config.registeredUsers[regUsername]) { alert('Username already taken. Please choose another.'); return; } const avatarFile = document.querySelector('#avatar-file').files[0]; if (avatarFile) { const reader = new FileReader(); reader.onload = async (event) => { 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 config.registeredUsers[regUsername] = `http://localhost:${servePort}/icons/${regUsername}.png`; // Use placeholder URL writeConfigToFile("./config.json"); continueRegistration(regUsername); }; reader.readAsArrayBuffer(avatarFile); } else { continueRegistration(regUsername); } } async function continueRegistration(regUsername) { const loadingDiv = document.querySelector('#loading'); const setupDiv = document.querySelector('#setup'); if (!regUsername) { alert('Please enter a username.'); return; } config.userName = regUsername; setupDiv.classList.remove('hidden'); document.querySelector('#register').classList.add('hidden'); loadingDiv.classList.add('hidden'); const randomTopic = crypto.randomBytes(32); document.querySelector('#chat-room-topic').innerText = truncateHash(b4a.toString(randomTopic, 'hex')); writeConfigToFile("./config.json"); } async function createGuild(guildName) { const topicBuffer = crypto.randomBytes(32); const topic = b4a.toString(topicBuffer, 'hex'); config.guilds[topic] = { alias: guildName, rooms: [], owner: config.userName }; addGuildToList(topic, guildName); writeConfigToFile("./config.json"); } async function joinChatRoom(e) { e.preventDefault(); const guildTopic = document.querySelector('#join-guild-topic').value.trim(); const roomTopic = document.querySelector('#join-chat-room-topic').value.trim(); // Validate the topic string const isValidGuildTopic = /^[0-9a-fA-F]{64}$/.test(guildTopic); const isValidRoomTopic = /^[0-9a-fA-F]{64}$/.test(roomTopic); if (!isValidGuildTopic || !isValidRoomTopic) { alert('Invalid topic format. Please enter 64-character hexadecimal strings.'); return; } const guildTopicBuffer = b4a.from(guildTopic, 'hex'); const roomTopicBuffer = b4a.from(roomTopic, 'hex'); addRoomToList(guildTopic, roomTopic); await joinSwarm(roomTopicBuffer); // Fetch guild data from a peer const guildJoinMessage = JSON.stringify({ type: 'guildJoin', guildTopic: guildTopic }); const guildSwarm = activeRooms.find(room => room.topic === guildTopic); if (guildSwarm) { const peers = [...guildSwarm.swarm.connections]; for (const peer of peers) { peer.write(guildJoinMessage); } } } async function joinSwarm(topicBuffer) { const topic = b4a.toString(topicBuffer, 'hex'); if (!activeRooms.some(room => room.topic === topic)) { try { const swarm = new Hyperswarm(); const discovery = swarm.join(topicBuffer, { client: true, server: true }); await discovery.flushed(); swarm.on('connection', (connection, info) => { handleConnection(connection, info); }); activeRooms.push({ topic, swarm, discovery }); console.log('Joined room:', topic); // Debugging log renderMessagesForRoom(topic); updatePeerCount(); } catch (error) { console.error('Error joining swarm for topic:', topic, error); } } } function addGuildToList(guildTopic, alias) { const guildList = document.querySelector('#guild-list'); const guildItem = document.createElement('li'); guildItem.textContent = alias || truncateHash(guildTopic); guildItem.dataset.guildTopic = guildTopic; guildItem.classList.add('guild-item'); if (config.guilds[guildTopic].owner === config.userName) { const manageButton = document.createElement('button'); manageButton.textContent = 'Manage'; manageButton.classList.add('mini-button', 'manage-guild-btn'); manageButton.addEventListener('click', (e) => { e.stopPropagation(); openManageGuildModal(guildTopic); }); guildItem.appendChild(manageButton); } const roomList = document.createElement('ul'); roomList.classList.add('room-list'); guildItem.appendChild(roomList); guildList.appendChild(guildItem); // Add the rooms for this guild config.guilds[guildTopic].rooms.forEach(room => { addRoomToList(guildTopic, room.topic, room.alias); }); } function addRoomToList(guildTopic, roomTopic, alias) { const guildItem = document.querySelector(`li[data-guild-topic="${guildTopic}"]`); if (guildItem) { let roomList = guildItem.querySelector('.room-list'); if (!roomList) { roomList = document.createElement('ul'); roomList.classList.add('room-list'); guildItem.appendChild(roomList); } if (!roomList.querySelector(`li[data-topic="${roomTopic}"]`)) { const roomItem = document.createElement('li'); roomItem.textContent = alias || truncateHash(roomTopic); roomItem.dataset.topic = roomTopic; roomItem.dataset.guildTopic = guildTopic; roomItem.classList.add('room-item'); roomItem.addEventListener('dblclick', () => enterEditMode(roomItem)); roomItem.addEventListener('click', () => switchRoom(guildTopic, roomTopic)); roomList.appendChild(roomItem); } } } function openManageGuildModal(guildTopic) { const guild = config.guilds[guildTopic]; if (!guild) return; const manageGuildModal = document.getElementById('manage-guild-modal'); manageGuildModal.dataset.guildTopic = guildTopic; const guildInfo = manageGuildModal.querySelector('#guild-info'); guildInfo.innerHTML = `