Hello, Habr. Console chat is a great thing, but for the front-end, what if you want the same, but for the backend. If yes, then this article is for you. But which tool is often used in the backend? That's right ssh, so I represent sshchat.
Somewhere on the server, the program on the node is spinning.
As soon as someone wants to connect to the chat, he enters:
ssh server -p 8022
After that, the system asks for a password and verifies it with the password in a special file. If the password matches, then connect to the chat (the user receives 100 previous messages and everyone else sees that he is connected).
Then he receives the messages of others, and can write his own.
Here with messages more interesting:
@box{@color(red){Red text in box}}
Send red text in box.
To work with ssh we will use https://www.npmjs.com/package/ssh2 .
For formatting, we use chalk and boxen.
So install them:
npm i ssh2 chalk boxen
Now the code itself is one of the most important parts of this message parser ( GitHub ):
// chalk boxen const chalk = require('chalk'); const boxen = require('boxen'); // @ // 2 let methods = { color: function(args, text) { return chalk.keyword(args)(text); }, bold: function(args, text) { return chalk.bold(text); }, underline: function(args, text) { return chalk.underline(text); }, hex: function(args, text) { return chalk.hex(args)(text); }, box: function(args, text) { return boxen(text, { borderStyle: 'round', padding: 1, borderColor: 'blueBright' }); } }; // function parseAndExecute(str) { let pos = 0; let stage = 0; let nS = ''; let bufs = ['', '', '', '']; let level = 0; while (pos < str.length) { let symbol = str[pos]; pos++; if (symbol == '\\' && '(){}@'.indexOf(str[pos]) !== -1) { bufs[stage] += str[pos]; pos++; continue; } if (stage == 0 && symbol == '@') { stage++; nS += bufs[0]; bufs[0] = ''; continue; } else if (stage >= 1) { if (symbol == '(') if (stage < 2) { stage = 2; } else { level++; } if (symbol == ')' && stage >= 2 && level > 0) level--; if (symbol == '{') if (stage != 3) { stage = 3; } else { level++; } if (symbol == '}') { if (level == 0) { bufs[3] += '}'; nS += methods[bufs[1]](bufs[2].slice(1, -1), parseAndExecute(bufs[3].slice(1, -1))); bufs = ['', '', '', '']; stage = 0; continue; } else { level--; } } } bufs[stage] += symbol; } return nS + bufs[0]; } module.exports.parseAndExecute = parseAndExecute;
Formatting ( GitHub ):
const chalk = require('chalk'); const { parseAndExecute } = require('./parserExec') // ( ) function getNick(nick) { let hash = 0; for (var i = 0; i < nick.length; i++) hash += nick.charCodeAt(i) - 32; return chalk.hsv((hash + 160) % 360, 90, 90)(chalk.bold(nick)); } module.exports.format = function(nick, message) { const nickSpace = '\r ' + ' '.repeat(nick.length); nick = getNick(nick) + ': '; message = message.replace(/\\n/gm, '\n'); // \n message = parseAndExecute(message) // // message = message .split('\n') .map((e, i) => '' + (i !== 0 ? nickSpace : '') + e) .join('\n'); return nick + message; };
Methods for sending a message to all users and saving 100 messages ( GitHub ):
let listeners = []; // let cache = new Array(100).fill('') // // module.exports.addListener = write => listeners.push(write) - 1; module.exports.delListener = id => listeners.splice(id, 1); // module.exports.broadcast = msg => { cache.shift() cache.push(msg) process.stdout.write(msg) listeners.forEach(wr => wr(msg)); } // module.exports.getCache = ()=>cache.join('\r\033[1K')
Lobby, server creation and authorization ( GitHub ):
const { Server } = require('ssh2'); const { readFileSync } = require('fs'); const hostKey = readFileSync('./ssh'); // const users = JSON.parse(readFileSync('./users.json')); // let connectionCallback = () => {}; module.exports.createServer = function createServer({ lobby }) { // const server = new Server( { banner: lobby, // hostKeys: [hostKey] }, function(client) { nick = ''; client .on('authentication', ctx => { // if (ctx.method !== 'password') return ctx.reject(); if (ctx.password !== users[ctx.username]) ctx.reject(); nick = ctx.username; ctx.accept(); }) .on('ready', function() { connectionCallback(client, nick); }); } ); return server }; module.exports.setConnectCallback = callback => { // connectionCallback = callback; };
Various methods ( GitHub ):
const { createInterface } = require('readline'); module.exports.getStream = function(client, onStream, onEnd){ client // .on('session', function(accept, reject) { accept() .on('pty', accept => accept & accept()) .on('shell', accept => onStream(accept())); }) .on('end', () => onEnd()); } // module.exports.getCommunicator = function(stream, onMessage, onEnd){ let readline = createInterface({ // input: stream, output: stream, prompt: '> ', historySize: 0, terminal: true }) readline.prompt() readline.on('close', ()=>{ radline = null; onEnd() stream.end() }) readline.on('line', (msg)=>{ stream.write('\033[s\033[1A\033[1K\r') onMessage(msg) readline.prompt() }) // return msg=>{ stream.write('\033[1K\r' + msg) readline.prompt() } }
Now combine ( GitHub ):
const { createServer, setConnectCallback } = require('./lobby'); const { getStream, getCommunicator } = require('./utils'); const { addListener, delListener, broadcast, getCache } = require('./broadcaster'); const { format, getNick } = require('./format'); // module.exports = function({ lobby = 'Hi' } = {}) { const server = createServer({ lobby }); setConnectCallback((client, nick) => { // console.log('Client authenticated!'); let id = null; getStream( // client, stream => { const write = getCommunicator( // stream, msg => { if (msg == '') return; try { broadcast(format(nick, msg) + '\n'); // , } catch (e) {} }, () => {} ); id = addListener(write); // write('\033c' + getCache()); // broadcast(getNick(nick) + ' connected\n'); // }, () => { delListener(id); broadcast(getNick(nick) + ' disconnected\n') // } ); }); server.listen(8022); };
And the final step is an example server:
const chat = require('.') chat({})
Also, users and their passwords are described in the users.json file.
This is how you can write not the easiest chat in ssh.
For such a chat, the client does not need to write, it has the design capabilities, and anyone can deploy it.
What else can be done: