Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Building a Real-Time Activity Stream on Cloud Foundry with Node.js, Redis and MongoDB--Part II #1

Open
wants to merge 2 commits into from

1 participant

Monica Wilkinson
Monica Wilkinson
Owner

This is the source code for the tutorial on building an Activity Stream Part 2 http://blog.cloudfoundry.com/2012/06/05/node-activity-streams-app-2/

Once you are done making these changes you can easily increase the number of instances

Example:

vmc instances node-express-start 4
Monica Wilkinson ciberch commented on the diff
package.json
@@ -6,6 +6,9 @@
"engines" : ["node"],
"repository" : { "type":"git", "url":"http://github.com/mape/node-express-boilerplate" },
"dependencies" : {
+ "activity-streams-mongoose": ">=0.0.11",
+ "mongoose": "",
+ "underscore": "",
Monica Wilkinson Owner
ciberch added a note

Run npm install after these changes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Monica Wilkinson ciberch commented on the diff
siteConfig.js
@@ -47,5 +50,11 @@ if (cf.cloud) {
settings.redisOptions.host = redisConfig.hostname;
settings.redisOptions.pass = redisConfig.password;
}
+
+ if (cf.mongodb['mongo-asms']) {
Monica Wilkinson Owner
ciberch added a note

Locally you need to start running mongo. On Cloud Foundry just do

vmc create-service mongodb mongo-asms
vmc bind-service mongo-asms node-express-start # Or whatever the name is of your app
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Monica Wilkinson ciberch commented on the diff
lib/socket-io-server.js
((91 lines not shown))
});
- client.on('disconnect', function() { console.log('disconnect'); });
+ client.on('disconnect', function() {
+ console.log('********* disconnect');
+ asmsServer.unsubscribe(desiredStream);
Monica Wilkinson Owner
ciberch added a note

Avoid memory leaks by unsubscribing from Redis

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Monica Wilkinson ciberch commented on the diff
public/js/jquery.client.js
((10 lines not shown))
- }
- $$('#bubble ul').prepend($li);
- $$('#bubble').scrollTop(98).stop().animate({
- 'scrollTop': '0'
- }, 500);
- setTimeout(function() {
- $li.remove();
- }, 5000);
-
- setTimeout(function() {
- socketIoClient.send('pong');
- }, 1000);
+ socketIoClient.on('message', function(json) {
+ var doc = JSON.parse(json);
+ if (doc) {
+ var msg = doc.actor.displayName + " " + doc.title + " " + doc.object.displayName;
Monica Wilkinson Owner
ciberch added a note

Now we have a full activity so we can use its properties to build a more meaningful message

On Part 3 we will see how to use jade templates server side and client side

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Monica Wilkinson ciberch commented on the diff
public/js/jquery.client.js
((34 lines not shown))
+ msg+= ": " + doc.object.content;
+ }
+
+ var $li = $('<li>').text(msg).append($('<img class="avatar">').attr('src', doc.actor.image.url));
+ if (doc.provider && doc.provider.icon && doc.provider.icon.url) {
+ $li.append($('<img class="service">').attr('src', doc.provider.icon.url));
+ }
+ $$('#stream ul').prepend($li);
+ $$('#bubble').scrollTop(98).stop().animate({
+ 'scrollTop': '0'
+ }, 5000);
+ setTimeout(function() {
+ $li.remove();
+ }, 5000);
+
+ if (doc.verb == "connect") {
Monica Wilkinson Owner
ciberch added a note

Doing something semi interesting with the different activities

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
115 lib/socket-io-server.js
View
@@ -1,6 +1,9 @@
module.exports = function Server(expressInstance, sessionStore) {
var parseCookie = require('connect').utils.parseCookie;
var io = require('socket.io').listen(expressInstance);
+ var asmsServer = expressInstance.asmsDB;
+ var thisApp = expressInstance.thisApp;
+ var thisInstance = expressInstance.thisInstance;
io.configure(function () {
io.set('log level', 0);
@@ -8,9 +11,9 @@ module.exports = function Server(expressInstance, sessionStore) {
io.set('authorization', function(handshakeData, ack) {
var cookies = parseCookie(handshakeData.headers.cookie);
- sessionStore.get(cookies['connect.sid'], function(err, sessionData) {
+ sessionStore.get(cookies[expressInstance.cookieName], function(err, sessionData) {
handshakeData.session = sessionData || {};
- handshakeData.sid = cookies['connect.sid']|| null;
+ handshakeData.sid = cookies[expressInstance.cookieName]|| null;
ack(err, err ? false : true);
});
});
@@ -18,20 +21,116 @@ module.exports = function Server(expressInstance, sessionStore) {
io.sockets.on('connection', function(client) {
var user = client.handshake.session.user ? client.handshake.session.user.name : 'UID: '+(client.handshake.session.uid || 'has no UID');
+ var desiredStream = "firehose";
+
+ if (client.handshake.session && client.handshake.session.desiredStream) {
+ desiredStream = client.handshake.session.desiredStream;
+ }
+
// Join user specific channel, this is good so content is send across user tabs.
client.join(client.handshake.sid);
- client.send('welcome: '+user);
- client.on('message', function(msg) {
- // Send back the message to the users room.
- io.sockets.in(client.handshake.sid).send('socket.io relay message "'+msg+'" from: '+ user +' @ '+new Date().toString().match(/[0-9]+:[0-9]+:[0-9]+/));
+ var avatarUrl = (client.handshake.session.auth && client.handshake.session.user && client.handshake.session.user.image) ? client.handshake.session.user.image : '/img/codercat-sm.jpg';
+ var currentUser = {displayName: user, image: {url: avatarUrl}};
+
+
+ console.log("Subscribing " + user);
+
+ asmsServer.subscribe(desiredStream, function(channel, json) {
+ client.send(json);
+ });
+
+ var cf_provider;
+ var provider = new asmsServer.ActivityObject({'displayName': 'The Internet', icon: {url: ''}});
+ if (client.handshake.session.auth) {
+ if (client.handshake.session.auth.github) {
+ provider.displayName = 'GitHub';
+ provider.icon.url = 'http://github.com/favicon.ico';
+ } else if (client.handshake.session.auth.facebook) {
+ provider.displayName = 'Facebook';
+ provider.icon = {url: 'http://facebook.com/favicon.ico'};
+ } else if (client.handshake.session.auth.twitter) {
+ provider.displayName = 'Twitter';
+ provider.icon = {url: 'http://twitter.com/favicon.ico'};
+ }
+ }
+ provider.save(function(err) {
+ if (err == null) {
+ var cf_provider = new asmsServer.ActivityObject({'displayName': 'Cloud Foundry', icon:{url: 'http://www.cloudfoundry.com/images/favicon.ico'}});
+ cf_provider.save(function(err) {
+ if (err == null) {
+ if (client.handshake.session && client.handshake.session.auth && client.handshake.session.user) {
+ var act = new asmsServer.Activity({
+ id: 1,
+ actor: currentUser,
+ verb: 'connect',
+ object: thisInstance,
+ target: thisApp,
+ title: "connected to",
+ provider: provider,
+ generator: cf_provider
+ });
+ asmsServer.publish(desiredStream, act);
+ } else {
+ console.log("We don't have a user name so don't raise an activity");
+ console.dir(client.handshake.session.user);
+ }
+
+ } else {
+ console.log("Got error publishing welcome message")
+ }
+ });
+ }
+ });
+
+
+
+ client.on('message', function(message) {
+ var actHash = {
+ actor: currentUser,
+ verb: 'post',
+ object: {objectType: "note", content: message, displayName: ""},
+ target: thisApp,
+ provider: provider,
+ generator: cf_provider
+ }
+
+ if (actHash.verb == "post") {
+ actHash.title = "posted a " + actHash.object.objectType;
+
+ }
+
+ var act = new asmsServer.Activity(actHash);
+ // Send back the message to the users room.
+ asmsServer.publish(desiredStream, act);
});
- client.on('disconnect', function() { console.log('disconnect'); });
+ client.on('disconnect', function() {
+ console.log('********* disconnect');
+ asmsServer.unsubscribe(desiredStream);
Monica Wilkinson Owner
ciberch added a note

Avoid memory leaks by unsubscribing from Redis

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ console.log("unsubscribed from firehose");
+
+ if (client.handshake.session.user && client.handshake.session.user.name) {
+ asmsServer.publish(desiredStream, new asmsServer.Activity({
+ actor: currentUser,
+ verb: 'disconnect',
+ object: thisInstance,
+ target: thisApp,
+ title: "disconnected from",
+ provider: provider,
+ generator: cf_provider
+ }));
+
+
+ } else {
+ console.log("User disconnected");
+ console.dir(client.handshake);
+ }
+ });
});
io.sockets.on('error', function(){ console.log(arguments); });
return io;
-};
+};
3  package.json
View
@@ -6,6 +6,9 @@
"engines" : ["node"],
"repository" : { "type":"git", "url":"http://github.com/mape/node-express-boilerplate" },
"dependencies" : {
+ "activity-streams-mongoose": ">=0.0.11",
+ "mongoose": "",
+ "underscore": "",
Monica Wilkinson Owner
ciberch added a note

Run npm install after these changes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
"cloudfoundry": ">=0.1.0",
"connect" : ">=1.6.0",
"connect-assetmanager" : ">=0.0.21",
14 public/css/client.css
View
@@ -50,7 +50,7 @@ strong {
height: 100%;
box-shadow: 0 0 5px rgba(0,0,0,0.6) inset;
}
-#bubble {
+#stream {
position: absolute;
top: 20px;
left: 155px;
@@ -58,10 +58,10 @@ strong {
overflow: hidden;
padding: 0 0 0 65px;
}
-#bubble ul {
+#stream ul {
height: 2000px;
}
-#bubble li {
+#stream li {
position:relative;
padding:15px;
margin:1em 0 3em 100px;
@@ -75,13 +75,13 @@ strong {
opacity: 0;
-vendor-transition: opacity 4s linear;
}
-#bubble .avatar {
+#stream .avatar {
position: absolute;
right: 100%;
top: 10px;
margin-right: 45px;
}
-#bubble .service {
+#stream .service {
position: absolute;
right: 100%;
top: -0px;
@@ -91,10 +91,10 @@ strong {
padding: 2px;
box-shadow: 0 0 5px #000;
}
-#bubble li:first-child {
+#stream li:first-child {
opacity: 1;
}
-#bubble li:after {
+#stream li:after {
content: "";
position: absolute;
bottom: -20px;
BIN  public/img/as-logo-sm.png
View
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/img/cf-process.jpg
View
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/img/codercat-sm.jpg
View
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 public/js/jquery.client.js
View
@@ -23,24 +23,43 @@
$$('#connected').addClass('on').find('strong').text('Online');
});
- var image = $.trim($('#image').val());
- var service = $.trim($('#service').val());
- socketIoClient.on('message', function(msg) {
- var $li = $('<li>').text(msg).append($('<img class="avatar">').attr('src', image));
- if (service) {
- $li.append($('<img class="service">').attr('src', service));
- }
- $$('#bubble ul').prepend($li);
- $$('#bubble').scrollTop(98).stop().animate({
- 'scrollTop': '0'
- }, 500);
- setTimeout(function() {
- $li.remove();
- }, 5000);
-
- setTimeout(function() {
- socketIoClient.send('pong');
- }, 1000);
+ socketIoClient.on('message', function(json) {
+ var doc = JSON.parse(json);
+ if (doc) {
+ var msg = doc.actor.displayName + " " + doc.title + " " + doc.object.displayName;
Monica Wilkinson Owner
ciberch added a note

Now we have a full activity so we can use its properties to build a more meaningful message

On Part 3 we will see how to use jade templates server side and client side

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ if (doc.target) {
+ msg+= " in " + doc.target.displayName;
+ }
+ if (doc.generator) {
+ msg+= " via " + doc.generator.displayName;
+ }
+
+ if (doc.object && doc.object.content) {
+ msg+= ": " + doc.object.content;
+ }
+
+ var $li = $('<li>').text(msg).append($('<img class="avatar">').attr('src', doc.actor.image.url));
+ if (doc.provider && doc.provider.icon && doc.provider.icon.url) {
+ $li.append($('<img class="service">').attr('src', doc.provider.icon.url));
+ }
+ $$('#stream ul').prepend($li);
+ $$('#bubble').scrollTop(98).stop().animate({
+ 'scrollTop': '0'
+ }, 5000);
+ setTimeout(function() {
+ $li.remove();
+ }, 5000);
+
+ if (doc.verb == "connect") {
Monica Wilkinson Owner
ciberch added a note

Doing something semi interesting with the different activities

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ setTimeout(function() {
+ socketIoClient.send('Ok great news !');
+ }, 1000);
+ } else {
+ setTimeout(function() {
+ socketIoClient.send('I am still here');
+ }, 10000);
+ }
+ }
});
socketIoClient.on('disconnect', function() {
72 server.js
View
@@ -1,5 +1,7 @@
// Fetch the site configuration
var siteConf = require('./lib/getConfig');
+var cf = require('cloudfoundry');
+var _ = require('underscore')._;
process.title = siteConf.uri.replace(/http:\/\/(www)?/, '');
@@ -21,12 +23,47 @@ var assetHandler = require('connect-assetmanager-handlers');
var notifoMiddleware = require('connect-notifo');
var DummyHelper = require('./lib/dummy-helper');
+var mongoose = require('mongoose');
+mongoose.connect(siteConf.mongoUrl);
+
// Session store
var RedisStore = require('connect-redis')(express);
var sessionStore = new RedisStore(siteConf.redisOptions);
+var asmsDB = require('activity-streams-mongoose')(mongoose, {full: false, redis: siteConf.redisOptions, defaultActor: '/img/default.png'});
+
+var thisApp = new asmsDB.ActivityObject({displayName: 'Activity Streams App', url: siteConf.uri, image:{url: '/img/as-logo-sm.png'}});
+var thisInstance = {displayName: "Instance 0 -- Local"};
+if (cf.app) {
+ thisInstance.image = {url: '/img/cf-process.jpg'};
+ thisInstance.url = "http://" + cf.host + ":" + cf.port;
+ thisInstance.displayName = "App Instance " + cf.app['instance_index'] + " at " + thisInstance.url;
+ thisInstance.content = cf.app['instance_id']
+}
+
+thisApp.save(function (err) {
+ if (err === null) {
+ var startAct = new asmsDB.Activity(
+ {
+ actor: {displayName: siteConf.user_email, image:{url: "img/me.jpg"}},
+ verb: 'start',
+ object: thisInstance,
+ target: thisApp,
+ title: "started"
+ });
+
+ asmsDB.publish('firehose', startAct);
+ }
+});
+
var app = module.exports = express.createServer();
app.listen(siteConf.internal_port, null);
+app.asmsDB = asmsDB;
+app.siteConf = siteConf;
+app.thisApp = thisApp;
+app.thisInstance = thisInstance;
+app.cookieName = "jsessionid"; //Use this name to get sticky sessions. Default connect name is 'connect.sid';
+// Cookie name must be lowercase
// Setup socket.io server
var socketIo = new require('./lib/socket-io-server.js')(app, sessionStore);
@@ -97,7 +134,8 @@ app.configure(function() {
app.use(express.cookieParser());
app.use(assetsMiddleware);
app.use(express.session({
- 'store': sessionStore
+ 'key': app.cookieName
+ , 'store': sessionStore
, 'secret': siteConf.sessionSecret
}));
app.use(express.logger({format: ':response-time ms - :date - :req[x-real-ip] - :method :url :user-agent / :referrer'}));
@@ -169,18 +207,32 @@ function NotFound(msg){
Error.captureStackTrace(this, arguments.callee);
}
-// Routing
-app.all('/', function(req, res) {
- // Set example session uid for use with socket.io.
+function loadUser(req, res, next) {
if (!req.session.uid) {
req.session.uid = (0 | Math.random()*1000000);
- }
- res.locals({
- 'key': 'value'
- });
- res.render('index');
+ } else if (req.session.auth){
+ if (req.session.auth.github)
+ req.providerFavicon = '//github.com/favicon.ico';
+ else if (req.session.auth.twitter)
+ req.providerFavicon = '//twitter.com/favicon.ico';
+ else if (req.session.auth.facebook)
+ req.providerFavicon = '//facebook.com/favicon.ico';
+ }
+ var displayName = req.session.user ? req.session.user.name : 'UID: '+(req.session.uid || 'has no UID');
+ var avatarUrl = ((req.session.auth && req.session.user.image) ? req.session.user.image : '/img/codercat-sm.jpg');
+ req.user = {displayName: displayName, image: {url: avatarUrl}};
+ next();
+}
+
+// Routing
+app.get('/', loadUser, function(req, res) {
+ res.render('index', {
+ currentUser: req.user,
+ providerFavicon: req.providerFavicon,
+ });
});
+
// Initiate this after all other routing is done, otherwise wildcard will go crazy.
var dummyHelpers = new DummyHelper(app);
@@ -189,4 +241,4 @@ app.all('*', function(req, res){
throw new NotFound;
});
-console.log('Running in '+(process.env.NODE_ENV || 'development')+' mode @ '+siteConf.uri);
+console.log('Running in '+(process.env.NODE_ENV || 'development')+' mode @ '+siteConf.uri);
17 siteConfig.js
View
@@ -1,11 +1,14 @@
var cf = require('cloudfoundry');
var settings = {
- 'sessionSecret': 'sessionSecret-238273283abs'
- , 'internal_host' : '127.0.0.1'
- , 'internal_port' : 8080
+ 'user_email' : 'mwilkinson@vmware.com',
+ 'sessionSecret': 'sessionSecret'
+ , 'internal_host' : '127.0.0.1'
+ , 'internal_port' : 8080
, 'port': 8080
, 'uri': 'http://moni-air.local:8080' // Without trailing /
- , 'redisOptions': {host: '127.0.0.1', port: 6379}
+ , 'redisOptions': {host: '127.0.0.1', port: 6379}
+ , 'mongoUrl': 'mongodb://localhost/mongodb-asms'
+ // You can add multiple recipients for notifo notifications
, 'notifoAuth': null /*[
{
'username': ''
@@ -47,5 +50,11 @@ if (cf.cloud) {
settings.redisOptions.host = redisConfig.hostname;
settings.redisOptions.pass = redisConfig.password;
}
+
+ if (cf.mongodb['mongo-asms']) {
Monica Wilkinson Owner
ciberch added a note

Locally you need to start running mongo. On Cloud Foundry just do

vmc create-service mongodb mongo-asms
vmc bind-service mongo-asms node-express-start # Or whatever the name is of your app
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ var cfg = cf.mongodb['mongo-asms'].credentials;
+ settings.mongoUrl = ["mongodb://", cfg.username, ":", cfg.password, "@", cfg.hostname, ":", cfg.port,"/" + cfg.db].join('');
+ }
+ settings.user_email = cf.app['users'][0];
}
module.exports = settings;
8 views/index.ejs
View
@@ -4,8 +4,12 @@
<%= (session.auth && session.auth.twitter) ? '//twitter.com/favicon.ico' : '' %>
<%= (session.auth && session.auth.facebook) ? '//facebook.com/favicon.ico' : '' %>
">
-<div id="bubble">
- <ul></ul>
+<div id="stream">
+ <ul>
+ <% if(!session.auth) {%>
+ <li>Log In to Send Messages</li>
+ <% } %>
+ </ul>
</div>
<div id="connected">
<strong>Online</strong>
Something went wrong with that request. Please try again.