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'; import MarkdownIt from 'markdown-it'; import hljs from 'highlight.js'; import DOMPurify from 'dompurify'; const md = new MarkdownIt({ highlight: function (str, lang) { if (lang && hljs.getLanguage(lang)) { try { return '
' +
               hljs.highlight(str, { language: lang }).value +
               '
'; } catch (__) {} } return '
' + md.utils.escapeHtml(str) + '
'; } }); 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', async (event) => { const { guildName } = event.detail; createGuild(guildName); await joinAllGuilds(); // Ensure the app joins all guilds on startup }); 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', name: 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 = `

${guild.alias}

`; const roomList = manageGuildModal.querySelector('#room-list'); roomList.innerHTML = ''; guild.rooms.forEach(room => { const roomItem = document.createElement('li'); roomItem.textContent = room.alias; roomItem.dataset.topic = room.topic; const editIcon = document.createElement('span'); editIcon.textContent = '✏️'; editIcon.classList.add('edit-icon'); editIcon.addEventListener('click', (e) => { e.stopPropagation(); enterEditMode(roomItem); }); const deleteIcon = document.createElement('span'); deleteIcon.textContent = '❌'; deleteIcon.classList.add('delete-icon'); deleteIcon.addEventListener('click', (e) => { e.stopPropagation(); removeRoom(guildTopic, room.topic); }); roomItem.appendChild(editIcon); roomItem.appendChild(deleteIcon); roomList.appendChild(roomItem); }); manageGuildModal.classList.remove('hidden'); } function enterEditMode(roomItem) { const originalText = roomItem.textContent; const topic = roomItem.dataset.topic; roomItem.innerHTML = ''; const input = document.createElement('input'); input.type = 'text'; input.value = originalText; input.style.maxWidth = '100%'; // Add this line to set the max width input.style.boxSizing = 'border-box'; // Add this line to ensure padding and border are included in the width roomItem.appendChild(input); input.focus(); input.addEventListener('blur', () => { exitEditMode(roomItem, input, topic); }); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { exitEditMode(roomItem, input, topic); } else if (e.key === 'Escape') { roomItem.textContent = originalText; } }); } function exitEditMode(roomItem, input, topic) { const newAlias = input.value.trim(); if (newAlias) { roomItem.textContent = newAlias; // Update the config with the new alias for (const guildTopic in config.guilds) { const guild = config.guilds[guildTopic]; const room = guild.rooms.find(room => room.topic === topic); 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); } } break; } } // Check if the edited room is the current room in view if (currentTopic() === topic) { const chatRoomName = document.querySelector('#chat-room-name'); if (chatRoomName) { chatRoomName.innerText = newAlias; } } } else { roomItem.textContent = truncateHash(topic); } } function removeRoom(guildTopic, roomTopic) { const guild = config.guilds[guildTopic]; if (guild) { const roomIndex = guild.rooms.findIndex(room => room.topic === roomTopic); if (roomIndex !== -1) { guild.rooms.splice(roomIndex, 1); writeConfigToFile("./config.json"); renderGuildList(); leaveRoom(roomTopic); // Synchronize the room removal with other peers const removeMessage = JSON.stringify({ type: 'remove-room', guildTopic, roomTopic }); const guildSwarm = activeRooms.find(room => room.topic === guildTopic); if (guildSwarm) { const peers = [...guildSwarm.swarm.connections]; for (const peer of peers) { peer.write(removeMessage); } } } } } function switchRoom(guildTopic, roomTopic) { if (!roomTopic) { console.error('Invalid room topic:', roomTopic); return; } console.log('Switching to room:', roomTopic); const chatRoomTopic = document.querySelector('#chat-room-topic'); const chatRoomName = document.querySelector('#chat-room-name'); const guild = config.guilds[guildTopic]; if (!guild) { console.error('Guild not found:', guildTopic); return; } if (chatRoomTopic) { chatRoomTopic.innerText = roomTopic; // Set full topic here } else { console.error('Element #chat-room-topic not found'); } if (chatRoomName) { // Find the room in the current guild const room = guild.rooms.find(room => room.topic === roomTopic); if (room) { // Update the room name in the header chatRoomName.innerText = room.alias; } else { console.error('Room not found in the current guild:', roomTopic); } } else { console.error('Element #chat-room-name not found'); } // Show the chat view document.querySelector('#chat').classList.remove('hidden'); document.querySelector('#setup').classList.add('hidden'); // Render the messages for the room renderMessagesForRoom(roomTopic); } async function leaveRoom(topic) { const roomIndex = activeRooms.findIndex(room => room.topic === topic); if (roomIndex !== -1) { const { swarm, discovery } = activeRooms[roomIndex]; await discovery.destroy(); swarm.destroy(); activeRooms.splice(roomIndex, 1); } const roomItem = document.querySelector(`li[data-topic="${topic}"]`); if (roomItem) { roomItem.remove(); } const messagesContainer = document.querySelector('#messages'); if (messagesContainer) { messagesContainer.innerHTML = ''; } const chatRoomName = document.querySelector('#chat-room-name'); if (chatRoomName) { chatRoomName.innerText = ''; } const chatRoomTopic = document.querySelector('#chat-room-topic'); if (chatRoomTopic) { chatRoomTopic.innerText = ''; } const chatDiv = document.querySelector('#chat'); const setupDiv = document.querySelector('#setup'); if (chatDiv && setupDiv) { chatDiv.classList.add('hidden'); setupDiv.classList.remove('hidden'); } } function writeConfigToFile(path) { fs.writeFileSync(path, JSON.stringify(config, null, 2)); } function loadConfigFromFile() { const configFile = fs.readFileSync("./config.json", 'utf8'); config = JSON.parse(configFile); } function renderGuildList() { const guildList = document.querySelector('#guild-list'); guildList.innerHTML = ''; for (const guildTopic in config.guilds) { const guild = config.guilds[guildTopic]; addGuildToList(guildTopic, guild.alias); } } async function connectToAllRooms() { for (const guildTopic in config.guilds) { const guild = config.guilds[guildTopic]; for (const room of guild.rooms) { await joinRoom(guildTopic, room.topic, room.alias); } } } function toggleSetupView() { const setupDiv = document.querySelector('#setup'); const chatDiv = document.querySelector('#chat'); setupDiv.classList.toggle('hidden'); chatDiv.classList.toggle('hidden'); } function truncateHash(hash) { return `${hash.substring(0, 4)}...${hash.substring(hash.length - 4)}`; } function updatePortInUrl(url) { if (typeof url !== 'string') { console.error('Invalid URL format:', url); return ''; } if (url === '') { console.error('Empty URL:', url); return ''; } const urlObj = new URL(url); urlObj.port = servePort; return urlObj.toString(); } function addFileMessage(name, fileName, fileUrl, fileType, avatar, topic) { const container = document.querySelector('#messages'); if (!container) { console.error('Element #messages not found'); return; } const messageDiv = document.createElement('div'); messageDiv.classList.add('message'); if (topic !== currentTopic()) { messageDiv.classList.add('hidden'); // Hide messages not belonging to the current room } const avatarImg = document.createElement('img'); avatarImg.src = updatePortInUrl(avatar); avatarImg.alt = `${name}'s avatar`; avatarImg.classList.add('avatar'); const messageContent = document.createElement('div'); messageContent.classList.add('message-content'); const senderName = document.createElement('div'); senderName.classList.add('message-sender'); senderName.textContent = name; const fileLink = document.createElement('a'); fileLink.href = fileUrl; fileLink.textContent = `File: ${fileName}`; fileLink.classList.add('message-file'); if (fileType.startsWith('image/')) { const filePreview = document.createElement('img'); filePreview.src = fileUrl; filePreview.alt = fileName; filePreview.classList.add('file-preview'); messageContent.appendChild(filePreview); } else if (fileType.startsWith('video/')) { const filePreview = document.createElement('video'); filePreview.src = fileUrl; filePreview.alt = fileName; filePreview.classList.add('file-preview'); filePreview.controls = true; messageContent.appendChild(filePreview); } else if (fileType.startsWith('audio/')) { const filePreview = document.createElement('audio'); filePreview.src = fileUrl; filePreview.alt = fileName; filePreview.classList.add('file-preview'); filePreview.controls = true; messageContent.appendChild(filePreview); } else { const filePreview = document.createElement('div'); filePreview.textContent = 'No preview available'; filePreview.classList.add('file-preview'); messageContent.appendChild(filePreview); } messageContent.appendChild(senderName); messageContent.appendChild(fileLink); messageDiv.appendChild(avatarImg); messageDiv.appendChild(messageContent); container.appendChild(messageDiv); if (topic === currentTopic()) { container.scrollTop = container.scrollHeight; } } function addAudioMessage(name, audioUrl, avatar, topic) { const container = document.querySelector('#messages'); if (!container) { console.error('Element #messages not found'); return; } const messageDiv = document.createElement('div'); messageDiv.classList.add('message'); if (topic !== currentTopic()) { messageDiv.classList.add('hidden'); // Hide messages not belonging to the current room } const avatarImg = document.createElement('img'); avatarImg.src = updatePortInUrl(avatar); avatarImg.alt = `${name}'s avatar`; avatarImg.classList.add('avatar'); const messageContent = document.createElement('div'); messageContent.classList.add('message-content'); const senderName = document.createElement('div'); senderName.classList.add('message-sender'); senderName.textContent = name; const audioElement = document.createElement('audio'); audioElement.src = audioUrl; audioElement.controls = true; // Autoplay only if the message is from a peer if (name !== config.userName) { audioElement.autoplay = true; } audioElement.classList.add('message-audio'); messageContent.appendChild(senderName); messageContent.appendChild(audioElement); messageDiv.appendChild(avatarImg); messageDiv.appendChild(messageContent); container.appendChild(messageDiv); if (topic === currentTopic()) { container.scrollTop = container.scrollHeight; } } function addMessage(name, message, avatar, topic) { const container = document.querySelector('#messages'); if (!container) { console.error('Element #messages not found'); return; } const messageDiv = document.createElement('div'); messageDiv.classList.add('message'); if (topic !== currentTopic()) { messageDiv.classList.add('hidden'); // Hide messages not belonging to the current room } const avatarImg = document.createElement('img'); avatarImg.src = updatePortInUrl(avatar); avatarImg.alt = `${name}'s avatar`; avatarImg.classList.add('avatar'); const messageContent = document.createElement('div'); messageContent.classList.add('message-content'); const senderName = document.createElement('div'); senderName.classList.add('message-sender'); senderName.textContent = name; const messageText = document.createElement('div'); messageText.classList.add('message-text'); messageText.innerHTML = DOMPurify.sanitize(md.render(message)); messageContent.appendChild(senderName); messageContent.appendChild(messageText); messageDiv.appendChild(avatarImg); messageDiv.appendChild(messageContent); container.appendChild(messageDiv); if (topic === currentTopic()) { container.scrollTop = container.scrollHeight; } hljs.highlightAll(); // Re-highlight all code blocks } async function updateIcon(username, avatarBuffer) { const userIcon = document.querySelector(`img[src*="${username}.png"]`); if (userIcon) { const avatarBlob = new Blob([avatarBuffer], { type: 'image/png' }); const avatarUrl = URL.createObjectURL(avatarBlob); userIcon.src = updatePortInUrl(avatarUrl); config.userAvatar = avatarUrl; writeConfigToFile("./config.json"); } } function clearMessages() { const messagesContainer = document.querySelector('#messages'); while (messagesContainer.firstChild) { messagesContainer.removeChild(messagesContainer.firstChild); } } function currentTopic() { return document.querySelector('#chat-room-topic').innerText; } async function sendMessage(e) { e.preventDefault(); const message = document.querySelector('#message').value; document.querySelector('#message').value = ''; const topic = currentTopic(); const timestamp = Date.now(); console.log("Sending message to current topic:", topic); // Add logging if (message.startsWith('>')) { await handleCommand(message, { eventEmitter, currentTopic: topic, // Pass the current topic as a string clearMessages, addMessage, joinRoom, leaveRoom, createRoom, listFiles, deleteFile, servePort }); return; } console.log('Sending message:', message); // Debugging log onMessageAdded(config.userName, message, config.userAvatar, topic, timestamp); const messageObj = JSON.stringify({ type: 'message', name: config.userName, avatar: updatePortInUrl(config.userAvatar), topic: topic, timestamp: timestamp, message }); const peers = [...activeRooms.find(room => room.topic === topic).swarm.connections]; for (const peer of peers) { peer.write(messageObj); } } function onMessageAdded(name, message, avatar, topic, timestamp) { if (!messagesStore[topic]) { messagesStore[topic] = []; } messagesStore[topic].push({ name, message, avatar, timestamp }); const chatRoomTopic = currentTopic(); if (topic === chatRoomTopic) { addMessage(name, message, avatar, topic); } } function renderMessagesForRoom(topic) { const container = document.querySelector('#messages'); if (!container) { console.error('Element #messages not found'); return; } container.innerHTML = ''; if (!messagesStore[topic]) return; messagesStore[topic].forEach(({ name, message, avatar }) => { addMessage(name, message, avatar, topic); }); } function handleFileInput(event) { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = async function(event) { const buffer = new Uint8Array(event.target.result); const filePath = `/files/${file.name}`; await drive.put(filePath, buffer); const fileUrl = `http://localhost:${servePort}/files/${file.name}`; const topic = currentTopic(); const fileMessage = { type: 'file', name: config.userName, fileName: file.name, file: b4a.toString(buffer, 'base64'), fileType: file.type, avatar: updatePortInUrl(config.userAvatar), topic: topic, timestamp: Date.now() }; const peers = [...activeRooms.find(room => room.topic === topic).swarm.connections]; for (const peer of peers) { peer.write(JSON.stringify(fileMessage)); } addFileMessage(config.userName, file.name, fileUrl, file.type, config.userAvatar, topic); }; reader.readAsArrayBuffer(file); } } async function joinGuildRequest(guildTopic) { const guildTopicBuffer = b4a.from(guildTopic, 'hex'); if (!activeRooms.some(room => room.topic === guildTopic)) { try { const swarm = new Hyperswarm(); const discovery = swarm.join(guildTopicBuffer, { client: true, server: true }); await discovery.flushed(); swarm.on('connection', (connection, info) => { handleConnection(connection, info); // Request guild information from peers const guildRequestMessage = JSON.stringify({ type: 'guildRequest', guildTopic }); connection.write(guildRequestMessage); }); activeRooms.push({ topic: guildTopic, swarm, discovery }); console.log('Joined guild topic:', guildTopic); // Debugging log updatePeerCount(); } catch (error) { console.error('Error joining swarm for guild topic:', guildTopic, error); } } } window.joinGuildRequest = joinGuildRequest; initialize();