mirror of
https://github.com/darkwire/darkwire.io.git
synced 2025-07-20 20:17:38 +00:00
Merge pull request #7 from seripap/feature/fileTransfer
Abstraction of darkwire, file transfer, slash commands
This commit is contained in:
commit
b3befa1247
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
src/public/main.js
|
src/public/main.js
|
||||||
|
@ -20,6 +20,6 @@ before_script:
|
|||||||
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16"
|
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16"
|
||||||
- sleep 5 # give xvfb some time to start
|
- sleep 5 # give xvfb some time to start
|
||||||
- gulp bundle
|
- gulp bundle
|
||||||
- node index.js &
|
- npm start &
|
||||||
- sleep 5
|
- sleep 5
|
||||||
script: node_modules/mocha/bin/mocha test/unit --compilers js:babel-core/register && node_modules/nightwatch/bin/nightwatch --test test/acceptance/index.js --config test/acceptance/nightwatch.json -e chrome
|
script: npm run test-travis
|
||||||
|
@ -21,6 +21,17 @@ gulp.task('bundle', function() {
|
|||||||
.pipe(gulp.dest('src/public'));
|
.pipe(gulp.dest('src/public'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task('dev', function() {
|
||||||
|
return browserify('src/js/main.js', {
|
||||||
|
debug: true
|
||||||
|
}).transform(babel.configure({
|
||||||
|
presets: ['es2015']
|
||||||
|
})).bundle()
|
||||||
|
.pipe(source('main.js'))
|
||||||
|
.pipe(buffer())
|
||||||
|
.pipe(gulp.dest('src/public'));
|
||||||
|
});
|
||||||
|
|
||||||
gulp.task('start', function() {
|
gulp.task('start', function() {
|
||||||
nodemon({
|
nodemon({
|
||||||
script: 'index.js',
|
script: 'index.js',
|
||||||
@ -29,7 +40,7 @@ gulp.task('start', function() {
|
|||||||
env: {
|
env: {
|
||||||
'NODE_ENV': 'development'
|
'NODE_ENV': 'development'
|
||||||
},
|
},
|
||||||
tasks: ['bundle']
|
tasks: ['dev']
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -51,7 +62,7 @@ gulp.task('test', function() {
|
|||||||
|
|
||||||
let acceptanceTest = spawn(
|
let acceptanceTest = spawn(
|
||||||
'node_modules/nightwatch/bin/nightwatch',
|
'node_modules/nightwatch/bin/nightwatch',
|
||||||
['--test', 'test/acceptance/index.js', '--config', 'test/acceptance/nightwatch.json'],
|
['--test', 'test/acceptance/index.js', '--config', 'test/acceptance/nightwatch-local.json'],
|
||||||
{stdio: 'inherit'}
|
{stdio: 'inherit'}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
11
package.json
11
package.json
@ -12,9 +12,13 @@
|
|||||||
"forever": "^0.15.1",
|
"forever": "^0.15.1",
|
||||||
"gulp": "^3.9.0",
|
"gulp": "^3.9.0",
|
||||||
"gulp-uglify": "^1.5.1",
|
"gulp-uglify": "^1.5.1",
|
||||||
|
"he": "^0.5.0",
|
||||||
|
"moment": "^2.11.2",
|
||||||
"mustache-express": "^1.2.2",
|
"mustache-express": "^1.2.2",
|
||||||
|
"sanitize-html": "^1.11.3",
|
||||||
"serve-favicon": "^2.3.0",
|
"serve-favicon": "^2.3.0",
|
||||||
"shortid": "^2.2.4",
|
"shortid": "^2.2.4",
|
||||||
|
"slug": "^0.9.1",
|
||||||
"socket.io": "^1.4.0",
|
"socket.io": "^1.4.0",
|
||||||
"underscore": "^1.8.3",
|
"underscore": "^1.8.3",
|
||||||
"uuid": "^2.0.1"
|
"uuid": "^2.0.1"
|
||||||
@ -36,8 +40,11 @@
|
|||||||
"vinyl-source-stream": "^1.1.0"
|
"vinyl-source-stream": "^1.1.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "gulp start",
|
"start": "npm run bundle && node index.js",
|
||||||
"test": "gulp test"
|
"dev": "npm run bundle && gulp start",
|
||||||
|
"bundle": "gulp bundle",
|
||||||
|
"test": "npm run bundle && gulp test",
|
||||||
|
"test-travis": "node_modules/mocha/bin/mocha test/unit --compilers js:babel-core/register && node_modules/nightwatch/bin/nightwatch --test test/acceptance/index.js --config test/acceptance/nightwatch.json -e chrome"
|
||||||
},
|
},
|
||||||
"author": "Daniel Seripap",
|
"author": "Daniel Seripap",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
27
readme.md
27
readme.md
@ -1,31 +1,30 @@
|
|||||||
# Darkwire.io
|
# Darkwire.io
|
||||||
|
|
||||||
|
[](https://travis-ci.org/seripap/darkwire.io) []()
|
||||||
|
|
||||||
Simple encrypted web chat. Powered by [socket.io](http://socket.io) and the [web cryptography API](https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto).
|
Simple encrypted web chat. Powered by [socket.io](http://socket.io) and the [web cryptography API](https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto).
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
# Get latest version of NodeJS for ES2015 support
|
||||||
# For es6 compatability, be sure you have the latest stable version of Node JS installed
|
|
||||||
npm install -g n
|
npm install -g n
|
||||||
n stable
|
n stable
|
||||||
|
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Bundle JS files
|
# Starting dev environment
|
||||||
npm run bundle
|
npm run dev
|
||||||
|
|
||||||
|
# Running tests locally
|
||||||
|
brew install chromedriver
|
||||||
|
npm test
|
||||||
|
|
||||||
# Start a local instance of darkwire
|
# Start a local instance of darkwire
|
||||||
npm start
|
npm start
|
||||||
|
|
||||||
Create a **.secret** file in **/src** folder with a your session secret. It doesn't matter what it is- just keep it private.
|
# Changing ports, default is 3000
|
||||||
|
port=3000 npm start
|
||||||
|
|
||||||
Darkwire is now running on `http://localhost:3000`
|
Darkwire is now running on `http://localhost:3000`
|
||||||
|
|
||||||
### Deployment
|
|
||||||
|
|
||||||
Build source
|
|
||||||
|
|
||||||
gulp bundle
|
|
||||||
|
|
||||||
### How it works
|
### How it works
|
||||||
|
|
||||||
Darkwire uses a combination of asymmetric encryption (RSA-OAEP), symmetric session keys (AES-CBC) and signing keys (HMAC) for security.
|
Darkwire uses a combination of asymmetric encryption (RSA-OAEP), symmetric session keys (AES-CBC) and signing keys (HMAC) for security.
|
||||||
@ -48,11 +47,11 @@ Darkwire does not provide any guarantee that the person you're communicating wit
|
|||||||
|
|
||||||
## File Transfer
|
## File Transfer
|
||||||
|
|
||||||
Files are not transferred over the wire-only the file name and extension. Darkwire encodes documents into base64 using [btoa](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa) and is encrypted the same way chat messages are.
|
Darkwire encodes documents (up to 1MB) into base64 using [btoa](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa) and is encrypted the same way chat messages are.
|
||||||
|
|
||||||
1. When a file is "uploaded", the document is encoded on the client and the server recieves the encrypted base64 string.
|
1. When a file is "uploaded", the document is encoded on the client and the server recieves the encrypted base64 string.
|
||||||
2. The server sends the encrypted base64 string to clients in the same chat room.
|
2. The server sends the encrypted base64 string to clients in the same chat room.
|
||||||
3. Clients recieving the encrypted base64 string then decrypts the string, then decodes the base64 string using [atob](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/atob).
|
3. Clients recieving the encrypted base64 string then decrypts and decodes the base64 string using [atob](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/atob).
|
||||||
|
|
||||||
## Sockets & Server
|
## Sockets & Server
|
||||||
|
|
||||||
|
@ -50,7 +50,13 @@ app.get('/:roomId', (req, res) => {
|
|||||||
let roomExists = _.findWhere(rooms, {_id: roomId}) || false;
|
let roomExists = _.findWhere(rooms, {_id: roomId}) || false;
|
||||||
|
|
||||||
if (roomExists) {
|
if (roomExists) {
|
||||||
return res.render('index', {username: shortid.generate()});
|
return res.render('index', {
|
||||||
|
APP: {
|
||||||
|
version: process.env.npm_package_version,
|
||||||
|
ip: req.headers['x-forwarded-for']
|
||||||
|
},
|
||||||
|
username: shortid.generate()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
|
345
src/js/chat.js
Normal file
345
src/js/chat.js
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
import _ from 'underscore';
|
||||||
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
import he from 'he';
|
||||||
|
|
||||||
|
export default class Chat {
|
||||||
|
constructor(darkwire, socket) {
|
||||||
|
this.usernamesInMemory = [];
|
||||||
|
this.FADE_TIME = 150; // ms
|
||||||
|
this.TYPING_TIMER_LENGTH = 400; // ms
|
||||||
|
this.typing = false;
|
||||||
|
this.lastTypingTime = null;
|
||||||
|
this.darkwire = darkwire;
|
||||||
|
this.socket = socket;
|
||||||
|
this.messages = $('.messages'); // Messages area
|
||||||
|
this.inputMessage = $('.inputMessage'); // Input message input box
|
||||||
|
this.chatPage = $('.chat.page');
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log a message
|
||||||
|
log(message, options) {
|
||||||
|
let html = options && options.html === true || false;
|
||||||
|
let $el;
|
||||||
|
|
||||||
|
let matchedUsernames = this.checkIfUsername(message.split(' '));
|
||||||
|
|
||||||
|
if (matchedUsernames.length > 0) {
|
||||||
|
for (let i = 0; i < matchedUsernames.length; i++) {
|
||||||
|
let usernameContainer = $('<span/>')
|
||||||
|
.text(matchedUsernames[i])
|
||||||
|
.css('color', this.getUsernameColor(matchedUsernames[i]));
|
||||||
|
message = message.replace(matchedUsernames[i], usernameContainer.prop('outerHTML'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options && options.error) {
|
||||||
|
$el = $('<li class="log-error">').addClass('log').html(message);
|
||||||
|
} else if (options && options.info) {
|
||||||
|
$el = $('<li class="log-info">').addClass('log').html(message);
|
||||||
|
} else {
|
||||||
|
$el = $('<li>').addClass('log').html(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addMessageElement($el, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIfUsername(words) {
|
||||||
|
let matchedUsernames = [];
|
||||||
|
this.darkwire.users.forEach((user) => {
|
||||||
|
let usernameMatch = new RegExp('^' + user.username + '$', 'g');
|
||||||
|
for (let i = 0; i < words.length; i++) {
|
||||||
|
let exactMatch = words[i].match(usernameMatch) || false;
|
||||||
|
let usernameInMemory = this.usernamesInMemory.indexOf(words[i]) > -1;
|
||||||
|
|
||||||
|
if (exactMatch && exactMatch.length > -1 || usernameInMemory) {
|
||||||
|
if (!usernameInMemory) {
|
||||||
|
this.usernamesInMemory.push(words[i]);
|
||||||
|
}
|
||||||
|
matchedUsernames.push(words[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return matchedUsernames;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the color of a username through our hash function
|
||||||
|
getUsernameColor(username) {
|
||||||
|
const COLORS = [
|
||||||
|
'#e21400', '#ffe400', '#ff8f00',
|
||||||
|
'#58dc00', '#dd9cff', '#4ae8c4',
|
||||||
|
'#3b88eb', '#f47777', '#d300e7',
|
||||||
|
'#99FF33', '#99CC33', '#999933',
|
||||||
|
'#996633', '#993333', '#990033',
|
||||||
|
];
|
||||||
|
// Compute hash code
|
||||||
|
let hash = 7;
|
||||||
|
for (let i = 0; i < username.length; i++) {
|
||||||
|
hash = username.charCodeAt(i) + (hash << 5) - hash;
|
||||||
|
}
|
||||||
|
// Calculate color
|
||||||
|
let index = Math.abs(hash % COLORS.length);
|
||||||
|
return COLORS[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
var _this = this;
|
||||||
|
// Select message input when clicking message body, unless selecting text
|
||||||
|
this.messages.on('click', () => {
|
||||||
|
if (!this.getSelectedText()) {
|
||||||
|
this.inputMessage.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.inputMessage.on('input propertychange paste change', function() {
|
||||||
|
_this.updateTyping();
|
||||||
|
let message = $(this).val().trim();
|
||||||
|
if (message.length) {
|
||||||
|
$('#send-message-btn').addClass('active');
|
||||||
|
} else {
|
||||||
|
$('#send-message-btn').removeClass('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the typing event
|
||||||
|
updateTyping() {
|
||||||
|
if (this.darkwire.connected) {
|
||||||
|
if (!this.typing) {
|
||||||
|
this.typing = true;
|
||||||
|
this.socket.emit('typing');
|
||||||
|
}
|
||||||
|
this.lastTypingTime = (new Date()).getTime();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
let typingTimer = (new Date()).getTime();
|
||||||
|
let timeDiff = typingTimer - this.lastTypingTime;
|
||||||
|
if (timeDiff >= this.TYPING_TIMER_LENGTH && this.typing) {
|
||||||
|
this.socket.emit('stop typing');
|
||||||
|
this.typing = false;
|
||||||
|
}
|
||||||
|
}, this.TYPING_TIMER_LENGTH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addChatTyping(data) {
|
||||||
|
data.typing = true;
|
||||||
|
data.message = 'is typing';
|
||||||
|
this.addChatMessage(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedText() {
|
||||||
|
let text = '';
|
||||||
|
if (typeof window.getSelection != 'undefined') {
|
||||||
|
text = window.getSelection().toString();
|
||||||
|
} else if (typeof document.selection != 'undefined' && document.selection.type == 'Text') {
|
||||||
|
text = document.selection.createRange().text;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTypingMessages(data) {
|
||||||
|
return $('.typing.message').filter(function(i) {
|
||||||
|
return $(this).data('username') === data.username;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeChatTyping(data) {
|
||||||
|
this.getTypingMessages(data).fadeOut(function() {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
slashCommands(trigger) {
|
||||||
|
let validCommands = [];
|
||||||
|
let expectedParams = 0;
|
||||||
|
const triggerCommands = [{
|
||||||
|
command: 'nick',
|
||||||
|
description: 'Changes nickname.',
|
||||||
|
paramaters: ['{username}'],
|
||||||
|
multiple: false,
|
||||||
|
usage: '/nick {username}',
|
||||||
|
action: () => {
|
||||||
|
let newUsername = trigger.params[0] || false;
|
||||||
|
|
||||||
|
if (newUsername > 16) {
|
||||||
|
return this.log('Username cannot be greater than 16 characters.', {error: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove things that arent digits or chars
|
||||||
|
newUsername = newUsername.replace(/[^A-Za-z0-9]/g, '-');
|
||||||
|
|
||||||
|
if (!newUsername.match(/^[A-Z0-9]/i)) {
|
||||||
|
return this.log('Username must start with a letter or number.', {error: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.darkwire.updateUsername(window.username, newUsername).then((socketData) => {
|
||||||
|
let modifiedSocketData = {
|
||||||
|
username: window.username,
|
||||||
|
newUsername: socketData.username
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.emit('update user', modifiedSocketData);
|
||||||
|
window.username = username = socketData.username;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
command: 'help',
|
||||||
|
description: 'Shows a list of commands.',
|
||||||
|
paramaters: [],
|
||||||
|
multiple: false,
|
||||||
|
usage: '/help',
|
||||||
|
action: () => {
|
||||||
|
validCommands = validCommands.map((command) => {
|
||||||
|
return '/' + command;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.log('Valid commands: ' + validCommands.sort().join(', '), {info: true});
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
command: 'me',
|
||||||
|
description: 'Invoke virtual action',
|
||||||
|
paramaters: ['{action}'],
|
||||||
|
multiple: true,
|
||||||
|
usage: '/me {action}',
|
||||||
|
action: () => {
|
||||||
|
|
||||||
|
expectedParams = 100;
|
||||||
|
|
||||||
|
let actionMessage = trigger.params.join(' ');
|
||||||
|
|
||||||
|
this.darkwire.encodeMessage(actionMessage, 'action').then((socketData) => {
|
||||||
|
this.addChatMessage({
|
||||||
|
username: username,
|
||||||
|
message: actionMessage,
|
||||||
|
messageType: 'action'
|
||||||
|
});
|
||||||
|
this.socket.emit('new message', socketData);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
|
const color = () => {
|
||||||
|
const hexTex = new RegExp(/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i);
|
||||||
|
};
|
||||||
|
|
||||||
|
triggerCommands.forEach((command) => {
|
||||||
|
validCommands.push(command.command);
|
||||||
|
});
|
||||||
|
|
||||||
|
let commandToTrigger = _.findWhere(triggerCommands, {command: trigger.command});
|
||||||
|
|
||||||
|
if (commandToTrigger) {
|
||||||
|
expectedParams = commandToTrigger.paramaters.length;
|
||||||
|
if (expectedParams && trigger.params.length > expectedParams || expectedParams && trigger.params.length < expectedParams) {
|
||||||
|
if (!commandToTrigger.multple && trigger.params.length < 1) {
|
||||||
|
return this.log('Missing or too many paramater. Usage: ' + commandToTrigger.usage, {error: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commandToTrigger.action.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(trigger.command + ' is not a valid command. Type /help for a list of valid commands.', {error: true});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
executeCommand(trigger) {
|
||||||
|
trigger = trigger || false;
|
||||||
|
if (trigger) {
|
||||||
|
let command = trigger.command;
|
||||||
|
this.slashCommands(trigger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseCommand(cleanedMessage) {
|
||||||
|
let trigger = {
|
||||||
|
command: null,
|
||||||
|
params: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cleanedMessage.indexOf('/') === 0) {
|
||||||
|
this.inputMessage.val('');
|
||||||
|
let parsedCommand = cleanedMessage.replace('/', '').split(' ');
|
||||||
|
trigger.command = sanitizeHtml(parsedCommand[0]) || null;
|
||||||
|
// Get params
|
||||||
|
if (parsedCommand.length >= 2) {
|
||||||
|
for (let i = 1; i < parsedCommand.length; i++) {
|
||||||
|
trigger.params.push(parsedCommand[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
addChatMessage(data, options) {
|
||||||
|
if (!data.message.trim().length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let messageType = data.messageType || 'text';
|
||||||
|
|
||||||
|
// Don't fade the message in if there is an 'X was typing'
|
||||||
|
let $typingMessages = this.getTypingMessages(data);
|
||||||
|
options = options || {};
|
||||||
|
if ($typingMessages.length !== 0) {
|
||||||
|
options.fade = false;
|
||||||
|
$typingMessages.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
let $usernameDiv = $('<span class="username"/>')
|
||||||
|
.text(data.username)
|
||||||
|
.css('color', this.getUsernameColor(data.username));
|
||||||
|
|
||||||
|
let $messageBodyDiv = $('<span class="messageBody">');
|
||||||
|
|
||||||
|
if (messageType === 'text' || messageType === 'action') {
|
||||||
|
if (messageType === 'action') {
|
||||||
|
$usernameDiv.css('color','').prepend('*');
|
||||||
|
}
|
||||||
|
let unescapedMessage = unescape(data.message);
|
||||||
|
let lineBreaks = /<br \/>/g;
|
||||||
|
unescapedMessage = unescapedMessage.replace(lineBreaks, '<br />');
|
||||||
|
$messageBodyDiv.html(unescapedMessage);
|
||||||
|
} else {
|
||||||
|
$messageBodyDiv.html(this.darkwire.addFileToQueue(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
let typingClass = data.typing ? 'typing' : '';
|
||||||
|
let actionClass = data.messageType === 'action' ? 'action' : '';
|
||||||
|
|
||||||
|
let $messageDiv = $('<li class="message"/>')
|
||||||
|
.data('username', data.username)
|
||||||
|
.addClass(typingClass)
|
||||||
|
.addClass(actionClass)
|
||||||
|
.append($usernameDiv, $messageBodyDiv);
|
||||||
|
|
||||||
|
this.addMessageElement($messageDiv, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessageElement(el, options) {
|
||||||
|
let $el = $(el);
|
||||||
|
|
||||||
|
if (!options) {
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
$el.hide().fadeIn(this.FADE_TIME);
|
||||||
|
this.messages.append($el);
|
||||||
|
|
||||||
|
this.messages[0].scrollTop = this.messages[0].scrollHeight; // minus 60 for key
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceMessage(id, message) {
|
||||||
|
let container = $(id);
|
||||||
|
if (container) {
|
||||||
|
container.html(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -9,9 +9,20 @@ export default class Darkwire {
|
|||||||
this._myUserId = false;
|
this._myUserId = false;
|
||||||
this._connected = false;
|
this._connected = false;
|
||||||
this._users = [];
|
this._users = [];
|
||||||
|
this._fileQueue = [];
|
||||||
this._keys = {};
|
this._keys = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFile(id) {
|
||||||
|
let file = _.findWhere(this._fileQueue, {id: id}) || false;
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
// TODO: Destroy object from memory when retrieved
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.data;
|
||||||
|
}
|
||||||
|
|
||||||
get keys() {
|
get keys() {
|
||||||
return this._keys;
|
return this._keys;
|
||||||
}
|
}
|
||||||
@ -30,12 +41,35 @@ export default class Darkwire {
|
|||||||
return this._connected;
|
return this._connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get audio() {
|
||||||
|
return this._audio;
|
||||||
|
}
|
||||||
|
|
||||||
get users() {
|
get users() {
|
||||||
return this._users;
|
return this._users;
|
||||||
}
|
}
|
||||||
|
|
||||||
get audio() {
|
getUserById(id) {
|
||||||
return this._audio;
|
return _.findWhere(this._users, {id: id});
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserByName(username) {
|
||||||
|
return _.findWhere(this._users, {username: username});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser(data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let user = this.getUserById(data.id);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
let oldUsername = user.username;
|
||||||
|
|
||||||
|
user.username = data.username;
|
||||||
|
resolve(oldUsername);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addUser(data) {
|
addUser(data) {
|
||||||
@ -76,14 +110,48 @@ export default class Darkwire {
|
|||||||
return this._users;
|
return this._users;
|
||||||
}
|
}
|
||||||
|
|
||||||
encodeMessage(message, messageType) {
|
updateUsername(username, newUsername) {
|
||||||
|
let user = null;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (newUsername) {
|
||||||
|
user = this.getUserByName(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username) {
|
||||||
|
if (!user) {
|
||||||
|
Promise.all([
|
||||||
|
this._cryptoUtil.createPrimaryKeys()
|
||||||
|
])
|
||||||
|
.then((data) => {
|
||||||
|
this._keys = {
|
||||||
|
public: data[0].publicKey,
|
||||||
|
private: data[0].privateKey
|
||||||
|
};
|
||||||
|
return Promise.all([
|
||||||
|
this._cryptoUtil.exportKey(data[0].publicKey, 'spki')
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.then((exportedKeys) => {
|
||||||
|
resolve({
|
||||||
|
username: username,
|
||||||
|
publicKey: exportedKeys[0]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
username: newUsername,
|
||||||
|
publicKey: user.publicKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeMessage(message, messageType, additionalData) {
|
||||||
// Don't send unless other users exist
|
// Don't send unless other users exist
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// if (this._users.length <= 1) {
|
additionalData = additionalData || {};
|
||||||
// console.log('rejected:' + this._users);
|
|
||||||
// reject();
|
|
||||||
// return;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if there is a non-empty message and a socket connection
|
// if there is a non-empty message and a socket connection
|
||||||
if (message && this._connected) {
|
if (message && this._connected) {
|
||||||
@ -95,7 +163,12 @@ export default class Darkwire {
|
|||||||
let signature = null;
|
let signature = null;
|
||||||
let signingKey = null;
|
let signingKey = null;
|
||||||
let encryptedMessageData = null;
|
let encryptedMessageData = null;
|
||||||
|
let messageToEncode = {
|
||||||
|
text: escape(message),
|
||||||
|
additionalData: additionalData
|
||||||
|
};
|
||||||
|
|
||||||
|
messageToEncode = JSON.stringify(messageToEncode);
|
||||||
// Generate new secret key and vector for each message
|
// Generate new secret key and vector for each message
|
||||||
this._cryptoUtil.createSecretKey()
|
this._cryptoUtil.createSecretKey()
|
||||||
.then((key) => {
|
.then((key) => {
|
||||||
@ -146,7 +219,7 @@ export default class Darkwire {
|
|||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
secretKeys = data;
|
secretKeys = data;
|
||||||
messageData = this._cryptoUtil.convertStringToArrayBufferView(message);
|
messageData = this._cryptoUtil.convertStringToArrayBufferView(messageToEncode);
|
||||||
return this._cryptoUtil.signKey(messageData, signingKey);
|
return this._cryptoUtil.signKey(messageData, signingKey);
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@ -202,7 +275,7 @@ export default class Darkwire {
|
|||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
decryptedMessageData = data;
|
decryptedMessageData = data;
|
||||||
decryptedMessage = this._cryptoUtil.convertArrayBufferViewToString(new Uint8Array(data));
|
decryptedMessage = JSON.parse(this._cryptoUtil.convertArrayBufferViewToString(new Uint8Array(data)));
|
||||||
return this._cryptoUtil.decryptSigningKey(signingKeyArrayBuffer, this._keys.private);
|
return this._cryptoUtil.decryptSigningKey(signingKeyArrayBuffer, this._keys.private);
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@ -217,10 +290,29 @@ export default class Darkwire {
|
|||||||
resolve({
|
resolve({
|
||||||
username: username,
|
username: username,
|
||||||
message: decryptedMessage,
|
message: decryptedMessage,
|
||||||
messageType: data.messageType
|
messageType: data.messageType,
|
||||||
|
timestamp: data.timestamp
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateMessage(fileId, fileName, messageType) {
|
||||||
|
let message = '<div id="file-transfer-request-' + fileId + '">is attempting to send you ' + fileName + ' (' + messageType + ')';
|
||||||
|
message += '<br><small class="file-disclaimer"><strong>WARNING: We cannot strictly verify the integrity of this file, its recipients or its owners. By accepting this file, you are liable for any risks that may arise from reciving this file.</strong></small>';
|
||||||
|
message += '<br><a onclick="triggerFileDownload(this);" data-file="' + fileId + '">Accept File</a></div>';
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFileToQueue(data) {
|
||||||
|
let fileData = {
|
||||||
|
id: data.additionalData.fileId,
|
||||||
|
data: data
|
||||||
|
};
|
||||||
|
this._fileQueue.push(fileData);
|
||||||
|
return this.generateMessage(data.additionalData.fileId, data.additionalData.fileName, data.messageType);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
|
import _ from 'underscore';
|
||||||
|
import uuid from 'uuid';
|
||||||
|
|
||||||
export default class FileHandler {
|
export default class FileHandler {
|
||||||
constructor(darkwire, socket) {
|
constructor(darkwire, socket, chat) {
|
||||||
if (window.File && window.FileReader && window.FileList && window.Blob && window.btoa && window.atob) {
|
this.localFileQueue = [];
|
||||||
|
if (window.File && window.FileReader && window.FileList && window.Blob && window.btoa && window.atob && window.Blob && window.URL) {
|
||||||
this._isSupported = true;
|
this._isSupported = true;
|
||||||
this.darkwire = darkwire;
|
this.darkwire = darkwire;
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
|
this.chat = chat;
|
||||||
this.listen();
|
this.listen();
|
||||||
} else {
|
} else {
|
||||||
this._isSupported = false;
|
this._isSupported = false;
|
||||||
@ -14,16 +19,22 @@ export default class FileHandler {
|
|||||||
return this._isSupported;
|
return this._isSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
encodeFile(event) {
|
set isSupported(state) {
|
||||||
|
this._isSupported = state;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmTransfer(event) {
|
||||||
|
const validFileTypes = ['png','jpg','jpeg','gif','zip','rar','gzip','pdf','txt','json','doc','docx'];
|
||||||
const file = event.target.files && event.target.files[0];
|
const file = event.target.files && event.target.files[0];
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
|
const fileExt = file.name.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
// let encodedFile = {
|
if (validFileTypes.indexOf(fileExt) <= -1) {
|
||||||
// fileName: file.name,
|
alert('file type not supported');
|
||||||
// fileSize: file.fileSize,
|
return false;
|
||||||
// base64: null
|
}
|
||||||
// };
|
|
||||||
|
|
||||||
// Support for only 1MB
|
// Support for only 1MB
|
||||||
if (file.size > 1000000) {
|
if (file.size > 1000000) {
|
||||||
@ -31,32 +42,98 @@ export default class FileHandler {
|
|||||||
alert('Max filesize is 1MB.');
|
alert('Max filesize is 1MB.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
let fileId = uuid.v4();
|
||||||
|
|
||||||
const reader = new FileReader();
|
let confirmMessage = '<span id="transfer-' + fileId + '">You are about to send ' + file.name + ' to all parties in this chat. <a onclick="triggerFileTransfer(this);" data-file="' + fileId + '">Confirm</a> | <a onclick="triggerFileDestroy(this)" data-file="' + fileId + '">Cancel</a></span>';
|
||||||
|
let fileData = {
|
||||||
reader.onload = (readerEvent) => {
|
id: fileId,
|
||||||
const base64 = window.btoa(readerEvent.target.result);
|
file: file
|
||||||
this.darkwire.encodeMessage(base64, 'file').then((socketData) => {
|
|
||||||
this.socket.emit('new message', socketData);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
this.localFileQueue.push(fileData);
|
||||||
|
this.chat.addChatMessage({
|
||||||
|
username: username,
|
||||||
|
message: confirmMessage
|
||||||
|
});
|
||||||
|
this.filesSent++;
|
||||||
|
|
||||||
reader.readAsBinaryString(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeFile(base64) {
|
encodeFile(fileId) {
|
||||||
return window.atob(base64);
|
const fileData = _.findWhere(this.localFileQueue, {id: fileId});
|
||||||
|
const file = fileData.file || false;
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
// TODO: Remove file from local queue
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
const fileType = file.type || 'file';
|
||||||
|
|
||||||
|
reader.onload = (readerEvent) => {
|
||||||
|
const base64 = window.btoa(readerEvent.target.result);
|
||||||
|
const additionalData = {
|
||||||
|
fileId: fileId,
|
||||||
|
fileName: file.name
|
||||||
|
};
|
||||||
|
this.darkwire.encodeMessage(base64, fileType, additionalData).then((socketData) => {
|
||||||
|
this.chat.replaceMessage('#transfer-' + fileId, 'Sent ' + file.name);
|
||||||
|
this.socket.emit('new message', socketData);
|
||||||
|
});
|
||||||
|
this.resetInput();
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsBinaryString(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyFile(fileId) {
|
||||||
|
const file = _.findWhere(this.localFileQueue, {id: fileId});
|
||||||
|
this.localFileQueue = _.without(this.localFileQueue, file);
|
||||||
|
return this.chat.replaceMessage('#transfer-' + fileId, 'The file transfer for ' + file.file.name + ' has been canceled.');
|
||||||
|
}
|
||||||
|
|
||||||
|
createBlob(base64, fileType) {
|
||||||
|
base64 = unescape(base64);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const sliceSize = 1024;
|
||||||
|
let byteCharacters = window.atob(base64);
|
||||||
|
let byteArrays = [];
|
||||||
|
|
||||||
|
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||||
|
let slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||||
|
|
||||||
|
let byteNumbers = new Array(slice.length);
|
||||||
|
for (let i = 0; i < slice.length; i++) {
|
||||||
|
byteNumbers[i] = slice.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
let byteArray = new Uint8Array(byteNumbers);
|
||||||
|
|
||||||
|
byteArrays.push(byteArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(new window.Blob(byteArrays, {type: fileType}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createUrlFromBlob(blob) {
|
||||||
|
return window.URL.createObjectURL(blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
listen() {
|
listen() {
|
||||||
// browser API
|
// browser API
|
||||||
document.getElementById('fileInput').addEventListener('change', this.encodeFile.bind(this), false);
|
document.getElementById('fileInput').addEventListener('change', this.confirmTransfer.bind(this), false);
|
||||||
|
|
||||||
// darkwire
|
// darkwire
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetInput() {
|
||||||
|
document.getElementById('fileInput').value = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
345
src/js/main.js
345
src/js/main.js
@ -1,6 +1,11 @@
|
|||||||
|
import _ from 'underscore';
|
||||||
import Darkwire from './darkwire';
|
import Darkwire from './darkwire';
|
||||||
import WindowHandler from './window';
|
import WindowHandler from './window';
|
||||||
import CryptoUtil from './crypto';
|
import CryptoUtil from './crypto';
|
||||||
|
import Chat from './chat';
|
||||||
|
import moment from 'moment';
|
||||||
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
import he from 'he';
|
||||||
|
|
||||||
let fs = window.RequestFileSystem || window.webkitRequestFileSystem;
|
let fs = window.RequestFileSystem || window.webkitRequestFileSystem;
|
||||||
|
|
||||||
@ -8,28 +13,9 @@ $(function() {
|
|||||||
const darkwire = new Darkwire();
|
const darkwire = new Darkwire();
|
||||||
const cryptoUtil = new CryptoUtil();
|
const cryptoUtil = new CryptoUtil();
|
||||||
|
|
||||||
let FADE_TIME = 150; // ms
|
|
||||||
let TYPING_TIMER_LENGTH = 400; // ms
|
|
||||||
|
|
||||||
let COLORS = [
|
|
||||||
'#e21400', '#ffe400', '#ff8f00',
|
|
||||||
'#58dc00', '#dd9cff', '#4ae8c4',
|
|
||||||
'#3b88eb', '#f47777', '#d300e7',
|
|
||||||
];
|
|
||||||
|
|
||||||
let $window = $(window);
|
let $window = $(window);
|
||||||
let $messages = $('.messages'); // Messages area
|
|
||||||
let $inputMessage = $('.inputMessage'); // Input message input box
|
|
||||||
let $key = $('.key');
|
|
||||||
let $genKey = $('.new_key');
|
|
||||||
let $participants = $('#participants');
|
let $participants = $('#participants');
|
||||||
|
|
||||||
let $chatPage = $('.chat.page'); // The chatroom page
|
|
||||||
|
|
||||||
let username;
|
|
||||||
let typing = false;
|
|
||||||
let lastTypingTime;
|
|
||||||
|
|
||||||
let roomId = window.location.pathname.length ? window.location.pathname : null;
|
let roomId = window.location.pathname.length ? window.location.pathname : null;
|
||||||
|
|
||||||
if (!roomId) { return; }
|
if (!roomId) { return; }
|
||||||
@ -43,7 +29,8 @@ $(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let socket = io(roomId);
|
let socket = io(roomId);
|
||||||
const windowHandler = new WindowHandler(darkwire, socket);
|
const chat = new Chat(darkwire, socket);
|
||||||
|
const windowHandler = new WindowHandler(darkwire, socket, chat);
|
||||||
|
|
||||||
FastClick.attach(document.body);
|
FastClick.attach(document.body);
|
||||||
|
|
||||||
@ -56,232 +43,48 @@ $(function() {
|
|||||||
|
|
||||||
// Sets the client's username
|
// Sets the client's username
|
||||||
function initChat() {
|
function initChat() {
|
||||||
username = window.username;
|
|
||||||
// warn not incognitor
|
// warn not incognitor
|
||||||
if (!fs) {
|
if (fs) {
|
||||||
console.log('no fs');
|
|
||||||
} else {
|
|
||||||
fs(window.TEMPORARY,
|
fs(window.TEMPORARY,
|
||||||
100,
|
100,
|
||||||
log.bind(log, 'WARNING: Your browser is not in incognito mode!'));
|
() => {
|
||||||
}
|
chat.log('WARNING: Your browser is not in incognito mode!', {error: true});
|
||||||
|
|
||||||
// If the username is valid
|
|
||||||
if (username) {
|
|
||||||
$chatPage.show();
|
|
||||||
$inputMessage.focus();
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
cryptoUtil.createPrimaryKeys()
|
|
||||||
])
|
|
||||||
.then(function(data) {
|
|
||||||
darkwire.keys = {
|
|
||||||
public: data[0].publicKey,
|
|
||||||
private: data[0].privateKey
|
|
||||||
};
|
|
||||||
return Promise.all([
|
|
||||||
cryptoUtil.exportKey(data[0].publicKey, 'spki')
|
|
||||||
]);
|
|
||||||
})
|
|
||||||
.then(function(exportedKeys) {
|
|
||||||
// Tell the server your username and send public keys
|
|
||||||
socket.emit('add user', {
|
|
||||||
username: username,
|
|
||||||
publicKey: exportedKeys[0]
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
chat.log(moment().format('MMMM Do YYYY, h:mm:ss a'), {info: true});
|
||||||
|
darkwire.updateUsername(username).then((socketData) => {
|
||||||
// Log a message
|
chat.chatPage.show();
|
||||||
function log(message, options) {
|
chat.inputMessage.focus();
|
||||||
let html = options && options.html === true || false;
|
socket.emit('add user', socketData);
|
||||||
let $el;
|
|
||||||
if (html) {
|
|
||||||
$el = $('<li>').addClass('log').html(message);
|
|
||||||
} else {
|
|
||||||
$el = $('<li>').addClass('log').text(message);
|
|
||||||
}
|
|
||||||
addMessageElement($el, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds the visual chat message to the message list
|
|
||||||
function addChatMessage(data, options) {
|
|
||||||
if (!data.message.trim().length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let messageType = data.messageType || 'text';
|
|
||||||
|
|
||||||
// Don't fade the message in if there is an 'X was typing'
|
|
||||||
let $typingMessages = getTypingMessages(data);
|
|
||||||
options = options || {};
|
|
||||||
if ($typingMessages.length !== 0) {
|
|
||||||
options.fade = false;
|
|
||||||
$typingMessages.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
let $usernameDiv = $('<span class="username"/>')
|
|
||||||
.text(data.username)
|
|
||||||
.css('color', getUsernameColor(data.username));
|
|
||||||
let $messageBodyDiv = $('<span class="messageBody">');
|
|
||||||
// TODO: Ask client if accept/reject attachment
|
|
||||||
// If reject, destroy object in memory
|
|
||||||
// If accept, render image or content dispose
|
|
||||||
if (messageType === 'file') {
|
|
||||||
let image = new Image();
|
|
||||||
image.src = `data:image/png;base64,${data.message}`;
|
|
||||||
$messageBodyDiv.html(image);
|
|
||||||
} else {
|
|
||||||
$messageBodyDiv.html(data.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
let typingClass = data.typing ? 'typing' : '';
|
|
||||||
|
|
||||||
let $messageDiv = $('<li class="message"/>')
|
|
||||||
.data('username', data.username)
|
|
||||||
.addClass(typingClass)
|
|
||||||
.append($usernameDiv, $messageBodyDiv);
|
|
||||||
|
|
||||||
addMessageElement($messageDiv, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds the visual chat typing message
|
|
||||||
function addChatTyping(data) {
|
|
||||||
data.typing = true;
|
|
||||||
data.message = 'is typing';
|
|
||||||
addChatMessage(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes the visual chat typing message
|
|
||||||
function removeChatTyping(data) {
|
|
||||||
getTypingMessages(data).fadeOut(function() {
|
|
||||||
$(this).remove();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a message element to the messages and scrolls to the bottom
|
|
||||||
// el - The element to add as a message
|
|
||||||
// options.fade - If the element should fade-in (default = true)
|
|
||||||
// options.prepend - If the element should prepend
|
|
||||||
// all other messages (default = false)
|
|
||||||
function addMessageElement(el, options) {
|
|
||||||
let $el = $(el);
|
|
||||||
|
|
||||||
// Setup default options
|
|
||||||
if (!options) {
|
|
||||||
options = {};
|
|
||||||
}
|
|
||||||
if (typeof options.fade === 'undefined') {
|
|
||||||
options.fade = true;
|
|
||||||
}
|
|
||||||
if (typeof options.prepend === 'undefined') {
|
|
||||||
options.prepend = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply options
|
|
||||||
if (options.fade) {
|
|
||||||
$el.hide().fadeIn(FADE_TIME);
|
|
||||||
}
|
|
||||||
if (options.prepend) {
|
|
||||||
$messages.prepend($el);
|
|
||||||
} else {
|
|
||||||
$messages.append($el);
|
|
||||||
}
|
|
||||||
|
|
||||||
$messages[0].scrollTop = $messages[0].scrollHeight; // minus 60 for key
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevents input from having injected markup
|
// Prevents input from having injected markup
|
||||||
function cleanInput(input) {
|
function cleanInput(input) {
|
||||||
let message = $('<div/>').html(input).text();
|
input = input.replace(/\r?\n/g, '<br />');
|
||||||
message = Autolinker.link(message);
|
let sanitized = he.encode(input);
|
||||||
return message;
|
sanitized = Autolinker.link(sanitized);
|
||||||
}
|
return sanitized;
|
||||||
|
|
||||||
// Updates the typing event
|
|
||||||
function updateTyping() {
|
|
||||||
if (darkwire.connected) {
|
|
||||||
if (!typing) {
|
|
||||||
typing = true;
|
|
||||||
socket.emit('typing');
|
|
||||||
}
|
|
||||||
lastTypingTime = (new Date()).getTime();
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
let typingTimer = (new Date()).getTime();
|
|
||||||
let timeDiff = typingTimer - lastTypingTime;
|
|
||||||
if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
|
|
||||||
socket.emit('stop typing');
|
|
||||||
typing = false;
|
|
||||||
}
|
|
||||||
}, TYPING_TIMER_LENGTH);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the 'X is typing' messages of a user
|
|
||||||
function getTypingMessages(data) {
|
|
||||||
return $('.typing.message').filter(function(i) {
|
|
||||||
return $(this).data('username') === data.username;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the color of a username through our hash function
|
|
||||||
function getUsernameColor(username) {
|
|
||||||
// Compute hash code
|
|
||||||
let hash = 7;
|
|
||||||
for (let i = 0; i < username.length; i++) {
|
|
||||||
hash = username.charCodeAt(i) + (hash << 5) - hash;
|
|
||||||
}
|
|
||||||
// Calculate color
|
|
||||||
let index = Math.abs(hash % COLORS.length);
|
|
||||||
return COLORS[index];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard events
|
// Keyboard events
|
||||||
|
|
||||||
$window.keydown(function(event) {
|
$window.keydown(function(event) {
|
||||||
// When the client hits ENTER on their keyboard and chat message input is focused
|
// When the client hits ENTER on their keyboard and chat message input is focused
|
||||||
if (event.which === 13 && $('.inputMessage').is(':focus')) {
|
if (event.which === 13 && !event.shiftKey && $('.inputMessage').is(':focus')) {
|
||||||
handleMessageSending();
|
handleMessageSending();
|
||||||
socket.emit('stop typing');
|
socket.emit('stop typing');
|
||||||
typing = false;
|
chat.typing = false;
|
||||||
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$inputMessage.on('input propertychange paste change', function() {
|
|
||||||
updateTyping();
|
|
||||||
let message = $(this).val().trim();
|
|
||||||
if (message.length) {
|
|
||||||
$('#send-message-btn').addClass('active');
|
|
||||||
} else {
|
|
||||||
$('#send-message-btn').removeClass('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Select message input when closing modal
|
// Select message input when closing modal
|
||||||
$('.modal').on('hidden.bs.modal', function(e) {
|
$('.modal').on('hidden.bs.modal', function(e) {
|
||||||
$inputMessage.focus();
|
chat.inputMessage.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Select message input when clicking message body, unless selecting text
|
|
||||||
$('.messages').on('click', function() {
|
|
||||||
if (!getSelectedText()) {
|
|
||||||
$inputMessage.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function getSelectedText() {
|
|
||||||
var text = '';
|
|
||||||
if (typeof window.getSelection != 'undefined') {
|
|
||||||
text = window.getSelection().toString();
|
|
||||||
} else if (typeof document.selection != 'undefined' && document.selection.type == 'Text') {
|
|
||||||
text = document.selection.createRange().text;
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whenever the server emits 'login', log the login message
|
// Whenever the server emits 'login', log the login message
|
||||||
socket.on('user joined', function(data) {
|
socket.on('user joined', function(data) {
|
||||||
darkwire.connected = true;
|
darkwire.connected = true;
|
||||||
@ -293,38 +96,43 @@ $(function() {
|
|||||||
$('#first-modal').modal('show');
|
$('#first-modal').modal('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
log(data.username + ' joined');
|
chat.log(data.username + ' joined');
|
||||||
renderParticipantsList();
|
renderParticipantsList();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('user update', (data) => {
|
||||||
|
darkwire.updateUser(data).then((oldUsername) => {
|
||||||
|
chat.log(oldUsername + ' changed name to ' + data.username);
|
||||||
|
renderParticipantsList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Whenever the server emits 'new message', update the chat body
|
// Whenever the server emits 'new message', update the chat body
|
||||||
socket.on('new message', function(data) {
|
socket.on('new message', function(data) {
|
||||||
darkwire.decodeMessage(data).then((data) => {
|
darkwire.decodeMessage(data).then((decodedMessage) => {
|
||||||
if (!windowHandler.isActive) {
|
if (!windowHandler.isActive) {
|
||||||
windowHandler.notifyFavicon();
|
windowHandler.notifyFavicon();
|
||||||
darkwire.audio.play();
|
darkwire.audio.play();
|
||||||
}
|
}
|
||||||
if (data.messageType === 'file') {
|
|
||||||
// let file = windowHandler.fileHandler.decodeFile(data.message);
|
let data = {
|
||||||
// let chatMessage = {
|
username: decodedMessage.username,
|
||||||
// username: data.username,
|
message: decodedMessage.message.text,
|
||||||
// message: file
|
messageType: decodedMessage.messageType,
|
||||||
// }
|
additionalData: decodedMessage.message.additionalData
|
||||||
addChatMessage(data, {messageType: 'file'});
|
};
|
||||||
} else {
|
chat.addChatMessage(data);
|
||||||
addChatMessage(data);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Whenever the server emits 'user left', log it in the chat body
|
// Whenever the server emits 'user left', log it in the chat body
|
||||||
socket.on('user left', function(data) {
|
socket.on('user left', function(data) {
|
||||||
log(data.username + ' left');
|
chat.log(data.username + ' left');
|
||||||
addParticipantsMessage(data);
|
addParticipantsMessage(data);
|
||||||
removeChatTyping(data);
|
chat.removeChatTyping(data);
|
||||||
|
|
||||||
darkwire.removeUser(data);
|
darkwire.removeUser(data);
|
||||||
|
|
||||||
@ -333,12 +141,12 @@ $(function() {
|
|||||||
|
|
||||||
// Whenever the server emits 'typing', show the typing message
|
// Whenever the server emits 'typing', show the typing message
|
||||||
socket.on('typing', function(data) {
|
socket.on('typing', function(data) {
|
||||||
addChatTyping(data);
|
chat.addChatTyping(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Whenever the server emits 'stop typing', kill the typing message
|
// Whenever the server emits 'stop typing', kill the typing message
|
||||||
socket.on('stop typing', function(data) {
|
socket.on('stop typing', function(data) {
|
||||||
removeChatTyping(data);
|
chat.removeChatTyping(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
initChat();
|
initChat();
|
||||||
@ -365,9 +173,9 @@ $(function() {
|
|||||||
let li;
|
let li;
|
||||||
if (user.username === window.username) {
|
if (user.username === window.username) {
|
||||||
// User is me
|
// User is me
|
||||||
li = $('<li>' + user.username + ' <span class="you">(you)</span></li>').css('color', getUsernameColor(user.username));
|
li = $('<li>' + user.username + ' <span class="you">(you)</span></li>').css('color', chat.getUsernameColor(user.username));
|
||||||
} else {
|
} else {
|
||||||
li = $('<li>' + user.username + '</li>').css('color', getUsernameColor(user.username));
|
li = $('<li>' + user.username + '</li>').css('color', chat.getUsernameColor(user.username));
|
||||||
}
|
}
|
||||||
$('#participants-modal ul.users')
|
$('#participants-modal ul.users')
|
||||||
.append(li);
|
.append(li);
|
||||||
@ -377,27 +185,33 @@ $(function() {
|
|||||||
$('#send-message-btn').click(function() {
|
$('#send-message-btn').click(function() {
|
||||||
handleMessageSending();
|
handleMessageSending();
|
||||||
socket.emit('stop typing');
|
socket.emit('stop typing');
|
||||||
typing = false;
|
chat.typing = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.navbar-collapse ul li a').click(function() {
|
$('.navbar-collapse ul li a').click(function() {
|
||||||
$('.navbar-toggle:visible').click();
|
$('.navbar-toggle:visible').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
let audioSwitch = $('input.bs-switch').bootstrapSwitch();
|
let audioSwitch = $('input.sound-enabled').bootstrapSwitch();
|
||||||
|
|
||||||
audioSwitch.on('switchChange.bootstrapSwitch', function(event, state) {
|
audioSwitch.on('switchChange.bootstrapSwitch', function(event, state) {
|
||||||
darkwire.audio.soundEnabled = state;
|
darkwire.audio.soundEnabled = state;
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleMessageSending() {
|
function handleMessageSending() {
|
||||||
let message = $inputMessage;
|
let message = chat.inputMessage;
|
||||||
let cleanedMessage = cleanInput(message.val());
|
let cleanedMessage = cleanInput(message.val());
|
||||||
|
let slashCommand = chat.parseCommand(cleanedMessage);
|
||||||
|
|
||||||
|
if (slashCommand) {
|
||||||
|
return chat.executeCommand(slashCommand, this);
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent markup from being injected into the message
|
// Prevent markup from being injected into the message
|
||||||
darkwire.encodeMessage(cleanedMessage, 'text').then((socketData) => {
|
darkwire.encodeMessage(cleanedMessage, 'text').then((socketData) => {
|
||||||
message.val('');
|
message.val('');
|
||||||
$('#send-message-btn').removeClass('active');
|
$('#send-message-btn').removeClass('active');
|
||||||
addChatMessage({
|
chat.addChatMessage({
|
||||||
username: username,
|
username: username,
|
||||||
message: cleanedMessage
|
message: cleanedMessage
|
||||||
});
|
});
|
||||||
@ -407,4 +221,51 @@ $(function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.triggerFileTransfer = function(context) {
|
||||||
|
const fileId = context.getAttribute('data-file');
|
||||||
|
if (fileId) {
|
||||||
|
return windowHandler.fileHandler.encodeFile(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat.log('Requested file transfer is no longer valid. Please try again.', {error: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.triggerFileDestroy = function(context) {
|
||||||
|
const fileId = context.getAttribute('data-file');
|
||||||
|
if (fileId) {
|
||||||
|
return windowHandler.fileHandler.destroyFile(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat.log('Requested file transfer is no longer valid. Please try again.', {error: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.triggerFileDownload = function(context) {
|
||||||
|
const fileId = context.getAttribute('data-file');
|
||||||
|
const file = darkwire.getFile(fileId);
|
||||||
|
windowHandler.fileHandler.createBlob(file.message, file.messageType).then((blob) => {
|
||||||
|
let url = windowHandler.fileHandler.createUrlFromBlob(blob);
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
if (file.messageType.match('image.*')) {
|
||||||
|
let image = new Image();
|
||||||
|
image.src = url;
|
||||||
|
chat.replaceMessage('#file-transfer-request-' + fileId, image);
|
||||||
|
} else {
|
||||||
|
let downloadLink = document.createElement('a');
|
||||||
|
downloadLink.href = url;
|
||||||
|
downloadLink.target = '_blank';
|
||||||
|
downloadLink.innerHTML = 'Download ' + file.additionalData.fileName;
|
||||||
|
chat.replaceMessage('#file-transfer-request-' + fileId, downloadLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
darkwire.encodeMessage('Downloaded ' + file.additionalData.fileName, 'text').then((socketData) => {
|
||||||
|
socket.emit('new message', socketData);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import FileHandler from './fileHandler';
|
import FileHandler from './fileHandler';
|
||||||
|
|
||||||
export default class WindowHandler {
|
export default class WindowHandler {
|
||||||
constructor(darkwire, socket) {
|
constructor(darkwire, socket, chat) {
|
||||||
this._isActive = false;
|
this._isActive = false;
|
||||||
this.fileHandler = new FileHandler(darkwire, socket);
|
this.fileHandler = new FileHandler(darkwire, socket, chat);
|
||||||
|
|
||||||
this.newMessages = 0;
|
this.newMessages = 0;
|
||||||
this.favicon = new Favico({
|
this.favicon = new Favico({
|
||||||
@ -35,6 +35,9 @@ export default class WindowHandler {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$('#fileInput').trigger('click');
|
$('#fileInput').trigger('click');
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
$('#send-file').remove();
|
||||||
|
$('#fileInput').remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ html {
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body, input {
|
html, body, input, textarea {
|
||||||
font-family:
|
font-family:
|
||||||
"SourceCodePro-Regular",
|
"SourceCodePro-Regular",
|
||||||
"HelveticaNeue-Light",
|
"HelveticaNeue-Light",
|
||||||
@ -160,6 +160,10 @@ input {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.log {
|
.log {
|
||||||
color: gray;
|
color: gray;
|
||||||
font-size: 70%;
|
font-size: 70%;
|
||||||
@ -196,19 +200,25 @@ input {
|
|||||||
|
|
||||||
/* Input */
|
/* Input */
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
.inputMessage{
|
.inputMessage{
|
||||||
background: black !important;
|
background: black !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid #282828;
|
border-top: 1px solid #282828;
|
||||||
bottom: 0;
|
// height: 60px;
|
||||||
height: 60px;
|
|
||||||
left: 0;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
padding-left: 10px;
|
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 10px 75px 10px 10px;
|
||||||
/*Fix for inner shadow on iOS*/
|
/*Fix for inner shadow on iOS*/
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
@ -296,3 +306,24 @@ html.no-touchevents .chat #input-icons {
|
|||||||
#fileInput {
|
#fileInput {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-disclaimer {
|
||||||
|
font-size: 10px;
|
||||||
|
color: red;
|
||||||
|
line-height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-info {
|
||||||
|
color: #FFF;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-error {
|
||||||
|
color: yellow;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
font-style: italic;
|
||||||
|
color: #00FF7F;
|
||||||
|
}
|
||||||
|
76
src/public/vendor/autogrow.js
vendored
Normal file
76
src/public/vendor/autogrow.js
vendored
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
(function($)
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Auto-growing textareas; technique ripped from Facebook
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* http://github.com/jaz303/jquery-grab-bag/tree/master/javascripts/jquery.autogrow-textarea.js
|
||||||
|
*/
|
||||||
|
$.fn.autogrow = function(options)
|
||||||
|
{
|
||||||
|
return this.filter('textarea').each(function()
|
||||||
|
{
|
||||||
|
var self = this;
|
||||||
|
var $self = $(self);
|
||||||
|
var minHeight = $self.height();
|
||||||
|
var noFlickerPad = $self.hasClass('autogrow-short') ? 0 : parseInt($self.css('lineHeight')) || 0;
|
||||||
|
var settings = $.extend({
|
||||||
|
preGrowCallback: null,
|
||||||
|
postGrowCallback: null
|
||||||
|
}, options );
|
||||||
|
|
||||||
|
var shadow = $('<div></div>').css({
|
||||||
|
position: 'absolute',
|
||||||
|
top: -10000,
|
||||||
|
left: -10000,
|
||||||
|
width: $self.width(),
|
||||||
|
fontSize: $self.css('fontSize'),
|
||||||
|
fontFamily: $self.css('fontFamily'),
|
||||||
|
fontWeight: $self.css('fontWeight'),
|
||||||
|
lineHeight: $self.css('lineHeight'),
|
||||||
|
resize: 'none',
|
||||||
|
'word-wrap': 'break-word'
|
||||||
|
}).appendTo(document.body);
|
||||||
|
|
||||||
|
var update = function(event)
|
||||||
|
{
|
||||||
|
var times = function(string, number)
|
||||||
|
{
|
||||||
|
for (var i=0, r=''; i<number; i++) r += string;
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
|
||||||
|
var val = self.value.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/\n$/, '<br/> ')
|
||||||
|
.replace(/\n/g, '<br/>')
|
||||||
|
.replace(/ {2,}/g, function(space){ return times(' ', space.length - 1) + ' ' });
|
||||||
|
|
||||||
|
// Did enter get pressed? Resize in this keydown event so that the flicker doesn't occur.
|
||||||
|
if (event && event.data && event.data.event === 'keydown' && event.keyCode === 13) {
|
||||||
|
val += '<br />';
|
||||||
|
}
|
||||||
|
|
||||||
|
shadow.css('width', $self.width());
|
||||||
|
shadow.html(val + (noFlickerPad === 0 ? '...' : '')); // Append '...' to resize pre-emptively.
|
||||||
|
|
||||||
|
var newHeight=Math.max(shadow.height() + noFlickerPad, minHeight);
|
||||||
|
if(settings.preGrowCallback!=null){
|
||||||
|
newHeight=settings.preGrowCallback($self,shadow,newHeight,minHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
$self.height(newHeight);
|
||||||
|
|
||||||
|
if(settings.postGrowCallback!=null){
|
||||||
|
settings.postGrowCallback($self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$self.change(update).keyup(update).keydown({event:'keydown'},update);
|
||||||
|
$(window).resize(update);
|
||||||
|
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})(jQuery);
|
35
src/room.js
35
src/room.js
@ -26,7 +26,8 @@ class Room {
|
|||||||
data: data.data,
|
data: data.data,
|
||||||
vector: data.vector,
|
vector: data.vector,
|
||||||
secretKeys: data.secretKeys,
|
secretKeys: data.secretKeys,
|
||||||
signature: data.signature
|
signature: data.signature,
|
||||||
|
timestamp: new Date()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -46,7 +47,8 @@ class Room {
|
|||||||
thisIO.emit('user joined', {
|
thisIO.emit('user joined', {
|
||||||
username: socket.username,
|
username: socket.username,
|
||||||
numUsers: this.numUsers,
|
numUsers: this.numUsers,
|
||||||
users: this.users
|
users: this.users,
|
||||||
|
timestamp: new Date()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -75,7 +77,8 @@ class Room {
|
|||||||
username: socket.username,
|
username: socket.username,
|
||||||
numUsers: this.numUsers,
|
numUsers: this.numUsers,
|
||||||
users: this.users,
|
users: this.users,
|
||||||
id: socket.user.id
|
id: socket.user.id,
|
||||||
|
timestamp: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
// remove room from rooms array
|
// remove room from rooms array
|
||||||
@ -84,10 +87,34 @@ class Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update user
|
||||||
|
socket.on('update user', (data) => {
|
||||||
|
if (data.newUsername.length > 16) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let user = _.find(this.users, (users) => {
|
||||||
|
return users === socket.user;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
user.username = data.newUsername;
|
||||||
|
socket.username = user.username;
|
||||||
|
socket.user = user;
|
||||||
|
|
||||||
|
thisIO.emit('user update', {
|
||||||
|
username: socket.username,
|
||||||
|
id: socket.user.id,
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get roomId() {
|
roomId() {
|
||||||
return this.id;
|
return this.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,11 +53,13 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<input class="inputMessage" placeholder="Type here..."/>
|
<div class="inputContainer">
|
||||||
<div id="input-icons">
|
<textarea class="inputMessage" placeholder="Type here..."/></textarea>
|
||||||
<span class="glyphicon glyphicon-file" id="send-file"></span>
|
<div id="input-icons">
|
||||||
<input type="file" id="fileInput">
|
<span class="glyphicon glyphicon-file" id="send-file"></span>
|
||||||
<span class="glyphicon glyphicon-send" id="send-message-btn" aria-hidden="true"></span>
|
<input type="file" name="fileUploader" id="fileInput">
|
||||||
|
<span class="glyphicon glyphicon-send" id="send-message-btn" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -68,13 +70,15 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
<h4 class="modal-title">About</h4>
|
<h4 class="modal-title">About</h4>
|
||||||
|
<p><strong>Darkwire Version:</strong> <span id="appVersion">v{{APP.version}}</span> (<a href="https://github.com/seripap/darkwire.io/releases/tag/v{{APP.version}}" target="_blank">View Release Notes</a>)<br></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p class="bold">WARNING: This product is in beta and its source code has not been peer-reviewed or undergone a security audit. This is a demo only and not intended for security-critical use. <a href="https://github.com/seripap/darkwire.io" target="_blank">View source code</a>.</p>
|
{{>partials/ip}}
|
||||||
|
|
||||||
<p>darkwire.io is the simplest way to chat with encryption online. Chat history is never stored on a server or database, and plain text messages are never transferred over the wire.</p>
|
<p class="bold">WARNING: Darkwire does not mask IP addresses nor can verify the integrity of parties recieving messages. Proceed with caution and always confirm recipients before starting a chat session.</p>
|
||||||
|
|
||||||
<p>Questions/comments? Email us at hello[at]darkwire.io</p>
|
<p>Questions/comments? Email us at hello[at]darkwire.io<br>
|
||||||
|
Found a bug or want a new feature? <a href="https://github.com/seripap/darkwire.io/issues" target="_blank">Open a ticket on Github</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
@ -97,7 +101,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<br>
|
<br>
|
||||||
<h6>Sound</h6>
|
<h6>Sound</h6>
|
||||||
<input type="checkbox" name="my-checkbox" class="form-control bs-switch" checked>
|
<input type="checkbox" name="sound-enabled" class="form-control bs-switch sound-enabled" checked>
|
||||||
|
<br>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default encryption-active" data-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-default encryption-active" data-dismiss="modal">Close</button>
|
||||||
@ -111,11 +116,13 @@
|
|||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
<h4 class="modal-title">Welcome to darkwire.io</h4>
|
<h4 class="modal-title">Welcome to darkwire.io v{{APP.version}}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<h6>We've placed you in a new chat room</h6>
|
<h6>We've placed you in a new chat room</h6>
|
||||||
<p class="bold">WARNING: This product is in beta and its source code has not been peer-reviewed or undergone a security audit. This is a demo only and not intended for security-critical use. <a href="https://github.com/seripap/darkwire.io" target="_blank">View source code</a>.</p>
|
<p class="bold">This software uses the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Crypto" target="_blank">Crypto Web API</a> to encrypt data which is transferred using <a href="https://en.wikipedia.org/wiki/WebSocket" target="_blank">secure WebSockets</a>. <a href="javascript:void(0)" id="about-nav" data-dismiss="modal">See Disclaimer</a>.</p>
|
||||||
|
|
||||||
|
<p><a href="https://github.com/seripap/darkwire.io" target="_blank">We believe in your privacy and full transparency. View the source code and documentation on Github.</a></p>
|
||||||
<br>
|
<br>
|
||||||
<h6>Invite People to This Room</h6>
|
<h6>Invite People to This Room</h6>
|
||||||
<p>
|
<p>
|
||||||
@ -166,7 +173,6 @@
|
|||||||
<script src="/favicon.js"></script>
|
<script src="/favicon.js"></script>
|
||||||
<script src="/vendor/bootstrap-3.3.6.min.js"></script>
|
<script src="/vendor/bootstrap-3.3.6.min.js"></script>
|
||||||
<script src="/vendor/autolinker.min.js"></script>
|
<script src="/vendor/autolinker.min.js"></script>
|
||||||
<script src="/vendor/underscore.min.js"></script>
|
|
||||||
<script src="/vendor/modernizr-custom.min.js"></script>
|
<script src="/vendor/modernizr-custom.min.js"></script>
|
||||||
<script src="/vendor/bootstrap-switch.min.js"></script>
|
<script src="/vendor/bootstrap-switch.min.js"></script>
|
||||||
<script src="/vendor/web-crypto-shim.js"></script>
|
<script src="/vendor/web-crypto-shim.js"></script>
|
||||||
|
4
src/views/partials/ip.mustache
Normal file
4
src/views/partials/ip.mustache
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{{#APP.ip}}
|
||||||
|
<p class="bold">Recognized IP: <a href="http://ipinfo.io/{{APP.ip}}/json" target="_blank">{{APP.ip}}</a>*</p>
|
||||||
|
<p>*This IP is your public IP recognized by all web servers. If this IP and/or its location data seems familiar to you, we recommend using <a href="https://www.torproject.org/" target="_blank">TOR</a> or a proxy before starting a new chat session.</p>
|
||||||
|
{{/APP.ip}}
|
@ -1,7 +1,11 @@
|
|||||||
|
/*jshint -W030 */
|
||||||
|
import App from '../../package.json';
|
||||||
|
|
||||||
describe('Darkwire', () => {
|
describe('Darkwire', () => {
|
||||||
|
|
||||||
describe('starting a room', () => {
|
describe('Creating a room', () => {
|
||||||
|
|
||||||
|
var testingRoom = null;
|
||||||
let browser;
|
let browser;
|
||||||
|
|
||||||
before((client, done) => {
|
before((client, done) => {
|
||||||
@ -25,31 +29,31 @@ describe('Darkwire', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show welcome modal', () => {
|
it('Should show welcome modal', () => {
|
||||||
browser
|
browser
|
||||||
.waitForElementVisible('#first-modal', 5000)
|
.waitForElementVisible('#first-modal', 5000)
|
||||||
.assert.containsText('#first-modal .modal-title', 'Welcome to darkwire.io');
|
.expect.element('#first-modal').to.be.visible;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have correct header', () => {
|
it('Should be started with NPM', () => {
|
||||||
browser.expect.element('#first-modal .modal-title').text.to.equal('Welcome to darkwire.io');
|
browser.expect.element('#first-modal .modal-title').text.to.equal('Welcome to darkwire.io v' + App.version);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('opening a second window', () => {
|
describe('Joining chat room', () => {
|
||||||
|
|
||||||
before((client, done) => {
|
before((client, done) => {
|
||||||
browser.url((result) => {
|
browser.url((result) => {
|
||||||
let urlSplit = result.value.split('/');
|
let urlSplit = result.value.split('/');
|
||||||
let roomId = urlSplit[urlSplit.length - 1];
|
testingRoom = urlSplit[urlSplit.length - 1];
|
||||||
let url = 'http://localhost:3000/' + roomId;
|
let url = 'http://localhost:3000/' + testingRoom;
|
||||||
browser.execute(() => {
|
browser.execute(() => {
|
||||||
window.open('http://localhost:3000/', '_blank');
|
window.open('http://localhost:3000/', '_blank');
|
||||||
}, [], () => {
|
}, [], () => {
|
||||||
browser.window_handles((result) => {
|
browser.windowHandles((result) => {
|
||||||
browser.switchWindow(result.value[1], () => {
|
browser.switchWindow(result.value[1], () => {
|
||||||
browser.execute((id) => {
|
browser.execute((id) => {
|
||||||
window.open('http://localhost:3000/' + id, '_self');
|
window.open('http://localhost:3000/' + id, '_self');
|
||||||
}, [roomId], () => {
|
}, [testingRoom], () => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -58,25 +62,26 @@ describe('Darkwire', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show welcome modal', () => {
|
it('Should not show welcome modal', () => {
|
||||||
browser.assert.hidden('#first-modal');
|
browser.assert.hidden('#first-modal');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sending messages', () => {
|
describe('Sending chat message', () => {
|
||||||
|
|
||||||
before((client, done) => {
|
before((client, done) => {
|
||||||
browser.waitForElementPresent('ul.users li:nth-child(2)', 5000, () => {
|
browser.waitForElementPresent('ul.users li:nth-child(2)', 5000, () => {
|
||||||
browser.setValue('input.inputMessage', ['Hello world', browser.Keys.RETURN], () => {
|
browser.setValue('textarea.inputMessage', ['Hello world!', browser.Keys.RETURN], () => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('Should send a message', () => {
|
||||||
browser.window_handles((result) => {
|
browser.windowHandles((result) => {
|
||||||
browser.switchWindow(result.value[0], () => {
|
browser.switchWindow(result.value[0], () => {
|
||||||
browser.waitForElementPresent('span.messageBody', 5000, () => {
|
browser.waitForElementPresent('span.messageBody', 5000, () => {
|
||||||
browser.assert.containsText('span.messageBody', 'Hello world');
|
browser.pause(2000);
|
||||||
|
browser.assert.containsText('span.messageBody', 'Hello world!');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -86,5 +91,129 @@ describe('Darkwire', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Slash Commands', () => {
|
||||||
|
|
||||||
|
before((client, done) => {
|
||||||
|
let url = 'http://localhost:3000/' + testingRoom;
|
||||||
|
browser.url(url, () => {
|
||||||
|
browser.windowHandles((result) => {
|
||||||
|
browser.switchWindow(result.value[0], () => {
|
||||||
|
browser.execute((id) => {
|
||||||
|
window.open('http://localhost:3000/' + id, '_self');
|
||||||
|
}, [testingRoom], () => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('/me', () => {
|
||||||
|
|
||||||
|
before((client, done) => {
|
||||||
|
browser.windowHandles((result) => {
|
||||||
|
browser.switchWindow(result.value[0], () => {
|
||||||
|
browser.waitForElementPresent('ul.users li:nth-child(2)', 5000, () => {
|
||||||
|
browser.setValue('textarea.inputMessage', ['/me is no stranger to love', browser.Keys.RETURN], () => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should express an interactive action', () => {
|
||||||
|
browser.windowHandles((result) => {
|
||||||
|
browser.switchWindow(result.value[0], () => {
|
||||||
|
browser.waitForElementPresent('span.messageBody', 5000, () => {
|
||||||
|
browser.pause(5000);
|
||||||
|
browser.assert.containsText('.action span.messageBody', 'is no stranger to love');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('/nick', () => {
|
||||||
|
|
||||||
|
before((client, done) => {
|
||||||
|
browser.url('http://localhost:3000/' + testingRoom, () => {
|
||||||
|
browser.waitForElementPresent('ul.users li:nth-child(2)', 5000, () => {
|
||||||
|
browser.setValue('textarea.inputMessage', ['/nick rickAnsley', browser.Keys.RETURN], () => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should change username', () => {
|
||||||
|
browser.windowHandles((result) => {
|
||||||
|
browser.switchWindow(result.value[3], () => {
|
||||||
|
browser.pause(5000);
|
||||||
|
browser.assert.containsText('.log:last-child', 'rickAnsley');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Before file transfer: Image: Confirm sending', () => {
|
||||||
|
|
||||||
|
before((client, done) => {
|
||||||
|
let url = 'http://localhost:3000/' + testingRoom;
|
||||||
|
browser.url(url, () => {
|
||||||
|
browser.windowHandles((result) => {
|
||||||
|
browser.switchWindow(result.value[0], () => {
|
||||||
|
browser.execute((id) => {
|
||||||
|
window.open('http://localhost:3000/' + id, '_self');
|
||||||
|
}, [testingRoom], () => {
|
||||||
|
browser.waitForElementPresent('#send-file', 5000, () => {
|
||||||
|
browser.execute(() => {
|
||||||
|
$('input[name="fileUploader"]').show();
|
||||||
|
}, [], () => {
|
||||||
|
browser.waitForElementPresent('input[name="fileUploader"]', 5000, () => {
|
||||||
|
let testFile = __dirname + '/ricky.jpg';
|
||||||
|
browser.setValue('input[name="fileUploader"]', testFile, (result) => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should prompt user confirmation', () => {
|
||||||
|
browser.windowHandles((result) => {
|
||||||
|
browser.switchWindow(result.value[0], () => {
|
||||||
|
browser.waitForElementPresent('span.messageBody', 5000, () => {
|
||||||
|
browser.pause(5000);
|
||||||
|
browser.assert.containsText('span.messageBody', 'You are about to send ricky.jpg to all parties in this chat. Confirm | Cancel');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should show sent confirmation message', () => {
|
||||||
|
browser.windowHandles((result) => {
|
||||||
|
browser.switchWindow(result.value[0], () => {
|
||||||
|
browser.waitForElementPresent('span.messageBody a:first-child', 5000, () => {
|
||||||
|
browser.click('span.messageBody a:first-child', () => {
|
||||||
|
browser.waitForElementNotPresent('span.messageBody a:first-child', 5000, () => {
|
||||||
|
browser.assert.containsText('span.messageBody', 'Sent ricky.jpg');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
1
test/acceptance/fileUtility.js
Normal file
1
test/acceptance/fileUtility.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
($('#fileInput').show)();
|
52
test/acceptance/nightwatch-local.json
Normal file
52
test/acceptance/nightwatch-local.json
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"src_folders" : ["test"],
|
||||||
|
"output_folder" : "reports",
|
||||||
|
"custom_commands_path" : "",
|
||||||
|
"custom_assertions_path" : "",
|
||||||
|
"page_objects_path" : "",
|
||||||
|
"globals_path" : "",
|
||||||
|
"test_runner" : "mocha",
|
||||||
|
"selenium" : {
|
||||||
|
"start_process" : true,
|
||||||
|
"server_path" : "test/acceptance/bin/selenium-server-standalone-2.52.0.jar",
|
||||||
|
"log_path" : false,
|
||||||
|
"host" : "127.0.0.1",
|
||||||
|
"port" : 4444,
|
||||||
|
"cli_args" : {
|
||||||
|
"webdriver.chrome.driver" : "/usr/local/bin/chromedriver",
|
||||||
|
"webdriver.ie.driver" : ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"test_settings" : {
|
||||||
|
"default" : {
|
||||||
|
"launch_url" : "http://localhost",
|
||||||
|
"selenium_port" : 4444,
|
||||||
|
"selenium_host" : "localhost",
|
||||||
|
"silent": true,
|
||||||
|
"screenshots" : {
|
||||||
|
"enabled" : false,
|
||||||
|
"path" : ""
|
||||||
|
},
|
||||||
|
"desiredCapabilities": {
|
||||||
|
"browserName": "chrome",
|
||||||
|
"javascriptEnabled": true,
|
||||||
|
"acceptSslCerts": true,
|
||||||
|
"chromeOptions" : {
|
||||||
|
"binary": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"chrome" : {
|
||||||
|
"desiredCapabilities": {
|
||||||
|
"browserName": "chrome",
|
||||||
|
"javascriptEnabled": true,
|
||||||
|
"acceptSslCerts": true,
|
||||||
|
"chromeOptions" : {
|
||||||
|
"args" : ["-e", "--no-sandbox"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
test/acceptance/ricky.jpg
Normal file
BIN
test/acceptance/ricky.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
Loading…
x
Reference in New Issue
Block a user