Monthly Archives: March 2018

Giving Day with Socket.IO

Introduction

In recent years college campuses have added Giving Day to entice donors to contribute to their alma maters, and Luther College is no exception. The first online campaign was in 2016, and the main goal that year was to integrate it within the Reason CMS. An improperly coded challenge that gave a higher total pledge was the only glitch, blamed on a mischievous gnome, and corrected quickly behind the scene.

But the application had one serious flaw—users had to refresh their browsers in order to get the latest running pledge totals. Instead what was needed was a secure websocket solution that pushed the information to the browser.

Reason, like many open-source content management products, runs on the LAMP platform. There was already a signed certificate issued by a third party certificate authority on our domain, but I was unable to get the PHP solutions available for websocket servers to work with secure connections. So I looked at socket.io, written in Javascript, that runs on node.js, and was able to construct a websocket server that allows pages served by https to connect.

The state diagram below shows how the socket.io server works with the rest of the Giving Day process:

giving-day-diagram

During the 18-hour window of Giving Day, when a person visits https://www.luther.edu/giving-day

  • the status of each challenge is calculated along with the current total raised
  • challenge totals and the total raised are written in JSON to a temporary file, which will be polled for changes every 15 seconds by the socket.io server
  • and finally a request is made as a socket.io client to securely connect to the socket.io server, ready to get new totals as they become available

The same temporary JSON files are also updated when a pledge form is submitted by calling the php function write_totals():

function write_totals()
// write updated totals to file so that socket io server can read files and broadcast updates to all connected clients
{
	file_put_contents("/var/tmp/giving-day-challenge-totals.txt", json_encode($this->challenge));
	file_put_contents("/var/tmp/giving-day-total.txt", $this->total_amount);
}

Installation and Implementation

If you want to try out a complete solution of this project on a virtual box of your own, please visit https://github.com/brifishjones/giving-day-socketio-server.

Ubuntu by default installs a very old version of nodejs, so get the lastest version instead:

curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -

See https://stackoverflow.com/questions/26020917/what-is-the-function-of-etc-apt-sources-list-d and https://stackoverflow.com/questions/28381359/probably-incorrect-release-file-causing-apt-get-update-to-fail if there are errors running the above command.

sudo apt-get install -y nodejs build-essential
nodejs --version
v8.9.4
npm --version
5.6.0

Create a directory for the socket.io server and create a new project:

sudo mkdir /etc/nodejs/giving-day
sudo cd /etc/nodejs/giving-day
sudo npm init

Name the package giving-day, and the entry point server.js then install the socket.io module and add it to package.json

sudo npm install --save socket.io

Create the server.js file:

#!/usr/bin/env node
'use strict';
const fs = require('fs');
const https = require('https');
const port = 4443;
const options = {
    key: fs.readFileSync('/etc/apache2/ssl/wildcard.key'),
    cert: fs.readFileSync('/etc/apache2/ssl/wildcard.crt'),
    ca: fs.readFileSync('/etc/apache2/ssl/gd_bundle-g2-g1.crt'), 
    requestCert: true,
    rejectUnauthorized: false 
};
var connections = [];
var app = https.createServer(options); 
var io = require('socket.io')(app);
var prev_total = 0;
var total = 0;
app.listen(port);
console.log('server started ...');
var timer = setInterval(function() {
  if (connections.length > 0) {
    fs.readFile('/var/tmp/giving-day-total.txt', 'ascii', function(err, content) {
      total = parseInt(content);
      if (total > 0 && total != prev_total) {
        console.log("%s: total amount = $%s", Date(), total.toString());
        io.sockets.emit('update-totals', {total: total.toString()});  
        fs.readFile('/var/tmp/giving-day-challenge-totals.txt', 'ascii', function(err, content) {
          var myjson = JSON.stringify(content);
          var obj = JSON.parse(content);
          io.sockets.emit('update-challenges', obj);  
        });
      }
      prev_total = total;
    });
  }
}, 15000);
io.on('connection', function (socket) {
  socket.once('disconnect', function() {
    connections.splice(connections.indexOf(socket), 1);
    console.log("%s: client id %s disconnected. (%s sockets remaining)", Date(), socket.id, connections.length);
    socket.disconnect();
  });
  connections.push(socket);
  console.log("%s: client id %s connected at ip %s. (%s sockets connected)", Date(), socket.id, socket.conn.remoteAddress, connections.length);
})

The client-side connection is quite simple since it is only receiving a simple emit from the socket.io server. In the Reason CMS module that displays the giving-day page first reference the socket.io javascript file:



Display the grand total:



Total Raised

$123,456

And the javascript that securely connects to the socket.io server and then updates the total when the socket.io server emits an update-totals message:


var socket = io(connecthost + ':4443', {secure: true});
socket.on('update-totals', function (data) {
	document.getElementsByClassName("money-raised")[0].innerHTML = "$" + data.total.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
});
giving-day-diagram

To make the giving day socket.io server into a service, add the following code to /etc/init/giving-day-socket-server.conf:

 

To start service:

 

To stop service:

 

The socket-server console.log messages will be saved in the log file: /var/log/node.js

The Final Results

On Giving Day the socket.IO server had at its peak about 250 connected clients, which were each updated with challenge progress during the course of the day.

giving-day-challenges

Viewers to the website could also access a live video feed of interviews with faculty, staff, students, and alumni.

giving-day-stream

Or could check out the latest social media posts using Tagboard.

giving-day-social