├── .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 |
7 |
8 |
9 |
10 | [](https://github.com/WhirIO/Client)
11 | [](https://github.com/WhirIO/Client)
12 | [](https://codebeat.co/projects/github-com-whirio-client-master)
13 | [](https://www.codacy.com/app/aichholzer/Client?utm_source=github.com&utm_medium=referral&utm_content=WhirIO/Client&utm_campaign=Badge_Grade)
14 | [](https://www.npmjs.com/package/whir.io)
15 | [](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 |
--------------------------------------------------------------------------------