LinkUp-P2P-Chat/app.js

1319 lines
40 KiB
JavaScript

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 '<pre class="hljs"><code>' +
hljs.highlight(str, { language: lang }).value +
'</code></pre>';
} catch (__) {}
}
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
}
});
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 = `<h3>${guild.alias}</h3>`;
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();