Assignment 3: Chatroom

Due: Friday, March 15, 2019 @ 11:59pm

This handout uses HTML and jQuery (or native JS) for the front end. If you would like to use React for the front end, refer to this handout

Introduction

beepbeep

After many unsuccessful attempts at curbing online profanity, the heads of Club Penguin could take it no longer. In an attempt to protect the innocence of young penguins everywhere, they came up with an infallible solution: remove the online chatting feature completely. But peace was short-lived, as the nefarious Herbert P. Bear took advantage of the penguins' inability to communicate to establish a dystopian hold over Club Penguin Island. Your job is to build a new chatroom so that the agents of Club Penguin HQ can once again work together to take down Herbert P. Bear.

Overview

In this assignment, we will utilize some popular Node modules in order to create a simple chatroom. We have seen some of these technologies before, such as Node and Express (Node’s own server-side framework), but we will also be using some new and exciting modules. None of the modules are required to use, but we make particular suggestions throughout this assignment for certain modules that we find particularly helpful. Among them are body-parser to parse data coming from a POST request, consolidate and hogan to generate web page templates efficiently, moment to handle dates and times, and mysql or mongoose to maintain a database for all of the chatroom’s messages. These are the modules the TA's find will be particularly useful for this project, but feel free to investigate and use other ones as well! For the database in particular, we encourage the use of either MySQL or MongoDB.

For this assignment, you will use HTTP methods (no web-sockets) to implement the messaging features. In the next assignment, you will enhance your chatroom with real-time capabilities (you will reuse your code, so good design now will pay off later). For this reason, you will receive a significant deduction if you use web-sockets in this assignment.

Requirements

Frontend

  1. Create a messaging application that communicates with your backend
  2. Allow users to create a new chatroom
  3. Have pages for the following routes

    • The homepage ("/") should be an interface that allows user(s) to see a list of existing chatrooms and be able to enter a new or existing chatroom, characterized by an ID generated by the server (e.g., ABC123).
    • Routes like ("/ABC123") should display the respective chatroom to the user
  4. Prompt user for nickname before they can post in a chatroom
  5. Any previous messages should be displayed to an entering user with most recent messages at the bottom
  6. When in a chatroom, periodically refresh messages (at least every 5 seconds)
  7. Each message should show the nickname, text, and datetime it was posted
  8. Allow users to post messages to a chatroom
  9. All DOM elements should be styled

Backend

  1. Run an Express web server listening on port 8080.
  2. Create API calls for routes (the design and naming is up to you as long as you can satisfy the other requirements, but below are some that you might have)

    • GET /: used for homepage, returns all created chatrooms
    • GET /:roomName: used when clicking into a room
    • POST /:roomName/create: creates a chatroom with roomName
    • GET /:roomName/messages: Returns all messages for a given chatroom
    • POST /:roomName/messages: Insert a new message that belongs to a chatroom in database
    As an example, our implementation has the server send the room.html template on the /:roomName route, and then the javascript for room.html makes a GET request which the server handles with app.get('/:roomname/messages', ...). Similarly, when a message is posted, the javascript for room.html sends a POST request, which the server handles with app.post('/:roomName/messages', ...)
  3. Each generated chatroom should be specified by a unique 6-character alphanumeric identifier.
  4. Queries that are secure (e.g. if using SQL, queries that are protected from SQL injections).

Database

  1. Chatrooms and messages should be stored in the database you use
  2. If you are using SQL (MySQL), it recommended you create two tables, one for chatrooms and one for messages

    • Each chatroom should have a unique id
    • Each message should have its own unique id, a chatroom id, nickname, message, and datetime posted
  3. If you are using NoSQL (MongoDB), it recommended you create only one collection that stores chatrooms and messages. This is based on best practices for NoSQL schema design. If you are curious, Amazon provides a nice, short explanation on the difference between SQL and NoSQL design here in their DynamoDB documentation. DynamoDB is a similar NoSQL database like MongoDB

    • Each chatroom should be a JSON object that has a unique id and an array of objects that represent messages
    • Each message should have a unique id, nickname, message, and datetime posted
  4. You are also welcome to use other databases such as PostgreSQL

Setup

Run cs132_install assignment3_dev to create an empty directory chatroom within /gpfs/data/course/cs1320/CS_LOGIN. As node modules can make the size of a project really large, TStaff kindly made these directories for you to work in. Make sure all of your work goes in this folder.

We will be asking you to use at least v10.15.0 of Node.js, which comes with v6.6.0 of npm. Make sure to look at the node pre-lab for info on how to use this Node version if you have not yet done so.

Please confirm that node --version outputs the right version before beginning this assignment!

Since this is a Node project, initialize the project by running:

npm init

After running this, a package.json file be created in your project directory. Once this is done, every time you need to install a new Node module, you can use the command npm install <module_name>. This will list the module as a dependency in the package.json file so that later on you or someone else can install all the required modules at once using the command npm install. This makes handing in the project and grading a lot quicker and easier!

We do not explicitly require you to use any specific dependencies for this project. That being said, you might want install express to get started:

npm install express

If anything goes wrong, it'll very clearly yell at you with an error message. If everything goes as planned, you should see a node_modules directory within the current project directory listing all the modules you've installed.

Now that all your dependencies are installed, create a new file called server.js, require Express in the file, setup an app, and tell it to listen on port 8080. When all is said and done, it should look something like this:

const express = require('express');
// you will probably need to require more dependencies here.
const app = express();

// your app's code here
// ... 

app.listen(8080);

Helpful Code References

In this section, you will find useful guides for getting started with implementing your Chatroom.

Databases

For guides on how to set up the databases with NodeJS, refer to the prelab here. If you are using MongoDB, you can use mongoose, and if you are using MySQL, you should write and query from the same database that was created for you in the prelab (yourLoginHere_db). You are also welcome to use any other resources for your database as long as you can achieve all requirements, such as SQLite3 and PostgreSQL (see Brown's service here).

Creating a schema and table

The schema for the database will include a message table that will probably be created using a statement that looks something like this:

CREATE TABLE message (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  room TEXT,
  nickname TEXT,
  body TEXT,
  time INTEGER
)

You are free to make modifications to it as you see fit, but this schema should be sufficient for the requirements of the project. Note that messages are organized by room, and when you create a new room, it should have a unique name that doesn't already exist. Be sure to handle this edge case, and generate a new identifier under those circumstances.

Parametrized Queries

It's very dangerous to put user-provided information directly into a SQL query, as that leaves you wide open to SQL injection attacks. For that reason, whenever you want to use user input in a query, you should use bound parameters or prepared statements. For example, the last section of the prelab shows you how to parametrize queries for MySQL.

(body-parser)

The body-parser module allows us to parse data that is sent to the server-side via a POST request. Recall that a POST request stores data within the HTTP request's body, so body-parser is simply a module that helps us recover that data.

To get started, include the following lines:

const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());

The first line is just telling Node to require the module. The second line tells your Express application to use the body-parser to parse the request body whenever the application receives POST requests. Now, whenever a POST request is made to the server, we can access the requests' body for its data parameters.

app.post('/:roomName/messages', saveMessage);

function saveMessage(request, response) {
  const name = request.params.roomName;   // 'ABC123'
  const nickname = request.body.nickname; // 'Herbert'
  const message = request.body.message;   // 'You cant stop me!'
  // ...
};

Notice here how we can also get the parameters from URLs in Express (like the roomName) using request.params

Templating

Note: If you don't want to rely on templating, check out the analagous guide (linked at top of this page) for implementing Chatroom with React.

When it comes time to send HTML back to the browser, you have a few options. You can compose the HTML by hand in your JavaScript, like so:

let html  = '<!DOCTYPE html>\n';
html += '<html>\n';
html += '<head>\n';
html += '    <title>Room: ' + roomName + '</title>\n';
// ...

... but why ever would you want to use that monstrosity? We recommend that you use a templating engine to process your HTML. Mustache is the templating language recommended by the TAs, and Hogan, is Twitter's node implementation of Mustache - which we can install as a module. Also useful would be the consolidate library, which is an adapter that lets various templating engines work directly with Express. Other templating options include, but are not limited to: handlebars, jade, ejs, and express-hogan.

If you'd like to use Hogan, start out by creating a directory called "templates" (or whatever you like, really) in your project directory, and then configure Express to render HTML files using Hogan:

const engines = require('consolidate');
app.engine('html', engines.hogan); // tell Express to run .html files through Hogan
app.set('views', __dirname + '/templates'); // tell Express where to find templates, in this case the '/templates' directory
app.set('view engine', 'html'); // register .html extension as template engine so we can render .html pages 

Then, inside your templates directory, create (e.g.) a room.html file. Write out your template using Mustache (see the Mustache website and the Mustache manual):

<!DOCTYPE html>
<html>
<head>
  <title>Room: {{roomName}}</title>
...

... and then use response.render(...) to serve it as a response:

app.get('/:roomName', function(request, response){
  // do any work you need to do, then
  response.render('room.html', {roomName: request.params.roomName});
});

Other Hints

Generating Room Identifiers

There are a number of strategies you could use to generate room identifiers. One straightforward one is this:

function generateRoomIdentifier() {
  // make a list of legal characters
  // we're intentionally excluding 0, O, I, and 1 for readability
  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';

  let result = '';
  for (let i = 0; i < 6; i++)
    result += chars[Math.floor(Math.random() * chars.length)];

  return result;
}

Remember that we need to make sure that all newly generated rooms are unique to the database. If you are curious about better random number generation, check out this npm package for strong random number generation.

Prompting the user for a nickname

Once in the chatroom, the user should be instructed to choose a nickname (feel free to use the prompt(...) function for that), but you are also welcome to be more creative!

Refreshing a message list

Recall from TwitterFeed how we were able to refresh the tweet being displayed every 5 seconds? We can use the same setInterval function in this project to refresh our messages in a chatroom. Setup a request (Ajax, Fetch, or other equivalent) to fire on a timer every 5 seconds or so to a URL that returns a JSON dictionary of messages. Express makes it easy to send JSON in a response, using response.json(...):

app.get('/:roomName/messages', function(request, response){
    // fetch all of the messages for this room 
    // below is a placeholder for the messages you need to fetch
    let messages = [{nickname: 'Herbert', body: 'Enjoy it before I destroy it!'}, ...];

    // encode the messages object as JSON and send it back
    response.json(messages);
});

response.json(messages) is equivalent to response.send(JSON.stringify(messages)).

The meta Tag Trick

If you use Ajax to refresh the messages list, and only send JSON back and forth between the client and server, your HTML rendering requirements should be fairly lightweight. For instance, it is likely that the only information you'll need to get to your room page is the name of your room. If you find yourself with a fairly small amount of variables that you need to expose, you might consider using the meta tag trick.

In your template, define meta tags that contain the values of your variables:

<!DOCTYPE html>
<html>
<head>
<meta name="roomName" content="{{roomName}}">
    ...

Then, when you need to access the variable from (client-side) JavaScript, access it via the DOM:

let meta = document.querySelector('meta[name=roomName]');
let roomName = meta.content;

If you do this a lot, you might consider writing a simple wrapper to get a defined meta tag:

function meta(name) {
    const tag = document.querySelector('meta[name=' + name + ']');
    if (tag != null)
        return tag.content;
    return '';
}

This way you can easily access the defined meta tag variable, roomName, like so:

let roomName = meta('roomName');

This is simply a clean and semantic way of dealing with configuration variables.

Posting Messages

You're going to have to provide some way for your users to post new messages, and one way to implement this is using forms. There are plenty of tutorials online, but as a simple example, a basic message form (in a Mustache template) might look something like this:

<form method="POST" action="/{{roomName}}/messages" id="messageForm">
    <input type="text" name="message"   id="messageField">
    <input type="hidden" name="nickname" id="nicknameField" value="">
    <input type="submit" value="Send">
</form>

The basic form element is the input element. The "text" type renders a text field the user can type into; the "submit" type renders a button that submits the form; and "hidden" fields do not display on the page, but allow you to send additional content back with the form data. We've included a nickname field here, which you should populate with information after your user picks a nickname:

$('#nicknameField').val() = 'Herbert';
// or with native JS
document.getElementByID('nicknameField').value = 'Herbert';
            

When you click submit, the browser will send a POST request to /ABC123/messages, and then take the user there. Remember we can avoid this behavior by using the preventDefault() method upon submitting the form.

Submitting a Form

The below example uses Ajax, but feel free to use whatever method you'd like, such as Fetch, axios, etc.

When the page finishes loading, attach a new submit handler to the message form. Here, we are attaching the sendMessage function as the submit handler:

$(document).ready(function(){
    const messageForm = $('#messageForm').submit(sendMessage);
});

Inside the handler, create a POST request, and include the parameters of your request as a string. Make sure to also call event.preventDefault() to prevent the form from actually submitting (and rendering a new page given the specified post request).

function sendMessage(event) {
    // prevent the page from redirecting
      event.preventDefault();

    // get the parameters
    const nickname = ... // get nickname 
    const message = ... // get message 

    // send it to the server
    $.post('/' + meta('roomName') + '/messages', {data for database..}, function(res){
        // you might want to add callback function that is executed post request success
    });
}

Extra Credit

Because the next assignment will be a direct extension of this one, all extra credit will be awarded when we grade assignment 4. This means that you should leave all extra-credit implementations in your handin for assignment 4, as we will only be grading for extra credit then. That being said, if you are interested in spicing up your chatroom (and we highly recommend it!) it might be good to start thinking about what sorts of things you could do sooner rather than later.

Extra credit may be awarded for any additional functionality on the home page or within a chatroom. Some ideas include maintaining a list of recently active chatrooms, or adding additional interaction capabilities (think stickers or images!) between users in a chatroom. Keep in mind that there is also a vast library of exciting npm modules out there, and any extra features that you can produce with them will be looked favorably upon. Having a server also gives you a lot of power! For example, you could provide recommendations or information based on a user's chats, or you could even censor messages coming in from your various users (we leave it up to you how and what content to censor on ;)).

We will also give extra credit for using connection pooling (check out this blog), and for storing/executing more complex queries with your database. Particularly aesthetic or compellingly designed chatrooms are also eligible for extra credit.

Please document any extra features in your README.

Handing In

Before handing in your project, make sure you've filled in your README.md file (you can use any format you like for your README, but Markdown is highly recommended).

To turn in run cs132_handin assignment3_dev in your chatroom directory.