├── .codeclimate.yml ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── Procfile ├── README.md ├── app ├── core │ ├── components.js │ ├── screen.js │ └── whir.js ├── index.js ├── library │ ├── crypto.js │ └── string.js └── support │ └── emoji.json ├── media ├── favicon.png ├── w.png └── whir.png ├── package.json ├── store └── .store ├── test └── index.js └── w.sh /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | JavaScript: true 2 | 3 | engines: 4 | eslint: 5 | enabled: true 6 | ratings: 7 | paths: 8 | - "app/**/*" 9 | - "**.js" 10 | exclude_paths: 11 | - "media/**/*" 12 | - "test/**/*" 13 | - "store/**/*" 14 | - "**.json" 15 | - "**.md" 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "mocha": true 5 | }, 6 | "extends": ["airbnb-base", "plugin:prettier/recommended"], 7 | "plugins": ["import", "prettier"], 8 | "rules": { 9 | "arrow-parens": ["error", "always"], 10 | "no-underscore-dangle": 0, 11 | "no-console": "error", 12 | "prettier/prettier": [ 13 | "error", 14 | { 15 | "semi": true, 16 | "printWidth": 100, 17 | "singleQuote": true, 18 | "arrowParens": "always" 19 | } 20 | ], 21 | "func-names": ["error", "always"], 22 | "comma-dangle": ["error", "never"], 23 | "no-param-reassign": ["error", { "props": false }], 24 | "max-len": ["error", 100, { "ignoreRegExpLiterals": true }], 25 | "import/no-extraneous-dependencies": 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | source/* 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | .env 40 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "printWidth": 100, 4 | "singleQuote": true, 5 | "arrowParens": "always" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Stefan Aichholzer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node app/index.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 👏🏻 😎 🚀 😻 3 |

4 | 5 |

6 | whir.io 7 |

8 |

 

9 | 10 | [![Alpha](https://img.shields.io/badge/Status-ALPHA-8456AC.svg)](https://github.com/WhirIO/Client) 11 | [![ES7](https://img.shields.io/badge/JavaScript-ES7-00008B.svg)](https://github.com/WhirIO/Client) 12 | [![codebeat badge](https://codebeat.co/badges/f82e9789-770f-4a7a-a8e5-c9e061f71f77)](https://codebeat.co/projects/github-com-whirio-client-master) 13 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a360fc01aeab4d7084254dc6c09de3d9)](https://www.codacy.com/app/aichholzer/Client?utm_source=github.com&utm_medium=referral&utm_content=WhirIO/Client&utm_campaign=Badge_Grade) 14 | [![Downloads](https://img.shields.io/npm/dt/whir.io.svg)](https://www.npmjs.com/package/whir.io) 15 | [![Dependency status](https://gemnasium.com/badges/github.com/WhirIO/Client.svg)](https://gemnasium.com/github.com/WhirIO/Client) 16 | 17 | As a developer, I use my command line a lot so why not integrate chat directly into it?
18 | **whir** aims to close this gap by providing a simple, flexible, extend-able and blazing fast chat environment, without having to install additional applications. 19 | 20 | **whir** does not store messages in any form. 21 | 22 | ### Installation 23 | **whir** is written in JavaScript and you should have installed the latest version of Nodejs. 24 | ``` 25 | $> npm i -g whir.io 26 | ``` 27 | 28 | 29 | ### Options 30 | - `--user` || `-u`: Your username (per channel) `Required` 31 | - `--channel` || `-c`: Channel you are joining (or creating) `Default: [random]` 32 | - `--host` || `-h`: Whir's host `Default: chat.whir.io` 33 | - `--pass` || `-p`: Password, for private channels 34 | - `--mute` || `-m`: Mute this conversation 35 | 36 | 37 | ### Chat 38 | ``` 39 | $> whir.io --user=stefan --channel=friends 40 | ``` 41 | 42 | or 43 | 44 | ``` 45 | $> whir.io -u stefan -c friends 46 | ``` 47 | 48 | or 49 | 50 | ``` 51 | // Random channel and muted conversation. 52 | $> whir.io -u stefan -m 53 | ``` 54 | 55 | or 56 | 57 | ``` 58 | // Running whir on your own server/domain. 59 | $> whir.io -u stefan -c friends -h myawesomedomain.chat 60 | ``` 61 | 62 | 63 | ### Help 64 | Once connected, type `/help` for a list of basic commands.
65 | Type `/exit` to leave at any time. 66 | 67 | 68 | ### Notes 69 | You can also setup and run your own **whir** server.
70 | Here's how to do that: [https://github.com/WhirIO/Server](https://github.com/WhirIO/Server) 71 | 72 | 73 | ### Contribute 74 | ``` 75 | fork https://github.com/WhirIO/Client 76 | ``` 77 | 78 | 79 | ### License 80 | 81 | [MIT](https://github.com/WhirIO/Client/blob/master/LICENSE) 82 | -------------------------------------------------------------------------------- /app/core/components.js: -------------------------------------------------------------------------------- 1 | const blessed = require('blessed'); 2 | const Emitter = require('events').EventEmitter; 3 | 4 | /** 5 | * Enable scrolling through the conversation with the arrow keys. 6 | * @see screen.input.key('up', scroll); 7 | * @see screen.input.key('down', scroll); 8 | */ 9 | const inputHandler = (screen) => { 10 | const scroll = (char, key) => { 11 | const condition = () => 12 | key.name === 'up' ? screen.scrollIndex < screen.scroll.length : screen.scrollIndex > 1; 13 | if (screen.scroll.length) { 14 | let found = false; 15 | while (!found) { 16 | if (condition()) { 17 | found = true; 18 | screen.scrollIndex += key.name === 'up' ? 1 : -1; 19 | const data = screen.scroll[screen.scroll.length - screen.scrollIndex]; 20 | screen.input.setValue(data.message); 21 | return screen.render(); 22 | } 23 | found = true; 24 | } 25 | } 26 | return true; 27 | }; 28 | 29 | screen.input.key(['up', 'down'], scroll); 30 | screen.input.key(['C-c'], () => { 31 | screen.input.clearValue(); 32 | return screen.render(); 33 | }); 34 | 35 | screen.input.on('submit', (value) => { 36 | const submitValue = value.trim(); 37 | if (!submitValue) { 38 | return screen.render(); 39 | } 40 | 41 | screen.input.clearValue(); 42 | return screen.emit('message', submitValue); 43 | }); 44 | }; 45 | 46 | class Components extends Emitter { 47 | constructor(options) { 48 | super(); 49 | 50 | this.screen = blessed.screen(options); 51 | this.screen.title = options.screenTitle; 52 | 53 | this.scroll = []; 54 | this.scrollIndex = 0; 55 | } 56 | 57 | render(status = null) { 58 | if (status === 'no_history') { 59 | return; 60 | } 61 | 62 | if (!this.input.detached) { 63 | this.input.focus(); 64 | } 65 | 66 | this.screen.render(); 67 | } 68 | 69 | title() { 70 | this.title = blessed.text({ 71 | screen: this.screen, 72 | top: 0, 73 | width: '100%', 74 | height: 3, 75 | padding: 1, 76 | style: { 77 | bg: 'green', 78 | fg: 'black' 79 | } 80 | }); 81 | 82 | this.title.setText(this.screen.title); 83 | return this.title; 84 | } 85 | 86 | users() { 87 | this.users = blessed.list({ 88 | screen: this.screen, 89 | width: '25%', 90 | top: 3, 91 | keys: true, 92 | border: 'line', 93 | interactive: false, 94 | padding: { 95 | top: 0, 96 | right: 0, 97 | bottom: 1, 98 | left: 1 99 | }, 100 | style: { 101 | border: { 102 | fg: 'white' 103 | }, 104 | selected: { 105 | bg: 'green', 106 | fg: 'black' 107 | } 108 | } 109 | }); 110 | 111 | return this.users; 112 | } 113 | 114 | timeline() { 115 | this.timeline = blessed.box({ 116 | screen: this.screen, 117 | mouse: true, 118 | top: 3, 119 | left: '25%-1', 120 | height: '100%-7', 121 | border: 'line', 122 | scrollable: true, 123 | alwaysScroll: true, 124 | scrollbar: true, 125 | padding: { 126 | top: 1, 127 | right: 0, 128 | bottom: 1, 129 | left: 2 130 | }, 131 | style: { 132 | border: { 133 | fg: 'white' 134 | }, 135 | scrollbar: { 136 | bg: 'white', 137 | fg: 'black' 138 | } 139 | } 140 | }); 141 | 142 | return this.timeline; 143 | } 144 | 145 | input() { 146 | this.input = blessed.textbox({ 147 | screen: this.screen, 148 | content: '', 149 | border: 'line', 150 | padding: { 151 | top: 1, 152 | right: 2, 153 | bottom: 1, 154 | left: 2 155 | }, 156 | style: { 157 | fg: 'default', 158 | bg: 'default', 159 | border: { 160 | fg: 'white', 161 | bg: 'default' 162 | } 163 | }, 164 | left: '25%-1', 165 | height: 5, 166 | top: '100%-5', 167 | keys: true, 168 | mouse: true, 169 | inputOnFocus: true 170 | }); 171 | 172 | inputHandler(this); 173 | return this.input; 174 | } 175 | } 176 | 177 | module.exports = Components; 178 | -------------------------------------------------------------------------------- /app/core/screen.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | const chalk = require('chalk'); 4 | const Components = require('./components'); 5 | const moment = require('moment'); 6 | const string = require('../library/string'); 7 | 8 | class Screen { 9 | constructor(whir, { user = null, scrollSize = 250, mute = false }) { 10 | this.whir = whir; 11 | 12 | this.components = new Components({ 13 | smartCSR: true, 14 | dockBorders: true, 15 | fullUnicode: true, 16 | screenTitle: 'Whir.io' 17 | }); 18 | 19 | this.user = user; 20 | this.scrollSize = scrollSize; 21 | this.muteChannel = mute; 22 | 23 | this.components.on('message', (message) => this.whir.send(message)); 24 | this.components.screen.key(['escape'], this.destroy.bind(this, true)); 25 | this.components.screen.append(this.components.title()); 26 | this.components.screen.append(this.components.users()); 27 | this.components.screen.append(this.components.timeline()); 28 | this.components.screen.append(this.components.input()); 29 | this.components.render(); 30 | } 31 | 32 | /** 33 | * This is the history for the current session only. 34 | * It is non-atomic and it will be cleared when the application closes. 35 | * It can be accessed using the arrow keys. 36 | * @param item 37 | */ 38 | scroll(item) { 39 | if (item.user === this.user) { 40 | if (this.components.scroll.length >= this.scrollSize) { 41 | this.components.scroll.shift(); 42 | } 43 | 44 | this.components.scroll.push(item); 45 | this.components.scrollIndex = 0; 46 | } 47 | } 48 | 49 | print(data, { sender = 'whir', render = true } = {}) { 50 | /** 51 | * Notification sound on incoming messages, when mute = false 52 | */ 53 | if (sender !== 'me' && !this.muteChannel) { 54 | process.stdout.write('\u0007'); 55 | } 56 | 57 | /** 58 | * A blank line between messages from different users. 59 | * Might be removed if a "floating-box" approach is adopted. 60 | */ 61 | if (this.lastSender !== data.user && !data.command) { 62 | this.components.timeline.pushLine(''); 63 | } 64 | 65 | /** 66 | * Add or remove users from the user panel. 67 | * Skip this step when loading a user's history. 68 | * @see this.components.users() 69 | */ 70 | if (!data.fromHistory) { 71 | if (data.action === 'join') { 72 | this.components.users.addItem(data.user); 73 | } else if (data.action === 'leave') { 74 | this.components.users.removeItem(data.user); 75 | } 76 | } 77 | 78 | /** 79 | * When establishing a connection, all users are sent back. 80 | * This takes the data sent by the server and merges it with the 81 | * existing users, sorts them (alphabetically) and re-populates 82 | * the users list. 83 | */ 84 | if (data.currentUsers) { 85 | data.currentUsers = data.currentUsers 86 | .concat(this.components.users.children) 87 | .filter((x, i, a) => a.indexOf(x) === i) 88 | .sort(); 89 | 90 | this.components.users.setItems(data.currentUsers); 91 | } 92 | 93 | /** 94 | * Replacements; underline, bold, italics, etc. 95 | * Find and replace any emoji (as per: http://www.fileformat.info/info/emoji/list.htm) 96 | * Format the line to be rendered. 97 | * Render any additional payload sent by the server. 98 | * Scroll the timeline to the bottom. 99 | */ 100 | data.message = data.message.replace(/_([\w\s.]+)_/gi, chalk.green.underline('$1')); 101 | data.message = data.message.replace(/-([\w\s.]+)-/gi, chalk.white('$1')); 102 | data.message = string.emojinize(data.message); 103 | 104 | if (data.payload && data.payload.showTitle) { 105 | this.components.timeline.pushLine(data.message); 106 | } else if (!data.command) { 107 | data.timestamp = moment(data.timestamp).format('HH:mm'); 108 | data.timestamp = `${chalk.black.bgGreen(data.timestamp)} `; 109 | const user = data.user ? chalk.green(`${data.timestamp}${data.user}: `) : ''; 110 | if (data.alert) { 111 | data.message = data.message.split('\n'); 112 | data.message = data.message.map((message) => chalk.white.bgRed(message)); 113 | data.message = data.message.join('\n'); 114 | } 115 | this.components.timeline.pushLine(user + data.message); 116 | } 117 | 118 | /** 119 | * The response (payload) is flexible in order to accommodate 120 | * various operations based on whatever the server returns. 121 | * Currently only the "date" is in use. 122 | */ 123 | if (data.payload) { 124 | let padding = null; 125 | if (typeof data.payload.pad === 'number') { 126 | padding = data.payload.pad; 127 | } else if (data.payload.pad) { 128 | padding = 0; 129 | Object.entries(data.payload.items).forEach(([key]) => { 130 | padding = key.length > padding ? key.length : padding; 131 | }); 132 | 133 | const match = data.payload.pad.match(/\+([\d])+/i); 134 | if (match) { 135 | padding += parseInt(match[1], 10); 136 | } 137 | } 138 | 139 | Object.entries(data.payload.items).forEach(([key, item]) => { 140 | let passedItem; 141 | switch (item.type) { 142 | case 'date': 143 | passedItem = moment(item.value).fromNow(); 144 | break; 145 | default: 146 | passedItem = item.value; 147 | } 148 | 149 | const line = `\u258B ${string.pad({ key, side: 'right', padding })}${chalk.white( 150 | passedItem 151 | )}`; 152 | this.components.timeline.pushLine(line); 153 | }); 154 | } 155 | this.components.timeline.setScrollPerc(100); 156 | 157 | /** 158 | * Keep track of the user who send the last message, just for rendering 159 | * purposes and update the connected number of users. 160 | * @see this.components.users 161 | */ 162 | this.lastSender = data.user; 163 | const channel = `Channel: ${data.channel}`; 164 | const user = `User: ${this.whir.user}`; 165 | const users = `${this.components.users.children.length + 1}`; 166 | const title = `${ 167 | this.muteChannel ? '\uD83D\uDD07' : '\uD83D\uDD09' 168 | } ${channel} | ${user} | Users: ${users}`; 169 | this.components.title.setText(title); 170 | 171 | if (render) { 172 | this.components.render(); 173 | } 174 | 175 | return this; 176 | } 177 | 178 | destroy(exit = false) { 179 | this.components.screen.destroy(); 180 | if (exit) { 181 | console.log(` 👋 See you soon, ${this.user}!`); 182 | process.exit(0); 183 | } 184 | } 185 | } 186 | 187 | module.exports = Screen; 188 | -------------------------------------------------------------------------------- /app/core/whir.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | const crypto = require('../library/crypto'); 4 | const fs = require('fs'); 5 | const lineReader = require('readline'); 6 | const Screen = require('./screen'); 7 | const WS = require('ws'); 8 | const { EventEmitter } = require('events'); 9 | const { Spinner } = require('cli-spinner'); 10 | 11 | const getHeaders = async ({ user, channel, pass, store }) => { 12 | const headers = (data) => ({ 13 | headers: { 14 | 'x-whir-session': data.session, 15 | 'x-whir-channel': channel || '', 16 | 'x-whir-user': user, 17 | 'x-whir-pass': pass || '' 18 | } 19 | }); 20 | 21 | try { 22 | const data = fs.readFileSync(`${store}/${user}.whir`, 'utf8'); 23 | return headers(JSON.parse(data)); 24 | } catch (error) { 25 | const session = crypto.hash(await crypto.bytes(128), 'RSA-SHA256'); 26 | try { 27 | fs.appendFileSync(`${store}/${user}.whir`, JSON.stringify({ session }), { flag: 'a' }); 28 | return headers({ session }); 29 | } catch (writeError) { 30 | return { error: writeError }; 31 | } 32 | } 33 | }; 34 | 35 | class Whir extends EventEmitter { 36 | constructor(argv = {}, unsecure = false) { 37 | super(); 38 | 39 | this.host = argv.host; 40 | this.user = argv.user; 41 | this.channel = argv.channel; 42 | this.scrollSize = argv.scroll; 43 | this.store = argv.store; 44 | this.muteChannel = argv.mute || false; 45 | 46 | this.protocol = `ws${unsecure ? '' : 's'}`; 47 | getHeaders(argv) 48 | .then((headers) => { 49 | if (headers.error) { 50 | throw headers.error; 51 | } 52 | 53 | console.log(); 54 | this.spinner = new Spinner(' %s Connecting...'); 55 | this.spinner.setSpinnerString(18); 56 | this.spinner.start(); 57 | return this.connect(headers); 58 | }) 59 | .catch((error) => this.emit('error', error)); 60 | } 61 | 62 | connect(headers) { 63 | try { 64 | this.socket = new WS(`${this.protocol}://${this.host}`, headers); 65 | } catch (error) { 66 | return this.emit('error', error); 67 | } 68 | 69 | setInterval(() => { 70 | if (!this.socket.whirAlive) { 71 | return this.socket.terminate(); 72 | } 73 | 74 | this.socket.whirAlive = false; 75 | return this.socket.ping('', true, true); 76 | }, 40000); 77 | 78 | return this.socket 79 | .on('open', async () => { 80 | this.socket.whirAlive = true; 81 | }) 82 | .on('message', this.messageHandler.bind(this)) 83 | .on('error', (error) => { 84 | this.spinner.stop(true); 85 | this.emit('error', error); 86 | }) 87 | .on('close', (code, data) => { 88 | this.spinner.stop(true); 89 | this.emit('close', data); 90 | }) 91 | .on('pong', function pong() { 92 | this.whirAlive = true; 93 | }); 94 | } 95 | 96 | async messageHandler(data) { 97 | try { 98 | if (!this.isLoaded) { 99 | this.spinner.stop(true); 100 | this.screen = new Screen(this, { 101 | user: this.user, 102 | scrollSize: this.scroll, 103 | mute: this.muteChannel 104 | }); 105 | this.screen.muteChannel = true; 106 | 107 | await this.loadHistory(); 108 | this.screen.muteChannel = this.muteChannel; 109 | this.isLoaded = true; 110 | } 111 | 112 | const parsedData = JSON.parse(data.toString('utf8')); 113 | this.channel = parsedData.channel || this.channel; 114 | parsedData.timestamp = new Date().getTime(); 115 | 116 | await this.writeHistory(parsedData); 117 | return this.emit('received', parsedData); 118 | } catch (error) { 119 | return this.emit('error', error); 120 | } 121 | } 122 | 123 | send(message) { 124 | const data = { 125 | user: this.user, 126 | channel: this.channel, 127 | message, 128 | timestamp: new Date().getTime() 129 | }; 130 | 131 | let localCommand = false; 132 | if (data.message.match(/^\/[\w]+/)) { 133 | data.command = data.message.replace(/^\//g, ''); 134 | switch (data.command) { 135 | case 'exit': 136 | return this.screen.destroy(true); 137 | case 'clear': 138 | localCommand = true; 139 | this.screen.components.timeline.getLines().forEach((lines, index) => { 140 | this.screen.components.timeline.deleteLine(index); 141 | }); 142 | break; 143 | case 'mute': 144 | localCommand = true; 145 | this.screen.muteChannel = true; 146 | break; 147 | case 'unmute': 148 | localCommand = true; 149 | this.screen.muteChannel = false; 150 | break; 151 | default: 152 | } 153 | } 154 | 155 | if (!localCommand) { 156 | this.socket.send(JSON.stringify(data), { binary: true, mask: true }); 157 | } 158 | 159 | this.writeHistory(data); 160 | this.screen.scroll(data); 161 | return this.emit('sent', data); 162 | } 163 | 164 | writeHistory(data) { 165 | return new Promise((yes) => { 166 | if (!data.user) { 167 | return yes(); 168 | } 169 | 170 | const file = `${this.store}/${this.user}.${this.channel}.whir`; 171 | return fs.appendFile(file, `${JSON.stringify(data)}\n`, (error) => { 172 | if (error) { 173 | return this.emit('alert', 'Your conversation could not be saved.'); 174 | } 175 | 176 | return yes(); 177 | }); 178 | }); 179 | } 180 | 181 | loadHistory() { 182 | const history = `${this.store}/${this.user}.${this.channel}.whir`; 183 | const fileStream = (yes) => 184 | fs 185 | .createReadStream(history) 186 | .on('error', yes.bind(null, 'no_history')) 187 | .on('end', yes); 188 | const readLine = (no, line) => { 189 | try { 190 | const data = JSON.parse(line); 191 | data.fromHistory = true; 192 | this.screen.print(data, { render: false }); 193 | if (data.user === this.user) { 194 | this.screen.scroll(data); 195 | } 196 | return true; 197 | } catch (error) { 198 | return no(error); 199 | } 200 | }; 201 | 202 | return new Promise((yes, no) => { 203 | lineReader.createInterface({ input: fileStream(yes) }).on('line', readLine.bind(null, no)); 204 | }); 205 | } 206 | 207 | error(data) { 208 | let errorData = {}; 209 | try { 210 | if (data) { 211 | errorData = JSON.parse(data); 212 | } 213 | } catch (error) { 214 | // data is not JSON or is empty 215 | } 216 | 217 | if (errorData.code === 'ECONNREFUSED') { 218 | errorData.message = ` It was not possible to connect to the server.\n (${errorData.message}) 219 | \n Make sure your whir.io server is listening.`; 220 | } else { 221 | errorData.message = ` ${errorData.message || 'The server terminated your connection.'}`; 222 | } 223 | 224 | if (this.screen) { 225 | this.screen.destroy(); 226 | } 227 | 228 | console.error(`${errorData.message}`); 229 | process.exit(0); 230 | } 231 | } 232 | 233 | module.exports = Whir; 234 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const yargs = require('yargs'); 5 | const Whir = require('./core/whir'); 6 | 7 | const expect = { 8 | user: { alias: 'u', describe: 'Username.', demand: true }, 9 | pass: { alias: 'p', describe: 'Password.', default: null }, 10 | channel: { alias: 'c', describe: 'Channel.', default: null }, 11 | host: { alias: 'h', describe: 'Whir.io server.', default: 'chat.whir.io' }, 12 | mute: { alias: 'm', describe: 'Mute the conversation.' }, 13 | store: { 14 | alias: 's', 15 | describe: 'Where to store application data.', 16 | default: path.normalize(`${__dirname}/../store`) 17 | }, 18 | scroll: { alias: 'sc', describe: 'Lines to keep in scroll history.', default: 100 } 19 | }; 20 | const { argv } = yargs 21 | .options(expect) 22 | .usage('\nUsage: whir.io --user=[user]') 23 | .example('whir.io --user=stefan') 24 | .example('whir.io -u stefan -c friends') 25 | .epilogue('For more information, visit https://whir.io'); 26 | const whir = new Whir(argv, process.env.UNSECURE_SOCKET === 'true'); 27 | 28 | /** 29 | * Emitting events makes the architecture more plug-able. 30 | * It's easy to implement custom logic -or extended the existing one- 31 | * for each emitted event. 32 | */ 33 | whir 34 | .on('sent', (data) => whir.screen.print(data, { sender: 'me' })) 35 | .on('received', (data) => whir.screen.print(data)) 36 | .on('alert', (data) => whir.error(data)) 37 | .on('close', (data) => whir.error(data)) 38 | .on('error', (data) => whir.error(data)); 39 | -------------------------------------------------------------------------------- /app/library/crypto.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | module.exports = { 4 | bytes: (length, encoding = 'hex') => 5 | new Promise((yes, no) => { 6 | crypto.randomBytes(length, (error, bytes) => { 7 | if (error || !bytes) { 8 | return no(new Error('Unable to get random data.')); 9 | } 10 | 11 | return yes(bytes.toString(encoding)); 12 | }); 13 | }), 14 | 15 | hash: (data, algorithm = 'RSA-SHA512', encoding = 'hex') => { 16 | const newData = typeof data !== 'string' ? JSON.stringify(data) : data; 17 | return crypto 18 | .createHash(algorithm) 19 | .update(newData, 'utf8') 20 | .digest(encoding); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /app/library/string.js: -------------------------------------------------------------------------------- 1 | const emoji = require('../support/emoji.json'); 2 | 3 | module.exports = { 4 | emojinize: (input) => input.replace(/:([\w]+):/g, (match, icon) => emoji[icon] || match), 5 | 6 | pad: ({ key, side = 'right', padding = null, char = ' ' }) => { 7 | if (!key || !padding || key.length >= padding) { 8 | return key; 9 | } 10 | 11 | const pad = char.repeat(padding - key.length); 12 | return side === 'right' ? key + pad : pad + key; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /app/support/emoji.json: -------------------------------------------------------------------------------- 1 | {"100":"💯","1234":"🔢","interrobang":"⁉️","tm":"™️","information_source":"ℹ️","left_right_arrow":"↔️","arrow_up_down":"↕️","arrow_upper_left":"↖️","arrow_upper_right":"↗️","arrow_lower_right":"↘️","arrow_lower_left":"↙️","keyboard":"⌨","sunny":"☀️","cloud":"☁️","umbrella":"☔️","showman":"☃","comet":"☄","ballot_box_with_check":"☑️","coffee":"☕️","shamrock":"☘","skull_and_crossbones":"☠","radioactive_sign":"☢","biohazard_sign":"☣","orthodox_cross":"☦","wheel_of_dharma":"☸","white_frowning_face":"☹","aries":"♈️","taurus":"♉️","sagittarius":"♐️","capricorn":"♑️","aquarius":"♒️","pisces":"♓️","spades":"♠️","clubs":"♣️","hearts":"♥️","diamonds":"♦️","hotsprings":"♨️","hammer_and_pick":"⚒","anchor":"⚓️","crossed_swords":"⚔","scales":"⚖","alembic":"⚗","gear":"⚙","scissors":"✂️","white_check_mark":"✅","airplane":"✈️","email":"✉️","envelope":"✉️","black_nib":"✒️","heavy_check_mark":"✔️","heavy_multiplication_x":"✖️","star_of_david":"✡","sparkles":"✨","eight_spoked_asterisk":"✳️","eight_pointed_black_star":"✴️","snowflake":"❄️","sparkle":"❇️","question":"❓","grey_question":"❔","grey_exclamation":"❕","exclamation":"❗️","heavy_exclamation_mark":"❗️","heavy_heart_exclamation_mark_ornament":"❣","heart":"❤️","heavy_plus_sign":"➕","heavy_minus_sign":"➖","heavy_division_sign":"➗","arrow_heading_up":"⤴️","arrow_heading_down":"⤵️","wavy_dash":"〰️","congratulations":"㊗️","secret":"㊙️","copyright":"©️","registered":"®️","bangbang":"‼️","leftwards_arrow_with_hook":"↩️","arrow_right_hook":"↪️","watch":"⌚️","hourglass":"⌛️","fast_forward":"⏩","rewind":"⏪","arrow_double_up":"⏫","arrow_double_down":"⏬","black_right_pointing_double_triangle_with_vertical_bar":"⏭","black_left_pointing_double_triangle_with_vertical_bar":"⏮","black_right_pointing_triangle_with_double_vertical_bar":"⏯","alarm_clock":"⏰","stopwatch":"⏱","timer_clock":"⏲","hourglass_flowing_sand":"⏳","double_vertical_bar":"⏸","black_square_for_stop":"⏹","black_circle_for_record":"⏺","m":"Ⓜ️","black_small_square":"▪️","white_small_square":"▫️","arrow_forward":"▶️","arrow_backward":"◀️","white_medium_square":"◻️","black_medium_square":"◼️","white_medium_small_square":"◽️","black_medium_small_square":"◾️","phone":"☎️","telephone":"☎️","point_up":"☝️","star_and_crescent":"☪","peace_symbol":"☮","yin_yang":"☯","relaxed":"☺️","gemini":"♊️","cancer":"♋️","leo":"♌️","virgo":"♍️","libra":"♎️","scorpius":"♏️","recycle":"♻️","wheelchair":"♿️","atom_symbol":"⚛","fleur_de_lis":"⚜","warning":"⚠️","zap":"⚡️","white_circle":"⚪️","black_circle":"⚫️","coffin":"⚰","funeral_urn":"⚱","soccer":"⚽️","baseball":"⚾️","snowman":"⛄️","partly_sunny":"⛅️","thunder_cloud_and_rain":"⛈","ophiuchus":"⛎","pick":"⛏","helmet_with_white_cross":"⛑","chains":"⛓","no_entry":"⛔️","shinto_shrine":"⛩","church":"⛪️","mountain":"⛰","umbrella_on_ground":"⛱","fountain":"⛲️","golf":"⛳️","ferry":"⛴","boat":"⛵️","sailboat":"⛵️","skier":"⛷","ice_skate":"⛸","person_with_ball":"⛹","tent":"⛺️","fuelpump":"⛽️","fist":"✊","hand":"✋","raised_hand":"✋","v":"✌️","writing_hand":"✍","pencil2":"✏️","latin_cross":"✝","x":"❌","negative_squared_cross_mark":"❎","arrow_right":"➡️","curly_loop":"➰","loop":"➿","arrow_left":"⬅️","arrow_up":"⬆️","arrow_down":"⬇️","black_large_square":"⬛️","white_large_square":"⬜️","star":"⭐️","o":"⭕️","part_alternation_mark":"〽️","mahjong":"🀄️","black_joker":"🃏","a":"🅰️","b":"🅱️","o2":"🅾️","parking":"🅿️","ab":"🆎","cl":"🆑","cool":"🆒","free":"🆓","id":"🆔","new":"🆕","ng":"🆖","ok":"🆗","sos":"🆘","up":"🆙","vs":"🆚","koko":"🈁","sa":"🈂️","u7121":"🈚️","u6307":"🈯️","u7981":"🈲","u7a7a":"🈳","u5408":"🈴","u6e80":"🈵","u6709":"🈶","u6708":"🈷️","u7533":"🈸","u5272":"🈹","u55b6":"🈺","ideograph_advantage":"🉐","accept":"🉑","cyclone":"🌀","foggy":"🌁","closed_umbrella":"🌂","night_with_stars":"🌃","sunrise_over_mountains":"🌄","sunrise":"🌅","city_sunset":"🌆","city_sunrise":"🌇","rainbow":"🌈","bridge_at_night":"🌉","ocean":"🌊","volcano":"🌋","milky_way":"🌌","earth_africa":"🌍","earth_americas":"🌎","earth_asia":"🌏","globe_with_meridians":"🌐","new_moon":"🌑","waxing_crescent_moon":"🌒","first_quarter_moon":"🌓","moon":"🌔","waxing_gibbous_moon":"🌔","full_moon":"🌕","waning_gibbous_moon":"🌖","last_quarter_moon":"🌗","waning_crescent_moon":"🌘","crescent_moon":"🌙","new_moon_with_face":"🌚","first_quarter_moon_with_face":"🌛","last_quarter_moon_with_face":"🌜","full_moon_with_face":"🌝","sun_with_face":"🌞","star2":"🌟","stars":"🌠","thermometer":"🌡","mostly_sunny":"🌤","sun_small_cloud":"🌤","barely_sunny":"🌥","sun_behind_cloud":"🌥","partly_sunny_rain":"🌦","sun_behind_rain_cloud":"🌦","rain_cloud":"🌧","snow_cloud":"🌨","lightning":"🌩","lightning_cloud":"🌩","tornado":"🌪","tornado_cloud":"🌪","fog":"🌫","wind_blowing_face":"🌬","hotdog":"🌭","taco":"🌮","burrito":"🌯","chestnut":"🌰","seedling":"🌱","evergreen_tree":"🌲","deciduous_tree":"🌳","palm_tree":"🌴","cactus":"🌵","hot_pepper":"🌶","tulip":"🌷","cherry_blossom":"🌸","rose":"🌹","hibiscus":"🌺","sunflower":"🌻","blossom":"🌼","corn":"🌽","ear_of_rice":"🌾","herb":"🌿","four_leaf_clover":"🍀","maple_leaf":"🍁","fallen_leaf":"🍂","leaves":"🍃","mushroom":"🍄","tomato":"🍅","eggplant":"🍆","grapes":"🍇","melon":"🍈","watermelon":"🍉","tangerine":"🍊","lemon":"🍋","banana":"🍌","pineapple":"🍍","apple":"🍎","green_apple":"🍏","pear":"🍐","peach":"🍑","cherries":"🍒","strawberry":"🍓","hamburger":"🍔","pizza":"🍕","meat_on_bone":"🍖","poultry_leg":"🍗","rice_cracker":"🍘","rice_ball":"🍙","rice":"🍚","curry":"🍛","ramen":"🍜","spaghetti":"🍝","bread":"🍞","fries":"🍟","sweet_potato":"🍠","dango":"🍡","oden":"🍢","sushi":"🍣","fried_shrimp":"🍤","fish_cake":"🍥","icecream":"🍦","shaved_ice":"🍧","ice_cream":"🍨","doughnut":"🍩","cookie":"🍪","chocolate_bar":"🍫","candy":"🍬","lollipop":"🍭","custard":"🍮","honey_pot":"🍯","cake":"🍰","bento":"🍱","stew":"🍲","egg":"🍳","fork_and_knife":"🍴","tea":"🍵","sake":"🍶","wine_glass":"🍷","cocktail":"🍸","tropical_drink":"🍹","beer":"🍺","beers":"🍻","baby_bottle":"🍼","knife_fork_plate":"🍽","champagne":"🍾","popcorn":"🍿","ribbon":"🎀","gift":"🎁","birthday":"🎂","jack_o_lantern":"🎃","christmas_tree":"🎄","santa":"🎅","fireworks":"🎆","sparkler":"🎇","balloon":"🎈","tada":"🎉","confetti_ball":"🎊","tanabata_tree":"🎋","crossed_flags":"🎌","bamboo":"🎍","dolls":"🎎","flags":"🎏","wind_chime":"🎐","rice_scene":"🎑","school_satchel":"🎒","mortar_board":"🎓","medal":"🎖","reminder_ribbon":"🎗","studio_microphone":"🎙","level_slider":"🎚","control_knobs":"🎛","film_frames":"🎞","admission_tickets":"🎟","carousel_horse":"🎠","ferris_wheel":"🎡","roller_coaster":"🎢","fishing_pole_and_fish":"🎣","microphone":"🎤","movie_camera":"🎥","cinema":"🎦","headphones":"🎧","art":"🎨","tophat":"🎩","circus_tent":"🎪","ticket":"🎫","clapper":"🎬","performing_arts":"🎭","video_game":"🎮","dart":"🎯","slot_machine":"🎰","8ball":"🎱","game_die":"🎲","bowling":"🎳","flower_playing_cards":"🎴","musical_note":"🎵","notes":"🎶","saxophone":"🎷","guitar":"🎸","musical_keyboard":"🎹","trumpet":"🎺","violin":"🎻","musical_score":"🎼","running_shirt_with_sash":"🎽","tennis":"🎾","ski":"🎿","basketball":"🏀","checkered_flag":"🏁","snowboarder":"🏂","runner":"🏃","running":"🏃","surfer":"🏄","sports_medal":"🏅","trophy":"🏆","horse_racing":"🏇","football":"🏈","rugby_football":"🏉","swimmer":"🏊","weight_lifter":"🏋","golfer":"🏌","racing_motorcycle":"🏍","racing_car":"🏎","cricket_bat_and_ball":"🏏","volleyball":"🏐","field_hockey_stick_and_ball":"🏑","ice_hockey_stick_and_puck":"🏒","table_tennis_paddle_and_ball":"🏓","snow_capped_mountain":"🏔","camping":"🏕","beach_with_umbrella":"🏖","building_construction":"🏗","house_buildings":"🏘","cityscape":"🏙","derelict_house_building":"🏚","classical_building":"🏛","desert":"🏜","desert_island":"🏝","national_park":"🏞","stadium":"🏟","house":"🏠","house_with_garden":"🏡","office":"🏢","post_office":"🏣","european_post_office":"🏤","hospital":"🏥","bank":"🏦","atm":"🏧","hotel":"🏨","love_hotel":"🏩","convenience_store":"🏪","school":"🏫","department_store":"🏬","factory":"🏭","izakaya_lantern":"🏮","lantern":"🏮","japanese_castle":"🏯","european_castle":"🏰","waving_white_flag":"🏳","waving_black_flag":"🏴","rosette":"🏵","label":"🏷","badminton_racquet_and_shuttlecock":"🏸","bow_and_arrow":"🏹","amphora":"🏺","skin-tone-2":"🏻","skin-tone-3":"🏼","skin-tone-4":"🏽","skin-tone-5":"🏾","skin-tone-6":"🏿","rat":"🐀","mouse2":"🐁","ox":"🐂","water_buffalo":"🐃","cow2":"🐄","tiger2":"🐅","leopard":"🐆","rabbit2":"🐇","cat2":"🐈","dragon":"🐉","crocodile":"🐊","whale2":"🐋","snail":"🐌","snake":"🐍","racehorse":"🐎","ram":"🐏","goat":"🐐","sheep":"🐑","monkey":"🐒","rooster":"🐓","chicken":"🐔","dog2":"🐕","pig2":"🐖","boar":"🐗","elephant":"🐘","octopus":"🐙","shell":"🐚","bug":"🐛","ant":"🐜","bee":"🐝","honeybee":"🐝","beetle":"🐞","fish":"🐟","tropical_fish":"🐠","blowfish":"🐡","turtle":"🐢","hatching_chick":"🐣","baby_chick":"🐤","hatched_chick":"🐥","bird":"🐦","penguin":"🐧","koala":"🐨","poodle":"🐩","dromedary_camel":"🐪","camel":"🐫","dolphin":"🐬","flipper":"🐬","mouse":"🐭","cow":"🐮","tiger":"🐯","rabbit":"🐰","cat":"🐱","dragon_face":"🐲","whale":"🐳","horse":"🐴","monkey_face":"🐵","dog":"🐶","pig":"🐷","frog":"🐸","hamster":"🐹","wolf":"🐺","bear":"🐻","panda_face":"🐼","pig_nose":"🐽","feet":"🐾","paw_prints":"🐾","chipmunk":"🐿","eyes":"👀","eye":"👁","ear":"👂","nose":"👃","lips":"👄","tongue":"👅","point_up_2":"👆","point_down":"👇","point_left":"👈","point_right":"👉","facepunch":"👊","punch":"👊","wave":"👋","ok_hand":"👌","+1":"👍","thumbsup":"👍","-1":"👎","thumbsdown":"👎","clap":"👏","open_hands":"👐","crown":"👑","womans_hat":"👒","eyeglasses":"👓","necktie":"👔","shirt":"👕","tshirt":"👕","jeans":"👖","dress":"👗","kimono":"👘","bikini":"👙","womans_clothes":"👚","purse":"👛","handbag":"👜","pouch":"👝","mans_shoe":"👞","shoe":"👞","athletic_shoe":"👟","high_heel":"👠","sandal":"👡","boot":"👢","footprints":"👣","bust_in_silhouette":"👤","busts_in_silhouette":"👥","boy":"👦","girl":"👧","man":"👨","woman":"👩","family":"👨‍👩‍👦","man-woman-boy":"👨‍👩‍👦","couple":"👫","man_and_woman_holding_hands":"👫","two_men_holding_hands":"👬","two_women_holding_hands":"👭","cop":"👮","dancers":"👯","bride_with_veil":"👰","person_with_blond_hair":"👱","man_with_gua_pi_mao":"👲","man_with_turban":"👳","older_man":"👴","older_woman":"👵","baby":"👶","construction_worker":"👷","princess":"👸","japanese_ogre":"👹","japanese_goblin":"👺","ghost":"👻","angel":"👼","alien":"👽","space_invader":"👾","imp":"👿","skull":"💀","information_desk_person":"💁","guardsman":"💂","dancer":"💃","lipstick":"💄","nail_care":"💅","massage":"💆","haircut":"💇","barber":"💈","syringe":"💉","pill":"💊","kiss":"💋","love_letter":"💌","ring":"💍","gem":"💎","couplekiss":"💏","bouquet":"💐","couple_with_heart":"💑","wedding":"💒","heartbeat":"💓","broken_heart":"💔","two_hearts":"💕","sparkling_heart":"💖","heartpulse":"💗","cupid":"💘","blue_heart":"💙","green_heart":"💚","yellow_heart":"💛","purple_heart":"💜","gift_heart":"💝","revolving_hearts":"💞","heart_decoration":"💟","diamond_shape_with_a_dot_inside":"💠","bulb":"💡","anger":"💢","bomb":"💣","zzz":"💤","boom":"💥","collision":"💥","sweat_drops":"💦","droplet":"💧","dash":"💨","hankey":"💩","poop":"💩","shit":"💩","muscle":"💪","dizzy":"💫","speech_balloon":"💬","thought_balloon":"💭","white_flower":"💮","moneybag":"💰","currency_exchange":"💱","heavy_dollar_sign":"💲","credit_card":"💳","yen":"💴","dollar":"💵","euro":"💶","pound":"💷","money_with_wings":"💸","chart":"💹","seat":"💺","computer":"💻","briefcase":"💼","minidisc":"💽","floppy_disk":"💾","cd":"💿","dvd":"📀","file_folder":"📁","open_file_folder":"📂","page_with_curl":"📃","page_facing_up":"📄","date":"📅","calendar":"📆","card_index":"📇","chart_with_upwards_trend":"📈","chart_with_downwards_trend":"📉","bar_chart":"📊","clipboard":"📋","pushpin":"📌","round_pushpin":"📍","paperclip":"📎","straight_ruler":"📏","triangular_ruler":"📐","bookmark_tabs":"📑","ledger":"📒","notebook":"📓","notebook_with_decorative_cover":"📔","closed_book":"📕","book":"📖","open_book":"📖","green_book":"📗","blue_book":"📘","orange_book":"📙","books":"📚","name_badge":"📛","scroll":"📜","memo":"📝","pencil":"📝","telephone_receiver":"📞","pager":"📟","fax":"📠","satellite":"🛰","loudspeaker":"📢","mega":"📣","outbox_tray":"📤","inbox_tray":"📥","package":"📦","e-mail":"📧","incoming_envelope":"📨","envelope_with_arrow":"📩","mailbox_closed":"📪","mailbox":"📫","mailbox_with_mail":"📬","mailbox_with_no_mail":"📭","postbox":"📮","postal_horn":"📯","newspaper":"📰","iphone":"📱","calling":"📲","vibration_mode":"📳","mobile_phone_off":"📴","no_mobile_phones":"📵","signal_strength":"📶","camera":"📷","camera_with_flash":"📸","video_camera":"📹","tv":"📺","radio":"📻","vhs":"📼","film_projector":"📽","prayer_beads":"📿","twisted_rightwards_arrows":"🔀","repeat":"🔁","repeat_one":"🔂","arrows_clockwise":"🔃","arrows_counterclockwise":"🔄","low_brightness":"🔅","high_brightness":"🔆","mute":"🔇","speaker":"🔈","sound":"🔉","loud_sound":"🔊","battery":"🔋","electric_plug":"🔌","mag":"🔍","mag_right":"🔎","lock_with_ink_pen":"🔏","closed_lock_with_key":"🔐","key":"🔑","lock":"🔒","unlock":"🔓","bell":"🔔","no_bell":"🔕","bookmark":"🔖","link":"🔗","radio_button":"🔘","back":"🔙","end":"🔚","on":"🔛","soon":"🔜","top":"🔝","underage":"🔞","keycap_ten":"🔟","capital_abcd":"🔠","abcd":"🔡","symbols":"🔣","abc":"🔤","fire":"🔥","flashlight":"🔦","wrench":"🔧","hammer":"🔨","nut_and_bolt":"🔩","hocho":"🔪","knife":"🔪","gun":"🔫","microscope":"🔬","telescope":"🔭","crystal_ball":"🔮","six_pointed_star":"🔯","beginner":"🔰","trident":"🔱","black_square_button":"🔲","white_square_button":"🔳","red_circle":"🔴","large_blue_circle":"🔵","large_orange_diamond":"🔶","large_blue_diamond":"🔷","small_orange_diamond":"🔸","small_blue_diamond":"🔹","small_red_triangle":"🔺","small_red_triangle_down":"🔻","arrow_up_small":"🔼","arrow_down_small":"🔽","om_symbol":"🕉","dove_of_peace":"🕊","kaaba":"🕋","mosque":"🕌","synagogue":"🕍","menorah_with_nine_branches":"🕎","clock1":"🕐","clock2":"🕑","clock3":"🕒","clock4":"🕓","clock5":"🕔","clock6":"🕕","clock7":"🕖","clock8":"🕗","clock9":"🕘","clock10":"🕙","clock11":"🕚","clock12":"🕛","clock130":"🕜","clock230":"🕝","clock330":"🕞","clock430":"🕟","clock530":"🕠","clock630":"🕡","clock730":"🕢","clock830":"🕣","clock930":"🕤","clock1030":"🕥","clock1130":"🕦","clock1230":"🕧","candle":"🕯","mantelpiece_clock":"🕰","hole":"🕳","man_in_business_suit_levitating":"🕴","sleuth_or_spy":"🕵","dark_sunglasses":"🕶","spider":"🕷","spider_web":"🕸","joystick":"🕹","linked_paperclips":"🖇","lower_left_ballpoint_pen":"🖊","lower_left_fountain_pen":"🖋","lower_left_paintbrush":"🖌","lower_left_crayon":"🖍","raised_hand_with_fingers_splayed":"🖐","middle_finger":"🖕","reversed_hand_with_middle_finger_extended":"🖕","spock-hand":"🖖","desktop_computer":"🖥","printer":"🖨","three_button_mouse":"🖱","trackball":"🖲","frame_with_picture":"🖼","card_index_dividers":"🗂","card_file_box":"🗃","file_cabinet":"🗄","wastebasket":"🗑","spiral_note_pad":"🗒","spiral_calendar_pad":"🗓","compression":"🗜","old_key":"🗝","rolled_up_newspaper":"🗞","dagger_knife":"🗡","speaking_head_in_silhouette":"🗣","left_speech_bubble":"🗨","right_anger_bubble":"🗯","ballot_box_with_ballot":"🗳","world_map":"🗺","mount_fuji":"🗻","tokyo_tower":"🗼","statue_of_liberty":"🗽","japan":"🗾","moyai":"🗿","grinning":"😀","grin":"😁","joy":"😂","smiley":"😃","smile":"😄","sweat_smile":"😅","laughing":"😆","satisfied":"😆","innocent":"😇","smiling_imp":"😈","wink":"😉","blush":"😊","yum":"😋","relieved":"😌","heart_eyes":"😍","sunglasses":"😎","smirk":"😏","neutral_face":"😐","expressionless":"😑","unamused":"😒","sweat":"😓","pensive":"😔","confused":"😕","confounded":"😖","kissing":"😗","kissing_heart":"😘","kissing_smiling_eyes":"😙","kissing_closed_eyes":"😚","stuck_out_tongue":"😛","stuck_out_tongue_winking_eye":"😜","stuck_out_tongue_closed_eyes":"😝","disappointed":"😞","worried":"😟","angry":"😠","rage":"😡","cry":"😢","persevere":"😣","triumph":"😤","disappointed_relieved":"😥","frowning":"😦","anguished":"😧","fearful":"😨","weary":"😩","sleepy":"😪","tired_face":"😫","grimacing":"😬","sob":"😭","open_mouth":"😮","hushed":"😯","cold_sweat":"😰","scream":"😱","astonished":"😲","flushed":"😳","sleeping":"😴","dizzy_face":"😵","no_mouth":"😶","mask":"😷","smile_cat":"😸","joy_cat":"😹","smiley_cat":"😺","heart_eyes_cat":"😻","smirk_cat":"😼","kissing_cat":"😽","pouting_cat":"😾","crying_cat_face":"😿","scream_cat":"🙀","slightly_frowning_face":"🙁","slightly_smiling_face":"🙂","upside_down_face":"🙃","face_with_rolling_eyes":"🙄","no_good":"🙅","ok_woman":"🙆","bow":"🙇","see_no_evil":"🙈","hear_no_evil":"🙉","speak_no_evil":"🙊","raising_hand":"🙋","raised_hands":"🙌","person_frowning":"🙍","person_with_pouting_face":"🙎","pray":"🙏","rocket":"🚀","helicopter":"🚁","steam_locomotive":"🚂","railway_car":"🚃","bullettrain_side":"🚄","bullettrain_front":"🚅","train2":"🚆","metro":"🚇","light_rail":"🚈","station":"🚉","tram":"🚊","train":"🚋","bus":"🚌","oncoming_bus":"🚍","trolleybus":"🚎","busstop":"🚏","minibus":"🚐","ambulance":"🚑","fire_engine":"🚒","police_car":"🚓","oncoming_police_car":"🚔","taxi":"🚕","oncoming_taxi":"🚖","car":"🚗","red_car":"🚗","oncoming_automobile":"🚘","blue_car":"🚙","truck":"🚚","articulated_lorry":"🚛","tractor":"🚜","monorail":"🚝","mountain_railway":"🚞","suspension_railway":"🚟","mountain_cableway":"🚠","aerial_tramway":"🚡","ship":"🚢","rowboat":"🚣","speedboat":"🚤","traffic_light":"🚥","vertical_traffic_light":"🚦","construction":"🚧","rotating_light":"🚨","triangular_flag_on_post":"🚩","door":"🚪","no_entry_sign":"🚫","smoking":"🚬","no_smoking":"🚭","put_litter_in_its_place":"🚮","do_not_litter":"🚯","potable_water":"🚰","non-potable_water":"🚱","bike":"🚲","no_bicycles":"🚳","bicyclist":"🚴","mountain_bicyclist":"🚵","walking":"🚶","no_pedestrians":"🚷","children_crossing":"🚸","mens":"🚹","womens":"🚺","restroom":"🚻","baby_symbol":"🚼","toilet":"🚽","wc":"🚾","shower":"🚿","bath":"🛀","bathtub":"🛁","passport_control":"🛂","customs":"🛃","baggage_claim":"🛄","left_luggage":"🛅","couch_and_lamp":"🛋","sleeping_accommodation":"🛌","shopping_bags":"🛍","bellhop_bell":"🛎","bed":"🛏","place_of_worship":"🛐","hammer_and_wrench":"🛠","shield":"🛡","oil_drum":"🛢","motorway":"🛣","railway_track":"🛤","motor_boat":"🛥","small_airplane":"🛩","airplane_departure":"🛫","airplane_arriving":"🛬","passenger_ship":"🛳","zipper_mouth_face":"🤐","money_mouth_face":"🤑","face_with_thermometer":"🤒","nerd_face":"🤓","thinking_face":"🤔","face_with_head_bandage":"🤕","robot_face":"🤖","hugging_face":"🤗","the_horns":"🤘","sign_of_the_horns":"🤘","crab":"🦀","lion_face":"🦁","scorpion":"🦂","turkey":"🦃","unicorn_face":"🦄","cheese_wedge":"🧀","hash":"#️⃣","keycap_star":"*⃣","zero":"0️⃣","one":"1️⃣","two":"2️⃣","three":"3️⃣","four":"4️⃣","five":"5️⃣","six":"6️⃣","seven":"7️⃣","eight":"8️⃣","nine":"9️⃣","flag-ac":"🇦🇨","flag-ad":"🇦🇩","flag-ae":"🇦🇪","flag-af":"🇦🇫","flag-ag":"🇦🇬","flag-ai":"🇦🇮","flag-al":"🇦🇱","flag-am":"🇦🇲","flag-ao":"🇦🇴","flag-aq":"🇦🇶","flag-ar":"🇦🇷","flag-as":"🇦🇸","flag-at":"🇦🇹","flag-au":"🇦🇺","flag-aw":"🇦🇼","flag-ax":"🇦🇽","flag-az":"🇦🇿","flag-ba":"🇧🇦","flag-bb":"🇧🇧","flag-bd":"🇧🇩","flag-be":"🇧🇪","flag-bf":"🇧🇫","flag-bg":"🇧🇬","flag-bh":"🇧🇭","flag-bi":"🇧🇮","flag-bj":"🇧🇯","flag-bl":"🇧🇱","flag-bm":"🇧🇲","flag-bn":"🇧🇳","flag-bo":"🇧🇴","flag-bq":"🇧🇶","flag-br":"🇧🇷","flag-bs":"🇧🇸","flag-bt":"🇧🇹","flag-bv":"🇧🇻","flag-bw":"🇧🇼","flag-by":"🇧🇾","flag-bz":"🇧🇿","flag-ca":"🇨🇦","flag-cc":"🇨🇨","flag-cd":"🇨🇩","flag-cf":"🇨🇫","flag-cg":"🇨🇬","flag-ch":"🇨🇭","flag-ci":"🇨🇮","flag-ck":"🇨🇰","flag-cl":"🇨🇱","flag-cm":"🇨🇲","flag-cn":"🇨🇳","cn":"🇨🇳","flag-co":"🇨🇴","flag-cp":"🇨🇵","flag-cr":"🇨🇷","flag-cu":"🇨🇺","flag-cv":"🇨🇻","flag-cw":"🇨🇼","flag-cx":"🇨🇽","flag-cy":"🇨🇾","flag-cz":"🇨🇿","flag-de":"🇩🇪","de":"🇩🇪","flag-dg":"🇩🇬","flag-dj":"🇩🇯","flag-dk":"🇩🇰","flag-dm":"🇩🇲","flag-do":"🇩🇴","flag-dz":"🇩🇿","flag-ea":"🇪🇦","flag-ec":"🇪🇨","flag-ee":"🇪🇪","flag-eg":"🇪🇬","flag-eh":"🇪🇭","flag-er":"🇪🇷","flag-es":"🇪🇸","es":"🇪🇸","flag-et":"🇪🇹","flag-eu":"🇪🇺","flag-fi":"🇫🇮","flag-fj":"🇫🇯","flag-fk":"🇫🇰","flag-fm":"🇫🇲","flag-fo":"🇫🇴","flag-fr":"🇫🇷","fr":"🇫🇷","flag-ga":"🇬🇦","flag-gb":"🇬🇧","gb":"🇬🇧","uk":"🇬🇧","flag-gd":"🇬🇩","flag-ge":"🇬🇪","flag-gf":"🇬🇫","flag-gg":"🇬🇬","flag-gh":"🇬🇭","flag-gi":"🇬🇮","flag-gl":"🇬🇱","flag-gm":"🇬🇲","flag-gn":"🇬🇳","flag-gp":"🇬🇵","flag-gq":"🇬🇶","flag-gr":"🇬🇷","flag-gs":"🇬🇸","flag-gt":"🇬🇹","flag-gu":"🇬🇺","flag-gw":"🇬🇼","flag-gy":"🇬🇾","flag-hk":"🇭🇰","flag-hm":"🇭🇲","flag-hn":"🇭🇳","flag-hr":"🇭🇷","flag-ht":"🇭🇹","flag-hu":"🇭🇺","flag-ic":"🇮🇨","flag-id":"🇮🇩","flag-ie":"🇮🇪","flag-il":"🇮🇱","flag-im":"🇮🇲","flag-in":"🇮🇳","flag-io":"🇮🇴","flag-iq":"🇮🇶","flag-ir":"🇮🇷","flag-is":"🇮🇸","flag-it":"🇮🇹","it":"🇮🇹","flag-je":"🇯🇪","flag-jm":"🇯🇲","flag-jo":"🇯🇴","flag-jp":"🇯🇵","jp":"🇯🇵","flag-ke":"🇰🇪","flag-kg":"🇰🇬","flag-kh":"🇰🇭","flag-ki":"🇰🇮","flag-km":"🇰🇲","flag-kn":"🇰🇳","flag-kp":"🇰🇵","flag-kr":"🇰🇷","kr":"🇰🇷","flag-kw":"🇰🇼","flag-ky":"🇰🇾","flag-kz":"🇰🇿","flag-la":"🇱🇦","flag-lb":"🇱🇧","flag-lc":"🇱🇨","flag-li":"🇱🇮","flag-lk":"🇱🇰","flag-lr":"🇱🇷","flag-ls":"🇱🇸","flag-lt":"🇱🇹","flag-lu":"🇱🇺","flag-lv":"🇱🇻","flag-ly":"🇱🇾","flag-ma":"🇲🇦","flag-mc":"🇲🇨","flag-md":"🇲🇩","flag-me":"🇲🇪","flag-mf":"🇲🇫","flag-mg":"🇲🇬","flag-mh":"🇲🇭","flag-mk":"🇲🇰","flag-ml":"🇲🇱","flag-mm":"🇲🇲","flag-mn":"🇲🇳","flag-mo":"🇲🇴","flag-mp":"🇲🇵","flag-mq":"🇲🇶","flag-mr":"🇲🇷","flag-ms":"🇲🇸","flag-mt":"🇲🇹","flag-mu":"🇲🇺","flag-mv":"🇲🇻","flag-mw":"🇲🇼","flag-mx":"🇲🇽","flag-my":"🇲🇾","flag-mz":"🇲🇿","flag-na":"🇳🇦","flag-nc":"🇳🇨","flag-ne":"🇳🇪","flag-nf":"🇳🇫","flag-ng":"🇳🇬","flag-ni":"🇳🇮","flag-nl":"🇳🇱","flag-no":"🇳🇴","flag-np":"🇳🇵","flag-nr":"🇳🇷","flag-nu":"🇳🇺","flag-nz":"🇳🇿","flag-om":"🇴🇲","flag-pa":"🇵🇦","flag-pe":"🇵🇪","flag-pf":"🇵🇫","flag-pg":"🇵🇬","flag-ph":"🇵🇭","flag-pk":"🇵🇰","flag-pl":"🇵🇱","flag-pm":"🇵🇲","flag-pn":"🇵🇳","flag-pr":"🇵🇷","flag-ps":"🇵🇸","flag-pt":"🇵🇹","flag-pw":"🇵🇼","flag-py":"🇵🇾","flag-qa":"🇶🇦","flag-re":"🇷🇪","flag-ro":"🇷🇴","flag-rs":"🇷🇸","flag-ru":"🇷🇺","ru":"🇷🇺","flag-rw":"🇷🇼","flag-sa":"🇸🇦","flag-sb":"🇸🇧","flag-sc":"🇸🇨","flag-sd":"🇸🇩","flag-se":"🇸🇪","flag-sg":"🇸🇬","flag-sh":"🇸🇭","flag-si":"🇸🇮","flag-sj":"🇸🇯","flag-sk":"🇸🇰","flag-sl":"🇸🇱","flag-sm":"🇸🇲","flag-sn":"🇸🇳","flag-so":"🇸🇴","flag-sr":"🇸🇷","flag-ss":"🇸🇸","flag-st":"🇸🇹","flag-sv":"🇸🇻","flag-sx":"🇸🇽","flag-sy":"🇸🇾","flag-sz":"🇸🇿","flag-ta":"🇹🇦","flag-tc":"🇹🇨","flag-td":"🇹🇩","flag-tf":"🇹🇫","flag-tg":"🇹🇬","flag-th":"🇹🇭","flag-tj":"🇹🇯","flag-tk":"🇹🇰","flag-tl":"🇹🇱","flag-tm":"🇹🇲","flag-tn":"🇹🇳","flag-to":"🇹🇴","flag-tr":"🇹🇷","flag-tt":"🇹🇹","flag-tv":"🇹🇻","flag-tw":"🇹🇼","flag-tz":"🇹🇿","flag-ua":"🇺🇦","flag-ug":"🇺🇬","flag-um":"🇺🇲","flag-us":"🇺🇸","us":"🇺🇸","flag-uy":"🇺🇾","flag-uz":"🇺🇿","flag-va":"🇻🇦","flag-vc":"🇻🇨","flag-ve":"🇻🇪","flag-vg":"🇻🇬","flag-vi":"🇻🇮","flag-vn":"🇻🇳","flag-vu":"🇻🇺","flag-wf":"🇼🇫","flag-ws":"🇼🇸","flag-xk":"🇽🇰","flag-ye":"🇾🇪","flag-yt":"🇾🇹","flag-za":"🇿🇦","flag-zm":"🇿🇲","flag-zw":"🇿🇼","man-man-boy":"👨‍👨‍👦","man-man-boy-boy":"👨‍👨‍👦‍👦","man-man-girl":"👨‍👨‍👧","man-man-girl-boy":"👨‍👨‍👧‍👦","man-man-girl-girl":"👨‍👨‍👧‍👧","man-woman-boy-boy":"👨‍👩‍👦‍👦","man-woman-girl":"👨‍👩‍👧","man-woman-girl-boy":"👨‍👩‍👧‍👦","man-woman-girl-girl":"👨‍👩‍👧‍👧","man-heart-man":"👨‍❤️‍👨","man-kiss-man":"👨‍❤️‍💋‍👨","woman-woman-boy":"👩‍👩‍👦","woman-woman-boy-boy":"👩‍👩‍👦‍👦","woman-woman-girl":"👩‍👩‍👧","woman-woman-girl-boy":"👩‍👩‍👧‍👦","woman-woman-girl-girl":"👩‍👩‍👧‍👧","woman-heart-woman":"👩‍❤️‍👩","woman-kiss-woman":"👩‍❤️‍💋‍👩"} -------------------------------------------------------------------------------- /media/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhirIO/Client/7d8700845762bcbbecbe010e4458e634c5a91361/media/favicon.png -------------------------------------------------------------------------------- /media/w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhirIO/Client/7d8700845762bcbbecbe010e4458e634c5a91361/media/w.png -------------------------------------------------------------------------------- /media/whir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhirIO/Client/7d8700845762bcbbecbe010e4458e634c5a91361/media/whir.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whir.io", 3 | "version": "1.4.0", 4 | "description": "The whir.io chat client. [alpha]", 5 | "author": { 6 | "name": "Stefan Aichholzer", 7 | "email": "play@analogbird.com", 8 | "url": "https://github.com/aichholzer" 9 | }, 10 | "contributors": [], 11 | "maintainers": [], 12 | "keywords": [ 13 | "chat", 14 | "terminal", 15 | "command", 16 | "interface", 17 | "cli", 18 | "messenger" 19 | ], 20 | "homepage": "http://whir.io", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/WhirIO/Client" 24 | }, 25 | "license": "MIT", 26 | "engines": { 27 | "node": "8.9.4" 28 | }, 29 | "dependencies": { 30 | "blessed": "^0.1.81", 31 | "bufferutil": "^3.0.4", 32 | "chalk": "^2.4.1", 33 | "cli-spinner": "^0.2.8", 34 | "moment": "^2.22.1", 35 | "utf-8-validate": "^4.0.1", 36 | "ws": "^5.1.1", 37 | "yargs": "^11.0.0" 38 | }, 39 | "devDependencies": { 40 | "dotenv": "^5.0.1", 41 | "eslint": "^4.18.0", 42 | "eslint-config-airbnb-base": "^12.1.0", 43 | "eslint-config-prettier": "^2.9.0", 44 | "eslint-plugin-import": "^2.11.0", 45 | "eslint-plugin-prettier": "^2.6.0", 46 | "mocha": "^5.1.1", 47 | "prettier": "^1.12.1", 48 | "should": "^13.2.1" 49 | }, 50 | "main": "./app/index.js", 51 | "directories": { 52 | "test": "test" 53 | }, 54 | "scripts": { 55 | "eslint": "eslint --quiet .", 56 | "eslint:fix": "eslint --quiet --fix .", 57 | "test": "mocha -R spec -t 5000" 58 | }, 59 | "bugs": { 60 | "url": "https://github.com/WhirIO/Client/issues" 61 | }, 62 | "_from": "whir.io@^1.3.6", 63 | "_npmUser": { 64 | "name": "aichholzer", 65 | "email": "theaichholzer@gmail.com" 66 | }, 67 | "preferGlobal": true, 68 | "bin": "./app/index.js" 69 | } 70 | -------------------------------------------------------------------------------- /store/.store: -------------------------------------------------------------------------------- 1 | This folder should exist. 😬 2 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | -------------------------------------------------------------------------------- /w.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### 4 | # This is for development & testing purposes only. 5 | # The channel defaults to "whir" 6 | # Runs against localhost:9000, provided this host and port are up. 7 | # Install the server locally: https://github.com/WhirIO/Server 8 | ### 9 | 10 | USER=$1 11 | CHANNEL='-c whir' 12 | UNSECURE='UNSECURE_SOCKET=true' 13 | 14 | if [ "$USER" == "" ]; then 15 | echo 16 | echo " 💥 You need a user." 17 | exit 1 18 | fi 19 | 20 | if [ "$2" != "" ]; then 21 | CHANNEL="-c $2" 22 | fi 23 | if [ "$2" == "rand" ]; then 24 | CHANNEL="" 25 | fi 26 | 27 | if [ "$3" == "false" ]; then 28 | UNSECURE='' 29 | fi 30 | 31 | clear 32 | eval "$UNSECURE node app/ -h localhost:9000 -u $USER $CHANNEL" 33 | --------------------------------------------------------------------------------