diff --git a/package.json b/package.json index 562eb06..01aeeec 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,4 @@ -{ +x{ "name": "sshchat", "version": "1.0.0", "description": "A peer to peer chat client that uses hyper-protocol and connects to the ssh.surf API", @@ -14,9 +14,11 @@ "license": "ISC", "dependencies": { "b4a": "^1.6.1", + "blessed": "^0.1.81", "graceful-goodbye": "^1.2.0", "hypercore-crypto": "^3.3.0", "hyperswarm": "^4.3.5", + "neo-blessed": "^0.2.0", "readline": "^1.3.0", "unirest": "^0.6.0" } diff --git a/sshChat.js b/sshChat.js index 9ed4622..41c65a3 100644 --- a/sshChat.js +++ b/sshChat.js @@ -1,3 +1,4 @@ +const blessed = require('neo-blessed'); // Require the needed libs const Hyperswarm = require('hyperswarm') const crypto = require('hypercore-crypto') @@ -22,18 +23,113 @@ let DAPI_KEY let LOGGEDIN = false let MYKEY = [] let conns = [] +let connectedUsers = []; let USERNAME = ["annon" + rand] function sleep(ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); } -async function clearCursor() { - readline.moveCursor(process.stdout, 0, -2) // up one line - readline.clearLine(process.stdout, 0) // from cursor to end - readline.moveCursor(process.stdout, 0, 2) // up one line + +function addUser(user, peerId) { + connectedUsers.push({ name: user, peerId: peerId }); + sidebarBox.setContent(connectedUsers.map(user => `${user.name} - ${user.peerId}`).join("\n")); + screen.render(); +} + +function removeUser(peerId) { + connectedUsers = connectedUsers.filter(user => user.peerId !== peerId); + sidebarBox.setContent("Connected Peers: \n" + connectedUsers.map(user => `${user.name} - ${user.peerId}`).join("\n")); + screen.render(); +} + +// Create the screen +const screen = blessed.screen({ + smartCSR: true, + fastCSR: true +}); + +let mainBox = blessed.box({ + parent: screen, + top: 0, + left: 0, + width: '80%', + height: '80%', + border: { + type: 'line' + }, + style: { + fg: 'white', + bg: 'black' + }, + keys: true, + vi: true, + alwaysScroll: true, + scrollable: true, + scrollbar: { + style: { + bg: 'yellow' + } + } +}); + +async function updateScroll() { + mainBox.scrollTo(mainBox.getScrollHeight()); + +} + +// Create the STDIN box for chat input and command input +const stdinBox = blessed.textbox({ + bottom: '0', + left: '0', + width: '80%', + height: '21%', + border: { + type: 'line' + }, + style: { + fg: 'white', + bg: 'black', + border: { + fg: '#f0f0f0' + }, + }, + inputOnFocus: true, + input: true +}); + +// Create the sidebar box for connected peers +const sidebarBox = blessed.box({ + top: '0', + right: '0', + width: '20%', + height: '100%', + content: '', + border: { + type: 'line' + }, + style: { + fg: 'white', + bg: 'black', + border: { + fg: '#f0f0f0' + }, + } +}); + +sidebarBox.setLabel("Connected Peers: (Currently None)") + + +const originalLog = console.log; +console.log = (...args) => { + mainBox.setContent(mainBox.getContent() + `\n${args.join(' ')}`); + updateScroll() + stdinBox.clearValue(); + + screen.render() + } // Generate a random public key @@ -48,133 +144,153 @@ const commandFiles = fs.readdirSync(commandDir); const commands = {} for (const file of commandFiles) { - const commandName = file.split(".")[0] - require(`${commandDir}/${file}`) - const command = require(`${commandDir}/${file}`); - commands[commandName] = command; + const commandName = file.split(".")[0] + require(`${commandDir}/${file}`) + const command = require(`${commandDir}/${file}`); + commands[commandName] = command; } async function handleCommand(input) { - if (input.startsWith("!") || input.startsWith(">")) { - const command = input.split(" ") - if (!command) return consoile.log("Please either send a message or enter a command.") - switch (command[0]) { - case "!": - AIRequest(command.slice(1).join(" ")) - break; - case ">cd": - USERPWD = await changeDir(command[1], USERPWD); - console.log(USERPWD) - break; - case ">stats": - stats(MYKEY[0]) - break; - case ">": - execute(MYKEY[0], command.slice(1).join(" "), USERPWD, conns); - break; - case ">restart": - restart(MYKEY[0]) - break; - case ">stop": - stop(MYKEY[0]) - break; - case ">start": - start(MYKEY[0]) - break; - case ">login": - login(command[1], conns, MYKEY, USERNAME) - break; - case ">exit": - console.log("Sending close message...") - for (let conn of conns) { - conn.write(`CLOSED: ${publicKey.toString('hex')}`) + if (input.startsWith("!") || input.startsWith(">")) { + const command = input.split(" ") + if (!command) return consoile.log("Please either send a message or enter a command.") + switch (command[0]) { + case "!": + AIRequest(command.slice(1).join(" ")) + break; + case ">cd": + USERPWD = await changeDir(command[1], USERPWD); + console.log(USERPWD) + break; + case ">stats": + stats(MYKEY[0]) + break; + case ">": + execute(MYKEY[0], command.slice(1).join(" "), USERPWD, conns); + break; + case ">restart": + restart(MYKEY[0]) + break; + case ">stop": + stop(MYKEY[0]) + break; + case ">start": + start(MYKEY[0]) + break; + case ">login": + login(command[1], conns, MYKEY, USERNAME) + break; + case ">exit": + console.log("Sending close message...") + for (let conn of conns) { + conn.write(`CLOSED: ${publicKey.toString('hex')}`) + } + + await sleep(2000) + process.exit() + break + default: + console.log("Command not found.") } - - await sleep(2000) - process.exit() - break - default: - console.log("Command not found.") + } else { + for (const conn of conns) + + conn.write(`${USERNAME[0]}: ${input}`) + } - } else { - for (const conn of conns) - - conn.write(`${USERNAME[0]}: ${input}`) - - } - console.log(`${USERNAME[0]}: ${input}`) - clearCursor() + console.log(`${USERNAME[0]}: ${input}`) } swarm.on('connection', conn => { - process.on('SIGINT', async () => { - console.log("Sending close message...") - for (let conn of conns) { - conn.write(`CLOSED: ${publicKey.toString('hex')}`) - } + process.on('SIGINT', async () => { + console.log("Sending close message...") + for (let conn of conns) { + conn.write(`CLOSED: ${publicKey.toString('hex')}`) + } - await sleep(2000) - process.exit() + await sleep(2000) + process.exit() - }) + }) - const name = b4a.toString(conn.remotePublicKey, 'hex') - console.log(`* got a connection from ${name} (${USERNAME[0]}) *`) - conns.push(conn) - conn.once('close', () => conns.splice(conns.indexOf(conn), 1)) - conn.on('data', data => { - if (data.toString().startsWith('CLOSED:')) { - // Extract the key from the message string - const key = data.toString().split(':')[1].trim(); - console.log(`Removing peer ${key}`); - (async () => { + const name = b4a.toString(conn.remotePublicKey, 'hex') + console.log(`* got a connection from ${name} (${USERNAME[0]}) *`) + addUser(USERNAME[0], name) + sidebarBox.setLabel("Connected Peers: " + connectedUsers.length) + screen.render() + conns.push(conn) + conn.once('close', () => conns.splice(conns.indexOf(conn), 1)) + conn.on('data', data => { + if (data.toString().startsWith('CLOSED:')) { + // Extract the key from the message string + const key = data.toString().split(':')[1].trim(); + removeUser(key) + console.log(`Removing peer ${key}`); + (async () => { - await sleep(5000) - conns = conns.filter(c => c !== conn); - conn.destroy(); - })(); + await sleep(5000) + conns = conns.filter(c => c !== conn); + conn.destroy(); + })(); - } else { - console.log(`${data}`) - } - // Use the USERNAME if it has been set, otherwise use the public key - }) + } else { + console.log(`${data}`) + } + // Use the USERNAME if it has been set, otherwise use the public key + }) }) swarm.on('error', (err) => { - console.log('Error connecting to peer:', err); + console.log('Error connecting to peer:', err); }); -// Use readline to accept input from the user -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout -}) - -// When the user inputs a line of text, broadcast it to all connections -rl.on('line', input => { - handleCommand(input) -}) // Join a common topic const topic = process.argv[2] ? b4a.from(process.argv[2], 'hex') : crypto.randomBytes(32) + setTimeout(() => { - const discovery = swarm.join(topic, { - lookup: true, - announce: true, - timeout: 300000 + const discovery = swarm.join(topic, { + lookup: true, + announce: true, + timeout: 300000 - }); + }); - // The flushed promise will resolve when the topic has been fully announced to the DHT - discovery.flushed().then(() => { - console.log(`joined topic: ${b4a.toString(topic, 'hex')}\n(Share this key to others so they may join.)`) - console.log('You are now in a chatroom for your topic, feel free to chat.\n') - console.log('Want to login to the SSH.SURF API? Type ">login [APIKEY]" to login.\nPease close using CTRL+X or use the >exit command\n') - }) + // The flushed promise will resolve when the topic has been fully announced to the DHT + discovery.flushed().then(() => { + mainBox.setLabel("Topic: " + b4a.toString(topic, 'hex') + " (Share to connect)") + stdinBox.setLabel("To login: >login [TOKEN]") + screen.render() + }) }, 1000); + + +// Append the boxes to the screen +screen.append(mainBox); +screen.append(stdinBox); +screen.append(sidebarBox); + + +// Handle input in the stdinBox +stdinBox.on('submit', (input) => { + // handle the input here + // for example : + handleCommand(input); + // clear the input field + stdinBox.focus(); + + // screen.render(); +}); + +// setInterval(() => { +// mainBox.scrollTo(mainBox.getScrollHeight()); +// }, 1000); +stdinBox.focus(); +// Render the screen + +screen.render() \ No newline at end of file