User:Jonas Lund/tri1.2

From XPUB & Lens-Based wiki

Let's Talk About Pictures (LTAP)

The LTAP is a collaboration with Louis Doulas and Harry Burke, to create a website that facilitates and encourages a one to one discussion about pictures. In LTAP, each user is initially presented with an upload form which accepts images. The user is required to upload a picture of their choosing and pick a nickname to enter the discussion, the chat room. The chat room displays a random picture from the database of images and the conversation can begin.

LTAP Development

Plan

Use nodejs, with the modules nowjs, mysql, forever, formidable, to create a realtime chat with image uploads and a mysql database for storage. (Consider switching mysql for couchdb or mongodb).

Resources

Application Logic

File Uploads

  • Node Formidable Module for handling multipart uploadshttps://github.com/felixge/node-formidable/

Mysql, Mysql Module

General articles to read


Deployment

  • Look into Nginx


Source

server.js

  //initiate database
  var mysql = require('mysql');
  var db = 'ltap';
  var chats = 'chats';
  var chatLogs = 'chat_logs'
  var client = mysql.createClient({
    host: '127.0.0.1',
    user: 'root',
    password: 'root',
    port: 8889
  });
  
  //includes
  var http = require("http");
  var url = require("url");
  var fs = require("fs");
  var util = require('util');
  var formidable = require('formidable');
  var exec = require('child_process').exec;
  var fu = exports;

  function onRequest(req, res) {
     var pathname = url.parse(req.url).pathname;
     console.log("Request for " + pathname + " received.");

     //routing
     switch (pathname) {
        case '/':
        req.setEncoding("utf8");
            var html = fs.readFileSync(__dirname+'/index.html');
            res.end(html);
        break;
        //handle file upload
        case '/upload':
            uploadFile(req, res);
        break;
          default:
            //check if file exits, otherwise return 404
            req.setEncoding("utf8");
             var content_type = fu.mime.lookupExtension(extname(pathname));

              fs.readFile(__dirname+"/"+pathname, function(err, data) { 
                console.log(pathname);
                if(err) {
                  res.writeHead(404, {"Content-Type": "text/plain"});
                  res.write("404 Not found");
                  res.end();
                } else {
                  headers = { "Content-Type": content_type
                            , "Content-Length": data.length
                            };

                  res.writeHead(200, headers);
                  res.write(data);
                  res.end();                
                }
              });
        break;
      }
  }
  var server = http.createServer(onRequest).listen(1337);
  console.log("server running on 1337");

  var nowjs = require("now");
  var everyone = nowjs.initialize(server);

  //Session Variables
  var sessions = {};
  var rooms = [];
  rooms[0] = 0;

  //Send message to everyone on the users group
  everyone.now.distributeMessage = function(message){
      console.log('Received '+message+' from '+this.now.nick +' in serverroom '+this.now.room);
      var group = nowjs.getGroup(this.now.room);    
      group.now.receiveMessage(this.now.id, this.now.nick, message);
      
      var date = new Date();

      //insert into DB
      client.query('USE '+db);
      client.query(
        'INSERT INTO '+chatLogs+' '+
        'SET user = ?, text = ?, date = ?',
        [this.user.clientId, message, date]
      );  

  };

  //Join action, called from client
  everyone.now.join = function() {        
      var userID = this.user.clientId;
      var nick = this.now.nick;  
      var image = this.now.image;
      var id = this.now.id;

      //get available room
      var room = getRoom();
      this.now.room = room;
      
      //create user object
      var user = {
        id: userID,
        nick: nick,
        room: room,
        image: image
        //timestamp: new Date()      
       };

      //add user to room
      nowjs.getGroup(room).addUser(userID);

      //add user to session
      sessions[user.id] = user;
      var group = nowjs.getGroup(this.now.room);

      if(rooms[room] == 2) {
        var status = "Partner Joined";   

        //get-users, add to db
        nowjs.getGroup(room).getUsers(function (users) { 
          var user1 = users[0];
          var user2 = users[1];
          var nick1 =  sessions[user1].nick;
          var image1 = sessions[user1].image;       
          var nick2 =  sessions[user2].nick;
          var image2 = sessions[user2].image;
          var date = new Date();
          
          //send image to the client
          group.now.updateImage(image1,image2);

          //insert into DB
          client.query('USE '+db);
          client.query(
            'INSERT INTO '+chats+' '+
            'SET user1 = ?, user2 = ?, nick1 = ?, nick2 = ?, image1 = ?, image2 = ?, date = ?',
            [user1, user2, nick1, nick2, image1, image2, date]
          );
        });

      } else {
        var status = "Finding chat buddy..";
      }  
      group.now.chatStatus(status);
  }
  
  //User Disconnect, remove from sessions
  nowjs.on('disconnect', function () { 
    var userroom = this.now.room;
    var nick = this.now.nick;
    var id = this.now.id;
    var userID = this.user.clientId;
    
    //check if user sticks around for a new chat buddy
    if(rooms[userroom] == 2) {
      rooms[userroom] = 1;
      var status = "Partner Left.. ;( – Looking for a new one...";
    } else {
      rooms[userroom] = 0;
    }

    //remove user
    if (userID && sessions[id]) {  
      delete sessions[userID];
    }
    var group = nowjs.getGroup(this.now.room);
    group.now.chatStatus(status);

    console.log("user:", this.now.id, this.now.nick, " left");
  });

  //check room array for available room
  function getRoom() {  
    for (var i=0; i < rooms.length; i++) {
      if(rooms[i] === 1) {
        rooms[i] = 2;      
        return i;

      } else if (rooms[i] === 0) {
          rooms[i] = 1;
          return i;
      }
    }; 

     rooms.push(1);
     return rooms.length-1; 
  }

  //handle image upload
  function uploadFile(req, res) {
    var form = new formidable.IncomingForm(),
        files = [],
        fields = [];
        fileob = {};

    form.uploadDir = __dirname + '/tmp';
    form.encoding = 'binary';

    form
      .on('field', function(field, value) {
        console.log(field, value);
        fields.push([field, value]);
      })
      .on('file', function(field, file) {
        //console.log("on file", field, file);
        files.push([field, file]);

        //check size type, cancel if wrong                  
        var newfile = {
            file: file.size, 
            name: file.name,
            path: file.path,
            type: file.type
        };

        fileob = newfile;

      })
      .on('end', function() {
        console.log('-> upload done');
        res.writeHead(200, {'content-type': 'text/plain'});

        //filename cleanup
        var ext = extname(fileob.name);      
        var uni = Math.floor(Math.random()*99999).toString();
        var cleanFilename = fileob.name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
        var filename = uni + cleanFilename + ext;
        
        //return file name to client
        res.write(filename);
        res.end();      
        
        //move file in place
        var cmd = "mv " + fileob.path + " /path/to/upload/folder/" + filename;
        console.log(cmd);
        exec(cmd, puts);

      });
  //  form.parse(req);

      form.parse(req, function(err, fields, files) {
        if (err) {
          console.log(err);
        }
      });
  }
  function puts(error, stdout, stderr) { util.puts(stdout) }

  function include(arr,obj) {
      return (arr.indexOf(obj) != -1);
  }

  Array.prototype.remove = function(from, to) {
    var rest = this.slice((to || from) + 1 || this.length);
    this.length = from < 0 ? this.length + from : from;
    return this.push.apply(this, rest);
  };

  //get ext name
  function extname (path) {
    var index = path.lastIndexOf(".");
    return index < 0 ? "" : path.substring(index);
  }

  //stolen from node_chat, jack- thanks
  fu.mime = {
    // returns MIME type for extension, or fallback, or octet-steam
    lookupExtension : function(ext, fallback) {
      return fu.mime.TYPES[ext.toLowerCase()] || fallback || 'application/octet-stream';
    },

    // List of most common mime-types, stolen from Rack.
    TYPES : { ".3gp"   : "video/3gpp"
            , ".a"     : "application/octet-stream"
            , ".ai"    : "application/postscript"
            , ".aif"   : "audio/x-aiff"
            , ".aiff"  : "audio/x-aiff"
            , ".asc"   : "application/pgp-signature"
            , ".asf"   : "video/x-ms-asf"
            , ".asm"   : "text/x-asm"
            , ".asx"   : "video/x-ms-asf"
            , ".atom"  : "application/atom+xml"
            , ".au"    : "audio/basic"
            , ".avi"   : "video/x-msvideo"
            , ".bat"   : "application/x-msdownload"
            , ".bin"   : "application/octet-stream"
            , ".bmp"   : "image/bmp"
            , ".bz2"   : "application/x-bzip2"
            , ".c"     : "text/x-c"
            , ".cab"   : "application/vnd.ms-cab-compressed"
            , ".cc"    : "text/x-c"
            , ".chm"   : "application/vnd.ms-htmlhelp"
            , ".class"   : "application/octet-stream"
            , ".com"   : "application/x-msdownload"
            , ".conf"  : "text/plain"
            , ".cpp"   : "text/x-c"
            , ".crt"   : "application/x-x509-ca-cert"
            , ".css"   : "text/css"
            , ".csv"   : "text/csv"
            , ".cxx"   : "text/x-c"
            , ".deb"   : "application/x-debian-package"
            , ".der"   : "application/x-x509-ca-cert"
            , ".diff"  : "text/x-diff"
            , ".djv"   : "image/vnd.djvu"
            , ".djvu"  : "image/vnd.djvu"
            , ".dll"   : "application/x-msdownload"
            , ".dmg"   : "application/octet-stream"
            , ".doc"   : "application/msword"
            , ".dot"   : "application/msword"
            , ".dtd"   : "application/xml-dtd"
            , ".dvi"   : "application/x-dvi"
            , ".ear"   : "application/java-archive"
            , ".eml"   : "message/rfc822"
            , ".eps"   : "application/postscript"
            , ".exe"   : "application/x-msdownload"
            , ".f"     : "text/x-fortran"
            , ".f77"   : "text/x-fortran"
            , ".f90"   : "text/x-fortran"
            , ".flv"   : "video/x-flv"
            , ".for"   : "text/x-fortran"
            , ".gem"   : "application/octet-stream"
            , ".gemspec" : "text/x-script.ruby"
            , ".gif"   : "image/gif"
            , ".gz"    : "application/x-gzip"
            , ".h"     : "text/x-c"
            , ".hh"    : "text/x-c"
            , ".htm"   : "text/html"
            , ".html"  : "text/html"
            , ".ico"   : "image/vnd.microsoft.icon"
            , ".ics"   : "text/calendar"
            , ".ifb"   : "text/calendar"
            , ".iso"   : "application/octet-stream"
            , ".jar"   : "application/java-archive"
            , ".java"  : "text/x-java-source"
            , ".jnlp"  : "application/x-java-jnlp-file"
            , ".jpeg"  : "image/jpeg"
            , ".jpg"   : "image/jpeg"
            , ".js"    : "application/javascript"
            , ".json"  : "application/json"
            , ".log"   : "text/plain"
            , ".m3u"   : "audio/x-mpegurl"
            , ".m4v"   : "video/mp4"
            , ".man"   : "text/troff"
            , ".mathml"  : "application/mathml+xml"
            , ".mbox"  : "application/mbox"
            , ".mdoc"  : "text/troff"
            , ".me"    : "text/troff"
            , ".mid"   : "audio/midi"
            , ".midi"  : "audio/midi"
            , ".mime"  : "message/rfc822"
            , ".mml"   : "application/mathml+xml"
            , ".mng"   : "video/x-mng"
            , ".mov"   : "video/quicktime"
            , ".mp3"   : "audio/mpeg"
            , ".mp4"   : "video/mp4"
            , ".mp4v"  : "video/mp4"
            , ".mpeg"  : "video/mpeg"
            , ".mpg"   : "video/mpeg"
            , ".ms"    : "text/troff"
            , ".msi"   : "application/x-msdownload"
            , ".odp"   : "application/vnd.oasis.opendocument.presentation"
            , ".ods"   : "application/vnd.oasis.opendocument.spreadsheet"
            , ".odt"   : "application/vnd.oasis.opendocument.text"
            , ".ogg"   : "application/ogg"
            , ".p"     : "text/x-pascal"
            , ".pas"   : "text/x-pascal"
            , ".pbm"   : "image/x-portable-bitmap"
            , ".pdf"   : "application/pdf"
            , ".pem"   : "application/x-x509-ca-cert"
            , ".pgm"   : "image/x-portable-graymap"
            , ".pgp"   : "application/pgp-encrypted"
            , ".pkg"   : "application/octet-stream"
            , ".pl"    : "text/x-script.perl"
            , ".pm"    : "text/x-script.perl-module"
            , ".png"   : "image/png"
            , ".pnm"   : "image/x-portable-anymap"
            , ".ppm"   : "image/x-portable-pixmap"
            , ".pps"   : "application/vnd.ms-powerpoint"
            , ".ppt"   : "application/vnd.ms-powerpoint"
            , ".ps"    : "application/postscript"
            , ".psd"   : "image/vnd.adobe.photoshop"
            , ".py"    : "text/x-script.python"
            , ".qt"    : "video/quicktime"
            , ".ra"    : "audio/x-pn-realaudio"
            , ".rake"  : "text/x-script.ruby"
            , ".ram"   : "audio/x-pn-realaudio"
            , ".rar"   : "application/x-rar-compressed"
            , ".rb"    : "text/x-script.ruby"
            , ".rdf"   : "application/rdf+xml"
            , ".roff"  : "text/troff"
            , ".rpm"   : "application/x-redhat-package-manager"
            , ".rss"   : "application/rss+xml"
            , ".rtf"   : "application/rtf"
            , ".ru"    : "text/x-script.ruby"
            , ".s"     : "text/x-asm"
            , ".sgm"   : "text/sgml"
            , ".sgml"  : "text/sgml"
            , ".sh"    : "application/x-sh"
            , ".sig"   : "application/pgp-signature"
            , ".snd"   : "audio/basic"
            , ".so"    : "application/octet-stream"
            , ".svg"   : "image/svg+xml"
            , ".svgz"  : "image/svg+xml"
            , ".swf"   : "application/x-shockwave-flash"
            , ".t"     : "text/troff"
            , ".tar"   : "application/x-tar"
            , ".tbz"   : "application/x-bzip-compressed-tar"
            , ".tcl"   : "application/x-tcl"
            , ".tex"   : "application/x-tex"
            , ".texi"  : "application/x-texinfo"
            , ".texinfo" : "application/x-texinfo"
            , ".text"  : "text/plain"
            , ".tif"   : "image/tiff"
            , ".tiff"  : "image/tiff"
            , ".torrent" : "application/x-bittorrent"
            , ".tr"    : "text/troff"
            , ".txt"   : "text/plain"
            , ".vcf"   : "text/x-vcard"
            , ".vcs"   : "text/x-vcalendar"
            , ".vrml"  : "model/vrml"
            , ".war"   : "application/java-archive"
            , ".wav"   : "audio/x-wav"
            , ".wma"   : "audio/x-ms-wma"
            , ".wmv"   : "video/x-ms-wmv"
            , ".wmx"   : "video/x-ms-wmx"
            , ".wrl"   : "model/vrml"
            , ".wsdl"  : "application/wsdl+xml"
            , ".xbm"   : "image/x-xbitmap"
            , ".xhtml"   : "application/xhtml+xml"
            , ".xls"   : "application/vnd.ms-excel"
            , ".xml"   : "application/xml"
            , ".xpm"   : "image/x-xpixmap"
            , ".xsl"   : "application/xml"
            , ".xslt"  : "application/xslt+xml"
            , ".yaml"  : "text/yaml"
            , ".yml"   : "text/yaml"
            , ".zip"   : "application/zip"
            }
  };

index.html

  <!doctype html>
  <html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <title>LTAP</title>
    <meta name="description" content="">
    <meta name="author" content="">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="stylesheet" href="style.css">
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
    <script src="/nowjs/now.js"></script>
    <script>
    $(document).ready(function(){
  	  now.receiveMessage = function(id, nick, message){

  	  if(id === now.id) {
  	    var nick = "you";
  	  }

  	  var content = '<div>'
                  + '  <span class="nick '+nick+'">' + nick + ':</span>'
                  + '  <span class="msg-text">' + message  + '</span'
                  + '</div>'
                  ;

  		$("#message").append(content).scrollTop(55000);
  	}

    $(".send").click(function() {
        var input = "";
        now.messageRoom(currRoom, input.text());
    });

    now.chatStatus = function(status) {
  	  var content = '<div>'
                  + '  <span class="status">' + status + '</span>'
                  + '</div>'
                  ;
      if(status === "Partner Joined") {
        $("#text-input").prop("disabled", false).focus();
      }

      $("#message").append(content);
    }

    now.updateImage = function(image1,image2) {
      if(image1 === now.image) {
        var image = image2;
      } else {
        var image = image1;
      }
      $("#image").attr("src", image);        
    }

  	// Send message to people in the same group
  	$("#chatBox").submit(function(){
  	  var message = $("#text-input").val();
  	  if(message) {
  		  now.distributeMessage($("#text-input").val());
  		  $("#text-input").val("");
  	  }
  	  return false;
  	});
    
    //handle login
    $("#loginform").submit(function(){ 
      var nick = $("#nick").val();

      if(!nick) {
        var nick = "Stranger";
      }
      
      var image = $("#imageurl").val();    
      if(!image) {
        $(".error").html("Image Can't Be Empty").show();      
      } else {
                          
        //ugly regex hack for validating image
        var match = image.match(/http:\/\/.+?\.jpg|jpeg|png|gif/gi);
        if(!match) {
          $(".error").html("Not a valid Image Url").show();
        } else {
          now.id = Math.floor(Math.random()*99999999999).toString();
          now.room = 1;
          now.nick = nick;
          now.image = image;
          now.join(nick, image);

          $("#chat").show();
          $("#login").hide();
          $("#text-input").focus();        
        }      
      }    
      return false;
    });

    //ugly file uploading hack
    $("#file").submit(function() { 
      $("#transFrame").load(function() {       
      response = $(this).contents().find('body').text();

      if(response) {
        $("#file").hide();
        var filename = "http://localhost:1337/upload/" + response;
        $("#imageurl").val(filename);      
        $(".toggle-handle").hide();

        $("#imageurl").prop("disabled", true);
      }

      //return false;
      });
    });

    $(".toggle-handle").click(function() { 
      $(this).next(".toggle-box").show();
      return false;
    });

    // on establishing 'now' connection, set server room and allow message sending
  	now.ready(function(){
      
  	});
  });
  </script>
  </head>
  <body>  
    <div id="login">
      <form id="loginform" action="" method="get">
        <h3 class="error hidden"></h3>

        <input type="text" id="nick" placeholder="nickname"/>
        <input type="text" id="imageurl" placeholder="Image (URL)"/>

        <input type="submit" id="submit" value="go" />
      </form>        

      <h3 class='toggle-handle'>Or Upload A File...</h3>
      <div class='toggle-box hidden'>
        <form target="transFrame" id="file" action="/upload" method="post" enctype="multipart/form-data">
          <input type="file" name="file-upload" id="file-upload" multiple="multiple">
          <input type="submit" value="Upload" >
        </form>    
      </div>
    </div>

    <iframe style="" class="hidden" name="transFrame" id="transFrame"></iframe>

    <div id="chat" class="hidden">
      <div class="image">
        <img id="image" src="http://i.imgur.com/aED4b.jpg" />
      </div>

      <div class="right">
        <div id="message">      
        </div>    

        <form id="chatBox" action="" method="get">
          <input type="text" id="text-input" name="chat" value="" disabled/>
        </form>
      </div>
    </div>

  </body>
  </html>

doc1.png doc2.png doc3.png doc4.png