SHIBUKAWA Yoshiki avatar SHIBUKAWA Yoshiki committed 6836b1e Merge

add retwis by node.js

Comments (0)

Files changed (12)

tutorial/retwis-js/README

+Description:
+
+    Redis tutorial program impelmented by node.js. 
+    This program is originated from retwis-py.
+
+Dependencies:
+
+    node.js (0.3.0) with following npm modules:
+    
+       - express@1.0.0rc4
+       - redis@0.3.7
+
+Usage:
+
+    node redis.js
+
+Limitations:
+
+   - need more strict error handling.
+   - We use only one client to connect redis. It works well under multi request environment,
+     but we do not confirm it is efficient or not.

tutorial/retwis-js/redis.conf

+# Redis configuration file example
+
+# By default Redis does not run as a daemon. Use 'yes' if you need it.
+# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
+daemonize no
+
+# When run as a daemon, Redis write a pid file in /var/run/redis.pid by default.
+# You can specify a custom pid file location here.
+pidfile /var/run/redis.pid
+
+# Accept connections on the specified port, default is 6379
+port 6379
+
+# If you want you can bind a single interface, if the bind option is not
+# specified all the interfaces will listen for connections.
+#
+# bind 127.0.0.1
+
+# Close the connection after a client is idle for N seconds (0 to disable)
+timeout 300
+
+# Save the DB on disk:
+#
+#   save <seconds> <changes>
+#
+#   Will save the DB if both the given number of seconds and the given
+#   number of write operations against the DB occurred.
+#
+#   In the example below the behaviour will be to save:
+#   after 900 sec (15 min) if at least 1 key changed
+#   after 300 sec (5 min) if at least 10 keys changed
+#   after 60 sec if at least 10000 keys changed
+save 10 1
+save 5 10
+save 1 10000
+
+# The filename where to dump the DB
+dbfilename dump.rdb
+
+# For default save/load DB in/from the working directory
+# Note that you must specify a directory not a file name.
+dir ./
+
+# Set server verbosity to 'debug'
+# it can be one of:
+# debug (a lot of information, useful for development/testing)
+# notice (moderately verbose, what you want in production probably)
+# warning (only very important / critical messages are logged)
+loglevel debug
+
+# Specify the log file name. Also 'stdout' can be used to force
+# the demon to log on the standard output. Note that if you use standard
+# output for logging but daemonize, logs will be sent to /dev/null
+logfile stdout
+
+# Set the number of databases. The default database is DB 0, you can select
+# a different one on a per-connection basis using SELECT <dbid> where
+# dbid is a number between 0 and 'databases'-1
+databases 16
+
+################################# REPLICATION #################################
+
+# Master-Slave replication. Use slaveof to make a Redis instance a copy of
+# another Redis server. Note that the configuration is local to the slave
+# so for example it is possible to configure the slave to save the DB with a
+# different interval, or to listen to another port, and so on.
+
+# slaveof <masterip> <masterport>
+
+################################## SECURITY ###################################
+
+# Require clients to issue AUTH <PASSWORD> before processing any other
+# commands.  This might be useful in environments in which you do not trust
+# others with access to the host running redis-server.
+#
+# This should stay commented out for backward compatibility and because most
+# people do not need auth (e.g. they run their own servers).
+
+# requirepass foobared
+
+############################### ADVANCED CONFIG ###############################
+
+# Glue small output buffers together in order to send small replies in a
+# single TCP packet. Uses a bit more CPU but most of the times it is a win
+# in terms of number of queries per second. Use 'yes' if unsure.
+glueoutputbuf yes
+

tutorial/retwis-js/redis.js

+var util = require('util'),
+    path = require('path'),
+    http = require('http'),
+    crypto = require('crypto');
+
+var express = require('express'),
+    redis = require('redis').createClient();
+
+function getrand(){
+   var bitstr = "";
+   for(var i=0; i<16; i++){
+      bitstr += String.fromCharCode(Math.random() * 256);
+   }
+   return crypto.createHash('md5').update(bitstr).digest('hex');
+}
+
+function Application(routes, settings){
+   var app = express.createServer();
+   app.configure(function(){
+      app.use(express.cookieDecoder());
+      app.use(express.bodyDecoder());
+      if( settings.staticPath ){
+         app.use(express.staticProvider(settings.staticPath));
+      }
+      if( settings.templatePath ){
+         app.set('views', settings.templatePath);
+      }
+      if( settings.loginUrl ){
+         app.set('loginUrl', settings.loginUrl);
+      }
+
+      app.dynamicHelpers({
+         logout: function(req, res){
+            return req.currentUser != null;
+         }
+      });
+
+      app.use(app.router);
+   });
+
+   // mount routes
+   for(var i=0, len=routes.length; i<len; i++){
+      var map = routes[i];
+      var path = map[0];
+      var handler = map[1];
+      ['get', 'post', 'put', 'del'].forEach(function(method){
+         if( typeof handler[method] === 'function' ){
+            app[method](path, handler[method]);
+         }
+      });
+   }
+
+   return app;
+}
+
+http.IncomingMessage.prototype.getCurrentUser = function(callback){
+   var self = this;
+   var authcookie = self.cookies.auth;
+   if( authcookie ){
+      redis.get("auth:" + authcookie, function(err1, userid){
+         if( userid ){
+            redis.get("uid:" + userid + ":auth", function(err2, authsecret){
+               if( authsecret == authcookie ){
+                  redis.get("uid:" + userid + ":username", function(err3, username){
+                     self._currentUser = {
+                        id: userid.toString(),
+                        name: username.toString()
+                     };
+                     callback(null, self._currentUser);
+                  });
+               }else{
+                  callback(null, null);
+               }
+            });
+         }else{
+            callback(null, null);
+         }
+      });
+   }else{
+      callback(undefined, null);
+   }
+}
+
+http.IncomingMessage.prototype.__defineGetter__('currentUser', function(){
+   return this._currentUser;
+});
+
+function authenticated(fun){
+   return function(req, res){
+      req.getCurrentUser(function(err, user){
+         if( user ){
+            fun(req, res);
+         }else{
+            // app.set(name) returns the value of name ...
+            var url = req.app.set('loginUrl');
+            res.redirect(url);
+         }
+      });
+   };
+}
+
+
+var WelcomeHandler = {
+   get: function(req, res){
+      res.render('welcome.ejs', {
+         locals: {
+            loginError: null,
+            registerError: null
+         }
+      });
+   },
+   post: function(req, res){
+      var username = req.param('username');
+      var password = req.param('password');
+      if( !username || !password ){
+         res.render('welcome.ejs', {
+            locals: {
+               loginError: "You need to enter both username and password to login",
+               registerError: null
+            }
+         });
+      }
+      redis.get("username:" + username + ":id", function(err, userid){
+         if( !userid ){
+            res.render('welcome.ejs', {
+               locals: {
+                  loginError: "Wrong username or password",
+                  registerError: null
+               }
+            });
+         }else{
+            redis.get("uid:" + userid + ":password", function(err, realpassword){
+               if( realpassword != password ){
+                  res.render('welcome.ejs', {
+                     locals: {
+                        loginError: "Wrong username or password",
+                        registerError: null
+                     }
+                  });
+               }else{
+                  redis.get("uid:" + userid + ":auth", function(err, authsecret){
+                     res.cookie("auth", authsecret, {expires: new Date(Date.now() + 31536000000)});
+                     res.redirect('/');
+                  });
+               }
+            });
+         }
+      });
+   }
+};
+
+var RegisterHandler = {
+   post: function(req, res){
+      var username = req.param('username');
+      var password = req.param('password');
+      var password2 = req.param('password2');
+
+      var errorMessage;
+      if( !username || !password || !password2 ){
+         errorMessage = "Every field of the registration form is needed!";
+      }else if( password !== password2 ){
+         errorMessage = "The two password fileds don't match!";
+      }
+      if( errorMessage ){
+         res.render('welcome.ejs', {
+            locals: {
+               loginError: null,
+               registerError: errorMessage
+            }
+         });
+      }else{
+         redis.get("username:" + username + ":id", function(err, userid){
+            if( userid ){
+               res.render('welcome.ejs', {
+                  locals: {
+                     loginError: null,
+                     registerError: "Sorry the selected username is already in use."
+                  }
+               });
+            }else{
+               redis.incr("global:nextuserId", function(err, userid){
+                  redis.set("username:" + username + ":id", userid);
+                  redis.set("uid:" + userid + ":username", username);
+                  redis.set("uid:" + userid + ":password", password);
+                  var authsecret = getrand();
+                  redis.set("uid:" + userid + ":auth", authsecret);
+                  redis.set("auth:" + authsecret, userid);
+                  redis.sadd("global:users", userid);
+                  res.cookie("auth", authsecret, {expires: new Date(Date.now() + 31536000000)});
+                  res.render('register.ejs', {
+                     locals: {
+                        username: username
+                     }
+                  });
+               });
+            }
+         });
+      }
+   }
+};
+
+var HomeHandler = {
+   get: authenticated(function(req, res){
+      var userid = req.currentUser.id;
+      var start = parseInt(req.param('start')) || 0;
+      var path = req.url.split("?")[0];
+      var formatter = new PostFormatter();
+      formatter.userPostWithPagenation(path, null, userid, start, 10, function(){
+         redis.scard("uid:" + userid + ":followers", function(err, followers){
+            redis.scard("uid:" + userid + ":following", function(err, following){
+               res.render('home.ejs', {
+                  locals : {
+                     username: req.currentUser.name,
+                     followers: followers,
+                     following: following,
+                     userPosts: formatter.post,
+                     link: formatter.link
+                  }
+               });
+            });
+         });
+      });
+   })
+};
+
+
+var LogoutHandler = {
+   get: authenticated(function(req, res){
+      var userid = req.currentUser.id;
+      var newauthsecret = getrand();
+      redis.get("uid:" + userid + ":auth", function(err, oldauthsecret){
+         redis.set("uid:" + userid + ":auth", newauthsecret);
+         redis.set("auth:" + newauthsecret, userid);
+         redis.del("auth:" + oldauthsecret);
+         res.redirect("/");
+      });
+   })
+};
+
+var PostHandler = {
+   post: authenticated(function(req, res){
+      var self = this;
+      var userid = req.currentUser.id;
+      var status = req.param('status').replace(/\n/g, " ");
+      var post = [userid, Date.now(), status].join("|");
+      redis.incr("global:nextPostId", function(err, postid){
+         // related resources pushed into redis withought blocking
+         redis.set("post:" + postid, post);
+         redis.smembers("uid:" + userid + ":followers", function(err, followers){
+            if( !followers ){
+               followers = [];
+            }
+            followers.push(userid);
+            followers.forEach(function(fid){
+               redis.lpush("uid:" + fid + ":posts", postid);
+            });
+            redis.lpush("global:timeline", postid);
+            redis.ltrim("global:timeline", 0, 1000);
+         });
+         res.redirect("/");
+      });
+   })
+};
+
+var TimelineHandler = {
+   get: function(req, res){
+      var formatter = new PostFormatter();
+      formatter.showUserPosts(null, 0, 50, function(err){
+         res.render('timeline.ejs', {
+            locals: {
+               posts: formatter.post
+            }
+         });
+
+      });
+   }
+};
+
+var ProfileHandler = {
+   get: authenticated(function(req, res){
+      var username = req.param('u');
+      redis.get("username:" + username + ":id", function(err, userid){
+         if( !userid ){
+            res.redirect("/");
+         }else{
+            redis.sismember("uid:" + req.currentUser.id + ":following", function(err, isfollowing){
+               res.render("profile.ejs", {
+                  locals: {
+                     isfollowing: isfollowing !== undefined,
+                     userid: userid,
+                     username: username
+                  }
+               });
+            });
+         }
+      });
+   })
+};
+
+var FollowHandler = {
+   get: authenticated(function(req, res){
+      var userid = req.param("uid");
+      var flag = req.param("f");
+      if( userid && flag ){
+         var myuserid = req.currentUser.id;
+         if( userid != myuserid ){
+            var command = (flag == "0") ? "sadd" : "srem";
+            redis[command]("uid:" + userid + ":followers", myuserid);
+            redis[command]("uid:" + myuserid + ":following", userid);
+         }
+      }
+      res.redirect("/");
+   })
+};
+
+function PostFormatter(){
+   this._posts = [];
+   this._links = [];
+   this._isLast = false;
+}
+
+PostFormatter.prototype.__defineGetter__("post", function(){
+   return this._posts.join("\n");
+});
+
+PostFormatter.prototype.__defineGetter__("link", function(){
+   if( this._links ){
+      return '<div class="rightlink">' +
+         this._links.join(" | ") +
+         '</div>';
+   }else{
+      return '';
+   }
+});
+
+PostFormatter.prototype._elapsed = function(t){
+   var d = Date.now() - t;
+   if( d < 60000 ){
+      return parseInt(d/1000) + " seconds";
+   }else if( d < 120000 ){
+      return "1 minute";
+   }else if( d < 3600000 ){
+      return parseInt(d/60000) + " minutes";
+   }else if( d < 7200000 ){
+      return "1 hour";
+   }else if( d < 3600000 * 24 ){
+      return parseInt(d/3600000) + " hours";
+   }else if( d < 3600000 * 48 ){
+      return "1 day";
+   }else{
+      return parseInt(d/3600000/24) + " days";
+   }
+}
+
+PostFormatter.prototype.showPost = function(id, callback){
+   var self = this;
+   redis.get("post:" + id, function(err, postdata){
+      if( postdata ){
+         postdata = postdata.toString();
+         var s = postdata.split('|');
+         var userid = s.shift(), time = s.shift(), post = s.join('|');
+         redis.get("uid:" + userid + ":username", function(err, username){
+            self._posts.push('<div class="post">' +
+                             '<a class="username" href="/profile?u=' + encodeURIComponent(username) + '">' + username + '</a>' + post + '<br>' +
+                             '<i>posted ' + self._elapsed(parseInt(time)) + ' ago via web</i>' +
+                             '</div>');
+            callback(null, userid, time, post);
+         });
+      }else{
+         callback(null, null, null, null);
+      }
+   });
+}
+
+PostFormatter.prototype.showUserPosts = function(userid, start, count, callback){
+   var self = this;
+   if( userid ){
+      var key = "uid:" + userid + ":posts";
+   }else{
+      var key = "global:timeline";
+   }
+   redis.lrange(key, start, start+count, function(err, posts){
+      var len = posts == null ? 0 : posts.length;
+      self._isLast = (len != count + 1);
+      // recursive call for PostFormatter#showPost
+      function showPost(i){
+         if( i == len ){
+            callback(null); // finally call the callback
+         }else{
+            var post = posts[i];
+            self.showPost(post, function(){
+               showPost(i+1);
+            });
+         }
+      }
+      showPost(0);
+   });
+}
+
+
+PostFormatter.prototype.userPostWithPagenation = function(path, username, userid, start, count,
+                                                          callback){
+   var self = this;
+   if( username ){
+      var userstr = '&u=' + encodeURIComponent(username);
+   }else{
+      var userstr = "";
+   }
+   this.showUserPosts(userid, start, count, function(){
+      if( start > 0 ){
+         self._links.push('<a href="' + path + '?start=' + Math.max(start-10, 0) + userstr + '">&laquo; Newer posts</a>');
+      }
+      if( !self._isLast ){
+         self._links.push('<a href="' + path + '?start=' + (start + 10) + userstr + '">Older posts &raquo;</a>');
+      }
+      callback(null);
+   });
+}
+
+
+
+var settings = {
+   staticPath : path.join(__dirname, "static"),
+   templatePath : path.join(__dirname, "template"),
+   loginUrl: "/welcome"
+}
+
+var app = Application([
+   ["/", HomeHandler],
+   ["/welcome", WelcomeHandler],
+   ["/logout", LogoutHandler],
+   ["/register", RegisterHandler],
+   ["/post", PostHandler],
+   ["/timeline", TimelineHandler],
+   ["/profile", ProfileHandler],
+   ["/follow", FollowHandler]
+], settings);
+app.listen(8888);
Add a comment to this file

tutorial/retwis-js/static/logo.png

Added
New image
Add a comment to this file

tutorial/retwis-js/static/sfondo.png

Added
New image

tutorial/retwis-js/static/style.css

+BODY {
+    font-family: Verdana, sans-serif;
+    background: url(/static/sfondo.png) repeat-x top white;
+    background-attachment: fixed;
+}
+
+#page {
+    margin: 0px;
+    width:900px;
+    margin-left: auto;
+    margin-right: auto;
+    padding:10px;
+    border:1px gray solid;
+    background-color:white;
+    -moz-border-radius:5px;
+    -webkit-border-radius:5px;
+    border-radius:5px;
+}
+
+#page h2 {
+    color:#0f2a44;
+    font-weight:bold;
+}
+
+#header {
+    width:885px;
+    height:85px;
+    border-bottom: 1px #aaa solid;
+    padding:5px;
+    margin-bottom:10px;
+    position:relative;
+
+}
+
+#header H1 {
+    margin:0px;
+    padding:0px;
+    margin-bottom:10px;
+}
+
+#navbar {
+    position:absolute;
+    top:65px;
+    right:8px;
+    font-size:14px;
+    color: #aaa;
+}
+
+#navbar a {
+    text-decoration: none;
+}
+
+#footer {
+    margin-top:20px;
+    border-top: 1px #aaa solid;
+    font-size:12px;
+    color: #666;
+    text-align:center;
+}
+
+.post {
+    margin:10px;
+    padding:10px;
+    border-top: 1px #ddd dashed;
+    color:#444;
+}
+
+.post i {
+    font-size:10px;
+    color:#999;
+}
+
+#postform {
+    -moz-border-radius:5px;
+    -webkit-border-radius:5px;
+    border-radius:5px;
+    position:relative;
+    padding:10px;
+    margin:10px;
+    background-color: #eee;
+}
+
+.rightlink {
+    text-align:right;
+    margin-right:30px;
+    color:#aaa;
+}
+
+.rightlink a {
+    color: #f55000;
+    text-decoration: none;
+}
+
+a.username {
+    text-decoration: none;
+    font-weight:bold;
+    color:#629e43;
+    margin-right:10px;
+}
+
+h2.username {
+    color:#66dd66;
+    margin-left:15px;
+}
+
+a.button {
+margin-left:15px;
+text-decoration:none;
+border: 1px #aaa dotted;
+padding:3px;
+background-color:#eee;
+color:#444;
+font-size:12px;
+}
+
+#homeinfobox {
+    -moz-border-radius:5px;
+    -webkit-border-radius:5px;
+    border-radius:5px;
+    position: absolute;
+    right:5px;
+    top:5px;
+    background-color: #ccc;
+    width:200px;
+    font-size:12px;
+    color:white;
+    padding:3px;
+}
+
+#welcomebox{
+    width:890px;
+    height:450px;
+    padding-top:10px;
+    position:relative;
+    font-size:14px;
+}
+#registerbox{
+    width:400px;
+    height:400px;
+    background-color:#e3e8e4;
+    float:right;
+    padding:5px;
+    -moz-border-radius:5px;
+    -webkit-border-radius:5px;
+    border-radius:5px;
+    border:3px #c8d8bf solid;
+    margin-right:0px;
+    margin-left:15px;
+}
+
+#alertbox{
+    width:350px;
+    background-color:#ffe8e4;
+    float:right;
+    padding:5px;
+    -moz-border-radius:5px;
+    -webkit-border-radius:5px;
+    border-radius:5px;
+    border:3px #ff6666 solid;
+    margin-right:20px;
+    margin-left:20px;
+    margin-bottom: 15px;
+    color: red;
+}
+

tutorial/retwis-js/template/home.ejs

+  <div id="postform">
+    <form method="POST" action="/post">
+      <%= username %>, what you are doing? <br>
+      <table>
+        <tr><td><textarea cols="70" rows="3" name="status"></textarea></td></tr>
+        <tr><td align="right"><input type="submit" name="doit" value="Update"></td></tr>
+      </table>
+    </form>
+    <div id="homeinfobox">
+        <%= followers %> followers<br>
+        <%= following %> following<br>
+    </div>
+  </div>
+  <%- userPosts %>
+  <%- link %>

tutorial/retwis-js/template/layout.ejs

+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+  <head>
+    <meta content="text/html; charset=UTF-8" http-equiv="content-type">
+    <title>Retwis.js - Example Twitter clone based on the Redis Key-Value DB</title>
+    <link href="/style.css" rel="stylesheet" type="text/css">
+  </head>
+  <body>
+    <div id="page">
+      <div id="header">
+        <a href="/"><img style="border:none" src="/logo.png" width="192" height="85" alt="Retwis"></a>
+        <div id="navbar">
+          <a href="/">home</a> | <a href="/timeline">timeline</a>
+          <% if( logout ){ %>
+            | <a href="/logout">logout</a>
+          <% } %>
+        </div>
+      </div>
+      <%- body %>
+      <div id="footer">Redis is a very simple Twitter clone written in node.js as example application of the <a href="http://code.google.com/p/redis/">Redis key-value database</a></div>
+    </div>
+  </body>
+</html>

tutorial/retwis-js/template/profile.ejs

+<h2><%= username %></h2>
+<% if( !isfollowing ){ %>
+<a href="/follow?uid=<%= userid %>&f=0" class="button">Follow this user</a>
+<% }else{ %>
+<a href="/follow?uid=<%= userid %>&f=1" class="button">Stop following</a>
+<% } %>

tutorial/retwis-js/template/register.ejs

+<h2>Welcome aboard!</h2>
+Hey <%= username %>, now you have an account, 
+<a href="/">a good start is to write your first message!</a>.
+

tutorial/retwis-js/template/timeline.ejs

+<h2>Timeline</h2>
+<i>Latest registered users (an example of SORT command!)</i><br>
+<%- posts %>
+<i>Latest 50 messages from users aroud the world!</i><br>

tutorial/retwis-js/template/welcome.ejs

+<div id="welcomebox">
+  <div id="registerbox">
+    <h2>Register!</h2>
+    <b>Want to try Retwis? Create an account!</b>
+    <form method="POST" action="/register">
+      <table>
+        <tr><td>Username</td><td><input type="text" name="username"></td></tr>
+        <tr><td>Password</td><td><input type="password" name="password"></td></tr>
+        <tr><td>Password (again)</td><td><input type="password" name="password2"></td></tr>
+        <tr><td colspan="2" align="right"><input type="submit" name="doit" value="Create an account"></td></tr>
+      </table>
+    </form>
+    <% if( registerError ){ %>
+    <div id="alertbox"><%= registerError %></div>
+    <% } %>
+    <h2>Already registered? Login here</h2>
+    <form method="POST" action="/welcome">
+      <table>
+        <tr><td>Username</td><td><input type="text" name="username"></td></tr>
+        <tr><td>Password</td><td><input type="password" name="password"></td></tr>
+        <tr><td colspan="2" align="right"><input type="submit" name="doit" value="Login"></td></tr>
+      </table>
+    </form>
+    <% if( loginError ){ %>
+    <div id="alertbox"><%= loginError %></div>
+    <% } %>
+  </div>
+  Hello! Retwis is a very simple clone of <a href="http://twitter.com">Twitter</a>, 
+  as a demo for the <a href="http://code.google.com/p/redis/">Redis</a> key-value database. Key points:
+  <ul>
+    <li>Redis is a key-value DB, and it is <b>the only DB used</b> by this application, no MySQL or alike at all.</li>
+    <li>This application can scale horizontally since there is no point where the whole dataset is needed at the same point. With consistent hashing (not implemented in the demo to make it simpler) different keys can be stored in different servers.</li>
+    <li>The source code of this application, and a tutorial explaining its design, is available <a href="http://code.google.com/p/redis/wiki/TwitterAlikeExample">here</a>.
+    <li>PHP and the Redis server communicate using the PHP Redis library client written by Ludovico Mangocavallo and included inside the Redis tar.gz distribution.
+  </ul>
+</div>
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.