Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
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

@ciberch
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
@ciberch 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": "",
@ciberch 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
@ciberch 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']) {
@ciberch 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
@ciberch 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);
@ciberch 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
@ciberch 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;
@ciberch 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
@ciberch 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") {
@ciberch 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.
View
115 lib/socket-io-server.js
@@ -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);
@ciberch 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;
-};
+};
View
3  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": "",
@ciberch 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",
View
14 public/css/client.css
@@ -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;
View
BIN  public/img/as-logo-sm.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/img/cf-process.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/img/codercat-sm.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
55 public/js/jquery.client.js
@@ -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;
@ciberch 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") {
@ciberch 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() {
View
72 server.js
@@ -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);
View
17 siteConfig.js
@@ -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']) {
@ciberch 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;
View
8 views/index.ejs
@@ -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.