Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aaron Saloff's Project #20

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bf5eb08
added name to readme
asaloff Oct 7, 2017
75b95e0
added UI
asaloff Oct 11, 2017
db77620
added chat socket functionality
asaloff Oct 12, 2017
8270791
added users and login
asaloff Oct 13, 2017
7e55208
users can add chat rooms
asaloff Oct 13, 2017
aae09d1
users can join and exit chats
asaloff Oct 13, 2017
1564dc6
added redis data structure to markdown file
asaloff Oct 16, 2017
ff07bbe
Delete test.rb
asaloff Oct 16, 2017
f73ba26
fixed nested logic to use async/await + moved router logic to seperat…
asaloff Oct 17, 2017
507c485
minor refactoring
asaloff Oct 17, 2017
680472f
prepared for deployment
asaloff Feb 1, 2018
f3e8b2d
configured redis for heroku
asaloff Feb 1, 2018
b43faab
fixed capitalization on redis file name
asaloff Feb 1, 2018
07ea427
fixed Procfile
asaloff Feb 1, 2018
58c93ef
added logged socket connection for heroku debug
asaloff Feb 1, 2018
7e19833
added logged socket connection for heroku debug
asaloff Feb 1, 2018
cfdc221
added logged socket connection for heroku debug
asaloff Feb 1, 2018
e686ba9
added logged socket connection for heroku debug
asaloff Feb 1, 2018
23b0927
added logged socket connection for heroku debug
asaloff Feb 1, 2018
766a7e2
moved socket listening code to app.js
asaloff Feb 2, 2018
c71fe9b
moved server listen above socket connection
asaloff Feb 2, 2018
ef067c8
fix for heroku and socket
asaloff Feb 2, 2018
58c27ea
fix for heroku and socket
asaloff Feb 2, 2018
f6930cc
removed host from socket definition
asaloff Feb 2, 2018
6cec39c
updated README
asaloff Feb 2, 2018
2c753a8
updated README
asaloff Feb 2, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: node app
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,26 @@
# project_superchat
Build a realtime multi-room chat application. Make it super.
# Realtime Chat App

A realtime multi-room chat application built using Node/Express and websockets.

![Superchat Demo](https://s3.amazonaws.com/demo-gifs-asaloff/superchat.gif)

[Demo](https://node-express-live-chat.herokuapp.com) - (Hosted free on heroku and may be sleeping. Could take 30 seconds the first time to load)

### Technologies Used

- Node.js
- Express.js
- Redis (data store)
- Bootstrap
- Handlebars (view engine)
- socket.io (realtime page updates)

### Usage

- Login using any username
- Open two seperate browsers to see live chat updates

Feel free to get in touch with any feedback or questions. [email protected]



68 changes: 68 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const express = require('express');
const path = require('path');
const logger = require('morgan');
const morganToolkit = require('morgan-toolkit')(logger);
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const helpers = require('./helpers');
const exphbs = require('express-handlebars');
const redis = require('redis');
const bluebird = require('bluebird');
bluebird.promisifyAll(redis.RedisClient.prototype);
bluebird.promisifyAll(redis.Multi.prototype);

const index = require('./routes/index');

const app = express();

// view engine setup
const hbs = exphbs.create({
helpers: helpers,
partialsDir: 'views/partials/',
defaultLayout: 'main.hbs',
extname: '.hbs'
});

app.engine('hbs', hbs.engine);
app.set('view engine', 'hbs');
app.set('Handlebars', hbs);

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/socket.io',express.static(path.join(__dirname, 'node_modules/socket.io-client/dist/')));
app.use(logger('short'));
app.use(morganToolkit());

app.use('/', index);

// catch 404 and forward to error handler
app.use((req, res, next) => {
var err = new Error('Not Found');
err.status = 404;
next(err);
});

// error handler
app.use((err, req, res) => {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500);
res.render('error');
});

const server = require('http').createServer(app);
const PORT = process.env.PORT || 3000;

const io = require('socket.io').listen(server);

server.listen(PORT, () => console.log(`Listening on ${ PORT }`));

module.exports = { app, io };

const socket = require('./lib/socket_service');
socket.setup(io);
8 changes: 8 additions & 0 deletions data_structure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Redis Tables
============

**SET** - roomName: [username1, username2 ...] - for listing rooms with amount of members

**SET** - 'users': [username1, username2 ...] - for adding new users and validating

**HASH** 'messages:roomName:uniqueId': { body: string, author: username, room: roomName, createdAt: time }
15 changes: 15 additions & 0 deletions helpers/debug_helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
var HandlebarsDebugHelper = {};

HandlebarsDebugHelper.debug = (optionalValue) => {
console.log("Current Context");
console.log("====================");
console.log(this);

if (optionalValue) {
console.log("Value");
console.log("====================");
console.log(optionalValue);
}
};

module.exports = HandlebarsDebugHelper;
6 changes: 6 additions & 0 deletions helpers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const HelperLoader = require('load-helpers');
const helperLoader = new HelperLoader();

const helpers = helperLoader.load('helpers/*_helper.js').cache;

module.exports = helpers;
17 changes: 17 additions & 0 deletions helpers/redisHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const RedisHelper = {
createClient: () => {
const redis = require('redis');
let redisClient;
if (process.env.REDISTOGO_URL) {
var rtg = require("url").parse(process.env.REDISTOGO_URL);
redisClient = redis.createClient(rtg.port, rtg.hostname);

redisClient.auth(rtg.auth.split(":")[1]);
} else {
redisClient = redis.createClient();
}
return redisClient;
}
};

module.exports = RedisHelper;
28 changes: 28 additions & 0 deletions helpers/view_helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const Handlebars = require('handlebars');

var viewHelper = {};

const getFormattedTime = (d) => {
var hours = d.getUTCHours();
var minutes = d.getUTCMinutes();

// 0 padding for minutes under 10
if (minutes < 10) { minutes = `0${minutes}`; }

if (hours > 12) {
return `${ hours - 12 }:${minutes}pm UTC`;
} else {
return `${ hours }:${minutes}am UTC`;
}
};

viewHelper.formatDate = (d) => {
d = new Date(d);
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()} ${getFormattedTime(d)}`;
};

viewHelper.addHr = (messages, message) => {
if (messages[0] !== message) return new Handlebars.SafeString('<hr>');
};

module.exports = viewHelper;
76 changes: 76 additions & 0 deletions lib/chat_rooms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const RedisHelper = require('../helpers/redisHelper');
const redisClient = RedisHelper.createClient();

var ChatRooms = {};

ChatRooms.getAll = (username, activeRoom) => {
return new Promise(async (resolve, reject) => {

const chatRoomName = activeRoom.split('-').join(' ');

var allRooms = { username, chatRoomName, activeRoom };

allRooms.rooms = await getRooms(activeRoom);
var keys = await redisClient.keysAsync(`messages:${activeRoom}:*`);
allRooms.isMember = await checkUserMember(activeRoom, username);

if (keys.length === 0) {
resolve(allRooms);
}

var messages = [];

for (let key of keys) {
redisClient.hgetallAsync(key).then(message => {

messages.push(message);

if (messages.length === keys.length) {
allRooms.messages = messages.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
resolve(allRooms);
}
});
}
});
};

const getRooms = (activeRoom) => {
return new Promise((resolve, reject) => {
redisClient.smembers('users', (err, list) => {

var totalUsers = list.length;

redisClient.keys('room:*', (err, roomKeys) => {
var rooms = [];

if (!roomKeys.length) {
resolve(rooms);
}

roomKeys.forEach(roomKey => {
redisClient.smembers(roomKey, (err, members) => {

var slug = roomKey.slice(5);
var name = slug.split('-').join(' ');

rooms.push({ name: name, memberAmount: members.length, active: slug == activeRoom, slug: slug });

if (rooms.length === roomKeys.length) {
resolve(rooms);
}
});
});
});
});
});
};

const checkUserMember = (room, user) => {
return new Promise((resolve, reject) => {
redisClient.sismember(`room:${ room }`, user, (err, isMember) => {
resolve(isMember);
});
});
};

module.exports = ChatRooms;
94 changes: 94 additions & 0 deletions lib/socket_service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
var Socket = {};

Socket.setup = (io) => {
const { app } = require('../app');
const hbs = app.get('Handlebars');
const RedisHelper = require('../helpers/redisHelper');
const redisClient = RedisHelper.createClient();
const randomstring = require('randomstring');

const generateRandomString = () => {
return randomstring.generate({
length: 8,
charset: 'alphanumeric'
});
};

const createUniqueId = () => {
return new Promise((resolve, reject) => {
var string = generateRandomString();

redisClient.sismember('ids', string, (err, used) => {
if (used) {
createUniqueId()
.then((string) => {
resolve(string);
});
} else {
redisClient.sadd('ids', string);
resolve(string);
}
});
});
};

io.on('connection', client => {

console.log('Socket Connected');

client.on('new-message', async (info, username) => {
// generate unique id
var id = await createUniqueId();

var room = info.room.split(' ').join('-');

redisClient.hmset(`messages:${ room }:${ id }`, 'body', info.body, 'createdAt', info.createdAt, 'author', username, 'room', info.room );
var partial = await hbs.render(
'views/partials/message.hbs',
{ body: info.body, createdAt: info.createdAt, author: username, leadingHr: info.leadingHr }
);

io.emit('render-message', partial, room);
});

client.on('new-room-name', (roomName, username) => {
var slug = roomName.trim().split(' ').join('-');
// check if exists
redisClient.smembers(`room:${ slug }`, async (err, members) => {
if (members.length) {
client.emit('room-name-taken');
} else {
redisClient.sadd(`room:${ slug }`, username);

// render partial
var partial = await hbs.render(
'views/partials/room.hbs',
{ name: `${ roomName }`, memberAmount: 1, slug: slug }
);

io.emit('new-room', partial);
}
});
});

client.on('join-user', async (room, username) => {
await redisClient.sadd(`room:${ room }`, username);
handleMemberCounts(room);
});

client.on('leave-user', async (room, username) => {
await redisClient.srem(`room:${ room }`, username);
handleMemberCounts(room);
});

const handleMemberCounts = room => {
redisClient.smembers(`room:${ room }`, (err, members) => {
var memberCount = members.length;
client.emit('refresh');
io.emit('change-member-count', room, memberCount);
});
};
});
};

module.exports = Socket;
26 changes: 26 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "project-superchat",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node app",
"debug": "node debug app"
},
"dependencies": {
"bluebird": "^3.5.1",
"body-parser": "~1.17.1",
"cookie-parser": "~1.4.3",
"debug": "~2.6.3",
"express": "^4.15.5",
"express-handlebars": "^3.0.0",
"hbs": "~4.0.1",
"load-helpers": "^1.0.1",
"morgan": "~1.8.1",
"morgan-toolkit": "^1.0.2",
"randomstring": "^1.1.5",
"redis": "^2.8.0",
"serve-favicon": "~2.4.2",
"socket.io": "^2.0.3",
"socket.io-client": "^2.0.3"
}
}
Binary file added public/.DS_Store
Binary file not shown.
18 changes: 18 additions & 0 deletions public/javascripts/behavior.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
$(document).ready(() => {
$('#new-chatroom').click(() => {
$('#newChat').modal('show');
});

$('#logout-link').click((e) => {
e.preventDefault();
$('#logout-form').submit();
});

$('.room-select').click((room) => {
var id = room.target.id;
var roomName = id.slice(5).split('-').join(' ');

document.cookie = `chatRoom=${ roomName }`;
document.location = '/';
});
});
Loading