import { EventEmitter } from 'events'; import Hyperswarm from 'hyperswarm'; import crypto from 'hypercore-crypto'; import b4a from 'b4a'; import ServeDrive from 'serve-drive'; import Localdrive from 'localdrive'; import fs from 'fs'; import Hyperdrive from 'hyperdrive'; import Corestore from 'corestore'; const store = new Corestore('./storage'); const drive = new Hyperdrive(store); await drive.ready(); class ChatHandler extends EventEmitter { constructor() { super(); this.userName = 'Anonymous'; this.userAvatar = ''; this.registeredUsers = JSON.parse(localStorage.getItem('registeredUsers')) || {}; this.peerCount = 0; this.currentRoom = null; this.swarm = new Hyperswarm(); this.initialize(); } async initialize() { const servePort = 1337; const serve = new ServeDrive({ port: servePort, get: ({ key, filename, version }) => drive }); await serve.ready(); console.log('Listening on http://localhost:' + serve.address().port); 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'); if (registerForm) { registerForm.addEventListener('submit', (e) => this.registerUser(e)); } if (selectAvatarButton) { selectAvatarButton.addEventListener('click', () => { document.querySelector('#avatar-file').click(); }); } if (createChatRoomButton) { createChatRoomButton.addEventListener('click', () => this.createChatRoom()); } if (joinChatRoomButton) { joinChatRoomButton.addEventListener('click', (e) => this.joinChatRoom(e)); } if (messageForm) { messageForm.addEventListener('submit', (e) => this.sendMessage(e)); } if (toggleSetupBtn) { toggleSetupBtn.addEventListener('click', () => this.toggleSetupView()); } if (removeRoomBtn) { removeRoomBtn.addEventListener('click', () => this.leaveRoom()); } const registerDiv = document.querySelector('#register'); if (registerDiv) { registerDiv.classList.remove('hidden'); } this.swarm.on('connection', async (connection, info) => { this.peerCount++; this.updatePeerCount(); const iconBuffer = await drive.get(`/icons/${this.userName}.png`); if (iconBuffer) { const iconMessage = JSON.stringify({ type: 'icon', username: this.userName, avatar: iconBuffer.toString('base64'), }); connection.write(iconMessage); } connection.on('data', (data) => this.emit('onMessage', data, connection)); connection.on('close', () => { this.peerCount--; this.updatePeerCount(); }); }); this.swarm.on('error', (err) => { console.error('Swarm error:', err); }); this.swarm.on('close', () => { console.log('Swarm closed'); }); this.on('onMessage', async (data, connection) => this.handleMessage(data, connection)); } async registerUser(e) { e.preventDefault(); const regUsername = document.querySelector('#reg-username').value; if (this.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 = (event) => { const buffer = new Uint8Array(event.target.result); drive.put(`/icons/${regUsername}.png`, buffer); this.userAvatar = URL.createObjectURL(new Blob([buffer])); this.registeredUsers[regUsername] = this.userAvatar; localStorage.setItem('registeredUsers', JSON.stringify(this.registeredUsers)); this.continueRegistration(regUsername); }; reader.readAsArrayBuffer(avatarFile); } else { this.continueRegistration(regUsername); } } async continueRegistration(regUsername) { const loadingDiv = document.querySelector('#loading'); const setupDiv = document.querySelector('#setup'); if (!regUsername) { alert('Please enter a username.'); return; } this.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 = this.truncateHash(b4a.toString(randomTopic, 'hex')); } async createChatRoom() { const topicBuffer = crypto.randomBytes(32); const topic = b4a.toString(topicBuffer, 'hex'); this.addRoomToList(topic); this.joinSwarm(topicBuffer); } async joinChatRoom(e) { e.preventDefault(); const topicStr = document.querySelector('#join-chat-room-topic').value; const topicBuffer = b4a.from(topicStr, 'hex'); this.addRoomToList(topicStr); this.joinSwarm(topicBuffer); } async joinSwarm(topicBuffer) { if (this.currentRoom) { this.currentRoom.destroy(); } document.querySelector('#setup').classList.add('hidden'); document.querySelector('#loading').classList.remove('hidden'); const discovery = this.swarm.join(topicBuffer, { client: true, server: true }); await discovery.flushed(); const topic = b4a.toString(topicBuffer, 'hex'); document.querySelector('#chat-room-topic').innerText = topic; // Set full topic here document.querySelector('#loading').classList.add('hidden'); document.querySelector('#chat').classList.remove('hidden'); this.currentRoom = discovery; this.clearMessages(); } addRoomToList(topic) { const roomList = document.querySelector('#room-list'); const roomItem = document.createElement('li'); roomItem.textContent = this.truncateHash(topic); roomItem.dataset.topic = topic; roomItem.addEventListener('click', () => this.switchRoom(topic)); roomList.appendChild(roomItem); } switchRoom(topic) { const topicBuffer = b4a.from(topic, 'hex'); this.joinSwarm(topicBuffer); } leaveRoom() { if (this.currentRoom) { const topic = b4a.toString(this.currentRoom.topic, 'hex'); const roomItem = document.querySelector(`li[data-topic="${topic}"]`); if (roomItem) { roomItem.remove(); } this.currentRoom.destroy(); this.currentRoom = null; } document.querySelector('#chat').classList.add('hidden'); document.querySelector('#setup').classList.remove('hidden'); } sendMessage(e) { e.preventDefault(); const message = document.querySelector('#message').value; document.querySelector('#message').value = ''; this.onMessageAdded(this.userName, message, this.userAvatar); const messageObj = JSON.stringify({ type: 'message', name: this.userName, message, avatar: this.userAvatar, timestamp: Date.now(), }); const peers = [...this.swarm.connections]; for (const peer of peers) { peer.write(messageObj); } } async handleMessage(data, connection) { const messageObj = JSON.parse(data.toString()); if (messageObj.type === 'icon') { const username = messageObj.username; const avatarBuffer = Buffer.from(messageObj.avatar, 'base64'); await drive.put(`/icons/${username}.png`, avatarBuffer); this.updateIcon(username, avatarBuffer); } else { this.onMessageAdded(messageObj.name, messageObj.message, messageObj.avatar); } } scrollToBottom() { var container = document.getElementById("messages-container"); container.scrollTop = container.scrollHeight; } onMessageAdded(from, message, avatar) { const $div = document.createElement('div'); $div.classList.add('message'); const $img = document.createElement('img'); $img.src = avatar || 'https://via.placeholder.com/40'; $div.appendChild($img); const $content = document.createElement('div'); $content.classList.add('message-content'); const $header = document.createElement('div'); $header.classList.add('message-header'); $header.textContent = from; const $text = document.createElement('div'); $text.classList.add('message-text'); const md = window.markdownit(); const markdownContent = md.render(message); $text.innerHTML = markdownContent; $content.appendChild($header); $content.appendChild($text); $div.appendChild($content); document.querySelector('#messages').appendChild($div); this.scrollToBottom(); } truncateHash(hash) { return `${hash.slice(0, 6)}...${hash.slice(-6)}`; } async updateIcon(username, avatarBuffer) { const userIcon = document.querySelector(`img[src*="${username}.png"]`); if (userIcon) { userIcon.src = URL.createObjectURL(new Blob([avatarBuffer])); } } clearMessages() { const messagesContainer = document.querySelector('#messages'); while (messagesContainer.firstChild) { messagesContainer.removeChild(messagesContainer.firstChild); } } toggleSetupView() { const setupDiv = document.querySelector('#setup'); setupDiv.classList.toggle('hidden'); } updatePeerCount() { // Implement this method based on your needs to update the peer count in the UI } } new ChatHandler();