How To Write An SSH Chat Just In 10 Minutes!

SSH Chat Main Image

How To Write An SSH Chat Just In 10 Minutes!

The 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.

How will it look like

Somewhere on the server, the program on the node is spinning.
As soon as someone wants to connect to the chat, it detects it:

</p>
<p>ssh server -p 8022</p>
<p>

After that, the system asks for a password and verifies it with the password in a special file. If the password matches, then we 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:

</p>
<p>@box{@color(red){Red text in box}}</p>
<p>

SSH Chat Image 1

Let’s get started

To work with ssh, we will use https://www.npmjs.com/package/ssh2.
For formatting, we use chalk and boxen.

Let’s  install them:

</p>
<p>npm i ssh2 chalk boxen</p>
<p>

Now the code itself is one of the most important parts of the message parser.

</p>
<p>// Connect chalk and boxen<br />const chalk = require ('chalk');<br />const boxen = require ('boxen');</p>
<p>// Here are the methods that we can use via @<br />// Functions take 2 arguments in brackets and tex in braces<br />let methods = {<br />color: function (args, text) {<br />return chalk.keyword (args) (text);<br />},</p>
<p>bold: function (args, text) {<br />return chalk.bold (text);<br />},</p>
<p>underline: function (args, text) {<br />return chalk.underline (text);<br />},</p>
<p>hex: function (args, text) {<br />return chalk.hex (args) (text);<br />},</p>
<p>box: function (args, text) {<br />return boxen (text, {<br />borderStyle: 'round',<br />padding: 1,<br />borderColor: 'blueBright'<br />});<br />}<br />};</p>
<p>// The parser itself<br />function parseAndExecute (str) {<br />let pos = 0;<br />let stage = 0;<br />let nS = '';<br />let bufs = ['', '', '', ''];<br />let level = 0;</p>
<p>while (pos &lt;str.length) {<br />let symbol = str [pos];<br />pos ++;</p>
<p>if (symbol == '\\' &amp;&amp; '() {} @'. indexOf (str [pos])! == -1) {<br />bufs [stage] + = str [pos];<br />pos ++;<br />continue;<br />}</p>
<p>if (stage == 0 &amp;&amp; symbol == '@') {<br />stage ++;<br />nS + = bufs [0];<br />bufs [0] = '';<br />continue;<br />} else if (stage&gt; = 1) {<br />if (symbol == '(')<br />if (stage &lt;2) {<br />stage = 2;<br />} else {<br />level ++;<br />}</p>
<p>if (symbol == ')' &amp;&amp; stage&gt; = 2 &amp;&amp; level&gt; 0) level--;</p>
<p>if (symbol == '{')<br />if (stage! = 3) {<br />stage = 3;<br />} else {<br />level ++;<br />}</p>
<p>if (symbol == '}') {<br />if (level == 0) {<br />bufs [3] + = '}';</p>
<p>nS + = methods [bufs [1]] (bufs [2] .slice (1, -1), parseAndExecute (bufs [3] .slice (1, -1)));</p>
<p>bufs = ['', '', '', ''];<br />stage = 0;<br />continue;<br />} else {<br />level--;<br />}<br />}<br />}<br />bufs [stage] + = symbol;<br />}<br />return nS + bufs [0];<br />}</p>
<p>module.exports.parseAndExecute = parseAndExecute;</p>
<p>

Formatting

</p>
<p>const chalk = require ('chalk');<br />const {parseAndExecute} = require ('./ parserExec')</p>
<p>// Stylize the nickname (Generate a color and make it bold)<br />function getNick (nick) {<br />   let hash = 0;<br />   for (var i = 0; i &lt;nick.length; i ++) hash + = nick.charCodeAt (i) - 32;</p>
<p>   return chalk.hsv ((hash + 160)% 360, 90, 90) (chalk.bold (nick));<br />}</p>
<p>module.exports.format = function (nick, message) {<br />   const nickSpace = '\ r' + '' .repeat (nick.length);<br />   nick = getNick (nick) + ':';</p>
<p>   message = message.replace (/ \\ n / gm, '\ n'); // Replace \ n with new lines<br />   message = parseAndExecute (message) // Parsim</p>
<p>   // Add indent to each new line<br />   message = message<br />     .split ('\ n')<br />     .map ((e, i) =&gt; '' + (i! == 0? nickSpace: '') + e)<br />     .join ('\ n');</p>
<p>   return nick + message;<br />};</p>
<p>

Methods for sending a message to all users and saving 100 messages

</p>
<p>let listeners = []; // All users<br />let cache = new Array (100) .fill ('') // Cache</p>
<p>// Add and remove subscribers<br />module.exports.addListener = write =&gt; listeners.push (write) - 1;<br />module.exports.delListener = id =&gt; listeners.splice (id, 1);</p>
<p>// Send a message<br />module.exports.broadcast = msg =&gt; {</p>
<p>   cache.shift ()<br />   cache.push (msg)<br />   process.stdout.write (msg)<br />   listeners.forEach (wr =&gt; wr (msg));<br />}</p>
<p>// Get the cache<br />module.exports.getCache = () =&gt; cache.join ('\ r \ 033 [1K')</p>
<p>

A server creation and authorization

</p>
<p>const {Server} = require ('ssh2');<br />const {readFileSync} = require ('fs');</p>
<p>const hostKey = readFileSync ('./ ssh'); // Read the key<br />const users = JSON.parse (readFileSync ('./ users.json')); // Users</p>
<p>let connectionCallback = () =&gt; {};</p>
<p>module.exports.createServer = function createServer ({lobby}) {<br />  // Create a server<br />  const server = new Server (<br />    {<br />      banner: lobby, // The banner meets before entering the password<br />      hostKeys: [hostKey]<br />    },<br />    function (client) {<br />      nick = '';<br />      client<br />        .on ('authentication', ctx =&gt; {// Authorization<br />          if (ctx.method! == 'password') return ctx.reject ();<br />          if (ctx.password! == users [ctx.username]) ctx.reject ();<br />          nick = ctx.username;<br />          ctx.accept ();<br />        })<br />        .on ('ready', function () {<br />          connectionCallback (client, nick);<br />        });<br />    }<br />  );</p>
<p>  return server<br />};</p>
<p>module.exports.setConnectCallback = callback =&gt; {// Sets the callback when connecting<br />  connectionCallback = callback;<br />};</p>
<p>

Various methods

</p>
<p>const {createInterface} = require ('readline');</p>
<p>module.exports.getStream = function (client, onStream, onEnd) {<br />  client // Get stream and client<br />    .on ('session', function (accept, reject) {<br />      accept ()<br />        .on ('pty', accept =&gt; accept &amp; accept ())<br />        .on ('shell', accept =&gt; onStream (accept ()));<br />    })<br />    .on ('end', () =&gt; onEnd ());<br />}</p>
<p>// Create a communicator<br />module.exports.getCommunicator = function (stream, onMessage, onEnd) {</p>
<p>  let readline = createInterface ({// Interface for reading lines<br />    input: stream,<br />    output: stream,<br />    prompt: '&gt;',<br />    historySize: 0,<br />    terminal: true<br />  })<br />  readline.prompt ()</p>
<p>  readline.on ('close', () =&gt; {<br />    radline = null;<br />    onEnd ()<br />    stream.end ()<br />  })</p>
<p>  readline.on ('line', (msg) =&gt; {<br />    stream.write ('\ 033 [s \ 033 [1A \ 033 [1K \ r')<br />    onMessage (msg)<br />    readline.prompt ()<br />  })</p>
<p>  // Method for writing a message<br />  return msg =&gt; {<br />    stream.write ('\ 033 [1K \ r' + msg)<br />    readline.prompt ()<br />  }<br />}</p>
<p>

Now, let’s compile it.

</p>
<p>const {createServer, setConnectCallback} = require ('./obby');<br />const {getStream, getCommunicator} = require ('./ utils');<br />const {addListener, delListener, broadcast, getCache} = require ('./ broadcaster');<br />const {format, getNick} = require ('./ format');</p>
<p>// Server create function<br />module.exports = function ({lobby = 'Hello'} = {}) {<br />const server = createServer ({<br />lobby<br />});</p>
<p>setConnectCallback ((client, nickname) =&gt; {// Waiting for connection<br />console.log ("The client is authenticated!");<br />let id = null;<br />getStream (// Get the stream<br />customer,<br />stream =&gt; {<br />const write = getCommunicator (// And interface<br />flow,<br />msg =&gt; {<br />if (msg == '') return;<br />try {<br />broadcast (format (nickname, message) + '\ n'); // As soon as we receive the message, send it to everyone<br />} catch (e) {}<br />},<br />() =&gt; {}<br />);</p>
<p>id = addListener (record); // Listen to messages<br />write ('\ 033c' + getCache ()); // Send cache<br />Broadcast (getNick (nickname) + 'connected \ n'); // Report connection<br />},<br />() =&gt; {</p>
<p>delListener (ID);<br />Broadcast (getNick (nickname) + 'disabled \ n') // Report disconnect<br />}<br />);<br />});</p>
<p>server.listen (8022);<br />};</p>
<p>

And the final step is an example server:

</p>
<p>const chat = require('.')</p>
<p>chat({})</p>
<p>

Conclusion

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:

  • Add the ability to create your own design features
  • Add markdown support
  • Add bot support
  • Sending files via scp