Tell me more ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I've written a nodejs script that fetches tweets from my twitter account (using the REST api at this moment), does a couple of things and stuff the tweets into a SQL database. The script intervals every 5 minutes.

The script has a Twitter object that has a couple of methods, the first one fetch() checks for tweets and for each tweet fires off an event with the tweet data. There are a couple of functions wich listen to an event fired by the last function and act upon it and, in their turn, fire a new event. (see the init function).

I was thinking that this way the tweets all get handled on their own without waiting for the rest of the batch. I also count the amount of tweets that goes into the 'group of functions' and the amount that goes out, this way I know when I'm done.

This makes good sense for me but I don't know what excepted solutions for these kinds of problems are.

EDIT: my questions: Is this an accepted approach to this problem? Are there more standard ways of handling this? Are there rookie mistakes in my fetch script?

var l = console.log
,  sql = require( '/config/mvr.me/sql.js' )
,  request = require( 'request' )
,  events = require( 'events' )
,  _ = require( 'underscore' )
;

function Twitter( con ) {
  // this.tweetCount = 0;
  this.tweets = [];
  this.tweetRegister = { started: 0, finished: 0}
  this.con = con;

}

Twitter.prototype = new events.EventEmitter;

Twitter.prototype.init = function() {

  _.bindAll( this );

  this
    .fetch()
    .on( 'new fetch'    , function(t) { this.matchTweet(t) } )
    .on( 'new match'    , function(t) { this.checkIfTweetIsStored(t) } )
    .on( 'checked existence', function(t) { this.getTitle(t) } )
    .on( 'checked title'  , function(t) { this.finishUp(t) } )
  ;

  return this;
}

Twitter.prototype.fetch = function() {
  var self = this
  ,  me = 'mikevanrossum';

  request('http://api.twitter.com/1/statuses/user_timeline/' + me + '.json?count=30&include_rts=1&exclude_replies=1', function (error, response, r) {
    if (!error && response.statusCode == 200) {
      var tweets = JSON.parse( r )
      ,  len = tweets.length
      ;

      if( len ) {

        self.tweetRegister.started += len;

        for( ;len--; ) {

          self.emit('new fetch', {
              tweet:   tweets[ len ]
            ,  source: ''
            ,  url: ''
            ,  title: ''
            ,  titleURL: ''
            ,  contentHTML: ''
            ,  timestamp: ''
          });
        }
      } else {
        self.emit('done');
      }
    } else {
      l('probably rate limited at ' + new Date() );
      self.emit('done');
    }
  });

  return this;
}

Twitter.prototype.signOutTweet = function() {
  this.tweetRegister.finished++;
  this.checkIfDone();
}

Twitter.prototype.matchTweet = function( tweet ) {

  var body = tweet.tweet.text
  ,  pos = body.indexOf( '#mvr' );

  if( pos + 1 ) {  
    tweet.tweet.text = body.replace(/\ #mvr/,''); 
    this.emit( 'new match', tweet );
  } else {
    this.signOutTweet();
  }

  return this;
}

Twitter.prototype.checkIfTweetIsStored = function( tweet ) {

  var self = this;

  tweet = this.addDate( tweet );

  this.con.query('SELECT timestamp FROM post WHERE timestamp = ? AND type = ?', [ tweet.timestamp, 'tweet' ], function( err, rows, fields ) {
    if (err) throw err;

    if( rows.length ) {
      self.signOutTweet();
    } else {
      self.emit( 'checked existence', tweet );
    }

  });

  return this;

}

Twitter.prototype.addDate = function( tweet ) {
  tweet.timestamp = Math.round( Date.parse( tweet.tweet.created_at ) / 1000 );
  return tweet;
}

Twitter.prototype.twitterify = function( text ) {
    text = text.replace( /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/i, "<a href='$1'>$1</a>" );
    text = text.replace( /(^|\s)#(\w+)/g, "$1<a href='http://search.twitter.com/search?q=%23$2'>#$2</a>" );
    text = text.replace( /(^|\s)@(\w+)/g, "$1<a href='http://www.twitter.com/$2'>@$2</a>" );
    return text;
}

Twitter.prototype.extractTitle = function( html ) {
  // first remove cdata and comments, safer way to regex html
  var limit = 75
  ,  str = (html.replace( /<!\[CDATA\[(.+?)\]\]>/g
  , function (_match, body) {
    return body.replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
  })
  .replace(/<!--.+?-->/g, '')
  .match(/<title>.+?<\/title>/ig) || [])
  .map(function (t) { return t.substring(7, t.length - 8) })
  .join(' ');

  str = this.trimFromChar( str, '|' );

  if( str.length > limit ) {
    str = this.trimFromChar( str, ':' );
  }

  if( str.length < limit ) {
    return str;
  }
}

Twitter.prototype.trimFromChar = function( str, character ) {

  var pos = str.indexOf( character );
  if( pos + 1 ) {
    str = str.substr( 0, pos );
  }

  return str;

}


Twitter.prototype.getTitle = function( tweet ) {

  var body = tweet.tweet.text
  ,  urls = body.match(/(^|\s)((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)/gi)
  ,  self = this;

  if( urls.length === 1 ) {

    request(urls[ 0 ], function (error, response, r) {
      if (!error && response.statusCode == 200) {

        var title = self.extractTitle( r );

        if( title ) {

          tweet.tweet.text = tweet.tweet.text.replace( /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/i, '' );

          tweet.title = title;
          tweet.titleURL = response.request.uri.href;

        }

      }

      self.emit( 'checked title', tweet );

    });

  } else this.emit( 'checked title', tweet );

  return this;
}

Twitter.prototype.getSource = function( tweet ) {

  var sourceTrigger = '/via '
  ,  body = tweet.tweet.text
  ,  pos = body.indexOf( sourceTrigger );

  if( pos + 1 ) {
    tweet.source = body.substr( pos + sourceTrigger.length );
    tweet.tweet.text = body.substr( 0, pos );
  } 

  return tweet;
}

Twitter.prototype.checkIfDone = function() {

  var len = this.tweets.length
  ,  tweetReg = this.tweetRegister
  ,  done = tweetReg.finished + len
  ;

  if( tweetReg.started === done ) {
    if( len ) {
      this.storeTweets();
    } else {
      this.emit('done');
    }

  } 

}

Twitter.prototype.addURLToTweet = (function( tweet ) {

  // from: http://dense13.com/blog/2009/05/03/converting-string-to-slug-javascript/
  function stringToSlug(str) {
    str = str.replace(/^\s+|\s+$/g, ''); // trim
    str = str.toLowerCase();

    // remove accents, swap ñ for n, etc
    var from = "àáäâèéëêìíïîòóöôùúüûñç·/_,:;";
    var to   = "aaaaeeeeiiiioooouuuunc------";
    for (var i=0, l=from.length ; i<l ; i++) {
      str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
    }

    str = str
    .replace(/[^a-z0-9 -]/g, '') // remove invalid chars
    .replace(/\s+/g, '-') // collapse whitespace and replace by -
    .replace(/-+/g, '-'); // collapse dashes

    return str;
  }

  return function( tweet ) {

    //      remove html entities
    var text = tweet.title.replace(/&#{0,1}[a-z0-9]+;/ig, "") || tweet.tweet.text;

    var str = ''

    ,  ar = text.split( ' ' )
    ,  words = ar.splice( 0, 7 ).join( ' ' )

    ,  date = new Date( tweet.tweet.created_at )
    ,  year = date.getFullYear()
    ,  month = date.getMonth() + 1;

    if( month < 10 ) {
      month = '0' + month;
    }

    tweet.url = year + '/' + month + '/' + stringToSlug( words ) + '/';

    return tweet;
  }


}());

Twitter.prototype.finishUp = function( tweet ) {

  tweet = this.getSource( tweet );
  tweet.contentHTML = '<p>' + this.twitterify( tweet.tweet.text );
  tweet = this.addURLToTweet( tweet );

  this.tweets.push( tweet );
  this.checkIfDone();
}

Twitter.prototype.storeTweets = function() {

  var self = this
  ,  tweets = this.tweets
  ,  len = tweets.length
  ,  insert = 'INSERT INTO `post` (`type`,`timestamp`,`url`,`title`,`titleURL`,`content`,`contentHTML`,`source`) VALUES ';

  while( len-- ) {

    var tweet = tweets[ len ];

    insert += '("tweet","' + tweet.timestamp;
    insert += '","' + tweet.url + '","' + tweet.title; 
    insert += '","' + tweet.titleURL + '","' + tweet.tweet.text;
    insert += '","' + tweet.contentHTML + '","' + tweet.source;
    insert += '"),';

  }

  insert = insert.slice( 0,-1 ) + ';';

  this.con.query(insert, function( err, result ) {
    if (err) throw err;

    self.emit('done', result);

  });

}

var timeout = 1000 * 60 * 5; // 5 min

function repeat() {
  new Twitter( sql ).init().once('done', function(d) {
    if( d ) {
      l( 'added ' + d.affectedRows + ' rows at ' + new Date() );
    } else {
      l( 'nothing new at ' + new Date() );
    }
    setTimeout( repeat, timeout );
  });
}

repeat();
share|improve this question

Know someone who can answer? Share a link to this question via email, Google+, Twitter, or Facebook.

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Browse other questions tagged or ask your own question.