Skip to content
This repository

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 June 05, 2012
package.json
@@ -6,6 +6,9 @@
6 6
   "engines" : ["node"],
7 7
   "repository" : { "type":"git", "url":"http://github.com/mape/node-express-boilerplate" },
8 8
   "dependencies" : {
  9
+	  "activity-streams-mongoose": ">=0.0.11",
  10
+	  "mongoose": "",
  11
+	  "underscore": "",
1
Monica Wilkinson Owner
ciberch added a note June 05, 2012

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 June 05, 2012
siteConfig.js
@@ -47,5 +50,11 @@ if (cf.cloud) {
47 50
         settings.redisOptions.host = redisConfig.hostname;
48 51
         settings.redisOptions.pass = redisConfig.password;
49 52
     }
  53
+
  54
+    if (cf.mongodb['mongo-asms']) {
1
Monica Wilkinson Owner
ciberch added a note June 05, 2012

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 June 05, 2012
lib/socket-io-server.js
((91 lines not shown))
29 107
 		});
30 108
 
31  
-		client.on('disconnect', function() { console.log('disconnect'); });
  109
+		client.on('disconnect', function() {
  110
+				console.log('********* disconnect');
  111
+				asmsServer.unsubscribe(desiredStream);
1
Monica Wilkinson Owner
ciberch added a note June 05, 2012

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 June 05, 2012
public/js/jquery.client.js
((10 lines not shown))
32  
-		}
33  
-		$$('#bubble ul').prepend($li);
34  
-		$$('#bubble').scrollTop(98).stop().animate({
35  
-			'scrollTop': '0'
36  
-		}, 500);
37  
-		setTimeout(function() {
38  
-			$li.remove();
39  
-		}, 5000);
40  
-
41  
-		setTimeout(function() {
42  
-			socketIoClient.send('pong');
43  
-		}, 1000);
  26
+	socketIoClient.on('message', function(json) {
  27
+    var doc = JSON.parse(json);
  28
+    if (doc) {
  29
+        var msg = doc.actor.displayName + " " + doc.title + " " + doc.object.displayName;
1
Monica Wilkinson Owner
ciberch added a note June 05, 2012

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 June 05, 2012
public/js/jquery.client.js
((34 lines not shown))
  38
+            msg+= ": " + doc.object.content;
  39
+        }
  40
+
  41
+        var $li = $('<li>').text(msg).append($('<img class="avatar">').attr('src', doc.actor.image.url));
  42
+        if (doc.provider && doc.provider.icon && doc.provider.icon.url) {
  43
+            $li.append($('<img class="service">').attr('src', doc.provider.icon.url));
  44
+        }
  45
+        $$('#stream ul').prepend($li);
  46
+        $$('#bubble').scrollTop(98).stop().animate({
  47
+        			'scrollTop': '0'
  48
+        		}, 5000);
  49
+        setTimeout(function() {
  50
+        			$li.remove();
  51
+        		}, 5000);
  52
+
  53
+        if (doc.verb == "connect") {
1
Monica Wilkinson Owner
ciberch added a note June 05, 2012

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
... ...
@@ -1,6 +1,9 @@
1 1
 module.exports = function Server(expressInstance, sessionStore) {
2 2
 	var parseCookie = require('connect').utils.parseCookie;
3 3
 	var io = require('socket.io').listen(expressInstance);
  4
+    var asmsServer = expressInstance.asmsDB;
  5
+    var thisApp = expressInstance.thisApp;
  6
+    var thisInstance = expressInstance.thisInstance;
4 7
 
5 8
 	io.configure(function () {
6 9
 		io.set('log level', 0);
@@ -8,9 +11,9 @@ module.exports = function Server(expressInstance, sessionStore) {
8 11
 
9 12
 	io.set('authorization', function(handshakeData, ack) {
10 13
 		var cookies = parseCookie(handshakeData.headers.cookie);
11  
-		sessionStore.get(cookies['connect.sid'], function(err, sessionData) {
  14
+		sessionStore.get(cookies[expressInstance.cookieName], function(err, sessionData) {
12 15
 			handshakeData.session = sessionData || {};
13  
-			handshakeData.sid = cookies['connect.sid']|| null;
  16
+			handshakeData.sid = cookies[expressInstance.cookieName]|| null;
14 17
 			ack(err, err ? false : true);
15 18
 		});
16 19
 	});
@@ -18,20 +21,116 @@ module.exports = function Server(expressInstance, sessionStore) {
18 21
 	io.sockets.on('connection', function(client) {
19 22
 		var user = client.handshake.session.user ? client.handshake.session.user.name : 'UID: '+(client.handshake.session.uid || 'has no UID');
20 23
 
  24
+        var desiredStream = "firehose";
  25
+
  26
+        if (client.handshake.session && client.handshake.session.desiredStream) {
  27
+            desiredStream = client.handshake.session.desiredStream;
  28
+        }
  29
+
21 30
 		// Join user specific channel, this is good so content is send across user tabs.
22 31
 		client.join(client.handshake.sid);
23 32
 
24  
-		client.send('welcome: '+user);
25 33
 
26  
-		client.on('message', function(msg) {
27  
-			// Send back the message to the users room.
28  
-			io.sockets.in(client.handshake.sid).send('socket.io relay message "'+msg+'" from: '+ user +' @ '+new Date().toString().match(/[0-9]+:[0-9]+:[0-9]+/));
  34
+        var avatarUrl = (client.handshake.session.auth && client.handshake.session.user && client.handshake.session.user.image) ? client.handshake.session.user.image : '/img/codercat-sm.jpg';
  35
+        var currentUser = {displayName: user, image: {url: avatarUrl}};
  36
+
  37
+
  38
+        console.log("Subscribing " + user);
  39
+
  40
+        asmsServer.subscribe(desiredStream,  function(channel, json) {
  41
+            client.send(json);
  42
+        });
  43
+
  44
+        var cf_provider;
  45
+        var provider = new asmsServer.ActivityObject({'displayName': 'The Internet', icon: {url: ''}});
  46
+        if (client.handshake.session.auth) {
  47
+            if (client.handshake.session.auth.github) {
  48
+                provider.displayName = 'GitHub';
  49
+                provider.icon.url = 'http://github.com/favicon.ico';
  50
+            } else if (client.handshake.session.auth.facebook) {
  51
+                provider.displayName = 'Facebook';
  52
+                provider.icon = {url: 'http://facebook.com/favicon.ico'};
  53
+            } else if (client.handshake.session.auth.twitter) {
  54
+                provider.displayName = 'Twitter';
  55
+                provider.icon = {url: 'http://twitter.com/favicon.ico'};
  56
+            }
  57
+        }
  58
+        provider.save(function(err) {
  59
+            if (err == null) {
  60
+                var cf_provider = new asmsServer.ActivityObject({'displayName': 'Cloud Foundry', icon:{url: 'http://www.cloudfoundry.com/images/favicon.ico'}});
  61
+                   cf_provider.save(function(err) {
  62
+                       if (err == null) {
  63
+                           if (client.handshake.session && client.handshake.session.auth &&  client.handshake.session.user) {
  64
+                               var act = new asmsServer.Activity({
  65
+                                       id: 1,
  66
+                                       actor: currentUser,
  67
+                                       verb: 'connect',
  68
+                                       object: thisInstance,
  69
+                                       target: thisApp,
  70
+                                       title: "connected to",
  71
+                                       provider: provider,
  72
+                                       generator: cf_provider
  73
+                                   });
  74
+                               asmsServer.publish(desiredStream, act);
  75
+                           } else {
  76
+                               console.log("We don't have a user name so don't raise an activity");
  77
+                               console.dir(client.handshake.session.user);
  78
+                           }
  79
+
  80
+                       } else {
  81
+                           console.log("Got error publishing welcome message")
  82
+                       }
  83
+                   });
  84
+            }
  85
+        });
  86
+
  87
+
  88
+
  89
+		client.on('message', function(message) {
  90
+            var actHash = {
  91
+                actor: currentUser,
  92
+                verb: 'post',
  93
+                object: {objectType: "note", content: message, displayName: ""},
  94
+                target: thisApp,
  95
+                provider: provider,
  96
+                generator: cf_provider
  97
+            }
  98
+
  99
+            if (actHash.verb == "post") {
  100
+                actHash.title = "posted a " + actHash.object.objectType;
  101
+
  102
+            }
  103
+
  104
+            var act = new asmsServer.Activity(actHash);
  105
+            // Send back the message to the users room.
  106
+            asmsServer.publish(desiredStream, act);
29 107
 		});
30 108
 
31  
-		client.on('disconnect', function() { console.log('disconnect'); });
  109
+		client.on('disconnect', function() {
  110
+				console.log('********* disconnect');
  111
+				asmsServer.unsubscribe(desiredStream);
  112
+				console.log("unsubscribed from firehose");
  113
+
  114
+				if (client.handshake.session.user && client.handshake.session.user.name) {
  115
+						asmsServer.publish(desiredStream, new asmsServer.Activity({
  116
+								actor: currentUser,
  117
+								verb: 'disconnect',
  118
+								object: thisInstance,
  119
+								target: thisApp,
  120
+								title: "disconnected from",
  121
+								provider: provider,
  122
+								generator: cf_provider
  123
+						}));
  124
+
  125
+
  126
+				} else {
  127
+						console.log("User disconnected");
  128
+						console.dir(client.handshake);
  129
+				}
  130
+		});
32 131
 	});
33 132
 
34 133
 	io.sockets.on('error', function(){ console.log(arguments); });
35 134
 
36 135
 	return io;
37  
-};
  136
+};
3  package.json
@@ -6,6 +6,9 @@
6 6
   "engines" : ["node"],
7 7
   "repository" : { "type":"git", "url":"http://github.com/mape/node-express-boilerplate" },
8 8
   "dependencies" : {
  9
+	  "activity-streams-mongoose": ">=0.0.11",
  10
+	  "mongoose": "",
  11
+	  "underscore": "",
9 12
     "cloudfoundry": ">=0.1.0",
10 13
     "connect" : ">=1.6.0",
11 14
     "connect-assetmanager" : ">=0.0.21",
14  public/css/client.css
@@ -50,7 +50,7 @@ strong {
50 50
 	height: 100%;
51 51
 	box-shadow: 0 0 5px rgba(0,0,0,0.6) inset;
52 52
 }
53  
-#bubble {
  53
+#stream {
54 54
 	position: absolute;
55 55
 	top: 20px;
56 56
 	left: 155px;
@@ -58,10 +58,10 @@ strong {
58 58
 	overflow: hidden;
59 59
 	padding: 0 0 0 65px;
60 60
 }
61  
-#bubble ul {
  61
+#stream ul {
62 62
 	height: 2000px;
63 63
 }
64  
-#bubble li {
  64
+#stream li {
65 65
 	position:relative;
66 66
 	padding:15px;
67 67
 	margin:1em 0 3em 100px;
@@ -75,13 +75,13 @@ strong {
75 75
 	opacity: 0;
76 76
 	-vendor-transition: opacity 4s linear;
77 77
 }
78  
-#bubble .avatar {
  78
+#stream .avatar {
79 79
 	position: absolute;
80 80
 	right: 100%;
81 81
 	top: 10px;
82 82
 	margin-right: 45px;
83 83
 }
84  
-#bubble .service {
  84
+#stream .service {
85 85
 	position: absolute;
86 86
 	right: 100%;
87 87
 	top: -0px;
@@ -91,10 +91,10 @@ strong {
91 91
 	padding: 2px;
92 92
 	box-shadow: 0 0 5px #000;
93 93
 }
94  
-#bubble li:first-child {
  94
+#stream li:first-child {
95 95
 	opacity: 1;
96 96
 }
97  
-#bubble li:after {
  97
+#stream li:after {
98 98
 	content: "";
99 99
 	position: absolute;
100 100
 	bottom: -20px;
BIN  public/img/as-logo-sm.png
BIN  public/img/cf-process.jpg
BIN  public/img/codercat-sm.jpg
55  public/js/jquery.client.js
@@ -23,24 +23,43 @@
23 23
 		$$('#connected').addClass('on').find('strong').text('Online');
24 24
 	});
25 25
 
26  
-	var image = $.trim($('#image').val());
27  
-	var service = $.trim($('#service').val());
28  
-	socketIoClient.on('message', function(msg) {
29  
-		var $li = $('<li>').text(msg).append($('<img class="avatar">').attr('src', image));
30  
-		if (service) {
31  
-			$li.append($('<img class="service">').attr('src', service));
32  
-		}
33  
-		$$('#bubble ul').prepend($li);
34  
-		$$('#bubble').scrollTop(98).stop().animate({
35  
-			'scrollTop': '0'
36  
-		}, 500);
37  
-		setTimeout(function() {
38  
-			$li.remove();
39  
-		}, 5000);
40  
-
41  
-		setTimeout(function() {
42  
-			socketIoClient.send('pong');
43  
-		}, 1000);
  26
+	socketIoClient.on('message', function(json) {
  27
+    var doc = JSON.parse(json);
  28
+    if (doc) {
  29
+        var msg = doc.actor.displayName + " " + doc.title + " " + doc.object.displayName;
  30
+        if (doc.target) {
  31
+          msg+= " in " + doc.target.displayName;
  32
+        }
  33
+        if (doc.generator) {
  34
+          msg+= " via " + doc.generator.displayName;
  35
+        }
  36
+
  37
+        if (doc.object && doc.object.content) {
  38
+            msg+= ": " + doc.object.content;
  39
+        }
  40
+
  41
+        var $li = $('<li>').text(msg).append($('<img class="avatar">').attr('src', doc.actor.image.url));
  42
+        if (doc.provider && doc.provider.icon && doc.provider.icon.url) {
  43
+            $li.append($('<img class="service">').attr('src', doc.provider.icon.url));
  44
+        }
  45
+        $$('#stream ul').prepend($li);
  46
+        $$('#bubble').scrollTop(98).stop().animate({
  47
+        			'scrollTop': '0'
  48
+        		}, 5000);
  49
+        setTimeout(function() {
  50
+        			$li.remove();
  51
+        		}, 5000);
  52
+
  53
+        if (doc.verb == "connect") {
  54
+            setTimeout(function() {
  55
+                socketIoClient.send('Ok great news !');
  56
+            }, 1000);
  57
+        } else {
  58
+            setTimeout(function() {
  59
+                socketIoClient.send('I am still here');
  60
+            }, 10000);
  61
+        }
  62
+    }
44 63
 	});
45 64
 
46 65
 	socketIoClient.on('disconnect', function() {
72  server.js
... ...
@@ -1,5 +1,7 @@
1 1
 // Fetch the site configuration
2 2
 var siteConf = require('./lib/getConfig');
  3
+var cf = require('cloudfoundry');
  4
+var _ = require('underscore')._;
3 5
 
4 6
 process.title = siteConf.uri.replace(/http:\/\/(www)?/, '');
5 7
 
@@ -21,12 +23,47 @@ var assetHandler = require('connect-assetmanager-handlers');
21 23
 var notifoMiddleware = require('connect-notifo');
22 24
 var DummyHelper = require('./lib/dummy-helper');
23 25
 
  26
+var mongoose = require('mongoose');
  27
+mongoose.connect(siteConf.mongoUrl);
  28
+
24 29
 // Session store
25 30
 var RedisStore = require('connect-redis')(express);
26 31
 var sessionStore = new RedisStore(siteConf.redisOptions);
27 32
 
  33
+var asmsDB = require('activity-streams-mongoose')(mongoose, {full: false, redis: siteConf.redisOptions, defaultActor: '/img/default.png'});
  34
+
  35
+var thisApp = new asmsDB.ActivityObject({displayName: 'Activity Streams App', url: siteConf.uri, image:{url: '/img/as-logo-sm.png'}});
  36
+var thisInstance = {displayName: "Instance 0 -- Local"};
  37
+if (cf.app) {
  38
+    thisInstance.image = {url: '/img/cf-process.jpg'};
  39
+    thisInstance.url = "http://" + cf.host + ":" + cf.port;
  40
+    thisInstance.displayName = "App Instance " + cf.app['instance_index'] + " at " + thisInstance.url;
  41
+    thisInstance.content = cf.app['instance_id']
  42
+}
  43
+
  44
+thisApp.save(function (err) {
  45
+    if (err === null) {
  46
+        var startAct = new asmsDB.Activity(
  47
+            {
  48
+            actor: {displayName: siteConf.user_email, image:{url: "img/me.jpg"}},
  49
+            verb: 'start',
  50
+            object: thisInstance,
  51
+            target: thisApp,
  52
+            title: "started"
  53
+            });
  54
+
  55
+        asmsDB.publish('firehose', startAct);
  56
+    }
  57
+});
  58
+
28 59
 var app = module.exports = express.createServer();
29 60
 app.listen(siteConf.internal_port, null);
  61
+app.asmsDB = asmsDB;
  62
+app.siteConf = siteConf;
  63
+app.thisApp = thisApp;
  64
+app.thisInstance = thisInstance;
  65
+app.cookieName = "jsessionid"; //Use this name to get sticky sessions. Default connect name is 'connect.sid';
  66
+// Cookie name must be lowercase
30 67
 
31 68
 // Setup socket.io server
32 69
 var socketIo = new require('./lib/socket-io-server.js')(app, sessionStore);
@@ -97,7 +134,8 @@ app.configure(function() {
97 134
 	app.use(express.cookieParser());
98 135
 	app.use(assetsMiddleware);
99 136
 	app.use(express.session({
100  
-		'store': sessionStore
  137
+        'key': app.cookieName
  138
+		, 'store': sessionStore
101 139
 		, 'secret': siteConf.sessionSecret
102 140
 	}));
103 141
 	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){
169 207
 	Error.captureStackTrace(this, arguments.callee);
170 208
 }
171 209
 
172  
-// Routing
173  
-app.all('/', function(req, res) {
174  
-	// Set example session uid for use with socket.io.
  210
+function loadUser(req, res, next) {
175 211
 	if (!req.session.uid) {
176 212
 		req.session.uid = (0 | Math.random()*1000000);
177  
-	}
178  
-	res.locals({
179  
-		'key': 'value'
180  
-	});
181  
-	res.render('index');
  213
+	} else if (req.session.auth){
  214
+       if (req.session.auth.github)
  215
+        req.providerFavicon = '//github.com/favicon.ico';
  216
+       else if (req.session.auth.twitter)
  217
+        req.providerFavicon = '//twitter.com/favicon.ico';
  218
+       else if (req.session.auth.facebook)
  219
+        req.providerFavicon = '//facebook.com/favicon.ico';
  220
+    }
  221
+    var displayName = req.session.user ? req.session.user.name : 'UID: '+(req.session.uid || 'has no UID');
  222
+    var avatarUrl = ((req.session.auth && req.session.user.image) ? req.session.user.image : '/img/codercat-sm.jpg');
  223
+    req.user = {displayName: displayName, image: {url: avatarUrl}};
  224
+    next();
  225
+}
  226
+
  227
+// Routing
  228
+app.get('/', loadUser, function(req, res) {
  229
+    res.render('index', {
  230
+				currentUser: req.user,
  231
+				providerFavicon: req.providerFavicon,
  232
+		});
182 233
 });
183 234
 
  235
+
184 236
 // Initiate this after all other routing is done, otherwise wildcard will go crazy.
185 237
 var dummyHelpers = new DummyHelper(app);
186 238
 
@@ -189,4 +241,4 @@ app.all('*', function(req, res){
189 241
 	throw new NotFound;
190 242
 });
191 243
 
192  
-console.log('Running in '+(process.env.NODE_ENV || 'development')+' mode @ '+siteConf.uri);
  244
+console.log('Running in '+(process.env.NODE_ENV || 'development')+' mode @ '+siteConf.uri);
17  siteConfig.js
... ...
@@ -1,11 +1,14 @@
1 1
 var cf = require('cloudfoundry');
2 2
 var settings = {
3  
-  'sessionSecret': 'sessionSecret-238273283abs'
4  
-  , 'internal_host' : '127.0.0.1'
5  
-  , 'internal_port' : 8080
  3
+    'user_email' : 'mwilkinson@vmware.com',
  4
+	'sessionSecret': 'sessionSecret'
  5
+    , 'internal_host' : '127.0.0.1'
  6
+    , 'internal_port' : 8080
6 7
 	, 'port': 8080
7 8
 	, 'uri': 'http://moni-air.local:8080' // Without trailing /
8  
-  , 'redisOptions': {host: '127.0.0.1', port: 6379}
  9
+    , 'redisOptions': {host: '127.0.0.1', port: 6379}
  10
+    , 'mongoUrl': 'mongodb://localhost/mongodb-asms'
  11
+	// You can add multiple recipients for notifo notifications
9 12
 	, 'notifoAuth': null /*[
10 13
 		{
11 14
 			'username': ''
@@ -47,5 +50,11 @@ if (cf.cloud) {
47 50
         settings.redisOptions.host = redisConfig.hostname;
48 51
         settings.redisOptions.pass = redisConfig.password;
49 52
     }
  53
+
  54
+    if (cf.mongodb['mongo-asms']) {
  55
+        var cfg = cf.mongodb['mongo-asms'].credentials;
  56
+        settings.mongoUrl = ["mongodb://", cfg.username, ":", cfg.password, "@", cfg.hostname, ":", cfg.port,"/" + cfg.db].join('');
  57
+    }
  58
+    settings.user_email = cf.app['users'][0];
50 59
 }
51 60
 module.exports = settings;
8  views/index.ejs
@@ -4,8 +4,12 @@
4 4
 	<%= (session.auth && session.auth.twitter) ? '//twitter.com/favicon.ico' : '' %>
5 5
 	<%= (session.auth && session.auth.facebook) ? '//facebook.com/favicon.ico' : '' %>
6 6
 ">
7  
-<div id="bubble">
8  
-	<ul></ul>
  7
+<div id="stream">
  8
+	<ul>
  9
+		<% if(!session.auth) {%>
  10
+		  <li>Log In to Send Messages</li>
  11
+		<% } %>
  12
+	</ul>
9 13
 </div>
10 14
 <div id="connected">
11 15
 	<strong>Online</strong>
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.