diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ccbe46 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/README.md b/README.md index 299362a..d8b3d66 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,8 @@ # project_superchat Build a realtime multi-room chat application. Make it super. + +Richard Bell + +To run, clone repo +Install Redis +npm start diff --git a/app.js b/app.js new file mode 100644 index 0000000..a3ae481 --- /dev/null +++ b/app.js @@ -0,0 +1,77 @@ +var express = require('express'); +var path = require('path'); +var favicon = require('serve-favicon'); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); +var bodyParser = require('body-parser'); + +var index = require('./routes/index'); +var users = require('./routes/users'); + +var app = express(); +var server = require('http').Server(app); +var io = require('socket.io')(server); + +const dataMgr = require('./bin/dataMgr'); +// dispatcher for IO requests from client_sockets +io.on('connection', client => { + console.log("New connection!") + + client.on('addRoom', (roomName) => { + dataMgr.addRoom(roomName).then(function(){ + io.emit('newRoom', roomName); + }); + }); + + client.on('getMessages', (roomName) => { + dataMgr.getMessages(roomName).then(function(data){ + client.emit('messageList', data); + }) + }); + + client.on('sendMessage', (message) => { + dataMgr.sendMessage(message.room, message.user, message.text).then(function(data){ + io.emit('newMessage', {room: message.room, user: message.user, text: message.text}); + }) + }); +}); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'hbs'); + +// uncomment after placing your favicon in /public +//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); +app.use(function(req, res, next){ + res.io = io; + req.io = io; + next(); +}); +app.use(logger('dev')); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use('/', index); +app.use('/users', users); + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + var err = new Error('Not Found'); + err.status = 404; + next(err); +}); + +// error handler +app.use(function(err, req, res, next) { + // 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'); +}); + +module.exports = {app: app, server: server}; diff --git a/bin/dataMgr.js b/bin/dataMgr.js new file mode 100644 index 0000000..f7a6979 --- /dev/null +++ b/bin/dataMgr.js @@ -0,0 +1,153 @@ +const redisClient = require("redis").createClient(); +const debug = require('debug')('dataMgr'); +const ROOMKEY = "rooms"; +const USERKEY = "users"; + +// uncomment for clean-up +// redisClient.flushall(); + +// promise returns 1 for room add 0 for duplicate room +function addRoom(room) { + debug(`requesting add room "${room}"`); + return new Promise(function(resolve, reject) { + redisClient.sadd(ROOMKEY, room, function(err, data) { + if (err) reject(err); + debug(`room added "${room}"`); + resolve(data); + }); + }); +} + +// promise returns array with list of rooms +function listRooms() { + debug(`requesting room list`); + return new Promise(function(resolve, reject) { + redisClient.smembers(ROOMKEY, function(err, data) { + if (err) reject(err); + debug('returning room list'); + resolve(data); + }); + }); +} + +function addUser(userName, userProfile) { + debug(`adding new user "${userName}" with profile ${userProfile}`); + return new Promise(function(resolve, reject) { + redisClient.hset(USERKEY, userName, JSON.stringify(userProfile), function(err, data) { + if (err) reject(err); + debug(`user added "${userName}"`); + resolve(data); + }); + }); +} + +// promise returns object containing the profile associated with userName or Null object if doesn't exist +function getUser(userName) { + debug(`requesting user "${userName}"`); + return new Promise(function(resolve, reject) { + redisClient.hget(USERKEY, userName, function( err, data) { + if (err) reject(err); + debug(`user "${userName}" returned with profile ${data}`); + resolve(JSON.parse(data)); + }); + }); +} + +// promise returns 1 if user exists and 0 otherwise +function isUser(userName) { + debug(`checking user exists "${userName}"`); + return new Promise(function(resolve, reject) { + redisClient.hexists(USERKEY, userName, function (err, data) { + if (err) reject(err); + debug(`user "${userName}" hexist with response ${data}`); + resolve(data); + }) + }); +} + +// promise resolves to number for message added where number is count of messages in room +function sendMessage(roomName, userName, msgText) { + debug(`sending message "${msgText}" to room "${roomName}" from user "${userName}"`); + return new Promise(function(resolve, reject) { + let message = {text: msgText, + sender: userName, + sentTime: Date.now() }; + redisClient.rpush(roomName, JSON.stringify(message), function (err, data) { + if (err) reject(err); + debug(`sent "${msgText}" to "${roomName}" from "${userName}"`); + resolve(data); + }) + }); +} + +// promise resolves to array containing message objects +function getMessages(roomName) { + debug(`requesting messages for "${roomName}"`); + return new Promise(function(resolve, reject) { + redisClient.lrange(roomName, 0, -1, function(err,data) { + if (err) reject(err); + debug(`messages for room "${roomName}" are "${data}"`); + let msgList = []; + data.forEach(function(message) { + msgList.push(JSON.parse(message)); + }); + resolve(msgList); + }) + }); +} + +module.exports = { + addRoom, + listRooms, + addUser, + getUser, + isUser, + sendMessage, + getMessages +}; + + +/* +sendMessage('test room', 'fred', 'hello world').then(function(data){ + console.log(data); +}); +sendMessage('test room', 'george', 'Goodbye cruel world').then(function(data){ + console.log(data); +}); +getMessages('test room').then(function(data){ + console.log(data); +}); + +addUser('fred', {fullname: 'Fred Smith', createDate: Date.now()}).then(function(data){ + console.log(data); +}); + +getUser('fred').then(function(data){ + console.log(data); +}); + +getUser('george').then(function(data){ + console.log(data); +}); + +isUser('fred').then(function(data){ + console.log(data); +}); +isUser('george').then(function(data){ + console.log(data); +}); + +addRoom("test").then(function(data){ + console.log(data); +}); +addRoom("test1").then(function(data){ + console.log(data); +}); +addRoom("test1").then(function(data){ + console.log(data); +}); +listRooms().then(function(data){ + console.log(data); + process.exit(); +}); +*/ diff --git a/bin/www b/bin/www new file mode 100755 index 0000000..78f4759 --- /dev/null +++ b/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app').app; +var debug = require('debug')('project-superchat:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = require('../app').server; + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..84924f7 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "project-superchat", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "node ./bin/www" + }, + "dependencies": { + "body-parser": "~1.17.1", + "cookie-parser": "~1.4.3", + "debug": "^2.6.6", + "express": "^4.15.2", + "hbs": "~4.0.1", + "morgan": "~1.8.1", + "redis": "^2.7.1", + "serve-favicon": "~2.4.2", + "socket.io": "^2.0.1", + "socket.io-client": "^2.0.1" + } +} diff --git a/public/javascripts/client_sockets.js b/public/javascripts/client_sockets.js new file mode 100644 index 0000000..0aec87f --- /dev/null +++ b/public/javascripts/client_sockets.js @@ -0,0 +1,61 @@ +var socket = io.connect('http://localhost:3000') +var activeRoom = null; +var activeUser = $('.username').html(); + +console.log(activeUser); + +socket.on('updateCount', function(data) { + $('#' + data.shortURL + '-count').html(data.count) +}); + +socket.on('newRoom', function(data) { + $('#roomList').append('
  • ' + data + '
  • '); +}); + +socket.on('messageList', function(data) { + console.log(data); + if (data.length !== 0) { + data.forEach(function(entry) { + $('#messageList').append('
  • ' + entry.sender + ': ' + entry.text + '
  • '); + }); + }; +}); + +socket.on('newMessage', function(data) { + if (activeRoom === data.room) { + $('#messageList').append('
  • ' + data.user + ': ' + data.text + '
  • '); + } else { + let selector = '[name="' + data.room + '"]'; + $(selector).removeClass('btn-info'); + $(selector).addClass('btn-warning'); + } +}); + + +$('#addRoom').click(function() { + let roomName = $('#roomValue').val(); + socket.emit('addRoom', roomName); + $('#roomValue').val(''); +}); + +$('#sendMessage').click(function() { + let messageTxt = $('#messageToSend').val(); + if (activeRoom === null) { + alert("Select a room first!") + } else { + socket.emit('sendMessage', {user: activeUser, room: activeRoom, text: messageTxt}); + $('#messageToSend').val(''); + } + +}); + +$(document).on("click", '.room-button', function() { + let roomName = $(this).attr("name"); + activeRoom = roomName; + let selector = '[name="' + roomName + '"]'; + $(selector).removeClass('btn-warning'); + $(selector).addClass('btn-info'); + $('.message-detail').remove(); + socket.emit('getMessages', roomName); + $('#messageRoomName').html(': ' + roomName); +}); diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css new file mode 100644 index 0000000..8d79175 --- /dev/null +++ b/public/stylesheets/style.css @@ -0,0 +1,37 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +a { + color: #00B7FF; +} + +.navbar-padding { + padding-right: 20px; +} + +.row-spacing { + padding: 20px; +} + +.username { + color: cornflowerblue; + text-decoration: underline; +} + +.boxed { + border: 1px; + border-style: solid; + border-radius: 5px; + border-color: black; +} + +.padded { + padding: 5px; +} + +.button-format{ + float: right; + +} diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..43365d9 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,40 @@ +var express = require('express'); +var router = express.Router(); +const debug = require('debug')('users'); +const dataMgr = require('../bin/dataMgr'); + +/* GET home page. */ +router.get('/', function(req, res, next) { + + dataMgr.isUser(req.cookies.username).then(function(data) { + if (data) { + debug(`logged in with profile`); + let pList = []; + pList.push(dataMgr.getUser(req.cookies.username)); + pList.push(dataMgr.listRooms()); + Promise.all(pList).then(function(data) { + res.render('index', { + title: 'Super Chat', + username: req.cookies.username, + greeting: data[0].firstname + ' ' + data[0].lastname, + firstname: data[0].firstname, + lastname: data[0].lastname, + rooms: data[1] + }); + }) + } else { + res.clearCookie("username"); + res.render('index', { + title: 'Super Chat', + username: null, + greeting: null, + firstname: null, + lastname: null + }); + } + }); + + +}); + +module.exports = router; diff --git a/routes/users.js b/routes/users.js new file mode 100644 index 0000000..809258d --- /dev/null +++ b/routes/users.js @@ -0,0 +1,60 @@ +var express = require('express'); +var router = express.Router(); +const debug = require('debug')('users'); +const dataMgr = require('../bin/dataMgr'); + +/* user validation */ +router.post('/', function(req, res, next) { + // check that username in login form exists --> if it does set cookie and redirect to home + // otherwise show profile page to create user + debug(`looking for user`); + dataMgr.isUser(req.body.username).then(function(data){ + if (data === 1) { + debug(`${req.body.username} exists`); + res.cookie("username", req.body.username); + res.redirect("/"); + } else { + debug(`${req.body.username} does not exist`); + res.render('users', { title: 'Super Chat: Create Profile', + update: false, + greeting: 'New User', + username: req.body.username, + firstname: 'First Name', + lastname: 'Last Name', + createdDate: new Date(Date.now()) + }); + }; + }); +}); + +router.get('/profile/:username', function(req, res, next) { + debug(`display profile info for "${req.params.username}"`); + dataMgr.getUser(req.params.username).then(function(data) { + res.render('users', { title: 'Super Chat: User Profile', + update: true, + greeting: data.firstname, + username: req.params.username, + firstname: data.firstname, + lastname: data.lastname, + createdDate: new Date(data.created) }); + }); +}); + +router.post('/profile/:username', function(req, res, next) { + debug(`saving user profile for ${req.params.username}`); + let profile = {firstname: req.body.firstname, + lastname: req.body.lastname, + created: Date.now()}; + dataMgr.addUser(req.params.username, profile).then(function(data) { + res.cookie("username", req.params.username); + res.redirect("/"); + }); +}); + +router.get('/logout', function(req, res, next){ + debug(`loging user "${req.cookies.username}" out`); + res.clearCookie("username"); + res.redirect("/"); +}); + +module.exports = router; diff --git a/views/error.hbs b/views/error.hbs new file mode 100644 index 0000000..0659765 --- /dev/null +++ b/views/error.hbs @@ -0,0 +1,3 @@ +

    {{message}}

    +

    {{error.status}}

    +
    {{error.stack}}
    diff --git a/views/index.hbs b/views/index.hbs new file mode 100644 index 0000000..9ce2cce --- /dev/null +++ b/views/index.hbs @@ -0,0 +1,64 @@ +
    + +
    +{{#if username}} +
    +
    +
    +
    +

    Chat Rooms

    +
    +
    +
    + + +
    + +
    +
      + {{#each rooms}} +
    • {{this}}
    • + {{/each}} +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Messages

    +
    +
    +
      + +
    +
    + + +
    +
    +
    +
    +
    +

    Chat

    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + + + +{{else}} +

    Please sign in first!

    +{{/if}} diff --git a/views/layout.hbs b/views/layout.hbs new file mode 100644 index 0000000..c2afd1e --- /dev/null +++ b/views/layout.hbs @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + {{title}} + + + + + +
    + + {{{body}}} +
    + + + + + + + + + + + + + diff --git a/views/users.hbs b/views/users.hbs new file mode 100644 index 0000000..2d9ac69 --- /dev/null +++ b/views/users.hbs @@ -0,0 +1,56 @@ +
    +
    +

    Welcome, {{greeting}}!

    + {{#unless update}} +

    Please create a profile below!

    + {{else}} +

    Please update your info below!

    + {{/unless}} +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + + +
    +
    +
    + +
    + +
    +
    +
    + +
    + + + + +
    +
    + +